@vielzeug/codex 1.0.4 → 2.0.0
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/README.md +46 -107
- package/data/catalog.json +1679 -0
- package/data/llms-full.txt +18826 -31876
- package/data/llms.txt +32 -114
- package/data/manifest.json +8 -0
- package/data/packages/arsenal.json +210 -0
- package/data/packages/assay.json +40 -0
- package/data/packages/clockwork.json +67 -0
- package/data/packages/codex.json +43 -0
- package/data/packages/coins.json +103 -0
- package/data/packages/conduit.json +60 -0
- package/data/packages/courier.json +58 -0
- package/data/packages/dnd.json +75 -0
- package/data/packages/familiar.json +30 -0
- package/data/packages/flux.json +93 -0
- package/data/packages/forge.json +84 -0
- package/data/packages/herald.json +122 -0
- package/data/packages/keymap.json +65 -0
- package/data/packages/ledger.json +54 -0
- package/data/packages/lingua.json +66 -0
- package/data/packages/orbit.json +112 -0
- package/data/packages/ore.json +73 -0
- package/data/packages/prism.json +70 -0
- package/data/packages/pulse.json +58 -0
- package/data/packages/refine.json +12 -0
- package/data/packages/ripple.json +79 -0
- package/data/packages/rune.json +81 -0
- package/data/packages/sandbox.json +39 -0
- package/data/packages/scout.json +60 -0
- package/data/packages/scroll.json +113 -0
- package/data/packages/sourcerer.json +74 -0
- package/data/packages/spell.json +134 -0
- package/data/packages/tempo.json +113 -0
- package/data/packages/vault.json +90 -0
- package/data/packages/ward.json +125 -0
- package/data/packages/wayfinder.json +113 -0
- package/data/refine.json +11752 -0
- package/data/search.json +1437 -0
- package/dist/catalog.js +149 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.js +33 -59
- package/dist/cli.js.map +1 -1
- package/dist/errors.js +0 -14
- package/dist/errors.js.map +1 -1
- package/dist/http.js +54 -96
- package/dist/http.js.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/server.js +4 -9
- package/dist/server.js.map +1 -1
- package/dist/snapshot.js +233 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/tools/index.js +21 -42
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/packages.js +67 -166
- package/dist/tools/packages.js.map +1 -1
- package/dist/tools/refine.js +99 -305
- package/dist/tools/refine.js.map +1 -1
- package/dist/tools/schema.js +8 -8
- package/dist/tools/schema.js.map +1 -1
- package/dist/tools/shared.js +1 -26
- package/dist/tools/shared.js.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/mcp-setup.json +10 -0
- package/package.json +7 -7
- package/data/.cache.json +0 -34
- package/data/vielzeug-data.json +0 -16118
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { createBehaviorBus } from './behavior-bus';\nexport { combineSignals, createBus } from './bus';\nexport { BusDisposedError, HeraldConfigError, HeraldError } from './errors';\nexport { pipeEvents } from './pipe';\nexport type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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// _delegate.ts is intentionally not exported — internal use only.\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Herald — Typed event bus for TypeScript\ndescription: Zero-dependency typed event bus with subscribe/emit, wait(), async streams, AbortSignal support, bus piping, and test helpers.\npackage: herald\ncategory: events\nkeywords: [event-bus, typed-events, pub-sub, reactive, decoupled, async-streams]\nrelated: [ripple, wayfinder, familiar]\nexports:\n [\n createBus,\n createBehaviorBus,\n pipeEvents,\n combineSignals,\n BusDisposedError,\n HeraldConfigError,\n debugBus,\n debugBehaviorBus,\n createTestBus,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"herald\" />\n\n## Why Herald?\n\nManual event emitters lack TypeScript inference across event names and payloads, and offer no async patterns — `await`ing an event or streaming all future emits requires bespoke wiring.\n\n```ts\n// Before — manual typed event bus\ntype Handlers = { 'user:login': (p: { userId: string }) => void };\nconst listeners = new Map<keyof Handlers, Set<Function>>();\nfunction on<K extends keyof Handlers>(event: K, fn: Handlers[K]) {\n /* ... */\n}\nfunction emit<K extends keyof Handlers>(event: K, payload: Parameters<Handlers[K]>[0]) {\n /* ... */\n}\n// No await, no stream, no AbortSignal, no error isolation\n\n// After — Herald\nimport { createBus } from '@vielzeug/herald';\nconst bus = createBus<AppEvents>();\nbus.on('user:login', ({ userId }) => loadProfile(userId));\nbus.emit('user:login', { userId: '42', email: 'alice@example.com' });\nconst session = await bus.wait('user:login'); // async one-shot\nfor await (const event of bus.events('cart:updated')) {\n} // async stream\n```\n\n| Feature | Herald | mitt | EventEmitter3 |\n| -------------------- | ----------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |\n| Bundle size | <PackageInfo package=\"herald\" type=\"size\" /> | ~200 B | ~1.5 kB |\n| TypeScript inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> Full | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Basic | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Basic |\n| Async/await (`wait`) | <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| Async streaming | <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 | <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| Event piping | <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| Wildcard (`onAny`) | <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| Disposal signal | <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| Error isolation | <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** you need a fully-typed event bus with async patterns (`wait`, `events` generator) and AbortSignal-based lifecycle management.\n\n**Consider mitt when** you only need a bare-minimum synchronous pub/sub with the smallest possible footprint.\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 { BusDisposedError, createBus, pipeEvents } from '@vielzeug/herald';\n\ntype AppEvents = {\n 'user:login': { userId: string; email: string };\n 'user:logout': void;\n};\n\nconst bus = createBus<AppEvents>();\n\nbus.on('user:login', ({ userId }) => {\n console.log('Logged in:', userId);\n});\n\nbus.emit('user:login', { email: 'alice@example.com', userId: '42' });\nbus.emit('user:logout');\n\nconst nextLogin = await bus.wait('user:login');\nconst nextSessionChange = await bus.waitAny(['user:login', 'user:logout']);\n\nif (nextSessionChange.event === 'user:login') {\n console.log(nextSessionChange.payload.userId);\n}\n\nfor await (const payload of bus.events('user:login', { signal: AbortSignal.timeout(5_000) })) {\n console.log(payload.email);\n}\n\n// Forward selected events to another bus\nconst auditBus = createBus<AppEvents>();\nconst unpipe = pipeEvents(bus, auditBus, ['user:login', 'user:logout']);\n\n// Disposal signal — use as an AbortSignal for external cleanup\notherBus.on('count', handler, { signal: bus.disposalSignal });\n\ntry {\n await bus.wait('user:login', { signal: AbortSignal.timeout(500) });\n} catch (err) {\n if (err instanceof BusDisposedError) {\n console.log('Bus was disposed');\n }\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Typed event maps** for strict event/payload correctness\n- **Persistent + one-shot listeners** with `on` and `once` — each registration is independent, including duplicate handlers\n- **Wildcard listeners** with `onAny` — subscribe to all events for cross-cutting concerns like logging and analytics\n- **Listener management APIs** with unsubscribe handles, `wildcardCount()`, and `eventNames()`\n- **Async event coordination** with `wait`\n- **First-event racing** with `waitAny`\n- **Async streaming** with `events` — eager subscription buffers events from the moment `events()` is called; `await using` ensures cleanup on early `break`\n- **Event piping** with `pipeEvents` — forward events across buses with optional renaming and automatic teardown\n- **Middleware pipeline** via `createBus({ middleware: [...] })` — intercept or block dispatches before listeners run\n- **Payload validation** via `createBus({ validatePayload: ... })` — schema-level guards applied before middleware\n- **Disposal signal** via `bus.disposalSignal` — use as an `AbortSignal` to tie external lifecycles to the bus\n- **Leak detection** via `maxListeners` — warn when a single event accumulates too many listeners\n- **Named buses** via `createBus({ name: 'myBus' })` — name appears in debug log prefixes and `BusDisposedError` messages for easier debugging across multiple bus instances\n- **Debug logging** via `logger.debug` or `debugBus()` / `debugBehaviorBus()` (`@vielzeug/herald/devtools`) — logs subscribe/emit/dispose activity with `[herald:*]` prefixes; tree-shaken from production bundles\n- **Abort-aware APIs** for lifecycle-safe teardown\n- **`onAny()` wildcard listener** for bus-wide observability; `wildcardCount()` to inspect active wildcards\n- **Custom logger** via `createBus({ logger: { debug, warn } })` — route or suppress debug and warn output\n- **`onError` hook** for listener-error isolation and resilience\n- **`dispose` and `[Symbol.dispose]`** for deterministic cleanup\n- **Testing helper** via `@vielzeug/herald/testing`\n- **Zero dependencies** — <PackageInfo package=\"herald\" type=\"size\" /> gzipped, <PackageInfo package=\"herald\" type=\"dependencies\" /> dependencies\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — reactive signals and computed state that pair naturally with event-driven update patterns\n- [Wayfinder](/wayfinder/) — client-side router whose navigation lifecycle hooks integrate with bus-dispatched events\n- [Familiar](/familiar/) — Web Worker pool that can use a bus to stream task progress and completion events\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Herald — API Reference\ndescription: Complete API reference for @vielzeug/herald.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --------------------- | ---------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------- |\n| `createBus()` | Create a typed event bus instance | Sync | Use a strict event map to avoid payload drift |\n| `createBehaviorBus()` | Create a bus that replays the last value to new subs | Sync | `events()` and `wait()` do not replay; `once()` fires immediately if buffer exists |\n| `pipeEvents()` | Forward events from one bus to another | Sync | Supports cross-type buses and event renaming |\n| `bus.on()` | Persistent subscription with optional `once` option | Sync | Pass `{ signal }` to auto-unsubscribe |\n| `bus.emit()` | Emit an event; returns listener invocation count | Sync | Every listener runs even if one throws; first error rethrows after |\n| `bus.events()` | Stream future emits as an async generator | Async | Subscribes eagerly; use `maxBuffer` to cap the buffer |\n| `combineSignals()` | Merge N AbortSignals into one | Sync | Returns the first already-aborted signal early |\n| `bus.wait()` | Await a one-time event occurrence | Async | Pass `{ signal }` for timeout / cancellation |\n| `bus.waitAny()` | Await the first event from many | Async | Result is a discriminated union by event key |\n| `bus.onAny()` | Subscribe to all events | Sync | Fires after event-specific listeners |\n| `bus.eventNames()` | Inspect events with active listeners | Sync | Snapshot reflects current subscriptions |\n| `createTestBus()` | Create deterministic test bus utilities | Sync | Reset emitted events between test cases |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --------------------------- | ---------------------------------------------------------- |\n| `@vielzeug/herald` | Main runtime API and types |\n| `@vielzeug/herald/devtools` | `debugBus`, `debugBehaviorBus` — debug wrappers (dev only) |\n| `@vielzeug/herald/testing` | Test helpers (`createTestBus`, `TestBus<T>` type) |\n\n## Types\n\n`EventMap`, `EventKey<T>`, `Listener<T>`, `Unsubscribe` — simple type aliases:\n\n```ts\ntype EventMap = Record<string, unknown>;\ntype EventKey<T extends EventMap> = keyof T & string;\ntype Listener<T> = (payload: T) => void;\ntype Unsubscribe = () => void;\n```\n\n`BusOptions<T>` — options object passed to `createBus()` and `createBehaviorBus()`:\n\n```ts\ntype SubscribeOptions = {\n once?: boolean; // auto-remove after first invocation\n signal?: AbortSignal; // auto-remove when signal aborts\n};\n\ntype EmissionErrorContext<T extends EventMap = EventMap> = {\n err: unknown; // the thrown error\n event: EventKey<T>; // event key that triggered the failing listener\n payload: unknown; // payload passed to the listener\n timestamp: number; // ms since epoch at emit() call time\n};\n\ntype BusLogger = {\n debug?: (msg: string) => void; // omit to silence debug output\n warn?: (msg: string) => void; // omit to silence warn output\n};\n\ntype BusOptions<T extends EventMap> = {\n logger?: BusLogger;\n maxListeners?: number;\n middleware?: readonly Middleware<T>[];\n name?: string; // optional display name — appears in log prefixes and BusDisposedError messages; avoid sensitive/user-derived values\n onError?: (context: EmissionErrorContext<T>) => void;\n validatePayload?: <K extends EventKey<T>>(event: K, payload: T[K]) => void;\n};\n```\n\n`Middleware<T>` — a function called in sequence on every `emit()`, before listeners run. Call `next()` to continue; omit it to block dispatch:\n\n```ts\ntype Middleware<T extends EventMap = EventMap> = (event: EventKey<T>, payload: unknown, next: () => void) => void;\n```\n\n`PipeableKey<S, T>` — keys shared between two event maps with compatible payload types:\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```\n\n`PipeEntry<S, T>` — a single entry for `pipeEvents`, either a key string or a `{ from, to }` rename:\n\n```ts\ntype PipeEntry<S extends EventMap, T extends EventMap> = PipeableKey<S, T> | { from: EventKey<S>; to: EventKey<T> };\n```\n\n`EventStream<T>` — returned by `bus.events()`. Extends `AsyncGenerator<T>` with `AsyncDisposable`:\n\n```ts\ntype EventStream<T> = AsyncGenerator<T> & AsyncDisposable;\n```\n\n`WaitAnyResult<T, K>` — discriminated-union result returned by `waitAny()`:\n\n```ts\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`Bus<T>` — the runtime bus interface. Individual method docs are in the [Bus Interface](#bus-interface) section.\n\n```ts\ntype Bus<T extends EventMap> = {\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.dispose](): void;\n dispose(): void;\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, options?: { 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`BehaviorBus<T>` — extends `Bus<T>` with last-value replay. Full docs in the [`createBehaviorBus()`](#createbehaviorbus) section.\n\n```ts\ntype BehaviorInitial<T extends EventMap> = { [K in EventKey<T>]?: T[K] };\n\ntype BehaviorBus<T extends EventMap> = Bus<T> & {\n current<K extends EventKey<T>>(event: K): T[K] | undefined;\n reset(event?: EventKey<T>): void;\n snapshot(): Partial<T>;\n};\n```\n\n`TestBus<T>` — extends `Bus<T>` with emission recording. Full docs in the [Testing Utilities](#testing-utilities) section.\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 removeAllListeners<K extends EventKey<T>>(event: K): void;\n reset(): void;\n};\n```\n\n## `createBus()`\n\nSignature: `createBus<T extends EventMap>(options?: BusOptions<T>): Bus<T>`\n\nCreates and returns a new `Bus<T>` instance.\n\n| Parameter | Type | Description |\n| --------- | --------------- | --------------------------- |\n| `options` | `BusOptions<T>` | Optional hook configuration |\n\n**Returns:** `Bus<T>`\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ntype AppEvents = {\n 'user:login': { userId: string };\n 'user:logout': void;\n};\n\nconst bus = createBus<AppEvents>({\n logger: { debug: myLogger.debug, warn: myLogger.warn }, // provide debug to enable logging; pass {} to silence all\n onError: ({ err, event, payload }) => console.error('[bus] error in', event, err, payload),\n middleware: [\n (event, _payload, next) => {\n // run before listeners; omit next() to block dispatch\n console.debug('[mw]', event);\n next();\n },\n ],\n validatePayload: (event, payload) => {\n // throw to reject the emit before any middleware or listener runs\n if (event === 'count' && typeof payload !== 'number') throw new TypeError('must be number');\n },\n});\n```\n\n## Bus Interface\n\n### `bus.disposed`\n\nType: `readonly boolean`\n\n`true` after `dispose()` has been called. Use this to guard against using a torn-down bus.\n\n```ts\nif (!bus.disposed) {\n bus.emit('user:login', payload);\n}\n```\n\n---\n\n### `bus.disposalSignal`\n\nType: `readonly AbortSignal`\n\nAn `AbortSignal` that fires when the bus is disposed. Use it to tie external lifecycles to the bus lifetime without polling `bus.disposed`.\n\n```ts\n// Automatically unsubscribe from another bus when this bus is torn down\notherBus.on('count', syncState, { signal: bus.disposalSignal });\n\n// Cancel a fetch when the bus is disposed\nfetch('/api/stream', { signal: bus.disposalSignal });\n\n// Combine with a timeout\nconst signal = AbortSignal.any([bus.disposalSignal, AbortSignal.timeout(10_000)]);\n```\n\nThe signal is already aborted when `bus.disposed` is `true`.\n\n---\n\n### `bus.on()`\n\nSignature: `on(event, listener, opts?) => Unsubscribe`\n\nSubscribe to an event. The listener runs synchronously on every emit.\n\n| Parameter | Type | Description |\n| ---------- | ------------------ | --------------------------------------------------- |\n| `event` | `K` | Event key to subscribe to |\n| `listener` | `Listener<T[K]>` | Callback for each emit |\n| `opts` | `SubscribeOptions` | Optional `{ signal?, once? }` for lifecycle control |\n\n**Returns:** `Unsubscribe` — call to remove the listener manually.\n\nIf `opts.signal` is already aborted, `on()` returns a no-op unsubscribe immediately without adding the listener.\n\n::: tip Multiple registrations\nRegistering the same listener function twice creates **two independent subscriptions**. The listener will fire twice per emit and each registration has its own unsubscribe handle. There is no deduplication.\n:::\n\n```ts\nconst unsub = bus.on('user:login', ({ userId }) => {\n console.log('login:', userId);\n});\nunsub();\n\n// Auto-unsubscribe via AbortSignal\nconst controller = new AbortController();\nbus.on('theme:change', applyTheme, { signal: controller.signal });\ncontroller.abort(); // listener removed\n\n// One-shot subscription inline\nbus.on('session:expired', redirectToLogin, { once: true });\n\n// Both options combined\nbus.on('cart:updated', syncState, { once: true, signal: controller.signal });\n```\n\n---\n\n### `bus.once()`\n\nSignature: `once(event, listener, opts?) => Unsubscribe`\n\nConvenience wrapper around `bus.on(event, listener, { once: true, signal: opts?.signal })`. The listener fires exactly once and is automatically removed.\n\n| Parameter | Type | Description |\n| ---------- | -------------------------- | ---------------------------------- |\n| `event` | `K` | Event key |\n| `listener` | `Listener<T[K]>` | One-shot callback |\n| `opts` | `{ signal?: AbortSignal }` | Optional `signal` for early cancel |\n\n**Returns:** `Unsubscribe`\n\n```ts\nbus.once('session:expired', () => redirectToLogin());\n\n// Cancel before it fires\nconst controller = new AbortController();\nbus.once('session:expired', redirectToLogin, { signal: controller.signal });\ncontroller.abort();\n```\n\n---\n\n### `bus.onAny()`\n\nSignature: `onAny(listener, opts?) => Unsubscribe`\n\nSubscribe to **all** events. The listener is called after event-specific listeners on every emit.\n\n| Parameter | Type | Description |\n| ---------- | ------------------------------------------------ | --------------------------------------------------- |\n| `listener` | `(event: EventKey<T>, payload: unknown) => void` | Wildcard callback |\n| `opts` | `SubscribeOptions` | Optional `{ signal?, once? }` for lifecycle control |\n\n**Returns:** `Unsubscribe`\n\n`onAny` listeners count is tracked separately via `wildcardCount()` and are not included in per-event `listenerCount()` results.\n\n```ts\nconst unsub = bus.onAny((event, payload) => {\n analytics.track(event, payload);\n});\n\nunsub(); // remove when done\n\n// With AbortSignal\nconst controller = new AbortController();\nbus.onAny(logger, { signal: controller.signal });\ncontroller.abort();\n\n// Fire exactly once\nbus.onAny(logFirstEvent, { once: true });\n```\n\n---\n\n### `bus.wait()`\n\nSignature: `wait(event, opts?) => Promise<payload>`\n\nReturns a `Promise` that resolves with the payload of the next emit of `event`.\n\n| Parameter | Type | Description |\n| --------- | -------------------------- | -------------------------- |\n| `event` | `K` | Event key to await |\n| `opts` | `{ signal?: AbortSignal }` | Optional `signal` to abort |\n\n**Returns:** `Promise<T[K]>`\n\n**Rejects when:**\n\n- The bus is disposed before the event fires — rejects with `BusDisposedError`\n- The provided `signal` aborts — rejects with `signal.reason`\n\n```ts\nconst login = await bus.wait('user:login');\n\n// With timeout\nconst timedLogin = await bus.wait('user:login', { signal: AbortSignal.timeout(5_000) });\n```\n\n---\n\n### `bus.waitAny()`\n\nSignature: `waitAny(events, opts?) => Promise<{ event, payload }>`\n\nWaits for the first emitted event among a list of event keys.\n\n| Parameter | Type | Description |\n| --------- | -------------------------- | -------------------------- |\n| `events` | `readonly K[]` | Event keys to race |\n| `opts` | `{ signal?: AbortSignal }` | Optional `signal` to abort |\n\n**Returns:** `Promise<WaitAnyResult<T, K>>`\n\n**Throws synchronously:** `HeraldConfigError` if fewer than 2 event keys are provided.\n\n**Rejects when:**\n\n- The bus is disposed before any listed event fires — rejects with `BusDisposedError`\n- The provided `signal` aborts — rejects with `signal.reason`\n\n```ts\nconst winner = await bus.waitAny(['user:login', 'user:logout']);\n\nif (winner.event === 'user:login') {\n console.log(winner.payload.userId);\n}\n```\n\n---\n\n### `bus.events()`\n\nSignature: `events(event, options?) => EventStream<payload>`\n\nReturns an `EventStream<T[K]>` — an `AsyncGenerator` extended with `AsyncDisposable` and chainable `.filter()`, `.map()`, and `.take()` operators — that yields payloads for every future emit of `event`.\n\n- `event: K` — event key to stream\n- `options?: { signal?: AbortSignal; maxBuffer?: number }` — optional early termination and buffering\n\n::: warning Synchronous validation\n`events()` validates `maxBuffer` **synchronously** at call time. If `maxBuffer` is `0` or negative, a `HeraldConfigError` is thrown immediately — not on the first `await`.\n:::\n\n::: tip Eager subscription\n`events()` subscribes **when called**, not when the first `for await` iteration begins. Events emitted before iteration starts are buffered and yielded immediately on the first iteration.\n:::\n\n**Terminates when:**\n\n- The bus is disposed — generator returns cleanly (no exception thrown)\n- The provided `signal` aborts — generator returns cleanly (no exception thrown)\n\n**`AsyncDisposable` support:** Use the `await using` keyword for guaranteed cleanup:\n\n```ts\n// Standard iteration\nfor await (const payload of bus.events('cart:updated')) {\n renderCart(payload.items);\n // loop exits cleanly when bus is disposed or signal aborts\n}\n\n// Stop early with a signal\nconst ctl = new AbortController();\nfor await (const payload of bus.events('data:loaded', { signal: ctl.signal, maxBuffer: 50 })) {\n if (payload.count === 0) ctl.abort();\n}\n\n// await using — cleanup guaranteed even on break or throw\nawait using stream = bus.events('user:login');\nfor await (const { userId } of stream) {\n if (userId === targetId) break; // subscription torn down automatically\n}\n\n// Stop early with a break — subscription torn down automatically via AsyncDisposable\nawait using stream2 = bus.events('count');\nfor await (const n of stream2) {\n if (n > 100) break;\n}\n```\n\n---\n\n### `bus.emit()`\n\nSignature: `emit(event, payload?) => number`\n\nEmit an event, calling all registered listeners synchronously in subscription order. Returns the number of listeners that were invoked.\n\n- For `void` events, no second argument is accepted.\n- For payload events, the second argument is required and type-checked.\n- Returns `0` when: the bus is disposed, a `middleware` function blocked dispatch, or `validatePayload` threw an error (with `onError` configured).\n- **Listener throws:** every registered listener (specific and wildcard) for the emission still runs, regardless of whether an earlier one threw. Without `onError` configured, the first thrown error is rethrown once every listener has been called — it never short-circuits the rest of the broadcast. With `onError` configured, every error is forwarded per-listener and `emit()` never throws for a listener failure.\n\n```ts\nconst count = bus.emit('user:login', { userId: '42', email: 'alice@example.com' });\nbus.emit('user:logout'); // void — no argument\nconsole.log(count); // number of listeners invoked\n```\n\n---\n\n### `bus.dispose()`\n\nSignature: `dispose() => void`\n\nPermanently tears down the bus:\n\n- All listeners are removed.\n- All pending `wait()` promises are rejected with `BusDisposedError`.\n- Subsequent `emit()` and `on()` calls become no-ops.\n\nIdempotent — safe to call multiple times.\n\n```ts\nbus.dispose();\nbus.disposed; // true\n```\n\n---\n\n### `bus[Symbol.dispose]()`\n\nSignature: `[Symbol.dispose]() => void`\n\nAlias for `dispose()`. Enables the `using` keyword (TypeScript 5.2+):\n\n```ts\n{\n using bus = createBus<AppEvents>();\n // ...\n} // dispose() called automatically\n```\n\n---\n\n### `bus.listenerCount()`\n\nSignature: `listenerCount(event?) => number`\n\nReturns the number of active listeners.\n\n| Parameter | Type | Description |\n| --------- | ------------------------ | ---------------------------------------- |\n| `event` | `EventKey<T>` (optional) | Specific event key; omit for total count |\n\n**Returns:** `number`\n\nWhen called with an event key, the count includes **both** event-specific listeners **and** any `onAny` wildcard listeners — since wildcards fire on every emission of every event. When called without an argument, wildcards are counted once in the total.\n\n```ts\nbus.on('user:login', handler1);\nbus.on('user:login', handler2);\nbus.on('user:logout', handler3);\nbus.onAny(wildcardHandler);\n\nbus.listenerCount('user:login'); // 3 — 2 specific + 1 wildcard\nbus.listenerCount('user:logout'); // 2 — 1 specific + 1 wildcard\nbus.listenerCount(); // 4 — 3 specific + 1 wildcard (wildcard counted once)\n\nbus.dispose();\nbus.listenerCount(); // 0\n```\n\n---\n\n### `bus.eventNames()`\n\nSignature: `eventNames() => EventKey<T>[]`\n\nReturns a snapshot of event keys that currently have at least one active listener.\n\n```ts\nbus.on('user:login', handler1);\nbus.on('user:logout', handler2);\n\nbus.eventNames(); // ['user:login', 'user:logout']\n```\n\n---\n\n### `bus.wildcardCount()`\n\nSignature: `wildcardCount() => number`\n\nReturns the number of active `onAny` wildcard listeners. Wildcards are counted separately from event-specific listeners — this is the count that `listenerCount(event)` adds on top of the specific count for each event.\n\n```ts\nbus.onAny(logAll);\nbus.onAny(trackAll);\n\nbus.wildcardCount(); // 2\nbus.listenerCount('user:login'); // 0 specific + 2 wildcard = 2\n```\n\n## `BusOptions` — middleware\n\n`BusOptions.middleware` accepts an array of `Middleware<T>` functions that run on every `emit()`, after `validatePayload` and before listeners. Each middleware receives `(event, payload, next)` — call `next()` to continue the chain; omit it to block dispatch entirely.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<AppEvents>({\n middleware: [\n // Logging middleware — logs every event then continues\n (event, payload, next) => {\n console.debug(`[mw] ${event}`, payload);\n next();\n },\n // Rate-limiting middleware — blocks dispatch under certain conditions\n (event, _payload, next) => {\n if (!rateLimiter.allow(event)) return; // omit next() to block listeners\n next();\n },\n ],\n});\n\nbus.on('user:login', handler);\nbus.emit('user:login', { userId: '1', email: 'a@b.com' });\n// → [mw] user:login { userId: '1', ... }\n// → handler fires (if rate limiter allows)\n```\n\n`emit()` returns `0` when any middleware omits `next()`.\n\n## `BusOptions` — validatePayload\n\n`BusOptions.validatePayload` is called on every `emit()` **before** middleware and listeners. Throw to reject the payload entirely — no middleware or listener will run.\n\n| Throw with `onError` | Error forwarded to `onError`; `emit()` returns `0`. |\n| Throw without `onError` | Error propagates to the `emit()` caller. |\n\n```ts\nconst bus = createBus<AppEvents>({\n validatePayload: (event, payload) => {\n if (event === 'count' && typeof payload !== 'number') {\n throw new TypeError(`\"count\" payload must be a number, got ${typeof payload}`);\n }\n },\n onError: ({ err, event }) => logger.warn('rejected emit', event, err),\n});\n\nbus.on('count', vi.fn());\nbus.emit('count', 'oops'); // → onError called; listener never fires; returns 0\nbus.emit('count', 42); // → listener fires; returns 1\n```\n\n## `createBehaviorBus()`\n\nSignature: `createBehaviorBus<T extends EventMap>(initial?, options?) => BehaviorBus<T>`\n\nCreates a bus that replays the last known value to new subscribers. Useful for state-like events where late subscribers should receive the current value immediately.\n\n| Parameter | Type | Description |\n| --------- | -------------------- | ---------------------------------------------- |\n| `initial` | `BehaviorInitial<T>` | Optional map of event names to starting values |\n| `options` | `BusOptions<T>` | Standard bus options (hooks, logger, etc.) |\n\n**Returns:** `BehaviorBus<T>`\n\n**Replay rules:**\n\n- `on()` and `once()` — replay the current value synchronously to new subscribers.\n- `events()`, `wait()`, `waitAny()` — no replay; behave like a regular bus.\n- The returned `BehaviorBus<T>` adds `current(event)`, `reset()`, and `snapshot()` methods.\n- The replay buffer is only updated when dispatch actually runs — payloads rejected by `validatePayload` or blocked by middleware that omits `next()` are never buffered.\n- **`once()` with a buffered value:** if a current value exists, the listener fires immediately (synchronously) and is never registered for future emits. Use `on()` if you need to receive the _next_ emit rather than the current state.\n\n```ts\nimport { createBehaviorBus } from '@vielzeug/herald';\n\ntype UIEvents = { theme: 'light' | 'dark'; zoom: number };\n\nconst bus = createBehaviorBus<UIEvents>({ theme: 'light', zoom: 1 });\n\n// New subscribers receive the current value immediately\nbus.on('theme', applyTheme); // called with 'light' right now\nbus.emit('theme', 'dark');\n\nbus.on('theme', applyTheme); // called with 'dark' right now\n\n// Read current value without subscribing\nbus.current('theme'); // 'dark'\nbus.current('zoom'); // 1 (from initial)\n```\n\n### `behaviorBus.current()`\n\nSignature: `current(event) => T[K] | undefined`\n\nReturns the last emitted value for the given event, or `undefined` if no value has been emitted and no initial value was provided.\n\n```ts\nbus.current('theme'); // 'dark'\nbus.current('unknown' as never); // undefined\n```\n\n---\n\n### `behaviorBus.reset()`\n\nSignature: `reset(event?) => void`\n\nClears the replay buffer for a specific event, or for all events when called without arguments. After reset, new subscribers will not receive a replayed value until the next `emit()`.\n\nDoes not affect active subscriptions or the disposed state of the bus.\n\n```ts\nbus.emit('theme', 'dark');\nbus.current('theme'); // 'dark'\n\nbus.reset('theme'); // clear only 'theme'\nbus.current('theme'); // undefined\n\nbus.reset(); // clear all buffers\n```\n\n---\n\n### `behaviorBus.snapshot()`\n\nSignature: `snapshot() => Partial<T>`\n\nReturns a plain object containing the most recently emitted value for every currently buffered event. Events with no value in the buffer are omitted from the result.\n\nUseful for serializing the current state of all channels at once, for debugging, or for hydrating a new bus from a snapshot.\n\n::: tip Dangerous event names are omitted\nAn event named `__proto__`, `constructor`, or `prototype` is silently excluded from the returned object — bracket-assigning one of these keys onto a plain object literal would hijack its prototype rather than set an own property. The value itself is still tracked internally and reachable via `current(event)`; only the object-literal snapshot omits it.\n:::\n\n```ts\ntype UIEvents = { theme: 'light' | 'dark'; zoom: number; sidebar: boolean };\n\nconst bus = createBehaviorBus<UIEvents>({ theme: 'light', zoom: 1 });\n\nbus.snapshot();\n// → { theme: 'light', zoom: 1 } (sidebar not buffered — omitted)\n\nbus.emit('theme', 'dark');\nbus.snapshot();\n// → { theme: 'dark', zoom: 1 }\n\nbus.reset('theme');\nbus.snapshot();\n// → { zoom: 1 }\n```\n\n## `pipeEvents()`\n\nSignature: `pipeEvents<S, T>(source, target, entries, opts?) => Unsubscribe`\n\nForwards a selected subset of events from a source bus to a target bus. Source and target may have different event map types — only the listed keys must be compatible.\n\n| Parameter | Type | Description |\n| --------- | -------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `source` | `Bus<S>` | The bus to listen on |\n| `target` | `Bus<T>` | The bus to forward events to |\n| `entries` | `readonly [PipeEntry<S, T>, ...PipeEntry<S, T>[]]` | One or more string keys or `{ from, to }` renames — throws `HeraldConfigError` if empty |\n| `opts` | `{ signal?: AbortSignal }` (optional) | Optional signal to stop forwarding early |\n\n**Returns:** `Unsubscribe` — call to stop forwarding manually.\n\nForwarding stops automatically when the **target bus is disposed**. Source disposal is handled via the source bus's own subscription lifecycle.\n\n```ts\nimport { createBus, pipeEvents } from '@vielzeug/herald';\n\nconst appBus = createBus<AppEvents>();\nconst auditBus = createBus<AppEvents>();\n\n// Forward only auth events — tears down automatically when auditBus disposes\nconst unpipe = pipeEvents(appBus, auditBus, ['user:login', 'user:logout']);\n\n// Stop forwarding manually\nunpipe();\n\n// Scope to a signal\nconst controller = new AbortController();\npipeEvents(appBus, auditBus, ['user:login'], { signal: controller.signal });\ncontroller.abort(); // forwarding stops\n\n// Rename events during forwarding\ntype AuthEvents = { 'auth:login': { userId: string }; 'auth:logout': void };\ntype AppEventsTarget = { 'user:authenticated': { userId: string }; 'user:signed-out': void };\n\nconst authBus = createBus<AuthEvents>();\nconst targetBus = createBus<AppEventsTarget>();\n\npipeEvents(authBus, targetBus, [\n { from: 'auth:login', to: 'user:authenticated' },\n { from: 'auth:logout', to: 'user:signed-out' },\n]);\n```\n\n## Testing Utilities\n\nImport from `@vielzeug/herald/testing`.\n\n### `createTestBus()`\n\nSignature: `createTestBus<T extends EventMap>(options?: BusOptions<T>): TestBus<T>`\n\nCreates a `TestBus<T>` (a full `Bus<T>` plus recording helpers).\n\nBehavior:\n\n- Every `emit()` is recorded per event key\n- `emitted(event)` returns a snapshot array\n- `reset()` clears records without removing listeners\n- `dispose()` clears listeners and records\n- Accepts full `BusOptions<T>` including `onError`\n\n| Parameter | Type | Description |\n| --------- | --------------- | ----------------------------------------------- |\n| `options` | `BusOptions<T>` | Optional hooks; composed with internal recorder |\n\n**Returns:** `TestBus<T>`\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\ntype AppEvents = {\n 'user:login': { userId: string };\n};\n\nconst bus = createTestBus<AppEvents>();\n\nbus.emit('user:login', { userId: '1' });\nconsole.log(bus.emitted('user:login')); // [{ userId: '1' }]\n```\n\n---\n\n### `testBus.emitted()`\n\nSignature: `emitted(event) => payload[]`\n\nReturns a **snapshot** of all payloads emitted for the given event key, in emission order. Each call returns a new array — mutations do not affect the internal records.\n\n```ts\nbus.emit('user:login', { userId: '1', email: 'a@example.com' });\nbus.emit('user:login', { userId: '2', email: 'b@example.com' });\n\nbus.emitted('user:login');\n// => [{ userId: '1', email: 'a@example.com' }, { userId: '2', email: 'b@example.com' }]\n```\n\n---\n\n### `testBus.emittedCount()`\n\nSignature: `emittedCount(event) => number`\n\nReturns the number of times the given event has been emitted. Shorthand for `emitted(event).length`.\n\n```ts\nbus.emit('user:login', { userId: '1' });\nbus.emit('user:login', { userId: '2' });\n\nbus.emittedCount('user:login'); // 2\nbus.emittedCount('user:logout'); // 0\n```\n\n---\n\n### `testBus.reset()`\n\nSignature: `reset() => void`\n\nClears all recorded payloads without disposing the bus or removing any listeners.\n\n```ts\nbus.reset();\nbus.emitted('user:login'); // => []\n```\n\n---\n\n### `testBus.allEmitted()`\n\nSignature: `allEmitted() => { [K in EventKey<T>]?: T[K][] }`\n\nReturns a snapshot object containing all recorded payloads for every event that has been emitted at least once. Keys absent from the result have never been emitted. Each call returns a new object — mutations do not affect internal records.\n\nUseful for asserting that **no other events** were emitted beyond the ones being tested.\n\n::: tip Dangerous event names are omitted\nAn event named `__proto__`, `constructor`, or `prototype` is silently excluded from the returned object — bracket-assigning one of these keys onto a plain object literal would hijack its prototype rather than set an own property. The recorded payloads are still tracked internally and reachable via `emitted(event)`; only the object-literal snapshot omits them.\n:::\n\n```ts\nbus.emit('user:login', { userId: '1' });\nbus.emit('theme:change', 'dark');\n\nbus.allEmitted();\n// => { 'user:login': [{ userId: '1' }], 'theme:change': ['dark'] }\n```\n\n---\n\n### `testBus.dispose()`\n\nSignature: `dispose() => void`\n\nClears recorded payloads and then calls the underlying `bus.dispose()`, removing all listeners and rejecting pending waits. Idempotent.\n\n---\n\n### `testBus.removeAllListeners()`\n\nSignature: `removeAllListeners(event) => void`\n\nUnsubscribes all listeners registered via `on()` for the given event key. Emission records are preserved — call `reset()` separately to clear them.\n\n```ts\nbus.on('user:login', handlerA);\nbus.on('user:login', handlerB);\nbus.emit('user:login', { userId: '1' });\n\nbus.removeAllListeners('user:login');\nbus.emit('user:login', { userId: '2' }); // neither handler fires\n\nbus.emitted('user:login'); // => [{ userId: '1' }, { userId: '2' }] — records intact\n```\n\n::: tip Test-only utility\n`removeAllListeners` is available on `TestBus` only — it is not part of the standard `Bus<T>` interface. Use unsubscribe handles or `{ signal }` options for lifecycle-managed cleanup in production code.\n:::\n\n## `combineSignals()`\n\nSignature: `combineSignals(first: AbortSignal, ...rest: AbortSignal[]) => AbortSignal`\n\nReturns a signal that aborts as soon as any of the provided signals abort. With a single argument, returns it directly (no allocation). Registers and cleans up its own event listeners — no leaks when no signal fires.\n\n| Parameter | Type | Description |\n| --------- | --------------- | ----------------------------------------------------- |\n| `first` | `AbortSignal` | First signal; returned directly if no others provided |\n| `...rest` | `AbortSignal[]` | Additional signals to race |\n\n**Returns:** `AbortSignal` — aborts when any input aborts.\n\n```ts\nimport { combineSignals, createBus } from '@vielzeug/herald';\n\nconst bus = createBus<AppEvents>();\nconst timeoutSignal = AbortSignal.timeout(5_000);\n\n// Unsubscribe when the bus disposes OR after 5 seconds\nconst signal = combineSignals(bus.disposalSignal, timeoutSignal);\nbus.on('user:login', handler, { signal });\n\n// Three signals — no nesting required\nconst signal3 = combineSignals(userSignal, timeoutSignal, bus.disposalSignal);\n```\n\n::: tip Why `combineSignals` over `AbortSignal.any()`?\n`AbortSignal.any([a, b])` is a platform equivalent, but it retains a strong reference to both signals until one fires. If neither signal ever fires, the internal `'abort'` listeners are never removed — a potential memory leak in long-lived buses.\n\n`combineSignals(a, b)` uses `once: true` listeners that clean up immediately as soon as either signal fires, making it the safer choice for `disposalSignal`-scoped subscriptions.\n:::\n\n## Errors\n\n### `BusDisposedError`\n\n```ts\nclass BusDisposedError extends Error {\n override name = 'BusDisposedError';\n // message: 'Bus is disposed' or 'Bus \"<name>\" is disposed' when name option is set\n}\n```\n\nThrown as the rejection reason when a pending `wait()` or `waitAny()` call is interrupted by `bus.dispose()`. Also used as the abort reason on `bus.disposalSignal`.\n\nWhen the bus was created with a `name` option, the message includes the name: `Bus \"myBus\" is disposed`.\n\nUse `instanceof` to distinguish from signal aborts and other rejections:\n\n```ts\nimport { BusDisposedError } from '@vielzeug/herald';\n\ntry {\n await bus.wait('user:login');\n} catch (err) {\n if (err instanceof BusDisposedError) {\n // bus was torn down before the event fired\n } else {\n throw err; // signal abort or unexpected error\n }\n}\n```\n\n---\n\n### `HeraldConfigError`\n\n```ts\nclass HeraldConfigError extends HeraldError {}\n```\n\nThrown synchronously for invalid arguments or configuration, before any async work begins:\n\n- `waitAny(events)` — fewer than 2 event keys provided\n- `events(event, { maxBuffer })` — `maxBuffer` is `0` or negative\n- `pipeEvents(source, target, entries)` — `entries` is empty\n\n```ts\nimport { HeraldConfigError } from '@vielzeug/herald';\n\ntry {\n bus.waitAny(['user:login']); // only one key — needs at least 2\n} catch (err) {\n if (err instanceof HeraldConfigError) {\n console.error('Invalid herald call:', err.message);\n }\n}\n```\n\n---\n\n### `HeraldError`\n\n```ts\nclass HeraldError extends Error {\n static is(err: unknown): err is HeraldError;\n}\n```\n\nBase class for every error herald throws (`BusDisposedError`, `HeraldConfigError`). Use `HeraldError.is()` to catch any herald-originated error without enumerating each subclass:\n\n```ts\nimport { HeraldError } from '@vielzeug/herald';\n\ntry {\n bus.waitAny(['user:login']);\n} catch (err) {\n if (HeraldError.is(err)) {\n // BusDisposedError or HeraldConfigError\n console.error('Herald error:', err.message);\n } else {\n throw err; // not from herald\n }\n}\n```\n\n## Devtools\n\nImport from `@vielzeug/herald/devtools` — a dedicated sub-path so `console.debug` references are tree-shaken from production bundles when this sub-path is not imported.\n\n### `debugBus()`\n\nSignature: `debugBus<T extends EventMap>(options?) => Bus<T>`\n\nEquivalent to `createBus({ logger: { debug: console.debug } })`. Pass `logger.warn` to also redirect or silence warnings; every other `BusOptions<T>` field (`maxListeners`, `middleware`, `name`, `onError`, `validatePayload`) passes through unchanged.\n\n```ts\nimport { debugBus } from '@vielzeug/herald/devtools';\n\nconst bus = debugBus<AppEvents>();\n// or redirect warnings:\nconst auditedBus = debugBus<AppEvents>({ logger: { warn: myLogger.warn } });\n```\n\n### `debugBehaviorBus()`\n\nSignature: `debugBehaviorBus<T extends EventMap>(initial?, options?) => BehaviorBus<T>`\n\nEquivalent to `createBehaviorBus(initial, { logger: { debug: console.debug } })`. Same `logger.warn` override and `BehaviorBusOptions<T>` passthrough as `debugBus()`.\n\n```ts\nimport { debugBehaviorBus } from '@vielzeug/herald/devtools';\n\nconst bus = debugBehaviorBus<UIState>({ theme: 'light' });\n// or redirect warnings:\nconst auditedBus = debugBehaviorBus<UIState>({ theme: 'light' }, { logger: { warn: myLogger.warn } });\n```\n",
|
|
6
|
+
"usage": "---\ntitle: Herald — Usage Guide\ndescription: Event maps, subscriptions, wait(), async event streams, hooks, cleanup, and testing for @vielzeug/herald.\n---\n\n[[toc]]\n\n::: tip New to Herald?\nStart with the [Overview](./index.md) for a quick introduction and installation, then come back here for in-depth usage patterns.\n:::\n\n## Basic Usage\n\nAn event map is a plain TypeScript type where each key is an event name and each value is the payload type. Use `void` for signal events that carry no data.\n\n```ts\ntype AppEvents = {\n // events with payloads\n 'user:login': { userId: string; email: string };\n 'user:logout': void; // signal — no payload\n 'cart:updated': { items: CartItem[]; total: number };\n 'theme:change': 'light' | 'dark';\n 'data:loaded': { count: number; items: unknown[] };\n};\n\nconst bus = createBus<AppEvents>();\n```\n\n## Subscribing\n\n### `on()` — Persistent subscription\n\n`on()` registers a listener for every future emit of an event. It returns an `Unsubscribe` function.\n\n```ts\nconst unsub = bus.on('user:login', ({ userId, email }) => {\n console.log('logged in:', userId, email); // fully typed\n});\n\n// Remove the listener\nunsub();\n```\n\n::: tip Multiple registrations\nRegistering the **same listener function** twice creates two independent subscriptions — the listener fires twice per emit and each registration has its own independent unsubscribe handle. There is no deduplication.\n:::\n\n### `once()` — One-shot listener\n\n`once()` registers a listener that fires exactly once, then removes itself automatically.\n\n```ts\nbus.once('user:logout', () => {\n redirectToLogin();\n});\n```\n\n### `eventNames()` — Introspect active subscriptions\n\nGet a snapshot of event keys that currently have listeners.\n\n```ts\nbus.on('user:login', handler);\nbus.on('user:logout', handler);\n\nconsole.log(bus.eventNames()); // ['user:login', 'user:logout']\n```\n\n### AbortSignal and `SubscribeOptions`\n\nPass a `SubscribeOptions` object as the third argument to `on()`. Use `signal` to auto-unsubscribe when an `AbortSignal` fires, and `once` to auto-remove after the first invocation.\n\n```ts\nconst controller = new AbortController();\n\n// Auto-remove when signal aborts\nbus.on('user:login', handler, { signal: controller.signal });\n\n// Later — removes the listener automatically\ncontroller.abort();\n\n// One-shot subscription inline (equivalent to bus.once())\nbus.on('user:login', handler, { once: true });\n\n// Both combined — fires at most once, and only before signal aborts\nbus.on('cart:updated', handler, { once: true, signal: controller.signal });\n```\n\n## Emitting Events\n\n`emit()` calls all registered listeners synchronously and returns the number of listeners that were invoked.\n\n```ts\nconst count = bus.emit('user:login', { userId: '42', email: 'alice@example.com' });\nbus.emit('user:logout'); // void event — no second argument\nconsole.log(count); // number of listeners fired\n```\n\n`emit()` returns `0` when the bus is disposed, when middleware blocks dispatch, or when `validatePayload` rejects the payload with `onError` configured.\n\nEvery registered listener still runs even if an earlier one throws. Without `onError` configured, the first thrown error is rethrown once every listener has been called — it never short-circuits the rest of the broadcast. With `onError`, every error is captured per-listener and `emit()` never throws for a listener failure.\n\n### Middleware\n\nPass `middleware` to `createBus()` to intercept every `emit()` before listeners run. Middleware functions receive `(event, payload, next)` — call `next()` to continue, or omit it to block dispatch:\n\n```ts\nconst bus = createBus<AppEvents>({\n middleware: [\n (event, payload, next) => {\n console.debug('[dispatch]', event, payload);\n next();\n },\n // Rate limit: block dispatch if quota is exceeded\n (event, _payload, next) => {\n if (rateLimiter.allow(event)) next();\n },\n ],\n});\n```\n\nMultiple middleware run in array order. If any omits `next()`, subsequent middleware and all listeners are skipped and `emit()` returns `0`.\n\n### `validatePayload`\n\nUse `validatePayload` for schema-level guards that run **before** middleware. Throw to reject the emit:\n\n```ts\nconst bus = createBus<AppEvents>({\n validatePayload: (event, payload) => {\n if (event === 'count' && typeof payload !== 'number') {\n throw new TypeError('count must be a number');\n }\n },\n onError: ({ err, event }) => logger.warn('rejected', event, err),\n});\n\nbus.emit('count', 'oops'); // → onError called, listeners skipped, returns 0\nbus.emit('count', 42); // → listeners run, returns listener count\n```\n\nWithout `onError`, a `validatePayload` throw propagates directly to the `emit()` caller.\n\n## Awaiting Events\n\n`wait()` returns a `Promise` that resolves with the payload of the next emit. This is useful for one-off async coordination patterns.\n\n```ts\n// Waits for the next 'user:login' emit and resolves\nconst { userId } = await bus.wait('user:login');\n```\n\n`wait()` rejects if:\n\n- The bus is disposed before the event fires\n- A provided `AbortSignal` aborts\n\n```ts\n// Reject if login hasn't happened within 5 seconds\nconst { userId } = await bus.wait('user:login', { signal: AbortSignal.timeout(5_000) });\n```\n\n### `waitAny()`\n\n`waitAny()` resolves with the first event that fires from a list and returns both the winning event key and payload.\n\n```ts\nconst result = await bus.waitAny(['user:login', 'user:logout']);\n\nif (result.event === 'user:login') {\n console.log(result.payload.userId);\n}\n```\n\nLike `wait()`, it rejects when the bus is disposed or when the provided signal aborts:\n\n```ts\nconst result = await bus.waitAny(['user:login', 'user:logout'], { signal: AbortSignal.timeout(10_000) });\n```\n\n## Async Iteration\n\n`events()` returns an `AsyncGenerator` that yields every future emit of an event. It terminates when the bus is disposed or the provided signal aborts.\n\n`events()` subscribes **eagerly** — the listener is registered when `events()` is called, so events emitted before the first `await` are buffered and will be yielded on the next iteration.\n\n```ts\nfor await (const { items, total } of bus.events('cart:updated')) {\n renderCart(items, total);\n}\n```\n\nUse the `options` object to stop iterating early or cap the internal buffer:\n\n```ts\nconst controller = new AbortController();\n\nfor await (const payload of bus.events('data:loaded', { signal: controller.signal, maxBuffer: 100 })) {\n process(payload);\n if (isDone(payload)) controller.abort(); // exits the loop cleanly\n}\n// loop ends here — no exception thrown on abort or dispose\n```\n\nThe generator is `AsyncDisposable` — use `await using` for guaranteed cleanup even on early `break`:\n\n```ts\nawait using stream = bus.events('user:login');\nfor await (const { userId } of stream) {\n if (userId === targetId) break; // stream subscription is torn down automatically\n}\n```\n\n## Error Handling\n\nEvery registered listener for an emission runs regardless of whether an earlier one throws. By default (no `onError` configured), the first thrown error propagates to the `emit()` caller once every listener has been called.\n\nConfigure `onError` to capture errors instead of rethrowing. `emit()` never throws for a listener failure when `onError` is set.\n\n```ts\nconst bus = createBus<AppEvents>({\n onError: ({ err, event, payload, timestamp }) => {\n // Structured context — event, payload, and timestamp at the time emit() was called\n logger.error(`[herald] Error in \"${event}\" listener`, err, { payload, timestamp });\n },\n});\n```\n\n`onError` receives an `EmissionErrorContext<T>` object:\n\n- `err` — the thrown value\n- `event` — the event key that was being emitted\n- `payload` — the payload passed to the failing listener (typed as `unknown`)\n- `timestamp` — `Date.now()` captured at the moment `emit()` was called\n\n## Dispose & Cleanup\n\n`dispose()` permanently tears down the bus: all listeners are removed and all pending `wait()` promises are rejected with `BusDisposedError`.\n\n```ts\nbus.dispose();\nbus.disposed; // true\n\n// Calling dispose() again is safe — idempotent\nbus.dispose(); // no-op\n```\n\nUse `instanceof BusDisposedError` to distinguish bus teardown from other rejections:\n\n```ts\nimport { BusDisposedError } from '@vielzeug/herald';\n\ntry {\n const payload = await bus.wait('user:login');\n} catch (err) {\n if (err instanceof BusDisposedError) {\n // bus was torn down before the event fired\n } else {\n throw err; // signal abort reason or unexpected error\n }\n}\n```\n\n### `disposalSignal`\n\nEvery bus exposes a `disposalSignal: AbortSignal` property. The signal fires when `dispose()` is called, giving you a handle to tie external lifecycles to the bus lifetime without polling `bus.disposed`.\n\n```ts\nconst bus = createBus<AppEvents>();\n\n// Pass disposalSignal to another bus subscription — auto-unsubscribes on teardown\notherBus.on('count', syncState, { signal: bus.disposalSignal });\n\n// Use with any AbortSignal-aware API\nfetch('/api/stream', { signal: bus.disposalSignal });\n\n// Combine with other signals\nconst combined = AbortSignal.any([bus.disposalSignal, AbortSignal.timeout(10_000)]);\nbus.events('data:loaded', { signal: combined });\n```\n\nThe signal is already aborted when `bus.disposed` is `true`.\n\n## Wildcard Listeners\n\n`onAny()` subscribes to **all events** on the bus. The listener receives the event name and payload on every emit, after event-specific listeners have run. Useful for cross-cutting concerns like logging, analytics, or dev-mode tracing.\n\n```ts\nconst unsub = bus.onAny((event, payload) => {\n console.debug(`[bus] ${event}`, payload);\n});\n\nunsub(); // remove the wildcard listener when done\n```\n\nLike `on()`, `onAny()` accepts an optional `SubscribeOptions` object:\n\n```ts\nconst controller = new AbortController();\nbus.onAny(logger, { signal: controller.signal });\ncontroller.abort(); // removes the wildcard listener\n\n// Once-only wildcard\nbus.onAny(logFirstEvent, { once: true });\n```\n\nUse `wildcardCount()` to inspect the current number of active wildcard listeners.\n\n::: tip `onAny` for bus-wide observability\n`onAny` is a runtime listener — it can be added and removed dynamically, and accepts `{ signal, once }` options just like `on()`. Prefer it over global tracing hooks for cross-cutting concerns.\n:::\n\n## Event Piping\n\nUse `pipeEvents()` to forward events from one bus to another. It supports same-name forwarding and event renaming.\n\n```ts\nimport { createBus, pipeEvents } from '@vielzeug/herald';\n\ntype AppEvents = {\n 'user:login': { userId: string; email: string };\n 'user:logout': void;\n 'cart:updated': { items: CartItem[]; total: number };\n};\n\nconst appBus = createBus<AppEvents>();\nconst auditBus = createBus<AppEvents>();\n\n// Forward only auth events to the audit bus\nconst unpipe = pipeEvents(appBus, auditBus, ['user:login', 'user:logout']);\n```\n\n`pipeEvents` returns an `Unsubscribe` function to stop forwarding manually:\n\n```ts\nunpipe(); // stop forwarding\n```\n\nForwarding stops automatically when the **target** bus is disposed — no manual cleanup needed. Source disposal is handled by the source bus's own `on()` lifecycle.\n\nYou can scope piping to a signal:\n\n```ts\nconst controller = new AbortController();\npipeEvents(appBus, auditBus, ['user:login'], { signal: controller.signal });\n\n// Stop forwarding after 30 seconds\nsetTimeout(() => controller.abort(), 30_000);\n```\n\n### Event renaming\n\nPass a `{ from, to }` object to forward an event under a different name on the target bus. This enables cross-domain event translation without manually wiring `on()` + `emit()`.\n\n```ts\ntype AuthEvents = { 'auth:login': { userId: string }; 'auth:logout': void };\ntype AppEvents = { 'user:authenticated': { userId: string }; 'user:signed-out': void };\n\nconst authBus = createBus<AuthEvents>();\nconst appBus = createBus<AppEvents>();\n\npipeEvents(authBus, appBus, [\n { from: 'auth:login', to: 'user:authenticated' },\n { from: 'auth:logout', to: 'user:signed-out' },\n]);\n```\n\nMix string keys and `{ from, to }` objects freely in the same array:\n\n```ts\npipeEvents(sourceBus, targetBus, [\n 'config:updated', // same name\n { from: 'auth:login', to: 'user:authenticated' }, // renamed\n]);\n```\n\n## Behavior Bus\n\n`createBehaviorBus()` creates a bus that remembers and replays the last emitted value to new subscribers. This is useful for state-like events where late subscribers should receive the current value immediately — similar to a BehaviorSubject in RxJS.\n\n```ts\nimport { createBehaviorBus } from '@vielzeug/herald';\n\ntype UIState = { theme: 'light' | 'dark'; zoom: number };\n\n// Provide initial values — these are replayed to first subscribers\nconst bus = createBehaviorBus<UIState>({ theme: 'light', zoom: 1 });\n\nbus.on('theme', applyTheme); // called with 'light' immediately\nbus.on('zoom', setZoom); // called with 1 immediately\n\nbus.emit('theme', 'dark');\n\nbus.on('theme', applyTheme); // called with 'dark' immediately — gets current value\n```\n\n### `current()`\n\nRead the current value for any event without subscribing:\n\n```ts\nbus.current('theme'); // 'dark'\nbus.current('zoom'); // 1\n```\n\n### `snapshot()`\n\nRead all currently buffered values at once as a plain object:\n\n```ts\nbus.snapshot();\n// → { theme: 'dark', zoom: 1 } (only buffered events are included)\n```\n\nThis is useful for serializing state, hydrating a new bus, or debugging all channels simultaneously.\n\n### Replay rules\n\n| Method | Replays current value? |\n| -------------- | ---------------------------------------------------------- |\n| `on()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> Yes |\n| `once()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> Yes (then done) |\n| `on({ once })` | <ore-icon name=\"check\" size=\"16\"></ore-icon> Yes (then done) |\n| `events()` | <ore-icon name=\"x\" size=\"16\"></ore-icon> No |\n| `wait()` | <ore-icon name=\"x\" size=\"16\"></ore-icon> No |\n\n::: warning `once()` on a BehaviorBus fires immediately\nIf the bus has a buffered value for the event, `once()` (and `on(event, fn, { once: true })`) fires the listener **synchronously** with the current value and is immediately done — the listener is never registered for future emits. If you need to react to the _next_ new emit rather than the current state, use `on()` and unsubscribe manually after the first call.\n:::\n\n## Debug Mode\n\nImport `debugBus` from the dedicated sub-path to create a bus with debug logging pre-enabled. The sub-path is tree-shaken from production bundles when not imported.\n\n```ts\nimport { debugBus } from '@vielzeug/herald/devtools';\n\nconst bus = debugBus<AppEvents>();\n\nbus.on('user:login', handler);\n// → [herald:on] on(\"user:login\")\n\nbus.emit('user:login', { email: 'alice@example.com', userId: '42' });\n// → [herald:emit] emit(\"user:login\") — 1 listener(s)\n\nbus.dispose();\n// → [herald:lifecycle] dispose()\n```\n\nAlternatively, wire logging manually by passing `logger.debug` directly to `createBus()`:\n\n```ts\nconst bus = createBus<AppEvents>({ logger: { debug: console.debug } }); // equivalent\n```\n\n`debugBehaviorBus` is the same wrapper for `createBehaviorBus()`:\n\n```ts\nimport { debugBehaviorBus } from '@vielzeug/herald/devtools';\n\nconst bus = debugBehaviorBus<UIState>({ theme: 'light' });\nbus.on('theme', applyTheme); // replays 'light' immediately, logs the subscription\n```\n\nDebug logging has no effect on behavior and should not be enabled in production.\n\n### Custom logger\n\nProvide a `logger` object to route or silence debug and warn output:\n\n```ts\nconst bus = createBus<AppEvents>({\n logger: {\n debug: (msg) => myLogger.trace(msg), // enable + redirect debug output\n warn: (msg) => myLogger.warn(msg), // redirect warn output\n },\n});\n\n// Omit logger.debug to disable debug logging, omit logger.warn to silence warnings\nconst warnOnlyBus = createBus<AppEvents>({ logger: { warn: console.warn } });\n\n// Pass {} to suppress all bus logging entirely\nconst silentBus = createBus<AppEvents>({ logger: {} });\n```\n\n### Naming a bus with `name`\n\nPass `name` to identify a bus in log messages and error output. Useful when multiple buses run concurrently and you need to distinguish their activity:\n\n```ts\nconst authBus = createBus<AuthEvents>({ name: 'auth', logger: { debug: console.debug } });\nconst cartBus = createBus<CartEvents>({ name: 'cart', logger: { debug: console.debug } });\n\nauthBus.emit('user:login', { userId: '1' });\n// → [herald:emit] emit(\"user:login\") — 1 listener(s) (auth)\n\ncartBus.dispose();\n// → [herald:lifecycle] dispose() (cart)\n```\n\nWhen a named bus is disposed, `BusDisposedError` includes the name in its message:\n\n```ts\n// Bus \"auth\" is disposed\n```\n\n`name` has no effect on behavior and does not need to be unique.\n\n### Detecting listener leaks with `maxListeners`\n\nPass `maxListeners` to `createBus()` to receive a `console.warn` whenever a single event's listener count exceeds the threshold. This helps catch accidental listener accumulation during development.\n\n```ts\nconst bus = createBus<AppEvents>({ maxListeners: 10 });\n\n// Registering an 11th listener for 'cart:updated' prints:\n// [herald:warn] \"cart:updated\" has 11 listeners, exceeding maxListeners (10). Possible memory leak.\n```\n\nThe warning fires for both event-specific listeners (`on`, `once`) and wildcard listeners (`onAny`). There is no effect on bus behavior — all listeners are still registered and invoked normally.\n\n### Counting listeners\n\n`listenerCount()` lets you inspect active subscriptions without needing to track them manually:\n\n```ts\nbus.on('user:login', handler1);\nbus.on('user:login', handler2);\nbus.on('user:logout', handler3);\nbus.onAny(wildcardHandler);\n\nbus.listenerCount('user:login'); // 3 — 2 specific + 1 wildcard\nbus.listenerCount(); // 4 — 3 specific + 1 wildcard (wildcards counted once)\n```\n\nThis is useful for debugging, assertions in tests, or conditional emit optimizations.\n\nYou can combine this with `eventNames()` when you need a quick snapshot of which channels are active.\n\n### `using` keyword\n\n`Bus` implements `[Symbol.dispose]`, so it works with the `using` keyword (TypeScript 5.2+, `\"lib\": [\"esnext\"]`):\n\n```ts\n{\n using bus = createBus<AppEvents>();\n bus.on('user:login', handler);\n bus.emit('user:login', { userId: '1', email: 'a@b.com' });\n} // bus.dispose() is called automatically here\n```\n\nThis is especially useful in test cases, request handlers, or any scope where you want guaranteed cleanup.\n\n## Testing\n\nImport `createTestBus` from `@vielzeug/herald/testing`. It wraps `createBus` and records every emitted payload by event key.\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nconst bus = createTestBus<AppEvents>();\n\nbus.emit('user:login', { userId: '1', email: 'a@example.com' });\nbus.emit('user:login', { userId: '2', email: 'b@example.com' });\n\n// emitted() returns a typed snapshot — not a live reference\nexpect(bus.emitted('user:login')).toEqual([\n { userId: '1', email: 'a@example.com' },\n { userId: '2', email: 'b@example.com' },\n]);\n\nbus.reset(); // clear recorded payloads, keep listeners active\nbus.dispose(); // clear listeners and recorded payloads\n```\n\nUse `emittedCount(event)` when you only need the count, not the full payload list:\n\n```ts\nbus.emittedCount('user:login'); // number of times the event was emitted\n```\n\n`createTestBus` accepts the full `BusOptions<T>` including `onError`.\n\nUse `reset()` to clear recorded payloads between assertions without affecting active listeners:\n\n```ts\nbus.emit('user:login', { email: 'a@example.com', userId: '1' });\nbus.reset(); // clears emission records — listeners remain active\n\nbus.emitted('user:login'); // => []\n```\n\nUse `using` for automatic cleanup in test cases:\n\n```ts\nit('records emitted events', () => {\n using bus = createTestBus<AppEvents>();\n bus.emit('user:logout');\n expect(bus.emitted('user:logout')).toHaveLength(1);\n}); // bus disposed automatically\n```\n\n## Framework Integration\n\n::: code-group\n\n```tsx [React]\nimport { useEffect } from 'react';\nimport { createBus } from '@vielzeug/herald';\n\ntype AppEvents = {\n 'user:login': { userId: string; email: string };\n 'user:logout': void;\n};\n\n// Module-level bus shared across components\nconst bus = createBus<AppEvents>();\n\nfunction useEvent<K extends keyof AppEvents>(event: K, handler: (payload: AppEvents[K]) => void) {\n useEffect(() => {\n const controller = new AbortController();\n bus.on(event as any, handler as any, { signal: controller.signal });\n return () => controller.abort();\n }, [event, handler]);\n}\n\nfunction LoginButton() {\n useEvent('user:login', ({ userId }) => console.log('logged in:', userId));\n return <button onClick={() => bus.emit('user:login', { userId: '1', email: 'a@x.com' })}>Login</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onScopeDispose } from 'vue';\nimport { createBus } from '@vielzeug/herald';\n\ntype AppEvents = {\n 'user:login': { userId: string; email: string };\n 'user:logout': void;\n};\n\nconst bus = createBus<AppEvents>();\n\nfunction useEvent<K extends keyof AppEvents>(event: K, handler: (payload: AppEvents[K]) => void) {\n const controller = new AbortController();\n bus.on(event as any, handler as any, { signal: controller.signal });\n onScopeDispose(() => controller.abort());\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createBus } from '@vielzeug/herald';\n\n type AppEvents = {\n 'user:login': { userId: string; email: string };\n 'user:logout': void;\n };\n\n const bus = createBus<AppEvents>();\n\n // Listen to events with automatic cleanup on component destroy\n const controller = new AbortController();\n bus.on('user:login', ({ userId }) => console.log('logged in:', userId), { signal: controller.signal });\n onDestroy(() => controller.abort());\n\n function login() {\n bus.emit('user:login', { userId: '1', email: 'alice@example.com' });\n }\n</script>\n\n<button on:click={login}>Login</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Rune\n\nUse Rune to trace all event dispatches in development.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst logger = createLogger({ scope: 'herald' });\n\nconst bus = createBus<AppEvents>({\n logger: { debug: logger.debug, warn: logger.warn },\n onError: ({ err, event }) => logger.error('handler failed', { err, event }),\n});\n```\n\n### With Ripple\n\nUse Ripple signals to reflect the latest event payload as reactive state.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { signal } from '@vielzeug/ripple';\n\ntype AppEvents = { 'user:login': { userId: string; email: string }; 'user:logout': void };\nconst bus = createBus<AppEvents>();\n\nconst currentUser = signal<{ userId: string; email: string } | null>(null);\n\nbus.on('user:login', (payload) => {\n currentUser.value = payload;\n});\nbus.on('user:logout', () => {\n currentUser.value = null;\n});\n```\n\n## Best Practices\n\n- Create one bus per logical domain (e.g., one bus per micro-frontend module) rather than a single global bus.\n- Pass `AbortSignal` to `on()` and `once()` for lifecycle-bound listeners — avoids manual `unsub()` tracking.\n- Use `wait()` for one-off async coordination; use `events()` for continuous processing pipelines.\n- Configure `onError` on the bus rather than wrapping each listener in try/catch.\n- Call `dispose()` when a bus is no longer needed — it rejects all pending `wait()` promises.\n- Use `pipeEvents()` to forward events between buses rather than re-emitting manually inside listeners.\n- Pass `bus.disposalSignal` to tie external subscriptions and fetch calls to the bus lifetime.\n- Prefer typed `EventMap` interfaces over generic string keys for full payload inference.\n- Use `createTestBus` from `@vielzeug/herald/testing` in unit tests rather than mocking the bus.\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": "behavior-bus",
|
|
32
|
+
"code": "import { createBehaviorBus } from '@vielzeug/herald'\n\n// BehaviorBus replays the last value to new subscribers — like BehaviorSubject\nconst bus = createBehaviorBus({ theme: 'light', zoom: 1 })\n\n// First subscriber receives current values immediately\nbus.on('theme', (t) => console.log('A sees theme:', t))\nbus.on('zoom', (z) => console.log('A sees zoom:', z))\n\nbus.emit('theme', 'dark')\n\n// Late subscriber gets the current value on registration\nbus.on('theme', (t) => console.log('B sees theme:', t))\n\n// Read current value without subscribing\nconsole.log('current theme:', bus.current('theme'))\nconsole.log('current zoom:', bus.current('zoom'))\n\n// snapshot() returns all buffered values at once\nconsole.log('snapshot:', bus.snapshot()) // { theme: 'dark', zoom: 1 }\n\n// reset() clears the buffer for one or all events\nbus.reset('zoom')\nconsole.log('after reset, snapshot:', bus.snapshot()) // { theme: 'dark' }\n\nbus.dispose()",
|
|
33
|
+
"name": "Behavior Bus"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "behavior-snapshot",
|
|
37
|
+
"code": "import { createBehaviorBus } from '@vielzeug/herald'\n\n// snapshot() reads all buffered values at once — useful for serialization\n// and hydrating another bus or UI from the current state\n\nconst bus = createBehaviorBus({ theme: 'light', zoom: 1, sidebar: true })\n\nconsole.log('initial snapshot:', bus.snapshot())\n// { theme: 'light', zoom: 1, sidebar: true }\n\nbus.emit('theme', 'dark')\nbus.emit('zoom', 2)\n\nconsole.log('after emits:', bus.snapshot())\n// { theme: 'dark', zoom: 2, sidebar: true }\n\n// Hydrate a new bus from the snapshot\nconst saved = bus.snapshot()\nconst restored = createBehaviorBus(saved)\n\nconsole.log('restored theme:', restored.current('theme')) // 'dark'\nconsole.log('restored zoom:', restored.current('zoom')) // 2\n\n// reset() only clears entries you choose\nbus.reset('zoom')\nconsole.log('after reset zoom:', bus.snapshot())\n// { theme: 'dark', sidebar: true } — zoom is gone\n\nbus.dispose()\nrestored.dispose()",
|
|
38
|
+
"name": "BehaviorBus snapshot()"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "bus-basics",
|
|
42
|
+
"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()",
|
|
43
|
+
"name": "createBus - Basics"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "disposal-signal",
|
|
47
|
+
"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)",
|
|
48
|
+
"name": "disposalSignal"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "error-handling",
|
|
52
|
+
"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// HeraldError.is() 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?', HeraldError.is(err), '-', err.message)\n}\n\nbus.dispose()",
|
|
53
|
+
"name": "Error Handling"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "event-stream-take",
|
|
57
|
+
"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)",
|
|
58
|
+
"name": "events() with break"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "logger-option",
|
|
62
|
+
"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))",
|
|
63
|
+
"name": "Custom Logger"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "named-bus",
|
|
67
|
+
"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})",
|
|
68
|
+
"name": "Named Bus"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "once-and-wait",
|
|
72
|
+
"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)",
|
|
73
|
+
"name": "once() and wait()"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "pipe-events",
|
|
77
|
+
"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())",
|
|
78
|
+
"name": "pipeEvents()"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "test-bus",
|
|
82
|
+
"code": "import { createTestBus } from '@vielzeug/herald/testing'\n\n// TestBus wraps a normal bus with emission recording — no mocking required\nconst bus = createTestBus()\n\nbus.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\n// removeAllListeners() stops delivery but keeps the historical records\nbus.removeAllListeners('cart:updated')\nbus.emit('cart:updated', { items: 3, total: 59.97 }) // no handler output, still recorded\n\nconsole.log('after removeAllListeners:', bus.emitted('cart:updated'))\n\nbus.dispose()",
|
|
83
|
+
"name": "createTestBus()"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "wait-any",
|
|
87
|
+
"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)",
|
|
88
|
+
"name": "waitAny()"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "wildcard-listeners",
|
|
92
|
+
"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()",
|
|
93
|
+
"name": "onAny + wildcardCount()"
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"typeSignatures": {
|
|
97
|
+
"createBehaviorBus": "export { createBehaviorBus } from './behavior-bus';",
|
|
98
|
+
"combineSignals": "export { combineSignals, createBus } from './bus';",
|
|
99
|
+
"createBus": "export { combineSignals, createBus } from './bus';",
|
|
100
|
+
"BusDisposedError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
101
|
+
"HeraldConfigError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
102
|
+
"HeraldError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
103
|
+
"pipeEvents": "export { pipeEvents } from './pipe';",
|
|
104
|
+
"BehaviorBus": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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
|
+
"BehaviorBusOptions": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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
|
+
"BehaviorInitial": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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
|
+
"Bus": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
108
|
+
"BusLogger": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
109
|
+
"BusOptions": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
110
|
+
"EmissionErrorContext": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
111
|
+
"EventKey": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
112
|
+
"EventMap": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
113
|
+
"EventStream": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
114
|
+
"Listener": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
115
|
+
"Middleware": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
116
|
+
"PipeableKey": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
117
|
+
"PipeEntry": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
118
|
+
"SubscribeOptions": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
119
|
+
"Unsubscribe": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';",
|
|
120
|
+
"WaitAnyResult": "export type {\n BehaviorBus,\n BehaviorBusOptions,\n BehaviorInitial,\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';"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { findShortcutConflicts } from './conflicts';\nexport { KeymapError, KeymapParseError } from './errors';\nexport { formatShortcut } from './format';\nexport { createKeymap } from './keymap';\nexport { createKeymapLayer } from './layer';\nexport { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';\nexport type { ConflictOptions } from './conflicts';\nexport type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';\nexport type { KeymapLayer } from './layer';\nexport type { ModifierKey, Shortcut, ShortcutStep } from './parser';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Keymap — Headless keyboard shortcut manager\ndescription: Chord-aware keyboard shortcut manager with context guards, modifier aliases, and disposable bindings — no DOM assumptions.\npackage: keymap\ncategory: app-infrastructure\nkeywords: [keyboard, shortcuts, hotkeys, chord, keybinding, headless, accessibility]\nexports:\n [\n canonicalizeShortcut,\n createKeymap,\n createKeymapLayer,\n detectModKey,\n findShortcutConflicts,\n formatShortcut,\n KeymapError,\n KeymapParseError,\n matchStep,\n parseShortcut,\n parseStep,\n ]\nrelated: [herald, refine, ore]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"keymap\" />\n\n## Why Keymap?\n\nBrowser keyboard handling is error-prone: modifier key normalisation, platform differences (`ctrl` vs `meta`), chord sequences, and cleanup all require boilerplate. Keymap handles all of it in a headless, zero-dependency package.\n\n```ts\n// Before\nwindow.addEventListener('keydown', (event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === 's') event.preventDefault();\n});\n\n// After\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst save = () => console.log('save');\nconst map = createKeymap({ 'mod+s': save });\nconst unmount = map.mount(document);\n```\n\n| Feature | Raw `addEventListener` | Keymap |\n| ------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | 0 B (built-in) | <PackageInfo package=\"keymap\" type=\"size\" /> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Chord sequences | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Modifier aliases | <ore-icon name=\"x\" size=\"16\"></ore-icon> | `cmd`, `win`, `option` → canonical |\n| Context guards | Manual `if` in handler | `when()` predicate per keymap |\n| Headless / SSR-safe | DOM required | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Disposable | Manual `removeEventListener` | `dispose()` + `using` |\n\n<div class=\"decision-callout\">\n\n**Use Keymap when** you need chord sequences (`g g`, `ctrl+k ctrl+s`), modifier aliases, or context-scoped hotkeys that can be cleanly mounted and unmounted.\n\n**Consider raw `addEventListener` when** you have a single, static, never-removed hotkey and don't need chords.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/keymap\n```\n\n```sh [npm]\nnpm install @vielzeug/keymap\n```\n\n```sh [yarn]\nyarn add @vielzeug/keymap\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst save = () => console.log('save');\nconst openPalette = () => console.log('palette');\nconst goToTop = () => window.scrollTo({ top: 0 });\nconst closePanel = () => console.log('close');\n\nconst map = createKeymap({\n 'ctrl+k ctrl+s': () => save(),\n 'meta+shift+p': () => openPalette(),\n 'g g': () => goToTop(),\n escape: () => closePanel(),\n});\n\nconst unmount = map.mount(document);\n\n// Later:\nunmount(); // remove from this target only\nmap.dispose(); // or: using map = createKeymap(…)\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createKeymap()` — Create a keymap from a bindings record; mount to any `EventTarget`\n- Chord sequences — `\"g g\"`, `\"ctrl+k ctrl+s\"` with configurable timeout (default 1 s)\n- Modifier aliases — `cmd`/`command`/`win` → `meta`; `opt`/`option` → `alt`; `mod` → platform-aware\n- `BindingOptions` — per-binding `{ handler, when?, trigger?, priority? }` object syntax\n- `modKey` option — explicit platform override for SSR and cross-platform tests\n- `formatShortcut()` — platform-aware display (`⇧⌘P` on Mac, `Ctrl+Shift+P` elsewhere)\n- `createKeymapLayer()` — scoped keymap stack with `activate()` / `deactivate()`\n- `parseShortcut()` / `parseStep()` / `matchStep()` — exposed for building custom matchers or testing\n- `canonicalizeShortcut()` — convert any shortcut alias to a stable key for conflict detection\n- `detectModKey()` — platform modifier detection (`'meta'` on Mac, `'ctrl'` elsewhere)\n- `listBindings()` — snapshot all active bindings (shortcut, trigger, priority) for palette UIs\n- `findShortcutConflicts()` — detect prefix/duplicate conflicts before binding a user-customized shortcut\n- Disposable — `dispose()` + `[Symbol.dispose]` for `using` declarations\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Herald](/herald/) — Typed event bus; pair with Keymap by publishing shortcut events to a bus instead of calling handlers directly\n- [Refine](/refine/) — `ore-command-palette` uses Keymap internally; register your own shortcuts alongside it\n- [Ore](/ore/) — Attach a keymap inside a `define()` setup function for component-scoped shortcuts\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Keymap — API Reference\ndescription: Full API reference for @vielzeug/keymap — createKeymap, createKeymapLayer, formatShortcut, and all types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Export | Kind | Description |\n| ------ | ---- | ----------- |\n| `createKeymap` | function | Creates a headless keyboard shortcut manager |\n| `createKeymapLayer` | function | Creates a scoped keymap layer that stacks on a parent |\n| `formatShortcut` | function | Formats a shortcut string for display (Mac symbols or word labels) |\n| `findShortcutConflicts` | function | Finds registered bindings that would conflict with a proposed shortcut |\n| `KeymapError` | class | Base class for all keymap errors |\n| `KeymapParseError` | class | Thrown when a shortcut string cannot be parsed |\n| `Keymap` | interface | Object returned by `createKeymap` |\n| `KeymapLayer` | interface | Extends `Keymap` with `activate()`, `deactivate()`, `active` |\n| `KeymapOptions` | interface | Options for `createKeymap` and `createKeymapLayer` |\n| `BindingOptions` | type | Per-binding object: `{ handler, when?, trigger?, priority? }` |\n| `BindingValue` | type | `Handler \\| BindingOptions` — accepted wherever a handler is bound |\n| `Handler` | type | `(event: KeyboardEvent) => void` |\n| `parseShortcut` | function | Parses a shortcut string into `ShortcutStep[]` |\n| `parseStep` | function | Parses a single chord step into `ShortcutStep \\| null` |\n| `matchStep` | function | Tests whether a `KeyboardEvent` matches a `ShortcutStep` |\n| `canonicalizeShortcut` | function | Converts `ShortcutStep[]` into a stable canonical string |\n| `detectModKey` | function | Detects the platform modifier key (`'ctrl'` or `'meta'`) |\n| `ShortcutStep` | type | `{ key: string; modifiers: Set<ModifierKey> }` — one parsed step |\n| `Shortcut` | type | `ShortcutStep[]` — the result of `parseShortcut` |\n| `ModifierKey` | type | `'alt' \\| 'ctrl' \\| 'meta' \\| 'shift'` |\n| `BindingEntry` | type | Snapshot of a registered binding: `{ shortcut, trigger, priority }` |\n| `ConflictOptions` | type | Options for `findShortcutConflicts`: `{ modKey?, trigger? }` |\n\n## Package Entry Points\n\n```ts\nimport {\n canonicalizeShortcut, createKeymap, createKeymapLayer, detectModKey,\n findShortcutConflicts, formatShortcut, KeymapError, KeymapParseError,\n matchStep, parseShortcut, parseStep,\n} from '@vielzeug/keymap';\nimport type {\n BindingEntry, BindingOptions, BindingValue, ConflictOptions, Handler,\n Keymap, KeymapLayer, KeymapOptions, ModifierKey, Shortcut, ShortcutStep,\n} from '@vielzeug/keymap';\n```\n\n## `createKeymap(bindings?, options?)`\n\nCreates a headless keyboard shortcut manager.\n\n```ts\nfunction createKeymap(\n bindings?: Record<string, BindingValue>,\n options?: KeymapOptions,\n): Keymap\n```\n\n**Parameters**\n\n- `bindings` — Optional record mapping shortcut strings to `BindingValue`. Shortcut strings support chord sequences (space-separated steps), modifier aliases (`mod`, `cmd`, `ctrl`, `alt`, `shift`), special key aliases (`esc`, `space`, `up`, etc.), and are case-insensitive.\n- `options` — Optional configuration (see `KeymapOptions`).\n\n**Returns** a `Keymap` object.\n\n**Example**\n\n```ts\nconst map = createKeymap({\n 'mod+k mod+s': () => save(),\n 'mod+shift+p': () => openPalette(),\n 'g g': () => goToTop(),\n esc: { handler: closePanel, when: () => isPanelOpen() },\n space: { handler: togglePlay, trigger: 'keyup' },\n}, { modKey: 'ctrl' });\n\nconst unmount = map.mount(document);\n```\n\n## `Keymap`\n\n```ts\ninterface Keymap {\n bind(shortcut: string, value: BindingValue): () => void;\n dispose(): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n listBindings(): readonly BindingEntry[];\n mount(target: EventTarget): () => void;\n unbind(shortcut: string): void;\n [Symbol.dispose](): void;\n}\n```\n\n### `mount(target)`\n\nAttaches `keydown` and `keyup` listeners to `target`. Returns an unmount function that removes only the listeners added by this call.\n\n```ts\nconst unmount = map.mount(document);\nunmount(); // detach\n```\n\nOne keymap can be mounted to multiple targets simultaneously. Each call returns an independent unmount function. Mounting the **same** target a second time without unmounting first still attaches a second listener (handlers fire twice) — this emits a dev warning rather than throwing, since remounting the same target is occasionally intentional.\n\n### `bind(shortcut, value)`\n\nAdds or replaces a binding at runtime. Returns an unbind function.\n\n```ts\nconst unbind = map.bind('ctrl+shift+f', () => openSearch());\nunbind(); // remove just this binding\n```\n\nThrows if `shortcut` is invalid (modifier-only, empty, or ambiguous).\n\n### `unbind(shortcut)`\n\nRemoves the binding for the given shortcut string. Emits a dev warning if the shortcut is not registered; never throws.\n\n```ts\nmap.unbind('ctrl+k');\n```\n\n### `dispose()`\n\nRemoves all mounted listeners and resets chord state. Idempotent.\n\n```ts\nmap.dispose();\n// or:\nusing map = createKeymap({ ... });\n```\n\n### `listBindings()`\n\nReturns a snapshot of all currently registered bindings. Does not include `handler` or `when` — only the shortcut shape, trigger, and priority.\n\n```ts\nconst entries = map.listBindings();\n// [\n// { shortcut: [{ key: 'k', modifiers: Set { 'ctrl' } }], trigger: 'keydown', priority: 0 },\n// ]\n```\n\nUseful for building shortcut palette UIs, conflict detection, and accessibility overlays.\n\n## `KeymapOptions`\n\n```ts\ninterface KeymapOptions {\n chordTimeout?: number; // default: 1000ms\n modKey?: 'ctrl' | 'meta'; // default: platform-detected\n preventDefault?: boolean; // default: true\n stopPropagation?: boolean; // default: false\n when?: () => boolean;\n}\n```\n\n| Option | Default | Description |\n| ------ | ------- | ----------- |\n| `chordTimeout` | `1000` | Milliseconds before a partial chord sequence resets. A non-finite or non-positive value falls back to `1000` with a dev warning. |\n| `modKey` | platform | Override `mod` alias resolution: `'meta'` (Mac ⌘) or `'ctrl'` (Windows/Linux). Auto-detected from `navigator` when omitted. |\n| `preventDefault` | `true` | Call `event.preventDefault()` on a matched binding |\n| `stopPropagation` | `false` | Call `event.stopPropagation()` on a matched binding |\n| `when` | — | Global guard predicate; all bindings are suppressed when `when()` returns `false` |\n\n## `createKeymapLayer(parent, bindings?, options?)`\n\nCreates a scoped keymap layer that stacks on top of a parent keymap. The caller is responsible for mounting both the parent and the layer independently — each manages its own event listeners.\n\n```ts\nfunction createKeymapLayer(\n parent: Keymap,\n bindings?: Record<string, BindingValue>,\n options?: KeymapOptions,\n): KeymapLayer\n```\n\n**Example**\n\n```ts\nconst base = createKeymap({ 'ctrl+z': undo });\nconst modal = createKeymapLayer(base, {\n esc: { handler: closeModal, when: () => isModalOpen() },\n});\n\n// Mount parent and layer independently — each manages its own listeners.\nconst unmountBase = base.mount(document);\nconst unmountModal = modal.mount(document);\n\nmodal.deactivate(); // base handles everything; layer is suspended\nmodal.activate(); // layer resumes\n\nunmountModal();\nunmountBase();\n```\n\nDisposing the layer does **not** dispose the parent — the caller owns the parent lifecycle.\n\n## `KeymapLayer`\n\n```ts\ninterface KeymapLayer extends Keymap {\n activate(): void;\n deactivate(): void;\n readonly active: boolean;\n readonly parent: Keymap;\n}\n```\n\n| Member | Description |\n| ------ | ----------- |\n| `activate()` | Re-enables the layer (default: active) |\n| `deactivate()` | Suspends the layer; the parent keymap continues to fire normally |\n| `active` | `true` when the layer is currently active |\n| `parent` | Returns the parent `Keymap` passed to `createKeymapLayer` |\n| `listBindings()` | Returns the layer's own bindings (not the parent's) |\n\n## `formatShortcut(shortcut, modKey?)`\n\nFormats a shortcut string into a human-readable display string. Resolves `mod` using `modKey`.\n\n```ts\nfunction formatShortcut(\n shortcut: string,\n modKey?: 'ctrl' | 'meta',\n): string\n```\n\nOn Mac (`modKey: 'meta'`), uses standard Mac symbols. On other platforms, uses word labels.\n\n```ts\nformatShortcut('mod+shift+p', 'meta') // '⇧⌘P'\nformatShortcut('mod+shift+p', 'ctrl') // 'Ctrl+Shift+P'\nformatShortcut('ctrl+k ctrl+s', 'meta') // '⌃K ⌃S'\nformatShortcut('escape', 'meta') // 'Esc'\n```\n\n## `findShortcutConflicts(shortcut, entries, options?)`\n\nFinds registered bindings that would conflict with a proposed shortcut — an exact duplicate, a\nshorter binding that would be shadowed as a chord prefix, or a longer binding the proposed\nshortcut would itself shadow. Only compares against entries sharing the same `trigger`.\n\n```ts\nfunction findShortcutConflicts(\n shortcut: string,\n entries: readonly BindingEntry[],\n options?: ConflictOptions,\n): BindingEntry[]\n```\n\n**Parameters**\n\n- `shortcut` — The shortcut string being considered for a new binding.\n- `entries` — Existing bindings to check against — typically `map.listBindings()`.\n- `options` — See `ConflictOptions`. `trigger` defaults to `'keydown'`.\n\n**Returns** the subset of `entries` that conflict; `[]` if there's no relationship (or `shortcut` is\nempty/whitespace-only).\n\n**Example**\n\n```ts\nconst map = createKeymap({ g: () => scrollToTop() });\n\nfindShortcutConflicts('g g', map.listBindings());\n// → [{ shortcut: [{ key: 'g', modifiers: Set {} }], trigger: 'keydown', priority: 0 }]\n// binding 'g g' would be shadowed: 'g' fires immediately before the second step is ever read\n```\n\nUseful when building a shortcut-customization UI — check `findShortcutConflicts()` before calling\n`bind()` to warn the user instead of silently creating an unreachable binding.\n\n### `ConflictOptions`\n\n```ts\ninterface ConflictOptions {\n modKey?: 'ctrl' | 'meta';\n trigger?: 'keydown' | 'keyup';\n}\n```\n\n| Field | Default | Description |\n| ----- | ------- | ----------- |\n| `modKey` | platform | Resolves `mod` in the proposed `shortcut` string |\n| `trigger` | `'keydown'` | Which entries to compare against — `'keydown'` and `'keyup'` never conflict with each other |\n\n## Errors\n\n### `KeymapError`\n\nBase class for all keymap errors. Use `instanceof KeymapError` (or `KeymapError.is()`) to catch\nany keymap-originated error.\n\n```ts\nclass KeymapError extends Error {\n static is(err: unknown): err is KeymapError;\n}\n```\n\n### `KeymapParseError`\n\nThrown when a shortcut string cannot be parsed — an ambiguous multi-key step (e.g. `'ctrl+k+j'`)\nor an invalid step (modifier-only, with no key). Extends `KeymapError`.\n\n```ts\nclass KeymapParseError extends KeymapError {}\n```\n\n```ts\nimport { KeymapError, KeymapParseError } from '@vielzeug/keymap';\n\ntry {\n map.bind('ctrl+k+j', handler);\n} catch (err) {\n if (KeymapError.is(err)) {\n console.error(err.message); // 'Ambiguous shortcut step: \"ctrl+k+j\" — multiple non-modifier keys found'\n }\n}\n```\n\n## Types\n\n### `BindingOptions`\n\nPer-binding configuration object.\n\n```ts\ntype BindingOptions = {\n handler: Handler;\n priority?: number; // default: 0\n trigger?: 'keydown' | 'keyup'; // default: 'keydown'\n when?: () => boolean;\n};\n```\n\n| Field | Default | Description |\n| ----- | ------- | ----------- |\n| `handler` | — | The function to call when the shortcut fires |\n| `priority` | `0` | Reserved for future conflict resolution — see note below. A non-finite value falls back to `0` with a dev warning. |\n| `trigger` | `'keydown'` | Which keyboard event phase fires the handler |\n| `when` | — | Per-binding guard; handler suppressed when `when()` returns `false` at event time |\n\n> **Note on `priority`:** because bindings are keyed by their canonical shortcut string, two *live* bindings can never share an identical step sequence — the moment they would, the second `bind()` call simply replaces the first (see `bind(shortcut, value)` above). There is currently no scenario where two distinct bindings compete to fire the same event, so `priority` has no observable effect on which handler runs. It's kept as a documented, validated field for forward compatibility rather than removed outright.\n\n### `BindingValue`\n\n```ts\ntype BindingValue = Handler | BindingOptions;\n```\n\nA plain function is treated as `{ handler: fn, priority: 0, trigger: 'keydown' }`. Use `BindingOptions` for any per-binding customisation.\n\n```ts\nconst map = createKeymap({\n 'ctrl+k': () => quickAction(),\n esc: { handler: closePanel, when: () => isPanelOpen() },\n space: { handler: togglePlay, trigger: 'keyup' },\n});\n```\n\n### `Handler`\n\n```ts\ntype Handler = (event: KeyboardEvent) => void;\n```\n\n### `ShortcutStep`\n\nOne parsed step within a shortcut sequence.\n\n```ts\ntype ShortcutStep = {\n key: string; // lowercase, alias-resolved key name\n modifiers: Set<ModifierKey>; // required modifier keys\n};\n```\n\n### `Shortcut`\n\nAn alias for `ShortcutStep[]` — the direct return type of `parseShortcut`.\n\n```ts\ntype Shortcut = ShortcutStep[];\n```\n\n### `ModifierKey`\n\n```ts\ntype ModifierKey = 'alt' | 'ctrl' | 'meta' | 'shift';\n```\n\n### `BindingEntry`\n\nA read-only snapshot of a registered binding, returned by `listBindings()`. The `handler` and `when` guard are intentionally omitted.\n\n```ts\ntype BindingEntry = {\n readonly priority: number;\n readonly shortcut: readonly ShortcutStep[];\n readonly trigger: 'keydown' | 'keyup';\n};\n```\n\n| Field | Description |\n| ----- | ----------- |\n| `priority` | The binding's priority value |\n| `shortcut` | The parsed shortcut steps |\n| `trigger` | Which event phase fires the handler |\n\n---\n\n## Parser Utilities\n\n### `parseShortcut(raw, modKey?)`\n\nParses a shortcut string into an array of `ShortcutStep` objects. Useful for building custom matchers, testing, or integrating with other libraries.\n\n```ts\nfunction parseShortcut(\n raw: string,\n modKey?: 'ctrl' | 'meta', // default: auto-detected\n): Shortcut\n```\n\nThrows if any non-empty step is invalid (modifier-only with no key, or ambiguous multi-key step like `ctrl+k+j`). Extra whitespace between steps is silently ignored.\n\n```ts\nparseShortcut('ctrl+k ctrl+s', 'ctrl')\n// [\n// { key: 'k', modifiers: Set { 'ctrl' } },\n// { key: 's', modifiers: Set { 'ctrl' } },\n// ]\n```\n\n### `parseStep(raw, modKey?)`\n\nParses a **single** chord step (one keypress) into a `ShortcutStep`, or returns `null` if the step is empty or invalid. Does not throw.\n\n```ts\nfunction parseStep(\n raw: string,\n modKey?: 'ctrl' | 'meta',\n): ShortcutStep | null\n```\n\nUnlike `parseShortcut`, `parseStep` returns `null` instead of throwing on invalid input — useful for \"try\" patterns when parsing user-typed shortcut strings one step at a time.\n\n```ts\nparseStep('ctrl+k', 'ctrl') // { key: 'k', modifiers: Set { 'ctrl' } }\nparseStep('', 'ctrl') // null\n```\n\n### `matchStep(event, step)`\n\nTests whether a `KeyboardEvent` matches a `ShortcutStep`. Zero allocations — pure boolean comparisons.\n\n```ts\nfunction matchStep(event: KeyboardEvent, step: ShortcutStep): boolean\n```\n\nReturns `false` (never throws) for a malformed event missing a string `.key` — safe to call with hand-built event objects in headless/non-DOM usage.\n\n### `canonicalizeShortcut(steps)`\n\nConverts a `ShortcutStep[]` (i.e. the result of `parseShortcut`) into a stable canonical string. Modifiers are sorted alphabetically, steps are space-separated. Useful for conflict detection: two shortcuts resolve to the same canonical string if and only if they match the same key events.\n\n```ts\nfunction canonicalizeShortcut(steps: readonly ShortcutStep[]): string\n```\n\n```ts\ncanonicalizeShortcut(parseShortcut('cmd+k', 'ctrl')) // 'meta+k'\ncanonicalizeShortcut(parseShortcut('meta+k', 'ctrl')) // 'meta+k'\ncanonicalizeShortcut(parseShortcut('ctrl+k ctrl+s', 'ctrl')) // 'ctrl+k ctrl+s'\n```\n\n### `detectModKey()`\n\nDetects the platform modifier key. Returns `'meta'` on macOS, `'ctrl'` elsewhere.\n\n```ts\nfunction detectModKey(): 'ctrl' | 'meta'\n```\n\nUseful when you need a consistent `modKey` across multiple calls to `createKeymap`, `formatShortcut`, and `parseShortcut` without threading it manually.\n\n```ts\nconst modKey = detectModKey();\nconst map = createKeymap(bindings, { modKey });\nconst label = formatShortcut('mod+k', modKey);\n```\n\n---\n\n## Shortcut String Syntax\n\nShortcut strings are space-separated steps. Each step is `+`-joined modifier names and a single non-modifier key.\n\n### Modifier aliases\n\n| You write | Resolves to |\n| --------- | ----------- |\n| `mod` | `meta` on Mac, `ctrl` elsewhere (per `modKey`) |\n| `cmd`, `command`, `win` | `meta` |\n| `opt`, `option` | `alt` |\n| `control` | `ctrl` |\n\n### Special key aliases\n\n| You write | `KeyboardEvent.key` |\n| --------- | ------------------- |\n| `esc` | `Escape` |\n| `space`, `spacebar` | ` ` (space character) |\n| `del` | `Delete` |\n| `up` | `ArrowUp` |\n| `down` | `ArrowDown` |\n| `left` | `ArrowLeft` |\n| `right` | `ArrowRight` |\n\n### Examples\n\n```\n'ctrl+k' → single step, Ctrl modifier\n'ctrl+k ctrl+s' → two-step chord (VS Code–style)\n'g g' → two-step key-key chord (Vim-style)\n'mod+shift+p' → ⌘⇧P on Mac, Ctrl+Shift+P elsewhere\n'escape' → Escape key, no modifiers\n'space' → Space key (alias for ' ')\n```\n",
|
|
6
|
+
"usage": "---\ntitle: Keymap — Usage Guide\ndescription: How to use createKeymap for single shortcuts, chord sequences, context guards, and framework integration.\n---\n\n[[toc]]\n\n## Basic Usage\n\nPass a record of shortcut strings to handlers:\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({\n 'ctrl+s': () => save(),\n 'ctrl+z': () => undo(),\n 'ctrl+shift+z': () => redo(),\n escape: () => closeModal(),\n});\n\nconst unmount = map.mount(document);\n```\n\nThe returned `unmount` function detaches listeners from that target only. Call `map.dispose()` to remove from all mounted targets at once.\n\n## Modifier Aliases\n\nYou can write shortcuts in the style that feels natural — Keymap normalises everything:\n\n| You write | Canonical form |\n| ----------------------- | -------------- |\n| `cmd`, `command`, `win` | `meta` |\n| `opt`, `option` | `alt` |\n| `ctrl`, `control` | `ctrl` |\n| `shift` | `shift` |\n\n```ts\n// All three are equivalent:\ncreateKeymap({ 'cmd+k': handler });\ncreateKeymap({ 'command+k': handler });\ncreateKeymap({ 'meta+k': handler });\n```\n\n## Chord Sequences\n\nSeparate chord steps with a space. The default timeout between steps is 1 s:\n\n```ts\nconst map = createKeymap(\n {\n 'ctrl+k ctrl+s': () => save(), // VS Code–style\n 'g g': () => goToTop(), // Vim-style\n 'g G': () => goToBottom(),\n },\n {\n chordTimeout: 750, // ms — reset partial chord if exceeded\n },\n);\n```\n\n> **Tip:** A shorter binding always fires immediately when typed, even if a longer chord shares its prefix — binding order doesn't matter. `'g'` and `'g g'` together means `'g g'` can never be reached, because `'g'` fires the instant it's pressed. Use `findShortcutConflicts()` (see below) to detect this before it surprises a user.\n\n## `BindingOptions` — Per-binding Configuration\n\nPass a `BindingOptions` object instead of a plain handler to add guards, trigger control, or priority:\n\n```ts\nconst map = createKeymap({\n 'ctrl+s': () => save(), // plain handler\n escape: { handler: closePanel, when: () => isOpen() }, // per-binding guard\n space: { handler: togglePlay, trigger: 'keyup' }, // fires on keyup\n 'ctrl+z': { handler: undo, priority: 10 }, // wins over lower-priority bindings\n});\n```\n\n## Context Guards\n\nUse a global `when()` in `KeymapOptions` to disable an entire keymap conditionally:\n\n```ts\nconst map = createKeymap({ escape: () => closePanel() }, { when: () => panelIsOpen() });\n```\n\nFor per-binding guards, use `BindingOptions.when`:\n\n```ts\nconst map = createKeymap({\n escape: { handler: closePanel, when: () => isPanelOpen() },\n backspace: { handler: deleteLine, when: () => isEditorFocused() },\n});\n```\n\n## Trigger Control\n\nBindings default to `keydown`. Use `trigger: 'keyup'` for actions that should fire on release:\n\n```ts\nconst map = createKeymap({\n space: { handler: confirmAction, trigger: 'keyup' },\n});\n```\n\nKeydown and keyup chord trackers are independent — a `'g g'` chord on `keyup` does not interfere with a `'g g'` chord on `keydown`.\n\n## Replacing a Binding at Runtime\n\n`bind()` always replaces any existing binding for the same shortcut — the most recent call wins, regardless of `priority`:\n\n```ts\nconst map = createKeymap({\n 'ctrl+k': defaultAction,\n});\n\n// A plugin registers an override at runtime — this replaces the default binding outright:\nmap.bind('ctrl+k', pluginOverride);\n```\n\n> `BindingOptions.priority` doesn't affect this — because bindings are keyed by their canonical shortcut, two _live_ bindings can never actually compete for the same event, so there's no tie for `priority` to break. It's a validated, reserved field kept for forward compatibility — see the note in [`api.md`](./api.md#bindingoptions).\n\n## Display with `formatShortcut`\n\nFormat shortcut strings for display in tooltips, menus, or documentation:\n\n```ts\nimport { formatShortcut } from '@vielzeug/keymap';\n\nformatShortcut('mod+shift+p', 'meta'); // '⇧⌘P'\nformatShortcut('mod+shift+p', 'ctrl'); // 'Ctrl+Shift+P'\nformatShortcut('ctrl+k ctrl+s'); // platform-detected\n```\n\nReturns `''` and emits a dev warning for empty or invalid shortcuts.\n\n## Detecting Conflicts\n\nBefore binding a user-customized shortcut, check whether it would shadow (or be shadowed by) an\nexisting binding — most useful for a shortcut-customization UI where the shortcut string comes\nfrom user input:\n\n```ts\nimport { createKeymap, findShortcutConflicts } from '@vielzeug/keymap';\n\nconst map = createKeymap({ g: () => scrollToTop() });\n\nconst conflicts = findShortcutConflicts('g g', map.listBindings());\n\nif (conflicts.length > 0) {\n warnUser('This shortcut would never fire — \"g\" already handles the first key.');\n} else {\n map.bind('g g', () => scrollToBottom());\n}\n```\n\n`findShortcutConflicts()` catches both directions: a shorter existing binding shadowing your\nproposal, and your proposal shadowing an existing longer chord.\n\n## Keymap Layers\n\nStack a scoped keymap on top of a base keymap for modal UIs. Mount each independently:\n\n```ts\nimport { createKeymap, createKeymapLayer } from '@vielzeug/keymap';\n\nconst base = createKeymap({ 'ctrl+z': undo, 'ctrl+s': save });\nconst modal = createKeymapLayer(base, {\n escape: { handler: closeModal, when: () => isModalOpen() },\n 'ctrl+enter': () => confirm(),\n});\n\nconst unmountBase = base.mount(document);\nconst unmountModal = modal.mount(document);\n\nmodal.deactivate(); // base handles everything; layer is suspended\nmodal.activate(); // layer resumes\n\nmodal.parent === base; // true\n\nunmountModal();\nunmountBase();\n```\n\n## Mounting to a Specific Element\n\nPass any `EventTarget` — not just `document`:\n\n```ts\nconst editorEl = document.getElementById('editor')!;\nconst unmount = map.mount(editorEl); // only fires when focus is inside editor\n```\n\nOne keymap can be mounted to multiple targets simultaneously:\n\n```ts\nconst u1 = map.mount(editorA);\nconst u2 = map.mount(editorB);\n// u1() removes from editorA only\n// map.dispose() removes from both\n```\n\nMounting the _same_ target twice without unmounting first (e.g. a forgotten cleanup in an effect) still works — handlers just fire twice — and emits a dev warning to flag the likely mistake.\n\n## `preventDefault` and `stopPropagation`\n\n```ts\nconst map = createKeymap(\n { 'ctrl+s': () => save() },\n {\n preventDefault: true, // default: true — prevents browser save dialog\n stopPropagation: false, // default: false\n },\n);\n```\n\n## Framework Integration\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createKeymap } from '@vielzeug/keymap';\n\nfunction App() {\n useEffect(() => {\n const map = createKeymap({\n 'ctrl+k': () => setOpen(true),\n escape: () => setOpen(false),\n });\n const unmount = map.mount(document);\n return () => unmount();\n }, []);\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted } from 'vue';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({\n 'ctrl+k': () => openPalette(),\n escape: () => closePalette(),\n});\n\nlet unmount: (() => void) | undefined;\nonMounted(() => {\n unmount = map.mount(document);\n});\nonUnmounted(() => unmount?.());\n</script>\n```\n\n```ts [Svelte]\nimport { onMount } from 'svelte';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({\n 'ctrl+k': () => openPalette(),\n escape: () => closePalette(),\n});\n\nonMount(() => {\n const unmount = map.mount(document);\n return () => unmount();\n});\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Keymap + Ledger\n\nWire undo/redo shortcuts to a `Ledger` instance:\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\nimport { createLedger } from '@vielzeug/ledger';\n\nconst ledger = createLedger();\nconst map = createKeymap({\n 'ctrl+z': () => ledger.undo(),\n 'ctrl+shift+z': () => ledger.redo(),\n 'ctrl+y': () => ledger.redo(), // Windows alias\n});\nmap.mount(document);\n```\n\n### Keymap + Herald\n\nPublish shortcut events to a bus instead of calling handlers directly:\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<{ 'shortcut:save': void; 'shortcut:palette': void }>();\nconst map = createKeymap({\n 'ctrl+s': () => bus.emit('shortcut:save'),\n 'meta+shift+p': () => bus.emit('shortcut:palette'),\n});\nmap.mount(document);\n```\n\n## Best Practices\n\n- **One keymap per scope**: create separate keymaps for global shortcuts, panel shortcuts, and editor shortcuts — mount/unmount them as the relevant UI state changes.\n- **Dispose on teardown**: always call `unmount()` or `map.dispose()` when the component unmounts or the scope is destroyed.\n- **Avoid modifier-only shortcuts**: shortcuts like `shift` alone (no key) can't be reliably parsed — always include a non-modifier key.\n- **Use `when()` for toggleable scopes**: simpler than manually mounting and unmounting on every state change.\n",
|
|
7
|
+
"examples": "---\ntitle: Keymap — Examples\ndescription: Worked examples for @vielzeug/keymap.\n---\n\n## Examples\n\n- [Global Shortcuts](./examples/global-shortcuts.md) — Register document-level hotkeys with a context guard\n- [Vim-style Navigation](./examples/vim-navigation.md) — Chord sequences for keyboard-driven navigation\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "basic-shortcuts",
|
|
12
|
+
"code": "import { createKeymap, formatShortcut } from '@vielzeug/keymap'\n\n// Create a keymap — bindings fire on keydown by default.\nconst map = createKeymap({\n 'ctrl+s': () => console.log('save triggered'),\n escape: { handler: () => console.log('close panel'), when: () => true },\n space: { handler: () => console.log('toggle play'), trigger: 'keyup' },\n}, { modKey: 'ctrl' })\n\n// Mount to document (required for event listening).\nconst unmount = map.mount(document)\n\n// Simulate events for demonstration.\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true }))\n\n// Format shortcuts for display in UI tooltips or menus.\nconsole.log(formatShortcut('ctrl+s', 'ctrl')) // 'Ctrl+S'\nconsole.log(formatShortcut('mod+shift+p', 'meta')) // '⇧⌘P'\nconsole.log(formatShortcut('ctrl+k ctrl+s', 'ctrl')) // 'Ctrl+K Ctrl+S'\n\nunmount()",
|
|
13
|
+
"name": "Basic Shortcuts"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "chord-sequences",
|
|
17
|
+
"code": "import { createKeymap, findShortcutConflicts } from '@vielzeug/keymap'\n\n// Chord sequences fire only after all steps are pressed in order within the timeout.\n// Shortcut strings are lowercased before matching, so 'g g' and 'g G' are the SAME\n// binding — writing both would silently overwrite one. Vim's actual 'G' is shift+g,\n// a single keystroke, not a 'g'-prefixed chord.\nconst map = createKeymap({\n 'ctrl+k ctrl+s': () => console.log('save all (VS Code-style)'),\n 'g g': () => console.log('go to top (Vim-style)'),\n 'shift+g': () => console.log('go to bottom'),\n}, { chordTimeout: 800, modKey: 'ctrl' })\n\nconst unmount = map.mount(document)\n\n// Simulate completing 'ctrl+k ctrl+s' chord.\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true }))\n\n// Simulate Vim 'g g' chord.\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'g', bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'g', bubbles: true }))\n\n// Gotcha: a single-key binding sharing a chord's first step always wins immediately,\n// making the longer chord unreachable — findShortcutConflicts() catches this up front.\nconsole.log('Would ctrl+k (alone) conflict with the chord above?')\nconsole.log(findShortcutConflicts('ctrl+k', map.listBindings()))\n\nunmount()",
|
|
18
|
+
"name": "Chord Sequences"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "conflict-detection",
|
|
22
|
+
"code": "import { createKeymap, findShortcutConflicts } from '@vielzeug/keymap'\n\n// findShortcutConflicts() catches unreachable bindings before you register them —\n// useful for a shortcut-customization UI driven by user input.\nconst map = createKeymap({\n g: () => console.log('go to top'),\n})\n\nconst proposed = 'g g'\nconst conflicts = findShortcutConflicts(proposed, map.listBindings())\n\nif (conflicts.length > 0) {\n console.log(`\"${proposed}\" would never fire — shadowed by an existing binding`)\n} else {\n map.bind(proposed, () => console.log('go to bottom'))\n}\n\n// A shortcut with no relationship to existing bindings reports no conflicts.\nconsole.log('ctrl+s conflicts:', findShortcutConflicts('ctrl+s', map.listBindings()).length)\n\n// keydown and keyup bindings never conflict — they're matched independently.\nconst withKeyup = createKeymap({ space: { handler: () => {}, trigger: 'keyup' } })\nconsole.log(\n 'space (keydown) vs space (keyup):',\n findShortcutConflicts('space', withKeyup.listBindings(), { trigger: 'keydown' }).length,\n)",
|
|
23
|
+
"name": "Conflict Detection"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "keymap-layers",
|
|
27
|
+
"code": "import { createKeymap, createKeymapLayer } from '@vielzeug/keymap'\n\n// createKeymapLayer() scopes a second set of bindings on top of a base keymap —\n// useful for modal UI. The caller mounts/disposes each independently.\nconst base = createKeymap({\n 'ctrl+z': () => console.log('undo'),\n})\n\nconst modal = createKeymapLayer(base, {\n escape: () => console.log('close modal'),\n})\n\nconst unmountBase = base.mount(document)\nconst unmountModal = modal.mount(document)\n\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))\n\n// deactivate() suspends the layer without touching the base keymap.\nmodal.deactivate()\nconsole.log('modal active:', modal.active)\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) // no longer logs\n\nmodal.activate()\nconsole.log('modal.parent === base:', modal.parent === base)\n\nunmountModal()\nunmountBase()",
|
|
28
|
+
"name": "Keymap Layers"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "parse-and-match",
|
|
32
|
+
"code": "import { KeymapError, KeymapParseError, formatShortcut, matchStep, parseShortcut } from '@vielzeug/keymap'\n\n// Parse shortcut strings into structured step objects.\nconst steps = parseShortcut('ctrl+k ctrl+s', 'ctrl')\nconsole.log('Steps:', steps.length)\nconsole.log('Step 0 key:', steps[0].key)\nconsole.log('Step 0 modifiers:', [...steps[0].modifiers])\n\n// matchStep tests a single KeyboardEvent against a parsed step.\nconst event = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true })\nconsole.log('event matches ctrl+k:', matchStep(event, steps[0])) // true\nconsole.log('event matches ctrl+s:', matchStep(event, steps[1])) // false\n\n// formatShortcut turns a shortcut string into a display label.\nconst shortcuts = [\n ['mod+shift+p', 'meta'],\n ['mod+shift+p', 'ctrl'],\n ['ctrl+k ctrl+s', 'ctrl'],\n ['escape', 'ctrl'],\n ['space', 'meta'],\n]\n\nfor (const [shortcut, modKey] of shortcuts) {\n console.log(shortcut, '→', formatShortcut(shortcut, modKey))\n}\n\n// parseShortcut() throws KeymapParseError for ambiguous or invalid steps.\n// Catch it with instanceof KeymapError (or KeymapError.is()) to handle any keymap error.\ntry {\n parseShortcut('ctrl+k+j', 'ctrl') // two non-modifier keys in one step — ambiguous\n} catch (err) {\n console.log('Caught:', KeymapError.is(err), err instanceof KeymapParseError, err.message)\n}",
|
|
33
|
+
"name": "Parse & Match"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "shortcut-utilities",
|
|
37
|
+
"code": "import {\n canonicalizeShortcut,\n createKeymap,\n detectModKey,\n parseShortcut,\n parseStep,\n} from '@vielzeug/keymap'\n\n// detectModKey() — platform modifier detection.\nconst modKey = detectModKey()\nconsole.log('Platform modifier:', modKey)\n\n// parseStep() — parse a single chord step (no throw on invalid input).\nconst step = parseStep('ctrl+k', modKey)\nconsole.log('parseStep ctrl+k:', step?.key, [...(step?.modifiers ?? [])])\n\nconst invalid = parseStep('', modKey)\nconsole.log('parseStep empty string:', invalid) // null\n\n// canonicalizeShortcut() — stable canonical key for conflict detection.\n// Different aliases for the same shortcut resolve to the same canonical key.\nconst a = canonicalizeShortcut(parseShortcut('cmd+k', modKey))\nconst b = canonicalizeShortcut(parseShortcut('meta+k', modKey))\nconsole.log('cmd+k canonical:', a)\nconsole.log('meta+k canonical:', b)\nconsole.log('Same canonical?', a === b)\n\n// listBindings() — inspect active bindings at runtime.\nconst map = createKeymap(\n {\n 'ctrl+k': () => console.log('ctrl+k fired'),\n 'ctrl+shift+s': { handler: () => console.log('save fired'), priority: 5, trigger: 'keyup' },\n },\n { modKey },\n)\n\nconst entries = map.listBindings()\nconsole.log('Bindings:', entries.length)\n\nfor (const entry of entries) {\n const canonical = canonicalizeShortcut(entry.shortcut)\n console.log(` ${canonical} — trigger: ${entry.trigger}, priority: ${entry.priority}`)\n}\n\n// bind() returns an unbind closure — uses canonical key internally.\nconst unbind = map.bind('ctrl+j', () => console.log('ctrl+j'))\nconsole.log('After bind:', map.listBindings().length)\n\nunbind()\nconsole.log('After unbind:', map.listBindings().length)",
|
|
38
|
+
"name": "Shortcut Utilities"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"typeSignatures": {
|
|
42
|
+
"findShortcutConflicts": "export { findShortcutConflicts } from './conflicts';",
|
|
43
|
+
"KeymapError": "export { KeymapError, KeymapParseError } from './errors';",
|
|
44
|
+
"KeymapParseError": "export { KeymapError, KeymapParseError } from './errors';",
|
|
45
|
+
"formatShortcut": "export { formatShortcut } from './format';",
|
|
46
|
+
"createKeymap": "export { createKeymap } from './keymap';",
|
|
47
|
+
"createKeymapLayer": "export { createKeymapLayer } from './layer';",
|
|
48
|
+
"canonicalizeShortcut": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
49
|
+
"detectModKey": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
50
|
+
"matchStep": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
51
|
+
"parseShortcut": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
52
|
+
"parseStep": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
53
|
+
"ConflictOptions": "export type { ConflictOptions } from './conflicts';",
|
|
54
|
+
"BindingEntry": "export type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';",
|
|
55
|
+
"BindingOptions": "export type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';",
|
|
56
|
+
"BindingValue": "export type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';",
|
|
57
|
+
"Handler": "export type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';",
|
|
58
|
+
"Keymap": "export type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';",
|
|
59
|
+
"KeymapOptions": "export type { BindingEntry, BindingOptions, BindingValue, Handler, Keymap, KeymapOptions } from './types';",
|
|
60
|
+
"KeymapLayer": "export type { KeymapLayer } from './layer';",
|
|
61
|
+
"ModifierKey": "export type { ModifierKey, Shortcut, ShortcutStep } from './parser';",
|
|
62
|
+
"Shortcut": "export type { ModifierKey, Shortcut, ShortcutStep } from './parser';",
|
|
63
|
+
"ShortcutStep": "export type { ModifierKey, Shortcut, ShortcutStep } from './parser';"
|
|
64
|
+
}
|
|
65
|
+
}
|