Sync protocol — master ⇄ clients [Flow]
From the Meridians Wiki · Public · Maintained · joint
One daemon owns the record; N browsers connect to it. The sync layer is the two-tier replication protocol underneath cache-coherence: how a client's optimistic edit reaches the master, how the master's echo suppresses a re-write, and how a reconnecting client catches up.
flowchart TB
subgraph Master["MASTER — owns the record (localhost / Electron / canonical web)"]
direction TB
Mut["applyLocalMutation<br/>gate → reduce → persist → attribute → echo"]
Broker["**broker (Hub)** — one master sink, many client sinks<br/>+ lightweight registries / bounded asset cache"]
Mut --> Broker
end
subgraph Client["CLIENT — renders synced state (tunnel origin)"]
direction TB
Opt["optimistic apply (local)"]
IDB[("IndexedDB — rebuildable projection")]
end
Client -->|"① push action (relayed)"| Broker
Broker -->|"② reduce + persist + echo"| Mut
Broker -->|"③ SSE: domain + version token"| Client
Client -->|"markRecordSynced(cls, id, version)"| Echo{"isRecordSynced?<br/>token matches?"}
Echo -->|"yes — echo of my own write"| IDBonly(["write IndexedDB only<br/>NO daemon re-post"])
Echo -->|"no — someone else's write"| Adopt(["adopt to store + IndexedDB"])
Entries["every `entries` broadcast"] -->|"reconcileReplica:<br/>entry.updatedAt > ledger stamp?"| Stale{"provably<br/>stale?"}
Stale -->|"yes — request (once per version)"| Broker
Stale -->|"unprovable (ledger cold)"| Defer(["defer → replay when<br/>hydration stamps warm"])
Recon(["reconnect / boot / foreground resume"]) -->|"active + version-gated doc + aux catch-up"| Broker
Invariants
- Role is by origin, not config.
getSyncRole()→masterfor localhost / Electron / canonical web;clientfor a tunnel origin. A host reaching its own tunnel URL lands as a client, never a competing master — there is exactly one writer per world. - Echo suppression is token-based. After a commit the master echoes a version token; the client
markRecordSynced(cls, id, version). When the SSE echo arrives,isRecordSyncedmatching the token means "this is my own write" → persist to IndexedDB only, don't re-post to the daemon. A non-matching token is someone else's write → adopt. This is what stops an infinite echo loop. - The broker is the fan-out. One in-process Hub holds the single master sink and all client sinks, retaining only lightweight registries and a byte-bounded asset working set. Full documents stream live; reconnect catch-up requests them fresh from the canonical master. Clients never hold credentials or keys.
- Catch-up is version-PROOF, and continuous. The
record-syncedstamps double as a version ledger of what each side provably holds. On the master plane, reconnect, visible/focus resume, persisted bfcache restore, and network return share one coalesced pass: active domain, every doc class throughplanStaleSweep, and aux stores. The sweep pulls what's not held, pulls what canon'supdatedAtmoved past, skips what's held-but-unprovable (the storm guard), and re-runs when hydration warms the ledger. On the client plane,reconcileReplicaruns the same plan against everyentriesbroadcast, loop-guarded per(id, version). In-sync replicas cost zero requests at every trigger. Boot reconciliation compares every doc: cache ahead → adopt to fs, record ahead → hydrate to cache (cache-coherence).
Where it lives: src/lib/server/sync/{role,broker,protocol}.ts, src/lib/server/record/catch-up.ts, src/lib/client/sync/{role,record-synced}.ts, src/lib/client/store/{hydration-signal.ts,hooks/use-record-stream.ts}, src/lib/client/cache/persistence.ts. The daemon/host topology: platform-model; the one-writer terminus: engine-pipeline.