API Reference

replay()

Drains one store's offline write queue back to the server — fire_and_forget for one store, replayAll() for every registered queue at once. Web Locks protected, cooldown guarded, and scoped to the right tenant automatically.

Prerequisites
  • A store registered with queueConfig — see api/register
  • core-concepts/offline-queue for how entries get queued in the first place
1

Basic usage

flux.replay(channel) drains one store's queue. flux.replayAll() drains every registered store's queue in one call — it's also exactly what runs automatically on the leader tab right after a successful bootstrap().
typescript
1// Replay one store's queued mutations
2await flux.replay('profile')
3
4// Replay every store's queue — this is also what bootstrap() calls
5// on the leader tab immediately after a successful fetch
6await flux.replayAll()
2

You rarely need to call this yourself

Replay is wired into a set of automatic triggers already. Manual calls are for the cases those triggers don't cover — a "retry now" button in your own offline or conflict UI, for instance.
typescript
1// You rarely need to call replay() yourself — it fires automatically on:
2
3// Initial load — 1200ms after mount
4// 'online' event — 2000ms delay
5// pageshow (iOS BFCache) — 3000ms delay
6// window focus — 800ms delay
7// visibilitychange visible — 3000ms delay
8// tab hidden — registers a Service Worker sync tag instead
9// SW message — FLUX_QUEUE_SYNC_SUCCESS
10
11// Manual calls are for cases outside these triggers — a custom
12// "retry now" button in a conflict or offline-queue UI, for example.
3

Locking and run guards

Every replay run is protected by a Web Lock scoped per store, so two open tabs can never both replay the same queue at once. Two additional guards sit on top of the lock to stop redundant runs when several triggers fire close together.
typescript
1// Every replay run is Web Locks protected under flux:queue:{storeName} —
2// Tab A holds the lock for the duration of its replay; Tab B waits,
3// then acquires and finds the queue already empty.
4
5// Two guards prevent redundant runs on top of the lock:
6// - in-flight guard — blocks a second concurrent replay on the same store
7// - 4s cooldown guard — after a run completes, blocks rapid re-triggering
8// from stacked trigger events (e.g. focus + online firing together)
9
10// Manual resolveUnifiedConflict() calls bypass the cooldown — the
11// user explicitly triggered that action.
4

Which strategy actually runs

replay() doesn't choose a strategy — it executes whichever one you configured on that store's queueConfig.replayStrategy at registration time.
typescript
1// The strategy itself is configured on queueConfig at register() time —
2// replay() is what actually executes whichever one you set.
3
4// fire_and_forget (default): all queued entries for one endpoint are
5// sent as a single batched request. No server state check.
6// Correct for append-only writes — waitlist, contact forms, logs.
7
8// handshake: Pass 1 sends one bulk probe with all entry IDs and
9// clock-skew-corrected timestamps. Pass 2 sends one batched execution
10// request containing only the safe (non-conflicted) entries.
11// Exactly 2 network requests total, regardless of queue size.
12// Correct for writes that overwrite an existing server record —
13// dashboard edits, settings, profile changes.
Full contract lives elsewhere

Conflict routing, onConflict decisions, and the universal conflict register are covered end-to-end in core-concepts/conflict-resolution.

5

Scoped replay

The tenant filter always comes from the target store's own registered scope — never from whichever user happens to be logged in at the moment replay() is called. This is what keeps a 'global'-scoped queue (a public waitlist, say) replaying correctly regardless of session state.
typescript
1// replayQueue() internally accepts a userId used to filter which
2// entries it's allowed to touch — resolved from the TARGET STORE'S
3// OWN registered scope, never unconditionally from whoever happens
4// to be logged in when replay() is called.
5
6// A 'global'-scoped queue always passes null, so it keeps replaying
7// correctly regardless of session state:
8await flux.replay('waitlist') // scope: 'global' on this store's registration
9
10// A 'user'-scoped queue passes the active userId, so it only ever
11// replays that tenant's own queued entries:
12await flux.replay('profile') // scope: 'user' on this store's registration
6

Cancellation on session teardown

Replay calls are threaded with the engine's per-scope AbortController. If clearUserSession() fires while a 'user'-scoped replay is mid-flight, that fetch is cancelled immediately rather than completing against a session that's already been torn down.
typescript
1// Each scope has its own AbortController in the engine. If
2// clearUserSession() fires mid-replay — say, while a handshake's
3// execution request is in flight — that scope's fetch rejects
4// immediately rather than landing against a torn-down session.
5
6// The entry stays untouched in IDB for the next trigger to reconsider —
7// nothing is lost, nothing is double-submitted.
8await flux.clearUserSession() // aborts any in-flight 'user'-scope replay
7

Clock skew correction

For handshake-strategy stores, every queued entry's timestamp is corrected before it's sent, so a device with a drifting clock doesn't produce false-positive conflicts against the server.
typescript
1// For handshake replay specifically, entry.timestamp is corrected
2// by flux.clockSkewMs (computed during bootstrap()) before the
3// manifest is built and sent to revalidateFn — this prevents false
4// conflict detection on devices with drifting clocks.
5const correctedTimestamp = entry.timestamp + flux.clockSkewMs

For configuring how a store's queue behaves before it's ever replayed, see api/register. For resolving conflicts a handshake replay surfaces, see api/resolve-unified-conflict.