Tell a browser to refetch when a server's channel subscription comes back - #311
Merged
davidmckayv merged 3 commits intoAug 31, 2026
Merged
Conversation
zopeVaibhav
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
August 31, 2026 19:37
davidmckayv
approved these changes
Aug 31, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
Verified + CI verify green; template-appropriate correctness fix.
davidmckayv
approved these changes
Aug 31, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
CI verify green pre-rebase; CHANGELOG-only rebase; template-appropriate.
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 #310.
What this changes
A conversation deleted while a server's Postgres subscription was down used to sit on the roster
until somebody reloaded the page. It now clears itself.
Announcements between servers travel as
NOTIFY, which reaches whoever is subscribed at the timeand is never replayed.
server/src/channels/events.tssubscribed with a message handler and nothingelse, so a dropped subscription silently swallowed every channel deletion, pin and message announced
while it was away.
The client could not compensate. Its recovery is
socket.onopeninapp/src/lib/channels/use-channel-events.ts, and its socket never dropped — the connection thatdropped was the server's, on a wire the browser cannot see. Nothing else covers it either:
app/src/query-client.ts:7setsrefetchOnWindowFocus: false, and the roster query overridesneither that nor
refetchInterval.server/src/computer/policy-listener.ts:59already answers this for the action policy, withonlistenas the third argument tolisten. This carries the same hook to the channel listener.It cannot re-read the way the policy does — the policy is one row, and this is a stream of
per-member deltas Postgres does not keep — so instead the server tells its browsers to refetch,
which is the recovery they already run on their own reconnect.
Two decisions worth reviewing:
earlier subscription behind it for anything to have been missed between, so sending one there asks
every connection to refetch on a boot where nothing was lost. Without the flag the message means
"possibly a gap", which is not something a client can act on differently — and it breaks seven
existing tests in this file that register a connection before starting the listener.
narrow it to: the events that were lost named their own recipients, and those events are gone.
Where it runs
let subscribedboolean inside
startChannelActivityListener, which is per-process by design: it recordswhether this process has subscribed before, and a second process has its own answer to that
and needs it.
sockets, and each answers for its own. When one replica's subscription drops, it resyncs the
browsers it is holding; the others were not affected and say nothing. That is the correct
outcome — a browser is connected to exactly one replica, and only that replica knows whether
its own delivery had a gap.
arriving together cost one refetch each on a query that is already the source of truth.
hub.resyncAll()walksconnections, which is this process's own registry, exactly ashub.deliveralready does. It is deliberately not announced through Postgres: a replica whosesubscription never dropped has nothing to tell its browsers about.
argument.
Boundary and audit
channel roster's fan-out.
cannot be refused, and a connection that throws on write is one that is closing, handled the
way
deliveralready handles it.the browser sends is read.
Changelog
CHANGELOG.md, underUnreleased: "A conversation deleted while a server was reconnecting nolonger lingers on the screen."
Proof
Four new tests in
server/tests/channel-events.integration.test.ts. The integration one breaks thesubscription for real rather than simulating it: it takes the backend pid from
pg_stat_activityand terminates it in a loop until the query returns nothing, so the announcement is published into a
gap that is known to exist rather than assumed to. It then proves the subscription is healthy again
with a later event, so the missing one cannot be blamed on a dead listener.
Against the listener as it stands on
main, with onlyserver/src/channels/events.tsreverted:With the change:
Run eight times, no flakes.
bun run lint,bun run format:checkandbun run typecheckare clean.Whole suite: 2058 pass against 2054 on
main, the difference being these four. Seven failures arepresent identically on
mainand on this branch —routine-sweep.integration.test.tsasserting onroutines it does not own, which fails against a local database holding seeded rows. Untouched by
this change.
No screenshot. The surface change is the absence of a stale row, which a still image cannot show,
and the assertion that replaces it is the last line of the integration test: the browser is sent
{"resync":true}and never receives the announcement made during the gap.