#4783·electric

Remove the client's replay-mode / UpToDateTracker mechanism

Author: alcoCreated Aug 27, 2026Updated Aug 28, 2026

Summary

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.

Why it can't fire

  • The server sets electric-cursor only on live=true responses (Electric.Shapes.Api.Response.put_cursor_headers/2), and has done so since #2593, which predates #3358.
  • 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.
  • ReplayingState and the suppressUpToDate transition add a state and a code path that reviewers and tests have to reason about; #4722 was a real bug in that path, found by a mock.
  • 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.

References

  • #3358 — original mechanism
  • #4722 / #4782 — data-drop bug in the suppression path and its fix, including the reachability analysis in the PR description and the C9 annotation in packages/typescript-client/SPEC.md