Skip to content

test(server): pin _sync_subs row count == live subscriptions across churn - #44

Merged
grrowl merged 1 commit into
mainfrom
test/sync-subs-liveness-pin
Aug 26, 2026
Merged

test(server): pin _sync_subs row count == live subscriptions across churn#44
grrowl merged 1 commit into
mainfrom
test/sync-subs-liveness-pin

Conversation

@grrowl

@grrowl grrowl commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Closes out the agent-canvas field report (2026-07-28, against the 0.6.0 dist) that _sync_subs grew 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 on await connect(); an unmount in that window deletes the handler but drops the unsub (this.ws null), and 0.6.0 then sent the parked sub unconditionally 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:

  • ghost race (the crux) — reproduces the exact 0.6.0 leak; red without the guard (received 14), green with it
  • eager mount/unmount/remount, 8 fresh subIds on one live socket — no accumulation
  • on-demand replaced where-shapes — refcounted release, never two live rows
  • reconnect resubscribe — new socket carries exactly the live set under the original subIds

🤖 Generated with Claude Code

…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>
@grrowl
grrowl merged commit eeb07cf into main Aug 26, 2026
1 check passed
@grrowl
grrowl deleted the test/sync-subs-liveness-pin branch August 26, 2026 06:00
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.

1 participant