@vielzeug/codex 2.2.9 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ {
2
+ "apiSource": "export { defineJobs } from './definitions.ts';\nexport { PostmasterDisposedError, PostmasterError, PostmasterJobError } from './errors.ts';\nexport { createPostmaster } from './postmaster.ts';\nexport type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Postmaster — Durable job outbox\ndescription: Typed durable job outbox with leased processing, retries, and dead-letter recovery for browser applications.\npackage: postmaster\ncategory: Async\nkeywords: [durable, outbox, jobs, retry, dead-letter, idempotency, indexeddb, lease]\nrelated: [courier, vault, sentinel, familiar, ripple]\nexports: [createPostmaster, defineJobs, createIndexedDbPostmasterStore, createMemoryPostmasterStore, PostmasterError, PostmasterDisposedError, PostmasterJobError]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"postmaster\" />\n\n## Why Postmaster?\n\nApplication 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.\n\n```ts\n// Before\nasync function createTodo(payload: { id: string; title: string }) {\n // Lost on reload. No retry. No recovery. Silent failure.\n await fetch('/api/todos', { method: 'POST', body: JSON.stringify(payload) });\n}\n\n// After\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\nimport { s } from '@vielzeug/spell';\n\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: s.object({ id: s.string(), title: s.string() }),\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await fetch('/api/todos', {\n method: 'POST',\n body: JSON.stringify(payload),\n headers: { 'Idempotency-Key': key },\n signal,\n });\n },\n },\n});\n\nconst store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });\nconst postmaster = createPostmaster({ jobs, store });\n\nawait postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });\nawait postmaster.start();\n```\n\n| Feature | Postmaster | Ad hoc outbox | Familiar |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"postmaster\" type=\"size\" /> | Application-defined | <PackageInfo package=\"familiar\" type=\"size\" /> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Survives page reload | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Leased cross-tab processing | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Dead-letter recovery | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed job payloads | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Postmaster when** application jobs must survive reloads, retry explicitly, and remain recoverable after terminal failure.\n\n**Consider Familiar when** jobs are CPU-bound, in-memory only, and never need to survive a page reload.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/postmaster\n```\n\n```sh [npm]\nnpm install @vielzeug/postmaster\n```\n\n```sh [yarn]\nyarn add @vielzeug/postmaster\n```\n\n:::\n\nFor browser persistence, also install `@vielzeug/vault` (a workspace peer of the IndexedDB adapter):\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/postmaster @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/postmaster @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/postmaster @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine typed jobs, create a durable store, enqueue work, and start the processor. Dispose both the processor and the store when the page lifetime ends.\n\n```ts\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\n\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: (v: unknown) => v as { id: string; title: string },\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await fetch('/api/todos', {\n method: 'POST',\n body: JSON.stringify(payload),\n headers: { 'Idempotency-Key': key },\n signal,\n });\n },\n retry: { maxAttempts: 5, shouldRetry: () => true },\n },\n});\n\nconst store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });\nconst postmaster = createPostmaster({ jobs, store });\n\nawait postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });\nawait postmaster.start();\n\n// On page unload:\nawait postmaster.dispose();\nawait store.dispose();\n```\n\n<div class=\"features-grid\">\n\n## Features\n\n- `defineJobs()` — Typed job registry with payload inference and validation.\n- `createPostmaster()` — Processor with leased claims, heartbeat renewal, and crash recovery.\n- `enqueue()` — Persist a job and wake the processor, with optional delayed eligibility via `availableAt`.\n- `flush()` — Process every available job until the queue is empty.\n- `retry()` / `remove()` — Recover or discard dead-letter jobs.\n- `tap()` — Typed runtime events for enqueued, started, completed, retry-scheduled, dead-lettered, removed, lease-lost, and processor-error.\n- `createIndexedDbPostmasterStore()` — Durable browser store backed by Vault IndexedDB.\n- `createMemoryPostmasterStore()` — Deterministic in-memory store for tests.\n\n</div>\n\n<div class=\"doc-links\">\n\n## Documentation\n\n- [**Usage Guide**](./usage.md)\n- [**API Reference**](./api.md)\n- [**Examples**](./examples.md)\n\n</div>\n\n<div class=\"see-also\">\n\n## See Also\n\n- [@vielzeug/courier](../courier/) — Perform the HTTP requests Postmaster jobs coordinate.\n- [@vielzeug/vault](../vault/) — IndexedDB storage primitive backing the durable store.\n- [@vielzeug/sentinel](../sentinel/) — Flush the outbox when the network returns.\n- [@vielzeug/familiar](../familiar/) — In-memory Web Worker pool for CPU-bound tasks.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Postmaster — API Reference\ndescription: Job definitions, processor, store contracts, events, errors, and entry points for Postmaster.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `defineJobs()` | Typed job registry with validation | Sync | Throws on invalid version, missing fields, or bad retry config |\n| `createPostmaster()` | Processor with leased claims and retry | Sync | Store is borrowed, not disposed with the processor |\n| `createIndexedDbPostmasterStore()` | Durable browser store | Sync | Requires `@vielzeug/vault` as a workspace peer |\n| `createMemoryPostmasterStore()` | Deterministic in-memory store | Sync | Use for tests only |\n| `PostmasterError` | Base class for package errors | Sync | Catch a subtype when recovery is specific |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/postmaster` | Job definitions, processor, store contract, events, errors |\n| `@vielzeug/postmaster/indexeddb` | Durable browser store backed by Vault IndexedDB |\n| `@vielzeug/postmaster/testing` | Deterministic in-memory store and test helpers |\n\n## Factories\n\n### `defineJobs()`\n\n```ts\nfunction defineJobs<const J extends JobDefinitions>(jobs: J): J;\n```\n\nReturns the job registry after validating each definition. Rejects invalid versions, missing `execute`/`key`, and retry configurations with non-positive `maxAttempts`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `jobs` | `J extends JobDefinitions` | Map of job name to definition |\n\n**Returns:** `J` — the same registry, typed for payload inference.\n\n**Example**\n\n```ts\nimport { defineJobs } from '@vielzeug/postmaster';\n\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: (v) => v as { id: string; title: string },\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await fetch('/api/todos', {\n method: 'POST',\n body: JSON.stringify(payload),\n headers: { 'Idempotency-Key': key },\n signal,\n });\n },\n },\n});\n```\n\n---\n\n### `createPostmaster()`\n\n```ts\nfunction createPostmaster<J extends JobDefinitions>(options: CreatePostmasterOptions<J>): Postmaster<J>;\n```\n\nReturns a Postmaster processor that claims, executes, retries, and dead-letters jobs from the borrowed store.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.jobs` | `J` | Job registry from `defineJobs()` |\n| `options.store` | `PostmasterStore` | Borrowed store; not disposed with the processor |\n| `options.leaseDuration` | `number` | Lease duration in ms (default 30000, minimum 1000) |\n| `options.clock` | `() => number` | Deterministic clock for tests (default `Date.now`) |\n| `options.signal` | `AbortSignal` | External signal that disposes the processor |\n\n**Returns:** `Postmaster<J>`.\n\n**Example**\n\n```ts\nimport { createPostmaster } from '@vielzeug/postmaster';\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\n\nconst store = createIndexedDbPostmasterStore({ name: 'outbox' });\nconst postmaster = createPostmaster({ jobs, store });\n\nawait postmaster.start();\nawait postmaster.dispose();\nawait store.dispose();\n```\n\n---\n\n### `createIndexedDbPostmasterStore()`\n\n```ts\nfunction createIndexedDbPostmasterStore(options: { name: string }): PostmasterStore;\n```\n\nReturns a durable Postmaster store backed by Vault IndexedDB. Uses one internal table indexed by `status`, `availableAt`, and `leaseExpiresAt`. All operations run inside Vault transactions.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.name` | `string` | IndexedDB database name |\n\n**Returns:** `PostmasterStore`.\n\n**Example**\n\n```ts\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\n\nconst store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });\nawait store.dispose();\n```\n\n---\n\n### `createMemoryPostmasterStore()`\n\n```ts\nfunction createMemoryPostmasterStore(entries?: readonly StoredJob[]): PostmasterStore;\n```\n\nReturns a deterministic in-memory store for tests. Serializes all operations through a promise chain.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `entries` | `readonly StoredJob[]` | Initial records (default empty) |\n\n**Returns:** `PostmasterStore`.\n\n**Example**\n\n```ts\nimport { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing';\n\nconst store = createMemoryPostmasterStore();\nawait store.dispose();\n```\n\n## Postmaster Methods\n\n### `enqueue()`\n\n```ts\nenqueue<K extends keyof J & string>(\n name: K,\n payload: InferJobPayload<J[K]>,\n options?: EnqueueOptions,\n): Promise<PostmasterEntry>;\n```\n\nValidates the payload (if `validate` is defined), derives the key, persists the job, and wakes the processor. Throws `PostmasterError` for an empty key, non-JSON-serializable payload, or invalid `availableAt`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `name` | `K` | Registered job name |\n| `payload` | `InferJobPayload<J[K]>` | Job payload (validated if `validate` is defined) |\n| `options.availableAt` | `number` | Earliest epoch timestamp (ms) the job may be claimed. Defaults to the Postmaster clock. Must be a finite non-negative safe integer. |\n\n**Delayed eligibility.** The job persists immediately but cannot be claimed before `availableAt`. Postmaster does not guarantee execution at that time — only that the job will not be claimed earlier. A live processor (`start()` or `flush()`) is required for execution. Past timestamps remain immediately eligible.\n\n**Example**\n\n```ts\nawait postmaster.enqueue('sendDigest', { userId }, { availableAt: Date.now() + 60_000 });\n```\n\n---\n\n### `start()`\n\n```ts\nstart(): Promise<void>;\n```\n\nBegins background processing. Idempotent.\n\n---\n\n### `flush()`\n\n```ts\nflush(options?: { signal?: AbortSignal }): Promise<FlushResult>;\n```\n\nProcesses every available job until the queue is empty or the signal aborts. Concurrent `flush()` calls join the same drain. Returns counts of processed, completed, dead-lettered, and retry-scheduled jobs.\n\n---\n\n### `list()`\n\n```ts\nlist(filter?: EntryFilter): Promise<PostmasterEntry[]>;\n```\n\nReturns entries ordered by `createdAt`. Filter by `status` optionally.\n\n---\n\n### `stats()`\n\n```ts\nstats(): Promise<PostmasterStats>;\n```\n\nReturns counts of queued, running, and dead-letter jobs.\n\n---\n\n### `retry()`\n\n```ts\nretry(id: string): Promise<RetryResult>;\n```\n\nMoves a dead-letter job back to queued. Returns a discriminated result: `retried`, `not-found`, `not-dead-letter`, or `running`.\n\n---\n\n### `remove()`\n\n```ts\nremove(id: string): Promise<RemoveResult>;\n```\n\nDeletes a queued or dead-letter job. Returns a discriminated result: `removed`, `not-found`, or `running`.\n\n---\n\n### `tap()`\n\n```ts\ntap(handler: (event: PostmasterEvent) => void, options?: { signal?: AbortSignal }): () => void;\n```\n\nObserve runtime events (enqueued, started, completed, retry-scheduled, dead-lettered, removed, lease-lost, processor-error, dispose). Handler errors are swallowed — observability never affects processing. Returns an unsubscribe function. Pass `{ signal }` to auto-detach on abort.\n\n---\n\n### `dispose()`\n\n```ts\ndispose(): Promise<void>;\n[Symbol.asyncDispose](): Promise<void>;\n```\n\nAborts owned work, releases all active leases, and tears down subscriptions. Idempotent. Does not dispose the borrowed store.\n\n## Types\n\n### `EnqueueOptions`\n\n```ts\ninterface EnqueueOptions {\n readonly availableAt?: number;\n}\n```\n\nOptions for `enqueue()`. `availableAt` is the earliest epoch timestamp (ms) at which the job may be claimed. Defaults to the Postmaster clock at enqueue time. Past timestamps remain immediately eligible. Postmaster does not guarantee execution at the requested time — only that the job will not be claimed before it. A live processor is required for execution.\n\n---\n\n### `JobDefinition<T>`\n\n```ts\ninterface JobDefinition<T> {\n readonly version: number;\n readonly validate?: Validate<T>;\n readonly key: (payload: T) => string;\n readonly execute: (payload: T, context: JobContext) => Promise<void>;\n readonly retry?: RetryPolicy;\n readonly migrate?: (payload: unknown, fromVersion: number) => unknown;\n}\n```\n\n`validate` is optional. Accepts a function `(value: unknown) => T` or any structural parser with `parse(value: unknown): T` (Spell schemas, Zod schemas, etc). Called once at enqueue. If omitted, payload trusted as-is.\n\n---\n\n### `Validate<T>`\n\n```ts\ntype Validate<T> = ((value: unknown) => T) | { parse(value: unknown): T };\n```\n\nAccepts either a plain validation function or any object with a `parse(value: unknown): T` method. Spell's `Schema` and `s.object(...)` satisfy this contract directly — no adapter needed.\n\n---\n\n### `JobContext`\n\n```ts\ninterface JobContext {\n readonly attempt: number;\n readonly entryId: string;\n readonly key: string;\n readonly signal: AbortSignal;\n}\n```\n\n---\n\n### `RetryPolicy`\n\n```ts\ninterface RetryPolicy {\n readonly maxAttempts: number;\n readonly shouldRetry: (error: unknown, attempt: number) => boolean;\n readonly delay?: (attempt: number) => number;\n}\n```\n\n`maxAttempts` is total executions including the first. `shouldRetry` is required when retries are enabled. Default delay uses Arsenal's `backoff(attempt)`.\n\n---\n\n### `StoredJob`\n\n```ts\ninterface StoredJob {\n readonly id: string;\n readonly name: string;\n readonly version: number;\n readonly payload: JsonValue;\n readonly key: string;\n readonly status: 'queued' | 'running' | 'dead-letter';\n readonly attempts: number;\n readonly createdAt: number;\n readonly updatedAt: number;\n readonly availableAt: number;\n readonly ownerId?: string;\n readonly leaseExpiresAt?: number;\n readonly failure?: StoredFailure;\n}\n```\n\n---\n\n### `StoredFailure`\n\n```ts\ninterface StoredFailure {\n readonly name: string;\n readonly message: string;\n readonly occurredAt: number;\n}\n```\n\nOnly a bounded error name/message/timestamp is persisted. Never persist arbitrary error objects, response bodies, headers, or stacks.\n\n---\n\n### `PostmasterEntry`\n\n```ts\ntype PostmasterEntry = Pick<StoredJob,\n 'attempts' | 'availableAt' | 'createdAt' | 'failure' | 'id' |\n 'key' | 'name' | 'status' | 'updatedAt' | 'version'\n>;\n```\n\nThe public entry view excludes `payload`, `ownerId`, and `leaseExpiresAt`.\n\n---\n\n### `PostmasterStore`\n\n```ts\ninterface PostmasterStore {\n transact<T>(fn: (tx: StoreTx) => Promise<T>): Promise<T>;\n list(filter?: EntryFilter): Promise<StoredJob[]>;\n subscribe(listener: () => void): () => void;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\ninterface StoreTx {\n get(id: string): Promise<StoredJob | undefined>;\n put(entry: StoredJob): Promise<void>;\n delete(id: string): Promise<void>;\n findClaimable(now: number): Promise<StoredJob | undefined>;\n findNextWake(now: number): Promise<number | undefined>;\n countByStatus(): Promise<PostmasterStats>;\n}\n```\n\nThe store exposes transactional primitives. The processor owns all ownership and transition logic — stores implement storage, not the job state machine. `transact` wraps all operations in an atomic transaction. `findClaimable` returns the earliest eligible job (queued with `availableAt <= now`, or running with expired lease). `findNextWake` returns the earliest future wake time across queued and running jobs.\n\n---\n\n### `PostmasterEvent`\n\n```ts\ntype PostmasterEvent =\n | { readonly type: 'enqueued' | 'started' | 'completed' | 'retry-scheduled' | 'dead-lettered'; readonly entry: PostmasterEntry }\n | { readonly type: 'removed' | 'lease-lost'; readonly id: string }\n | { readonly type: 'processor-error'; readonly error: Error }\n | { readonly type: 'dispose' };\n```\n\n---\n\n### `FlushResult`\n\n```ts\ninterface FlushResult {\n readonly processed: number;\n readonly completed: number;\n readonly deadLettered: number;\n readonly retryScheduled: number;\n}\n```\n\n---\n\n### `RetryResult` / `RemoveResult`\n\n```ts\ntype RetryResult =\n | { readonly status: 'not-found' | 'not-dead-letter' | 'running' }\n | { readonly status: 'retried'; readonly entry: PostmasterEntry };\n\ntype RemoveResult =\n | { readonly status: 'not-found' | 'running' }\n | { readonly status: 'removed'; readonly id: string };\n```\n\n## Errors\n\n### `PostmasterError`\n\n```ts\nclass PostmasterError extends Error {\n constructor(message: string, options?: ErrorOptions);\n}\n```\n\nBase class for package-defined errors. Use `instanceof PostmasterError` to narrow to the hierarchy. Covers configuration errors, serialization errors, and store failures.\n\n---\n\n### `PostmasterDisposedError`\n\n```ts\nclass PostmasterDisposedError extends PostmasterError {}\n```\n\nThrown when a public method is called after disposal.\n\n---\n\n### `PostmasterJobError`\n\n```ts\nclass PostmasterJobError extends PostmasterError {}\n```\n\nThrown when a job definition is missing, a version is incompatible, or a migration fails. These errors move the job to dead-letter rather than rejecting the public call.\n",
6
+ "usage": "---\ntitle: Postmaster — Usage Guide\ndescription: Define durable jobs, process them with leases, retry failures, and recover dead-letter work.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine typed jobs, create a durable store, enqueue work, and start the processor. Dispose both handles when the owner ends.\n\n```ts\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\n\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: (v: unknown) => v as { id: string; title: string },\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await fetch('/api/todos', {\n method: 'POST',\n body: JSON.stringify(payload),\n headers: { 'Idempotency-Key': key },\n signal,\n });\n },\n },\n});\n\nconst store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });\nconst postmaster = createPostmaster({ jobs, store });\n\nawait postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });\nawait postmaster.start();\n\n// On page unload:\nawait postmaster.dispose();\nawait store.dispose();\n```\n\nThe store is borrowed by `createPostmaster()` and is not disposed with the processor. Dispose both explicitly.\n\n## At-least-once delivery and idempotency\n\nPostmaster provides **at-least-once delivery**. A crash after the remote write but before local completion can repeat the job. Every job must derive a stable idempotency key, and handlers must send or otherwise enforce that key.\n\n```ts\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: (v: unknown) => v as { id: string; title: string },\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await fetch('/api/todos', {\n method: 'POST',\n body: JSON.stringify(payload),\n headers: { 'Idempotency-Key': key },\n signal,\n });\n },\n },\n});\n```\n\nNever assume exactly-once execution. Design handlers so a repeated delivery is safe.\n\n## Postmaster jobs vs Courier mutations\n\nCourier performs immediate HTTP requests and cache reconciliation. Postmaster coordinates durable delivery. Use Courier inside a Postmaster job when the write must survive reloads.\n\n```ts\nimport { createCourier, CourierNetworkError } from '@vielzeug/courier';\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: (v: unknown) => v as { id: string; title: string },\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await courier.mutate({\n request: () =>\n courier.post('/todos', {\n body: payload,\n headers: { 'Idempotency-Key': key },\n signal,\n }),\n invalidateKeys: [['todos']],\n });\n },\n retry: { maxAttempts: 5, shouldRetry: (error) => error instanceof CourierNetworkError },\n },\n});\n```\n\nPostmaster does not import Courier. The integration happens in your job definitions.\n\n## Payload and version migration\n\nEach job declares a `version` and an optional `validate` function. When a stored job's version is older than the registered version, Postmaster calls `migrate()` before validating. `validate` is called once at enqueue; omit it to accept the payload as-is. `validate` accepts a plain function `(value: unknown) => T` or any structural parser with `parse(value: unknown): T` — Spell schemas work directly:\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst jobs = defineJobs({\n createTodo: {\n version: 2,\n validate: s.object({ id: s.string(), title: s.string(), priority: s.number().optional() }),\n key: (p) => p.id,\n migrate: (payload, fromVersion) => {\n if (fromVersion === 1) return { ...(payload as { id: string; title: string }), priority: 0 };\n return payload;\n },\n execute: async (payload, { key, signal }) => {\n await fetch('/api/todos', {\n method: 'POST',\n body: JSON.stringify(payload),\n headers: { 'Idempotency-Key': key },\n signal,\n });\n },\n },\n});\n```\n\nUnknown job names, incompatible versions, failed migrations, and invalid persisted payloads move to dead-letter rather than being executed.\n\n## Retry semantics\n\nRetries are opt-in and explicitly classified. No `retry` block means one attempt followed by dead-letter.\n\n```ts\nconst jobs = defineJobs({\n syncTodo: {\n version: 1,\n validate: (v: unknown) => v as { id: string },\n key: (p) => p.id,\n execute: async (payload, { signal }) => {\n await fetch(`/api/todos/${payload.id}/sync`, { signal });\n },\n retry: {\n maxAttempts: 5,\n shouldRetry: (error) => error instanceof TypeError, // network errors only\n },\n },\n});\n```\n\n- `maxAttempts` means total executions, including the first.\n- `shouldRetry` is required when retries are enabled. Postmaster never guesses whether a write is safe to repeat.\n- Default delay uses Arsenal's deterministic `backoff(attempt)` helper. Override with `delay`.\n- Delay must be finite and non-negative.\n- Lifecycle aborts caused by disposal are not classified as job failures.\n\n## Delayed eligibility\n\n`enqueue()` accepts an optional `availableAt` timestamp. The job persists immediately but cannot be claimed before that time. Use this for scheduled writes, cooldowns, or any work that must survive a reload but should not run yet.\n\n```ts\nawait postmaster.enqueue('sendDigest', { userId }, { availableAt: Date.now() + 60_000 });\n```\n\nPostmaster does not guarantee execution at `availableAt` — only that the job will not be claimed earlier. A live processor (`start()` or `flush()`) is required for execution. In a browser, a closed page or suspended service worker will run the job when the processor next becomes active. Past timestamps remain immediately eligible. The same mechanism already backs retry delays, so delayed eligibility reuses the existing claim, wake, and persistence paths.\n\n## Dead-letter recovery\n\nJobs that exhaust retries or hit a terminal failure move to dead-letter. Inspect, retry, or remove them.\n\n```ts\nconst deadLettered = await postmaster.list({ status: 'dead-letter' });\n\nfor (const entry of deadLettered) {\n console.log(entry.id, entry.name, entry.failure);\n}\n\n// Retry a dead-letter job back into the queue.\nawait postmaster.retry(entry.id);\n\n// Or remove it permanently.\nawait postmaster.remove(entry.id);\n```\n\n`retry()` and `remove()` return discriminated results so callers can distinguish `not-found`, `not-dead-letter`, `running`, and successful outcomes without exceptions.\n\n## Lifecycle and disposal\n\n`start()` begins background processing. `dispose()` stops claiming new work, aborts owned work, and is idempotent. `flush()` processes every available job synchronously.\n\n```ts\nawait postmaster.start();\n// ...on unload\nawait postmaster.dispose();\nawait store.dispose();\n```\n\nDisposal aborts owned work, releases the active lease, and is idempotent. A controlled disposal abort does not consume the attempt — the job returns to queued.\n\n## Events\n\nTap runtime events for observability. Handler errors are swallowed — observability never affects processing.\n\n```ts\nconst unsubscribe = postmaster.tap((event) => {\n switch (event.type) {\n case 'enqueued':\n console.log('enqueued', event.entry.id);\n break;\n case 'completed':\n console.log('completed', event.entry.id);\n break;\n case 'dead-lettered':\n console.error('dead-lettered', event.entry.id, event.entry.failure);\n break;\n case 'processor-error':\n console.error('processor error', event.error);\n break;\n }\n});\n```\n\nPass an `AbortSignal` to auto-detach:\n\n```ts\nconst controller = new AbortController();\npostmaster.tap(handler, { signal: controller.signal });\ncontroller.abort(); // stops tapping\n```\n\n## Testing\n\nUse the in-memory store for deterministic tests.\n\n```ts\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\nimport { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing';\n\nconst store = createMemoryPostmasterStore();\nconst postmaster = createPostmaster({\n jobs: defineJobs({\n send: {\n version: 1,\n validate: (v: unknown) => String(v),\n key: (p) => p,\n execute: async () => {},\n },\n }),\n store,\n});\n\nawait postmaster.enqueue('send', 'hello');\nawait postmaster.flush();\nawait postmaster.dispose();\n```\n\nInject a deterministic clock to control retry scheduling.\n\n```ts\nlet now = 0;\nconst postmaster = createPostmaster({ clock: () => now, jobs, store });\n```\n\n## Framework Integration\n\nCreate the Postmaster after the component mounts, start processing, and dispose on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\nimport { createPostmaster, defineJobs, type Postmaster } from '@vielzeug/postmaster';\nimport { useEffect } from 'react';\n\nconst jobs = defineJobs({\n sync: {\n version: 1,\n validate: (v: unknown) => v as { id: string },\n key: (p) => p.id,\n execute: async (payload, { signal }) => {\n await fetch(`/api/sync/${payload.id}`, { signal });\n },\n },\n});\n\nexport function OutboxProvider() {\n useEffect(() => {\n const store = createIndexedDbPostmasterStore({ name: 'outbox' });\n const postmaster = createPostmaster({ jobs, store });\n void postmaster.start();\n\n return () => {\n void postmaster.dispose();\n void store.dispose();\n };\n }, []);\n\n return null;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\nimport { onMounted, onUnmounted } from 'vue';\n\nconst jobs = defineJobs({\n sync: {\n version: 1,\n validate: (v: unknown) => v as { id: string },\n key: (p) => p.id,\n execute: async (payload, { signal }) => {\n await fetch(`/api/sync/${payload.id}`, { signal });\n },\n },\n});\n\nlet postmaster: ReturnType<typeof createPostmaster> | undefined;\nlet store: ReturnType<typeof createIndexedDbPostmasterStore> | undefined;\n\nonMounted(() => {\n store = createIndexedDbPostmasterStore({ name: 'outbox' });\n postmaster = createPostmaster({ jobs, store });\n void postmaster.start();\n});\n\nonUnmounted(() => {\n void postmaster?.dispose();\n void store?.dispose();\n});\n</script>\n\n<template>\n <slot />\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';\n import { createPostmaster, defineJobs } from '@vielzeug/postmaster';\n import { onMount } from 'svelte';\n\n const jobs = defineJobs({\n sync: {\n version: 1,\n validate: (v: unknown) => v as { id: string },\n key: (p) => p.id,\n execute: async (payload, { signal }) => {\n await fetch(`/api/sync/${payload.id}`, { signal });\n },\n },\n });\n\n onMount(() => {\n const store = createIndexedDbPostmasterStore({ name: 'outbox' });\n const postmaster = createPostmaster({ jobs, store });\n void postmaster.start();\n\n return () => {\n void postmaster.dispose();\n void store.dispose();\n };\n });\n</script>\n\n<slot />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Postmaster + Courier\n\nUse Courier inside job handlers for HTTP transport and cache invalidation. Postmaster coordinates delivery; Courier performs the request.\n\n```ts\nimport { createCourier, CourierNetworkError } from '@vielzeug/courier';\nimport { createPostmaster, defineJobs } from '@vielzeug/postmaster';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\nconst jobs = defineJobs({\n createTodo: {\n version: 1,\n validate: (v: unknown) => v as { id: string; title: string },\n key: (p) => p.id,\n execute: async (payload, { key, signal }) => {\n await courier.mutate({\n request: () =>\n courier.post('/todos', {\n body: payload,\n headers: { 'Idempotency-Key': key },\n signal,\n }),\n invalidateKeys: [['todos']],\n });\n },\n retry: { maxAttempts: 5, shouldRetry: (e) => e instanceof CourierNetworkError },\n },\n});\n```\n\n### Postmaster + Sentinel\n\nFlush the outbox when the network returns. Sentinel reports online state; Postmaster does the rest.\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\nimport { createPostmaster } from '@vielzeug/postmaster';\n\nconst network = createNetwork();\nconst postmaster = createPostmaster({ jobs, store });\n\nconst unsubscribe = network.subscribe(() => {\n if (network.value.online) void postmaster.flush();\n});\n\n// On teardown:\nunsubscribe();\nnetwork.dispose();\nawait postmaster.dispose();\n```\n\n### Postmaster + Vault\n\nThe IndexedDB adapter is built on Vault. Use Vault directly for unrelated storage; the Postmaster store owns its own database name.\n\n## Best Practices\n\n- **Derive** a stable idempotency key from every job payload and send it with the remote write.\n- **Dispose** both the processor and the store explicitly; the processor does not own the store.\n- **Classify** retryable errors explicitly with `shouldRetry`; never let Postmaster guess.\n- **Migrate** persisted payloads when job versions change; test migrations against stored fixtures.\n- **Inspect** the dead-letter queue regularly and retry or remove terminal failures.\n- **Avoid** persisting sensitive data in payloads or failure messages; IndexedDB is per-origin but not encrypted.\n- **Flush** the outbox when Sentinel reports the network returns.\n- **Test** with the in-memory store and a deterministic clock for reproducible retry timing.\n",
7
+ "examples": "---\ntitle: Postmaster — Examples\ndescription: Durable outbox recipes for offline mutations, network recovery, delayed eligibility, and dead-letter handling.\n---\n\n## Examples\n\n- [Queue Offline Courier Mutations](./examples/queue-offline-courier-mutations.md)\n- [Resume When Network Returns](./examples/resume-when-network-returns.md)\n- [Delayed Eligibility](./examples/delayed-eligibility.md)\n- [Recover Dead-Letter Jobs](./examples/recover-dead-letter-jobs.md)\n- [Service Worker Background Sync](./examples/service-worker-background-sync.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "define-jobs",
12
+ "code": "import { createPostmaster, defineJobs } from '@vielzeug/postmaster'\nimport { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing'\n\nconst jobs = defineJobs({\n send: {\n version: 1,\n validate: (v) => String(v),\n key: (p) => `send:${p}`,\n execute: async (payload, { key, attempt }) => {\n console.log(`delivering \"${payload}\" (attempt ${attempt}, key ${key})`)\n },\n },\n})\n\nconst store = createMemoryPostmasterStore()\nconst postmaster = createPostmaster({ jobs, store })\n\nawait postmaster.enqueue('send', 'hello')\nconst result = await postmaster.flush()\nconsole.log('flush result:', result)\nawait postmaster.dispose()",
13
+ "name": "defineJobs - Basic Outbox"
14
+ },
15
+ {
16
+ "id": "delayed-enqueue",
17
+ "code": "import { createPostmaster, defineJobs } from '@vielzeug/postmaster'\nimport { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing'\n\nconst jobs = defineJobs({\n send: {\n version: 1,\n validate: (v) => String(v),\n key: (p) => `send:${p}`,\n execute: async (payload, { key, attempt }) => {\n console.log(`delivering \"${payload}\" (attempt ${attempt}, key ${key})`)\n },\n },\n})\n\nlet now = 0\nconst store = createMemoryPostmasterStore()\nconst postmaster = createPostmaster({ clock: () => now, jobs, store })\n\n// Persist now, but the job is not claimable until availableAt.\nconst entry = await postmaster.enqueue('send', 'hello', { availableAt: 60_000 })\nconsole.log('enqueued with availableAt:', entry.availableAt)\n\n// Nothing eligible yet.\nlet result = await postmaster.flush()\nconsole.log('flush before eligible:', result)\n\n// Advance the clock past availableAt.\nnow = 60_000\nresult = await postmaster.flush()\nconsole.log('flush after eligible:', result)\nawait postmaster.dispose()",
18
+ "name": "delayedEnqueue - Delayed Eligibility"
19
+ }
20
+ ],
21
+ "typeSignatures": {
22
+ "defineJobs": "export { defineJobs } from './definitions.ts';",
23
+ "PostmasterDisposedError": "export { PostmasterDisposedError, PostmasterError, PostmasterJobError } from './errors.ts';",
24
+ "PostmasterError": "export { PostmasterDisposedError, PostmasterError, PostmasterJobError } from './errors.ts';",
25
+ "PostmasterJobError": "export { PostmasterDisposedError, PostmasterError, PostmasterJobError } from './errors.ts';",
26
+ "createPostmaster": "export { createPostmaster } from './postmaster.ts';",
27
+ "CreatePostmasterOptions": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
28
+ "EnqueueOptions": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
29
+ "EntryFilter": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
30
+ "EntryStatus": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
31
+ "FlushResult": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
32
+ "InferJobPayload": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
33
+ "JobContext": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
34
+ "JobDefinition": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
35
+ "JobDefinitions": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
36
+ "JsonPrimitive": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
37
+ "JsonValue": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
38
+ "Postmaster": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
39
+ "PostmasterEntry": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
40
+ "PostmasterEvent": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
41
+ "PostmasterStats": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
42
+ "PostmasterStore": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
43
+ "RemoveResult": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
44
+ "RetryPolicy": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
45
+ "RetryResult": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
46
+ "StoredFailure": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
47
+ "StoredJob": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
48
+ "StoreTx": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';",
49
+ "Validate": "export type {\n CreatePostmasterOptions,\n EnqueueOptions,\n EntryFilter,\n EntryStatus,\n FlushResult,\n InferJobPayload,\n JobContext,\n JobDefinition,\n JobDefinitions,\n JsonPrimitive,\n JsonValue,\n Postmaster,\n PostmasterEntry,\n PostmasterEvent,\n PostmasterStats,\n PostmasterStore,\n RemoveResult,\n RetryPolicy,\n RetryResult,\n StoredFailure,\n StoredJob,\n StoreTx,\n Validate,\n} from './types.ts';"
50
+ }
51
+ }
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseRoomTimeoutError,\n PulseTimeoutError,\n} from './errors';\nexport { createPulse } from './pulse';\nexport type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';\n",
2
+ "apiSource": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseRoomTimeoutError,\n PulseTimeoutError,\n} from './errors';\nexport { createPulse } from './pulse';\nexport type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Pulse — Typed WebSocket sessions\ndescription: Explicitly connected, typed WebSocket sessions with scoped channels, ref-counted rooms with reactive presence, reconnect restoration, and heartbeat.\npackage: pulse\ncategory: websockets\nkeywords: [websocket, realtime, channels, presence, rooms, reconnect, heartbeat, typed-messaging, ripple]\nrelated: [herald, ripple, courier, clockwork]\nexports:\n [\n createPulse,\n Pulse,\n PulseChannel,\n RoomScope,\n RoomScopeBase,\n PresenceRoomScope,\n PulseOptions,\n PulseSchema,\n ChannelDefinition,\n ChannelDefinitions,\n RoomDefinition,\n RoomDefinitions,\n RoomOptions,\n OutgoingMessage,\n OutgoingTransform,\n PulseError,\n PulseConnectionError,\n PulseTimeoutError,\n PulseRoomTimeoutError,\n PulseAbortError,\n PulseDisposedError,\n PulseProtocolError,\n ]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"pulse\" />\n\n## Why Pulse?\n\nNative WebSocket leaves connection ownership, event routing, reconnect restoration, and cleanup to each application. Pulse provides those boundaries while making readiness explicit: applications connect before sending, and disconnected messages never disappear silently.\n\n```ts\n// Before\nconst socket = new WebSocket('wss://api.example.com/ws');\nsocket.addEventListener('message', (event) => route(JSON.parse(event.data)));\nsocket.addEventListener('close', () => setTimeout(() => reconnect(), 1_000));\n\n// After\nconst pulse = createPulse<{ server: { 'chat:message': { text: string } }; client: { 'chat:send': { text: string } } }>(\n 'wss://api.example.com/ws',\n { reconnect: true },\n);\ntry {\n await pulse.connect();\n pulse.on('chat:message', (message) => console.log(message.text));\n pulse.send('chat:send', { text: 'Hello!' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n```\n\n| Feature | Pulse | Native WebSocket | socket.io-client |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"pulse\" type=\"size\" /> | 0 B | ~44 kB gzip |\n| Explicit readiness | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Session restoration | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Protocol-specific |\n| Typed scoped channels | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Basic |\n| Typed rooms with presence | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero runtime dependencies | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> ripple | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Pulse when** you need a typed WebSocket session whose reconnect and cleanup behavior must be deterministic.\n\n**Consider native WebSocket when** a single untyped connection does not need retry, routing, or session restoration.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/pulse @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nDefine the protocol schema at construction time, create scopes, then connect before sending.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n server: { 'chat:message': { text: string } };\n client: { 'chat:send': { text: string } };\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n };\n rooms: {\n lobby: { presence: { name: string } };\n };\n};\n\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n onError: (error) => console.error(error),\n});\nconst chat = pulse.channel('chat');\nconst lobby = pulse.room('lobby');\n\ntry {\n await pulse.connect();\n chat.send('send', { text: 'Hello!' });\n await lobby.joined;\n lobby.updatePresence({ name: 'Ada' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n\npulse.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`connect()`** — explicit readiness; application messages throw while disconnected.\n- **`channel()`** — named, schema-bound scopes with independent disposal and reference-counted server subscriptions.\n- **`room()`** — named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sends `join`; the last disposal sends `leave`.\n- **`reconnect`** — ordered restoration of channel subscriptions, room memberships, and local presence state.\n- **`transform`** — one synchronous transform or filter for application messages.\n- **`onError`** — typed connection and protocol errors.\n- **`heartbeat`** — ping/pong liveness detection that uses the same reconnect controller.\n- **`status` and `rooms`** — ripple readables for transport and confirmed membership state.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — provides the reactive values exposed by Pulse.\n- [Herald](/herald/) — receives routed Pulse events in an in-process application bus.\n- [Courier](/courier/) — handles request/response traffic alongside a Pulse session.\n- [Clockwork](/clockwork/) — models application-level authentication or session workflows.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: API — Pulse\ndescription: Complete API reference for Pulse, including schema types, options, scopes, and error classes.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createPulse()` | Create a typed WebSocket session instance. | Sync (returns `Pulse`) | Does not open the connection — call `connect()`. |\n| `Pulse` | Main instance: channels, rooms, messaging, lifecycle. | Sync methods, async `connect()`/`wait()` | `send()` throws while disconnected. |\n| `PulseChannel` | Scoped channel namespace with independent disposal. | Sync methods, async `wait()` | Each call returns a new scope; ref-counted subscription. |\n| `RoomScope` | Ref-counted room membership with optional presence. | Sync methods, async `joined` | `joined` rejects on transport close or timeout. |\n| `PulseSchema` | Declares server/client events, channels, and rooms. | Type-only | Infer all named scope types from this schema. |\n| `PulseOptions` | Configuration: heartbeat, reconnect, transform, onError. | Type-only | `reconnect` and `heartbeat` default to `false`. |\n| `PulseError` | Base class for all Pulse errors. | Runtime | Check `instanceof` against subclasses. |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/pulse` | All public exports: `createPulse`, types, and error classes. |\n\n## `createPulse()`\n\n```ts\nfunction createPulse<S extends PulseSchema = PulseSchema>(url: string, options?: PulseOptions): Pulse<S>\n```\n\nCreates a Pulse instance. The WebSocket is not opened until `connect()` is called.\n\n### Type parameters\n\n| Parameter | Constraint | Description |\n| --- | --- | --- |\n| `S` | `PulseSchema` | Schema declaring server events, client events, channels, and rooms. |\n\n### Parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `string` | WebSocket URL. |\n| `options` | `PulseOptions` | Optional configuration. |\n\n### Returns\n\n`Pulse<S>` — the Pulse instance.\n\n---\n\n## `PulseSchema`\n\n```ts\ntype PulseSchema = {\n server?: MessageMap;\n client?: MessageMap;\n channels?: ChannelDefinitions;\n rooms?: RoomDefinitions;\n};\n```\n\nDeclare all protocol surfaces once at construction. Named scopes infer their types from this schema.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `server` | `MessageMap` | Root events the server sends. |\n| `client` | `MessageMap` | Root events the client sends. |\n| `channels` | `ChannelDefinitions` | Named channel schemas. |\n| `rooms` | `RoomDefinitions` | Named room schemas with optional presence. |\n\n---\n\n## `PulseOptions`\n\n```ts\ntype PulseOptions = {\n heartbeat?: boolean | HeartbeatOptions;\n onError?: (error: PulseError) => void;\n protocols?: string | string[];\n reconnect?: boolean | ReconnectOptions;\n transform?: OutgoingTransform;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `heartbeat` | `boolean \\| HeartbeatOptions` | `false` | Ping/pong keep-alive. |\n| `onError` | `(error: PulseError) => void` | — | Receives typed transport and protocol errors. |\n| `protocols` | `string \\| string[]` | — | Sub-protocols passed to the WebSocket constructor. |\n| `reconnect` | `boolean \\| ReconnectOptions` | `false` | Auto-reconnect on unexpected close. |\n| `transform` | `OutgoingTransform` | — | Transform or filter outgoing application messages. |\n\n---\n\n## `HeartbeatOptions`\n\n```ts\ntype HeartbeatOptions = {\n interval?: number;\n timeout?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `interval` | `number` | `30_000` | Interval between pings in ms. |\n| `timeout` | `number` | `5_000` | How long to wait for a pong before treating the connection as dead. |\n\n---\n\n## `ReconnectOptions`\n\n```ts\ntype ReconnectOptions = {\n delay?: number | ((attempt: number) => number);\n maxAttempts?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `delay` | `number \\| ((attempt: number) => number)` | Full-jitter exponential backoff capped at 30 s | Delay between reconnect attempts in ms. `attempt` is zero-based. |\n| `maxAttempts` | `number` | `5` | Maximum number of reconnect attempts after initial failure. |\n\n---\n\n## `OutgoingMessage`\n\n```ts\ntype OutgoingMessage = { channel?: string; event: string; payload: unknown };\n```\n\nAn outgoing application message before it is serialized.\n\n---\n\n## `OutgoingTransform`\n\n```ts\ntype OutgoingTransform = (message: Readonly<OutgoingMessage>) => OutgoingMessage | null;\n```\n\nTransform or filter outgoing application messages. Internal protocol frames (subscribe, join, leave, presence, ping) bypass this hook. Return `null` to drop the message.\n\n---\n\n## `Pulse`\n\n```ts\ntype Pulse<S extends PulseSchema = PulseSchema> = {\n // Channels\n channel<K extends keyof ChannelMap<S> & string>(\n name: K,\n ): PulseChannel<ChannelMap<S>[K]['server'], ChannelMap<S>[K]['client']>;\n\n // Connection\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n\n // Lifecycle\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n\n // Messaging\n on<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n once<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n send<K extends EventKey<ClientEvents<S>>>(event: K, payload: ClientEvents<S>[K]): void;\n wait<K extends EventKey<ServerEvents<S>>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<ServerEvents<S>[K]>;\n\n // Rooms\n room<K extends keyof RoomMap<S> & string>(name: K, opts?: RoomOptions): RoomScope<RoomMap<S>[K]>;\n readonly rooms: Readable<ReadonlySet<string>>;\n\n // Status\n readonly status: Readable<PulseStatus>;\n\n [Symbol.dispose](): void;\n};\n```\n\n### `channel(name)`\n\nCreates an isolated message namespace over the shared connection. Each call returns an independently disposable scope. The server subscription is reference-counted.\n\n### `connect()`\n\nExplicitly opens the connection. Resolves after session restoration completes. Rejects if the connection closes before opening.\n\n### `disconnect(code?, reason?)`\n\nCloses the connection without triggering reconnection. Default code is `1000`.\n\n### `dispose()`\n\nPermanently closes the connection and releases all resources. Idempotent.\n\n### `on(event, handler)`\n\nSubscribes to a typed server event. Returns an unsubscribe function.\n\n### `once(event, handler)`\n\nSubscribes once — auto-removes after first invocation.\n\n### `send(event, payload)`\n\nSends a typed event to the server. Throws `PulseConnectionError` unless the connection is open.\n\n### `wait(event, opts?)`\n\nResolves on the next emission of the given server event. Rejects when `opts.signal` aborts, the timeout elapses, or the instance is disposed.\n\n### `room(name, opts?)`\n\nCreates a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n### `rooms`\n\nReactive set of rooms the client is currently a confirmed member of.\n\n### `status`\n\nReactive connection status: `'connecting' | 'open' | 'reconnecting' | 'closed'`.\n\n---\n\n## `PulseChannel`\n\n```ts\ntype PulseChannel<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap> = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n dispose(): void;\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;\n wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;\n [Symbol.dispose](): void;\n};\n```\n\n---\n\n## `RoomScope`\n\n```ts\ntype RoomScope<R extends RoomDefinition = RoomDefinition> = R extends { presence: infer P }\n ? P extends undefined\n ? RoomScopeBase\n : PresenceRoomScope<P>\n : RoomScopeBase;\n```\n\nA room scope. When the room definition includes `presence`, the scope is a `PresenceRoomScope`; otherwise it is a `RoomScopeBase`.\n\n### `RoomScopeBase`\n\n```ts\ntype RoomScopeBase = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n readonly joined: Promise<void>;\n dispose(): void;\n [Symbol.dispose](): void;\n};\n```\n\n### `PresenceRoomScope`\n\n```ts\ntype PresenceRoomScope<T = unknown> = RoomScopeBase & {\n readonly presence: Readable<ReadonlyMap<string, T>>;\n updatePresence(state: T): void;\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe;\n onLeave(handler: (memberId: string) => void): Unsubscribe;\n};\n```\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `presence` | `Readable<ReadonlyMap<string, T>>` | Reactive map of `memberId → state`. |\n| `updatePresence(state)` | `(state: T) => void` | Broadcast this client's presence state. Throws `PulseConnectionError` unless open. |\n| `onJoin(handler)` | `(handler) => Unsubscribe` | Called whenever a new member joins with their initial state. |\n| `onLeave(handler)` | `(handler) => Unsubscribe` | Called whenever a member leaves. |\n\n### `RoomOptions`\n\n```ts\ntype RoomOptions = {\n signal?: AbortSignal;\n timeout?: number;\n};\n```\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `signal` | `AbortSignal` | Aborts the join, rejecting `joined` with `PulseAbortError`. |\n| `timeout` | `number` | Join timeout in ms. Rejects `joined` with `PulseRoomTimeoutError`. |\n\n---\n\n## Errors\n\nAll errors extend `PulseError`.\n\n### `PulseError`\n\nBase class for all Pulse errors.\n\n### `PulseConnectionError`\n\nTransport failure, send while disconnected, or room join rejected on close.\n\n### `PulseProtocolError`\n\nMalformed frame or server error frame.\n\n### `PulseTimeoutError`\n\n`wait()` timed out before the server event arrived.\n\n### `PulseRoomTimeoutError`\n\nRoom scope `joined` timed out before the server confirmed membership.\n\n### `PulseAbortError`\n\n`wait()` or room `joined` aborted via AbortSignal.\n\n### `PulseDisposedError`\n\nOperation attempted after disposal.\n\n---\n\n## Channel and room definitions\n\n### `ChannelDefinition`\n\n```ts\ntype ChannelDefinition = { client: MessageMap; server: MessageMap };\n```\n\n### `ChannelDefinitions`\n\n```ts\ntype ChannelDefinitions = Record<string, ChannelDefinition>;\n```\n\n### `RoomDefinition`\n\n```ts\ntype RoomDefinition = { presence?: unknown };\n```\n\n### `RoomDefinitions`\n\n```ts\ntype RoomDefinitions = Record<string, RoomDefinition>;\n```\n\n---\n\n## Utility types\n\n### `MessageMap`\n\n```ts\ntype MessageMap = Record<string, unknown>;\n```\n\n### `EventKey`\n\n```ts\ntype EventKey<T extends MessageMap> = keyof T & string;\n```\n\n### `ServerEvents`\n\n```ts\ntype ServerEvents<S extends PulseSchema> = S extends { server: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract server events from a schema, defaulting to an empty map.\n\n### `ClientEvents`\n\n```ts\ntype ClientEvents<S extends PulseSchema> = S extends { client: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract client events from a schema, defaulting to an empty map.\n\n### `RoomMap`\n\n```ts\ntype RoomMap<S extends PulseSchema> = S extends { rooms: infer R extends RoomDefinitions } ? R : RoomDefinitions;\n```\n\nExtract room definitions from a schema, defaulting to an empty map.\n\n### `Unsubscribe`\n\n```ts\ntype Unsubscribe = () => void;\n```\n\n### `PulseStatus`\n\n```ts\ntype PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n```\n",
6
- "usage": "---\ntitle: Usage — Pulse\ndescription: Practical guide for connecting, sending, subscribing, joining rooms, and managing lifecycle with Pulse.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## Basic Usage\n\nDeclare server events, client events, channel schemas, and room schemas once at construction. Named scopes infer their types from this schema.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n // Root events the server sends\n server: { 'chat:message': { text: string }; notice: string };\n // Root events the client sends\n client: { 'chat:send': { text: string } };\n // Named channel scopes\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n alerts: {\n client: { subscribe: { topic: string } };\n server: { alert: { topic: string; severity: 'info' | 'warn' | 'error' } };\n };\n };\n // Named room scopes with optional presence state\n rooms: {\n lobby: { presence: { name: string; color: string } };\n announcements: {};\n };\n};\n```\n\n## Create and connect\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 5 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n onError: (error) => console.error(error),\n});\n\ntry {\n await pulse.connect();\n} catch (error) {\n console.error('Connection failed:', error);\n}\n```\n\n`connect()` opens the WebSocket and resolves after session restoration completes. `send()` throws `PulseConnectionError` while disconnected — Pulse never silently drops or buffers application messages.\n\n## Send and receive root events\n\n```ts\npulse.on('chat:message', (message) => console.log(message.text));\npulse.send('chat:send', { text: 'Hello!' });\n```\n\n## Channels\n\nEach `channel()` call returns an independently disposable scope. The server subscription is reference-counted: the first scope sends `subscribe`, the last disposal sends `unsubscribe`.\n\n```ts\nconst chat = pulse.channel('chat');\n\nchat.on('message', (message) => console.log(message.text));\nchat.send('send', { text: 'Hello!' });\n\n// Later\nchat.dispose();\n```\n\nUse `using` for automatic cleanup:\n\n```ts\n{\n using chat = pulse.channel('chat');\n chat.on('message', (message) => console.log(message.text));\n} // chat.dispose() called automatically\n```\n\n## Rooms and presence\n\nEach `room()` call returns a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n```ts\nconst lobby = pulse.room('lobby');\n\n// joined resolves when the server confirms membership\nawait lobby.joined;\n\n// Reactive presence map: memberId → state\nlobby.onJoin((memberId, state) => console.log(`${memberId} joined: ${state.name}`));\nlobby.onLeave((memberId) => console.log(`${memberId} left`));\n\n// Broadcast your presence\nlobby.updatePresence({ name: 'Ada', color: 'blue' });\n\n// Read current presence\nfor (const [memberId, state] of lobby.presence.value) {\n console.log(`${memberId}: ${state.name}`);\n}\n\n// Leave\nlobby.dispose();\n```\n\nPlain rooms (without presence) work the same way but don't expose presence members:\n\n```ts\nconst announcements = pulse.room('announcements');\nawait announcements.joined;\nannouncements.dispose();\n```\n\n### Room scope options\n\n```ts\n// Timeout if the server doesn't confirm in time\nconst lobby = pulse.room('lobby', { timeout: 5_000 });\ntry {\n await lobby.joined;\n} catch (error) {\n console.error('Join failed:', error);\n}\n\n// Abort via AbortSignal\nconst ctrl = new AbortController();\nconst lobby = pulse.room('lobby', { signal: ctrl.signal });\nctrl.abort(); // joined rejects with PulseAbortError, scope auto-disposes\n```\n\n### Reactive rooms set\n\n`pulse.rooms` is a ripple readable that tracks confirmed room memberships:\n\n```ts\nimport { effect } from '@vielzeug/ripple';\n\neffect(() => {\n console.log('Joined rooms:', [...pulse.rooms.value]);\n});\n```\n\n## Reconnect\n\nWhen the connection drops unexpectedly, Pulse reconnects using the configured strategy. On reconnect, it restores:\n\n1. Channel subscriptions (sends `subscribe` for each active channel).\n2. Room memberships (sends `join` for each active room scope).\n3. Local presence state (sends `presence` with the last successfully published state).\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: {\n delay: (attempt) => Math.min(1_000 * 2 ** attempt, 30_000),\n maxAttempts: 5,\n },\n});\n```\n\n`joined` rejects on transport close. For post-reconnect membership, read `pulse.rooms` instead.\n\n## Heartbeat\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n```\n\nPulse sends periodic pings. If a pong doesn't arrive before the timeout, it forces a reconnect using the same reconnect controller.\n\n## Transform outgoing messages\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => {\n // Add a timestamp to all messages\n return { ...message, payload: { ...message.payload, ts: Date.now() } };\n },\n});\n```\n\nReturn `null` to drop a message:\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => (message.event === 'debug' ? null : message),\n});\n```\n\n## Wait for a specific event\n\n```ts\nconst notice = await pulse.wait('notice', { timeout: 10_000 });\nconsole.log(notice);\n```\n\n## Dispose\n\n```ts\npulse.dispose();\n```\n\nDisposal is idempotent. It closes the connection, rejects pending room joins, clears all listeners, and aborts all scope disposal signals.\n\n## Error handling\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n onError: (error) => {\n if (error instanceof PulseConnectionError) {\n console.error('Connection error:', error);\n } else if (error instanceof PulseProtocolError) {\n console.error('Protocol error:', error);\n }\n },\n});\n```\n\n| Error | When |\n| --- | --- |\n| `PulseConnectionError` | Transport failure, send while disconnected, room join rejected on close. |\n| `PulseProtocolError` | Malformed frame or server error frame. |\n| `PulseTimeoutError` | `wait()` times out. |\n| `PulseRoomTimeoutError` | Room scope `joined` times out. |\n| `PulseAbortError` | `wait()` or room `joined` aborted via AbortSignal. |\n| `PulseDisposedError` | Operation attempted after disposal. |\n\n## Best Practices\n\n- Await `connect()` before sending; never assume construction opens the transport.\n- Define the full schema at `createPulse()` so named scopes are type-safe without per-call generics.\n- Use `using` declarations for channel and room scopes so disposal is automatic at block exit.\n- Always call `dispose()` when done — it closes the connection, rejects pending joins, and clears listeners.\n- Provide an `onError` handler; Pulse reports transport and protocol errors there rather than throwing asynchronously.\n- Read `pulse.rooms` for post-reconnect membership; `joined` rejects on transport close.\n- Set a `timeout` on room scopes when the server may never confirm membership.\n- Keep `transform` synchronous; resolve async policy decisions before calling `send()`.\n",
4
+ "index": "---\ntitle: Pulse — Typed WebSocket sessions\ndescription: Explicitly connected, typed WebSocket sessions with scoped channels, ref-counted rooms with reactive presence, reconnect restoration, and heartbeat.\npackage: pulse\ncategory: websockets\nkeywords: [websocket, realtime, channels, presence, rooms, reconnect, heartbeat, typed-messaging, ripple]\nrelated: [herald, ripple, courier, clockwork]\nexports:\n [\n createPulse,\n Pulse,\n PulseChannel,\n RoomScope,\n RoomScopeBase,\n PresenceRoomScope,\n PulseOptions,\n PulseSchema,\n ChannelDefinition,\n ChannelDefinitions,\n RoomDefinition,\n RoomDefinitions,\n RoomOptions,\n OutgoingMessage,\n OutgoingTransform,\n PulseError,\n PulseConnectionError,\n PulseTimeoutError,\n PulseRoomTimeoutError,\n PulseAbortError,\n PulseDisposedError,\n PulseProtocolError,\n PulseEvent,\n ]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"pulse\" />\n\n## Why Pulse?\n\nNative WebSocket leaves connection ownership, event routing, reconnect restoration, and cleanup to each application. Pulse provides those boundaries while making readiness explicit: applications connect before sending, and disconnected messages never disappear silently.\n\n```ts\n// Before\nconst socket = new WebSocket('wss://api.example.com/ws');\nsocket.addEventListener('message', (event) => route(JSON.parse(event.data)));\nsocket.addEventListener('close', () => setTimeout(() => reconnect(), 1_000));\n\n// After\nconst pulse = createPulse<{ server: { 'chat:message': { text: string } }; client: { 'chat:send': { text: string } } }>(\n 'wss://api.example.com/ws',\n { reconnect: true },\n);\ntry {\n await pulse.connect();\n pulse.on('chat:message', (message) => console.log(message.text));\n pulse.send('chat:send', { text: 'Hello!' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n```\n\n| Feature | Pulse | Native WebSocket | socket.io-client |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"pulse\" type=\"size\" /> | 0 B | ~44 kB gzip |\n| Explicit readiness | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Session restoration | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Protocol-specific |\n| Typed scoped channels | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Basic |\n| Typed rooms with presence | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero runtime dependencies | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> ripple | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Pulse when** you need a typed WebSocket session whose reconnect and cleanup behavior must be deterministic.\n\n**Consider native WebSocket when** a single untyped connection does not need retry, routing, or session restoration.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/pulse @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nDefine the protocol schema at construction time, create scopes, then connect before sending.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n server: { 'chat:message': { text: string } };\n client: { 'chat:send': { text: string } };\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n };\n rooms: {\n lobby: { presence: { name: string } };\n };\n};\n\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n});\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\nconst chat = pulse.channel('chat');\nconst lobby = pulse.room('lobby');\n\ntry {\n await pulse.connect();\n chat.send('send', { text: 'Hello!' });\n await lobby.joined;\n lobby.updatePresence({ name: 'Ada' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n\npulse.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`connect()`** — explicit readiness; application messages throw while disconnected.\n- **`channel()`** — named, schema-bound scopes with independent disposal and reference-counted server subscriptions.\n- **`room()`** — named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sends `join`; the last disposal sends `leave`.\n- **`reconnect`** — ordered restoration of channel subscriptions, room memberships, and local presence state.\n- **`transform`** — one synchronous transform or filter for application messages.\n- **`tap()`** — subscribe to lifecycle events (status changes, errors, disposal) via a typed `PulseEvent` stream.\n- **`heartbeat`** — ping/pong liveness detection that uses the same reconnect controller.\n- **`status` and `rooms`** — ripple readables for transport and confirmed membership state.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — provides the reactive values exposed by Pulse.\n- [Herald](/herald/) — receives routed Pulse events in an in-process application bus.\n- [Courier](/courier/) — handles request/response traffic alongside a Pulse session.\n- [Clockwork](/clockwork/) — models application-level authentication or session workflows.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: API — Pulse\ndescription: Complete API reference for Pulse, including schema types, options, scopes, and error classes.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createPulse()` | Create a typed WebSocket session instance. | Sync (returns `Pulse`) | Does not open the connection — call `connect()`. |\n| `Pulse` | Main instance: channels, rooms, messaging, lifecycle. | Sync methods, async `connect()`/`wait()` | `send()` throws while disconnected. |\n| `PulseChannel` | Scoped channel namespace with independent disposal. | Sync methods, async `wait()` | Each call returns a new scope; ref-counted subscription. |\n| `RoomScope` | Ref-counted room membership with optional presence. | Sync methods, async `joined` | `joined` rejects on transport close or timeout. |\n| `PulseSchema` | Declares server/client events, channels, and rooms. | Type-only | Infer all named scope types from this schema. |\n| `PulseOptions` | Configuration: heartbeat, reconnect, transform. | Type-only | `reconnect` and `heartbeat` default to `false`. |\n| `PulseError` | Base class for all Pulse errors. | Runtime | Check `instanceof` against subclasses. |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/pulse` | All public exports: `createPulse`, types, and error classes. |\n\n## `createPulse()`\n\n```ts\nfunction createPulse<S extends PulseSchema = PulseSchema>(url: string, options?: PulseOptions): Pulse<S>\n```\n\nCreates a Pulse instance. The WebSocket is not opened until `connect()` is called.\n\n### Type parameters\n\n| Parameter | Constraint | Description |\n| --- | --- | --- |\n| `S` | `PulseSchema` | Schema declaring server events, client events, channels, and rooms. |\n\n### Parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `string` | WebSocket URL. |\n| `options` | `PulseOptions` | Optional configuration. |\n\n### Returns\n\n`Pulse<S>` — the Pulse instance.\n\n---\n\n## `PulseSchema`\n\n```ts\ntype PulseSchema = {\n server?: MessageMap;\n client?: MessageMap;\n channels?: ChannelDefinitions;\n rooms?: RoomDefinitions;\n};\n```\n\nDeclare all protocol surfaces once at construction. Named scopes infer their types from this schema.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `server` | `MessageMap` | Root events the server sends. |\n| `client` | `MessageMap` | Root events the client sends. |\n| `channels` | `ChannelDefinitions` | Named channel schemas. |\n| `rooms` | `RoomDefinitions` | Named room schemas with optional presence. |\n\n---\n\n## `PulseOptions`\n\n```ts\ntype PulseOptions = {\n heartbeat?: boolean | HeartbeatOptions;\n protocols?: string | string[];\n reconnect?: boolean | ReconnectOptions;\n transform?: OutgoingTransform;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `heartbeat` | `boolean \\| HeartbeatOptions` | `false` | Ping/pong keep-alive. |\n| `protocols` | `string \\| string[]` | — | Sub-protocols passed to the WebSocket constructor. |\n| `reconnect` | `boolean \\| ReconnectOptions` | `false` | Auto-reconnect on unexpected close. |\n| `transform` | `OutgoingTransform` | — | Transform or filter outgoing application messages. |\n\n---\n\n## `HeartbeatOptions`\n\n```ts\ntype HeartbeatOptions = {\n interval?: number;\n timeout?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `interval` | `number` | `30_000` | Interval between pings in ms. |\n| `timeout` | `number` | `5_000` | How long to wait for a pong before treating the connection as dead. |\n\n---\n\n## `ReconnectOptions`\n\n```ts\ntype ReconnectOptions = {\n delay?: number | ((attempt: number) => number);\n maxAttempts?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `delay` | `number \\| ((attempt: number) => number)` | Full-jitter exponential backoff capped at 30 s | Delay between reconnect attempts in ms. `attempt` is zero-based. |\n| `maxAttempts` | `number` | `5` | Maximum number of reconnect attempts after initial failure. |\n\n---\n\n## `OutgoingMessage`\n\n```ts\ntype OutgoingMessage = { channel?: string; event: string; payload: unknown };\n```\n\nAn outgoing application message before it is serialized.\n\n---\n\n## `OutgoingTransform`\n\n```ts\ntype OutgoingTransform = (message: Readonly<OutgoingMessage>) => OutgoingMessage | null;\n```\n\nTransform or filter outgoing application messages. Internal protocol frames (subscribe, join, leave, presence, ping) bypass this hook. Return `null` to drop the message.\n\n---\n\n## `Pulse`\n\n```ts\ntype Pulse<S extends PulseSchema = PulseSchema> = {\n // Channels\n channel<K extends keyof ChannelMap<S> & string>(\n name: K,\n ): PulseChannel<ChannelMap<S>[K]['server'], ChannelMap<S>[K]['client']>;\n\n // Connection\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n\n // Lifecycle\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n\n // Messaging\n on<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n once<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n send<K extends EventKey<ClientEvents<S>>>(event: K, payload: ClientEvents<S>[K]): void;\n wait<K extends EventKey<ServerEvents<S>>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<ServerEvents<S>[K]>;\n\n // Rooms\n room<K extends keyof RoomMap<S> & string>(name: K, opts?: RoomOptions): RoomScope<RoomMap<S>[K]>;\n readonly rooms: Readable<ReadonlySet<string>>;\n\n // Status\n readonly status: Readable<PulseStatus>;\n\n // Tap\n tap(handler: (event: PulseEvent) => void, options?: { signal?: AbortSignal }): () => void;\n\n [Symbol.dispose](): void;\n};\n```\n\n### `channel(name)`\n\nCreates an isolated message namespace over the shared connection. Each call returns an independently disposable scope. The server subscription is reference-counted.\n\n### `connect()`\n\nExplicitly opens the connection. Resolves after session restoration completes. Rejects if the connection closes before opening.\n\n### `disconnect(code?, reason?)`\n\nCloses the connection without triggering reconnection. Default code is `1000`.\n\n### `dispose()`\n\nPermanently closes the connection and releases all resources. Idempotent.\n\n### `on(event, handler)`\n\nSubscribes to a typed server event. Returns an unsubscribe function.\n\n### `once(event, handler)`\n\nSubscribes once — auto-removes after first invocation.\n\n### `send(event, payload)`\n\nSends a typed event to the server. Throws `PulseConnectionError` unless the connection is open.\n\n### `wait(event, opts?)`\n\nResolves on the next emission of the given server event. Rejects when `opts.signal` aborts, the timeout elapses, or the instance is disposed.\n\n### `room(name, opts?)`\n\nCreates a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n### `rooms`\n\nReactive set of rooms the client is currently a confirmed member of.\n\n### `status`\n\nReactive connection status: `'connecting' | 'open' | 'reconnecting' | 'closed'`.\n\n### `tap(handler, options?)`\n\nSubscribes to lifecycle events emitted by the Pulse instance. The handler receives a discriminated-union `PulseEvent`. Returns an unsubscribe function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `handler` | `(event: PulseEvent) => void` | Called for each lifecycle event. |\n| `options.signal` | `AbortSignal` | Optional signal to stop the subscription. |\n\n```ts\nconst pulse = createPulse(url, { reconnect: true });\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\n```\n\n---\n\n## `PulseEvent`\n\n```ts\ntype PulseEvent =\n | { type: 'status-change'; status: PulseStatus }\n | { type: 'error'; error: PulseError }\n | { type: 'dispose' };\n```\n\nA discriminated union of lifecycle events emitted by a `Pulse` instance. Inspect `event.type` to narrow the payload.\n\n| `type` | Payload | When |\n| --- | --- | --- |\n| `status-change` | `status: PulseStatus` | The connection status transitions. |\n| `error` | `error: PulseError` | A typed transport or protocol error occurs. |\n| `dispose` | — | The instance is disposed. |\n\n---\n\n## `PulseChannel`\n\n```ts\ntype PulseChannel<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap> = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n dispose(): void;\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;\n wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;\n [Symbol.dispose](): void;\n};\n```\n\n---\n\n## `RoomScope`\n\n```ts\ntype RoomScope<R extends RoomDefinition = RoomDefinition> = R extends { presence: infer P }\n ? P extends undefined\n ? RoomScopeBase\n : PresenceRoomScope<P>\n : RoomScopeBase;\n```\n\nA room scope. When the room definition includes `presence`, the scope is a `PresenceRoomScope`; otherwise it is a `RoomScopeBase`.\n\n### `RoomScopeBase`\n\n```ts\ntype RoomScopeBase = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n readonly joined: Promise<void>;\n dispose(): void;\n [Symbol.dispose](): void;\n};\n```\n\n### `PresenceRoomScope`\n\n```ts\ntype PresenceRoomScope<T = unknown> = RoomScopeBase & {\n readonly presence: Readable<ReadonlyMap<string, T>>;\n updatePresence(state: T): void;\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe;\n onLeave(handler: (memberId: string) => void): Unsubscribe;\n};\n```\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `presence` | `Readable<ReadonlyMap<string, T>>` | Reactive map of `memberId → state`. |\n| `updatePresence(state)` | `(state: T) => void` | Broadcast this client's presence state. Throws `PulseConnectionError` unless open. |\n| `onJoin(handler)` | `(handler) => Unsubscribe` | Called whenever a new member joins with their initial state. |\n| `onLeave(handler)` | `(handler) => Unsubscribe` | Called whenever a member leaves. |\n\n### `RoomOptions`\n\n```ts\ntype RoomOptions = {\n signal?: AbortSignal;\n timeout?: number;\n};\n```\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `signal` | `AbortSignal` | Aborts the join, rejecting `joined` with `PulseAbortError`. |\n| `timeout` | `number` | Join timeout in ms. Rejects `joined` with `PulseRoomTimeoutError`. |\n\n---\n\n## Errors\n\nAll errors extend `PulseError`.\n\n### `PulseError`\n\nBase class for all Pulse errors.\n\n### `PulseConnectionError`\n\nTransport failure, send while disconnected, or room join rejected on close.\n\n### `PulseProtocolError`\n\nMalformed frame or server error frame.\n\n### `PulseTimeoutError`\n\n`wait()` timed out before the server event arrived.\n\n### `PulseRoomTimeoutError`\n\nRoom scope `joined` timed out before the server confirmed membership.\n\n### `PulseAbortError`\n\n`wait()` or room `joined` aborted via AbortSignal.\n\n### `PulseDisposedError`\n\nOperation attempted after disposal.\n\n---\n\n## Channel and room definitions\n\n### `ChannelDefinition`\n\n```ts\ntype ChannelDefinition = { client: MessageMap; server: MessageMap };\n```\n\n### `ChannelDefinitions`\n\n```ts\ntype ChannelDefinitions = Record<string, ChannelDefinition>;\n```\n\n### `RoomDefinition`\n\n```ts\ntype RoomDefinition = { presence?: unknown };\n```\n\n### `RoomDefinitions`\n\n```ts\ntype RoomDefinitions = Record<string, RoomDefinition>;\n```\n\n---\n\n## Utility types\n\n### `MessageMap`\n\n```ts\ntype MessageMap = Record<string, unknown>;\n```\n\n### `EventKey`\n\n```ts\ntype EventKey<T extends MessageMap> = keyof T & string;\n```\n\n### `ServerEvents`\n\n```ts\ntype ServerEvents<S extends PulseSchema> = S extends { server: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract server events from a schema, defaulting to an empty map.\n\n### `ClientEvents`\n\n```ts\ntype ClientEvents<S extends PulseSchema> = S extends { client: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract client events from a schema, defaulting to an empty map.\n\n### `RoomMap`\n\n```ts\ntype RoomMap<S extends PulseSchema> = S extends { rooms: infer R extends RoomDefinitions } ? R : RoomDefinitions;\n```\n\nExtract room definitions from a schema, defaulting to an empty map.\n\n### `Unsubscribe`\n\n```ts\ntype Unsubscribe = () => void;\n```\n\n### `PulseStatus`\n\n```ts\ntype PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n```\n",
6
+ "usage": "---\ntitle: Usage — Pulse\ndescription: Practical guide for connecting, sending, subscribing, joining rooms, and managing lifecycle with Pulse.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## Basic Usage\n\nDeclare server events, client events, channel schemas, and room schemas once at construction. Named scopes infer their types from this schema.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n // Root events the server sends\n server: { 'chat:message': { text: string }; notice: string };\n // Root events the client sends\n client: { 'chat:send': { text: string } };\n // Named channel scopes\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n alerts: {\n client: { subscribe: { topic: string } };\n server: { alert: { topic: string; severity: 'info' | 'warn' | 'error' } };\n };\n };\n // Named room scopes with optional presence state\n rooms: {\n lobby: { presence: { name: string; color: string } };\n announcements: {};\n };\n};\n```\n\n## Create and connect\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 5 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\n\ntry {\n await pulse.connect();\n} catch (error) {\n console.error('Connection failed:', error);\n}\n```\n\n`connect()` opens the WebSocket and resolves after session restoration completes. `send()` throws `PulseConnectionError` while disconnected — Pulse never silently drops or buffers application messages.\n\n## Send and receive root events\n\n```ts\npulse.on('chat:message', (message) => console.log(message.text));\npulse.send('chat:send', { text: 'Hello!' });\n```\n\n## Channels\n\nEach `channel()` call returns an independently disposable scope. The server subscription is reference-counted: the first scope sends `subscribe`, the last disposal sends `unsubscribe`.\n\n```ts\nconst chat = pulse.channel('chat');\n\nchat.on('message', (message) => console.log(message.text));\nchat.send('send', { text: 'Hello!' });\n\n// Later\nchat.dispose();\n```\n\nUse `using` for automatic cleanup:\n\n```ts\n{\n using chat = pulse.channel('chat');\n chat.on('message', (message) => console.log(message.text));\n} // chat.dispose() called automatically\n```\n\n## Rooms and presence\n\nEach `room()` call returns a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n```ts\nconst lobby = pulse.room('lobby');\n\n// joined resolves when the server confirms membership\nawait lobby.joined;\n\n// Reactive presence map: memberId → state\nlobby.onJoin((memberId, state) => console.log(`${memberId} joined: ${state.name}`));\nlobby.onLeave((memberId) => console.log(`${memberId} left`));\n\n// Broadcast your presence\nlobby.updatePresence({ name: 'Ada', color: 'blue' });\n\n// Read current presence\nfor (const [memberId, state] of lobby.presence.value) {\n console.log(`${memberId}: ${state.name}`);\n}\n\n// Leave\nlobby.dispose();\n```\n\nPlain rooms (without presence) work the same way but don't expose presence members:\n\n```ts\nconst announcements = pulse.room('announcements');\nawait announcements.joined;\nannouncements.dispose();\n```\n\n### Room scope options\n\n```ts\n// Timeout if the server doesn't confirm in time\nconst lobby = pulse.room('lobby', { timeout: 5_000 });\ntry {\n await lobby.joined;\n} catch (error) {\n console.error('Join failed:', error);\n}\n\n// Abort via AbortSignal\nconst ctrl = new AbortController();\nconst lobby = pulse.room('lobby', { signal: ctrl.signal });\nctrl.abort(); // joined rejects with PulseAbortError, scope auto-disposes\n```\n\n### Reactive rooms set\n\n`pulse.rooms` is a ripple readable that tracks confirmed room memberships:\n\n```ts\nimport { effect } from '@vielzeug/ripple';\n\neffect(() => {\n console.log('Joined rooms:', [...pulse.rooms.value]);\n});\n```\n\n## Reconnect\n\nWhen the connection drops unexpectedly, Pulse reconnects using the configured strategy. On reconnect, it restores:\n\n1. Channel subscriptions (sends `subscribe` for each active channel).\n2. Room memberships (sends `join` for each active room scope).\n3. Local presence state (sends `presence` with the last successfully published state).\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: {\n delay: (attempt) => Math.min(1_000 * 2 ** attempt, 30_000),\n maxAttempts: 5,\n },\n});\n```\n\n`joined` rejects on transport close. For post-reconnect membership, read `pulse.rooms` instead.\n\n## Heartbeat\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n```\n\nPulse sends periodic pings. If a pong doesn't arrive before the timeout, it forces a reconnect using the same reconnect controller.\n\n## Transform outgoing messages\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => {\n // Add a timestamp to all messages\n return { ...message, payload: { ...message.payload, ts: Date.now() } };\n },\n});\n```\n\nReturn `null` to drop a message:\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => (message.event === 'debug' ? null : message),\n});\n```\n\n## Wait for a specific event\n\n```ts\nconst notice = await pulse.wait('notice', { timeout: 10_000 });\nconsole.log(notice);\n```\n\n## Dispose\n\n```ts\npulse.dispose();\n```\n\nDisposal is idempotent. It closes the connection, rejects pending room joins, clears all listeners, and aborts all scope disposal signals.\n\n## Error handling\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n});\n\npulse.tap((event) => {\n if (event.type === 'error') {\n if (event.error instanceof PulseConnectionError) {\n console.error('Connection error:', event.error);\n } else if (event.error instanceof PulseProtocolError) {\n console.error('Protocol error:', event.error);\n }\n }\n});\n```\n\n| Error | When |\n| --- | --- |\n| `PulseConnectionError` | Transport failure, send while disconnected, room join rejected on close. |\n| `PulseProtocolError` | Malformed frame or server error frame. |\n| `PulseTimeoutError` | `wait()` times out. |\n| `PulseRoomTimeoutError` | Room scope `joined` times out. |\n| `PulseAbortError` | `wait()` or room `joined` aborted via AbortSignal. |\n| `PulseDisposedError` | Operation attempted after disposal. |\n\n## Best Practices\n\n- Await `connect()` before sending; never assume construction opens the transport.\n- Define the full schema at `createPulse()` so named scopes are type-safe without per-call generics.\n- Use `using` declarations for channel and room scopes so disposal is automatic at block exit.\n- Always call `dispose()` when done — it closes the connection, rejects pending joins, and clears listeners.\n- Call `tap()` to observe lifecycle events; Pulse reports transport and protocol errors there rather than throwing asynchronously.\n- Read `pulse.rooms` for post-reconnect membership; `joined` rejects on transport close.\n- Set a `timeout` on room scopes when the server may never confirm membership.\n- Keep `transform` synchronous; resolve async policy decisions before calling `send()`.\n",
7
7
  "examples": "---\ntitle: Examples — Pulse\ndescription: Practical examples for common Pulse usage patterns.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n- [Basic Connection](./examples/basic-connection.md)\n- [Channel Multiplexing](./examples/channels.md)\n- [Outgoing Transform](./examples/middleware.md)\n- [Reconnect and Heartbeat](./examples/reconnect-and-heartbeat.md)\n- [Rooms and Presence](./examples/rooms-and-presence.md)\n"
