# 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`](/remote/)) and a **cache** (the [IndexedDB log](/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 replica copied from a host that has since **started over** (a reset, or
  a restart that lost its log) is not a replica of that host any more. So a
  session that opens a warm replica first asks the host for the event at the
  replica's tail. If the host has nothing there, or has a different event,
  the load fails with `HostResetError`. The board then drops the replica and
  loads again from the host, as "Reload from server" does. The CLI deletes
  its replica files, says so on stderr, and loads again. A fresh replica has
  nothing to check, so this happens once. The check runs once per session:
  a host reset in the middle of a session shows up on the next reload.

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
```
