Skip to content

Route invalidation through PSR-14 events - #643

Draft
Firehed wants to merge 4 commits into
mainfrom
642/event-driven-invalidation
Draft

Firehed wants to merge 4 commits into
mainfrom
642/event-driven-invalidation

Conversation

@Firehed

@Firehed Firehed commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #642.

The invalidator fan-out (InvalidatableInterface, CompositeInvalidatable) is replaced with a PSR-14 event bus. Two file-shaped events are published by the LSP handlers, and one internal event is published by the map reader when it detects a vendor/composer/ change:

  • WatchedFileChangedEvent — from workspace/didChangeWatchedFiles
  • OpenDocumentClosedEvent — from textDocument/didClose
  • AutoloadMapRegeneratedEvent (in Knowledge; carries the fresh map) — from the map reader

Subscribers register against each concrete event type. Backends stop polling ComposerAutoloadMapReader::current() on every invalidation to check for map identity changes; instead they receive the new map on the event and rebuild their derived indexes on the next query.

Notes

  • PSR-14 dispatcher and provider are home-grown, ~40 LoC together in src/Events/. Sufficient for one composition root; nothing depends on Symfony or Tukio.
  • Subscription is exact-string match on EventInterface::$type (each event sets it to self::class). No is_a, get_class, $event::class, or $event instanceof $var on the dispatch path — those are all disallowed as kind inspection (RFC 1 §4.5). A subscriber that treats two events the same way registers the same callable against each concrete type. FileEventInterface still exists as a listener parameter type for narrowing; it is not a subscription target.
  • EventDispatcher is typed on the concrete ListenerProvider, not Psr\EventDispatcher\ListenerProviderInterface. The PSR interface's getListenersForEvent() returns iterable with no value type, so an interface-typed field forces every dispatch site to widen through mixed. This is one call site; the concrete typing avoids that.
  • AutoloadMapRegeneratedEvent lives in Knowledge/, not Events/, because it carries ComposerAutoloadMap (confined to backends by SymbolDiscoveryAuthorityExtension). Keeps the exempt-namespaces list unchanged.
  • Nesting is synchronous: the map reader dispatches AutoloadMapRegeneratedEvent from inside its WatchedFileChangedEvent handler; downstream listeners run in the same tick before control returns.
  • Container-compatibility: every new class is a plain-constructor service with typed deps. The step-12 migration moves the assembly body from KnowledgeStack::assemble() into a container factory closure (or a bootstrap seam) with no shape changes.

Enforcement edits, made at Eric's direction

deptrac.yaml: new Events layer covering src/Events/; edges Handler -> Events and Knowledge -> Events; Handler -> Cache edge removed (no Handler file references Cache after this change).

Test plan

  • composer test (phpstan, deptrac, layer-coverage, phpunit, phpcs) green
  • Unit tests updated for the new API: CachingSymbolSourceTest, ComposerAutoloadMapReaderTest, ComposerMapBackendTest, AutoloadFilesBackendTest, DidChangeWatchedFilesHandlerTest, TextDocumentSyncHandlerTest, plus new ListenerProviderTest and EventDispatcherTest
  • Integration tests kept and updated: ExternalFileChangeInvalidationTest, ComposerRegenerationInvalidationTest

🤖 Generated with Claude Code

Firehed and others added 4 commits September 27, 2026 08:32
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two file-shaped events (WatchedFileChangedEvent, OpenDocumentClosedEvent)
and one internal AutoloadMapRegeneratedEvent replace the invalidator
fan-out. The map reader listens to WatchedFileChangedEvent, and when the
path is under vendor/composer, publishes AutoloadMapRegeneratedEvent
carrying the fresh map. Downstream backends subscribe to the concrete
events they care about rather than polling the reader for map identity
changes.

Subscription is exact-string match against the event's own $type, so
the provider does no runtime kind inspection (RFC 1 §4.5). Subscribers
that treat two file events the same way register the same callable
against each concrete type; the tax is two lines instead of one, and
it keeps the disallowed introspection functions off the dispatch path.

InvalidatableInterface and CompositeInvalidatable are gone; handlers
publish events through EventDispatcherInterface instead of calling an
invalidator directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Made at Eric's direction: adding a layer and adding ruleset edges are
both human-only edits per docs/architecture/enforcement-edits.md.

- New Events layer covers src/Events/.
- Handler -> Events (publish WatchedFileChangedEvent, OpenDocumentClosedEvent).
- Knowledge -> Events (subscribe; the map reader also publishes
  AutoloadMapRegeneratedEvent, which lives in Knowledge because it
  carries ComposerAutoloadMap).
- Handler -> Cache is removed; no Handler file references Cache after
  InvalidatableInterface's deletion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.09804% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.55%. Comparing base (4c534ba) to head (82341dd).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/Knowledge/AutoloadFilesBackend.php 70.00% 3 Missing ⚠️
src/Events/OpenDocumentClosedEvent.php 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #643      +/-   ##
============================================
- Coverage     99.65%   99.55%   -0.10%     
- Complexity     1913     1922       +9     
============================================
  Files           134      138       +4     
  Lines          4940     4968      +28     
============================================
+ Hits           4923     4946      +23     
- Misses           17       22       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Revisit overall cache invalidation strategy

1 participant