You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The TypeScript client's replay-mode machinery (UpToDateTracker, ReplayingState, the suppressUpToDate transition) has never engaged against a conforming Electric server. Its only observable effects are a localStorage write per shape on every up-to-date, an extra state in the state machine, and a class of bugs (#4722) that can only be reached through a fetch mock. It should be removed.
Background
#3358 added the mechanism to fix multiple renders on page refresh: cached responses replaying rapidly, each ending in an up-to-date. The design records the last seen electric-cursor per shape in localStorage; on a fresh stream within 60 s it enters ReplayingState and suppresses the first up-to-date whose response cursor matches the recorded one.
The client sends live=true only from LiveState (shape-stream-state.ts, LiveState.applyUrlParams).
ReplayingState is entered only from a fetching state (Initial/Syncing) — i.e. around a non-live request — and exits to LiveState on the first up-to-date it sees, whether or not it suppresses.
Every route into a fetching state zeroes liveCacheBuster (createInitialState, reset, markMustRefetch), and a recorded "" cursor is rejected by the if (lastSeenCursor) guard before replay mode is entered.
So the first up-to-date a replaying stream handles never carries a cursor, currentCursor is "", and replayCursor === currentCursor is never true. The tests in up-to-date-tracker.test.ts (both the #3358 originals and the C9 test from #4782) only exercise the branch by putting electric-cursor on non-live responses, which the server never does.
Cost of keeping it
UpToDateTracker is a module-level singleton that reads and writes localStorage (throttled) for every shape on every up-to-date.
When suppression does fire (mock or misbehaving proxy), the stream's state-derived view (stream.isUpToDate, lastSyncedAt()) says up-to-date while subscribers and Shape.status haven't seen an up-to-date, and the reconciling fresh up-to-date only arrives with the next live response — for an idle shape, the long-poll timeout.
Recommendation
Remove it: drop UpToDateTracker, ReplayingState, the suppressUpToDate transition, the C9 invariant, and the associated tests. The state machine loses one state and the client stops touching localStorage on every up-to-date. The original multiple-render symptom from #3358 does not appear reproducible on current main — browser-cached live responses would only replay if the client re-issued identical offset/handle/cursor URLs after a refresh, which it doesn't from offset=-1 — and if it resurfaces it should be addressed with an input the server actually emits on the responses in question.
Summary
The TypeScript client's replay-mode machinery (
UpToDateTracker,ReplayingState, thesuppressUpToDatetransition) has never engaged against a conforming Electric server. Its only observable effects are alocalStoragewrite per shape on every up-to-date, an extra state in the state machine, and a class of bugs (#4722) that can only be reached through a fetch mock. It should be removed.Background
#3358 added the mechanism to fix multiple renders on page refresh: cached responses replaying rapidly, each ending in an
up-to-date. The design records the last seenelectric-cursorper shape inlocalStorage; on a fresh stream within 60 s it entersReplayingStateand suppresses the firstup-to-datewhose response cursor matches the recorded one.Why it can't fire
electric-cursoronly onlive=trueresponses (Electric.Shapes.Api.Response.put_cursor_headers/2), and has done so since feat: Do not cache empty live responses #2593, which predates Fix multiple renders from cached up-to-date messages on page refresh #3358.live=trueonly fromLiveState(shape-stream-state.ts,LiveState.applyUrlParams).ReplayingStateis entered only from a fetching state (Initial/Syncing) — i.e. around a non-live request — and exits toLiveStateon the first up-to-date it sees, whether or not it suppresses.liveCacheBuster(createInitialState,reset,markMustRefetch), and a recorded""cursor is rejected by theif (lastSeenCursor)guard before replay mode is entered.So the first up-to-date a replaying stream handles never carries a cursor,
currentCursoris"", andreplayCursor === currentCursoris never true. The tests inup-to-date-tracker.test.ts(both the #3358 originals and the C9 test from #4782) only exercise the branch by puttingelectric-cursoron non-live responses, which the server never does.Cost of keeping it
UpToDateTrackeris a module-level singleton that reads and writeslocalStorage(throttled) for every shape on every up-to-date.ReplayingStateand thesuppressUpToDatetransition add a state and a code path that reviewers and tests have to reason about; ShapeStream replay suppression can discard data messages #4722 was a real bug in that path, found by a mock.stream.isUpToDate,lastSyncedAt()) says up-to-date while subscribers andShape.statushaven't seen an up-to-date, and the reconciling fresh up-to-date only arrives with the next live response — for an idle shape, the long-poll timeout.Recommendation
Remove it: drop
UpToDateTracker,ReplayingState, thesuppressUpToDatetransition, the C9 invariant, and the associated tests. The state machine loses one state and the client stops touchinglocalStorageon every up-to-date. The original multiple-render symptom from #3358 does not appear reproducible on currentmain— browser-cached live responses would only replay if the client re-issued identicaloffset/handle/cursorURLs after a refresh, which it doesn't fromoffset=-1— and if it resurfaces it should be addressed with an input the server actually emits on the responses in question.References
packages/typescript-client/SPEC.md