Keep comments durable across collab session token expiry - #76
Open
rclod wants to merge 1 commit into
Open
Conversation
A collab session token expired while the provider was connected. The
server closed the socket 4401 "Invalid or expired collab session token",
and comments added afterwards stayed visible in the tab while SQLite kept
revision=1, marks='{}' and zero document_y_updates.
Four independent defects had to line up, and each one alone loses the
comment:
1. src/editor/index.ts renewal loop returned early whenever the
connection was `connected && isSynced` — exactly the state a session
is in for the whole minute before it expires — so it never renewed
ahead of expiry and only reacted after being closed.
2. Nothing in src/ ever assigned collabClient.terminalCloseReason. The
immediate-refresh hook that read it was therefore unreachable, so an
auth failure had no fast path back.
3. flushShareMarks gated the durable REST write on collabEnabled and
collabCanEdit. Those are capability flags and stay true on a provider
that has been closed out, so the comment went to the local Y.Doc and
nowhere else, with no unsaved state surfaced.
4. shouldDeferExpiringCollabRefresh deferred renewal whenever there was
pending local state, without considering connection status. Adding a
comment set unsyncedChanges=1, which then blocked the very refresh
that would have saved it. The stalled-collab recovery path that could
still fire passed preserveLocalState=false, resetting the Y.Doc and
discarding the comment outright.
The decisions move into src/bridge/collab-session-renewal.ts as pure
functions, because the editor is browser-only and cannot be instantiated
in this harness — the logic that lost data was untestable where it lived.
Renewal now runs on a healthy connection, defers only while the
connection is usable, and never defers past a hard deadline. Auth
failures take one deterministic refresh that preserves local marks. REST
persistence is selected on provider liveness rather than capability, and
only while no provider is connected, so it never writes alongside a live
Yjs writer.
Tests: collab-session-renewal-expiry.test.ts drives a fake clock across
expiry and runs each scenario against a reference implementation of the
replaced inline logic, asserting the old rule gets it wrong — a test both
implementations pass would not have caught this.
collab-comment-durability-across-expiry.test.ts proves a comment written
while the provider is dead persists once and converges on replay.
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.
Fixes #72.
A collab session token expires while the provider is connected. The server closes the socket 4401
Invalid or expired collab session token, and comments added afterwards stay visible in the tab while storage keepsrevision=1,marks='{}'and zerodocument_y_updates.#72 has the full trace. The short version is that four things have to line up, and each one alone loses the comment:
startCollabRefreshLoopreturned early onconnected && isSynced— the state a healthy session is in for the whole minute before expiry — so renewal never ran ahead of the close.collabClient.terminalCloseReason, which nothing insrc/ever assigns. Unreachable.flushShareMarkspicked the durable REST path fromcollabEnabled/collabCanEdit. Those are capability flags and stay true on a closed provider, so the mark went to the localY.Docand nowhere else.shouldDeferExpiringCollabRefreshdeferred on pending local state without checking connection status, so the unsaved comment blocked its own rescue; the fallback recovery passedpreserveLocalState: falseand reset theY.Doc, discarding it.Approach
The decisions move into
src/bridge/collab-session-renewal.tsas pure functions. The editor is browser-only and cannot be instantiated in this harness, so the logic that lost data was untestable where it lived — this follows the same seam assrc/bridge/marks-preservation.ts.permission-deniedthroughonAuthenticate— so the refresh response decides. A genuine revocation still returns 401/403/404/410 and falls through to the existing read-only teardown.terminalCloseReasonis now actually assigned for a real denial, which also revives the two other consumers that were dead for the same reason.Tests
src/tests/collab-session-renewal-expiry.test.tsdrives a deterministic fake clock across expiry. Each scenario also runs against a reference implementation of the inline logic it replaces, asserting the old rule returns the wrong answer — a test both implementations pass would not have caught this defect. Covers proactive renewal, the hard deadline overriding deferral, the unsaved-work deadlock, backoff, preserve-on-reconnect, the REST liveness decision, auth classification, and durable acknowledgement.src/tests/collab-comment-durability-across-expiry.test.tsruns against a temp database with a live collab room loaded for the slug, and proves a comment written while the provider is dead persists once, converges on replay rather than duplicating, and still admits a genuinely new comment.Both new files pass, along with
proof-sdk-agent-bridge-client,server-routes-and-share(74/74), andcollab-browser-persist-after-external-apply-regression.Notes
tsc -p tsconfig.jsonreports 519 errors onmainbefore this change and the same 519 after, so I compared normalized before/after rather than treating it as a gate.npm testonly runs two of the 133 files insrc/tests/, so the new tests are not picked up automatically. I left the script alone rather than change what CI runs in this PR — happy to wire them in if you want.