Version v1.0.0 Size 3.0 KB gzip
Why Postmaster?
Application jobs that touch a remote service — posting a form, syncing state, sending analytics — must survive page reloads, resume later, retry according to an explicit policy, and retain terminal failures for recovery. Postmaster coordinates that delivery with typed job definitions, leased processing, and a dead-letter queue, all backed by IndexedDB.
// Before
async function createTodo(payload: { id: string; title: string }) {
// Lost on reload. No retry. No recovery. Silent failure.
await fetch('/api/todos', { method: 'POST', body: JSON.stringify(payload) });
}
// After
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
import { s } from '@vielzeug/spell';
const jobs = defineJobs({
createTodo: {
version: 1,
validate: s.object({ id: s.string(), title: s.string() }),
key: (p) => p.id,
execute: async (payload, { key, signal }) => {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Idempotency-Key': key },
signal,
});
},
},
});
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
const postmaster = createPostmaster({ jobs, store });
await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
await postmaster.start();| Feature | Postmaster | Ad hoc outbox | Familiar |
|---|---|---|---|
| Bundle size | 3.0 KB | Application-defined | 3.8 KB |
| Zero dependencies | |||
| Survives page reload | |||
| Leased cross-tab processing | |||
| Dead-letter recovery | |||
| Typed job payloads |
Use Postmaster when application jobs must survive reloads, retry explicitly, and remain recoverable after terminal failure.
Consider Familiar when jobs are CPU-bound, in-memory only, and never need to survive a page reload.
Installation
pnpm add @vielzeug/postmasternpm install @vielzeug/postmasteryarn add @vielzeug/postmasterFor browser persistence, also install @vielzeug/vault (a workspace peer of the IndexedDB adapter):
pnpm add @vielzeug/postmaster @vielzeug/vaultnpm install @vielzeug/postmaster @vielzeug/vaultyarn add @vielzeug/postmaster @vielzeug/vaultQuick Start
Define typed jobs, create a durable store, enqueue work, and start the processor. Dispose both the processor and the store when the page lifetime ends.
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
const jobs = defineJobs({
createTodo: {
version: 1,
validate: (v: unknown) => v as { id: string; title: string },
key: (p) => p.id,
execute: async (payload, { key, signal }) => {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Idempotency-Key': key },
signal,
});
},
retry: { maxAttempts: 5, shouldRetry: () => true },
},
});
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
const postmaster = createPostmaster({ jobs, store });
await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
await postmaster.start();
// On page unload:
await postmaster.dispose();
await store.dispose();Features
defineJobs()— Typed job registry with payload inference and validation.createPostmaster()— Processor with leased claims, heartbeat renewal, and crash recovery.enqueue()— Persist a job and wake the processor.flush()— Process every available job until the queue is empty.retry()/remove()— Recover or discard dead-letter jobs.tap()— Typed runtime events for enqueued, started, completed, retry-scheduled, dead-lettered, removed, lease-lost, and processor-error.createIndexedDbPostmasterStore()— Durable browser store backed by Vault IndexedDB.createMemoryPostmasterStore()— Deterministic in-memory store for tests.
Documentation
See Also
- @vielzeug/courier — Perform the HTTP requests Postmaster jobs coordinate.
- @vielzeug/vault — IndexedDB storage primitive backing the durable store.
- @vielzeug/sentinel — Flush the outbox when the network returns.
- @vielzeug/familiar — In-memory Web Worker pool for CPU-bound tasks.