Skip to content

Remove the client's replay-mode / UpToDateTracker mechanism #4783

Description

@alco

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 feat: Do not cache empty live responses #2593, which predates Fix multiple renders from cached up-to-date messages on page refresh #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; ShapeStream replay suppression can discard data messages #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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions