WorkerCacheStorage
WorkerCacheStorage is an asynchronous storage (extends AsyncBaseStorage, type: 'worker')
whose data lives inside a SharedWorker instead of the tab. It mirrors the key/path semantics of
MemoryStorage key-for-key — the difference is where the Map lives: in a
live in-memory cache inside a SharedWorker, shared across every tab of the origin. Think
"MemoryStorage that other tabs can see too".
Creating
import { WorkerCacheStorage } from 'synapse-storage/core'
// Live cross-tab cache held inside a SharedWorker
const storage = new WorkerCacheStorage<TodoState>({
name: 'todo-worker',
initialState: initialTodoState,
options: {}, // channelName defaults to config.name
})
await storage.initialize()
// Or via the static .create()
const storage = WorkerCacheStorage.create<TodoState>({
name: 'todo-worker',
initialState: initialTodoState,
})The API is identical to any other async storage — await set/get/update/delete, getStateSync(),
subscriptions. See Reading, Writing,
remove/has/keys, Subscriptions.
Stores that share the same channelName (defaults to name) share the same cache — that is how
two tabs, or an API QueryStorage and its consumer, land on the same data.
Live cross-tab cache
const storage = new WorkerCacheStorage<TodoState>({
name: 'live-cache',
initialState: initialTodoState,
options: { channelName: 'live-cache' },
})
await storage.initialize()
// Written in one tab, immediately visible to another tab reading the same channelName.
await storage.set('filter', 'active')Capabilities:
{ shared: true }.The cache is live: it holds the current state and is shared between tabs, exactly like a MemoryStorage that lives in the SharedWorker. A tab that joins late always receives a fresh snapshot from the worker.
If SharedWorker is unavailable (SSR / tests), the transport falls back to an in-process
Map— the round-trip is identical, but without cross-tab sharing (only a real SharedWorker gives that).
Need offline or fetch control?
WorkerCacheStorageis a live cross-tab cache, not an offline layer — it never touches the network and does not survive a fully closed session. For real offline / network control, use a separate recipe instead of this storage:
Custom fetch-intercepting ServiceWorker — your own
sw.jstransparently interceptsfetch(precache, offline routing, cache-first / network-first / stale-while-revalidate). It lives belowApiClientand is unrelated to its storage.Custom
baseQuery.fetchFn— replace how the client performs a request (auth-retry, metrics, axios, or a Web Worker transport) while the cache/tags layer keeps working on top unchanged.
Tag invalidation (API cache)
When WorkerCacheStorage backs an API QueryStorage, the tag index is not
part of the shared payload — it is rebuilt on connect via rebuildTagIndex. A second consumer
attaching to the same channelName scans the existing entries and reconstructs its tag → keys map,
so tag-based invalidation keeps working across tabs / instances without shipping the index through
the port.
Pitfalls and limitations
This is a live cache, not persistence. The state lives in the SharedWorker's memory. It is shared across open tabs and survives a tab reload while the worker stays alive, but it is not an offline / session-surviving store. For that, see the recipes linked above.
Values must be structured-clone-able. Everything that crosses the worker port is subject to the structured-clone algorithm — functions,
Symbol,Blob/Response/Headersand cyclic structures cannot cross the port. Store plain, clone-compatible data only.The tag index is rebuilt, not transferred. As above,
rebuildTagIndexreconstructs the API tag index on connect rather than sending it through the port — expect a rebuild pass when a new consumer attaches.SharedWorker fallback is silent-but-degraded. Without SharedWorker support the transport uses an in-process
Map: the API is identical but there is no cross-tab sharing.A broken custom
workerUrldoes not auto-fall-back.new SharedWorker(url)succeeds even if the script 404s or has the wrong MIME type — the failure surfaces asynchronously, after the transport already committed to worker mode. There is no mid-flight fallback: operations then reject by timeout. The channel logs an actionable warning; make sure a customworkerUrlis served same-origin asapplication/javascript. OmitworkerUrlto use the safe inline blob.Large initial
getAllmay hit the RPC timeout. Each store RPC has arequestTimeoutMsbudget (default 1000ms). Priming a very large cache on a slow device can exceed it and failinitialize()— raiseoptions.requestTimeoutMsin that case.
When to use
Live cross-tab cache (shared state across tabs held in a worker) →
WorkerCacheStorage.Offline / network control (data available without a network, fetch interception) → not this storage; see custom fetch-intercepting ServiceWorker or a custom
fetchFn.Just cross-tab notifications on an existing store → you don't need this at all; use
sharedWorkerMiddleware/broadcastMiddleware.
Types
import { WorkerCacheStorage } from 'synapse-storage/core'
import type { WorkerStorageConfig, WorkerStorageOptions } from 'synapse-storage/core'
interface WorkerStorageOptions {
channelName?: string // default: config.name — same channelName ⇒ shared cache
workerUrl?: string | URL // custom SharedWorker script (else inline blob)
requestTimeoutMs?: number // per-RPC timeout, default 1000 — raise for large initial getAll
}
// WorkerStorageConfig<T> extends AsyncStorageConfig<T> with options?: WorkerStorageOptions