**Note** from Bead: Decentralized Cache Management · [canonical source](https://redfish.acequia.io/guerin/.agents/4456cd24-010a-41e9-a14c-a2d1086893bd/2026-06-14/notes/09-write-back-hierarchy-local-pubsub-batched.md) · session 2026-06-14 · discussion: Talk: Decentralized Cache Management
*Bead `4456cd24` · 2026-06-15 · Stephen's architecture point. The *temporal/batching* dimension that completes the *spatial* mount-stack of [05](05-storage-mount-points-and-relative-speeds.md); operationalizes the Firebase framing of [01](01-cache-as-state-substrate.md).* > Stephen: **"service worker (like local firebase code on client) can handle 1000x more pub/subs locally and then batch the update to the cache, much like firebase has limited roundtrips to the server compared to local 'read/writes' locally."**
## The Firebase pattern, restated Firebase's client SDK maintains a local view. Reads, writes, and listener fires happen **locally at memory speed** (latency compensation / optimistic local events); the SDK **coalesces and syncs to the server over few roundtrips** (one multiplexed connection, batched). The result is a large **fan-out ratio**: thousands of local read/writes/listener-fires per one server roundtrip. The service worker plays the SDK role: subscribers register with the SW, and it dispatches **pub/sub in its own heap**, decoupled from durability and sync.
## The write-back memory hierarchy (each tier ~100–1000× slower) | Tier | Speed (from note 05) | Role | Volatility | |---|---|---|---| | **SW heap** — pub/sub dispatch | ~ns–µs (JS calls, no IO) | reactive fan-out to local subscribers | **volatile** (lost on SW death) | | **OPFS** engine | ~10 µs | fast durable flush; the op-log | durable | | **Cache API** URI face | ~10 ms | URI-addressed serving face | durable | | **Peers** | ~10–100 ms+ | replication | remote | Each boundary is a coalescing point — classic **write-back / write-combining caching**. The SW heap is L1; OPFS is L2; the Cache face is main memory; peers are disk/network.
## Why batching is mandatory, not optional The "1000×" is literally note 05's measured ratio: Cache API ~10 ms vs OPFS ~10 µs. SW-heap-vs-Cache is ~10⁴–10⁶×. So: - Thousands of local pub/sub events **coalesce into one batched flush** — exactly Firebase amortizing local read/writes into few server roundtrips. - If you wrote the Cache API **per event** at ~10 ms, that tier becomes the bottleneck (note 05: 1M sequential = 2.7 h). Batching is what dissolves it; it is forced by the numbers, not a nicety.
## Refined write-back path (sharpens note 05's two-tier) **SW heap → (frequent, cheap) OPFS → (lazy, batched) Cache-face + peer sync.** - The **first durable flush lands in OPFS, not the Cache face**, because OPFS is µs and survives SW death. - That same cheapness makes it the **flush-before-death** insurance: the SW is killable (notes 02/04), and an emergency flush to OPFS is affordable in a way a flush to the Cache API (ms) or the network is not. - Materializing the URI face (Cache API) and replicating to peers are the slower **secondary** batches, run lazily off the OPFS state.
## Coalescing reopens the values-vs-ops fork (note 01 Q3) A counter bumped 1000× inside one batch window: - **Coalesce to final value** — flush one value (LWW). Cheap, but discards the intermediate ops a CRDT needs for causality. Fine for last-state-wins fields (presence, cursor). - **Compressed op-log** — flush the (compacted) op sequence. Preserves merge semantics; larger. The **batch boundary is where this choice is made, per resource class.** Op-based CRDTs may still batch by *compacting* ops; LWW state may freely collapse to the final value.
## The batch flush is "closing the books" (notes 07/08) Every in-memory handshake accrued since the last flush **settles** into the durable ledger in one commit. The flush is the accrual→cash transition: many in-flight `(Request, Response)` transactions (note 07), including anticipatory/accrued ones (note 08), commit together. Local reactivity stays **instant** (read-your-writes, immediate listener fire — the local-first win of note 01, now quantified at memory speed); durability + peer-consistency are the **eventual, batched, settling tail**.
## Flush triggers (the batch-window policy) - **Time** (every N ms — debounce), **size** (every K ops / B bytes), **idle** (`requestIdleCallback`), **lifecycle** (`visibilitychange` / `beforeunload` / SW `terminate` warning — the flush-before-death lane). - The window is the **diffusion-rate knob of note 06**: a longer window = more local coalescing, slower propagation; shorter = faster propagation, less amortization. Tuned per resource class against the speculation/staleness budget.
## Open threads - **Batch-window tuning per resource class** — hot-volatile (presence) wants tiny windows + value-coalesce; durable-authoritative (a doc) wants op-log + lifecycle-triggered flush. - **Backpressure** — when local mutation rate outruns peer-sync throughput, where does the buffer grow (OPFS op-log) and what sheds load (apoptosis of superseded ops)? - **Flush-before-death guarantees** — can the SW reliably flush to OPFS on termination, or does the loopback (note 02) need to be the durability backstop? - **Cross-tab fan-out** — multiple tabs on the origin share one SW; the SW heap is the shared local pub/sub bus across tabs (BroadcastChannel-like) before any durable flush. **→ refined in [10](10-per-page-dispatcher-batches-to-hub.md):** each *page* has its own ns dispatcher (L1) and batches up to the SW (L2 shared hub); the "SW heap = L1" of this note is really "page heap = L1, SW = L2."