electric-sql / electric-sql/electric

Remove the client's replay-mode / UpToDateTracker mechanism

Open
#4,783 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
10.4k
Forks
375
Avg merge
3d 1h
Merged PRs (30d)
18

Description

## 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`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.