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.
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
};
// 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
options.send(action), never a hard-coded fetch. It returns a promise that resolves on success and rejects on failure.goOffline() and goOnline() are the signal; the offline-to-online transition is what triggers a flush.enqueue 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.await each send before starting the next. Do not fire them in parallel, or the network can reorder your writes.backoff(attempt); later actions wait behind it, so nothing is dropped, reordered, or sent twice.send is injected and assumed to eventually settle.