Skip to content

FileSystemRepository.close() never reaches its event broker — a parked watch() iterator stays parked after shutdown #11127

Description

@os-zhuang

Found while implementing #11021 (SysMetadataRepository.close() cannot drain a filtered or numeric-since watcher). Same defect class, different implementation and different package, so it is filed rather than fixed in that PR.

The mechanism

FileSystemRepository.close() (packages/metadata-fs/src/repository.ts:198) does exactly two things:

asyncclose(): Promise<void>{this.stopResync();if(this.watcher){awaitthis.watcher.close();// chokidarthis.watcher=null;}this.started=false;}

It never touches this.broker (packages/metadata-fs/src/repository.ts:117, createBroker in packages/metadata-fs/src/sync.ts:49). The broker has no teardown of its own — subscribe/unsubscribe add to and delete from a plain Set, and nothing else empties it.

Each watch() iterator (packages/metadata-fs/src/watch-iterable.ts) parks its pending next() on a waiter callback that only two things can settle: a broker push, or the iterator's own local close() — which runs from iterator.return() / iterator.throw() and from nowhere else. So after repo.close():

  • the chokidar source is gone, so no further push can arrive;
  • the subscriber is still registered, and nothing calls its terminator.

A consumer holding a for await over fsRepo.watch(...) at shutdown therefore never sees the loop end. Unlike the #11021 shape this is not filter-dependent — there is no drain attempt at all, so it applies to every subscription shape including watch({}).

Confidence

Code read, not a runtime probe.#11021's scope is packages/metadata-protocol, and building/booting metadata-fs to confirm was out of that card's surface. The read is unambiguous (there is no path from close() to the broker or to any iterator terminator), but the matrix that #11021 carries for its own repository has not been produced here. Whoever picks this up should measure it first.

Related contract statement

#11021 adds invariant 8 to the MetadataRepository invariant table in packages/metadata-core/src/repository.ts — "shutdown terminates; it does not emit" — stating what a repository-level close() owes a pending iterator. That row names this gap as the one measured non-conformance among today's implementations. Fixing it is the same one-line shape the #11021 repair uses: hold each subscription's terminator alongside its event sink, and run every terminator from close().

InMemoryRepository is unaffected: it exposes no repository-level shutdown at all, so its iterators end only through return().


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions