@vielzeug/codex 2.2.7 → 2.2.8
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 +1693 -0
- package/data/llms-full.txt +27748 -0
- package/data/llms.txt +40 -0
- package/data/manifest.json +8 -0
- package/data/packages/arsenal.json +210 -0
- package/data/packages/assay.json +39 -0
- package/data/packages/clockwork.json +67 -0
- package/data/packages/codex.json +43 -0
- package/data/packages/coins.json +102 -0
- package/data/packages/conduit.json +60 -0
- package/data/packages/courier.json +58 -0
- package/data/packages/dnd.json +77 -0
- package/data/packages/familiar.json +40 -0
- package/data/packages/flux.json +93 -0
- package/data/packages/forge.json +84 -0
- package/data/packages/herald.json +108 -0
- package/data/packages/keymap.json +60 -0
- package/data/packages/ledger.json +57 -0
- package/data/packages/lingua.json +67 -0
- package/data/packages/necromancer.json +50 -0
- package/data/packages/orbit.json +99 -0
- package/data/packages/ore.json +73 -0
- package/data/packages/prism.json +66 -0
- package/data/packages/pulse.json +69 -0
- package/data/packages/refine.json +12 -0
- package/data/packages/ripple.json +83 -0
- package/data/packages/rune.json +79 -0
- package/data/packages/sandbox.json +40 -0
- package/data/packages/scout.json +60 -0
- package/data/packages/scroll.json +109 -0
- package/data/packages/sourcerer.json +72 -0
- package/data/packages/spell.json +133 -0
- package/data/packages/tempo.json +81 -0
- package/data/packages/vault.json +85 -0
- package/data/packages/ward.json +114 -0
- package/data/packages/wayfinder.json +110 -0
- package/data/refine.json +11926 -0
- package/data/search.json +1432 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { toAsyncIterable } from './async';\nexport { stream } from './core';\nexport { FluxError, FluxTimeoutError } from './errors';\nexport { combineLatest, concat, merge } from './operators/combination';\nexport type { IntervalOptions, TimerOptions } from './operators/creation';\nexport { from, fromEvent, interval, of, timer } from './operators/creation';\nexport type { DebounceOptions, TimeoutOptions } from './operators/filtering';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering';\nexport type { ConcatMapOptions } from './operators/transformation';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';\nexport { first, last, retry, toArray } 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
|
+
"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()` — adapt Courier query state\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — 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
|
+
"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>(\n target: {\n addEventListener(type: string, listener: (event: T) => void): void;\n removeEventListener(type: string, listener: (event: T) => void): void;\n },\n type: string,\n): 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> **`initial` + `replay` interaction:** When `initial` is set and `replay` is omitted, `replay` defaults to `1` so the initial value is retained. Setting `replay: 0` with `initial` throws `RangeError` — the initial value would be immediately dropped.\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. On source error, `toSignal()` calls `options.onError` if provided (otherwise logs via `console.error` in dev — in production the log is stripped and the error is silently swallowed), then disposes — the signal freezes at its last value. Pass `onError` to surface source errors in production builds.\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>\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<S extends PulseSchema, K extends EventKey<ServerEvents<S>>>(pulse: Pulse<S>, event: K): Stream<ServerEvents<S>[K]>\nfromRoomPresence<T>(room: PresenceRoomScope<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 | undefined;\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 };\ntype Channel<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n send(value: T): void;\n readonly stream: Stream<T>;\n};\ntype ToSignalOptions<T> = { initial: T; onError?: (reason: unknown) => void; signal?: AbortSignal };\ntype SignalBinding<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly signal: Readable<T>;\n readonly value: T;\n};\n```\n\n## Errors\n\n### `FluxError`\n\nBase Flux error. Use `instanceof FluxError` to narrow unknown values.\n\n### `FluxTimeoutError`\n\nRaised by `timeout()`. `ms` contains configured inactivity duration.\n",
|
|
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 { fromRoomPresence } 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
|
+
"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"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "async-conversion",
|
|
12
|
+
"code": "// Consume a push stream with an explicit bounded async queue.\nimport { of, toAsyncIterable } from '@vielzeug/flux'\n\nasync function run() {\n const values = toAsyncIterable(of(1, 2, 3), {\n capacity: 3,\n overflow: 'error',\n })\n\n for await (const value of values) {\n console.log('value:', value)\n }\n\n console.log('iterator complete')\n}\n\nvoid run()",
|
|
13
|
+
"name": "Async Conversion"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "basic-flux",
|
|
17
|
+
"code": "// Build a cold stream and convert a bounded derived sequence to an array.\nimport { toArray, map, pipe, stream, take } from '@vielzeug/flux'\n\nconst integers = stream((sink) => {\n let value = 0\n const id = setInterval(() => sink.next(value++), 50)\n\n return () => clearInterval(id)\n})\n\nconst values = pipe(\n integers,\n map((value) => value * 2),\n take(3),\n)\n\ntoArray(values, { maxItems: 3 })\n .then((result) => console.log('values:', result))\n .catch(console.error)",
|
|
18
|
+
"name": "Creating a Stream"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "cancellation",
|
|
22
|
+
"code": "// Stop synchronous iterable work as soon as take() reaches its limit.\nimport { from, pipe, take } from '@vielzeug/flux'\n\nfunction* ids() {\n for (let value = 1; value <= 5; value++) {\n console.log('produced:', value)\n yield value\n }\n}\n\npipe(from(ids()), take(2)).subscribe({\n complete: () => console.log('complete'),\n error: console.error,\n next: (value) => console.log('consumed:', value),\n})",
|
|
23
|
+
"name": "Cancelling an Iterable"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "combination",
|
|
27
|
+
"code": "// Combine current filter and page state from replaying channels.\nimport { combineLatest } from '@vielzeug/flux'\nimport { createChannel } from '@vielzeug/flux/subjects'\n\nconst count = createChannel({ initial: 0 })\nconst label = createChannel({ initial: 'items' })\n\ncombineLatest(count.stream, label.stream).subscribe({\n error: console.error,\n next: ([value, text]) => console.log(value, text),\n})\n\ncount.send(1) // 1 items\nlabel.send('tasks') // 1 tasks\ncount.dispose()\nlabel.dispose()",
|
|
28
|
+
"name": "Combining Streams"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "error-handling",
|
|
32
|
+
"code": "// Retry a transient producer error with a bounded attempt count.\nimport { toArray, pipe, retry, stream } from '@vielzeug/flux'\n\nlet attempt = 0\nconst source = stream((sink) => {\n attempt++\n console.log('attempt:', attempt)\n\n if (attempt < 3) {\n sink.error(new Error('temporary failure'))\n return\n }\n\n sink.next('success')\n sink.complete()\n})\n\ntoArray(pipe(source, retry({ attempts: 2 })), { maxItems: 1 })\n .then((values) => console.log('result:', values))\n .catch(console.error)",
|
|
33
|
+
"name": "Error Handling"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "operators",
|
|
37
|
+
"code": "// Build a typed transformation pipeline with filter, map, and scan.\nimport { toArray, filter, map, of, pipe, scan } from '@vielzeug/flux'\n\nconst result = pipe(\n of(1, 2, 3, 4, 5),\n filter((value) => value % 2 !== 0),\n map((value) => value * 10),\n scan((total, value) => total + value, 0),\n)\n\ntoArray(result, { maxItems: 3 })\n .then((values) => console.log('running totals:', values))\n .catch(console.error)",
|
|
38
|
+
"name": "Operators"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "subjects",
|
|
42
|
+
"code": "// Replay latest events to late subscribers while keeping send() at one boundary.\nimport { createChannel } from '@vielzeug/flux/subjects'\n\nconst events = createChannel({ replay: 2 })\n\nevents.stream.subscribe({\n error: console.error,\n next: (value) => console.log('first:', value),\n})\nevents.send('connected')\nevents.send('ready')\nevents.send('updated')\n\nevents.stream.subscribe({\n error: console.error,\n next: (value) => console.log('late:', value),\n})\n// late: ready\n// late: updated\n\nevents.dispose()",
|
|
43
|
+
"name": "Channels"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"typeSignatures": {
|
|
47
|
+
"toAsyncIterable": "export { toAsyncIterable } from './async';",
|
|
48
|
+
"stream": "export { stream } from './core';",
|
|
49
|
+
"FluxError": "export { FluxError, FluxTimeoutError } from './errors';",
|
|
50
|
+
"FluxTimeoutError": "export { FluxError, FluxTimeoutError } from './errors';",
|
|
51
|
+
"combineLatest": "export { combineLatest, concat, merge } from './operators/combination';",
|
|
52
|
+
"concat": "export { combineLatest, concat, merge } from './operators/combination';",
|
|
53
|
+
"merge": "export { combineLatest, concat, merge } from './operators/combination';",
|
|
54
|
+
"IntervalOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
|
|
55
|
+
"TimerOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
|
|
56
|
+
"from": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
|
|
57
|
+
"fromEvent": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
|
|
58
|
+
"interval": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
|
|
59
|
+
"of": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
|
|
60
|
+
"timer": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
|
|
61
|
+
"DebounceOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
|
|
62
|
+
"TimeoutOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
|
|
63
|
+
"debounce": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
|
|
64
|
+
"take": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
|
|
65
|
+
"takeUntil": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
|
|
66
|
+
"timeout": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
|
|
67
|
+
"ConcatMapOptions": "export type { ConcatMapOptions } from './operators/transformation';",
|
|
68
|
+
"concatMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
|
|
69
|
+
"filter": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
|
|
70
|
+
"map": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
|
|
71
|
+
"mergeMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
|
|
72
|
+
"scan": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
|
|
73
|
+
"switchMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
|
|
74
|
+
"RetryOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
|
|
75
|
+
"ToArrayOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
|
|
76
|
+
"ValueOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
|
|
77
|
+
"first": "export { first, last, retry, toArray } from './operators/utility';",
|
|
78
|
+
"last": "export { first, last, retry, toArray } from './operators/utility';",
|
|
79
|
+
"retry": "export { first, last, retry, toArray } from './operators/utility';",
|
|
80
|
+
"toArray": "export { first, last, retry, toArray } from './operators/utility';",
|
|
81
|
+
"pipe": "export { pipe } from './pipe';",
|
|
82
|
+
"AsyncIterableOptions": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
83
|
+
"Observer": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
84
|
+
"Operator": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
85
|
+
"OverflowPolicy": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
86
|
+
"Producer": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
87
|
+
"Sink": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
88
|
+
"Stream": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
89
|
+
"SubscribeOptions": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
90
|
+
"Subscription": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
|
|
91
|
+
"Teardown": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { toFormData } from './adapters/form-data';\nexport { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport { createForm } from './form';\nexport * from './types';\n",
|
|
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- [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
|
+
"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| `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/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 = void>(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## Adapters\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): () => void;\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, SchemaMode>,\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 ? string | { 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<NoInfer<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>> = 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>> =\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 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
|
+
"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
|
+
"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"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "array-fields",
|
|
12
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { tags: ['typescript'] } })\nconst tags = form.field('tags')\n\ntags.set((previous) => [...previous, 'forms'])\ntags.set((previous) => previous.filter((tag) => tag !== 'typescript'))\nconsole.log(form.value.tags)",
|
|
13
|
+
"name": "Immutable Array Updates"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "create-form",
|
|
17
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { account: { email: '' }, name: '' } })\nconst email = form.field('account').field('email')\n\nemail.set('ada@example.com')\nconsole.log(form.value)\nconsole.log(form.state.valid)",
|
|
18
|
+
"name": "Create Immutable Form"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "dynamic-fields",
|
|
22
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { contacts: [] as { email: string }[] } })\nconst contacts = form.field('contacts')\n\ncontacts.set((previous) => [...previous, { email: 'ada@example.com' }])\ncontacts.set((previous) => previous.slice(1))\nconsole.log(form.value)",
|
|
23
|
+
"name": "Dynamic Values"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "field-binding",
|
|
27
|
+
"code": "import { createForm } from '@vielzeug/forge'\nimport { bindField } from '@vielzeug/forge/dom'\n\nconst form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n})\nconst email = form.field('email')\nconst input = document.createElement('input')\nconst stop = bindField(input, email, {\n read: (element) => element.value,\n write: (element, value) => { element.value = value },\n})\n\ninput.value = 'ada'\ninput.dispatchEvent(new Event('input'))\ninput.dispatchEvent(new Event('blur'))\nconsole.log(email.value, email.touched)\nconsole.log(await form.validate())\nstop()",
|
|
28
|
+
"name": "DOM Field Binding"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "field-operations",
|
|
32
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { profile: { name: 'Ada' } } })\nconst name = form.field('profile').field('name')\n\nname.set('Grace')\nname.touch()\nconsole.log(name.value, name.dirty, name.touched)\nname.reset()\nconsole.log(form.value)",
|
|
33
|
+
"name": "Focused Field Operations"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "form-submission",
|
|
37
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n})\n\nform.field('email').set('ada@example.com')\nconst result = await form.submit(async (value) => ({ ...value, saved: true }))\nconsole.log(result)",
|
|
38
|
+
"name": "Form Submission"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "form-subscriptions",
|
|
42
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({\n initialValues: { email: '', name: '' },\n onSubscriberError: (error) => console.log('Subscriber error:', error),\n})\nconst stopForm = form.subscribe((state) => console.log('Valid:', state.valid), { immediate: true })\nconst stopEmail = form.field('email').subscribe((state) => console.log('Email:', state), { immediate: true })\n\nform.field('name').set('Ada')\nform.field('email').set('ada@example.com')\nstopEmail()\nstopForm()",
|
|
43
|
+
"name": "Form and Field Subscriptions"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "form-validation",
|
|
47
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = 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\nform.field('password').set('short')\nconsole.log(await form.validate())\n\nform.field('password').set('strong-password')\nform.field('passwordConfirmation').set('strong-password')\nconsole.log(await form.validate())",
|
|
48
|
+
"name": "Whole-Value Validation"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "schema-integration",
|
|
52
|
+
"code": "import { 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({\n initialValues: { email: '' },\n validate: customValidator(Profile),\n})\n\nform.field('email').set('ada')\nconsole.log(await form.validate())\nconsole.log(form.field('email').error)",
|
|
53
|
+
"name": "Spell Schema Integration"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "scoped-sub-forms",
|
|
57
|
+
"code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { shipping: { city: '', street: '' } } })\nconst shipping = form.field('shipping')\n\nshipping.field('street').set('123 Main Street')\nshipping.field('city').set('Portland')\nconsole.log(shipping.value)\nconsole.log(form.value.shipping)",
|
|
58
|
+
"name": "Nested Field Handles"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"typeSignatures": {
|
|
62
|
+
"toFormData": "export { toFormData } from './adapters/form-data';",
|
|
63
|
+
"ForgeConfigError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
|
|
64
|
+
"ForgeDisposedError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
|
|
65
|
+
"ForgeError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
|
|
66
|
+
"ForgeSubmitError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
|
|
67
|
+
"ForgeValidationError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
|
|
68
|
+
"createForm": "export { createForm } from './form';",
|
|
69
|
+
"Unsubscribe": "export type Unsubscribe = () => void;",
|
|
70
|
+
"MaybePromise": "export type MaybePromise<T> = T | PromiseLike<T>;",
|
|
71
|
+
"ReadonlyDeep": "export type 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;",
|
|
72
|
+
"FormErrors": "export type FormErrors<T> = T extends readonly unknown[]\n ? string\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;",
|
|
73
|
+
"ValidationErrors": "export type ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;",
|
|
74
|
+
"FormValidator": "export type FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>,\n signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;",
|
|
75
|
+
"FormOptions": "export type FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<NoInfer<TValues>>;\n}>;",
|
|
76
|
+
"SubscribeOptions": "export type SubscribeOptions = Readonly<{\n immediate?: boolean;\n}>;",
|
|
77
|
+
"FieldState": "export type FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;",
|
|
78
|
+
"FormState": "export type FormState<TValues extends 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}>;",
|
|
79
|
+
"ValidationResult": "export type ValidationResult<TValues extends Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;",
|
|
80
|
+
"SubmitResult": "export type 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' }>;",
|
|
81
|
+
"Field": "export type Field<V> = ChildField<V> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\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 readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n};",
|
|
82
|
+
"Form": "export type Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\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 readonly state: FormState<TValues>;\n submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>,\n ): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n readonly value: ReadonlyDeep<TValues>;\n};"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { combineSignals, createBus } from './bus';\nexport { BusDisposedError, HeraldConfigError, HeraldError } from './errors';\nexport { pipeEvents } from './pipe';\nexport type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Herald — Typed event bus for TypeScript\ndescription: Typed temporal event delivery with sync subscriptions, async waiting, streams, pipes, and AbortSignal lifecycle.\npackage: herald\ncategory: events\nkeywords: [event-bus, typed-events, pub-sub, async-streams, abort-signal]\nrelated: [ripple, wayfinder, familiar]\nexports: [createBus, pipeEvents, combineSignals, HeraldError, BusDisposedError, HeraldConfigError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"herald\" />\n\n## Why Herald?\n\nRaw event emitters lose payload inference and leave waiting, streaming, cancellation, and teardown to every caller. Herald keeps events temporal: use [Ripple](/ripple/) when you need retained state.\n\n```ts\n// Before\nconst listeners = new Set<(payload: unknown) => void>();\nlisteners.add((payload) => loadProfile((payload as { id: string }).id));\n\n// After\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n}\n\nfunction loadProfile(id: string): void {\n console.log(id);\n}\n\nconst bus = createBus<AppEvents>();\nbus.on('user:login', ({ id }) => loadProfile(id));\n```\n\n| Feature | Herald | mitt | EventEmitter3 |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"herald\" type=\"size\" /> | ~200 B | ~1.5 kB |\n| Typed payloads | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Async wait and streams | <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 lifecycle | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed event pipes | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Herald when** modules need typed temporal event delivery with owned lifecycle.\n\n**Consider Ripple when** consumers need current state and replayed values.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/herald\n```\n\n```sh [npm]\nnpm install @vielzeug/herald\n```\n\n```sh [yarn]\nyarn add @vielzeug/herald\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n 'user:logout': void;\n}\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.on('user:login', ({ id }) => console.log(id));\n\nbus.emit('user:login', { id: '42' });\nstop();\nbus.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `on()` / `once()` — typed subscriptions with explicit teardown\n- `onAny()` — cross-cutting event observation\n- `wait()` / `waitAny()` — one-shot async coordination\n- `events()` — bounded async event streams\n- `pipeEvents()` — compatible cross-bus forwarding\n- `AbortSignal` — cancellation and disposal ownership\n- `createTestBus()` — emitted-payload recording for tests\n- `debugBus()` — development logging from `/devtools`\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/) — retained reactive state.\n- [Wayfinder](/wayfinder/) — route lifecycle events.\n- [Familiar](/familiar/) — worker completion events.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Herald — API Reference\ndescription: Reference for typed temporal event delivery, lifecycle ownership, and compatible event piping.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createBus()` | Create typed temporal event bus | Sync | `emit()` and middleware are synchronous |\n| `pipeEvents()` | Forward compatible source events | Sync | Payloads must be assignable to target event |\n| `combineSignals()` | Abort when any input aborts | Sync | Public composition has no manual teardown |\n| `createTestBus()` | Record dispatched test events | Sync | Available from `/testing` only |\n| `debugBus()` | Create console-debug instrumented bus | Sync | Available from `/devtools` only |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/herald` | Runtime bus, pipes, public types, and errors |\n| `@vielzeug/herald/testing` | `createTestBus()` and `TestBus` |\n| `@vielzeug/herald/devtools` | `debugBus()` |\n\n## Core Functions\n\n### `createBus()`\n\n```ts\nfunction createBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions<T>,\n): Bus<T>;\n```\n\nCreates a synchronous bus for future event delivery.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `BusOptions<T>` | Optional middleware, validation, error handling, logging, and listener threshold configuration. |\n\n**Returns:** `Bus<T>`.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface Events {\n count: number;\n ready: void;\n}\n\nconst bus = createBus<Events>();\nbus.emit('count', 1);\nbus.emit('ready');\nbus.dispose();\n```\n\n---\n\n### `pipeEvents()`\n\n```ts\nfunction pipeEvents<S extends EventMap, T extends EventMap>(\n source: Bus<S>,\n target: Bus<T>,\n entries: readonly [NoInfer<PipeEntry<S, T>>, ...NoInfer<PipeEntry<S, T>>[]],\n opts?: { signal?: AbortSignal },\n): Unsubscribe;\n```\n\nForwards listed compatible events until manually stopped, either bus disposes, or `options.signal` aborts.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `Bus<S>` | Bus that emits source events. |\n| `target` | `Bus<T>` | Bus that receives compatible events. |\n| `entries` | non-empty `PipeEntry` tuple | Same-name keys or compatible `{ from, to }` mappings. |\n| `opts.signal` | `AbortSignal` | Optional pipe lifetime signal. |\n\n**Returns:** Idempotent `Unsubscribe` function.\n\n```ts\nimport { createBus, pipeEvents } from '@vielzeug/herald';\n\ninterface SourceEvents {\n 'auth:login': { id: string };\n}\n\ninterface TargetEvents {\n 'user:authenticated': { id: string };\n}\n\nconst source = createBus<SourceEvents>();\nconst target = createBus<TargetEvents>();\nconst stop = pipeEvents(source, target, [{ from: 'auth:login', to: 'user:authenticated' }]);\n\nstop();\nsource.dispose();\ntarget.dispose();\n```\n\n---\n\n### `combineSignals()`\n\n```ts\nfunction combineSignals(first: AbortSignal, ...rest: AbortSignal[]): AbortSignal;\n```\n\nReturns a signal aborted with first input signal's reason.\n\n**Returns:** `AbortSignal`.\n\n```ts\nimport { combineSignals } from '@vielzeug/herald';\n\nconst signal = combineSignals(AbortSignal.timeout(1_000), controller.signal);\n```\n\nInput listeners remain until an input aborts. Bus APIs that accept `{ signal }` clean their internal signal composition when their owned operation ends.\n\n## Types\n\n### `EventMap` and `EventKey`\n\n```ts\ntype EventMap = object;\ntype EventKey<T extends EventMap> = Extract<keyof T, string>;\n```\n\n`EventMap` accepts interfaces and type aliases. Only string keys are event names.\n\n---\n\n### `BusOptions`\n\n```ts\ntype BusOptions<T extends EventMap = EventMap> = {\n logger?: BusLogger;\n maxListeners?: number;\n middleware?: readonly Middleware<T>[];\n name?: string;\n onError?: (context: EmissionErrorContext<T>) => void;\n validatePayload?: <K extends EventKey<T>>(event: K, payload: T[K]) => void;\n};\n```\n\n| Field | Description |\n| --- | --- |\n| `logger` | Optional debug and warning output. |\n| `maxListeners` | Warn when one event exceeds this active-listener count. |\n| `middleware` | Synchronous dispatch middleware. |\n| `name` | Display name in debug logs and disposal errors. |\n| `onError` | Handles listener and validation errors instead of rethrowing. |\n| `validatePayload` | Runs before middleware and listeners. |\n\n---\n\n### `Bus`\n\n```ts\ntype Bus<T extends EventMap> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n emit<K extends EventKey<T>>(event: K, ...args: T[K] extends void ? [] : [payload: T[K]]): number;\n eventNames(): EventKey<T>[];\n events<K extends EventKey<T>>(event: K, opts?: { maxBuffer?: number; signal?: AbortSignal }): EventStream<T[K]>;\n listenerCount(event?: EventKey<T>): number;\n on<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, opts?: SubscribeOptions): Unsubscribe;\n onAny(listener: (event: EventKey<T>, payload: unknown) => void, opts?: SubscribeOptions): Unsubscribe;\n once<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, opts?: { signal?: AbortSignal }): Unsubscribe;\n wait<K extends EventKey<T>>(event: K, opts?: { signal?: AbortSignal }): Promise<T[K]>;\n waitAny<const K extends readonly [EventKey<T>, EventKey<T>, ...EventKey<T>[]]>(\n events: K,\n opts?: { signal?: AbortSignal },\n ): Promise<WaitAnyResult<T, K>>;\n wildcardCount(): number;\n};\n```\n\n`emit()` returns listener count or `0` after disposal, blocked middleware, or handled validation rejection.\n\n---\n\n### `BusLogger`, `Listener`, `SubscribeOptions`, and `Unsubscribe`\n\n```ts\ntype BusLogger = {\n debug?: (message: string) => void;\n warn?: (message: string) => void;\n};\n\ntype Listener<T> = (payload: T) => void;\ntype SubscribeOptions = { once?: boolean; signal?: AbortSignal };\ntype Unsubscribe = () => void;\n```\n\n---\n\n### `EmissionErrorContext` and `Middleware`\n\n```ts\ntype EmissionErrorContext<T extends EventMap = EventMap> = {\n err: unknown;\n event: EventKey<T>;\n payload: unknown;\n timestamp: number;\n};\n\ntype Middleware<T extends EventMap = EventMap> = (\n event: EventKey<T>,\n payload: unknown,\n next: () => void,\n) => void;\n```\n\nCall middleware `next()` synchronously at most once. Omit it to block dispatch.\n\n---\n\n### `EventStream` and `WaitAnyResult`\n\n```ts\ntype EventStream<T> = AsyncGenerator<T> & AsyncDisposable;\n\ntype WaitAnyResult<T extends EventMap, K extends readonly EventKey<T>[]> = {\n [I in keyof K]: K[I] extends EventKey<T> ? { event: K[I]; payload: T[K[I]] } : never;\n}[number];\n```\n\n---\n\n### `PipeableKey`, `RenamedPipeEntry`, and `PipeEntry`\n\n```ts\ntype PipeableKey<S extends EventMap, T extends EventMap> = {\n [K in EventKey<S> & EventKey<T>]: S[K] extends T[K] ? K : never;\n}[EventKey<S> & EventKey<T>];\n\ntype RenamedPipeEntry<S extends EventMap, T extends EventMap> = {\n [From in EventKey<S>]: {\n [To in EventKey<T>]: S[From] extends T[To] ? { from: From; to: To } : never;\n }[EventKey<T>];\n}[EventKey<S>];\n\ntype PipeEntry<S extends EventMap, T extends EventMap> =\n | PipeableKey<S, T>\n | RenamedPipeEntry<S, T>;\n```\n\n## Testing and Devtools\n\n### `createTestBus()`\n\n```ts\nfunction createTestBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions<T>,\n): TestBus<T>;\n```\n\nCreates a bus that records dispatched payloads.\n\n**Returns:** `TestBus<T>`.\n\n### `TestBus`\n\n```ts\ntype TestBus<T extends EventMap> = Bus<T> & {\n allEmitted(): { [K in EventKey<T>]?: T[K][] };\n emitted<K extends EventKey<T>>(event: K): T[K][];\n emittedCount<K extends EventKey<T>>(event: K): number;\n reset(): void;\n};\n```\n\n### `debugBus()`\n\n```ts\nfunction debugBus<T extends EventMap>(\n options?: Omit<BusOptions<T>, 'logger'> & { logger?: { warn?: BusLogger['warn'] } },\n): Bus<T>;\n```\n\nCreates a bus with `console.debug` logging. Import from `@vielzeug/herald/devtools`.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `BusDisposedError` | `wait()` or `waitAny()` interrupted by disposal | Bus name appears when configured. |\n| `HeraldConfigError` | Invalid stream buffer, empty pipe entries, or fewer than two `waitAny()` events | — |\n| `HeraldError` | Base class for Herald-originated errors | `instanceof HeraldError` narrows subclasses. |\n",
|
|
6
|
+
"usage": "---\ntitle: Herald — Usage Guide\ndescription: Typed event maps, lifecycle-owned subscriptions, waits, streams, pipes, and testing.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse interface or type alias event maps. Events model facts that happened; use Ripple for current state.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'cart:updated': { count: number };\n 'user:logout': void;\n}\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.on('cart:updated', ({ count }) => console.log(count));\n\nbus.emit('cart:updated', { count: 1 });\nstop();\nbus.dispose();\n```\n\n## Subscriptions\n\nUse `once()` for one event and `{ signal }` for owned subscription lifetime.\n\n```ts\nconst controller = new AbortController();\n\nbus.on('cart:updated', renderCart, { signal: controller.signal });\nbus.once('user:logout', clearSession);\ncontroller.abort();\n```\n\n## Middleware and Validation\n\nMiddleware is synchronous. Call `next()` once to continue; omit it to block dispatch.\n\n```ts\nconst bus = createBus<AppEvents>({\n middleware: [\n (event, payload, next) => {\n audit(event, payload);\n next();\n },\n ],\n validatePayload: (event, payload) => {\n if (event === 'cart:updated' && payload.count < 0) throw new RangeError('count must be non-negative');\n },\n});\n```\n\n## Awaiting Events\n\n```ts\nconst cart = await bus.wait('cart:updated', { signal: AbortSignal.timeout(5_000) });\nconst winner = await bus.waitAny(['cart:updated', 'user:logout'], { signal: AbortSignal.timeout(5_000) });\n```\n\n## Streaming Events\n\n`events()` subscribes eagerly. Bound buffers for producers faster than consumers.\n\n```ts\nawait using stream = bus.events('cart:updated', { maxBuffer: 100 });\n\nfor await (const cart of stream) {\n renderCart(cart);\n}\n```\n\n## Piping Events\n\n`pipeEvents()` only accepts compatible payloads. Stop explicitly or tie pipe to signal.\n\n```ts\nconst stopPipe = pipeEvents(sourceBus, auditBus, ['cart:updated'], { signal: pageSignal });\nstopPipe();\n```\n\n## Testing\n\n`createTestBus()` records dispatched payloads without mocks.\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nconst bus = createTestBus<AppEvents>();\nbus.emit('cart:updated', { count: 2 });\nexpect(bus.emitted('cart:updated')).toEqual([{ count: 2 }]);\nbus.dispose();\n```\n\n## Debugging\n\n```ts\nimport { debugBus } from '@vielzeug/herald/devtools';\n\nconst bus = debugBus<AppEvents>({ name: 'cart' });\n```\n\n## Working with Other Vielzeug Libraries\n\nUse Herald for temporal events. Use Ripple for retained reactive state. Use Familiar or Courier completion handlers to emit application events.\n\n## Best Practices\n\n- Define one explicit event map per boundary.\n- Emit facts, not mutable application state.\n- Keep middleware synchronous and call `next()` once.\n- Pass AbortSignals for component/request scoped work.\n- Set `maxBuffer` for long-lived streams.\n- Use `wait()` only for one-off coordination.\n- Use unsubscribe handles instead of global listener removal.\n- Dispose owner-scoped buses.\n",
|
|
7
|
+
"examples": "---\ntitle: Herald — Examples\ndescription: Practical examples and recipes for herald.\n---\n\n## Examples\n\n- [Standalone Entry](./examples/standalone-entry.md)\n- [Module Level Bus](./examples/module-level-bus.md)\n- [Awaiting A One Time Event](./examples/awaiting-a-one-time-event.md)\n- [Inspecting Listener Counts](./examples/inspecting-listener-counts.md)\n- [Custom Error Boundary](./examples/custom-error-boundary.md)\n- [Handling Disposal In Async Code](./examples/handling-disposal-in-async-code.md)\n- [Request Scoping](./examples/request-scoping.md)\n- [Streaming With Events](./examples/streaming-with-events.md)\n- [Bus Bridging With pipeEvents](./examples/bus-bridging-with-pipeevents.md)\n- [Testing With Createtestbus](./examples/testing-with-createtestbus.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "abort-signal",
|
|
12
|
+
"code": "import { createBus, BusDisposedError } from '@vielzeug/herald'\n\n// Demonstrate AbortSignal auto-unsubscribe and BusDisposedError\nconst bus = createBus()\n\nconst controller = new AbortController()\nconst { signal } = controller\n\nbus.on('message', (msg) => {\n console.log('listener received:', msg)\n}, { signal })\n\nbus.emit('message', 'first') // fires\nbus.emit('message', 'second') // fires\n\ncontroller.abort() // removes the listener\n\nbus.emit('message', 'third') // ignored — no listeners\nconsole.log('listeners after abort:', bus.listenerCount())\n\n// BusDisposedError: pending wait() rejects when bus is disposed\nconst bus2 = createBus()\n\nvoid bus2.wait('done').catch((err) => {\n if (err instanceof BusDisposedError) {\n console.log('BusDisposedError caught:', err.message)\n }\n})\n\nbus2.dispose()",
|
|
13
|
+
"name": "AbortSignal & BusDisposedError"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "async-generator",
|
|
17
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// events() returns an async generator that yields each emitted value in order\nconst bus = createBus()\n\nasync function consumeTicks() {\n let received = 0\n for await (const tick of bus.events('tick', { maxBuffer: 2 })) {\n console.log('tick:', tick)\n received++\n if (received >= 3) break\n }\n console.log('done after', received, 'ticks')\n}\n\nvoid consumeTicks()\n\nlet count = 0\nconst interval = setInterval(() => {\n bus.emit('tick', ++count)\n if (count >= 5) {\n clearInterval(interval)\n bus.dispose()\n }\n}, 30)",
|
|
18
|
+
"name": "events() - Async Generator"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "async-wait",
|
|
22
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// Await a single event with wait(), race multiple with waitAny()\nconst bus = createBus()\n\n// Emit after a short delay\nsetTimeout(() => bus.emit('user:login', { userId: '42', email: 'alice@example.com' }), 50)\nsetTimeout(() => bus.emit('theme:change', 'dark'), 80)\n\n// wait() resolves on the next emit of that event\nconst loginPayload = await bus.wait('user:login')\nconsole.log('got login:', loginPayload.userId)\n\n// Reset and race two events — whichever fires first wins\nsetTimeout(() => bus.emit('user:login', { userId: '1', email: 'a@b.com' }), 20)\nsetTimeout(() => bus.emit('theme:change', 'light'), 60)\n\nconst result = await bus.waitAny(['user:login', 'theme:change'])\n\nif (result.event === 'user:login') {\n console.log('login won the race:', result.payload.userId)\n} else {\n console.log('theme won the race:', result.payload)\n}\n\nbus.dispose()",
|
|
23
|
+
"name": "Async wait()"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "basic-bus",
|
|
27
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// Typed pub/sub with on(), emit(), and once()\nconst bus = createBus()\n\nconst unsub = bus.on('user:login', ({ userId, email }) => {\n console.log('login:', userId, email)\n})\n\nbus.once('user:logout', () => {\n console.log('logged out (fires once)')\n})\n\nbus.emit('user:login', { userId: '1', email: 'alice@example.com' })\nbus.emit('user:logout')\nbus.emit('user:logout') // once() already removed; no output\n\nunsub()\nbus.emit('user:login', { userId: '2', email: 'bob@example.com' }) // no output — unsubscribed\n\nbus.dispose()",
|
|
28
|
+
"name": "Basic Bus"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "bus-basics",
|
|
32
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// on() returns an unsubscribe handle; emit() delivers to all active listeners\nconst bus = createBus()\n\nconst unsubLogin = bus.on('user:login', (payload) => {\n console.log('login:', payload.name, '(' + payload.userId + ')')\n})\n\nbus.on('user:logout', () => console.log('logged out'))\nbus.on('notification', (msg) => console.log('notification:', msg))\n\nbus.emit('user:login', { userId: '123', name: 'Alice' })\nbus.emit('notification', 'welcome back!')\n\nunsubLogin()\n\nbus.emit('user:login', { userId: '456', name: 'Bob' }) // no output — unsubscribed\nconsole.log('active listeners:', bus.listenerCount())\n\nbus.dispose()",
|
|
33
|
+
"name": "createBus - Basics"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "disposal-signal",
|
|
37
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// disposalSignal ties an external subscription's lifetime to this bus\nconst mainBus = createBus()\nconst childBus = createBus()\n\n// Pass disposalSignal so the child listener is removed when mainBus disposes\nchildBus.on('data:update', ({ value }) => {\n console.log('child received:', value)\n}, { signal: mainBus.disposalSignal })\n\nconsole.log('child listeners before dispose:', childBus.listenerCount())\n\nchildBus.emit('data:update', { value: 10 }) // fires — listener is active\n\nmainBus.dispose() // disposalSignal fires — child listener auto-removed\n\nconsole.log('child listeners after mainBus.dispose():', childBus.listenerCount())\n\nchildBus.emit('data:update', { value: 20 }) // no output — listener is gone\nconsole.log('mainBus.disposed:', mainBus.disposed)\nconsole.log('disposalSignal aborted:', mainBus.disposalSignal.aborted)",
|
|
38
|
+
"name": "disposalSignal"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "error-handling",
|
|
42
|
+
"code": "import { createBus, HeraldError } from '@vielzeug/herald'\n\n// onError captures listener throws — every listener still runs, even a buggy one\nconst errors = []\n\nconst bus = createBus({\n onError: ({ err, event }) => errors.push({ event, message: err.message }),\n})\n\nbus.on('order:placed', () => console.log('confirmation email sent'))\nbus.on('order:placed', () => {\n throw new Error('inventory check failed')\n})\nbus.on('order:placed', () => console.log('analytics event recorded')) // still runs\n\nbus.emit('order:placed', { id: 'ORD-1', total: 49.99 })\n\nconsole.log('captured errors:', errors)\n// [{ event: 'order:placed', message: 'inventory check failed' }]\n\n// Without onError, the first error rethrows once every listener has run —\n// instanceof HeraldError catches it without importing every herald error subclass\ntry {\n bus.waitAny(['event-a']) // waitAny requires at least 2 event keys\n} catch (err) {\n console.log('caught herald error?', err instanceof HeraldError, '-', err.message)\n}\n\nbus.dispose()",
|
|
43
|
+
"name": "Error Handling"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "event-stream-take",
|
|
47
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// Collect the first 3 emits then break — await using ensures cleanup\nconst bus = createBus()\n\nasync function collectFirstThree() {\n const collected = []\n\n await using stream = bus.events('score')\n for await (const n of stream) {\n collected.push(n)\n console.log('score:', n)\n if (collected.length >= 3) break\n }\n\n console.log('done — collected:', collected.length, 'values')\n}\n\nvoid collectFirstThree()\n\nlet i = 0\nconst timer = setInterval(() => {\n bus.emit('score', ++i * 10)\n if (i >= 6) {\n clearInterval(timer)\n bus.dispose()\n }\n}, 40)",
|
|
48
|
+
"name": "events() with break"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "logger-option",
|
|
52
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// Custom logger — route or suppress debug and warn output\nconst logs = []\n\nconst bus = createBus({\n maxListeners: 2,\n logger: {\n debug: (msg) => logs.push('[debug] ' + msg),\n warn: (msg) => logs.push('[warn] ' + msg),\n },\n})\n\nbus.on('order:placed', (order) => console.log('order:', order.id))\nbus.on('order:placed', (order) => console.log('copy:', order.id))\nbus.on('order:placed', () => {}) // triggers maxListeners warning (> 2)\n\nbus.emit('order:placed', { id: 'ORD-001', total: 49.99 })\n\nbus.dispose()\n\nconsole.log('captured log lines:')\nlogs.forEach((l) => console.log(l))",
|
|
53
|
+
"name": "Custom Logger"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "named-bus",
|
|
57
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// name appears in log prefixes and BusDisposedError messages\nconst logs = []\nconst warns = []\n\nconst authBus = createBus({\n name: 'auth',\n logger: {\n debug: (msg) => { logs.push(msg); console.log(msg) },\n warn: (msg) => warns.push(msg),\n },\n maxListeners: 1,\n})\n\nauthBus.on('login', (userId) => console.log('user:', userId))\nauthBus.on('login', (userId) => console.log('audit:', userId)) // triggers warn\n\nauthBus.emit('login', 'alice')\n\nconst pending = authBus.wait('logout')\nauthBus.dispose()\n\npending.catch((err) => {\n console.log('error name:', err.name)\n console.log('error message:', err.message)\n console.log('warn included name:', warns[0].includes('auth'))\n})",
|
|
58
|
+
"name": "Named Bus"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "once-and-wait",
|
|
62
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// once() fires exactly once then auto-removes; wait() resolves on the next emit\nconst bus = createBus()\n\nbus.once('data:ready', (payload) => {\n console.log('data (once):', payload.items.join(', '))\n})\n\nbus.emit('data:ready', { items: ['alpha', 'beta', 'gamma'] }) // fires\nbus.emit('data:ready', { items: ['ignored'] }) // once already consumed\n\nasync function waitForTask() {\n console.log('waiting for task...')\n const result = await bus.wait('task:done')\n console.log('task done! result:', result.result)\n}\n\nvoid waitForTask()\n\nsetTimeout(() => {\n bus.emit('task:done', { result: 99 })\n}, 50)",
|
|
63
|
+
"name": "once() and wait()"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "pipe-events",
|
|
67
|
+
"code": "import { createBus, pipeEvents } from '@vielzeug/herald'\n\n// pipeEvents forwards a subset of events from one bus to another\nconst appBus = createBus()\nconst auditBus = createBus()\n\n// auditBus only receives auth events, not cart events\nauditBus.on('user:login', ({ email, userId }) => {\n console.log('[audit] login:', email, '(id:', userId + ')')\n})\nauditBus.on('user:logout', () => {\n console.log('[audit] logout recorded')\n})\n\nconst controller = new AbortController()\nconst unpipe = pipeEvents(appBus, auditBus, ['user:login', 'user:logout'], { signal: controller.signal })\n\nappBus.emit('user:login', { email: 'alice@example.com', userId: '1' })\nappBus.emit('cart:updated', { total: 99 }) // not forwarded\nappBus.emit('user:logout')\n\nunpipe() // stop forwarding\n\nappBus.emit('user:login', { email: 'bob@example.com', userId: '2' }) // not forwarded\nconsole.log('auditBus listeners after unpipe:', auditBus.listenerCount())",
|
|
68
|
+
"name": "pipeEvents()"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "test-bus",
|
|
72
|
+
"code": "import { createTestBus } from '@vielzeug/herald/testing'\n\n// TestBus wraps a normal bus with emission recording — no mocking required\nconst bus = createTestBus()\n\nconst stop = bus.on('cart:updated', (cart) => console.log('handler saw total:', cart.total))\n\nbus.emit('cart:updated', { items: 1, total: 19.99 })\nbus.emit('cart:updated', { items: 2, total: 39.98 })\nbus.emit('user:logout')\n\nconsole.log('emitted count:', bus.emittedCount('cart:updated')) // 2\nconsole.log('all payloads:', bus.emitted('cart:updated'))\nconsole.log('every recorded event:', bus.allEmitted())\n\nstop()\nbus.emit('cart:updated', { items: 3, total: 59.97 }) // still recorded\n\nconsole.log('after unsubscribe:', bus.emitted('cart:updated'))\n\nbus.dispose()",
|
|
73
|
+
"name": "createTestBus()"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "wait-any",
|
|
77
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\n// waitAny resolves with { event, payload } for whichever event fires first\nconst bus = createBus()\n\nasync function watchNextSessionEvent() {\n console.log('waiting for first session event...')\n\n const result = await bus.waitAny(['user:login', 'user:logout', 'session:expired'])\n\n if (result.event === 'user:login') {\n console.log('login:', result.payload.email, '(id:', result.payload.userId + ')')\n } else if (result.event === 'session:expired') {\n console.log('session expired:', result.payload.reason)\n } else {\n console.log('user logged out')\n }\n}\n\nvoid watchNextSessionEvent()\n\nsetTimeout(() => {\n bus.emit('user:login', { email: 'alice@example.com', userId: '42' })\n bus.emit('user:logout') // ignored — waitAny already resolved\n}, 30)",
|
|
78
|
+
"name": "waitAny()"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "wildcard-listeners",
|
|
82
|
+
"code": "import { createBus } from '@vielzeug/herald'\n\ntype AppEvents = {\n 'user:login': { userId: string }\n 'user:logout': void\n 'cart:updated': { items: number }\n}\n\nconst bus = createBus<AppEvents>({ name: 'app' })\n\n// onAny receives every event — useful for logging, analytics, tracing\nconst unsub = bus.onAny((event, payload) => {\n console.log('[audit]', event, payload)\n})\n\nconsole.log('wildcard listeners:', bus.wildcardCount()) // 1\n\nbus.emit('user:login', { userId: 'alice' })\nbus.emit('cart:updated', { items: 3 })\nbus.emit('user:logout')\n\n// Remove the wildcard listener\nunsub()\nconsole.log('after unsub:', bus.wildcardCount()) // 0\n\nbus.dispose()",
|
|
83
|
+
"name": "onAny + wildcardCount()"
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"typeSignatures": {
|
|
87
|
+
"combineSignals": "export { combineSignals, createBus } from './bus';",
|
|
88
|
+
"createBus": "export { combineSignals, createBus } from './bus';",
|
|
89
|
+
"BusDisposedError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
90
|
+
"HeraldConfigError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
91
|
+
"HeraldError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
92
|
+
"pipeEvents": "export { pipeEvents } from './pipe';",
|
|
93
|
+
"Bus": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
94
|
+
"BusLogger": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
95
|
+
"BusOptions": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
96
|
+
"EmissionErrorContext": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
97
|
+
"EventKey": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
98
|
+
"EventMap": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
99
|
+
"EventStream": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
100
|
+
"Listener": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
101
|
+
"Middleware": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
102
|
+
"PipeableKey": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
103
|
+
"PipeEntry": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
104
|
+
"SubscribeOptions": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
105
|
+
"Unsubscribe": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
106
|
+
"WaitAnyResult": "export type {\n Bus,\n BusLogger,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';"
|
|
107
|
+
}
|
|
108
|
+
}
|