8
8
  },
9
9
  "examples": [
@@ -14,17 +14,17 @@
14
14
  },
15
15
  {
16
16
  "id": "connect-and-send",
17
- "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Typed WebSocket client: on(), once(), send(), wait()\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5 },\n onError: (error) => console.log('transport error:', error.message),\n})\n\n// Subscribe before connecting — listeners are synchronous\nconst unsub = pulse.on('chat:message', ({ from, text }) => {\n console.log('[' + from + '] ' + text)\n})\n\n// One-shot listener: fires once and auto-removes\npulse.once('chat:message', (msg) => {\n console.log('first message:', msg.text)\n})\n\n// Connect; send when open\ntry {\n await pulse.connect()\n pulse.send('chat:send', { text: 'Hello, world!' })\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// Await next server event with a 5 s deadline\ntry {\n const msg = await pulse.wait('chat:message', { timeout: 500 })\n console.log('received:', msg.text)\n} catch (err) {\n console.log('wait ended:', err.message)\n}\n\nunsub()\npulse.dispose()",
17
+ "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Typed WebSocket client: on(), once(), send(), wait()\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5 },\n})\n\n// Observe runtime events via tap()\npulse.tap((event) => {\n if (event.type === 'error') console.log('transport error:', event.error.message)\n})\n\n// Subscribe before connecting — listeners are synchronous\nconst unsub = pulse.on('chat:message', ({ from, text }) => {\n console.log('[' + from + '] ' + text)\n})\n\n// One-shot listener: fires once and auto-removes\npulse.once('chat:message', (msg) => {\n console.log('first message:', msg.text)\n})\n\n// Connect; send when open\ntry {\n await pulse.connect()\n pulse.send('chat:send', { text: 'Hello, world!' })\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// Await next server event with a 5 s deadline\ntry {\n const msg = await pulse.wait('chat:message', { timeout: 500 })\n console.log('received:', msg.text)\n} catch (err) {\n console.log('wait ended:', err.message)\n}\n\nunsub()\npulse.dispose()",
18
18
  "name": "Connect & Send"
19
19
  },
20
20
  {
21
21
  "id": "lifecycle",
22
- "code": "import { createPulse, PulseDisposedError } from '@vielzeug/pulse'\n\n// Status signal, disposalSignal, and error handling on dispose\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 3 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n onError: (error) => console.log('Pulse error:', error.message),\n})\n\n// Construction is closed. connect() makes the transport available.\nconsole.log('initial status:', pulse.status.value)\n\n// disposalSignal aborts when dispose() is called\npulse.disposalSignal.addEventListener('abort', () => {\n console.log('disposal signal fired')\n})\n\ntry {\n await pulse.connect()\n console.log('connected:', pulse.status.value)\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// dispose() is idempotent — safe to call multiple times\npulse.dispose()\npulse.dispose()\nconsole.log('disposed:', pulse.disposed)\n\n// Methods reject with PulseDisposedError after dispose\ntry {\n await pulse.connect()\n} catch (err) {\n if (err instanceof PulseDisposedError) {\n console.log('connect() rejected with PulseDisposedError — correct')\n }\n}",
22
+ "code": "import { createPulse, PulseDisposedError } from '@vielzeug/pulse'\n\n// Status signal, disposalSignal, and error handling on dispose\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 3 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n})\n\n// Observe runtime events via tap()\npulse.tap((event) => {\n if (event.type === 'error') console.log('Pulse error:', event.error.message)\n})\n\n// Construction is closed. connect() makes the transport available.\nconsole.log('initial status:', pulse.status.value)\n\n// disposalSignal aborts when dispose() is called\npulse.disposalSignal.addEventListener('abort', () => {\n console.log('disposal signal fired')\n})\n\ntry {\n await pulse.connect()\n console.log('connected:', pulse.status.value)\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// dispose() is idempotent — safe to call multiple times\npulse.dispose()\npulse.dispose()\nconsole.log('disposed:', pulse.disposed)\n\n// Methods reject with PulseDisposedError after dispose\ntry {\n await pulse.connect()\n} catch (err) {\n if (err instanceof PulseDisposedError) {\n console.log('connect() rejected with PulseDisposedError — correct')\n }\n}",
23
23
  "name": "Lifecycle & Disposal"
24
24
  },
