replay()
Drains one store's offline write queue back to the server — fire_and_forget for append-only writes, a two-pass handshake for writes that could conflict, replayAll() for every registered queue at once. A write is only removed when the server accepts it, confirms it already has it, or the user discards it. Errors from the server become failed writes the user can retry or discard. Web Locks protected, cooldown guarded, network-probed, and scoped to the right tenant automatically.
- A store registered with queueConfig — see api/register
- core_concepts/offline_queue for how entries get queued in the first place
What replay() is for
replay() is the function that drains that queue back to your server once a connection is confirmed. It is deliberately dumb about *what* it is sending — the interesting decision, whether a given write is safe to fire blind or needs to check for conflicts first, is made once, up front, when you register the store.A 4xx no longer deletes the entry. It is kept and marked failed. This applies to both strategies.
A 409 is a failure, not a success. To treat a status as "the server already has this", set idempotency.duplicateStatus.
onRollback fires only when the user discards a failed entry, not when a send fails. Use onFailed to react to failures.
Replay only runs on a reconnect and at session start. Window focus, plain visibility changes and the background sync retry after a 5xx are gone.
Basic usage
flux.replay(channel) drains one store's queue. flux.replayAll() drains every registered store's queue in one call — it is also exactly what runs automatically on the leader tab right after a successful bootstrap(). Called without options, a replay sends pending entries only and never touches failed ones.When replay runs, and what each run sends
| Reason | Started by | Sends |
|---|---|---|
'session-start' | Page load. The default for flux.replay(). | Pending entries only. Failed entries are never touched. |
'reconnect' | Back online, or a resume after the app last saw itself offline. | Pending entries, plus failed entries that are retryable (see step 6). |
'sw-sync' | A background sync finishing in the service worker. | Pending entries only. |
'manual' | retryFailed(), resolveUnifiedConflict(), or replay() with this reason. | Exactly the entries named, whatever their status. With none named, every failed entry. Ignores the cooldown. |
Network reachability probe
replayQueue() confirms the device can actually reach your server — not just that it has some network connection.This route runs at the start of every replay run — reconnect, resume, manual retry — often well before there is anything worth replaying yet. Avoid auth checks, database calls, or anything else that could make the probe itself the reason a replay gets skipped.
Locking, run guards, and timeouts
Failed writes: what happens when a send doesn't succeed
| Failure | Retried automatically on reconnect? |
|---|---|
| 5xx server error (kind 'server') | Yes |
| 401 / 403 (kind 'auth') | Yes |
| 429 (kind 'rate_limited') | Yes, once the server's Retry-After window has passed |
| Handshake revalidation error (kind 'revalidation') | Yes |
| 408 timeout answer | Yes |
| Any other 4xx, including 400, 409, 422 (kind 'rejected') | No. Waits for the user. |
In onComplete(replayed, failed) and in the returned outcome, failed is the number of entries the server rejected during that run. Entries interrupted by a lost connection are not counted, because they are still pending and not failed.
Retrying and discarding failed writes
retryFailed() sends exactly what you name. discardFailed() removes an entry and fires onRollback so you can revert the optimistic change. For subscribing to the failed list and a complete panel component, see core_concepts/offline_queue.Two replay strategies, and why both exist
replay() does not choose a strategy — it executes whichever one you configured on that store's queueConfig.replayStrategy at registration time. The choice comes down to one question: does this write overwrite something that could have changed on the server while you were offline?An endpoint that sends an email or forwards a message has nothing to dedupe against, so don't opt in. Flux still sends the entry keys, which a server may use if it wants. The only duplicate possible without idempotency is when the server handled a write but the response was lost on the way back, in which case the write is sent once more on the next reconnect.
revalidateFn is what makes Pass 1 possible — it is the one place you tell Flux how to check "is the server ahead of what I queued":How held conflicts reach your UI, and how resolveUnifiedConflict() is expected to be called, is covered end-to-end in core_concepts/conflict_resolution. Resolving a conflict with 'proceed' or 'apply_merged' sends only that one entry. If that send fails, the entry joins your failed list like any other.
Queue options for failures and limits
Scoped replay
replay() is called. This is what keeps a 'global'-scoped queue (a public waitlist, say) replaying correctly regardless of session state.Cancellation on session teardown
AbortController. If clearUserSession() fires while a 'user'-scoped replay is mid-flight, that fetch is cancelled immediately rather than completing against a session that has already been torn down.Clock skew correction
For configuring how a store's queue behaves before it is ever replayed, see api/register. For building a failed changes panel and confirming before logout, see core_concepts/offline_queue. For resolving conflicts a handshake replay surfaces, see api/resolve_unified_conflict.