@vielzeug/codex 2.0.0 → 2.0.2
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.
- package/data/catalog.json +139 -130
- package/data/llms-full.txt +13091 -17593
- package/data/llms.txt +12 -11
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +1 -1
- package/data/packages/assay.json +1 -1
- package/data/packages/clockwork.json +2 -2
- package/data/packages/codex.json +1 -1
- package/data/packages/coins.json +1 -1
- package/data/packages/conduit.json +1 -1
- package/data/packages/courier.json +1 -1
- package/data/packages/dnd.json +14 -12
- package/data/packages/familiar.json +26 -16
- package/data/packages/flux.json +1 -1
- package/data/packages/forge.json +1 -1
- package/data/packages/herald.json +19 -33
- package/data/packages/keymap.json +13 -19
- package/data/packages/ledger.json +28 -25
- package/data/packages/lingua.json +30 -28
- package/data/packages/necromancer.json +50 -0
- package/data/packages/orbit.json +34 -39
- package/data/packages/ore.json +1 -1
- package/data/packages/prism.json +37 -40
- package/data/packages/pulse.json +26 -24
- package/data/packages/refine.json +1 -1
- package/data/packages/ripple.json +1 -1
- package/data/packages/rune.json +6 -7
- package/data/packages/sandbox.json +7 -6
- package/data/packages/scout.json +10 -10
- package/data/packages/scroll.json +18 -17
- package/data/packages/sourcerer.json +1 -1
- package/data/packages/spell.json +1 -1
- package/data/packages/tempo.json +49 -81
- package/data/packages/vault.json +37 -40
- package/data/packages/ward.json +5 -17
- package/data/packages/wayfinder.json +9 -9
- package/data/refine.json +4914 -4914
- package/data/search.json +210 -211
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/http.js +46 -6
- package/dist/http.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/index.js +13 -5
- package/dist/tools/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export * from './worker';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Familiar — Typed
|
|
5
|
-
"api": "---\ntitle: Familiar — API Reference\ndescription: Complete API reference for @vielzeug/familiar.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ------------------------------------------------ | -------------- | ----------------------------------------------- |\n| `createWorker()` | Create an inline worker or pool | Sync | Task functions must be entirely self-contained |\n| `createModuleWorker()` | Create a pool from a real module-worker file | Sync | Worker file must implement the message protocol |\n| `worker.run()` | Execute a task in a Worker | Async | Pass transferables for large buffers |\n| `worker.runStream()` | Execute a streaming task, yield partial results | Async iterator | Requires a free slot — cannot be queued |\n| `worker.batch()` | Run multiple inputs, yield results | Async iterator | Cancels remaining tasks on first failure |\n| `worker.group()` | Submit related tasks that share an abort + drain | Sync | `drain()` only waits for tasks added so far |\n| `createTestWorker()` | Run tasks in-process for tests | Async | Does not enforce serialization constraints |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| `@vielzeug/familiar` | All public exports and types |\n| `@vielzeug/familiar/testing` | Test utilities (not in main bundle) |\n| `@vielzeug/familiar/protocol` | `handleMessages()`, `handleStreamMessages()` helpers + `PROTOCOL_VERSION` constant for module worker implementations |\n\n## Package Exports\n\n```ts\n// Main entry\nexport {\n createWorker,\n createModuleWorker,\n task,\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from '@vielzeug/familiar';\n\nexport type {\n BatchOptions,\n GroupOptions,\n RunOptions,\n TaskFn,\n TaskGroup,\n WorkerHandle,\n WorkerOptions,\n WorkerStatus,\n} from '@vielzeug/familiar';\n\n// Test utilities\nexport { createTestWorker } from '@vielzeug/familiar/testing';\nexport type { TestWorkerHandle, TestWorkerOptions } from '@vielzeug/familiar/testing';\n\n// Protocol helpers (module worker implementations only)\nexport { handleMessages, handleStreamMessages, PROTOCOL_VERSION } from '@vielzeug/familiar/protocol';\n```\n\n## Types\n\n### `TaskFn`\n\n```ts\ntype TaskFn<TInput, TOutput> = (input: TInput) => TOutput | Promise<TOutput>;\n```\n\nThe signature for the task function passed to `createWorker`. Accepts a single typed input and returns a value or a Promise.\n\n::: warning Self-contained functions only\nThe function is serialized via `.toString()` and runs in an isolated Worker scope. It cannot reference variables, imports, or helpers from the outer module.\n:::\n\n---\n\n### `WorkerStatus`\n\n```ts\ntype WorkerStatus = 'idle' | 'running' | 'terminated';\n```\n\n| Value | Meaning |\n| -------------- | -------------------------------------- |\n| `'idle'` | All worker slots are free |\n| `'running'` | One or more slots are executing a task |\n| `'terminated'` | `dispose()` was called |\n\n---\n\n### `WorkerOptions`\n\n```ts\ntype WorkerOptions = {\n concurrency?: number | 'auto';\n heartbeatWindow?: number;\n maxQueue?: number;\n onFull?: 'reject' | 'wait';\n onSlotError?: (error: FamiliarRuntimeError, restart: () => void) => void;\n timeout?: number;\n};\n```\n\n| Field | Type | Default | Description |\n| ----------------- | -------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `concurrency` | `number \\| 'auto'` | `1` | Worker slot count (1–512). `'auto'` uses `navigator.hardwareConcurrency`. Throws `FamiliarInvalidOptionsError` if out of range. |\n| `heartbeatWindow` | `number` | — | Watchdog window in ms applied to every task in the pool. If no heartbeat arrives within this window, the task is killed with `FamiliarTimeoutError`. Inline workers send heartbeats automatically at `heartbeatWindow / 2` intervals. |\n| `maxQueue` | `number` | unlimited | Maximum queued tasks. Exceeding this rejects with `FamiliarQueueFullError` (or suspends when `onFull='wait'`). |\n| `onFull` | `'reject' \\| 'wait'` | `'reject'` | Behavior when queue is full. `'wait'` suspends the caller until a slot opens (natural backpressure). |\n| `onSlotError` | `(error, restart) => void` | — | Called when a Worker slot encounters an unhandled runtime error. `restart()` pre-warms a replacement Worker. |\n| `timeout` | `number` | — | Pool-level task timeout in milliseconds. Can be overridden per-run via `RunOptions.timeout`. |\n\n---\n\n### `RunOptions`\n\n```ts\ntype RunOptions = {\n priority?: number;\n signal?: AbortSignal;\n timeout?: number;\n transferables?: Transferable[];\n};\n```\n\n| Field | Type | Default | Description |\n| --------------- | ---------------- | ------- | -------------------------------------------------------------------------------------------- |\n| `priority` | `number` | `0` | Scheduling priority. Higher values run first when tasks queue up. Equal priorities are FIFO. |\n| `signal` | `AbortSignal` | — | Cancel a queued task before it starts. In-flight tasks cannot be interrupted. |\n| `timeout` | `number` | — | Per-run timeout in ms. Overrides `WorkerOptions.timeout` for this task. |\n| `transferables` | `Transferable[]` | `[]` | Objects to move (not copy) to the Worker thread. |\n\n::: tip Heartbeat window is pool-level\n`heartbeatWindow` is set on `WorkerOptions`, not per-run. All tasks in the pool share the same heartbeat watchdog window.\n:::\n\n---\n\n### `BatchOptions`\n\n```ts\ntype BatchOptions = Omit<RunOptions, 'signal'> & {\n ordered?: boolean;\n};\n```\n\nExtends `RunOptions` (minus `signal`) with:\n\n- `ordered`: `boolean`, default `true`. When `false`, results are yielded as each task completes (unordered, maximum throughput).\n\n---\n\n### `WorkerHandle`\n\n`WorkerHandle` is a flat interface — all capabilities on one type, no need to reference sub-types:\n\n```ts\ninterface WorkerHandle<TInput, TOutput> {\n // Lifecycle\n drain(timeoutMs?: number): Promise<void>;\n dispose(): void;\n prime(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n readonly status: WorkerStatus;\n [Symbol.dispose](): void;\n [Symbol.asyncDispose](): Promise<void>;\n\n // Metrics\n readonly active: number;\n readonly completed: number;\n readonly concurrency: number;\n readonly failed: number;\n readonly groupCount: number;\n readonly queued: number;\n\n // Execution\n run(input: TInput, options?: RunOptions): Promise<TOutput>;\n runStream(input: TInput, options?: Omit<RunOptions, 'signal'>): AsyncIterable<TOutput>;\n batch(inputs: TInput[], options?: BatchOptions): AsyncIterable<TOutput>;\n group(name?: string, options?: GroupOptions): TaskGroup<TInput, TOutput>;\n}\n```\n\n---\n\n### `GroupOptions`\n\n```ts\ntype GroupOptions = {\n signal?: AbortSignal;\n};\n```\n\n| Field | Type | Description |\n| -------- | ------------- | ------------------------------------------------------------------------------------------------ |\n| `signal` | `AbortSignal` | When aborted, the group is aborted automatically. Composable with `WorkerHandle.disposalSignal`. |\n\n---\n\n### `TaskGroup`\n\n```ts\ntype TaskGroup<TInput, TOutput> = {\n abort(reason?: unknown): void;\n drain(): Promise<PromiseSettledResult<TOutput>[]>;\n run(input: TInput, options?: Omit<RunOptions, 'signal'>): Promise<TOutput>;\n readonly name: string | undefined;\n readonly pending: number;\n readonly size: number;\n};\n```\n\nReturned by `worker.group()`. See [`group()`](#group) below.\n\n| Member | Description |\n| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |\n| `abort` | Cancels all pending tasks. In-flight tasks run to completion. |\n| `drain` | Resolves with `PromiseSettledResult[]` for every task submitted so far. Also closes the group (decrements `groupCount`). |\n| `name` | Optional name passed to `group(name)`, useful for logging and debugging. |\n| `pending` | Tasks not yet settled — decrements as tasks complete. |\n| `run` | Submits a task associated with this group. Throws `FamiliarTerminatedError` if the pool has been disposed or is draining (same as `worker.run()`). |\n| `size` | Total tasks ever submitted to this group (never decrements). |\n\n---\n\n### `task(fn)` — optional validator\n\n```ts\nfunction task<TInput, TOutput>(fn: TaskFn<TInput, TOutput>): TaskFn<TInput, TOutput>;\n```\n\nOptional helper that validates `fn` is safe to serialize before passing to `createWorker`. `createWorker` accepts any `TaskFn` directly — `task()` exists only to catch the common mistake of passing a bound or native function.\n\nThrows `FamiliarInvalidOptionsError` if `fn` is a bound or native function.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\n// task() is optional — both forms are equivalent:\nconst worker1 = createWorker((n: number) => n * 2);\nconst worker2 = createWorker(task((n: number) => n * 2)); // validates fn is not native/bound\n\n// Catches mistakes at construction time:\ncreateWorker(task(Math.sqrt)); // throws FamiliarInvalidOptionsError\n```\n\n## createWorker\n\n```ts\nfunction createWorker<TInput, TOutput>(\n fn: TaskFn<TInput, TOutput>,\n options?: WorkerOptions,\n): WorkerHandle<TInput, TOutput>;\n```\n\nCreates a typed worker or pool that executes `fn` in a Web Worker. `fn` is serialized via `.toString()` and runs in an isolated scope — it cannot close over module-level variables.\n\nSafe to call in any runtime — errors from Worker unavailability surface on the first `run()` call.\n\n### Parameters\n\n| Parameter | Type | Description |\n| --------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| `fn` | `TaskFn<TInput, TOutput>` | Task function. Serialized via `.toString()`, runs in an isolated scope. Use `task()` to validate it is not native/bound. |\n| `options` | `WorkerOptions` | Optional pool configuration. |\n\nReturns `WorkerHandle<TInput, TOutput>`.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\n// Single worker (concurrency=1) — pass fn directly:\nconst worker = createWorker((text: string) => text.toUpperCase());\n\n// Pool of 4 with a 3 s timeout:\nconst pool = createWorker((n: number) => n ** 2, { concurrency: 4, timeout: 3000 });\n\n// CPU-count concurrency:\nconst autoPool = createWorker((n: number) => n * 2, { concurrency: 'auto' });\n```\n\n## createModuleWorker\n\n```ts\nfunction createModuleWorker<TInput, TOutput>(url: URL | string, options?: WorkerOptions): WorkerHandle<TInput, TOutput>;\n```\n\nCreates a pool where each slot is a `{ type: 'module' }` Web Worker loaded from `url`. The Worker file is a normal ES module — it can use imports, top-level await, and module-scope helpers.\n\n### Parameters\n\n| Parameter | Type | Description |\n| --------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `url` | `URL \\| string` | URL of the worker module. Use `new URL('./my-worker.ts', import.meta.url)` in bundlers. |\n| `options` | `WorkerOptions` | Optional pool configuration (same as `createWorker`). Note: `heartbeatWindow` is validated but has no effect on module workers (they must implement heartbeat manually). A dev-mode warning is emitted if it is set. |\n\n### Worker File Protocol\n\nThe module must handle `postMessage` with this schema:\n\n```ts\n// Incoming from host:\n{ id: number; input: TInput; stream?: boolean }\n\n// Reply with success:\nself.postMessage({ id, result: TOutput });\n\n// Reply with error (any Error is structured-cloned natively — no manual serialization needed):\nself.postMessage({ id, error });\n\n// Streaming — send chunks then a final result:\nself.postMessage({ id, chunk: TOutput }); // one or more chunks\nself.postMessage({ id, result: undefined }); // signals end of stream\n\n// Heartbeat (sent automatically by inline workers; module workers must send manually):\nself.postMessage({ id, heartbeat: true }); // sent at heartbeatWindow / 2 ms\n```\n\n### `@vielzeug/familiar/protocol`\n\nImport from the `/protocol` sub-path in module worker files to implement the message protocol without boilerplate.\n\n#### `handleMessages(fn)`\n\n```ts\nfunction handleMessages<TInput, TOutput>(fn: (input: TInput) => TOutput | Promise<TOutput>): void;\n```\n\nSets up `self.onmessage` to handle the familiar host↔worker protocol. Errors from `fn` are caught and forwarded as structured `{ id, error }` messages — no manual try/catch needed.\n\n```ts\n// my-worker.ts — zero boilerplate:\nimport { handleMessages } from '@vielzeug/familiar/protocol';\n\nhandleMessages(async (input: { a: number; b: number }) => input.a + input.b);\n\n// main.ts\nimport { createModuleWorker } from '@vielzeug/familiar';\n\nconst pool = createModuleWorker<{ a: number; b: number }, number>(new URL('./my-worker.ts', import.meta.url), {\n concurrency: 4,\n});\n```\n\n#### `handleStreamMessages(fn)`\n\n```ts\nfunction handleStreamMessages<TInput, TOutput>(\n fn: (input: TInput) => AsyncIterable<TOutput> | Promise<AsyncIterable<TOutput>>,\n): void;\n```\n\nSets up `self.onmessage` for a module worker that **yields streaming results**. The function must return an `AsyncIterable<TOutput>` (e.g. an `async function*`). Each yielded value is forwarded as a `{ id, chunk }` message, followed by `{ id, result: undefined }` to signal completion — the same protocol used by inline blob workers.\n\n```ts\n// my-streaming-worker.ts\nimport { handleStreamMessages } from '@vielzeug/familiar/protocol';\n\nhandleStreamMessages(async function* (n: number) {\n for (let i = 0; i < n; i++) {\n yield i;\n }\n});\n\n// main.ts\nconst pool = createModuleWorker<number, number>(new URL('./my-streaming-worker.ts', import.meta.url));\n\nfor await (const chunk of pool.runStream(5)) {\n console.log(chunk); // 0, 1, 2, 3, 4\n}\n```\n\n#### `PROTOCOL_VERSION`\n\n```ts\nexport const PROTOCOL_VERSION: 2;\n```\n\nNumeric constant for the current host↔worker message protocol. Include in a startup message (`self.postMessage({ protocol: PROTOCOL_VERSION })`) as a debugging convention to detect version skew from cached module workers. The host does **not** validate this value at runtime.\n\n## WorkerHandle Members\n\n### `run(input, options?)`\n\n`run(input: TInput, options?: RunOptions): Promise<TOutput>`\n\nDispatches a task to the next available slot. If all slots are busy, the task enters the queue.\n\n**Rejects with:**\n\n| Error | Condition |\n| ---------------------------- | ------------------------------------------------------------------- |\n| `FamiliarQueueFullError` | `maxQueue` is set and the queue is at capacity (`onFull='reject'`) |\n| `FamiliarTimeoutError` | Task exceeded its timeout or heartbeat window |\n| `FamiliarTerminatedError` | `dispose()` was called before or during the task |\n| `FamiliarTaskError` | Task function threw an error |\n| `FamiliarRuntimeError` | Worker runtime or setup failure |\n| `DOMException (AbortError)` | Provided `signal` was aborted before the task started |\n\n---\n\n### `runStream(input, options?)`\n\n`runStream(input: TInput, options?: Omit<RunOptions, 'signal'>): AsyncIterable<TOutput>`\n\nRuns a streaming task and yields partial results as they arrive. The task function must return an async iterable; each yielded value is forwarded as a chunk.\n\n::: warning Requires a free slot — throws synchronously\n`runStream()` cannot be queued. If all slots are busy it **throws `FamiliarRuntimeError` synchronously** at the call site. Use `run()` for queueable work.\n:::\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst counter = task<number, number[]>(\n (n) =>\n (async function* () {\n for (let i = 0; i < n; i++) yield i;\n })() as unknown as number[],\n);\nconst worker = createWorker(counter);\n\nfor await (const chunk of worker.runStream(5)) {\n console.log(chunk); // 0, 1, 2, 3, 4\n}\n\nworker.dispose();\n```\n\nBreaking out of a `for-await-of` loop (or throwing from the body) releases the slot cleanly — no leak, no stale timers.\n\nThe `timeout` option works the same as for `run()`: if the stream task exceeds the timeout, it is killed and the iterator throws `FamiliarTimeoutError`.\n\n---\n\n### `batch(inputs, options?)`\n\n`batch(inputs: TInput[], options?: BatchOptions): AsyncIterable<TOutput>`\n\nRuns all inputs through the pool and yields results. By default yields in submission order. Pass `ordered: false` to yield as each task completes (maximum throughput).\n\n```ts\nconst pool = createWorker<number, number>((n) => n * 2, { concurrency: 4 });\n\n// Submission order (default)\nfor await (const result of pool.batch([1, 2, 3, 4, 5])) {\n console.log(result); // 2, 4, 6, 8, 10\n}\n\n// Completion order — maximum throughput\nfor await (const result of pool.batch([1, 2, 3], { ordered: false })) {\n console.log(result); // arrives as soon as each task finishes\n}\n\npool.dispose();\n```\n\nIf any task throws, `batch()` aborts remaining queued tasks and re-throws the error.\n\n::: tip Memory — `ordered: false`\n`batch()` submits tasks in a window bounded by `concurrency`: at most `concurrency` results can be settled-but-unread ahead of the consumer at any time, regardless of the total batch size. A slow consumer applies natural backpressure instead of buffering the entire batch in memory.\n:::\n\n---\n\n### `group(name?, options?)`\n\n`group(name?: string, options?: GroupOptions): TaskGroup<TInput, TOutput>`\n\nCreates a task group. All tasks submitted via `group.run()` share an `AbortController` and can be cancelled or awaited as a unit. An optional `name` is stored on the group for logging and debugging.\n\nPass `options.signal` to tie the group's lifetime to an external `AbortSignal` — when the signal aborts, the group aborts automatically:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst square = task<number, number>((n) => n * 2);\nconst pool = createWorker(square, { concurrency: 4 });\n\n// Tied to pool lifetime — group aborts automatically when pool disposes:\nconst g = pool.group('batch-1', { signal: pool.disposalSignal });\n\nconst p1 = g.run(1);\nconst p2 = g.run(2);\nconst p3 = g.run(3);\n\nconst results = await g.drain(); // resolves with PromiseSettledResult[]\nconsole.log(results[0]); // { status: 'fulfilled', value: 2 }\n\n// Cancel all pending tasks in the group\ng.abort();\npool.dispose();\n```\n\n#### `TaskGroup.drain()`\n\n`drain(): Promise<PromiseSettledResult<TOutput>[]>`\n\nWaits for all tasks submitted to the group _so far_ to settle. Returns an array of `PromiseSettledResult` — one per task, in submission order. Unlike `Promise.allSettled`, the group's individual promises still reject normally; `drain()` collects all outcomes without throwing.\n\nCalling `abort()` concurrently while `drain()` is pending is safe: aborted tasks appear as `{ status: 'rejected' }` entries in the result.\n\n::: tip Memory — long-lived groups\n`drain()` clears its internal task list after snapshotting, so settled Promise references become eligible for GC. For groups that cycle through many drain() calls, this prevents accumulation of settled Promise objects.\n:::\n\n#### `TaskGroup.abort(reason?)`\n\n`abort(reason?: unknown): void`\n\nCancels all pending group tasks. In-flight tasks run to natural completion.\n\n#### `TaskGroup.name`\n\n`readonly name: string | undefined`\n\nOptional name provided when the group was created via `group(name)`.\n\n#### `TaskGroup.pending`\n\n`readonly pending: number`\n\nNumber of tasks not yet settled (active + queued). Decrements as tasks complete or fail.\n\n#### `TaskGroup.size`\n\n`readonly size: number`\n\nTotal tasks ever submitted to this group (never decrements).\n\n---\n\n### `drain(timeoutMs?)`\n\n`drain(timeoutMs?: number): Promise<void>`\n\nGraceful shutdown. Waits until all queued and in-flight tasks settle, then terminates all workers. Calling `run()` after `drain()` has started rejects with `FamiliarTerminatedError`.\n\nIf `timeoutMs` is given and the pool has not gone idle within that window, rejects with `FamiliarTimeoutError` and force-terminates.\n\n```ts\nawait pool.drain(); // drain then terminate\nawait pool.drain(5000); // must drain within 5 s\n```\n\n---\n\n### `dispose()`\n\n`dispose(): void`\n\nImmediate forceful termination. Rejects all in-flight and queued tasks with `FamiliarTerminatedError`. After `dispose()`, `status` is `'terminated'` and further `run()` calls reject immediately.\n\n---\n\n### `disposed`\n\n`readonly disposed: boolean`\n\n`true` after `dispose()` has been called or `drain()` has settled. Use to guard against post-termination calls.\n\n---\n\n### `disposalSignal`\n\n`readonly disposalSignal: AbortSignal`\n\n`AbortSignal` aborted when the pool is terminated (via `dispose()` or `drain()` settling). Use to tie external lifetimes (polling loops, SSE connections, etc.) to the pool's lifecycle.\n\n```ts\nconst pool = createWorker<number, number>((n) => n * 2);\n\nstartPolling({ signal: pool.disposalSignal });\n// polling stops automatically when the pool is disposed\n```\n\n---\n\n### `prime()`\n\n`prime(): Promise<void>`\n\nPre-initializes all worker slots by spawning their `Worker` instances now. Resolves when all slots are ready. Call during application startup to eliminate first-task cold-start latency.\n\n::: tip Best-effort\nIf the Worker API is unavailable (e.g. SSR), `prime()` silently resolves. Errors surface on the first `run()` call.\n:::\n\n```ts\nconst pool = createWorker<number, number>((n) => n * 2, { concurrency: 4 });\nawait pool.prime(); // pre-spawn all 4 threads\nconst result = await pool.run(21); // no cold-start\n```\n\n---\n\n### Metrics\n\n| Member | Type | Description |\n| ------------- | -------------- | -------------------------------------------------------------------------------------- |\n| `active` | `number` | Slots currently executing a task |\n| `completed` | `number` | Successful tasks since creation |\n| `concurrency` | `number` | Configured slot count |\n| `failed` | `number` | Tasks rejected with task/timeout/worker error (excludes aborts and terminations) |\n| `groupCount` | `number` | Active groups — decrements when all tasks settle naturally or when `drain()` is called |\n| `queued` | `number` | Tasks waiting in queue (accurate — excludes cancelled items) |\n| `status` | `WorkerStatus` | Current lifecycle state |\n\n---\n\n### `[Symbol.dispose]()` / `[Symbol.asyncDispose]()`\n\n`[Symbol.dispose](): void` — alias for `dispose()`, enables the `using` keyword.\n\n`[Symbol.asyncDispose](): Promise<void>` — alias for `drain()`, enables `await using`.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\n\n// Synchronous dispose — terminates immediately\n{\n using worker = createWorker(double);\n await worker.run(21); // 42\n} // dispose() called automatically\n\n// Async dispose — drains then terminates\n{\n await using pool = createWorker(double, { concurrency: 4 });\n const results = await Promise.all([1, 2, 3].map((n) => pool.run(n)));\n} // drain() called automatically\n```\n\n## Error Model\n\nAll worker errors extend `FamiliarError`. Use `instanceof FamiliarError` to catch any familiar error, or the specific subclass for precise handling.\n\n```ts\nclass FamiliarError extends Error {}\n```\n\n### Error Hierarchy\n\n| Class | Extra fields | When thrown |\n| ------------------------------ | -------------------- | ----------------------------------------------------- |\n| `FamiliarTimeoutError` | `.timeoutMs: number` | Task exceeded timeout or heartbeat window |\n| `FamiliarTaskError` | `.cause: unknown` | Task function threw |\n| `FamiliarQueueFullError` | `.maxQueue: number` | Queue at capacity (`onFull='reject'`) |\n| `FamiliarTerminatedError` | — | `dispose()` called; task was in-flight or queued |\n| `FamiliarRuntimeError` | `.cause?: unknown` | Worker API unavailable or unhandled thread error |\n| `FamiliarInvalidOptionsError` | — | Invalid `createWorker` / `createModuleWorker` options |\n\n```ts\nimport { FamiliarQueueFullError, FamiliarTaskError, FamiliarTerminatedError, FamiliarTimeoutError } from '@vielzeug/familiar';\n\ntry {\n await worker.run(input, { timeout: 500 });\n} catch (err) {\n if (err instanceof FamiliarTimeoutError) {\n console.error(`Timed out after ${err.timeoutMs}ms`);\n } else if (err instanceof FamiliarTaskError) {\n console.error('Task threw:', err.cause);\n } else if (err instanceof FamiliarQueueFullError) {\n console.error(`Queue full (maxQueue=${err.maxQueue})`);\n } else if (err instanceof FamiliarTerminatedError) {\n console.error('Worker was disposed');\n }\n}\n```\n\n## Testing Utilities\n\nImport from the `/testing` subpath — not included in the main bundle:\n\n```ts\nimport { createTestWorker } from '@vielzeug/familiar/testing';\nimport type { TestWorkerHandle, TestWorkerOptions } from '@vielzeug/familiar/testing';\n```\n\nError classes are also re-exported from the `/testing` subpath so test files need only one import.\n\n---\n\n### `createTestWorker`\n\n```ts\nfunction createTestWorker<TInput, TOutput>(\n fn: (input: TInput) => TOutput | Promise<TOutput>,\n options?: TestWorkerOptions,\n): TestWorkerHandle<TInput, TOutput>;\n```\n\nCreates an in-process test double. `fn` runs on the same thread — no Worker is spawned. Successful calls are recorded in `handle.calls`. Errors propagate unwrapped (not wrapped in `FamiliarError`), so vitest assertion errors surface directly in test output.\n\n---\n\n### `TestWorkerOptions`\n\n```ts\ntype TestWorkerOptions = {\n concurrency?: number;\n errorWrapping?: boolean;\n maxQueue?: number;\n onFull?: 'reject' | 'wait';\n};\n```\n\n| Field | Type | Default | Description |\n| --------------- | -------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `concurrency` | `number` | `1` | In-process slot count. Default `1` for deterministic ordering. Increase only when testing concurrency-specific behavior. |\n| `errorWrapping` | `boolean` | `false` | When `true`, errors from `fn` are wrapped in `FamiliarTaskError` (with `.cause` set to the original error), mirroring real worker behavior. Useful when testing code that checks `instanceof FamiliarError`. |\n| `maxQueue` | `number` | unlimited | Queue capacity before rejecting with `FamiliarQueueFullError`. |\n| `onFull` | `'reject' \\| 'wait'` | `'reject'` | Queue-full behavior. |\n\n---\n\n### `TestWorkerHandle`\n\n```ts\ntype TestWorkerHandle<TInput, TOutput> = WorkerHandle<TInput, TOutput> & {\n readonly calls: ReadonlyArray<{ input: TInput; output: TOutput }>;\n};\n```\n\nExtends `WorkerHandle` with `.calls` — all successful `run()` invocations in call order.\n\n**Differences from the real worker:**\n\n- Tasks run in-process — serialization constraints are not enforced.\n- `prime()` is a no-op (tasks run in-process).\n- `runStream()` is not supported (rejects with `FamiliarRuntimeError` on first `next()`).\n- Error wrapping is skipped by default — task errors propagate as-is for better test DX. Set `errorWrapping: true` to mirror real worker behavior.\n\n```ts\nimport { createTestWorker } from '@vielzeug/familiar/testing';\nimport { describe, expect, it } from 'vitest';\n\ndescribe('add worker', () => {\n it('records calls', async () => {\n const worker = createTestWorker<{ a: number; b: number }, number>(({ a, b }) => a + b);\n\n expect(await worker.run({ a: 2, b: 3 })).toBe(5);\n expect(await worker.run({ a: 10, b: 20 })).toBe(30);\n\n expect(worker.calls).toHaveLength(2);\n expect(worker.calls[0]!.input).toEqual({ a: 2, b: 3 });\n expect(worker.calls[1]!.output).toBe(30);\n\n worker.dispose();\n });\n});\n```\n",
|
|
6
|
-
"usage": "---\ntitle: Familiar — Usage Guide\ndescription: How to use familiar for task functions, single workers, pools, streaming, priorities, heartbeat, timeouts, cancellation, typed errors, and testing.\n---\n\n[[toc]]\n\n::: tip\nNew to Worker? Start with the [Overview](./index.md) for a quick introduction.\n:::\n\n## Basic Usage\n\nWrap your task function with `task()` before passing it to `createWorker`. This marks the function as self-contained and safe to serialize into a Web Worker.\n\nBecause the function is serialized via `.toString()` and executed in a separate global scope, it **must be entirely self-contained** — it cannot close over variables from the surrounding module.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\nimport type { TaskFn } from '@vielzeug/familiar';\n\n// Inline function wrapped with task()\nconst worker = createWorker(task<number, number>((n) => n * 2));\n\n// Named function reference — also fine\nfunction double(n: number): number {\n return n * 2;\n}\nconst worker2 = createWorker(task<number, number>(double));\n\n// Define once, reuse across pools\ntype Fn = TaskFn<{ a: number; b: number }, number>;\nconst add = task<{ a: number; b: number }, number>(({ a, b }) => a + b);\nconst addWorker = createWorker(add);\n```\n\n::: warning Self-contained closures\nThe task function runs inside a Web Worker with a separate global scope. Any outer-scope variable you reference will be `undefined` at runtime. Put helpers inside the task function or encode them into the input payload.\n\n`task()` throws `FamiliarInvalidOptionsError` if you pass a bound or native function (e.g. `Math.sqrt.bind(null)`).\n:::\n\n## Single Worker\n\nCalling `createWorker` without a `concurrency` option creates a single worker that processes one task at a time. Additional calls to `run()` are queued and dispatched in order.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst upper = task<string, string>((text) => text.toUpperCase());\nconst worker = createWorker(upper);\n\nconsole.log(await worker.run('hello')); // 'HELLO'\nconsole.log(await worker.run('world')); // 'WORLD'\n\nworker.dispose();\n```\n\n## Worker Pool\n\nPass `concurrency` to spin up multiple worker slots. Tasks are dispatched to the first idle slot; if all slots are busy the task is queued.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\n// Fixed pool of 4\nconst fib = task<number, number>((n) => {\n function fib(x: number): number {\n return x <= 1 ? x : fib(x - 1) + fib(x - 2);\n }\n return fib(n);\n});\nconst pool = createWorker(fib, { concurrency: 4 });\n\n// Automatically uses all 4 slots in parallel\nconst results = await Promise.all([30, 31, 32, 33].map((n) => pool.run(n)));\n\npool.dispose();\n\n// 'auto' — uses navigator.hardwareConcurrency when available\nconst square = task<number, number>((n) => n ** 2);\nconst autoPool = createWorker(square, { concurrency: 'auto' });\n```\n\n## Queue Back-Pressure (`maxQueue`)\n\nSet `maxQueue` to cap how many tasks can wait in the queue. When the queue is full and `onFull` is `'reject'` (the default), `run()` rejects immediately with `FamiliarQueueFullError`:\n\n```ts\nimport { createWorker, task, FamiliarQueueFullError } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\nconst worker = createWorker(double, {\n concurrency: 1,\n maxQueue: 100,\n});\n\ntry {\n await worker.run(1);\n} catch (error) {\n if (error instanceof FamiliarQueueFullError) {\n console.error(`Back-pressure triggered: queue is full (max ${error.maxQueue})`);\n }\n}\n```\n\nFor producer→consumer pipelines, use `onFull: 'wait'` to suspend the caller instead. See [Queue Back-Pressure (`onFull`)](#queue-back-pressure-onfull) below.\n\n## Timeouts\n\nSet `timeout` (in milliseconds) to automatically reject tasks that run too long. A `FamiliarTimeoutError` is thrown.\n\n```ts\nimport { createWorker, task, FamiliarTimeoutError } from '@vielzeug/familiar';\n\nconst delay = task<number, number>((ms) => new Promise((resolve) => setTimeout(() => resolve(ms), ms)));\nconst worker = createWorker(delay, {\n timeout: 1000,\n});\n\ntry {\n await worker.run(5000); // will reject after 1 s\n} catch (err) {\n if (err instanceof FamiliarTimeoutError) {\n console.error(`Task timed out after ${err.timeoutMs}ms`);\n }\n}\n\nworker.dispose();\n```\n\n## AbortSignal\n\nPass an `AbortSignal` via `RunOptions` to cancel a **queued** task before it starts. Tasks already in flight cannot be interrupted.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst upper = task<string, string>((text) => text.toUpperCase());\nconst worker = createWorker(upper, { concurrency: 1 });\n\nconst ac = new AbortController();\n\n// Queue multiple tasks\nconst p1 = worker.run('first');\nconst p2 = worker.run('second', { signal: ac.signal });\nconst p3 = worker.run('third', { signal: ac.signal });\n\n// Cancel the queued tasks\nac.abort(); // p2 and p3 reject with DOMException (AbortError)\n\nawait p1; // still resolves — it was already in flight\nworker.dispose();\n```\n\n## Transferables\n\nLarge `ArrayBuffer`, `MessagePort`, or `OffscreenCanvas` values can be moved to the Worker thread instead of copied. This avoids the structured-clone overhead on large payloads.\n\nPass the transferable list via `RunOptions.transferables`:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\ntype ImageTask = { pixels: Uint8ClampedArray; width: number; height: number };\ntype ImageResult = { pixels: Uint8ClampedArray };\n\nconst grayscale = task<ImageTask, ImageResult>(({ pixels }) => {\n const out = new Uint8ClampedArray(pixels.length);\n for (let i = 0; i < pixels.length; i += 4) {\n const g = 0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2];\n out[i] = out[i + 1] = out[i + 2] = g;\n out[i + 3] = pixels[i + 3];\n }\n return { pixels: out };\n});\nconst worker = createWorker(grayscale);\n\nconst imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n\n// Transfer the buffer — zero-copy move to the worker\nconst { pixels } = await worker.run(\n { pixels: imageData.data, width: imageData.width, height: imageData.height },\n { transferables: [imageData.data.buffer] },\n);\n\nworker.dispose();\n```\n\n::: warning\nOnce a buffer is transferred it is detached (length = 0) in the sending context. Do not access it after the `run()` call.\n:::\n\n## Worker Status\n\nThe `status` property reflects the current state of the worker handle:\n\n| Value | Meaning |\n| -------------- | -------------------------------------------- |\n| `'idle'` | All slots are free and waiting for tasks |\n| `'running'` | One or more slots are executing a task |\n| `'terminated'` | `dispose()` was called — `run()` will reject |\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\nimport type { WorkerStatus } from '@vielzeug/familiar';\n\nconst worker = createWorker(task<number, number>((n) => n * 2));\n\nconsole.log(worker.status); // 'idle'\n\nconst p = worker.run(21);\nconsole.log(worker.status); // 'running'\n\nawait p;\nconsole.log(worker.status); // 'idle'\n\nworker.dispose();\nconsole.log(worker.status); // 'terminated'\n```\n\n## Stats\n\nUse lightweight counters for visibility and load monitoring:\n\n- `completed`: successful tasks since creation\n- `failed`: tasks rejected with a task / timeout / worker error (aborts and terminations excluded)\n- `active`: number of slots currently executing a task\n- `queued`: tasks currently waiting in the queue\n- `groupCount`: active task groups (decrements when `drain()` is called or all tasks settle)\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst pool = createWorker(\n task<number, number>((n) => n + 1),\n { concurrency: 4 },\n);\n\nconsole.log(pool.completed); // 0\nconsole.log(pool.failed); // 0\nconsole.log(pool.active); // 0\nconsole.log(pool.queued); // 0\nconsole.log(pool.groupCount); // 0\n\nawait pool.run(1);\nawait pool.run(-1).catch(() => {}); // throws inside worker\nconsole.log(pool.completed); // 1\nconsole.log(pool.failed); // 1\n```\n\n## Batch Processing (`batch`)\n\nUse `batch()` to run a list of inputs through the pool and consume results as they arrive, in submission order:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst fib = task<number, number>((n) => {\n function fib(x: number): number {\n return x <= 1 ? x : fib(x - 1) + fib(x - 2);\n }\n return fib(n);\n});\nconst pool = createWorker(fib, { concurrency: 4 });\n\nfor await (const result of pool.batch([30, 31, 32, 33])) {\n console.log(result); // printed in submission order as each task finishes\n}\n\npool.dispose();\n```\n\n`batch()` accepts the same per-run options as `run()` (except `signal`, which is managed internally):\n\n```ts\n// Apply a 500ms timeout to every task in the batch\nfor await (const result of pool.batch([1, 2, 3], { timeout: 500 })) {\n console.log(result);\n}\n```\n\nIf any task throws, `batch()` cancels remaining queued tasks and re-throws the error.\n\nPass `ordered: false` to yield results in completion order instead of submission order (higher throughput when tasks take different amounts of time):\n\n```ts\nfor await (const result of pool.batch([1, 2, 3], { ordered: false })) {\n console.log(result); // arrives as soon as each task finishes\n}\n```\n\n## Streaming (`runStream`)\n\nWhen a task yields multiple partial results, use `runStream()`. The task function must return an async iterable; each yielded value is forwarded to the caller as a chunk.\n\n::: warning Requires a free slot\n`runStream()` cannot queue. If all slots are busy it throws `FamiliarRuntimeError` immediately. Design streaming workloads so slots are available or use `run()` for queueable alternatives.\n:::\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\n// Task returns an async iterable\nconst rangeStream = task<{ start: number; end: number }, number[]>(\n ({ start, end }) =>\n (async function* () {\n for (let i = start; i <= end; i++) {\n await new Promise((r) => setTimeout(r, 10)); // simulate work per chunk\n yield i;\n }\n })() as unknown as number[],\n);\nconst worker = createWorker(rangeStream);\n\nfor await (const chunk of worker.runStream({ start: 1, end: 5 })) {\n console.log('chunk:', chunk); // 1, 2, 3, 4, 5\n}\n\nworker.dispose();\n```\n\nBreaking out of the loop early (or throwing from the body) releases the slot cleanly — no leak, no stale timers.\n\n## Task Groups (`group`)\n\nUse `group()` when you want to cancel and drain a set of related tasks together. Pass an optional name for logging and debugging.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\nconst pool = createWorker(double, { concurrency: 4 });\nconst g = pool.group('my-batch');\n\n// Submit tasks into the group\nconst p1 = g.run(1);\nconst p2 = g.run(2);\nconst p3 = g.run(3);\n\n// Wait for all group tasks to settle — returns PromiseSettledResult[]\nconst results = await g.drain();\nconsole.log(results[0]); // { status: 'fulfilled', value: 2 }\nconsole.log(g.name); // 'my-batch'\nconsole.log(g.size); // 3\n\npool.dispose();\n```\n\n`drain()` resolves with a `PromiseSettledResult[]` array — every task outcome in submission order. Individual promises still reject normally; `drain()` just collects all outcomes without throwing:\n\n```ts\nconst g = pool.group();\n\ng.run(1).catch(() => {});\ng.run(-1).catch(() => {}); // will fail inside the worker\ng.run(3).catch(() => {});\n\nconst settled = await g.drain();\nconst failures = settled.filter((r) => r.status === 'rejected');\nconsole.log(failures.length); // 1\n```\n\nCancel all pending tasks in the group with `g.abort()`:\n\n```ts\nconst g = pool.group();\n\nconst p1 = g.run(slowTask1);\nconst p2 = g.run(slowTask2);\nconst p3 = g.run(slowTask3);\n\ng.abort(); // p2, p3 (queued) reject; p1 (in-flight) completes normally\nawait p1;\n```\n\n## Priority Queue\n\nPass `priority` per `run()` call. Higher values run before lower values when tasks queue up. Equal priorities are FIFO.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst upper = task<string, string>((s) => s.toUpperCase());\nconst pool = createWorker(upper, { concurrency: 1 });\n\n// Queue multiple tasks — the slot is busy with a blocker\nconst blocker = pool.run('low-priority-blocker');\n\npool.run('low', { priority: 1 });\npool.run('critical', { priority: 100 }); // runs first once blocker finishes\npool.run('normal', { priority: 10 });\n\n// Execution order after blocker: critical → normal → low\n```\n\nDefault priority is `0`.\n\n## Per-Run Timeout\n\nThe `timeout` option can also be passed per `run()` call, overriding the pool-level timeout for that specific task:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst upper = task<string, string>((s) => s.toUpperCase());\nconst pool = createWorker(upper, {\n timeout: 5000, // default: 5 s\n});\n\n// This specific task must complete in 100ms\nawait pool.run('hello', { timeout: 100 });\n```\n\n## Graceful Shutdown (`drain`)\n\nUse `drain()` to finish queued/in-flight work before terminating workers:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\nconst worker = createWorker(double, { concurrency: 1 });\n\nconst p1 = worker.run(1);\nconst p2 = worker.run(2);\n\nawait worker.drain();\n\nawait p1;\nawait p2;\nconsole.log(worker.status); // 'terminated'\n```\n\nPass a timeout to prevent indefinite hangs — if the pool hasn't drained within that window, `drain()` rejects with `FamiliarTimeoutError` and force-terminates:\n\n```ts\ntry {\n await worker.drain(5000); // must drain within 5 s\n} catch (err) {\n // timed out — worker is now force-terminated\n}\n```\n\nUse `dispose()` for immediate forceful termination.\n\n## Heartbeat Monitoring\n\nSet `heartbeatWindow` on `WorkerOptions` to kill tasks that stop responding (e.g., blocked CPU work). If the worker does not send a heartbeat within `heartbeatWindow` ms, the task is rejected with `FamiliarTimeoutError`.\n\n**Inline workers** (`createWorker`) send heartbeats automatically at `heartbeatWindow / 2` intervals — no worker-side code needed:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst heavy = task<void, void>(() => new Promise((r) => setTimeout(r, 5000)));\n\n// 60s watchdog — auto-heartbeats keep it alive throughout\nconst worker = createWorker(heavy, { heartbeatWindow: 60_000 });\nawait worker.run(undefined);\nworker.dispose();\n```\n\n**Module workers** (`createModuleWorker`) must send heartbeats manually at `heartbeatWindow / 2` intervals. Note: passing `heartbeatWindow` to `createModuleWorker` emits a dev warning — it has no automatic effect on module workers:\n\n```ts\n// my-worker.ts\nself.onmessage = async (event) => {\n const { id, input } = event.data;\n\n // Send heartbeat at heartbeatWindow / 2 intervals (e.g. every 30s for a 60s window)\n const hb = setInterval(() => self.postMessage({ id, heartbeat: true }), 30_000);\n\n try {\n self.postMessage({ id, result: await heavyWork(input) });\n } finally {\n clearInterval(hb);\n }\n};\n\n// main.ts — heartbeatWindow is informational; the worker must implement heartbeats itself\nconst pool = createModuleWorker(new URL('./my-worker.ts', import.meta.url));\n```\n\n## Slot Error Handling (`onSlotError`)\n\nUse `onSlotError` to be notified when a Worker slot crashes with an unhandled runtime error. The slot is stopped automatically; call `restart()` to pre-warm a replacement Worker.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\nconst pool = createWorker(double, {\n concurrency: 4,\n onSlotError: (error, restart) => {\n console.error('Worker slot crashed:', error.message);\n // Optionally pre-warm the replacement Worker immediately\n restart();\n },\n});\n```\n\nIf `onSlotError` is omitted, errors are handled silently and the slot restarts lazily on the next `run()` call.\n\n## Queue Back-Pressure (`onFull`)\n\nBy default, `run()` rejects with `FamiliarQueueFullError` when `maxQueue` is reached (`onFull: 'reject'`). Set `onFull: 'wait'` to suspend the caller instead — natural backpressure for producer→consumer pipelines:\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\nconst pool = createWorker(double, {\n concurrency: 2,\n maxQueue: 10,\n onFull: 'wait', // callers suspend until a queue slot opens\n});\n\n// Producer — never rejects with FamiliarQueueFullError\nfor (let i = 0; i < 1000; i++) {\n await pool.run(i); // suspends automatically when queue is full\n}\n\nawait pool.drain();\n```\n\n## Module Workers (`createModuleWorker`)\n\nUse `createModuleWorker` when the task needs imports, top-level await, or module-scope helpers that cannot be inlined into a self-contained function.\n\n```ts\n// my-worker.ts — a regular ES module\nimport { heavyLib } from './heavy-lib.js';\n\nself.onmessage = async (event) => {\n const { id, input } = event.data;\n try {\n self.postMessage({ id, result: await heavyLib.process(input) });\n } catch (error) {\n // Error objects are structured-cloned natively — no manual serialization needed\n self.postMessage({ id, error });\n }\n};\n\n// main.ts\nimport { createModuleWorker } from '@vielzeug/familiar';\n\nconst pool = createModuleWorker<string, string>(new URL('./my-worker.ts', import.meta.url), { concurrency: 4 });\n\nconst result = await pool.run('hello');\npool.dispose();\n```\n\nSee the [API Reference](./api.md#createmoduleworker) for the full message protocol schema.\n\n## Typed Errors\n\nEach failure reason has its own class with extra fields for context. Use `instanceof` checks for precise handling:\n\n```ts\nimport {\n createWorker,\n task,\n FamiliarQueueFullError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from '@vielzeug/familiar';\n\nconst validate = task<number, number>((n) => {\n if (n < 0) throw new RangeError('negative input');\n return n * 2;\n});\nconst pool = createWorker(validate, { concurrency: 1, maxQueue: 5, timeout: 1000 });\n\ntry {\n await pool.run(input);\n} catch (err) {\n if (err instanceof FamiliarTimeoutError) {\n // .timeoutMs tells you exactly how long it waited\n console.error(`Timed out after ${err.timeoutMs}ms`);\n } else if (err instanceof FamiliarTaskError) {\n // .cause is the original error thrown inside the task\n console.error('Task threw:', (err.cause as Error).message);\n } else if (err instanceof FamiliarQueueFullError) {\n // .maxQueue is the configured limit\n console.error(`Queue full (max ${err.maxQueue} tasks)`);\n } else if (err instanceof FamiliarTerminatedError) {\n console.error('Pool was disposed');\n }\n}\n```\n\nAll error subclasses extend `FamiliarError` — use `instanceof FamiliarError` as a catch-all:\n\n```ts\nimport { FamiliarError } from '@vielzeug/familiar';\n\ntry {\n await pool.run(input);\n} catch (err) {\n if (err instanceof FamiliarError) {\n // err.name is the specific subclass name, e.g. 'FamiliarTimeoutError'\n console.error(`Worker error [${err.name}]:`, err.message);\n }\n}\n```\n\n## Runtime Availability\n\n`createWorker()` is safe to call in any runtime. Actual execution happens only when you call `run()`, and that requires a real Worker implementation.\n\n```ts\nimport { createWorker, task, FamiliarRuntimeError } from '@vielzeug/familiar';\n\nconst worker = createWorker(task<number, number>((n) => n * 2));\n\ntry {\n console.log(await worker.run(21));\n} catch (error) {\n if (error instanceof FamiliarRuntimeError) {\n console.error('Worker execution is unavailable in this runtime');\n }\n}\n```\n\nThis keeps construction cheap and predictable in shared modules, while still failing clearly when the runtime cannot execute Workers.\n\n## `Symbol.dispose` / `using` Declarations\n\n`WorkerHandle` implements `[Symbol.dispose]` as an alias for `dispose()`, enabling the TC39 [explicit resource management](https://github.com/tc39/proposal-explicit-resource-management) `using` keyword (TypeScript ≥ 5.2 with `\"lib\": [\"es2025\"]`):\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\n\n{\n using worker = createWorker(double);\n const result = await worker.run(21); // 42\n} // worker.dispose() is called automatically here\n```\n\nThis also works with worker pools:\n\n```ts\nconst upper = task<string, string>((text) => text.toUpperCase());\n\n{\n using pool = createWorker(upper, { concurrency: 4 });\n const results = await Promise.all(['hello', 'world'].map((s) => pool.run(s)));\n // ['HELLO', 'WORLD']\n} // all 4 slots terminated automatically\n```\n\n`createTestWorker` supports `[Symbol.dispose]` as well:\n\n```ts\nimport { createTestWorker } from '@vielzeug/familiar/testing';\n\n{\n using worker = createTestWorker<number, number>((n) => n * 3);\n const result = await worker.run(7); // 21\n} // disposed automatically\n```\n\n## Testing\n\nUse `createTestWorker` from the `/test` subpath to run tasks in-process with call recording. Workers never spawn, so tests run in any environment (Node, jsdom, etc.) without additional setup.\n\n```ts\nimport { createTestWorker } from '@vielzeug/familiar/testing';\nimport { describe, expect, it } from 'vitest';\n\ntype Input = { a: number; b: number };\ntype Output = number;\n\ndescribe('add worker', () => {\n it('returns the sum', async () => {\n const worker = createTestWorker<Input, Output>(({ a, b }) => a + b);\n\n expect(await worker.run({ a: 2, b: 3 })).toBe(5);\n expect(await worker.run({ a: 10, b: 20 })).toBe(30);\n\n // Inspect recorded calls\n expect(worker.calls).toHaveLength(2);\n expect(worker.calls[0]!.input).toEqual({ a: 2, b: 3 });\n expect(worker.calls[1]!.output).toBe(30);\n\n worker.dispose();\n });\n});\n```\n\n`TestWorkerHandle` also supports `[Symbol.dispose]`:\n\n```ts\n{\n using worker = createTestWorker<number, number>((n) => n * 2);\n const result = await worker.run(21); // 42\n}\n```\n\n### `TestWorkerOptions`\n\n```ts\ntype TestWorkerOptions = {\n concurrency?: number; // default: 1\n errorWrapping?: boolean; // default: false\n maxQueue?: number;\n onFull?: 'reject' | 'wait';\n};\n```\n\nThe default `concurrency: 1` gives deterministic serial execution. Increase it only when testing concurrency-specific behavior:\n\n```ts\n// Test that 3 tasks run truly in parallel\nconst worker = createTestWorker<number, number>((n) => new Promise((r) => setTimeout(() => r(n), 20)), {\n concurrency: 3,\n});\n\nconst start = Date.now();\nconst results = await Promise.all([worker.run(1), worker.run(2), worker.run(3)]);\nconsole.log(Date.now() - start); // ~20ms — ran in parallel\n```\n\n## Prime (Pre-initialize)\n\nCall `prime()` after creating a pool to pre-spawn all Worker threads and eliminate cold-start latency on the first task.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\n\nconst double = task<number, number>((n) => n * 2);\nconst pool = createWorker(double, { concurrency: 4 });\n\n// Pre-spawn during app init and await readiness\nawait pool.prime();\n\n// First run() has no cold-start overhead\nconst result = await pool.run(21); // 42\n\npool.dispose();\n```\n\nPrime is best-effort. If the Worker API is unavailable (SSR, Node.js without Worker support), it silently does nothing and the error surfaces on the first `run()` call instead.\n\n## Framework Integration\n\nWorker handles are plain objects — wrap them in a hook or composable to integrate with your framework's lifecycle.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createWorker, task, type WorkerHandle, type TaskFn } from '@vielzeug/familiar';\n\nfunction useWorker<TInput, TOutput>(fn: TaskFn<TInput, TOutput>, concurrency = 2) {\n const ref = useRef<WorkerHandle<TInput, TOutput> | null>(null);\n\n useEffect(() => {\n const worker = createWorker(fn, { concurrency });\n void worker.prime();\n ref.current = worker;\n return () => {\n worker.drain();\n };\n }, []);\n\n return ref;\n}\n\nconst getByteLength = task<ArrayBuffer, number>((buf) => buf.byteLength);\n\nfunction ImageProcessor() {\n const workerRef = useWorker(getByteLength, 4);\n\n async function handleUpload(file: File) {\n const buf = await file.arrayBuffer();\n const size = await workerRef.current!.run(buf);\n console.log('Processed', size, 'bytes');\n }\n\n return <button onClick={() => handleUpload(selectedFile)}>Process</button>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createWorker, task } from '@vielzeug/familiar';\nimport { onScopeDispose, ref } from 'vue';\n\nconst pool = createWorker(\n task((n: number) => n * n),\n { concurrency: 2 },\n);\nvoid pool.prime();\nonScopeDispose(() => pool.drain());\n\nconst result = ref<number | null>(null);\n\nasync function runTask(n: number) {\n result.value = await pool.run(n);\n}\n</script>\n\n<template>\n <button @click=\"runTask(9)\">Square</button>\n <p>{{ result }}</p>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createWorker, task } from '@vielzeug/familiar';\n import { onDestroy } from 'svelte';\n\n const pool = createWorker(task((n: number) => n * n), { concurrency: 2 });\n void pool.prime();\n onDestroy(() => pool.drain());\n\n let result: number | null = null;\n\n async function runTask() {\n result = await pool.run(9);\n }\n</script>\n\n<button on:click={runTask}>Square</button>\n<p>{result}</p>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Initializing the pool with `createWorker(fn, ...)` directly in the component body (not inside `useEffect` or `useRef`) creates a new pool on every render. Always use `useRef` for stable initialization.\n- **Vue 3:** Creating the pool inside a `watch` or `computed` callback instead of at the top level of `setup()` can result in multiple pools being created. Always create at the top level and register `onScopeDispose` immediately.\n- **Svelte:** The pool created at the top of `<script>` starts immediately — if the component is conditionally rendered with `{#if}`, the pool is created when the component mounts. This is correct. Ensure `onDestroy` is called to close it when the component is removed.\n\n## Working with Other Vielzeug Libraries\n\n### With Herald\n\nEmit progress events from a worker task and consume them on the main thread.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<{ progress: number }>();\nbus.on('progress', (pct) => console.log(`${pct}%`));\n\nconst processItems = task<string[], number>(async (items) => {\n for (let i = 0; i < items.length; i++) {\n await processItem(items[i]!);\n bus.emit('progress', Math.round((i / items.length) * 100));\n }\n return items.length;\n});\nconst worker = createWorker(processItems);\n```\n\n### With Ripple\n\nTrack worker pool status in a reactive signal to drive UI state.\n\n```ts\nimport { createWorker, task } from '@vielzeug/familiar';\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst double = task<number, number>((n) => n * 2);\nconst pool = createWorker(double, { concurrency: 4 });\nconst active = signal(pool.active);\nconst queued = signal(pool.queued);\n\nconst isBusy = computed(() => active() > 0);\n\nasync function runTask(input: number) {\n active.set(pool.active);\n const result = await pool.run(input);\n active.set(pool.active);\n queued.set(pool.queued);\n return result;\n}\n```\n\n## Best Practices\n\n- Always wrap task functions with `task()` — this is the compile-time signal that a function is safe to serialize.\n- Use `concurrency` > 1 for CPU-bound tasks — multiple slots prevent head-of-line blocking.\n- Set `maxQueue` to bound memory usage when consumers are slower than producers.\n- Pass large binary data (images, audio, WASM buffers) via `transfer()` or `RunOptions.transferables` to avoid copying.\n- Use `AbortSignal` to cancel queued tasks when the user navigates away.\n- Call `await pool.prime()` at startup when you know tasks will arrive soon, to eliminate first-task cold-start latency.\n- Always call `drain()` in framework cleanup callbacks to terminate worker threads and free resources.\n- Keep worker task functions pure and self-contained — avoid closures over mutable main-thread state.\n- Use `createTestWorker()` in unit tests to run tasks in-process without spinning up real Worker threads.\n",
|
|
7
|
-
"examples": "---\ntitle: Familiar — Examples\ndescription:
|
|
4
|
+
"index": "---\ntitle: Familiar — Typed module-worker pools\ndescription: Typed ES module Worker pools with cancellation, priority scheduling, streaming, and test utilities.\npackage: familiar\ncategory: workers\nkeywords: [web-workers, module-workers, pool, concurrency, timeout, cancellation, streaming]\nrelated: [arsenal, ripple, herald]\nexports: [createWorker, createStreamWorker, batch, createTaskGroup, FamiliarError, FamiliarTimeoutError, FamiliarTaskError, FamiliarQueueFullError, FamiliarTerminatedError, FamiliarRuntimeError]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"familiar\" />\n\n## Why Familiar?\n\nRaw workers force every application to maintain its own message contract, lifecycle, cancellation, and pool scheduler. Familiar provides those boundaries while keeping worker code in normal typed ES modules.\n\n```ts\n// Before\nconst worker = new Worker(new URL('./sum.worker.ts', import.meta.url), { type: 'module' });\nworker.postMessage([1, 2, 3]);\n\n// After\nconst pool = createWorker<number[], number>(new URL('./sum.worker.ts', import.meta.url));\nawait pool.run([1, 2, 3]);\n```\n\n| Feature | Familiar | Raw Worker | Comlink |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"familiar\" type=\"size\" /> | built-in | ~2 kB |\n| Module-worker contract | <ore-icon name=\"check\" size=\"16\"></ore-icon> | manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Pool scheduling | <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| AbortSignal cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | manual | manual |\n| Versioned protocol | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | implementation-specific |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <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 Familiar when** worker jobs need bounded concurrency, typed errors, cancellation, or queue policy.\n\n**Consider raw Worker when** one isolated worker and custom messaging are enough.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/familiar\n```\n\n```sh [npm]\nnpm install @vielzeug/familiar\n```\n\n```sh [yarn]\nyarn add @vielzeug/familiar\n```\n\n:::\n\n## Quick Start\n\nRegister task logic inside a worker module.\n\n```ts\n// double.worker.ts\nimport { exposeTask } from '@vielzeug/familiar/protocol';\n\nexposeTask((value: number) => value * 2);\n```\n\nCreate pool from module URL and dispose it after use.\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst worker = createWorker<number, number>(new URL('./double.worker.ts', import.meta.url));\n\ntry {\n console.log(await worker.run(21));\n} finally {\n worker.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createWorker()` — versioned task protocol over ES module workers\n- `createStreamWorker()` — stream-only worker capability\n- `run()` — priority scheduling, transferables, timeout, and cancellation\n- `batch()` — ordered task composition\n- `createTaskGroup()` — shared cancellation and settlement tracking\n- `stats` — active, queued, completed, and failed counters\n- `createTestWorker()` — faithful in-process task-pool testing\n- `dispose()` and `drain()` — immediate or draining teardown, with `using` support\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\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Arsenal](/arsenal/) — async helpers for application coordination.\n- [Ripple](/ripple/) — expose worker results through reactive state.\n- [Herald](/herald/) — publish application events after worker jobs settle.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Familiar — API Reference\ndescription: API reference for module-worker pools and worker-side protocol registration.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createWorker()` | Create single-result module-worker pool | Sync | Worker must call `exposeTask()` |\n| `createStreamWorker()` | Create stream-only module-worker pool | Sync | Worker must call `exposeStream()` |\n| `batch()` | Yield ordered task-pool results | Async iterator | Stops remaining work on first failure |\n| `createTaskGroup()` | Coordinate related task-pool jobs | Sync | Call `abort()` to stop group work |\n| `createTestWorker()` | Create an in-process task-pool test double | Sync | Task modules are not executed |\n| `exposeTask()` | Register worker task handler | Sync | Worker-only import |\n| `exposeStream()` | Register worker stream handler | Sync | Worker-only import |\n| `createTestWorker()` | Faithful in-process task-pool test adapter | Sync | Does not execute worker modules |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/familiar` | Pool factories, helpers, types, errors |\n| `@vielzeug/familiar/protocol` | Versioned worker protocol and registration helpers |\n| `@vielzeug/familiar/testing` | Task-pool testing adapter |\n\n## Pool Factories\n\n### `createWorker()`\n\n```ts\nfunction createWorker<TInput, TOutput>(url: URL | string, options?: WorkerOptions): WorkerPool<TInput, TOutput>;\n```\n\nCreates a task pool for a worker module registered with `exposeTask()`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `URL \\| string` | Module-worker URL, usually `new URL('./task.worker.ts', import.meta.url)` |\n| `options` | `WorkerOptions` | Pool concurrency, queue, timeout, and worker-error policy |\n\n**Returns:** `WorkerPool<TInput, TOutput>`.\n\n**Example:**\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker<number, number>(new URL('./double.worker.ts', import.meta.url));\n\ntry {\n console.log(await pool.run(21));\n} finally {\n pool.dispose();\n}\n```\n\n### `createStreamWorker()`\n\n```ts\nfunction createStreamWorker<TInput, TChunk>(url: URL | string, options?: WorkerOptions): StreamWorkerPool<TInput, TChunk>;\n```\n\nCreates a stream-only pool for a worker module registered with `exposeStream()`.\n\n**Returns:** `StreamWorkerPool<TInput, TChunk>`.\n\n---\n\n### `batch()`\n\n```ts\nfunction batch<TInput, TOutput>(\n pool: WorkerPool<TInput, TOutput>,\n inputs: readonly TInput[],\n options?: BatchOptions,\n): AsyncIterable<TOutput>;\n```\n\nYields results in submission order. A failure or cancellation aborts remaining batch work.\n\n**Returns:** `AsyncIterable<TOutput>`.\n\n---\n\n### `createTaskGroup()`\n\n```ts\nfunction createTaskGroup<TInput, TOutput>(\n pool: WorkerPool<TInput, TOutput>,\n name?: string,\n options?: TaskGroupOptions,\n): TaskGroup<TInput, TOutput>;\n```\n\nCreates group-scoped cancellation and settlement tracking for one task pool.\n\n**Returns:** `TaskGroup<TInput, TOutput>`.\n\n## Testing\n\n### `createTestWorker()`\n\n```ts\nfunction createTestWorker<TInput, TOutput>(\n handler: (input: TInput) => TOutput | Promise<TOutput>,\n options?: TestWorkerOptions,\n): TestWorkerHandle<TInput, TOutput>;\n```\n\nCreates an in-process task-pool double. It structured-clones values, records settlement, and matches task-pool timeout and cancellation behavior without loading a worker module.\n\n**Returns:** `TestWorkerHandle<TInput, TOutput>`.\n\n## Worker Protocol\n\n### `exposeTask()`\n\n```ts\nfunction exposeTask<TInput, TOutput>(handler: TaskHandler<TInput, TOutput>): void;\n```\n\nRegisters one single-result handler in a module worker.\n\n### `exposeStream()`\n\n```ts\nfunction exposeStream<TInput, TChunk>(handler: StreamHandler<TInput, TChunk>): void;\n```\n\nRegisters one chunk-producing handler in a module worker.\n\n### `PROTOCOL_VERSION`\n\n```ts\nconst PROTOCOL_VERSION: 1;\n```\n\nVersion included in every host request and worker response.\n\n## Types\n\n### `WorkerOptions`\n\n```ts\ntype WorkerOptions = {\n concurrency?: number | 'auto';\n maxQueue?: number;\n onFull?: 'reject' | 'wait';\n timeout?: number;\n onSlotError?: (error: FamiliarRuntimeError) => void;\n};\n```\n\n### `RunOptions`\n\n```ts\ntype RunOptions = {\n priority?: number;\n signal?: AbortSignal;\n timeout?: number;\n transferables?: Transferable[];\n};\n```\n\n`signal` cancels capacity waits, queued work, and executing work. Executing cancellation terminates and replaces its worker slot.\n\n### `WorkerPool`\n\n```ts\ninterface WorkerPool<TInput, TOutput> {\n [Symbol.asyncDispose](): Promise<void>;\n [Symbol.dispose](): void;\n run(input: TInput, options?: RunOptions): Promise<TOutput>;\n prime(): Promise<void>;\n drain(options?: DrainOptions): Promise<void>;\n dispose(): void;\n readonly stats: WorkerStats;\n readonly status: WorkerStatus;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n}\n```\n\n### `StreamWorkerPool`\n\n```ts\ninterface StreamWorkerPool<TInput, TChunk> {\n [Symbol.asyncDispose](): Promise<void>;\n [Symbol.dispose](): void;\n runStream(input: TInput, options?: RunOptions): AsyncIterable<TChunk>;\n prime(): Promise<void>;\n drain(options?: DrainOptions): Promise<void>;\n dispose(): void;\n readonly stats: WorkerStats;\n readonly status: WorkerStatus;\n}\n```\n\n### `WorkerStats`\n\n```ts\ntype WorkerStats = {\n readonly active: number;\n readonly completed: number;\n readonly failed: number;\n readonly queued: number;\n};\n```\n\n### `WorkerStatus`\n\n```ts\ntype WorkerStatus = 'idle' | 'running' | 'terminated';\n```\n\n### `BatchOptions`\n\n```ts\ntype BatchOptions = RunOptions;\n```\n\n### `DrainOptions`\n\n```ts\ntype DrainOptions = {\n timeout?: number;\n};\n```\n\n### `TaskGroup`\n\n```ts\ntype TaskGroup<TInput, TOutput> = {\n abort(reason?: unknown): void;\n drain(): Promise<PromiseSettledResult<TOutput>[]>;\n readonly name: string | undefined;\n readonly pending: number;\n run(input: TInput, options?: Omit<RunOptions, 'signal'>): Promise<TOutput>;\n readonly size: number;\n};\n```\n\n### `TaskGroupOptions`\n\n```ts\ntype TaskGroupOptions = {\n signal?: AbortSignal;\n};\n```\n\n### `TestWorkerOptions`\n\n```ts\ntype TestWorkerOptions = Omit<WorkerOptions, 'concurrency' | 'onSlotError'> & {\n concurrency?: number;\n};\n```\n\n### `TestWorkerCall`\n\n```ts\ntype TestWorkerCall<TInput, TOutput> =\n | { input: TInput; status: 'fulfilled'; value: TOutput }\n | { input: TInput; reason: unknown; status: 'rejected' };\n```\n\n### `TestWorkerHandle`\n\n```ts\ntype TestWorkerHandle<TInput, TOutput> = WorkerPool<TInput, TOutput> & {\n readonly calls: ReadonlyArray<TestWorkerCall<TInput, TOutput>>;\n};\n```\n\n### `SerializedError`\n\n```ts\ntype SerializedError = {\n message: string;\n name: string;\n stack?: string;\n};\n```\n\n### `WorkerRequest`\n\n```ts\ntype WorkerRequest<TInput> =\n | { id: number; input: TInput; kind: 'run'; version: 1 }\n | { id: number; input: TInput; kind: 'stream'; version: 1 };\n```\n\n### `WorkerResponse`\n\n```ts\ntype WorkerResponse<TOutput> =\n | { id: number; kind: 'chunk'; value: TOutput; version: 1 }\n | { error: SerializedError; id: number; kind: 'error'; version: 1 }\n | { id: number; kind: 'result'; value: TOutput; version: 1 };\n```\n\n### `TaskHandler` and `StreamHandler`\n\n```ts\ntype TaskHandler<TInput, TOutput> = (input: TInput) => TOutput | Promise<TOutput>;\ntype StreamHandler<TInput, TChunk> = (input: TInput) => AsyncIterable<TChunk> | Promise<AsyncIterable<TChunk>>;\n```\n\n## Errors\n\n| Error | Trigger | Notable property |\n| --- | --- | --- |\n| `FamiliarError` | Base class for all Familiar errors | `FamiliarError.is(error)` |\n| `FamiliarInvalidOptionsError` | Invalid factory or test options | — |\n| `FamiliarQueueFullError` | Queue limit reached with `onFull: 'reject'` | `maxQueue` |\n| `FamiliarTaskError` | Worker handler throws or payload cannot clone | `cause` |\n| `FamiliarTimeoutError` | Task or drain deadline expires | `timeoutMs` |\n| `FamiliarTerminatedError` | Pool is disposed or draining | — |\n| `FamiliarRuntimeError` | Worker API or worker process fails | `cause` |\n",
|
|
6
|
+
"usage": "---\ntitle: Familiar — Usage Guide\ndescription: Run task and stream module workers with bounded concurrency, cancellation, and test parity.\n---\n\n[[toc]]\n\n## Basic Usage\n\nPut task logic in a worker module. Imports and helpers stay normal module code.\n\n```ts\n// normalize.worker.ts\nimport { exposeTask } from '@vielzeug/familiar/protocol';\n\nimport { normalize } from './normalize';\n\nexposeTask((text: string) => normalize(text));\n```\n\nCreate one long-lived pool at its owner boundary.\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker<string, string>(new URL('./normalize.worker.ts', import.meta.url), {\n concurrency: 2,\n timeout: 2_000,\n});\n\ntry {\n const normalized = await pool.run(' Familiar ');\n console.log(normalized);\n} finally {\n pool.dispose();\n}\n```\n\n## Cancellation and Timeouts\n\nPass one signal to stop capacity waits, queued work, or active work. Cancelling active work terminates and lazily replaces its slot.\n\n```ts\nconst controller = new AbortController();\nconst result = pool.run('input', { signal: controller.signal, timeout: 500 });\n\ncontroller.abort();\nawait result.catch((error) => console.log(error.name)); // AbortError\n```\n\n## Queue Policy and Priority\n\nUse `maxQueue` to bound waiting work. Higher priorities dispatch first once a slot opens.\n\n```ts\nconst pool = createWorker<Job, Result>(new URL('./job.worker.ts', import.meta.url), {\n concurrency: 2,\n maxQueue: 100,\n onFull: 'wait',\n});\n\nawait pool.run(criticalJob, { priority: 10 });\n```\n\n## Batch and Groups\n\nCompose task pools with free helpers instead of carrying unrelated methods on every pool.\n\n```ts\nimport { batch, createTaskGroup } from '@vielzeug/familiar';\n\nfor await (const value of batch(pool, inputs)) {\n console.log(value);\n}\n\nconst group = createTaskGroup(pool, 'import');\nconst tasks = rows.map((row) => group.run(row));\nawait group.drain();\nawait Promise.all(tasks);\n```\n\n## Streaming\n\nStream workers have their own capability and registration helper.\n\n```ts\n// tokenize.worker.ts\nimport { exposeStream } from '@vielzeug/familiar/protocol';\n\nexposeStream(async function* (text: string) {\n for (const token of text.split(/\\s+/)) yield token;\n});\n```\n\n```ts\nimport { createStreamWorker } from '@vielzeug/familiar';\n\nconst pool = createStreamWorker<string, string>(new URL('./tokenize.worker.ts', import.meta.url));\nfor await (const token of pool.runStream('typed module workers')) {\n console.log(token);\n}\npool.dispose();\n```\n\n## Testing\n\nUse `createTestWorker()` when testing consumer code that depends on a task pool. It clones input/output, wraps task failures, and honors cancellation and timeout behavior.\n\n```ts\nimport { createTestWorker } from '@vielzeug/familiar/testing';\n\nconst pool = createTestWorker((value: number) => value * 2);\nawait expect(pool.run(21)).resolves.toBe(42);\nexpect(pool.calls).toEqual([{ input: 21, status: 'fulfilled', value: 42 }]);\npool.dispose();\n```\n\nTest worker-module business logic directly when possible. `createTestWorker()` does not run module files or support stream pools.\n\n## Framework Integration\n\nCreate a pool once per component lifetime. Abort obsolete requests during effect cleanup and dispose the pool on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useMemo } from 'react';\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = useMemo(() => createWorker(new URL('./sort.worker.ts', import.meta.url)), []);\n\nuseEffect(() => () => pool.dispose(), [pool]);\n```\n\n```ts [Vue]\nimport { onUnmounted } from 'vue';\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker(new URL('./sort.worker.ts', import.meta.url));\n\nonUnmounted(() => pool.dispose());\n```\n\n```ts [Svelte]\nimport { onDestroy } from 'svelte';\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker(new URL('./sort.worker.ts', import.meta.url));\n\nonDestroy(() => pool.dispose());\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse `@vielzeug/arsenal` async helpers in application orchestration. Keep worker module protocol registration in `@vielzeug/familiar/protocol`.\n\n## Best Practices\n\n- Put every task handler in its own module-worker boundary.\n- Reuse pools for repeated work; dispose owner-scoped pools.\n- Abort work made obsolete by navigation or newer input.\n- Transfer large binary buffers instead of cloning them.\n- Set explicit timeouts for work with a bounded latency budget.\n- Keep worker handlers deterministic and data-only.\n- Test module logic directly; test pool consumers with `createTestWorker()`.\n",
|
|
7
|
+
"examples": "---\ntitle: Familiar — Examples\ndescription: Module-worker recipes for familiar.\n---\n\n## Examples\n\n- [Fibonacci With Pool And Timeout](./examples/fibonacci-with-pool-and-timeout.md)\n- [Data Transformation Pipeline](./examples/data-transformation-pipeline.md)\n- [Image Processing](./examples/image-processing.md)\n- [Using Transferables](./examples/using-transferables.md)\n- [Cancellable Batch](./examples/cancellable-batch.md)\n- [Priority Queue](./examples/priority-queue.md)\n- [Streaming With Stream Worker](./examples/streaming-with-runstream.md)\n- [Module Worker](./examples/module-worker.md)\n- [Typed Error Handling](./examples/typed-error-handling.md)\n- [React Integration](./examples/react-integration.md)\n- [Testing With createTestWorker](./examples/testing-with-createtestworker.md)\n"
|
|
8
8
|
},
|
|
9
|
-
"examples": [
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "error-contracts",
|
|
12
|
+
"code": "import { FamiliarTimeoutError } from '@vielzeug/familiar'\n\nconst error = new FamiliarTimeoutError(500)\nconsole.log(error.name)\nconsole.log(error.timeoutMs)",
|
|
13
|
+
"name": "Familiar Error Contracts"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
10
16
|
"typeSignatures": {
|
|
11
|
-
"BatchOptions": "export type {\n BatchOptions,\n
|
|
12
|
-
"
|
|
13
|
-
"RunOptions": "export type {\n BatchOptions,\n
|
|
14
|
-
"
|
|
15
|
-
"TaskGroup": "export type {\n BatchOptions,\n
|
|
16
|
-
"
|
|
17
|
-
"WorkerOptions": "export type {\n BatchOptions,\n
|
|
18
|
-
"
|
|
17
|
+
"BatchOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
18
|
+
"DrainOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
19
|
+
"RunOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
20
|
+
"StreamWorkerPool": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
21
|
+
"TaskGroup": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
22
|
+
"TaskGroupOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
23
|
+
"WorkerOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
24
|
+
"WorkerPool": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
25
|
+
"WorkerStats": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
26
|
+
"WorkerStatus": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
|
|
19
27
|
"FamiliarError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
|
|
20
28
|
"FamiliarInvalidOptionsError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
|
|
21
29
|
"FamiliarQueueFullError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
|
|
@@ -23,8 +31,10 @@
|
|
|
23
31
|
"FamiliarTaskError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
|
|
24
32
|
"FamiliarTerminatedError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
|
|
25
33
|
"FamiliarTimeoutError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
34
|
+
"batch": "export { batch, createTaskGroup } from './_pool';",
|
|
35
|
+
"createTaskGroup": "export { batch, createTaskGroup } from './_pool';",
|
|
36
|
+
"RunningStream": "export type RunningStream<TChunk> = {\n done: Promise<void>;\n iterable: AsyncIterable<TChunk>;\n};",
|
|
37
|
+
"createWorker": "export function createWorker<TInput, TOutput>(\n url: URL | string,\n options: WorkerOptions = {},\n): WorkerPool<TInput, TOutput> {\n const resolved = resolveOptions(options);\n\n return createPool(slots<TInput, TOutput>(url, resolved), {\n concurrency: resolved.concurrency,\n defaultTimeout: resolved.timeout,\n maxQueue: resolved.maxQueue,\n onFull: resolved.onFull,\n });\n}",
|
|
38
|
+
"createStreamWorker": "export function createStreamWorker<TInput, TChunk>(\n url: URL | string,\n options: WorkerOptions = {},\n): StreamWorkerPool<TInput, TChunk> {\n const resolved = resolveOptions(options);\n\n return createStreamPool(slots<TInput, TChunk>(url, resolved), {\n concurrency: resolved.concurrency,\n defaultTimeout: resolved.timeout,\n maxQueue: resolved.maxQueue,\n onFull: resolved.onFull,\n });\n}"
|
|
29
39
|
}
|
|
30
40
|
}
|
package/data/packages/flux.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { toAsyncIterable } from './async';\nexport { stream } from './core';\nexport { FluxError, FluxTimeoutError } from './errors';\nexport { combineLatest, concat, merge } from './operators/combination';\nexport { from, fromEvent, interval, of, timer } from './operators/creation';\nexport type { IntervalOptions, TimerOptions } from './operators/creation';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering';\nexport type { DebounceOptions, TimeoutOptions } from './operators/filtering';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';\nexport type { ConcatMapOptions } from './operators/transformation';\nexport { first, last, retry, toArray } from './operators/utility';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';\nexport { pipe } from './pipe';\nexport type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Flux — Explicit push streams for TypeScript\ndescription: Reusable push streams with subscription-owned cancellation, bounded buffering, and optional ecosystem adapters.\npackage: flux\ncategory: reactive\nkeywords: [streams, reactive, operators, cancellation, buffering, channels]\nrelated: [ripple, herald, pulse, courier]\nexports: [stream, pipe, of, from, fromEvent, interval, timer, map, filter, scan, switchMap, mergeMap, concatMap, take, takeUntil, debounce, timeout, merge, concat, combineLatest, retry, toArray, first, last, toAsyncIterable]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"flux\" />\n\n## Why Flux?\n\nUse Flux when an API pushes many values over time and consumers need independent cancellation. Streams describe reusable work; subscriptions own cleanup. Explicit queue capacity keeps async iteration from silently growing memory.\n\n```ts\n// Before\nconst controller = new AbortController();\nconst render = (value: string) => console.log(value);\nconst handler = (event: Event) => render((event.target as HTMLInputElement).value);\ninput.addEventListener('input', handler);\nsetTimeout(() => controller.abort(), 5_000);\n\n// After\nimport { fromEvent, map, pipe, takeUntil } from '@vielzeug/flux';\n\nconst updates = pipe(\n fromEvent<InputEvent>(input, 'input'),\n map((event) => (event.target as HTMLInputElement).value),\n takeUntil(controller.signal),\n);\n\nupdates.subscribe({ error: console.error, next: render });\n```\n\n| Feature | Flux | RxJS | TC39 Observable |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"flux\" type=\"size\" /> | Varies by imported operators | Native proposal / polyfill |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Subscription-owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit async queue policy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Operator-dependent | No standard policy |\n| Vielzeug adapters | Ripple, Courier, Herald, Pulse | Manual adapters | Manual adapters |\n\n<div class=\"decision-callout\">\n\n**Use Flux when** you need a small TypeScript stream primitive, explicit cancellation, and first-party Vielzeug adapters.\n\n**Consider RxJS when** you need its larger operator catalog or third-party Observable integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/flux\n```\n\n```sh [npm]\nnpm install @vielzeug/flux\n```\n\n```sh [yarn]\nyarn add @vielzeug/flux\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { toArray, interval, map, pipe, take } from '@vielzeug/flux';\n\nconst firstThree = pipe(\n interval({ every: 100 }),\n map((value) => value * 2),\n take(3),\n);\n\ntry {\n console.log(await toArray(firstThree, { maxItems: 3 })); // [0, 2, 4]\n} catch (reason) {\n console.error('Stream failed', reason);\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `stream()` — define cold reusable work with one teardown function\n- `pipe()` — compose any number of typed operators\n- `Subscription` — own cancellation through `unsubscribe()` or `AbortSignal`\n- `createChannel()` — mutable multicast state with bounded replay\n- `toAsyncIterable()` — explicit capacity and overflow policy for pull consumers\n- `retry()` — retry failures with optional backoff\n- `fromSignal()` / `toSignal()` — bridge Ripple signals\n- `fromQuery()` / `fromSse()` — adapt Courier state and events\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\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — adapt reactive signal state through `@vielzeug/flux/ripple`.\n- [Courier](/courier/) — adapt query snapshots and SSE events through `@vielzeug/flux/courier`.\n- [Herald](/herald/) — adapt typed bus events through `@vielzeug/flux/herald`.\n- [Pulse](/pulse/) — adapt connection and presence events through `@vielzeug/flux/pulse`.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Flux — Explicit push streams for TypeScript\ndescription: Reusable push streams with subscription-owned cancellation, bounded buffering, and optional ecosystem adapters.\npackage: flux\ncategory: reactive\nkeywords: [streams, reactive, operators, cancellation, buffering, channels]\nrelated: [ripple, herald, pulse, courier]\nexports: [stream, pipe, of, from, fromEvent, interval, timer, map, filter, scan, switchMap, mergeMap, concatMap, take, takeUntil, debounce, timeout, merge, concat, combineLatest, retry, toArray, first, last, toAsyncIterable]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"flux\" />\n\n## Why Flux?\n\nUse Flux when an API pushes many values over time and consumers need independent cancellation. Streams describe reusable work; subscriptions own cleanup. Explicit queue capacity keeps async iteration from silently growing memory.\n\n```ts\n// Before\nconst controller = new AbortController();\nconst render = (value: string) => console.log(value);\nconst handler = (event: Event) => render((event.target as HTMLInputElement).value);\ninput.addEventListener('input', handler);\nsetTimeout(() => controller.abort(), 5_000);\n\n// After\nimport { fromEvent, map, pipe, takeUntil } from '@vielzeug/flux';\n\nconst updates = pipe(\n fromEvent<InputEvent>(input, 'input'),\n map((event) => (event.target as HTMLInputElement).value),\n takeUntil(controller.signal),\n);\n\nupdates.subscribe({ error: console.error, next: render });\n```\n\n| Feature | Flux | RxJS | TC39 Observable |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"flux\" type=\"size\" /> | Varies by imported operators | Native proposal / polyfill |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Subscription-owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit async queue policy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Operator-dependent | No standard policy |\n| Vielzeug adapters | Ripple, Courier, Herald, Pulse | Manual adapters | Manual adapters |\n\n<div class=\"decision-callout\">\n\n**Use Flux when** you need a small TypeScript stream primitive, explicit cancellation, and first-party Vielzeug adapters.\n\n**Consider RxJS when** you need its larger operator catalog or third-party Observable integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/flux\n```\n\n```sh [npm]\nnpm install @vielzeug/flux\n```\n\n```sh [yarn]\nyarn add @vielzeug/flux\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { toArray, interval, map, pipe, take } from '@vielzeug/flux';\n\nconst firstThree = pipe(\n interval({ every: 100 }),\n map((value) => value * 2),\n take(3),\n);\n\ntry {\n console.log(await toArray(firstThree, { maxItems: 3 })); // [0, 2, 4]\n} catch (reason) {\n console.error('Stream failed', reason);\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `stream()` — define cold reusable work with one teardown function\n- `pipe()` — compose any number of typed operators\n- `Subscription` — own cancellation through `unsubscribe()` or `AbortSignal`\n- `createChannel()` — mutable multicast state with bounded replay\n- `toAsyncIterable()` — explicit capacity and overflow policy for pull consumers\n- `retry()` — retry failures with optional backoff\n- `fromSignal()` / `toSignal()` — bridge Ripple signals\n- `fromQuery()` / `fromSse()` — adapt Courier state and events\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/) — adapt reactive signal state through `@vielzeug/flux/ripple`.\n- [Courier](/courier/) — adapt query snapshots and SSE events through `@vielzeug/flux/courier`.\n- [Herald](/herald/) — adapt typed bus events through `@vielzeug/flux/herald`.\n- [Pulse](/pulse/) — adapt connection and presence events through `@vielzeug/flux/pulse`.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Flux — API Reference\ndescription: Complete reference for @vielzeug/flux streams, operators, channels, and adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `stream()` | Create cold stream | Lazy | Return one teardown function |\n| `pipe()` | Compose operators | Lazy | Source is first argument |\n| `of()` / `from()` | Convert known values | Sync / mixed | `from()` promise cannot be aborted |\n| `fromEvent()` | Adapt event target | Async | Unsubscribe removes listener |\n| `interval()` / `timer()` | Create timed values | Async | Use `take()` or unsubscribe for intervals |\n| `map()` / `filter()` / `scan()` | Transform values | Sync | Callback throws terminate stream |\n| `switchMap()` / `mergeMap()` / `concatMap()` | Flatten streams | Mixed | `concatMap()` queue is bounded |\n| `take()` / `takeUntil()` | Stop values | Mixed | Notifier emission completes output |\n| `debounce()` / `timeout()` / `retry()` | Control time and failures | Async | `timeout()` measures inactivity |\n| `merge()` / `concat()` / `combineLatest()` | Combine streams | Mixed | `combineLatest()` waits for every source |\n| `toArray()` / `first()` / `last()` | Consume finite values | Async | Bound `toArray()` with `maxItems` |\n| `toAsyncIterable()` | Use `for await` | Async | Capacity and overflow required |\n| `createChannel()` | Imperative multicast boundary | Sync | Dispose to complete subscribers |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/flux` | Core streams, operators, consumers, errors, and types |\n| `@vielzeug/flux/async` | `toAsyncIterable()` only |\n| `@vielzeug/flux/subjects` | `createChannel()` and channel types |\n| `@vielzeug/flux/ripple` | Ripple signal adapters |\n| `@vielzeug/flux/courier` | Courier query and SSE adapters |\n| `@vielzeug/flux/herald` | Herald bus adapters |\n| `@vielzeug/flux/pulse` | Pulse event and presence adapters |\n\n## Core\n\n### `stream()`\n\n```ts\nstream<T>(producer: Producer<T>): Stream<T>\n```\n\nCreates cold reusable work. Producer runs once for every subscription.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `producer` | `Producer<T>` | Emits through sink and returns optional teardown |\n\n**Returns:** `Stream<T>`.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst ticks = stream<number>((sink) => {\n const id = setInterval(() => sink.next(Date.now()), 1_000);\n return () => clearInterval(id);\n});\n```\n\n---\n\n### `pipe()`\n\n```ts\npipe<Input, Operators>(source: Stream<Input>, ...operators: Operators): Stream<Output>\n```\n\nApplies operators left to right while inferring output value type.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `Stream<Input>` | Source stream |\n| `operators` | `Operator[]` | Operators applied in order |\n\n**Returns:** transformed `Stream<Output>`.\n\n```ts\nimport { map, of, pipe } from '@vielzeug/flux';\n\nconst labels = pipe(of(1, 2), map((value) => `#${value}`));\n```\n\n## Creation\n\n### `of()`\n\n```ts\nof<T>(...values: T[]): Stream<T>\n```\n\nEmits every value synchronously, then completes.\n\n```ts\nimport { of } from '@vielzeug/flux';\n\nof(1, 2, 3).subscribe(console.log);\n```\n\n---\n\n### `from()`\n\n```ts\nfrom<T>(source: Iterable<T> | AsyncIterable<T> | Promise<T>): Stream<T>\n```\n\nConverts iterable, async iterable, or promise into a stream. Cancellation stops iterable consumption and calls `return()` when available.\n\n```ts\nimport { from } from '@vielzeug/flux';\n\nfrom(Promise.resolve('ready')).subscribe({ error: console.error, next: console.log });\n```\n\n---\n\n### `fromEvent()`\n\n```ts\nfromEvent<T = Event>(target, type: string): Stream<T>\n```\n\nEmits target events until subscription ends.\n\n```ts\nimport { fromEvent } from '@vielzeug/flux';\n\nfromEvent<MouseEvent>(document, 'click').subscribe(console.log);\n```\n\n---\n\n### `interval()`\n\n```ts\ninterval(options: IntervalOptions): Stream<number>\n```\n\nEmits incrementing values starting at zero.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `every` | `number` | Non-negative interval duration in milliseconds |\n\n---\n\n### `timer()`\n\n```ts\ntimer(options: TimerOptions): Stream<number>\n```\n\nEmits zero after `delay`; optionally continues at `interval`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `delay` | `number` | Non-negative initial delay in milliseconds |\n| `interval` | `number` | Optional non-negative repeat duration |\n\n## Transformation Operators\n\n### `map()`\n\n```ts\nmap<A, B>(project: (value: A) => B): Operator<A, B>\n```\n\nMaps every value. A thrown callback error terminates output.\n\n---\n\n### `filter()`\n\n```ts\nfilter<T>(predicate: (value: T) => boolean): Operator<T, T>\n```\n\nForwards values matching predicate.\n\n---\n\n### `scan()`\n\n```ts\nscan<T, A>(reducer: (state: A, value: T) => A, initial: A): Operator<T, A>\n```\n\nEmits accumulated state after every source value.\n\n---\n\n### `switchMap()`\n\n```ts\nswitchMap<A, B>(project: (value: A) => Stream<B>): Operator<A, B>\n```\n\nCancels previous inner stream when source emits.\n\n---\n\n### `mergeMap()`\n\n```ts\nmergeMap<A, B>(project: (value: A) => Stream<B>): Operator<A, B>\n```\n\nRuns every inner stream concurrently.\n\n---\n\n### `concatMap()`\n\n```ts\nconcatMap<A, B>(project: (value: A) => Stream<B>, options: ConcatMapOptions): Operator<A, B>\n```\n\nRuns inner streams in order. Exceeding capacity errors output.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive maximum queued source values |\n\n## Control Operators\n\n### `take()`\n\n```ts\ntake<T>(count: number): Operator<T, T>\n```\n\nForwards `count` values, cancels upstream, then completes. Count must be non-negative integer.\n\n---\n\n### `takeUntil()`\n\n```ts\ntakeUntil<T>(notifier: AbortSignal | Stream<unknown>): Operator<T, T>\n```\n\nCompletes when notifier aborts or emits.\n\n---\n\n### `debounce()`\n\n```ts\ndebounce<T>(options: DebounceOptions): Operator<T, T>\n```\n\nEmits latest value after configured silence. Pending value flushes on source completion.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `for` | `number` | Non-negative silence duration in milliseconds |\n\n---\n\n### `timeout()`\n\n```ts\ntimeout<T>(options: TimeoutOptions): Operator<T, T>\n```\n\nErrors with `FluxTimeoutError` when source is silent too long.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `after` | `number` | Non-negative inactivity duration in milliseconds |\n\n---\n\n### `retry()`\n\n```ts\nretry<T>(options: RetryOptions): Operator<T, T>\n```\n\nResubscribes after source errors until attempts are exhausted.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `attempts` | `number` | Non-negative retry count |\n| `delay` | `number \\| (attempt: number) => number` | Optional delay or backoff function |\n\n## Combination\n\n### `merge()`\n\n```ts\nmerge<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nForwards values from all sources and completes after every source completes.\n\n---\n\n### `concat()`\n\n```ts\nconcat<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nSubscribes to each source only after previous source completes.\n\n---\n\n### `combineLatest()`\n\n```ts\ncombineLatest<T extends readonly Stream<unknown>[]>(...sources: T): Stream<{ [K in keyof T]: T[K] extends Stream<infer V> ? V : never }>\n```\n\nEmits latest tuple after every source emits once. Completes without emission when a source completes before first value.\n\n## Value Consumers\n\n### `toArray()`\n\n```ts\ntoArray<T>(source: Stream<T>, options: ToArrayOptions): Promise<T[]>\n```\n\nCollects finite output. Rejects on source error, abort, or `maxItems` overflow.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `maxItems` | `number` | Non-negative maximum toArrayed values |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n---\n\n### `first()`\n\n```ts\nfirst<T>(source: Stream<T>, options?: ValueOptions): Promise<T>\n```\n\nResolves first value and cancels source. Rejects on source error or abort.\n\n---\n\n### `last()`\n\n```ts\nlast<T>(source: Stream<T>, options?: ValueOptions): Promise<T | undefined>\n```\n\nResolves last value on completion, or `undefined` when source completes empty.\n\n## Async Conversion\n\n### `toAsyncIterable()`\n\n```ts\ntoAsyncIterable<T>(source: Stream<T>, options: AsyncIterableOptions): AsyncIterable<T>\n```\n\nConverts push stream to async iterable with bounded queue.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive queue capacity |\n| `overflow` | `OverflowPolicy` | `error`, `drop-oldest`, or `drop-newest` |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n## Channels\n\n### `createChannel()`\n\n```ts\ncreateChannel<T>(options?: ChannelOptions<T>): Channel<T>\n```\n\nCreates imperative multicast boundary. Disposal completes subscribers.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `initial` | `T` | Optional initial replay value |\n| `replay` | `number` | Non-negative retained value count |\n\n## Adapters\n\n### `@vielzeug/flux/ripple`\n\n```ts\nfromSignal<T>(source: Readable<T>): Stream<T>\ntoSignal<T>(source: Stream<T>, options: ToSignalOptions<T>): SignalBinding<T>\n```\n\n`fromSignal()` emits current value first. `toSignal()` preserves final value then disposes binding when source completes, errors, or supplied signal aborts.\n\n### `@vielzeug/flux/courier`\n\n```ts\nfromQuery<T extends { key: readonly unknown[]; fetch: (...args: never[]) => Promise<unknown> }>(\n cache: { getSnapshot<T>(key: readonly unknown[]): T | null; subscribe(key: readonly unknown[], listener: () => void): () => void },\n definition: T,\n): Stream<AsyncState<Awaited<ReturnType<T['fetch']>>> | null>\nfromSse<T>(source: AsyncIterable<{ data: T; event: string }>, event: string): Stream<T>\n```\n\n`fromQuery()` infers data from `definition.fetch` and emits Courier-compatible `AsyncState` snapshots.\n\n### `@vielzeug/flux/herald`\n\n```ts\nfromBus<T extends EventMap, K extends EventKey<T>>(bus: Bus<T>, event: K): Stream<T[K]>\ntoBus<T extends EventMap, K extends EventKey<T>>(bus: Bus<T>, event: K): Operator<T[K], T[K]>\n```\n\n### `@vielzeug/flux/pulse`\n\n```ts\nfromPulse<T extends MessageMap, K extends EventKey<T>>(pulse: Pulse<T>, event: K): Stream<T[K]>\nfromPresence<T>(presence: PresenceChannel<T>): Stream<ReadonlyMap<string, T>>\n```\n\n## Types\n\n```ts\ntype Teardown = () => void;\n\ntype Subscription = {\n [Symbol.dispose](): void;\n readonly closed: boolean;\n unsubscribe(): void;\n};\n\ntype Observer<T> = {\n complete?: () => void;\n error?: (reason: unknown) => void;\n next: (value: T) => void;\n};\n\ntype SubscribeOptions = { signal?: AbortSignal };\n\ntype Sink<T> = {\n complete(): void;\n error(reason: unknown): void;\n next(value: T): void;\n};\n\ntype Producer<T> = (sink: Sink<T>, signal: AbortSignal) => Teardown | void;\ntype Operator<A = unknown, B = unknown> = (source: Stream<A>) => Stream<B>;\n\ninterface Stream<T> {\n subscribe(observer: Observer<T> | ((value: T) => void), options?: SubscribeOptions): Subscription;\n}\n\ntype OverflowPolicy = 'drop-newest' | 'drop-oldest' | 'error';\ntype AsyncIterableOptions = { capacity: number; overflow: OverflowPolicy; signal?: AbortSignal };\ntype IntervalOptions = { every: number };\ntype TimerOptions = { delay: number; interval?: number };\ntype DebounceOptions = { for: number };\ntype TimeoutOptions = { after: number };\ntype ConcatMapOptions = { capacity: number };\ntype RetryOptions = { attempts: number; delay?: number | ((attempt: number) => number) };\ntype ToArrayOptions = { maxItems: number; signal?: AbortSignal };\ntype ValueOptions = { signal?: AbortSignal };\ntype ChannelOptions<T> = { initial?: T; replay?: number };\n```\n\n## Errors\n\n### `FluxError`\n\nBase Flux error. Use `FluxError.is(reason)` to narrow unknown values.\n\n### `FluxTimeoutError`\n\nRaised by `timeout()`. `ms` contains configured inactivity duration.\n",
|
|
6
6
|
"usage": "---\ntitle: Flux — Usage Guide\ndescription: Create streams, compose operators, consume values safely, and bridge Vielzeug primitives.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine one cold stream. Return teardown work from producer. Every subscription runs producer independently.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst clock = stream<number>((sink) => {\n let value = 0;\n const id = setInterval(() => sink.next(value++), 1_000);\n\n return () => clearInterval(id);\n});\n\nconst subscription = clock.subscribe({\n error: console.error,\n next: console.log,\n});\n\nsubscription.unsubscribe();\n```\n\nPass `AbortSignal` when another owner controls lifetime.\n\n```ts\nconst controller = new AbortController();\nclock.subscribe(console.log, { signal: controller.signal });\ncontroller.abort();\n```\n\n## Compose Streams\n\nPass source first to `pipe()`. Operators retain inferred value types across chains.\n\n```ts\nimport { filter, fromEvent, map, pipe, take } from '@vielzeug/flux';\n\nconst clicks = pipe(\n fromEvent<MouseEvent>(document, 'click'),\n filter((event) => event.button === 0),\n map((event) => ({ x: event.clientX, y: event.clientY })),\n take(10),\n);\n\nclicks.subscribe({\n complete: () => console.log('done'),\n error: console.error,\n next: console.log,\n});\n```\n\nUse `switchMap()` for latest-only work, `mergeMap()` for concurrent work, and `concatMap()` for ordered work with bounded queue capacity.\n\n```ts\nimport { from, pipe, retry, switchMap } from '@vielzeug/flux';\n\nconst results = pipe(\n queries,\n switchMap((query) => from(fetch(`/api/search?q=${encodeURIComponent(query)}`).then((response) => response.json()))),\n retry({ attempts: 2, delay: (attempt) => 250 * (attempt + 1) }),\n);\n```\n\n## Consume Values\n\nUse bounded array conversion for finite streams. `toArray()` rejects once source exceeds `maxItems`.\n\n```ts\nimport { toArray, of } from '@vielzeug/flux';\n\ntry {\n const values = await toArray(of(1, 2, 3), { maxItems: 3 });\n console.log(values);\n} catch (reason) {\n console.error('Collection failed', reason);\n}\n```\n\nUse `first()` for first emission and `last()` for last value before completion. Pass `{ signal }` to cancel waiting; cancellation rejects with `AbortError`.\n\n## Channels\n\nUse channels only at imperative boundaries. Expose `channel.stream` to consumers; keep `send()` near event producer.\n\n```ts\nimport { createChannel } from '@vielzeug/flux/subjects';\n\nconst status = createChannel({ initial: 'starting', replay: 1 });\nstatus.stream.subscribe({ error: console.error, next: console.log });\nstatus.send('ready');\nstatus.dispose();\n```\n\nDisposal completes active and future subscribers. Replay retains only configured latest values.\n\n## Async Iteration and Bounds\n\nConvert push stream only when pull syntax is required. Capacity and overflow policy are mandatory.\n\n```ts\nimport { interval, toAsyncIterable } from '@vielzeug/flux';\n\nconst values = toAsyncIterable(interval({ every: 100 }), {\n capacity: 32,\n overflow: 'error',\n});\n\nfor await (const value of values) {\n console.log(value);\n if (value === 2) break;\n}\n```\n\n`return()` from loop permanently completes iterator. Use `drop-oldest` or `drop-newest` only when loss is acceptable.\n\n## Testing\n\nUse fake timers for time operators. Test producer cleanup through returned subscription.\n\n```ts\nimport { expect, it, vi } from 'vitest';\nimport { first, pipe, stream, timeout } from '@vielzeug/flux';\n\nit('fails after inactivity', async () => {\n vi.useFakeTimers();\n const result = first(pipe(stream(() => {}), timeout({ after: 500 })));\n const expectation = expect(result).rejects.toThrow('Timeout after 500ms');\n\n await vi.advanceTimersByTimeAsync(500);\n await expectation;\n vi.useRealTimers();\n});\n```\n\n## Framework Integration\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useState } from 'react';\nimport type { Stream } from '@vielzeug/flux';\n\nexport function useStream<T>(source: Stream<T>, initial: T): T {\n const [value, setValue] = useState(initial);\n\n useEffect(() => {\n const subscription = source.subscribe({ error: console.error, next: setValue });\n return () => subscription.unsubscribe();\n }, [source]);\n\n return value;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport type { Stream } from '@vielzeug/flux';\n\nexport function useStream<T>(source: Stream<T>, initial: T) {\n const value = ref(initial);\n const subscription = source.subscribe({ error: console.error, next: (next) => (value.value = next) });\n\n onUnmounted(() => subscription.unsubscribe());\n\n return value;\n}\n```\n\n```ts [Svelte]\nimport type { Stream } from '@vielzeug/flux';\n\nexport function streamStore<T>(source: Stream<T>, initial: T) {\n return {\n subscribe(run: (value: T) => void) {\n run(initial);\n const subscription = source.subscribe({ error: console.error, next: run });\n return () => subscription.unsubscribe();\n },\n };\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nImport adapters from dedicated subpaths. Core Flux does not require adapter peers.\n\n```ts\nimport { fromQuery } from '@vielzeug/flux/courier';\nimport { fromBus } from '@vielzeug/flux/herald';\nimport { fromPresence } from '@vielzeug/flux/pulse';\nimport { fromSignal, toSignal } from '@vielzeug/flux/ripple';\n```\n\n`toSignal()` preserves final source value, then disposes binding when source completes, errors, or external signal aborts.\n\n## Best Practices\n\n- Return one idempotent producer teardown function.\n- Pass `{ signal }` from component, request, or task owner.\n- Provide `error` when subscription can recover locally.\n- Use `pipe(source, ...)`; never mutate stream definitions.\n- Bound `concatMap()` queue capacity.\n- Bound `toArray()` with realistic `maxItems`.\n- Choose async iterator overflow policy deliberately.\n- Keep `Channel.send()` at integration boundaries.\n",
|
|
7
7
|
"examples": "---\ntitle: Flux — Examples\ndescription: Practical examples and recipes for @vielzeug/flux.\n---\n\n## Examples\n\n- [Debounced Search Input](./examples/debounce-search.md)\n- [Combining Streams with combineLatest](./examples/combine-streams.md)\n- [Ripple Signal Integration](./examples/signal-integration.md)\n"
|
package/data/packages/forge.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport * from './types';\nexport { createForm } from './form';\nexport { toFormData } from './adapters/form-data';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Forge — Immutable form state for TypeScript\ndescription: Framework-agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form-state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createForm, toFormData, bindField, customValidator, saveForm, loadForm]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"forge\" />\n\n## Why Forge?\n\nNative form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// Before\nconst values = { email: '', password: '' };\nconst errors: Record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'Invalid email';\n errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';\n}\n\n// After\nconst form = createForm({\n initialValues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'Invalid email',\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n },\n }),\n});\n```\n\n| Feature | Forge | Native form state | Framework-owned form state |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"forge\" type=\"size\" /> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Varies |\n| Zero external dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Immutable nested values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Typed object field handles | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Framework-independent state | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <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 Forge when** form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.\n\n**Consider framework-owned form state when** application only needs a single UI framework's native input bindings.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\nInstall `@vielzeug/spell` or `@vielzeug/vault` only when importing Forge's matching optional adapter.\n\n## Quick Start\n\nCreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: JSON.stringify(value),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n });\n\n return response.ok;\n});\n\nif (!result.ok && result.type === 'validation') console.log(result.errors);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `form.value` exposes one immutable nested value tree.\n- `form.field(key)` selects typed object branches without string paths.\n- `field.set(updater)` replaces array values without index handles.\n- `form.validate()` returns valid, invalid, or aborted results.\n- `form.submit(handler)` touches, validates, and invokes the handler when valid.\n- `bindField()` connects one DOM element without owning validation timing.\n- `customValidator()` maps Spell schema errors into Forge fields.\n- `saveForm()` and `loadForm()` persist explicit Vault draft records.\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\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — adapt a Spell schema through `customValidator()`.\n- [Vault](/vault/) — save and restore explicit Forge draft records.\n- [Courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
|
|
4
|
+
"index": "---\ntitle: Forge — Immutable form state for TypeScript\ndescription: Framework-agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form-state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createForm, toFormData, bindField, customValidator, saveForm, loadForm]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"forge\" />\n\n## Why Forge?\n\nNative form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// Before\nconst values = { email: '', password: '' };\nconst errors: Record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'Invalid email';\n errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';\n}\n\n// After\nconst form = createForm({\n initialValues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'Invalid email',\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n },\n }),\n});\n```\n\n| Feature | Forge | Native form state | Framework-owned form state |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"forge\" type=\"size\" /> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Varies |\n| Zero external dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Immutable nested values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Typed object field handles | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Framework-independent state | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <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 Forge when** form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.\n\n**Consider framework-owned form state when** application only needs a single UI framework's native input bindings.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\nInstall `@vielzeug/spell` or `@vielzeug/vault` only when importing Forge's matching optional adapter.\n\n## Quick Start\n\nCreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: JSON.stringify(value),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n });\n\n return response.ok;\n});\n\nif (!result.ok && result.type === 'validation') console.log(result.errors);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `form.value` exposes one immutable nested value tree.\n- `form.field(key)` selects typed object branches without string paths.\n- `field.set(updater)` replaces array values without index handles.\n- `form.validate()` returns valid, invalid, or aborted results.\n- `form.submit(handler)` touches, validates, and invokes the handler when valid.\n- `bindField()` connects one DOM element without owning validation timing.\n- `customValidator()` maps Spell schema errors into Forge fields.\n- `saveForm()` and `loadForm()` persist explicit Vault draft records.\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- [Spell](/spell/) — adapt a Spell schema through `customValidator()`.\n- [Vault](/vault/) — save and restore explicit Forge draft records.\n- [Courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
|
|
5
5
|
"api": "---\ntitle: Forge — API Reference\ndescription: Complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createForm()` | Create immutable form state | Sync | `initialValues` cannot contain mutable class instances |\n| `form.field()` | Select a top-level or object child field | Sync | Arrays have no index field handles |\n| `form.validate()` | Validate complete value | Async | Handle `aborted` separately |\n| `form.submit()` | Touch, validate, then invoke handler | Async | Concurrent calls reject |\n| `form.reset()` | Restore or replace baseline | Sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | Observe form metadata | Sync | Throws after disposal |\n| `toFormData()` | Serialize values for multipart transport | Sync | `FileList` is transport-only |\n| `debugForm()` | Log public state transitions | Sync | Import from `/devtools` |\n| `bindField()` | Bind one DOM element | Sync | Does not schedule validation |\n| `customValidator()` | Adapt a Spell schema | Async | Does not transform `form.value` |\n| `saveForm()` / `loadForm()` | Persist explicit Vault records | Async | FormDraftCodec owns record shape |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/forge` | Core form factory, serialization helper, types, and errors |\n| `@vielzeug/forge/devtools` | `debugForm()` |\n| `@vielzeug/forge/dom` | `bindField()` and DOM binding types |\n| `@vielzeug/forge/spell` | `customValidator()` |\n| `@vielzeug/forge/vault` | `saveForm()`, `loadForm()`, and `FormDraftCodec` |\n\n## Core Functions\n\n### `createForm(options)`\n\n```ts\nfunction createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues>;\n```\n\nCreates a form with immutable initial values and an optional full-form validator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.initialValues` | `TValues` | Initial value and reset baseline. Supports primitives, plain objects, arrays, `File`, and `Blob`. |\n| `options.validate` | `FormValidator<TValues>` | Optional validator for the entire current value. |\n| `options.onSubscriberError` | `(error: unknown) => void` | Optional subscriber failure reporter. |\n\n**Returns:** `Form<TValues>`.\n\n**Example:**\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n```\n\n---\n\n### `toFormData(values)`\n\n```ts\nfunction toFormData(values: Record<string, unknown>): FormData;\n```\n\nConverts nested values into `FormData` with dot-separated object keys and repeated array keys.\n\n**Returns:** a populated `FormData` instance.\n\n**Example:**\n\n```ts\nimport { toFormData } from '@vielzeug/forge';\n\nconst body = toFormData({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## Form Handles\n\n### `Form<TValues>`\n\n`createForm()` returns this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<TValues>` | Current immutable value. |\n| `state` | `FormState<TValues>` | Submission, validation, touch, and error metadata. |\n| `field(key)` | `Field<TValues[K]>` | Select a top-level field. |\n| `set(next)` | `void` | Replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | Restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `Promise<ValidationResult<TValues>>` | Run full-form validation. |\n| `submit(handler)` | `Promise<SubmitResult<TResult, TValues>>` | Touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe form state; throws after disposal. |\n| `dispose()` | `void` | Abort validation and clear subscribers. |\n| `disposed` | `boolean` | Whether the form has been disposed. |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal. |\n\n### `Field<V>`\n\n`form.field(key)` and object-field `.field(key)` return this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<V>` | Current immutable branch value. |\n| `error` | `string \\| undefined` | Current field error. |\n| `dirty` | `boolean` | Whether branch differs from baseline. |\n| `touched` | `boolean` | Whether field was touched. |\n| `field(key)` | `Field<V[K]>` | Select child object field only. |\n| `set(next)` | `void` | Replace branch or derive a replacement. |\n| `reset()` | `void` | Restore exact baseline branch. |\n| `touch()` | `void` | Mark field touched. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe field transitions; throws after disposal. |\n\n## Validation Results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n```\n\nRuns the configured validator against the complete value. A newer validation aborts the older run.\n\n**Returns:** `ValidationResult<TValues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formError);\n```\n\n### `form.submit(handler)`\n\n```ts\nfunction submit<TResult>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;\n```\n\nTouches all fields, validates once, and invokes `handler` when validation is valid.\n\n**Returns:** `SubmitResult<TResult, TValues>`. Handler failures reject normally.\n\n```ts\nconst result = await form.submit((value) => Promise.resolve(value));\n```\n\n## Devtools and Adapters\n\n### `debugForm(form, options?)`\n\n```ts\nfunction debugForm<TValues extends Record<string, unknown>>(\n form: Form<TValues>,\n options?: ForgeDevtoolsOptions,\n): Unsubscribe;\n```\n\nLogs public validity, validation, and submission transitions through `console.debug`.\n\n**Example:**\n\n```ts\nimport { debugForm } from '@vielzeug/forge/devtools';\n\nconst stop = debugForm(form, { label: 'checkout' });\nstop();\n```\n\n---\n\n### `bindField(element, field, options)`\n\n```ts\nfunction bindField<Element extends HTMLElement, V>(\n element: Element,\n field: Field<V>,\n options: FieldBindingOptions<Element, V>,\n): Unsubscribe;\n```\n\nBinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**Example:**\n\n```ts\nimport { bindField } from '@vielzeug/forge/dom';\n\nconst stop = bindField(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n---\n\n### `customValidator(schema)`\n\n```ts\nfunction customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues>,\n): FormValidator<TValues>;\n```\n\nAdapts a Spell schema. Every failing union maps its closest branch while preserving unrelated errors. Array item issues map to the parent array field; duplicate paths retain the first message.\n\n**Example:**\n\n```ts\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n---\n\n### `saveForm()` and `loadForm()`\n\n```ts\nfunction saveForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, codec: FormDraftCodec<TValues, S, K>,\n): Promise<void>;\n\nfunction loadForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, key: KeyOf<S, K>, codec: FormDraftCodec<TValues, S, K>,\n): Promise<boolean>;\n```\n\nPersists or restores a codec-defined Vault record. `loadForm()` calls `form.reset()` when the codec decodes a record.\n\n**Returns:** `loadForm()` returns `false` for a missing or rejected record.\n\n## Types\n\n```ts\ntype Unsubscribe = () => void;\ntype MaybePromise<T> = T | PromiseLike<T>;\ntype ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;\n\ntype FormErrors<T> = T extends readonly unknown[]\n ? string\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;\n\ntype ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;\n\ntype FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>, signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;\n\ntype FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<TValues>;\n}>;\n\ntype SubscribeOptions = Readonly<{ immediate?: boolean }>;\n\ntype FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;\n\ntype FormState<TValues extends Record<string, unknown> = Record<string, unknown>> = Readonly<{\n error: string | undefined;\n errors: FormErrors<TValues> | undefined;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n valid: boolean;\n validating: boolean;\n}>;\n\ntype ValidationResult<TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;\n\ntype SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ ok: true; value: TResult }>\n | Readonly<{ ok: false; type: 'aborted' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; ok: false; type: 'validation' }>;\n```\n\n```ts\ntype Field<V> = {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]>;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n};\n\ntype Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly state: FormState<TValues>;\n readonly value: ReadonlyDeep<TValues>;\n dispose(): void;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n};\n\ntype ForgeDevtoolsOptions = Readonly<{ label?: string }>;\n\ntype FieldBindingOptions<Element extends HTMLElement, V> = Readonly<{\n event?: keyof HTMLElementEventMap;\n read(element: Element): V;\n write?: (element: Element, value: ReadonlyDeep<V>) => void;\n}>;\n\ntype FormDraftCodec<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string> = Readonly<{\n fromRecord(record: RecordOf<S, K>): TValues | undefined;\n toRecord(values: ReadonlyDeep<TValues>): RecordOf<S, K>;\n}>;\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `ForgeError` | Base Forge error | `ForgeError.is(error)` narrows unknown values. |\n| `ForgeConfigError` | Unsafe key or unsupported form value | Extends `ForgeError`. |\n| `ForgeDisposedError` | Operation or subscription after disposal | Message names the attempted operation. |\n| `ForgeSubmitError` | Concurrent `submit()` call | Extends `ForgeError`. |\n| `ForgeValidationError` | Validator throws unexpectedly | Preserves original error as `cause`. |\n",
|
|
6
6
|
"usage": "---\ntitle: Forge — Usage Guide\ndescription: Build immutable forms, validate whole values, and use optional adapters.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one form value and update object branches through stable typed operations. Form values support primitives, plain objects, arrays, `File`, and `Blob`; mutable class instances such as `Date`, `Map`, and `Set` are rejected.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' }, tags: [] as string[] },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nconst email = form.field('profile').field('email');\nemail.set('ada@example.com');\nform.field('tags').set((tags) => [...tags, 'typescript']);\n\nconsole.log(form.value.profile.email);\n```\n\n## Reset Values and Branches\n\nReset a field when one branch should return to its exact baseline. Reset the form with a value when newly loaded data should become the clean baseline.\n\n```ts\nconst name = form.field('profile').field('name');\n\nname.set('Ada');\nname.touch();\nname.reset();\n\nform.reset({ profile: { email: 'ada@example.com', name: 'Ada' }, tags: [] });\n```\n\nAn absent optional parent remains absent after a child reset. Arrays are complete values; replace them with an updater instead of retaining index handles.\n\n## Validate and Submit\n\nReturn `fields` and an optional `formError` from one validator. `validate()` replaces the complete validation snapshot and returns an explicit status.\n\n```ts\nconst passwordForm = createForm({\n initialValues: { password: '', passwordConfirmation: '' },\n validate: (value) => ({\n fields: {\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n passwordConfirmation: value.password === value.passwordConfirmation ? undefined : 'Passwords must match',\n },\n }),\n});\n\nconst validation = await passwordForm.validate();\n\nif (validation.status === 'invalid') console.log(validation.errors);\nif (validation.status === 'aborted') console.log('Validation cancelled');\n\nconst result = await passwordForm.submit((value) => Promise.resolve(value.password.length));\n\nif (result.ok) console.log(result.value);\n```\n\nStarting another validation aborts the previous run. Field edits preserve existing errors until the next validation replaces them. Unexpected validator failures reject as `ForgeValidationError` with the original error as `cause`.\n\n## Observe State\n\nUse form subscriptions for aggregate metadata and field subscriptions for one branch. Subscribing after disposal throws `ForgeDisposedError`.\n\n```ts\nconst errors: unknown[] = [];\nconst observedForm = createForm({\n initialValues: { email: '' },\n onSubscriberError: (error) => errors.push(error),\n});\n\nconst stopForm = observedForm.subscribe((state) => {\n console.log(state.valid, state.submitting);\n}, { immediate: true });\nconst stopField = observedForm.field('email').subscribe((state) => {\n console.log(state.value, state.error);\n}, { immediate: true });\n\nstopField();\nstopForm();\n```\n\nWithout `onSubscriberError`, Forge rethrows subscriber failures asynchronously after completing its state transition.\n\n## Testing\n\nTest the form without a DOM. Read its immutable value, invoke a method, then assert the resulting state or validation result.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createForm } from '@vielzeug/forge';\n\ntest('requires an email address', async () => {\n const form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n });\n\n await expect(form.validate()).resolves.toEqual({\n errors: { email: 'Invalid email' },\n formError: undefined,\n status: 'invalid',\n });\n});\n```\n\n## Framework Integration\n\nUse `form.value` and subscriptions with any renderer. Bind one DOM input through `/dom`; validation scheduling remains application policy.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n\nexport function EmailForm() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = form.subscribe(() => rerender((revision) => revision + 1));\n\n return () => stop();\n }, []);\n\n return <input value={form.field('email').value} onChange={(event) => form.field('email').set(event.target.value)} />;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\nconst revision = ref(0);\nconst stop = form.subscribe(() => revision.value++);\n\nonUnmounted(stop);\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createForm } from '@vielzeug/forge';\n\n const form = createForm({ initialValues: { email: '' } });\n let revision = 0;\n const stop = form.subscribe(() => revision++);\n\n onDestroy(stop);\n</script>\n\n<input value={form.field('email').value} on:input={(event) => form.field('email').set(event.currentTarget.value)} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Spell when one schema owns validation and Vault when an explicit record codec owns persistence.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n`customValidator()` preserves unrelated Spell errors, maps each union to its closest branch, and maps array-item failures to the parent array field. Parse again at the submit boundary when a Spell transform must produce the outgoing payload.\n\n```ts\nimport { loadForm, saveForm } from '@vielzeug/forge/vault';\n\nawait saveForm(form, db, 'drafts', codec);\nconst restored = await loadForm(form, db, 'drafts', 'profile', codec);\nconsole.log(restored);\n```\n\n`loadForm()` uses `form.reset()`, so a restored value is clean. Store a selected `File`, not `FileList`, in form state; `FileList` is transport-only for `toFormData()`.\n\n## Best Practices\n\n- Keep form values to primitives, plain objects, arrays, `File`, and `Blob`.\n- Update array fields through immutable replacement functions.\n- Validate complete values instead of rebuilding field-validator graphs.\n- Handle `aborted` validation results before rendering errors.\n- Preserve errors through field edits until a deliberate validation refresh.\n- Return subscription cleanup from framework lifecycle hooks.\n- Provide `onSubscriberError` when application subscribers can throw.\n- Decode Vault records before passing them to `loadForm()`.\n",
|
|
7
7
|
"examples": "---\ntitle: Forge — Examples\ndescription: Practical immutable form recipes.\n---\n\n## Examples\n\n- [Login form](./examples/login-form.md)\n- [Conditional values](./examples/form-with-conditional-fields.md)\n- [Dynamic arrays](./examples/dynamic-form-fields.md)\n- [Contact form with file upload](./examples/contact-form-with-file-upload.md)\n- [Registration form](./examples/registration-form.md)\n- [Multi-step wizard](./examples/multi-step-wizard.md)\n- [Search form with debounce](./examples/search-form-with-debounce.md)\n"
|