25
25
  {
26
26
  "id": "reconnect",
27
- "code": "import { createPulse, PulseConnectionError } from '@vielzeug/pulse'\n\n// Channels, rooms, and local presence state are restored on reconnect.\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 500, maxAttempts: 3 },\n onError: (error) => console.log('transport error:', error.message),\n})\n\n// Channel is tracked: re-subscribed automatically after every reconnect\nconst chat = pulse.channel('chat')\nchat.on('message', ({ from, text }) => console.log(from + ': ' + text))\n\n// Connect explicitly to observe the status\ntry {\n await pulse.connect()\n console.log('connected, status:', pulse.status.value)\n} catch (err) {\n if (err instanceof PulseConnectionError) {\n console.log('connection failed:', err.message)\n }\n}\n\nconsole.log('channel name:', chat.name)\nconsole.log('channel disposed?', chat.disposed)\n\n// Disposing a channel removes it from re-subscription tracking\nchat.dispose()\nconsole.log('channel disposed, pulse still running:', !pulse.disposed)\n\npulse.dispose()",
27
+ "code": "import { createPulse, PulseConnectionError } from '@vielzeug/pulse'\n\n// Channels, rooms, and local presence state are restored on reconnect.\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 500, maxAttempts: 3 },\n})\n\npulse.tap((event) => {\n if (event.type === 'error') console.log('transport error:', event.error.message)\n})\n\n// Channel is tracked: re-subscribed automatically after every reconnect\nconst chat = pulse.channel('chat')\nchat.on('message', ({ from, text }) => console.log(from + ': ' + text))\n\n// Connect explicitly to observe the status\ntry {\n await pulse.connect()\n console.log('connected, status:', pulse.status.value)\n} catch (err) {\n if (err instanceof PulseConnectionError) {\n console.log('connection failed:', err.message)\n }\n}\n\nconsole.log('channel name:', chat.name)\nconsole.log('channel disposed?', chat.disposed)\n\n// Disposing a channel removes it from re-subscription tracking\nchat.dispose()\nconsole.log('channel disposed, pulse still running:', !pulse.disposed)\n\npulse.dispose()",
28
28
  "name": "Reconnect & Restoration"
29
29
  },
