presence client: report request failures and rejoin after eviction (CL-7202) - #482
Merged
TheGreatAxios merged 4 commits intoAug 30, 2026
Conversation
Reproduces CL-7202: connectPresence blind-posts every join/heartbeat/ leave/update request, never checks response.ok, exposes no way for a caller to hear about a failure, and never rejoins after a failed join or a heartbeat the server has already forgotten about (404 not_joined) -- all while its EventSource stays open regardless, so the UI keeps reading as live.
…L-7202) connectPresence blind-posted every join/heartbeat/leave/update request and never checked response.ok, so a failed join or a heartbeat 404'd by the server (not_joined) went unnoticed while the SSE stream stayed open regardless -- the UI kept reading as live with no signal that membership had been lost. PresenceHandle now exposes onError, firing for every request that never reached the server or came back non-2xx. Heartbeats are gated behind a joined flag: a heartbeat is only sent once join has actually succeeded, and a 404 response flips that flag back off and triggers an immediate rejoin -- the same self-healing a publishCursor/ publishTyping call gets if it fires before the initial join settles.
A caller publishing cursor/typing updates while unjoined called doJoin on every publish; with no delay between attempts, a client stuck unable to join (or a room that keeps evicting it) would re-POST /join as fast as each failed attempt resolved. Joins now back off exponentially after a failure and reset the moment one succeeds.
A local Yjs edit whose /update POST failed was only ever surfaced through onError; the edit itself was gone for good, so a dropped update meant silent, permanent divergence from the server's doc for a collaborative document. Failed updates now queue and are redelivered in order on the next successful join or heartbeat -- safe because Yjs updates are idempotent against a doc that has already applied them.
Contributor
Author
ReviewVerified the four acceptance criteria against the diff:
Fixed
Verified
Left alone
Pushed as two follow-up commits on this branch (stacked correctly on |
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.
Stacked on #477 (CL-7203) — base this PR onto that one, not
main.Summary
connectPresenceblind-posted every join/heartbeat/leave/update request (.catch(() => undefined)), never checkedresponse.ok, exposed no way for a caller to hear about a failure, and never rejoined after a failed join or a heartbeat the server had already forgotten about (404not_joined) — all while itsEventSourcestayed open regardless, so the UI kept reading as live with no signal that membership had been lost.Changes to
packages/presence/src/client.ts:PresenceFetch's response shape now carriesstatus.PresenceHandlegainsonError(listener), firing for every join/heartbeat/leave/update request that never reached the server (rejectedfetch, nostatus) or came back non-2xx (statusset).joinedflag: a heartbeat is only ever sent once join has actually succeeded.publishCursor/publishTypingfired before the initial join settles now rejoin instead of heartbeating a membership the server doesn't have.joinedback tofalseand immediately triggers a rejoin — self-healing an eviction (including the CL-7203 self-eviction race) without the consuming UI having to notice and reconnect by hand.doJoinguards against overlapping join requests (joinInFlight).Test plan
packages/presence/src/client.test.ts: request failures (network + non-ok) reported viaonErrorfor join/heartbeat/update; a heartbeat 404 triggers an automatic rejoin; publishing before the initial join settles rejoins instead of heartbeating;onErrorunsubscribe stops delivery; existing tests updated for the newstatusfield / async join settlingWORKBENCH_CHECK_SINCE=origin/main bun run typecheck— clean (6 affected packages)bun run lint— 0 errorsbun run check:structural— all checks ok,@corbits/presence/clientstill browser-safepackages/presencetest suite: 77 pass, 0 failScope note
Diff confined to
packages/presence/src/client.tsper the ticket —packages/presence/src/room-registry.ts(CL-7204/CL-7205) and the SSE teardown leak inroutes.ts(CL-7212, addressed by a different lane) are untouched.