first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# `sync-queue/sync-queue.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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:
```ts
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.