MeridiansMeridians

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()master for localhost / Electron / canonical web; client for 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, isRecordSynced matching 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-synced stamps 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 through planStaleSweep, and aux stores. The sweep pulls what's not held, pulls what canon's updatedAt moved past, skips what's held-but-unprovable (the storm guard), and re-runs when hydration warms the ledger. On the client plane, reconcileReplica runs the same plan against every entries broadcast, 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.