Cached log: a remote log with a local replica
The board reads from a local replica and writes to the host. CachedLog is
the SyncLog that makes that one object: a remote log (the host, through
RemoteLog) and a cache (the IndexedDB log
in a browser, a MemoryLog anywhere else).
- A pull is served from the cache first. When the cache runs out, the delta after its tail is pulled from the host, appended to the cache with the host's sequence numbers, and returned. So a cold start replays what the cache holds without touching the network, then catches up; a warm sync is one small request.
- A push goes to the host — atomically, idempotent by id — and nothing is written to the cache from the push result. The store then replays, and the replay pulls the new events back through the cache like anything else. The log is the truth; the cache is a copy of it. That is the same invariant the local logs keep, now with a network in between.
- The cache is per host per browser, and keyed by the credential as
well as the host —
replicaDbName(host, principal)(s:<uid>for a session,t:<token fingerprint>for a token), so a different credential opens a different database by construction and can never replay what another credential was allowed to read; a page reload, a second tab and a fresh board included. It never holds anything the host did not sequence. Deleting it costs one cold load.
A pull while the host is unreachable answers from the replica alone — a
degraded, read-only view, announced through onRemoteError (the board
shows offline over real cards; the CLI answers read verbs) — unless the
replica is empty, in which case there is nothing to show and the pull
fails. A refused credential (401/403) is not an outage and is never
absorbed: the pull fails so the caller can forget the token or ask for a
sign-in, instead of serving a replica the reader is no longer allowed to
see. And while the replica is stale a push is refused
(StaleReplicaError): a writer must not decide what is new against a view
the host has moved past — an importer would resend ids the host holds.
Sync, then retry. There is no offline write queue: a push while the host is
unreachable fails, and the card stays as it was. That is the design's "not designed
here" (DESIGN.md §7.2) and it stays that way until it hurts.
const log = new CachedLog({
remote: new RemoteLog({ client: new ServiceClient({ host, token }) }),
cache: new IndexedDBLog(replicaDbName(new URL(host).host, principal)),
})
const store = new VirtaStore({ log, identity })
await store.load() // the cache, then the delta
setInterval(() => store.sync(), 10_000) // other writers' events arrive here