test(server): pin _sync_subs row count == live subscriptions across churn - #44
Merged
Conversation
…hurn `_sync_subs` (ADR-0019) is socket ephemera: it must hold exactly the live subscriptions of currently-open sockets. A downstream field report (agent-canvas, against 0.6.0) saw it grow to 44 rows across one interactive session and asked whether a facet still leaks on a LIVE socket — one that never closes, so neither close-cleanup nor the orphan sweep can catch it. These pins drive the REAL client paths (WebSocketTransport + doCollectionOptions sync lifecycle) against a REAL Durable Object and assert, via `_sync_subs`, that the durable row count keyed to live sockets always equals the number of genuinely live subscriptions, across the churn the report named: - ghost-race (the crux): mount/unmount many fresh-subId collections while the INITIAL connect is still pending, then open the socket. This reproduces the exact 0.6.0 leak — tagged 0.6.0's `subscribe()` sent its `sub` frame unconditionally after `await connect()`, so a sub unsubscribed during the pending-connect window (its `unsub` dropped because `this.ws` was null) still persisted a durable row with no live handler. Removing the ghost-sub guard (`if (!this.handlers.has(subId)) return`, transport.ts, added in 227c2aa) turns this test red with 1 + BURST rows on one never-closing socket. - eager mount/unmount/remount with genuinely distinct subIds — the table returns to zero between mounts and holds exactly one row while one collection is live. - on-demand replaced where-shapes — each replaced subset's refcounted row is released; only currently-loaded subsets remain. - reconnect resubscribe — the new socket carries exactly the live set under the original subIds (resubscribe reuses subIds; no per-reconnect growth). Verdict: on current main the live-socket accumulation is FIXED — by the ghost-sub guard (227c2aa) for the pending-connect race, on top of ADR-0019's unsub/close/sweep cleanup seams. The field report raced the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes out the agent-canvas field report (2026-07-28, against the 0.6.0 dist) that
_sync_subsgrew to 44 rows across 2 socket tags over one interactive session.Verdict: fixed on main — no live bug. The leak was real on tagged 0.6.0:
subscribe()parks onawait connect(); an unmount in that window deletes the handler but drops theunsub(this.wsnull), and 0.6.0 then sent the parkedsubunconditionally on open — a durable row with no live handler, repeated per mount during one pending connect. Fixed by the ghost-sub guard (if (!this.handlers.has(subId)) return) in 227c2aa (SSR-lift hardening), on top of ADR-0019's unsub/close/sweep seams.Four pins, test-only, driving real client paths against a real DO:
received 14), green with it🤖 Generated with Claude Code