Remote / Server-derived state

Mutations and live data

A mutation flows through the ordinary Foldkit path — UI Message → update → Data.mutate → Command — and returns to the Model as a RemoteMessage:

mermaid
sequenceDiagram
  participant UI
  participant update
  participant Data
  participant Server as server
  UI->>update: ClickedRename
  update->>Data: Data.mutate
  Data->>Server: Command (RPC)
  Server-->>update: MutationSucceeded { requestId, entities, now }
  update->>Data: Data.reduce

Data.mutate applies MutationStarted to the Model (the request id comes from the Model's own sequence, so update stays pure) and returns the Command whose Message settles it. That Message carries the normalized entity patches and now, the clock reading that dates what they write; the typed Output is what Remote.mutateInto returns. Settling is idempotent per requestId, so a transport retry cannot apply the same change twice. Remote.mutateInto is the one-step imperative form for SSR and tests.

Optimistic changes belong to the mutation: MutationStarted carries its entity patches and connection changes (ConnectionChange.prepend/append/remove), and success or failure releases them together by request id. Patches are ordered layers over the base store, not inverse patches: the visible store is recomputed as base + layers, so overlapping layers rebase for free. Connection changes are overlays outside the server-known region; a result's confirmed connections take the place of the request's own, so a temporary edge becomes the real one in place.

On the server, RemoteServer.liveHub turns "these fields of this entity changed" into the right patch for each subscriber: it re-reads the changed fields a subscriber selects through the entity's own source, under that subscriber's principal, so authorization and computed fields take the normal path and a subscriber selecting none of the fields costs nothing.

Live data is an Effect streaming RPC. Each stream has a monotonic cursor: duplicates are ignored, and an event ahead of the cursor is a gap — it is not applied, and the stream is recorded so the host can resync rather than silently miss facts. Entity events update the store; connection events change membership and ordering. A deleted entity is a tombstone that every connection listing it skips, a live removal hides a known edge until a fresh page brings it back, and a merged page prunes the settled overlays it supersedes.