Remote / foldkit-remote

Live data

Data.live marks a Projection so active Data.subscriptions follow server changes for it.

The Remote Model owns each stream's cursor. Live events carry monotonically ordered cursors:

  • an old/repeated cursor is a duplicate and is ignored;
  • the next cursor is applied;
  • a cursor ahead of the expected value is a gap and is not applied.

Entity patches update normalized fields, deletes create tombstones, and connection insert/remove/invalidate events reconcile the same connection model used by query pages and optimistic overlays. A stream failure becomes a Remote failure Message rather than mutating anything out of band.

When a stream breaks

A stream breaks when its transport fails or when an event arrives ahead of its cursor (a gap). Either way, what the server said meanwhile is lost: no server replays it, since after only numbers the events that follow. So recovery is a refetch, and the live entry does it on its own:

break -> gap marked, restarts + 1 -> backoff
      -> RefreshStarted (what the stream covers) + GapCleared -> resubscribe from the cursor
                    \-> read entry refetches
  • The backoff is Sync's transport policy: 50 ms after the first break, doubling for each break in a row, at most 5 s, with ±20% jitter. retryBase and maxRetryDelay in the Data.subscriptions options change it. An event applied after a resubscribe starts it over.
  • A gap stays until the restart. An in-order event that a broken stream still delivers is applied, but does not clear it. The event that arrived ahead was dropped, and only the refetch brings back what it said.
  • What a stream covers is its requirements. A list the server watches for it (a ConnectionInvalidate it sends) is the source's to re-send when the stream opens; the cloudflare example's poll invalidates its list on every open for this reason.

Data.liveStatus(model, active) says how what one active entry reads live is doing, for a view that shows it:

ts
Match.value(Data.liveStatus(model, Watched)).pipe(
  Match.tagsExhaustive({
    Idle: () => 'not live',
    Live: () => 'live',
    Reconnecting: ({ attempt }) => `reconnecting (attempt ${attempt})`,
  }),
)

Reconnecting lasts from the break until the stream has resubscribed, and carries the error, which is none for a gap. RemoteModel.streams holds each stream's restarts, its breaks in a row and its last error. forget clears it with every other server-derived fact.