Online/Offline Flush ManagerLoading saved progress…

Online/Offline Flush Manager

An offline flush manager is a local outbox: it queues the write actions a user takes while the network is down and automatically replays them, in order, the moment connectivity returns — retrying anything that fails, and never dropping or reordering a write. It is the pattern behind apps like Gmail, Linear, and Notion, where you can keep starring, editing, and archiving on a dead connection and everything quietly syncs once you are back online.

You will implement onlineOfflineFlushManager(options). Both the network and the connectivity signal are injected, so the manager can be tested without a real network: options.send(action) returns a promise for one action, and you drive connectivity by calling goOffline() and goOnline(). In production you wire those two methods to the browser's offline and online events; in a test you call them by hand to simulate losing and regaining signal.

Signature

type Options = {
  send: (action: any) => Promise<any>; // the one network call, per queued action
  online?: boolean; // starting connectivity (default true)
  backoff?: (attempt: number) => number; // ms before retry N (1-based); default exponential
  onSent?: (action: any) => void; // called with each action once it is delivered
  onError?: (action: any, error: any) => void; // called when a send attempt fails
};

function onlineOfflineFlushManager(options: Options): {
  enqueue(action: any): void; // buffer an action; if online, try to send it now
  goOnline(): void; // the offline-to-online edge: flush the outbox in order
  goOffline(): void; // stop sending; un-sent actions stay queued
  flush(): Promise<void>; // manually attempt a drain (no-op while offline)
  pending(): number; // how many actions are still waiting to be sent
};

Examples

// Production: wire connectivity to the browser's online/offline events.
const outbox = onlineOfflineFlushManager({
  send: (action) => fetch('/api/mutations', { method: 'POST', body: JSON.stringify(action) }),
  online: navigator.onLine,
});
window.addEventListener('online', () => outbox.goOnline());
window.addEventListener('offline', () => outbox.goOffline());

outbox.enqueue({ type: 'star', id: 42 }); // sent now if online, queued if not
// Test: inject send and drive connectivity by hand — no real network.
const sent = [];
const outbox = onlineOfflineFlushManager({ send: (a) => { sent.push(a); return Promise.resolve(); } });

outbox.goOffline();
outbox.enqueue('a');
outbox.enqueue('b');
outbox.pending(); // 2 — buffered, sent is still []
outbox.goOnline(); // flushes in order -> sent becomes ['a', 'b'], pending() -> 0

Notes

  • Injected send — always perform a queued action through options.send(action), never a hard-coded fetch. It returns a promise that resolves on success and rejects on failure.
  • Injected connectivity — the manager never reads the network itself. goOffline() and goOnline() are the signal; the offline-to-online transition is what triggers a flush.
  • Enqueue in both statesenqueue while online tries to send the action right away; while offline it only buffers. Either way the action lands in the outbox first, so the outbox is the single source of truth.
  • Ordered, sequential flush — drain the outbox oldest-first, and await each send before starting the next. Do not fire them in parallel, or the network can reorder your writes.
  • Dequeue only on success — remove an action from the outbox only after its send resolves. A send that rejects keeps its action queued and is retried after backoff(attempt); later actions wait behind it, so nothing is dropped, reordered, or sent twice.
  • Do not worry about the real network, persistence across reloads, request cancellation, or conflict resolution — send is injected and assumed to eventually settle.

FAQ

What happens to an action if its send fails while flushing?
It stays at the head of the outbox and is retried after a backoff delay. Later actions wait behind it, so nothing is dropped or reordered.
How does the manager know the connection came back?
It does not poll the network. You tell it, by calling goOnline() and goOffline() — typically wired to the browser's online and offline events. The connectivity signal is injected.
Why remove an action from the queue only after the send succeeds?
So a failed send never loses the action. The outbox is the source of truth; an action leaves it only once the server has accepted it.
Loading editor…