Measured by the #10842 dev (PR #11018) while implementing invariant-6 face 1. Filed by the PM seat on its behalf: the dev hit a GitHub API rate limit, so its mandatory pre-file duplicate search could not run, and it refused to blind-file — the right call. The duplicate search has now been run clean by this seat (3 hits: #10842 itself, its parent #10420, and the closed unrelated #4867).
⚠️ Pre-existing. Not caused by PR #11018 — close(), matchesFilter and the since drop-filter line are untouched by it.
The mechanism
SysMetadataRepository.close() drains its watchers by broadcasting a synthetic drain event:
{ seq: -1, ref: { org: '', type: 'view', name: '_close' } }
watch()'s dispatch closure then runs that event through the same two filters every real event passes:
matchesFilter(evt, filter) — rejects it whenever the subscription named any filter.org (the drain event's org is the empty string), any filter.type other than 'view', or any filter.name.
- the
since drop-filter — evt.seq <= since is true for -1 against every real seq, so any numeric-since watcher drops it too.
watchers.clear() then removes the listener, so nothing can ever settle the promise. The consumer's pending next() hangs forever.
Measured on the branch
With a throwaway probe (deleted afterwards; porcelain clean):
| subscription |
drained by close()? |
watch({org:'system'}, a.seq) |
⛔ no — still unsettled 100ms after close() |
watch({org:'system'}) — no since at all |
⛔ no — still unsettled |
watch({}) |
✅ yes |
⭐ Note the middle row: the org-filter half bites on its own, without any since. This is not only a numeric-since problem.
Who can reach it
MetadataCache.start() (packages/metadata-core/src/cache.ts) subscribes with this.watchFilter. Whenever that filter is non-empty, this is the production shape that hangs on close(). Reachable today.
MetadataManager.startRepositoryWatch() uses repo.watch({}) and is drained.
PR #11018 does not create this, but it does make numeric-since watchers functional for the first time, widening who can reach it.
Why it was not fixed in place
The bounded-exemption condition fails: the correct shape is not pinned by existing evidence. Two defensible answers, and choosing between them is a contract question about what close() owes:
- the drain event bypasses the filters — keep the synthetic-event mechanism, exempt it from
matchesFilter and the since drop; or
close() resolves pending promises with done: true directly — stop modelling shutdown as an event at all.
The second is arguably cleaner (shutdown is not a metadata event and giving it a seq of -1 is what makes it collide with the since comparison in the first place), but that is a design opinion, not a measurement, and the invariant table says nothing today about what close() owes a pending iterator.
Provenance
Recorded in issuecomment-5379256754. Filed unassigned for triage to rank.
Measured by the #10842 dev (PR #11018) while implementing invariant-6 face 1. Filed by the PM seat on its behalf: the dev hit a GitHub API rate limit, so its mandatory pre-file duplicate search could not run, and it refused to blind-file — the right call. The duplicate search has now been run clean by this seat (3 hits: #10842 itself, its parent #10420, and the closed unrelated #4867).
close(),matchesFilterand thesincedrop-filter line are untouched by it.The mechanism
SysMetadataRepository.close()drains its watchers by broadcasting a synthetic drain event:watch()'sdispatchclosure then runs that event through the same two filters every real event passes:matchesFilter(evt, filter)— rejects it whenever the subscription named anyfilter.org(the drain event's org is the empty string), anyfilter.typeother than'view', or anyfilter.name.sincedrop-filter —evt.seq <= sinceis true for-1against every real seq, so any numeric-sincewatcher drops it too.watchers.clear()then removes the listener, so nothing can ever settle the promise. The consumer's pendingnext()hangs forever.Measured on the branch
With a throwaway probe (deleted afterwards; porcelain clean):
close()?watch({org:'system'}, a.seq)close()watch({org:'system'})— nosinceat allwatch({})⭐ Note the middle row: the org-filter half bites on its own, without any
since. This is not only a numeric-sinceproblem.Who can reach it
MetadataCache.start()(packages/metadata-core/src/cache.ts) subscribes withthis.watchFilter. Whenever that filter is non-empty, this is the production shape that hangs onclose(). Reachable today.MetadataManager.startRepositoryWatch()usesrepo.watch({})and is drained.PR #11018 does not create this, but it does make numeric-
sincewatchers functional for the first time, widening who can reach it.Why it was not fixed in place
The bounded-exemption condition fails: the correct shape is not pinned by existing evidence. Two defensible answers, and choosing between them is a contract question about what
close()owes:matchesFilterand thesincedrop; orclose()resolves pending promises withdone: truedirectly — stop modelling shutdown as an event at all.The second is arguably cleaner (shutdown is not a metadata event and giving it a
seqof-1is what makes it collide with thesincecomparison in the first place), but that is a design opinion, not a measurement, and the invariant table says nothing today about whatclose()owes a pending iterator.Provenance
Recorded in issuecomment-5379256754. Filed unassigned for triage to rank.