30
30
  {
@@ -42,28 +42,29 @@
42
42
  "PulseRoomTimeoutError": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseRoomTimeoutError,\n PulseTimeoutError,\n} from './errors';",
43
43
  "PulseTimeoutError": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseRoomTimeoutError,\n PulseTimeoutError,\n} from './errors';",
44
44
  "createPulse": "export { createPulse } from './pulse';",
45
- "ChannelDefinition": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
46
- "ChannelDefinitions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
47
- "ClientEvents": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
48
- "EventKey": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
49
- "HeartbeatOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
50
- "MessageMap": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
51
- "OutgoingMessage": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
52
- "OutgoingTransform": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
53
- "PresenceRoomScope": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
54
- "Pulse": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
55
- "PulseChannel": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
56
- "PulseOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
57
- "PulseSchema": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
58
- "PulseStatus": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
59
- "ReconnectOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
60
- "RoomDefinition": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
61
- "RoomDefinitions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
62
- "RoomMap": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
63
- "RoomOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
64
- "RoomScope": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
65
- "RoomScopeBase": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
66
- "ServerEvents": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
67
- "Unsubscribe": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';"
45
+ "ChannelDefinition": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
46
+ "ChannelDefinitions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
47
+ "ClientEvents": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
48
+ "EventKey": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
49
+ "HeartbeatOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
50
+ "MessageMap": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
51
+ "OutgoingMessage": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
52
+ "OutgoingTransform": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
53
+ "PresenceRoomScope": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
54
+ "Pulse": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
55
+ "PulseChannel": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
56
+ "PulseEvent": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
57
+ "PulseOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
58
+ "PulseSchema": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
59
+ "PulseStatus": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
60
+ "ReconnectOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
61
+ "RoomDefinition": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
62
+ "RoomDefinitions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
63
+ "RoomMap": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
64
+ "RoomOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
65
+ "RoomScope": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
66
+ "RoomScopeBase": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
67
+ "ServerEvents": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';",
68
+ "Unsubscribe": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Unsubscribe,\n} from './types';"
68
69
  }
69
70
  }