Files
skills/docs/foundation/data/pwa/src/sync-queue/sync-queue.md
2026-05-29 08:20:56 +01:00

3.1 KiB

sync-queue/sync-queue.ts

(no submodules)

Purpose

Offline-first request queue that persists requests while the device is offline and replays them when connectivity returns. The queue is backend-agnostic: it delegates persistence to any SyncQueueStore (memory, localStorage, or IndexedDB), validates every entry through SyncQueueEntrySchema at the enqueue boundary, runs an exponential-backoff retry loop, and emits OTel counters and histograms covering enqueue / processed / failed counts plus flush duration. Migration from older schema versions happens automatically on every load.

Features

  • Pluggable store (MemoryStore, LocalStorageStore, IndexedDbStore).
  • Zod boundary validation on every enqueue; failures raise PwaError(SYNC_QUEUE_VALIDATION).
  • Capacity enforcement via PwaError(SYNC_QUEUE_FULL) when maxSize is hit.
  • Auto-migration of older schema versions on every load (idempotent).
  • Single-flight process() — concurrent invocations short-circuit to 0.
  • Exponential-backoff retryDelayFor(retryCount) accessor.
  • Injectable Clock and IdGenerator for deterministic tests.
  • OTel emission: pwa_sync_enqueued_total, pwa_sync_processed_total, pwa_sync_failed_total, pwa_sync_flush_ms.

Usage: Basic

import { SyncQueue, IndexedDbStore } from "@suaveplan/pwa";

const queue = new SyncQueue({ store: new IndexedDbStore() });
await queue.enqueue("/api/v1/orders", "POST", {
    body: JSON.stringify({ sku: "abc", qty: 2 }),
    headers: { "content-type": "application/json" },
});
await queue.process();

Usage: Advanced

Inject a clock + id generator + custom processor + failure hook for tests:

import { SyncQueue, MemoryStore, type Clock } from "@suaveplan/pwa";

const clock: Clock = { now: () => 1_700_000_000_000 };
let n = 0;
const queue = new SyncQueue({
    store: new MemoryStore(),
    clock,
    idGenerator: () => `sq_${++n}`,
    maxRetries: 2,
    retryDelay: 100,
    retryBackoffFactor: 3,
    onProcess: async (entry) => entry.url !== "/fail",
    onFailure: (entry) => console.error("permanently failed", entry.id),
});

API Reference

  • new SyncQueue(options: SyncQueueOptions).
  • enqueue(url, method, options?): Promise<SyncQueueEntry>.
  • remove(id): Promise<boolean>.
  • getAll(): Promise<SyncQueueEntry[]>.
  • size(): Promise<number>.
  • clear(): Promise<void>.
  • process(): Promise<number>.
  • retryDelayFor(retryCount): number.
  • isProcessing: boolean (readonly).

Implementation Notes

The queue serialises the entire snapshot on every mutation rather than performing per-entry CRUD against the underlying store. This keeps the SyncQueueStore contract minimal (load / save) and lets every store implementation guarantee atomicity simply by wrapping save() in its native transaction primitive. The process() body avoids Promise.all so failures in one entry cannot cancel sibling work; instead each entry is awaited sequentially and its outcome recorded. The histogram records the entire flush duration including loads and saves so the observed value matches user-felt latency rather than CPU-only time.