@vielzeug/codex 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +137 -129
- package/data/llms-full.txt +13088 -17651
- package/data/llms.txt +12 -11
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +1 -1
- package/data/packages/assay.json +1 -1
- package/data/packages/clockwork.json +2 -2
- package/data/packages/codex.json +1 -1
- package/data/packages/coins.json +1 -1
- package/data/packages/conduit.json +1 -1
- package/data/packages/courier.json +1 -1
- package/data/packages/dnd.json +14 -12
- package/data/packages/familiar.json +26 -16
- package/data/packages/flux.json +1 -1
- package/data/packages/forge.json +1 -1
- package/data/packages/herald.json +19 -33
- package/data/packages/keymap.json +13 -19
- package/data/packages/ledger.json +28 -25
- package/data/packages/lingua.json +2 -2
- package/data/packages/necromancer.json +50 -0
- package/data/packages/orbit.json +34 -39
- package/data/packages/ore.json +1 -1
- package/data/packages/prism.json +37 -40
- package/data/packages/pulse.json +26 -24
- package/data/packages/refine.json +1 -1
- package/data/packages/ripple.json +1 -1
- package/data/packages/rune.json +6 -7
- package/data/packages/sandbox.json +7 -6
- package/data/packages/scout.json +10 -10
- package/data/packages/scroll.json +18 -17
- package/data/packages/sourcerer.json +1 -1
- package/data/packages/spell.json +1 -1
- package/data/packages/tempo.json +49 -81
- package/data/packages/vault.json +37 -40
- package/data/packages/ward.json +5 -17
- package/data/packages/wayfinder.json +9 -9
- package/data/refine.json +4902 -4902
- package/data/search.json +205 -206
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/http.js +46 -6
- package/dist/http.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/index.js +13 -5
- package/dist/tools/index.js.map +1 -1
- package/package.json +4 -4
package/data/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vielzeug
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 32 focused TypeScript packages. Version: 2.0.2
|
|
4
4
|
|
|
5
5
|
Install any package independently: `pnpm add @vielzeug/<name>`
|
|
6
6
|
|
|
@@ -14,26 +14,27 @@ Install any package independently: `pnpm add @vielzeug/<name>`
|
|
|
14
14
|
- [@vielzeug/conduit](/conduit/): Dependency-first asynchronous dependency injection with typed tokens, lifecycle scopes, startup validation, and deterministic disposal.
|
|
15
15
|
- [@vielzeug/courier](/courier/): A framework-neutral fetch client with explicit cache keys, direct mutations, and abortable streams.
|
|
16
16
|
- [@vielzeug/dnd](/dnd/): Framework-agnostic drag-and-drop. Drop zones with MIME filtering, sortable lists with drag handles, and explicit connected scopes — zero dependencies.
|
|
17
|
-
- [@vielzeug/familiar](/familiar/): Typed
|
|
17
|
+
- [@vielzeug/familiar](/familiar/): Typed ES module Worker pools with cancellation, priority scheduling, streaming, and test utilities.
|
|
18
18
|
- [@vielzeug/flux](/flux/): Reusable push streams with subscription-owned cancellation, bounded buffering, and optional ecosystem adapters.
|
|
19
19
|
- [@vielzeug/forge](/forge/): Framework-agnostic immutable form state with focused object fields and explicit validation results.
|
|
20
|
-
- [@vielzeug/herald](/herald/):
|
|
21
|
-
- [@vielzeug/keymap](/keymap/):
|
|
22
|
-
- [@vielzeug/ledger](/ledger/):
|
|
20
|
+
- [@vielzeug/herald](/herald/): Typed temporal event delivery with sync subscriptions, async waiting, streams, pipes, and AbortSignal lifecycle.
|
|
21
|
+
- [@vielzeug/keymap](/keymap/): Target-local keyboard shortcut manager with chords, event-aware guards, modifier aliases, and terminal disposal.
|
|
22
|
+
- [@vielzeug/ledger](/ledger/): Serialized reversible command history with cancellation ownership and atomic reactive snapshots.
|
|
23
23
|
- [@vielzeug/lingua](/lingua/): Framework-neutral locale catalogs, typed translations, and explicit plural messages.
|
|
24
|
-
- [@vielzeug/
|
|
24
|
+
- [@vielzeug/necromancer](/necromancer/): Lifecycle-owned Web Animations API primitives for native playback, groups, and additive FLIP transitions.
|
|
25
|
+
- [@vielzeug/orbit](/orbit/): Dependency-free floating positioning with lifecycle-owned geometry and middleware.
|
|
25
26
|
- [@vielzeug/ore](/ore/): Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, observers, and testing utilities.
|
|
26
27
|
- [@vielzeug/prism](/prism/): Reactive SVG charting library — line, bar, and area charts. Signal-driven updates, CSS-themeable, accessible.
|
|
27
|
-
- [@vielzeug/pulse](/pulse/):
|
|
28
|
+
- [@vielzeug/pulse](/pulse/): Explicitly connected, typed WebSocket sessions with scoped channels, presence, reconnect restoration, and heartbeat.
|
|
28
29
|
- [@vielzeug/refine](/refine/): Accessible, themeable web components built with Ore for framework and vanilla DOM apps.
|
|
29
30
|
- [@vielzeug/ripple](/ripple/): Framework-agnostic signals, derived values, effects, scopes, async resources, and immutable state.
|
|
30
31
|
- [@vielzeug/rune](/rune/): Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.
|
|
31
32
|
- [@vielzeug/sandbox](/sandbox/): Isolated iframe runtime with a typed postMessage bridge for safe execution of untrusted HTML — component previews, playgrounds, plugin sandboxes, and more.
|
|
32
33
|
- [@vielzeug/scout](/scout/): Trigram-indexed fuzzy search with per-field weights, match highlighting, and an optional reactive layer.
|
|
33
|
-
- [@vielzeug/scroll](/scroll/): Lightweight, framework-agnostic virtual list engine with variable heights, sticky headers, grid support, and
|
|
34
|
+
- [@vielzeug/scroll](/scroll/): Lightweight, framework-agnostic virtual list engine with variable heights, sticky headers, grid support, and reactive integration.
|
|
34
35
|
- [@vielzeug/sourcerer](/sourcerer/): Framework-agnostic collection sources for local, page, cursor, and infinite pagination.
|
|
35
36
|
- [@vielzeug/spell](/spell/): Schema validation with explicit sync/async checks, portable definitions, JSON Schema export, and tree-shakeable entry points.
|
|
36
|
-
- [@vielzeug/tempo](/tempo/): Temporal
|
|
37
|
-
- [@vielzeug/vault](/vault/): Typed browser storage with portable keys, TTL, observation, and
|
|
38
|
-
- [@vielzeug/ward](/ward/): Typed authorization policies with wildcard matching, deterministic precedence,
|
|
37
|
+
- [@vielzeug/tempo](/tempo/): Explicit Temporal parsing, timezone-safe arithmetic, and localized date/time formatting for TypeScript.
|
|
38
|
+
- [@vielzeug/vault](/vault/): Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.
|
|
39
|
+
- [@vielzeug/ward](/ward/): Typed authorization policies with wildcard matching, deterministic precedence, and decision tracing.
|
|
39
40
|
- [@vielzeug/wayfinder](/wayfinder/): Framework-agnostic client-side router with typed params, async data loading, middleware, leave guards, and View Transitions support.
|
package/data/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export * from './errors';\nexport * from './array/chunk';\nexport * from './array/filterMap';\nexport * from './array/groupBy';\nexport * from './array/indexBy';\nexport * from './array/partition';\nexport * from './array/sort';\nexport * from './array/uniq';\nexport * from './async/attempt';\nexport * from './async/parallel';\nexport * from './async/retry';\nexport * from './async/sleep';\nexport * from './function/debounce';\nexport * from './function/once';\nexport * from './function/pipe';\nexport * from './function/throttle';\nexport * from './guards/combinators';\nexport * from './guards/isDefined';\nexport * from './guards/isEqual';\nexport * from './guards/isNil';\nexport * from './guards/isPlainObject';\nexport * from './math/clamp';\nexport * from './math/range';\nexport * from './object/getPath';\nexport * from './object/hash';\nexport * from './object/omit';\nexport * from './object/pick';\nexport * from './random/uuid';\nexport * from './string/camelCase';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Arsenal — Utility library for TypeScript\ndescription: Tree-shakeable TypeScript utilities with focused category entry points for arrays, async work, caching, objects, strings, math, and guards.\npackage: arsenal\ncategory: utilities\nkeywords: [utility, array, string, object, math, async, debounce, throttle, cache]\nexports: [chunk, groupBy, retry, debounce, clamp, isEqual, taskPool, cache, fuzzyFilter, tryParseJson]\nrelated: [tempo, sourcerer, spell, coins]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"arsenal\" />\n\n## Why Arsenal?\n\nArsenal keeps common utilities at package root and places specialized behavior behind category entry points. This keeps autocomplete focused while preserving one dependency and tree-shakeable modules.\n\n```ts\n// Before\nconst users = JSON.parse(raw).filter((user) => user.name.includes(query));\n\n// After\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n\nconst parsed = tryParseJson(raw);\nconst users = parsed.ok ? fuzzyFilter(parsed.value as User[], query, { select: (user) => user.name }) : [];\n```\n\n| Feature | Arsenal | lodash-es | Remeda |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"arsenal\" type=\"size\" /> | ~72 kB | ~18 kB |\n| Typed root utilities | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Category entry points | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Async task pool and cache | <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 Arsenal when** you need one typed utility dependency with focused subpaths for specialized behavior.\n\n**Consider narrower alternatives when** you need only platform APIs or a small functional subset.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/arsenal\n```\n\n```sh [npm]\nnpm install @vielzeug/arsenal\n```\n\n```sh [yarn]\nyarn add @vielzeug/arsenal\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { chunk, groupBy, retry } from '@vielzeug/arsenal';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { taskPool } from '@vielzeug/arsenal/async';\n\nconst pages = chunk([1, 2, 3, 4, 5], 2);\nconst byRole = groupBy([{ role: 'admin' }, { role: 'user' }], (user) => user.role);\n\nconst pool = taskPool({ concurrency: 2 });\nconst health = await pool.run((signal) => retry(() => fetch('/health', { signal }).then((response) => response.json())));\n\nconst responses = cache<string, unknown>({ ttlMs: 60_000 });\nconst profile = await responses.getOrLoad('/profile', () => fetch('/profile').then((response) => response.json()));\n\npool.dispose();\nconsole.log(pages, byRole, health, profile);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`chunk`**: common array/string chunking from package root\n- **`retry`**: retry async work with cancellation support from package root\n- **`taskPool`**: bounded, disposable concurrent work from `/async`\n- **`cache`**: identity-keyed TTL cache with async load deduplication from `/cache`\n- **`fuzzyFilter`**: explicit-field fuzzy filtering from `/array`\n- **`tryParseJson`**: preserve JSON syntax failures from `/object`\n- **`clamp`**: numeric bounds from package root\n- **`isEqual`**: structural equality from package root\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — validate `unknown` JSON data after `tryParseJson`.\n- [Vault](/vault/) — persistent storage; Arsenal cache is in-memory only.\n- [Tempo](/tempo/) — date/time utilities kept outside Arsenal.\n- [Coins](/coins/) — money formatting and currency conversion.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Arsenal — Utility library for TypeScript\ndescription: Tree-shakeable TypeScript utilities with focused category entry points for arrays, async work, caching, objects, strings, math, and guards.\npackage: arsenal\ncategory: utilities\nkeywords: [utility, array, string, object, math, async, debounce, throttle, cache]\nexports: [chunk, groupBy, retry, debounce, clamp, isEqual, taskPool, cache, fuzzyFilter, tryParseJson]\nrelated: [tempo, sourcerer, spell, coins]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"arsenal\" />\n\n## Why Arsenal?\n\nArsenal keeps common utilities at package root and places specialized behavior behind category entry points. This keeps autocomplete focused while preserving one dependency and tree-shakeable modules.\n\n```ts\n// Before\nconst users = JSON.parse(raw).filter((user) => user.name.includes(query));\n\n// After\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n\nconst parsed = tryParseJson(raw);\nconst users = parsed.ok ? fuzzyFilter(parsed.value as User[], query, { select: (user) => user.name }) : [];\n```\n\n| Feature | Arsenal | lodash-es | Remeda |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"arsenal\" type=\"size\" /> | ~72 kB | ~18 kB |\n| Typed root utilities | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Category entry points | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Async task pool and cache | <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 Arsenal when** you need one typed utility dependency with focused subpaths for specialized behavior.\n\n**Consider narrower alternatives when** you need only platform APIs or a small functional subset.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/arsenal\n```\n\n```sh [npm]\nnpm install @vielzeug/arsenal\n```\n\n```sh [yarn]\nyarn add @vielzeug/arsenal\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { chunk, groupBy, retry } from '@vielzeug/arsenal';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { taskPool } from '@vielzeug/arsenal/async';\n\nconst pages = chunk([1, 2, 3, 4, 5], 2);\nconst byRole = groupBy([{ role: 'admin' }, { role: 'user' }], (user) => user.role);\n\nconst pool = taskPool({ concurrency: 2 });\nconst health = await pool.run((signal) => retry(() => fetch('/health', { signal }).then((response) => response.json())));\n\nconst responses = cache<string, unknown>({ ttlMs: 60_000 });\nconst profile = await responses.getOrLoad('/profile', () => fetch('/profile').then((response) => response.json()));\n\npool.dispose();\nconsole.log(pages, byRole, health, profile);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`chunk`**: common array/string chunking from package root\n- **`retry`**: retry async work with cancellation support from package root\n- **`taskPool`**: bounded, disposable concurrent work from `/async`\n- **`cache`**: identity-keyed TTL cache with async load deduplication from `/cache`\n- **`fuzzyFilter`**: explicit-field fuzzy filtering from `/array`\n- **`tryParseJson`**: preserve JSON syntax failures from `/object`\n- **`clamp`**: numeric bounds from package root\n- **`isEqual`**: structural equality from package root\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — validate `unknown` JSON data after `tryParseJson`.\n- [Vault](/vault/) — persistent storage; Arsenal cache is in-memory only.\n- [Tempo](/tempo/) — date/time utilities kept outside Arsenal.\n- [Coins](/coins/) — money formatting and currency conversion.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Arsenal — API Reference\ndescription: Reference for Arsenal root utilities and category entry points.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `chunk` | Split arrays or strings | Sync | Root export |\n| `groupBy` | Group values by key | Sync | Root export |\n| `retry` | Retry async work | Async | Rethrows final error |\n| `taskPool` | Bound concurrent tasks | Async | Available from `/async` |\n| `cache` | In-memory identity-keyed cache | Async | Available from `/cache` |\n| `fuzzyFilter` | Filter string or selected object fields | Sync | Object collections require `select` |\n| `fuzzyScore` | Rank string or selected object fields | Sync | Object collections require `select` |\n| `tryParseJson` | Preserve JSON syntax result | Sync | Returns `unknown` on success |\n| `getPath` | Optional object lookup | Sync | Available from `/object` |\n| `clamp` | Bound number to range | Sync | Root export |\n| `isEqual` | Structural equality | Sync | Root export |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/arsenal` | Curated common utilities |\n| `@vielzeug/arsenal/array` | Array transforms, sorting, fuzzy search |\n| `@vielzeug/arsenal/async` | Retry, cancellation, task pool, timing |\n| `@vielzeug/arsenal/cache` | In-memory cache and memoization |\n| `@vielzeug/arsenal/function` | Composition, timing, assertions |\n| `@vielzeug/arsenal/guards` | Predicate and type guard helpers |\n| `@vielzeug/arsenal/math` | Numeric and statistical helpers |\n| `@vielzeug/arsenal/object` | Paths, transforms, hash, JSON parse result |\n| `@vielzeug/arsenal/random` | Random selection and UUID helpers |\n| `@vielzeug/arsenal/string` | Text transforms and similarity |\n\n## Array\n\n### fuzzyFilter / fuzzyScore\n\n```ts\nfuzzyFilter(strings: readonly string[], query: string, options?: FuzzyOptions): string[]\nfuzzyFilter<T>(items: readonly T[], query: string, options: FuzzySelection<T>): T[]\nfuzzyScore(strings: readonly string[], query: string, options?: FuzzyOptions): ScoredResult<string>[]\nfuzzyScore<T>(items: readonly T[], query: string, options: FuzzySelection<T>): ScoredResult<T>[]\n```\n\n`fuzzyFilter` preserves input order. `fuzzyScore` orders results by descending score.\n\n```ts\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\n\nconst users = [{ email: 'alice@example.com', name: 'Alice' }];\nconst matches = fuzzyFilter(users, 'alice', { select: (user) => [user.name, user.email] });\n```\n\n---\n\n## Async\n\n### taskPool\n\n```ts\ninterface TaskPool {\n run<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>;\n idle(): Promise<void>;\n dispose(reason?: unknown): void;\n readonly active: number;\n readonly pending: number;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n}\n\ntaskPool(options?: { concurrency?: number }): TaskPool\n```\n\n`dispose()` aborts running cooperative tasks and rejects pending tasks.\n\n```ts\nimport { taskPool } from '@vielzeug/arsenal/async';\n\nconst pool = taskPool({ concurrency: 2 });\nconst user = await pool.run((signal) => fetch('/user', { signal }).then((response) => response.json()));\npool.dispose();\n```\n\n---\n\n## Cache\n\n### cache\n\n```ts\ninterface Cache<K, T> {\n get(key: K): T | undefined;\n set(key: K, value: T, options?: { ttlMs?: number }): void;\n getOrLoad(key: K, load: () => Promise<T>): Promise<T>;\n delete(key: K): boolean;\n clear(): void;\n readonly size: number;\n}\n\ncache<K, T>(options?: CacheOptions): Cache<K, T>\n```\n\nKeys use native `Map` identity. Expiry is lazy, evaluated by `get`, `getOrLoad`, and `size`.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\nconst profiles = cache<string, Profile>({ ttlMs: 60_000 });\nconst profile = await profiles.getOrLoad('me', loadProfile);\n```\n\n---\n\n## Object\n\n### tryParseJson\n\n```ts\ntype JsonParseResult = { ok: true; value: unknown } | { error: SyntaxError; ok: false };\n\ntryParseJson(text: string): JsonParseResult\n```\n\nUse a schema validator after success to refine `unknown` data.\n\n```ts\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n\nconst result = tryParseJson(raw);\nif (!result.ok) throw result.error;\n```\n\n### getPath\n\n```ts\ngetPath<T extends Record<string, unknown>, P extends string>(item: T, path: P): PathValue<T, P> | undefined\ngetPathOr<T extends Record<string, unknown>, P extends string, F>(item: T, path: P, fallback: F): PathValue<T, P> | F\nrequirePath<T extends Record<string, unknown>, P extends string>(item: T, path: P): Exclude<PathValue<T, P>, undefined>\n```\n\n## Types\n\n```ts\ntype FuzzyOptions = {\n normalize?: boolean;\n threshold?: number;\n};\n\ntype FuzzySelection<T> = FuzzyOptions & {\n select: (item: T) => string | readonly string[];\n};\n\ntype ScoredResult<T> = { item: T; score: number };\n\ntype CacheOptions = {\n capacity?: number;\n now?: () => number;\n ttlMs?: number;\n};\n```\n\n## Errors\n\n- `RangeError` — invalid numeric bounds, capacity, concurrency, or retry count.\n- `TypeError` — invalid value types, unsupported comparison, or required path missing.\n- `ArsenalSerializationError` — memo or hash cannot serialize supplied input.\n",
|
|
6
6
|
"usage": "---\ntitle: Arsenal — Usage Guide\ndescription: Use Arsenal root utilities for common work and category entry points for specialized collection, async, cache, object, and string behavior.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart at package root for common transforms. Move to a category entry point when code needs specialized behavior. This keeps imports readable and bundles focused.\n\n```ts\nimport { chunk, groupBy, retry } from '@vielzeug/arsenal';\n\nconst users = [\n { id: 'a1', role: 'admin' },\n { id: 'u1', role: 'user' },\n { id: 'u2', role: 'user' },\n];\n\nconst pages = chunk(users, 2);\nconst byRole = groupBy(users, (user) => user.role);\nconst health = await retry(() => fetch('/health').then((response) => response.json()));\n\nconsole.log(pages, byRole, health);\n```\n\nUse category imports for APIs absent from root:\n\n```ts\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\nimport { taskPool } from '@vielzeug/arsenal/async';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n```\n\n## Transform Collections\n\nUse `/array` for transforms that preserve input immutability. `filterMap` combines mapping and omission; `indexBy` and `groupBy` build lookup structures without mutation.\n\n```ts\nimport { filterMap, indexBy, sort } from '@vielzeug/arsenal/array';\n\nconst products = [\n { id: 'p1', price: 20, published: true },\n { id: 'p2', price: 10, published: false },\n { id: 'p3', price: 15, published: true },\n];\n\nconst publishedLabels = filterMap(products, (product) => (product.published ? `${product.id}: ${product.price}` : undefined));\nconst byId = indexBy(products, (product) => product.id);\nconst byPrice = sort(products, (product) => product.price);\n\nconsole.log(publishedLabels, byId, byPrice);\n```\n\n## Search Explicit Fields\n\nSearch string arrays directly. Object collections require `select`, so callers define exactly what can match.\n\n```ts\nimport { fuzzyFilter, fuzzyScore } from '@vielzeug/arsenal/array';\n\nconst users = [\n { email: 'alice@example.com', name: 'Alice' },\n { email: 'bob@example.com', name: 'Bob' },\n];\n\nconst matches = fuzzyFilter(users, 'alice', { select: (user) => [user.name, user.email] });\nconst ranked = fuzzyScore(users, 'ali', { select: (user) => user.name });\n```\n\n## Work with Object Data\n\nUse `/object` for paths, key selection, stable cache keys, and object transforms.\n\n```ts\nimport { getPathOr, hash, omit, pick } from '@vielzeug/arsenal/object';\n\nconst config = { api: { host: 'localhost', port: 3000 }, debug: true };\nconst port = getPathOr(config, 'api.port', 8080);\nconst publicConfig = pick(config, ['api']);\nconst productionConfig = omit(config, ['debug']);\nconst key = hash({ port, productionConfig });\n\nconsole.log(publicConfig, key);\n```\n\n## Parse and Validate JSON\n\n`tryParseJson` distinguishes syntax failure from schema failure. Treat successful values as `unknown`, then validate with Spell or application code.\n\n```ts\nimport { tryParseJson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({ id: s.string(), name: s.string() });\nconst parsed = tryParseJson(raw);\n\nif (!parsed.ok) throw parsed.error;\n\nconst user = User.parse(parsed.value);\n```\n\n## Bound Concurrent Work\n\nUse `parallel` for one finite collection. Use `taskPool` when tasks arrive over time or need disposal.\n\n```ts\nimport { parallel, taskPool } from '@vielzeug/arsenal/async';\n\nconst metadata = await parallel(urls, (url) => fetch(url).then((response) => response.json()), { limit: 4 });\n\nconst pool = taskPool({ concurrency: 2 });\nconst profile = await pool.run((signal) => fetch('/profile', { signal }).then((response) => response.json()));\n\nawait pool.idle();\npool.dispose();\n\nconsole.log(metadata, profile);\n```\n\n## Cache Loaded Values\n\nUse `cache` for process-local values. Keys retain native `Map` identity. `getOrLoad` deduplicates concurrent loads for one key.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\ntype Profile = { id: string; name: string };\n\nconst profiles = cache<string, Profile>({ capacity: 100, ttlMs: 60_000 });\nconst profile = await profiles.getOrLoad('me', () => fetch('/profile').then((response) => response.json()));\n\nprofiles.delete('me');\nconst freshProfile = await profiles.getOrLoad('me', () => fetch('/profile').then((response) => response.json()));\n\nconsole.log(profile, freshProfile);\n```\n\n## Test Deterministic Randomness\n\nRandom helpers use cryptographic entropy by default. Pass `RandomSource` in tests when output must be deterministic.\n\n```ts\nimport { random, type RandomSource } from '@vielzeug/arsenal/random';\n\nconst source: RandomSource = { next: () => 0.5 };\n\nrandom(1, 4, source); // 3\n```\n\n## Working with Other Vielzeug Libraries\n\nUse Spell after `tryParseJson` for typed external data. Use Vault instead of `cache` when data must survive reloads or process restart.\n\n```ts\nimport { tryParseJson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({ theme: s.string() });\nconst parsed = tryParseJson(rawSettings);\nconst settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };\n```\n\n## Best Practices\n\n- Import common transforms from package root.\n- Import specialized APIs from category entry points.\n- Pass `select` for every fuzzy search over objects.\n- Validate parsed JSON before using it as application data.\n- Use `parallel` for finite batches and `taskPool` for ongoing work.\n- Dispose task pools when their owner ends.\n- Use `cache` only for in-memory data.\n- Inject `RandomSource` in deterministic tests.\n",
|
|
7
7
|
"examples": "---\ntitle: Arsenal — Examples\ndescription: Practical examples and recipes for arsenal.\n---\n\n## Examples\n\n- [Array utilities](./examples/array.md)\n- [Async utilities](./examples/async.md)\n- [Cache utilities](./examples/cache.md)\n- [Function utilities](./examples/function.md)\n- [Guards / Typed predicates](./examples/typed.md)\n- [Math utilities](./examples/math.md)\n- [Object utilities](./examples/object.md) — includes `getPath`, `tryParseJson`, `stringify`, `diff`, `deepMerge`\n- [Random utilities](./examples/random.md)\n- [String utilities](./examples/string.md)\n"
|
package/data/packages/assay.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { AssayError, AssayQueryError, AssayTimeoutError } from './errors';\n\nexport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n type CustomEventOptions,\n} from './events';\nexport { within, queryInShadow, queryAllInShadow, queryPart, getSlotted, type QueryScope } from './query';\nexport {\n delay,\n nextTick,\n retry,\n waitForEvent,\n waitUntil,\n type DelayOptions,\n type RetryOptions,\n type WaitOptions,\n} from './wait';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Assay — Framework-agnostic DOM testing primitives\ndescription: Scoped DOM queries, exact event dispatch, and cancellable async waiting for browser tests.\npackage: assay\ncategory: testing\nkeywords: [testing, dom, events, queries, custom-elements]\nrelated: [ore, refine]\nexports:\n [\n within,\n queryInShadow,\n queryAllInShadow,\n queryPart,\n getSlotted,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n waitUntil,\n retry,\n waitForEvent,\n delay,\n nextTick,\n AssayError,\n AssayQueryError,\n AssayTimeoutError,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"assay\" />\n\n## Why Assay?\n\nAssay provides focused DOM test primitives that work with vanilla elements, custom elements, and framework-rendered output. It scopes queries, dispatches exact browser event classes, and waits on explicit conditions without imposing a renderer or browser automation stack.\n\n```ts\n// Before\nbutton.dispatchEvent(new MouseEvent('click', { bubbles: true }));\nawait new Promise((resolve) => setTimeout(resolve, 100));\n\n// After\nfireClick(view.get('button.submit'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n| Feature | Assay | Testing Library DOM | Browser automation |\n| ------------------- | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- |\n| Bundle size | <PackageInfo package=\"assay\" type=\"size\" /> | Larger query layer | Browser runtime required |\n| Zero dependencies | <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| Scoped DOM queries | `within()` and shadow helpers | Renderer-oriented queries | Manual selectors |\n| Deterministic waits | `waitUntil()` and `waitForEvent()` | Framework-dependent | Full browser timing |\n\n<div class=\"decision-callout\">\n\n**Use Assay when** a DOM unit test needs readable queries, dispatched events, or a bounded async wait without adopting a rendering framework.\n\n**Consider browser integration tests when** correctness depends on browser default actions, focus behavior, pointer capture, layout, or accessibility-tree behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add -D @vielzeug/assay\n```\n\n```sh [npm]\nnpm install -D @vielzeug/assay\n```\n\n```sh [yarn]\nyarn add -D @vielzeug/assay\n```\n\n:::\n\n## Quick Start\n\nScope a test fixture, dispatch an event, and wait for resulting DOM state.\n\n```ts\nimport { fireClick, waitUntil, within } from '@vielzeug/assay';\n\nconst panel = document.createElement('section');\npanel.innerHTML = '<button>Save</button><output></output>';\npanel.querySelector('button')!.addEventListener('click', () => {\n panel.querySelector('output')!.textContent = 'Saved';\n});\n\nconst view = within(panel);\nfireClick(view.get('button'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `within(root)` scopes nullable and required DOM queries.\n- `queryInShadow`, `queryPart`, and `getSlotted` cross custom-element boundaries explicitly.\n- `fireClick`, `fireInput`, `fireKeyDown`, and peers dispatch exact synchronous events.\n- `waitUntil`, `retry`, and `waitForEvent` provide bounded, abortable async waiting.\n- `delay` and `nextTick` model explicit timer and microtask scheduling.\n- `AssayError`, `AssayQueryError`, and `AssayTimeoutError` provide typed failures.\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- [Ore](/ore/) — component authoring and test fixtures that pair with Assay DOM helpers.\n- [Refine](/refine/) — accessible components with component-specific test assertions.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Assay — Framework-agnostic DOM testing primitives\ndescription: Scoped DOM queries, exact event dispatch, and cancellable async waiting for browser tests.\npackage: assay\ncategory: testing\nkeywords: [testing, dom, events, queries, custom-elements]\nrelated: [ore, refine]\nexports:\n [\n within,\n queryInShadow,\n queryAllInShadow,\n queryPart,\n getSlotted,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n waitUntil,\n retry,\n waitForEvent,\n delay,\n nextTick,\n AssayError,\n AssayQueryError,\n AssayTimeoutError,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"assay\" />\n\n## Why Assay?\n\nAssay provides focused DOM test primitives that work with vanilla elements, custom elements, and framework-rendered output. It scopes queries, dispatches exact browser event classes, and waits on explicit conditions without imposing a renderer or browser automation stack.\n\n```ts\n// Before\nbutton.dispatchEvent(new MouseEvent('click', { bubbles: true }));\nawait new Promise((resolve) => setTimeout(resolve, 100));\n\n// After\nfireClick(view.get('button.submit'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n| Feature | Assay | Testing Library DOM | Browser automation |\n| ------------------- | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- |\n| Bundle size | <PackageInfo package=\"assay\" type=\"size\" /> | Larger query layer | Browser runtime required |\n| Zero dependencies | <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| Scoped DOM queries | `within()` and shadow helpers | Renderer-oriented queries | Manual selectors |\n| Deterministic waits | `waitUntil()` and `waitForEvent()` | Framework-dependent | Full browser timing |\n\n<div class=\"decision-callout\">\n\n**Use Assay when** a DOM unit test needs readable queries, dispatched events, or a bounded async wait without adopting a rendering framework.\n\n**Consider browser integration tests when** correctness depends on browser default actions, focus behavior, pointer capture, layout, or accessibility-tree behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add -D @vielzeug/assay\n```\n\n```sh [npm]\nnpm install -D @vielzeug/assay\n```\n\n```sh [yarn]\nyarn add -D @vielzeug/assay\n```\n\n:::\n\n## Quick Start\n\nScope a test fixture, dispatch an event, and wait for resulting DOM state.\n\n```ts\nimport { fireClick, waitUntil, within } from '@vielzeug/assay';\n\nconst panel = document.createElement('section');\npanel.innerHTML = '<button>Save</button><output></output>';\npanel.querySelector('button')!.addEventListener('click', () => {\n panel.querySelector('output')!.textContent = 'Saved';\n});\n\nconst view = within(panel);\nfireClick(view.get('button'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `within(root)` scopes nullable and required DOM queries.\n- `queryInShadow`, `queryPart`, and `getSlotted` cross custom-element boundaries explicitly.\n- `fireClick`, `fireInput`, `fireKeyDown`, and peers dispatch exact synchronous events.\n- `waitUntil`, `retry`, and `waitForEvent` provide bounded, abortable async waiting.\n- `delay` and `nextTick` model explicit timer and microtask scheduling.\n- `AssayError`, `AssayQueryError`, and `AssayTimeoutError` provide typed failures.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — component authoring and test fixtures that pair with Assay DOM helpers.\n- [Refine](/refine/) — accessible components with component-specific test assertions.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Assay — API Reference\ndescription: API reference for @vielzeug/assay queries, event dispatch, and async waiting.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------------------------------- | ------------------------------------------- | -------------- | ------------------------------------------------ |\n| `within` | Creates scoped query API | Sync | Required `get*` methods throw `AssayQueryError` |\n| `queryInShadow` / `queryPart` / `getSlotted` | Crosses custom-element boundaries | Sync | Open shadow roots are required |\n| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |\n| `waitUntil` / `retry` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |\n| `delay` / `nextTick` | Schedules timers or microtasks | Async | Prefer `nextTick()` for microtask-scheduled work |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ----------------- | ---------------------------------------------------- |\n| `@vielzeug/assay` | DOM queries, events, wait helpers, errors, and types |\n\n## Queries\n\n### `within(root)`\n\nCreates a `QueryScope` for an `Element`, `ShadowRoot`, `Document`, or `DocumentFragment`.\n\n| Method | Returns | Use |\n| --------------------------------- | ----------------- | -------------------------------------------- |\n| `get(selector)` | `Element` | Required CSS match; throws `AssayQueryError` |\n| `query(selector)` | `Element \\| null` | Optional CSS match |\n| `queryAll(selector)` | `Element[]` | All CSS matches |\n| `getByText(text, selector?)` | `Element` | Required exact trimmed-text match |\n| `queryByText(text, selector?)` | `Element \\| null` | Optional exact trimmed-text match |\n| `queryAllByText(text, selector?)` | `Element[]` | All exact trimmed-text matches |\n| `getByTestId(id)` | `Element` | Required `data-testid` match |\n| `queryByTestId(id)` | `Element \\| null` | Optional `data-testid` match |\n| `queryAllByTestId(id)` | `Element[]` | All `data-testid` matches |\n\nText selectors default to `'*'`. Required-query failures include the lookup and a bounded view of the scoped DOM.\n\n### Shadow and slot helpers\n\n| Function | Returns | Description |\n| ---------------------------------- | ----------------- | ---------------------------------------------------- |\n| `queryInShadow(host, selector)` | `Element \\| null` | First match in an open shadow root |\n| `queryAllInShadow(host, selector)` | `Element[]` | All matches in an open shadow root |\n| `queryPart(host, part)` | `Element \\| null` | First shadow element whose `part` token matches |\n| `getSlotted(host, slotName?)` | `Element[]` | Direct light-DOM children in a named or default slot |\n\nThese helpers return `null` or `[]` when there is no shadow root. Dynamic test IDs, parts, and slot names are matched\nas attribute values rather than interpolated into CSS selectors.\n\n## Event dispatch\n\nAll event helpers synchronously return `dispatchEvent()`'s boolean result.\n\n```ts\nimport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from '@vielzeug/assay';\n\nfireClick(button, { clientX: 20 });\nfireInput(input);\nfireKeyDown(input, { key: 'Enter' });\nfireCustom(element, { detail: { id: '42' }, type: 'item-added' });\ndispatch(element, new Event('ready'));\n```\n\n| Function | Event class | Defaults |\n| --------------------------- | --------------- | ------------------------------------------------------ |\n| `fireBlur` / `fireFocus` | `FocusEvent` | Platform defaults (`bubbles: false`) |\n| `fireChange` | `Event` | `bubbles: true` |\n| `fireInput` | `InputEvent` | `bubbles: true` |\n| `fireClick` | `MouseEvent` | `bubbles: true`, `cancelable: true` |\n| `fireKeyDown` / `fireKeyUp` | `KeyboardEvent` | `bubbles: true`, `cancelable: true` |\n| `fireSubmit` | `SubmitEvent` | `bubbles: true`, `cancelable: true` |\n| `fireCustom` | `CustomEvent` | `bubbles: true`, `cancelable: true`, `composed: false` |\n\n`fireCustom(target, { type, ...init })` requires the event type in its options object. Assay intentionally does not\nprovide browser-default or fallback pointer/touch simulation.\n\n## Async waiting\n\n```ts\nawait waitUntil(() => ready, { interval: 20, signal, timeout: 1000 });\nawait retry(() => expect(spy).toHaveBeenCalled(), { signal, timeout: 1000 });\nawait waitForEvent(target, 'ready', { signal, timeout: 1000 });\nawait delay(100, { signal });\nawait nextTick();\n```\n\n| Function | Success condition | Options |\n| -------------------------------------- | ------------------------ | ------------------------------------------ |\n| `waitUntil(predicate, options?)` | Predicate returns `true` | `timeout`, `interval`, `signal` |\n| `retry(assertion, options?)` | Assertion stops throwing | `timeout`, `interval`, `signal`, `message` |\n| `waitForEvent(target, type, options?)` | Target emits `type` | `timeout`, `signal` |\n| `delay(ms?, options?)` | Timer elapses | `signal` |\n| `nextTick()` | Next microtask | none |\n\n`waitUntil`, `retry`, and `waitForEvent` reject with `AssayTimeoutError` when their timeout expires. A supplied abort\nsignal rejects with its reason and removes timers and event listeners.\n\n## Types\n\n```ts\nexport interface QueryScope {\n get(selector: string): Element;\n query(selector: string): Element | null;\n queryAll(selector: string): Element[];\n getByText(text: string, selector?: string): Element;\n queryByText(text: string, selector?: string): Element | null;\n queryAllByText(text: string, selector?: string): Element[];\n getByTestId(id: string): Element;\n queryByTestId(id: string): Element | null;\n queryAllByTestId(id: string): Element[];\n}\n```\n\n`CustomEventOptions`, `DelayOptions`, `RetryOptions`, and `WaitOptions` are exported option types for event and wait helpers.\n\n## Errors\n\n| Error | Meaning |\n| ------------------- | -------------------------------------- |\n| `AssayError` | Base class for Assay-originated errors |\n| `AssayQueryError` | A required `get*` query had no match |\n| `AssayTimeoutError` | A wait operation reached its timeout |\n\n`AssayError.is(value)` narrows any value to the Assay error hierarchy.\n",
|
|
6
6
|
"usage": "---\ntitle: Assay — Usage Guide\ndescription: Scoped DOM queries, exact event dispatch, and cancellable waiting with @vielzeug/assay.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`within(root)` is Assay's query API. It accepts an element, document fragment, or shadow root and keeps every lookup\nin that scope. Use `get*` when a match is required and nullable `query*` methods when absence is part of the assertion.\n\n```ts\nimport { within } from '@vielzeug/assay';\n\nconst view = within(panel.shadowRoot!);\n\nconst save = view.get<HTMLButtonElement>('button.save');\nconst status = view.queryByText('Saved');\n\nexpect(view.query('.error')).toBeNull();\nexpect(view.getByTestId('summary').textContent).toContain('Complete');\n```\n\n`get()`, `getByText()`, and `getByTestId()` throw `AssayQueryError` with the lookup and a bounded rendering of the\nscoped DOM. This keeps a failed required lookup diagnosable without non-null assertions.\n\nUse the cross-boundary helpers for custom elements:\n\n```ts\nimport { getSlotted, queryAllInShadow, queryPart } from '@vielzeug/assay';\n\nconst trigger = queryPart(menu, 'trigger');\nconst options = queryAllInShadow(menu, '[role=\"option\"]');\nconst footerActions = getSlotted(dialog, 'footer');\n```\n\n## Exact event dispatch\n\nAssay dispatches the platform event class named by each helper. It does not model browser activation, focus\nmanagement, or form defaults. `fireFocus()` and `fireBlur()` use their non-bubbling platform defaults; use\n`focusin`/`focusout` events when testing delegated focus listeners. Use `element.click()` when native activation is\nthe behavior under test; use Assay when testing an event listener or a controlled state transition.\n\n```ts\nimport { fireClick, fireCustom, fireInput, fireKeyDown } from '@vielzeug/assay';\n\ninput.value = 'Ada';\nfireInput(input);\n\nfireKeyDown(input, { key: 'Enter' });\nfireClick(saveButton);\nfireCustom(panel, { detail: { value: 42 }, type: 'value-change' });\n```\n\nEvery helper returns `dispatchEvent()`'s boolean result. `dispatch(target, event)` is available when an existing\nevent instance is the clearest expression of the test.\n\n## Waiting\n\nChoose the waiting primitive by the test's assertion shape:\n\n```ts\nimport { delay, retry, waitForEvent, waitUntil } from '@vielzeug/assay';\n\nawait waitUntil(() => panel.querySelector('.status')?.textContent === 'Ready');\n\nawait retry(() => {\n expect(onSave).toHaveBeenCalledOnce();\n});\n\nconst completed = waitForEvent<CustomEvent<{ id: string }>>(panel, 'save-complete', {\n signal: AbortSignal.timeout(1000),\n});\nfireClick(saveButton);\nexpect((await completed).detail.id).toBeDefined();\n\nawait delay(100); // real timer dependency, such as a debounce\n```\n\n`waitUntil()` retries only a boolean predicate. `retry()` retries only a callback that throws until it succeeds.\nBoth, and `waitForEvent()`, accept `timeout` and `signal`; `waitUntil()` and `retry()` also accept `interval`.\nTimeouts reject with `AssayTimeoutError`; aborts reject with the signal's reason.\n\n`nextTick()` resolves after one microtask. Prefer it for microtask-scheduled reactive work over a timer delay.\n\n## Testing custom elements\n\nUse Ore for component mounting and Assay for generic DOM concerns:\n\n```ts\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { mount } from '@vielzeug/ore/testing';\n\nconst fixture = await mount(\n () => html`\n <button @click=${onSave}>Save</button>\n `,\n);\n\nfireClick(fixture.get('button'));\nawait fixture.flush();\n\nexpect(onSave).toHaveBeenCalledOnce();\n```\n\nRefine's testing entry point contains only Refine-specific assertions and typed mount wrappers. Import Assay helpers\ndirectly instead of routing generic DOM operations through another package.\n\n## Best Practices\n\n- Scope multiple assertions with `within()` rather than repeatedly querying the document.\n- Prefer `get*` for required controls and `query*` for intentional absence checks.\n- Make form state and event boundaries explicit: assign `.value`, then call `fireInput()` or `fireChange()`.\n- Use browser integration tests for focus, disabled activation, pointer capture, and other browser-default behavior.\n- Use `waitForEvent()` for an emitted event, `waitUntil()` for a condition, and `retry()` for assertions.\n",
|
|
7
7
|
"examples": "---\ntitle: Assay — Examples\ndescription: Practical examples and recipes for assay.\n---\n\n## Examples\n\n- [Custom Element Interaction](./examples/custom-element-interaction.md)\n- [Waiting for Async Updates](./examples/waiting-for-async-updates.md)\n"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { ClockworkError } from './errors.js';\nexport { defineMachine } from './interpret.js';\nexport type {\n Actor,\n ActorErrorContext,\n ActorErrorDisposition,\n ActorOptions,\n After,\n Effect,\n EffectArgs,\n EventByType,\n EventType,\n Guard,\n Invoke,\n InvokeArgs,\n Machine,\n MachineConfig,\n MachineEvent,\n MachineSnapshot,\n Reducer,\n StateNode,\n Transition,\n TransitionInput,\n TransitionResult,\n} from './types.js';\nexport type { ClockworkErrorCode } from './errors.js';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Clockwork — Typed finite state machines for TypeScript\ndescription: Framework-neutral typed state machines with pure transitions, actor-owned runtime work, timers, invokes, and explicit effects.\npackage: clockwork\ncategory: state\nkeywords: [state-machine, finite-state, typed, actor, async-tasks]\nrelated: [herald, ripple, ward]\nexports: [defineMachine, ClockworkError, Machine, Actor, MachineConfig, MachineSnapshot, TransitionResult]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"clockwork\" />\n\n## Why Clockwork?\n\nApplication workflows often mix state changes with timers, requests, rendering, and cleanup. Clockwork keeps transition logic pure while each disposable actor owns runtime work. You can test state decisions without starting effects or invokes.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\n// Before\nif (status === 'idle') status = 'loading';\nfetchItems().then((items) => {\n status = 'ready';\n data = items;\n});\n\n// After\ntype Event = { type: 'FETCH' } | { items: string[]; type: 'DONE' };\nconst machine = defineMachine<{ items: string[] }, Event>()({\n context: { items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [{\n src: ({ signal }) => fetch('/api/items', { signal }).then((response) => response.json() as Promise<string[]>),\n onDone: ({ result }) => ({ items: result, type: 'DONE' }),\n }],\n on: { DONE: { reduce: ({ event }) => ({ items: event.items }), target: 'ready' } },\n },\n ready: {},\n },\n});\n```\n\n| Feature | Clockwork | XState | Zustand |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"clockwork\" type=\"size\" /> | Larger actor/statechart runtime | Smaller store runtime |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Pure transition API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Statechart-focused | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> Actor disposal | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework coupling | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None |\n\n<div class=\"decision-callout\">\n\n**Use Clockwork when** your feature has explicit workflow states, cancellable work, or effects that must run after a state commit.\n\n**Consider XState when** you need statecharts, visual tooling, or its broader actor ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/clockwork\n```\n\n```sh [npm]\nnpm install @vielzeug/clockwork\n```\n\n```sh [yarn]\nyarn add @vielzeug/clockwork\n```\n\n:::\n\n## Quick Start\n\nDefine the context and event union, create an actor, observe its snapshot, then dispose it when its owner ends.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'DEC' } | { type: 'INC' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n },\n },\n },\n});\n\nusing actor = counter.createActor();\nactor.subscribe((snapshot) => console.log(snapshot));\nactor.send({ type: 'INC' });\n// { context: { count: 1 }, state: 'idle' }\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`defineMachine()`** — validates and compiles one flat machine definition.\n- **`machine.transition()`** — evaluates a transition without actor runtime work.\n- **`machine.createActor()`** — creates isolated, disposable runtime ownership.\n- **`reduce`** — returns a replacement context from a transition.\n- **`effects`** — run only after the actor commits and notifies subscribers.\n- **`invoke`** — runs cancellable asynchronous work on state entry.\n- **`after`** — schedules cancellable delayed transitions.\n- **`actor.snapshot`** — exposes the current readonly state/context value.\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/) — publish events between independent actors without coupling machine definitions.\n- [Ripple](/ripple/) — bridge actor snapshots into a reactive graph when you need fine-grained rendering.\n- [Ward](/ward/) — call authorization predicates from transition guards.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Clockwork — Typed finite state machines for TypeScript\ndescription: Framework-neutral typed state machines with pure transitions, actor-owned runtime work, timers, invokes, and explicit effects.\npackage: clockwork\ncategory: state\nkeywords: [state-machine, finite-state, typed, actor, async-tasks]\nrelated: [herald, ripple, ward]\nexports: [defineMachine, ClockworkError, Machine, Actor, MachineConfig, MachineSnapshot, TransitionResult]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"clockwork\" />\n\n## Why Clockwork?\n\nApplication workflows often mix state changes with timers, requests, rendering, and cleanup. Clockwork keeps transition logic pure while each disposable actor owns runtime work. You can test state decisions without starting effects or invokes.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\n// Before\nif (status === 'idle') status = 'loading';\nfetchItems().then((items) => {\n status = 'ready';\n data = items;\n});\n\n// After\ntype Event = { type: 'FETCH' } | { items: string[]; type: 'DONE' };\nconst machine = defineMachine<{ items: string[] }, Event>()({\n context: { items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [{\n src: ({ signal }) => fetch('/api/items', { signal }).then((response) => response.json() as Promise<string[]>),\n onDone: ({ result }) => ({ items: result, type: 'DONE' }),\n }],\n on: { DONE: { reduce: ({ event }) => ({ items: event.items }), target: 'ready' } },\n },\n ready: {},\n },\n});\n```\n\n| Feature | Clockwork | XState | Zustand |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"clockwork\" type=\"size\" /> | Larger actor/statechart runtime | Smaller store runtime |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Pure transition API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Statechart-focused | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> Actor disposal | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework coupling | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None |\n\n<div class=\"decision-callout\">\n\n**Use Clockwork when** your feature has explicit workflow states, cancellable work, or effects that must run after a state commit.\n\n**Consider XState when** you need statecharts, visual tooling, or its broader actor ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/clockwork\n```\n\n```sh [npm]\nnpm install @vielzeug/clockwork\n```\n\n```sh [yarn]\nyarn add @vielzeug/clockwork\n```\n\n:::\n\n## Quick Start\n\nDefine the context and event union, create an actor, observe its snapshot, then dispose it when its owner ends.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'DEC' } | { type: 'INC' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n },\n },\n },\n});\n\nusing actor = counter.createActor();\nactor.subscribe((snapshot) => console.log(snapshot));\nactor.send({ type: 'INC' });\n// { context: { count: 1 }, state: 'idle' }\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`defineMachine()`** — validates and compiles one flat machine definition.\n- **`machine.transition()`** — evaluates a transition without actor runtime work.\n- **`machine.createActor()`** — creates isolated, disposable runtime ownership.\n- **`reduce`** — returns a replacement context from a transition.\n- **`effects`** — run only after the actor commits and notifies subscribers.\n- **`invoke`** — runs cancellable asynchronous work on state entry.\n- **`after`** — schedules cancellable delayed transitions.\n- **`actor.snapshot`** — exposes the current readonly state/context value.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Herald](/herald/) — publish events between independent actors without coupling machine definitions.\n- [Ripple](/ripple/) — bridge actor snapshots into a reactive graph when you need fine-grained rendering.\n- [Ward](/ward/) — call authorization predicates from transition guards.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Clockwork — API Reference\ndescription: Reference for Clockwork machine definitions, actors, devtools, and types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `defineMachine()` | Compile a typed flat machine definition | Sync | Call the generic factory before supplying the definition |\n| `Machine.transition()` | Resolve a pure next snapshot | Sync | Does not run effects, invokes, or timers |\n| `Machine.createActor()` | Create a runtime owner | Sync | Fresh and restored actors have different entry behavior |\n| `Actor.send()` | Dispatch an event | Sync | Returns `void`; re-entrant events queue internally |\n| `debugActor()` | Observe committed snapshots | Sync | Observes only; it does not trace sends or errors |\n| `ClockworkError` | Report definition and snapshot validation failures | Sync | Use `code`, not message text |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/clockwork` | Machine compiler, actor runtime, errors, and types |\n| `@vielzeug/clockwork/devtools` | Opt-in snapshot observation through `debugActor()` |\n\n## Core Functions\n\n### `defineMachine()`\n\n```ts\nfunction defineMachine<\n Context extends Record<string, unknown> = Record<string, never>,\n Event extends MachineEvent = MachineEvent,\n>(): <State extends string>(definition: MachineConfig<State, Context, Event>) => Machine<State, Context, Event>;\n```\n\nReturns a factory that validates and compiles a typed flat machine definition. Context must be a non-array record. Omit `context` only when the context type has no keys.\n\n**Returns:** A definition function that returns `Machine`.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'START' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'idle',\n states: { idle: { on: { START: { target: 'running' } } }, running: {} },\n});\n```\n\nThrows `ClockworkError` when a definition has an invalid context, initial state, target, transition, effect, invoke, or timer delay.\n\n---\n\n### `debugActor()`\n\n```ts\nfunction debugActor<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent>(\n actor: Actor<State, Context, Event>,\n options?: DebugActorOptions<State, Context>,\n): () => void;\n```\n\nSubscribes to committed actor snapshots and logs each one with `console.debug` by default. It does not modify actor behavior and does not observe dispatched events or runtime errors.\n\n**Returns:** An unsubscribe cleanup function.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\nimport { debugActor } from '@vielzeug/clockwork/devtools';\n\nconst machine = defineMachine<Record<string, never>, { type: 'NEXT' }>()({\n initial: 'idle',\n states: { idle: { on: { NEXT: { target: 'idle' } } } },\n});\n\nconst actor = machine.createActor();\nconst stopDebugging = debugActor(actor);\nactor.send({ type: 'NEXT' });\nstopDebugging();\nactor.dispose();\n```\n\n## Machine Methods\n\n### `machine.transition()`\n\n```ts\ntransition(\n snapshot: MachineSnapshot<State, Context>,\n event: Event,\n): TransitionResult<State, Context>;\n```\n\nResolves a snapshot for one user event without actor runtime work.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `snapshot` | `MachineSnapshot<State, Context>` | Input state and context |\n| `event` | `Event` | User event to evaluate |\n\n**Returns:** A `TransitionResult` with `transition` or `ignored` type.\n\n**Example:**\n\n```ts\nconst result = machine.transition(machine.initialSnapshot, { type: 'START' });\n```\n\n---\n\n### `machine.can()`\n\n```ts\ncan(snapshot: MachineSnapshot<State, Context>, event: Event): boolean;\n```\n\nReturns whether a transition exists and its guard passes.\n\n**Returns:** `true` when the supplied snapshot accepts the event.\n\n---\n\n### `machine.createActor()`\n\n```ts\ncreateActor(options?: ActorOptions<State, Context, Event>): Actor<State, Context, Event>;\n```\n\nCreates an independent actor for event dispatch, timers, invokes, effects, subscriptions, and disposal. A fresh actor starts the initial state's entry effects and resources. An actor restored with `options.snapshot` starts only the restored state's resources: invokes and timers, not entry effects.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.snapshot` | `MachineSnapshot<State, Context>` | Optional restored actor snapshot |\n| `options.maxTransitions` | `number` | Positive queued-transition limit for one synchronous flush |\n| `options.onError` | `(error, context) => 'continue' \\| 'dispose'` | Explicit disposition for runtime failures |\n\n**Returns:** Disposable `Actor`.\n\n**Example:**\n\n```ts\nconst actor = machine.createActor({\n onError(error, { phase, state }) {\n console.error(phase, state, error);\n return 'continue';\n },\n snapshot: { context: {}, state: 'idle' },\n});\n```\n\n## Actor Methods\n\n### `actor.send()`\n\n```ts\nsend(event: Event): void;\n```\n\nDispatches a user event to the current actor state. Events sent while the actor is processing queue and flush synchronously; sends to a disposed actor are ignored. Use `actor.snapshot` after sending to read the current snapshot.\n\n**Returns:** Nothing.\n\n---\n\n### `actor.can()`\n\n```ts\ncan(event: Event): boolean;\n```\n\nReturns whether the current actor snapshot accepts an event. Returns `false` after disposal.\n\n**Returns:** Boolean transition availability.\n\n---\n\n### `actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nRegisters a listener for committed snapshots. The listener does not run immediately.\n\n**Returns:** An unsubscribe function.\n\n---\n\n### `actor.dispose()`\n\n```ts\ndispose(): void;\n[Symbol.dispose](): void;\n```\n\nCancels timers and invokes, clears queued events and listeners, and aborts `disposalSignal`.\n\n**Returns:** Nothing. Idempotent.\n\n## Types\n\n### `MachineEvent`\n\n```ts\ntype MachineEvent = { readonly type: string };\n```\n\nBase constraint for event unions.\n\n### `EventType<Event>` and `EventByType<Event, Type>`\n\n```ts\ntype EventType<Event extends MachineEvent> = Event['type'] & string;\n\ntype EventByType<Event extends MachineEvent, Type extends EventType<Event>> =\n Extract<Event, { type: Type }>;\n```\n\nExtract event type names and a matching event from an event union.\n\n### `MachineSnapshot<State, Context>`\n\n```ts\ntype MachineSnapshot<State extends string, Context extends Record<string, unknown>> = {\n readonly context: Readonly<Context>;\n readonly state: State;\n};\n```\n\nThe plain readonly snapshot value used by machines and actors. Readonly is a TypeScript contract; Clockwork does not copy or freeze snapshots at runtime.\n\n### `Guard<Context, Event>` and `Reducer<Context, Event>`\n\n```ts\ntype Guard<Context extends Record<string, unknown>, Event> = (args: {\n readonly context: Readonly<Context>;\n readonly event: Event;\n}) => boolean;\n\ntype Reducer<Context extends Record<string, unknown>, Event> = (args: {\n readonly context: Readonly<Context>;\n readonly event: Event;\n}) => Context;\n```\n\nA guard selects a transition. A reducer returns replacement context, which must be a non-array record.\n\n### `EffectArgs<Context, Event>` and `Effect<Context, Event>`\n\n```ts\ntype EffectArgs<Context extends Record<string, unknown>, Event extends MachineEvent> = {\n readonly context: Readonly<Context>;\n readonly event: Event | undefined;\n readonly send: (event: Event) => void;\n readonly signal: AbortSignal;\n};\n\ntype Effect<Context extends Record<string, unknown>, Event extends MachineEvent> =\n (args: EffectArgs<Context, Event>) => void;\n```\n\nPost-commit effects receive `undefined` for initial entry and actor timer transitions. They cannot update machine context directly.\n\n### `Transition<State, Context, Event, Type>` and `TransitionInput`\n\n```ts\ntype Transition<\n State extends string,\n Context extends Record<string, unknown>,\n Event extends MachineEvent,\n Type extends EventType<Event> = EventType<Event>,\n> = {\n readonly effects?: readonly Effect<Context, Event>[];\n readonly guard?: Guard<Context, EventByType<Event, Type>>;\n readonly reduce?: Reducer<Context, EventByType<Event, Type>>;\n readonly target: State;\n};\n\ntype TransitionInput<\n State extends string,\n Context extends Record<string, unknown>,\n Event extends MachineEvent,\n Type extends EventType<Event> = EventType<Event>,\n> = Transition<State, Context, Event, Type> | readonly Transition<State, Context, Event, Type>[];\n```\n\nAn ordered transition array selects the first guard that passes.\n\n### `After<State, Context, Event>`\n\n```ts\ntype After<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n readonly delay: number;\n readonly effects?: readonly Effect<Context, Event>[];\n readonly guard?: Guard<Context, Event | undefined>;\n readonly reduce?: Reducer<Context, Event | undefined>;\n readonly target: State;\n};\n```\n\nA delayed state transition. Its guard and reducer receive `event: undefined`.\n\n### `InvokeArgs<Context, Event>` and `Invoke<Context, Event, Result>`\n\n```ts\ntype InvokeArgs<Context extends Record<string, unknown>, Event extends MachineEvent> = {\n readonly context: Readonly<Context>;\n readonly event: Event | undefined;\n readonly signal: AbortSignal;\n};\n\ntype Invoke<Context extends Record<string, unknown>, Event extends MachineEvent, Result = unknown> = {\n readonly onDone?: (args: { readonly context: Readonly<Context>; readonly result: Result }) => Event;\n readonly onError?: (args: { readonly context: Readonly<Context>; readonly error: unknown }) => Event;\n readonly src: (args: InvokeArgs<Context, Event>) => Promise<Result> | Result;\n};\n```\n\nAn actor-owned task started on state entry. `event` is the triggering event or `undefined` for initial or restored resources.\n\n### `StateNode<State, Context, Event>` and `MachineConfig<State, Context, Event>`\n\n```ts\ntype StateNode<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n readonly after?: readonly After<State, Context, Event>[];\n readonly entry?: readonly Effect<Context, Event>[];\n readonly exit?: readonly Effect<Context, Event>[];\n readonly invoke?: readonly Invoke<Context, Event>[];\n readonly on?: Partial<{ [Type in EventType<Event>]: TransitionInput<State, Context, Event, Type> }>;\n};\n\ntype MachineConfig<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> =\n (keyof Context extends never ? { readonly context?: Context } : { readonly context: Context }) & {\n readonly initial: State;\n readonly states: Record<State, StateNode<State, Context, Event>>;\n };\n```\n\nA flat machine definition. State nodes cannot contain child states.\n\n### `TransitionResult<State, Context>`\n\n```ts\ntype TransitionResult<State extends string, Context extends Record<string, unknown>> = {\n readonly snapshot: MachineSnapshot<State, Context>;\n readonly type: 'ignored' | 'transition';\n};\n```\n\nResult of a pure user-event transition. It contains no effect plan.\n\n### `ActorErrorContext<State, Event>`, `ActorErrorDisposition`, and `ActorOptions<State, Context, Event>`\n\n```ts\ntype ActorErrorContext<State extends string, Event extends MachineEvent> = {\n readonly event?: Event;\n readonly phase: 'effect' | 'invoke' | 'subscriber' | 'transition';\n readonly state: State;\n};\n\ntype ActorErrorDisposition = 'continue' | 'dispose';\n\ntype ActorOptions<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n readonly maxTransitions?: number;\n readonly onError?: (error: unknown, context: ActorErrorContext<State, Event>) => ActorErrorDisposition;\n readonly snapshot?: MachineSnapshot<State, Context>;\n};\n```\n\n`onError` must explicitly return `'continue'` to keep the actor alive or `'dispose'` to end it. Without an error handler, Clockwork disposes the actor silently.\n\n### `Actor<State, Context, Event>`\n\n```ts\ntype Actor<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n [Symbol.dispose](): void;\n can(event: Event): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n send(event: Event): void;\n readonly snapshot: MachineSnapshot<State, Context>;\n subscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n};\n```\n\nAn actor's `snapshot` is the current plain readonly snapshot.\n\n### `Machine<State, Context, Event>`\n\n```ts\ntype Machine<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n can(snapshot: MachineSnapshot<State, Context>, event: Event): boolean;\n createActor(options?: ActorOptions<State, Context, Event>): Actor<State, Context, Event>;\n readonly initialSnapshot: MachineSnapshot<State, Context>;\n transition(snapshot: MachineSnapshot<State, Context>, event: Event): TransitionResult<State, Context>;\n};\n```\n\nA compiled, reusable machine. Its transition lookup is map-based, so unknown or poison event names such as `__proto__` are safely ignored when no transition exists.\n\n### `DebugActorOptions<State, Context>`\n\n```ts\ntype DebugActorOptions<State extends string, Context extends Record<string, unknown>> = {\n readonly logger?: (snapshot: MachineSnapshot<State, Context>) => void;\n};\n```\n\nOptional logger for `debugActor()`. Logger failures are ignored so observation cannot affect the actor's error policy.\n\n## Errors\n\n### `ClockworkError`\n\n`ClockworkError` reports invalid definitions, contexts, snapshots, and actor transition limits. It has `code`, `details`, and standard `Error` fields. Use `ClockworkError.is(error)` to narrow an unknown error.\n\n```ts\nif (ClockworkError.is(error)) {\n console.error(error.code, error.details);\n}\n```\n",
|
|
6
|
-
"usage": "---\ntitle: Clockwork — Usage Guide\ndescription: Build deterministic state machines with pure transitions and actor-owned runtime work.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine a non-array record context and an event union before supplying the flat definition. Create one actor for each independently owned workflow.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'TOGGLE' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'on',\n states: {\n off: { on: { TOGGLE: { target: 'on' } } },\n on: { on: { TOGGLE: { target: 'off' } } },\n },\n});\n\nconst actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\nconsole.log(actor.snapshot.state); // 'off'\nactor.dispose();\n```\n\nDispose actors when a feature, request, or test ends. You can use `using` when the surrounding runtime supports `Symbol.dispose`.\n\n```ts\nusing actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\n```\n\n## Context reducers\n\nA reducer receives readonly context and returns the next context. Clockwork does not copy or freeze context at runtime, so do not mutate data that other code may retain.\n\n```ts\ntype Event = { type: 'DEC' } | { type: 'INC' } | { type: 'RESET' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n RESET: { reduce: () => ({ count: 0 }), target: 'idle' },\n },\n },\n },\n});\n```\n\nKeep reducers pure. Make nested copies yourself when nested data changes.\n\n```ts\nSAVE: {\n reduce: ({ context, event }) => ({\n ...context,\n profile: { ...context.profile, name: event.name },\n }),\n target: 'editing',\n}\n```\n\n## Guards\n\nGuards decide whether a transition can run. They receive readonly context and the matching event. For several choices, use an ordered array; the first passing guard wins.\n\n```ts\nPAY: [\n {\n guard: ({ context }) => context.balance >= context.total,\n reduce: ({ context }) => ({ ...context, balance: context.balance - context.total }),\n target: 'success',\n },\n { target: 'insufficientFunds' },\n]\n```\n\nCall `actor.can(event)` for the current actor snapshot or `machine.can(snapshot, event)` for an arbitrary snapshot.\n\n## Pure transitions\n\n`machine.transition()` enables isolated unit tests and decision UIs. It returns the unchanged snapshot with `type: 'ignored'` when no transition matches; it does not expose or run effects.\n\n```ts\nconst result = counter.transition(\n { context: { count: 3 }, state: 'idle' },\n { type: 'INC' },\n);\n\nif (result.type === 'transition') {\n console.log(result.snapshot.context.count); // 4\n}\n```\n\n## Effects\n\nEntry, exit, and transition effects run only through an actor. The actor commits, establishes the new state's timers and invokes, notifies subscribers, then runs exit, transition, and entry effects. Effects cannot change context; send a regular event for another state change.\n\n```ts\ntype WorkflowEvent = { type: 'SUBMIT' };\nconst workflow = defineMachine<{ orderId: string }, WorkflowEvent>()({\n context: { orderId: '' },\n initial: 'draft',\n states: {\n draft: {\n on: {\n SUBMIT: {\n effects: [({ context }) => console.debug('submitted', context)],\n target: 'submitted',\n },\n },\n },\n submitted: { entry: [({ context }) => console.log(`Submitted ${context.orderId}`)] },\n },\n});\n```\n\nEffects receive `context`, the triggering `event` (or `undefined` for initial entry), actor `send`, and the actor lifetime `signal`.\n\n## Async invokes\n\nInvokes start on state entry. `src` gets readonly entry context, the triggering event or `undefined`, and an `AbortSignal`. `onDone` or `onError` map settlement to ordinary events. All invokes are cancelled when the actor exits the state or disposes.\n\n```ts\ntype LoadEvent =\n | { type: 'FETCH' }\n | { items: string[]; type: 'SUCCESS' }\n | { message: string; type: 'FAILURE' }\n | { type: 'RETRY' };\n\nconst loader = defineMachine<{ error: string; items: string[] }, LoadEvent>()({\n context: { error: '', items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [{\n src: async ({ signal }) => {\n const response = await fetch('/api/items', { signal });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return response.json() as Promise<string[]>;\n },\n onDone: ({ result }) => ({ items: result, type: 'SUCCESS' }),\n onError: ({ error }) => ({ message: String(error), type: 'FAILURE' }),\n }],\n on: {\n FAILURE: { reduce: ({ event }) => ({ error: event.message, items: [] }), target: 'error' },\n SUCCESS: { reduce: ({ event }) => ({ error: '', items: event.items }), target: 'ready' },\n },\n },\n ready: {},\n error: { on: { RETRY: { target: 'loading' } } },\n },\n});\n```\n\n## Delayed transitions\n\n`after` starts timers on state entry and cancels them on exit or disposal. Its guard and reducer receive `event: undefined`; a user event with `type: '$after'` remains a normal user event.\n\n```ts\ntype NotificationEvent = { type: 'DISMISS' } | { message: string; type: 'SHOW' };\nconst notification = defineMachine<{ message: string }, NotificationEvent>()({\n context: { message: '' },\n initial: 'hidden',\n states: {\n hidden: { on: { SHOW: { reduce: ({ event }) => ({ message: event.message }), target: 'visible' } } },\n visible: {\n after: [{ delay: 5_000, target: 'hidden' }],\n on: { DISMISS: { target: 'hidden' } },\n },\n },\n});\n```\n\n## Snapshot observation and persistence\n\n`actor.snapshot` is the current plain readonly snapshot; read it directly rather than calling a snapshot method. Use `subscribe()` to integrate a state library or persist future committed snapshots. Fresh actors run their initial entry effects and resources; restored actors start only the restored state's invokes and timers, not its entry effects.\n\n```ts\nconst stored = sessionStorage.getItem('wizard');\nconst actor = machine.createActor({\n snapshot: stored ? JSON.parse(stored) : undefined,\n});\n\nconst stopSaving = actor.subscribe((snapshot) => {\n sessionStorage.setItem('wizard', JSON.stringify(snapshot));\n});\n\nconsole.log(actor.snapshot);\nstopSaving();\nactor.dispose();\n```\n\nValidate untrusted persisted data before passing it to `createActor()`. Clockwork validates the restored state name but cannot validate application-specific context fields.\n\n## Error handling\n\nUse `onError` to choose what happens after failures from transitions, effects, invokes, or subscribers. The context identifies the runtime phase and state; an event is present when one triggered the failure. Return `'continue'` to keep the actor alive or `'dispose'` to end it.\n\n```ts\nconst actor = machine.createActor({\n onError(error, { event, phase, state }) {\n console.error({ error, event, phase, state });\n return 'continue';\n },\n});\n```\n\nWithout `onError`, an actor disposes silently. Return `'dispose'` explicitly when an error handler logs an unrecoverable failure.\n\n## Debugging\n\nUse opt-in snapshot logging during development. `debugActor()` observes committed snapshots only; it does not trace dispatched events or runtime errors.\n\n```ts\nimport { debugActor } from '@vielzeug/clockwork/devtools';\n\nconst actor = machine.createActor();\nconst stopDebugging = debugActor(actor);\nactor.send({ type: 'NEXT' });\nstopDebugging();\nactor.dispose();\n```\n\nFor richer inspection, subscribe to snapshots and record them in application devtools. Clockwork intentionally has no internal trace buffer.\n\n## Flat state maps\n\nClockwork has flat state IDs. Prefer explicit states such as `editingDraft` and `editingSaving`, or compose several actors when domains have independent lifecycles.\n\n## SSR\n\nReuse a compiled machine definition, but create and dispose an actor per request. Never share an actor across concurrent requests.\n\n## Testing\n\nTest deterministic state behavior through `machine.transition()`. Create actors only for timers, invokes, effects, queueing, subscriptions, or disposal behavior.\n\n```ts\nimport { expect, test } from 'vitest';\n\ntest('increments without an actor', () => {\n const result = counter.transition(\n { context: { count: 2 }, state: 'idle' },\n { type: 'INC' },\n );\n\n expect(result).toMatchObject({\n snapshot: { context: { count: 3 }, state: 'idle' },\n type: 'transition',\n });\n});\n```\n\n## Framework Integration\n\nBridge the current actor snapshot into renderer state through one subscription. Dispose that subscription with component lifecycle.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nfunction useActor<Snapshot>(actor: { readonly snapshot: Snapshot; subscribe(listener: (snapshot: Snapshot) => void): () => void }) {\n return useSyncExternalStore(\n (notify) => actor.subscribe(() => notify()),\n () => actor.snapshot,\n );\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nconst snapshot = shallowRef(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nonUnmounted(stop);\n```\n\n```ts [Svelte]\nimport { onDestroy } from 'svelte';\n\nlet snapshot = actor.snapshot;\nconst stop = actor.subscribe((next) => (snapshot = next));\nonDestroy(stop);\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Herald when separate actors exchange application events. Bridge Clockwork snapshots into Ripple only at a UI or application boundary.\n\n```ts\nimport { createEventBus } from '@vielzeug/herald';\n\nconst bus = createEventBus<{ type: 'REFRESH' }>();\nbus.on('REFRESH', () => actor.send({ type: 'FETCH' }));\n```\n\n## Best Practices\n\n- Define context and event unions with `defineMachine<Context, Event>()`.\n- Return replacement context from reducers; do not rely on runtime copying or freezing.\n- Keep guards and reducers pure.\n- Use actors for effects, timers, invokes, subscriptions, and cancellation.\n- Read the current snapshot from `actor.snapshot`, not a wrapper value.\n- Validate persisted context before restoring a snapshot.\n- Dispose every actor at its ownership boundary.\n- Route runtime failures through `onError` when the owner can recover.\n",
|
|
6
|
+
"usage": "---\ntitle: Clockwork — Usage Guide\ndescription: Build deterministic state machines with pure transitions and actor-owned runtime work.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine a non-array record context and an event union before supplying the flat definition. Create one actor for each independently owned workflow.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'TOGGLE' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'on',\n states: {\n off: { on: { TOGGLE: { target: 'on' } } },\n on: { on: { TOGGLE: { target: 'off' } } },\n },\n});\n\nconst actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\nconsole.log(actor.snapshot.state); // 'off'\nactor.dispose();\n```\n\nDispose actors when a feature, request, or test ends. You can use `using` when the surrounding runtime supports `Symbol.dispose`.\n\n```ts\nusing actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\n```\n\n## Context reducers\n\nA reducer receives readonly context and returns the next context. Clockwork does not copy or freeze context at runtime, so do not mutate data that other code may retain.\n\n```ts\ntype Event = { type: 'DEC' } | { type: 'INC' } | { type: 'RESET' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n RESET: { reduce: () => ({ count: 0 }), target: 'idle' },\n },\n },\n },\n});\n```\n\nKeep reducers pure. Make nested copies yourself when nested data changes.\n\n```ts\nSAVE: {\n reduce: ({ context, event }) => ({\n ...context,\n profile: { ...context.profile, name: event.name },\n }),\n target: 'editing',\n}\n```\n\n## Guards\n\nGuards decide whether a transition can run. They receive readonly context and the matching event. For several choices, use an ordered array; the first passing guard wins.\n\n```ts\nPAY: [\n {\n guard: ({ context }) => context.balance >= context.total,\n reduce: ({ context }) => ({ ...context, balance: context.balance - context.total }),\n target: 'success',\n },\n { target: 'insufficientFunds' },\n]\n```\n\nCall `actor.can(event)` for the current actor snapshot or `machine.can(snapshot, event)` for an arbitrary snapshot.\n\n## Pure transitions\n\n`machine.transition()` enables isolated unit tests and decision UIs. It returns the unchanged snapshot with `type: 'ignored'` when no transition matches; it does not expose or run effects.\n\n```ts\nconst result = counter.transition(\n { context: { count: 3 }, state: 'idle' },\n { type: 'INC' },\n);\n\nif (result.type === 'transition') {\n console.log(result.snapshot.context.count); // 4\n}\n```\n\n## Effects\n\nEntry, exit, and transition effects run only through an actor. The actor commits, establishes the new state's timers and invokes, notifies subscribers, then runs exit, transition, and entry effects. Effects cannot change context; send a regular event for another state change.\n\n```ts\ntype WorkflowEvent = { type: 'SUBMIT' };\nconst workflow = defineMachine<{ orderId: string }, WorkflowEvent>()({\n context: { orderId: '' },\n initial: 'draft',\n states: {\n draft: {\n on: {\n SUBMIT: {\n effects: [({ context }) => console.debug('submitted', context)],\n target: 'submitted',\n },\n },\n },\n submitted: { entry: [({ context }) => console.log(`Submitted ${context.orderId}`)] },\n },\n});\n```\n\nEffects receive `context`, the triggering `event` (or `undefined` for initial entry), actor `send`, and the actor lifetime `signal`.\n\n## Async invokes\n\nInvokes start on state entry. `src` gets readonly entry context, the triggering event or `undefined`, and an `AbortSignal`. `onDone` or `onError` map settlement to ordinary events. All invokes are cancelled when the actor exits the state or disposes.\n\n```ts\ntype LoadEvent =\n | { type: 'FETCH' }\n | { items: string[]; type: 'SUCCESS' }\n | { message: string; type: 'FAILURE' }\n | { type: 'RETRY' };\n\nconst loader = defineMachine<{ error: string; items: string[] }, LoadEvent>()({\n context: { error: '', items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [{\n src: async ({ signal }) => {\n const response = await fetch('/api/items', { signal });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return response.json() as Promise<string[]>;\n },\n onDone: ({ result }) => ({ items: result, type: 'SUCCESS' }),\n onError: ({ error }) => ({ message: String(error), type: 'FAILURE' }),\n }],\n on: {\n FAILURE: { reduce: ({ event }) => ({ error: event.message, items: [] }), target: 'error' },\n SUCCESS: { reduce: ({ event }) => ({ error: '', items: event.items }), target: 'ready' },\n },\n },\n ready: {},\n error: { on: { RETRY: { target: 'loading' } } },\n },\n});\n```\n\n## Delayed transitions\n\n`after` starts timers on state entry and cancels them on exit or disposal. Its guard and reducer receive `event: undefined`; a user event with `type: '$after'` remains a normal user event.\n\n```ts\ntype NotificationEvent = { type: 'DISMISS' } | { message: string; type: 'SHOW' };\nconst notification = defineMachine<{ message: string }, NotificationEvent>()({\n context: { message: '' },\n initial: 'hidden',\n states: {\n hidden: { on: { SHOW: { reduce: ({ event }) => ({ message: event.message }), target: 'visible' } } },\n visible: {\n after: [{ delay: 5_000, target: 'hidden' }],\n on: { DISMISS: { target: 'hidden' } },\n },\n },\n});\n```\n\n## Snapshot observation and persistence\n\n`actor.snapshot` is the current plain readonly snapshot; read it directly rather than calling a snapshot method. Use `subscribe()` to integrate a state library or persist future committed snapshots. Fresh actors run their initial entry effects and resources; restored actors start only the restored state's invokes and timers, not its entry effects.\n\n```ts\nconst stored = sessionStorage.getItem('wizard');\nconst actor = machine.createActor({\n snapshot: stored ? JSON.parse(stored) : undefined,\n});\n\nconst stopSaving = actor.subscribe((snapshot) => {\n sessionStorage.setItem('wizard', JSON.stringify(snapshot));\n});\n\nconsole.log(actor.snapshot);\nstopSaving();\nactor.dispose();\n```\n\nValidate untrusted persisted data before passing it to `createActor()`. Clockwork validates the restored state name but cannot validate application-specific context fields.\n\n## Error handling\n\nUse `onError` to choose what happens after failures from transitions, effects, invokes, or subscribers. The context identifies the runtime phase and state; an event is present when one triggered the failure. Return `'continue'` to keep the actor alive or `'dispose'` to end it.\n\n```ts\nconst actor = machine.createActor({\n onError(error, { event, phase, state }) {\n console.error({ error, event, phase, state });\n return 'continue';\n },\n});\n```\n\nWithout `onError`, an actor disposes silently. Return `'dispose'` explicitly when an error handler logs an unrecoverable failure.\n\n## Debugging\n\nUse opt-in snapshot logging during development. `debugActor()` observes committed snapshots only; it does not trace dispatched events or runtime errors.\n\n```ts\nimport { debugActor } from '@vielzeug/clockwork/devtools';\n\nconst actor = machine.createActor();\nconst stopDebugging = debugActor(actor);\nactor.send({ type: 'NEXT' });\nstopDebugging();\nactor.dispose();\n```\n\nFor richer inspection, subscribe to snapshots and record them in application devtools. Clockwork intentionally has no internal trace buffer.\n\n## Flat state maps\n\nClockwork has flat state IDs. Prefer explicit states such as `editingDraft` and `editingSaving`, or compose several actors when domains have independent lifecycles.\n\n## SSR\n\nReuse a compiled machine definition, but create and dispose an actor per request. Never share an actor across concurrent requests.\n\n## Testing\n\nTest deterministic state behavior through `machine.transition()`. Create actors only for timers, invokes, effects, queueing, subscriptions, or disposal behavior.\n\n```ts\nimport { expect, test } from 'vitest';\n\ntest('increments without an actor', () => {\n const result = counter.transition(\n { context: { count: 2 }, state: 'idle' },\n { type: 'INC' },\n );\n\n expect(result).toMatchObject({\n snapshot: { context: { count: 3 }, state: 'idle' },\n type: 'transition',\n });\n});\n```\n\n## Framework Integration\n\nBridge the current actor snapshot into renderer state through one subscription. Dispose that subscription with component lifecycle.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nfunction useActor<Snapshot>(actor: { readonly snapshot: Snapshot; subscribe(listener: (snapshot: Snapshot) => void): () => void }) {\n return useSyncExternalStore(\n (notify) => actor.subscribe(() => notify()),\n () => actor.snapshot,\n );\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nconst snapshot = shallowRef(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nonUnmounted(stop);\n```\n\n```ts [Svelte]\nimport { onDestroy } from 'svelte';\n\nlet snapshot = actor.snapshot;\nconst stop = actor.subscribe((next) => (snapshot = next));\nonDestroy(stop);\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Herald when separate actors exchange application events. Bridge Clockwork snapshots into Ripple only at a UI or application boundary.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<{ REFRESH: void }>();\nbus.on('REFRESH', () => actor.send({ type: 'FETCH' }));\n```\n\n## Best Practices\n\n- Define context and event unions with `defineMachine<Context, Event>()`.\n- Return replacement context from reducers; do not rely on runtime copying or freezing.\n- Keep guards and reducers pure.\n- Use actors for effects, timers, invokes, subscriptions, and cancellation.\n- Read the current snapshot from `actor.snapshot`, not a wrapper value.\n- Validate persisted context before restoring a snapshot.\n- Dispose every actor at its ownership boundary.\n- Route runtime failures through `onError` when the owner can recover.\n",
|
|
7
7
|
"examples": "---\ntitle: Clockwork — Examples\ndescription: Practical state machine patterns with pure transitions and actors.\n---\n\n- [Counter with Reset](./examples/counter-with-reset.md)\n- [Form Validation](./examples/form-validation.md)\n- [Auto-Dismiss Notification](./examples/auto-dismiss-notification.md)\n- [Model Nested Workflows with Flat States](./examples/hierarchical-states.md)\n- [Pure Transition Testing](./examples/unit-testing.md)\n- [Auth Flow with Guards](./examples/auth-flow.md)\n- [Data Fetching with Error Recovery](./examples/data-fetching.md)\n- [Fetch with Retry](./examples/fetch-retry.md)\n- [Paginated Data Loading](./examples/paginated-data-loading.md)\n- [Media Player](./examples/media-player.md)\n- [Persisted Wizard](./examples/persisted-wizard.md)\n- [Multi-Step Wizard with Routing](./examples/wizard-with-routing.md)\n- [Shopping Cart Checkout](./examples/checkout.md)\n- [Permission-Based Access Control](./examples/permission-based-access.md)\n- [Event Boundaries](./examples/middleware-pipeline.md)\n- [Multi-Machine Coordination](./examples/multi-machine-coordination.md)\n- [Debugging Transitions](./examples/debugging-transitions.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
package/data/packages/codex.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { CatalogError, SnapshotCatalog, type Catalog, type SearchHit } from './catalog.js';\nexport { CodexError } from './errors.js';\nexport { startHttpHost, type HttpHost, type HttpHostOptions } from './http.js';\nexport { createMcpServer } from './server.js';\nexport {\n loadSnapshot,\n parseCatalog,\n parseContent,\n parseManifest,\n parsePointer,\n parseSearch,\n validateSnapshot,\n} from './snapshot.js';\nexport {\n DOC_PAGES,\n SNAPSHOT_SCHEMA_VERSION,\n type CemAttribute,\n type CemCssPart,\n type CemCssProperty,\n type CemDeclaration,\n type CemEvent,\n type CemMember,\n type CemSlot,\n type DocPage,\n type Example,\n type PackageContent,\n type PackageMeta,\n type SnapshotManifest,\n type SnapshotPointer,\n} from './types.js';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Codex\ndescription: Local MCP access to Vielzeug documentation and package metadata.\npackage: codex\ncategory: AI\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports: [loadSnapshot, SnapshotCatalog, createMcpServer, startHttpHost]\nenvironments: [node]\n---\n\n<PackageHero package=\"codex\" />\n\n## Why Codex?\n\nCodex exposes current Vielzeug catalog data through MCP without scanning source at request time.\n\n## Installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## Quick Start\n\n```sh\nnpx -y @vielzeug/codex\n```\n\n## Features\n\n- `loadSnapshot` validates chunked snapshot metadata.\n- `SnapshotCatalog` loads package content only when requested.\n- `createMcpServer` adapts catalog operations to MCP.\n\n## Documentation\n\n- [Usage](./usage.md)\n- [API](./api.md)\n- [Examples](./examples.md)\n\n## See Also\n\n- [Refine](../refine/) provides component metadata bundled by Codex.\n",
|
|
4
|
+
"index": "---\ntitle: Codex\ndescription: Local MCP access to Vielzeug documentation and package metadata.\npackage: codex\ncategory: AI\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports: [loadSnapshot, SnapshotCatalog, createMcpServer, startHttpHost]\nenvironments: [node]\n---\n\n<PackageHero package=\"codex\" />\n\n## Why Codex?\n\nCodex exposes current Vielzeug catalog data through MCP without scanning source at request time.\n\n## Installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## Quick Start\n\n```sh\nnpx -y @vielzeug/codex\n```\n\n## Features\n\n- `loadSnapshot` validates chunked snapshot metadata.\n- `SnapshotCatalog` loads package content only when requested.\n- `createMcpServer` adapts catalog operations to MCP.\n\n## Documentation\n\n- [Usage](./usage.md)\n- [API](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n## See Also\n\n- [Refine](../refine/) provides component metadata bundled by Codex.\n",
|
|
5
5
|
"api": "---\ntitle: Codex API\ndescription: Snapshot, catalog, MCP server, and local HTTP host APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `loadSnapshot` | Read validated snapshot metadata | Sync | Content chunks load lazily |\n| `SnapshotCatalog` | Query package corpus | Sync | Construct from loaded snapshot |\n| `createMcpServer` | MCP adapter factory | Sync | Requires catalog and version |\n| `startHttpHost` | Loopback Streamable HTTP host | Async | HTTP remains local-only |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/codex` | Snapshot, catalog, MCP, and HTTP APIs |\n\n## Snapshot\n\n### `loadSnapshot`\n\n```ts\nloadSnapshot(snapshotDirectory?: string): LoadedSnapshot;\n```\n\nLoads catalog/search metadata only. Use `validateSnapshot()` during generation, integration tests, or explicit artifact verification; package chunks stay lazy at runtime.\n\n### `SnapshotCatalog`\n\n```ts\nnew SnapshotCatalog(snapshot: LoadedSnapshot)\n```\n\nProvides package lookup, docs/source/example/signature access, deterministic search, and Refine component lookup.\n\n## MCP\n\n### `createMcpServer`\n\n```ts\ncreateMcpServer(catalog: Catalog, options: { version: string; debug?: boolean }): Server;\n```\n\nRegisters MCP tools as an adapter over `Catalog`.\n\n## HTTP\n\n### `startHttpHost`\n\n```ts\nstartHttpHost(options: HttpHostOptions): Promise<HttpHost>;\n```\n\nStarts Streamable HTTP on `127.0.0.1` by default. Host accepts only loopback addresses.\n\n## Types\n\n```ts\ninterface SnapshotPointer {\n directory: 'snapshots/<immutable-id>';\n}\n\n// Dev snapshots use SnapshotPointer; published snapshots are static directories.\ninterface SnapshotManifest {\n schemaVersion: 1;\n catalog: 'catalog.json';\n search: 'search.json';\n contentDirectory: 'packages';\n}\n```\n\n## Errors\n\n`CodexError` signals malformed snapshots or host failures. `CatalogError` adds `INVALID_ARG`, `NOT_FOUND`, or `UNAVAILABLE` for expected tool failures.\n",
|
|
6
6
|
"usage": "---\ntitle: Codex — Usage Guide\ndescription: Install, connect, develop, and debug the Vielzeug MCP server.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRun local stdio server:\n\n```sh\nnpx -y @vielzeug/codex\n```\n\nUse shipped `mcp-setup.json` for machine-readable generic configuration. Client-specific configuration must use its documented MCP format.\n\n## HTTP Mode\n\nHTTP uses Streamable HTTP and binds loopback only:\n\n```sh\nnpx -y @vielzeug/codex --port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nResponse includes snapshot version. No legacy SSE endpoint, CORS wildcard, or remote host mode exists.\n\n## Local Development\n\nRequires Node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## Debugging\n\n```sh\npnpm dev\nnode src/cli.ts --port=3100 --debug\ncurl http://127.0.0.1:3100/health\n```\n\n`--debug` logs tool durations and expected catalog errors to stderr. Build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## Programmatic Usage\n\n```ts\nimport { SnapshotCatalog, createMcpServer, loadSnapshot } from '@vielzeug/codex';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotCatalog(snapshot);\nawait createMcpServer(catalog, { version: snapshot.manifest.version }).connect(new StdioServerTransport());\n```\n\n## Best Practices\n\n- Use `search-packages` for capability discovery before loading broad source.\n- Use `get-type-signature` before loading full source.\n- Published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n- Run `validateSnapshot()` in artifact verification paths, not normal server startup.\n- Keep HTTP local. Use stdio for normal client integration.\n- Run `pnpm test:unit` before `pnpm test:integration`.\n",
|
|
7
7
|
"examples": "---\ntitle: Codex — Examples\ndescription: Practical MCP tool-call examples for package discovery, docs lookup, and Refine component queries.\n---\n\n## Examples\n\n- [Listing Packages](./examples/listing-packages.md)\n- [Searching Packages](./examples/searching-packages.md)\n- [Package Metadata](./examples/package-metadata.md)\n- [Reading Docs](./examples/reading-docs.md)\n- [Running REPL Examples](./examples/running-repl-examples.md)\n- [Looking Up Components](./examples/looking-up-components.md)\n- [Inspector](./examples/inspector.md)\n"
|
package/data/packages/coins.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { clamp, allocate, sum } from './aggregate';\nexport { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';\nexport { decimal } from './decimal';\nexport { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';\nexport type { CoinsErrorCode } from './errors';\nexport { exchange, exchangeRate } from './exchange';\nexport { format, formatParts } from './format';\nexport {\n abs,\n add,\n compare,\n divide,\n isMoney,\n money,\n multiply,\n negate,\n parseMoney,\n round,\n subtract,\n toDecimal,\n} from './money';\nexport { parseMoneyJSON, toJSON } from './serialization';\nexport type {\n Currency,\n CurrencyCode,\n Decimal,\n ExchangeRate,\n FormatOptions,\n Money,\n MoneyFormatPart,\n MoneyJSON,\n RoundingMode,\n} from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Coins — Exact Money for TypeScript\ndescription: Exact bigint monetary arithmetic with explicit currency definitions, decimal strings, allocation, exchange, formatting, and JSON boundaries.\npackage: coins\ncategory: finance\nkeywords: [money, currency, bigint, decimal, exchange, formatting]\nexports: [money, currency, add, allocate, exchange, format]\nrelated: [vault, courier, spell]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"coins\" />\n\n## Why Coins?\n\nCoins keeps monetary values in bigint minor units, but makes units explicit at construction. Currency scale comes from deterministic definitions; `Intl` formats a known value without deciding its arithmetic representation.\n\n```ts\n// Before\nconst total = (19.99 + 7.25) * 1.08;\n\n// After\nimport { USD, add, money, multiply } from '@vielzeug/coins';\n\nconst total = multiply(add(money('19.99', USD), money('7.25', USD)), '1.08');\n```\n\n| Feature | Coins | decimal.js | Dinero.js |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"coins\" type=\"size\" /> | External dependency | External dependency |\n| Bigint minor units | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit currency scale | <ore-icon name=\"check\" size=\"16\"></ore-icon> | App-defined | Partial |\n| Exact allocation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Zero dependencies | <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\n<div class=\"decision-callout\">\n\n**Use Coins when** application values represent real money and every rounding boundary must be visible.\n\n**Consider native numbers when** values are estimates, analytics, or display-only approximations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/coins\n```\n\n```sh [npm]\nnpm install @vielzeug/coins\n```\n\n```sh [yarn]\nyarn add @vielzeug/coins\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { USD, add, format, money, multiply } from '@vielzeug/coins';\n\nconst subtotal = add(money('12.50', USD), money('7.25', USD));\nconst total = multiply(subtotal, '1.08', { rounding: 'halfEven' });\n\nconsole.log(format(total));\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`money`**: one constructor for decimal and explicit minor-unit values\n- **`currency`**: deterministic built-in currency definitions\n- **`add`**: exact same-currency arithmetic\n- **`allocate`**: split every minor unit without loss\n- **`exchange`**: typed source and target currency conversion\n- **`format`**: locale presentation for bigint values\n- **`parseMoneyJSON`**: validate persisted money values\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- [Vault](/vault/) — persist validated money JSON.\n- [Courier](/courier/) — retrieve exchange-rate data.\n- [Spell](/spell/) — validate external monetary payloads.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Coins — Exact Money for TypeScript\ndescription: Exact bigint monetary arithmetic with explicit currency definitions, decimal strings, allocation, exchange, formatting, and JSON boundaries.\npackage: coins\ncategory: finance\nkeywords: [money, currency, bigint, decimal, exchange, formatting]\nexports: [money, currency, add, allocate, exchange, format]\nrelated: [vault, courier, spell]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"coins\" />\n\n## Why Coins?\n\nCoins keeps monetary values in bigint minor units, but makes units explicit at construction. Currency scale comes from deterministic definitions; `Intl` formats a known value without deciding its arithmetic representation.\n\n```ts\n// Before\nconst total = (19.99 + 7.25) * 1.08;\n\n// After\nimport { USD, add, money, multiply } from '@vielzeug/coins';\n\nconst total = multiply(add(money('19.99', USD), money('7.25', USD)), '1.08');\n```\n\n| Feature | Coins | decimal.js | Dinero.js |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"coins\" type=\"size\" /> | External dependency | External dependency |\n| Bigint minor units | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit currency scale | <ore-icon name=\"check\" size=\"16\"></ore-icon> | App-defined | Partial |\n| Exact allocation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Zero dependencies | <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\n<div class=\"decision-callout\">\n\n**Use Coins when** application values represent real money and every rounding boundary must be visible.\n\n**Consider native numbers when** values are estimates, analytics, or display-only approximations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/coins\n```\n\n```sh [npm]\nnpm install @vielzeug/coins\n```\n\n```sh [yarn]\nyarn add @vielzeug/coins\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { USD, add, format, money, multiply } from '@vielzeug/coins';\n\nconst subtotal = add(money('12.50', USD), money('7.25', USD));\nconst total = multiply(subtotal, '1.08', { rounding: 'halfEven' });\n\nconsole.log(format(total));\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`money`**: one constructor for decimal and explicit minor-unit values\n- **`currency`**: deterministic built-in currency definitions\n- **`add`**: exact same-currency arithmetic\n- **`allocate`**: split every minor unit without loss\n- **`exchange`**: typed source and target currency conversion\n- **`format`**: locale presentation for bigint values\n- **`parseMoneyJSON`**: validate persisted money values\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Vault](/vault/) — persist validated money JSON.\n- [Courier](/courier/) — retrieve exchange-rate data.\n- [Spell](/spell/) — validate external monetary payloads.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Coins — API Reference\ndescription: Exact money, currency definitions, exchange, formatting, serialization, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `money` | Construct validated money | Sync | Bigint requires `{ unit: 'minor' }` |\n| `currency` | Resolve supported definition | Sync | Unknown codes throw |\n| `defineCurrency` | Define an explicit scale | Sync | Code must be three uppercase letters |\n| `add` / `subtract` | Combine matching currencies | Sync | Mismatches throw |\n| `multiply` / `divide` | Exact decimal scaling | Sync | Use decimal strings |\n| `sum` | Aggregate with identity currency | Sync | Pass `{ currency }` |\n| `allocate` | Split without losing minor units | Sync | Weights must be non-negative |\n| `exchange` | Convert through an exact rate | Sync | Rate source must match value currency |\n| `format` | Present money with `Intl` | Sync | Formatting does not define currency scale |\n| `toJSON` / `parseMoneyJSON` | Cross JSON boundary | Sync | Persisted amount uses minor units |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/coins` | Complete public Coins API |\n\n## Construction\n\n### currency / defineCurrency\n\n```ts\ncurrency(code: string): Currency\ndefineCurrency({ code, minorUnit }): Currency\n```\n\nBuilt-ins: `USD`, `EUR`, `GBP`, `JPY`, `KRW`, `BHD`, `KWD`.\n\n### money\n\n```ts\nmoney(amount: string, currency: Currency): Money\nmoney(amount: string, currency: Currency, options: { rounding: RoundingMode }): Money\nmoney(amount: bigint, currency: Currency, options: { unit: 'minor' }): Money\n```\n\n```ts\nmoney('19.99', USD);\nmoney(1999n, USD, { unit: 'minor' });\n```\n\n### decimal\n\n```ts\ndecimal(value: string): Decimal\n```\n\nCreates an exact rational value for multiplication, division, or exchange rates.\n\n## Arithmetic\n\n```ts\nadd(left, right)\nsubtract(left, right)\nmultiply(value, factor, { rounding? })\ndivide(value, divisor, { rounding? })\ncompare(left, right)\nabs(value)\nnegate(value)\nround(value, { fractionDigits, rounding? })\n```\n\n`factor` and `divisor` are decimal strings. Matching currency is required for binary money operations.\n\n## Aggregation\n\n```ts\nsum(values, { currency })\nallocate(value, count)\nallocate(value, weights)\nclamp(value, { min, max })\n```\n\n`sum([], { currency: USD })` returns zero USD. `allocate` returns values whose minor-unit total exactly equals input.\n\n## Exchange\n\n```ts\nexchangeRate({ from, to, value }): ExchangeRate\nexchange(value, rate, { rounding? }): Money\n```\n\n```ts\nconst rate = exchangeRate({ from: USD, to: EUR, value: '0.9234' });\nexchange(money('100.00', USD), rate);\n```\n\n## Formatting\n\n```ts\nformat(value, options?): string\nformatParts(value, options?): MoneyFormatPart[]\n```\n\n`FormatOptions` uses `locale`, `style`, `minimumFractionDigits`, and `maximumFractionDigits`.\n\n## Serialization\n\n```ts\ntoDecimal(value): string\ntoJSON(value): MoneyJSON\nparseMoneyJSON(value: unknown, options?: { currency?: (code: string) => Currency }): Money\nparseMoney(value: unknown): Money\nisMoney(value: unknown): value is Money\n```\n\n## Types\n\n```ts\ntype Currency = { code: CurrencyCode; minorUnit: number };\ntype Money = { amount: bigint; currency: Currency };\ntype Decimal = { numerator: bigint; denominator: bigint };\ntype ExchangeRate = { from: Currency; to: Currency; value: Decimal };\ntype MoneyJSON = { amount: string; currency: string; unit: 'minor' };\ntype RoundingMode = 'awayFromZero' | 'ceil' | 'floor' | 'halfAwayFromZero' | 'halfEven' | 'towardZero';\n```\n\n## Errors\n\nEvery Coins failure extends `CoinsError` and exposes `code`.\n\n- `INVALID_CURRENCY`\n- `INVALID_DECIMAL`\n- `INVALID_MONEY`\n- `INVALID_ALLOCATION`\n- `INVALID_ROUNDING`\n- `DIVISION_BY_ZERO`\n- `CURRENCY_MISMATCH`\n\n`CurrencyMismatchError` and `InvalidCurrencyError` are specialized `CoinsError` subclasses.\n",
|
|
6
6
|
"usage": "---\ntitle: Coins — Usage Guide\ndescription: Construct exact money, aggregate values, convert currencies, and format results with Coins.\n---\n\n[[toc]]\n\n## Basic Usage\n\nConstruct decimal values with a currency definition. Coins stores minor units internally and never accepts implicit floating-point input.\n\n```ts\nimport { USD, add, money, toDecimal } from '@vielzeug/coins';\n\nconst subtotal = add(money('12.50', USD), money('7.25', USD));\n\nconsole.log(toDecimal(subtotal)); // '19.75'\n```\n\nUse bigint only when data is already in minor units:\n\n```ts\nimport { USD, money } from '@vielzeug/coins';\n\nconst cents = money(1999n, USD, { unit: 'minor' });\n```\n\n## Define Currencies\n\nUse built-in currency definitions for supported ISO currencies. Define a currency explicitly when your domain has a distinct scale.\n\n```ts\nimport { EUR, USD, defineCurrency, money } from '@vielzeug/coins';\n\nconst rewards = defineCurrency({ code: 'PTS', minorUnit: 0 });\n\nmoney('10.00', USD);\nmoney('10.00', EUR);\nmoney('500', rewards);\n```\n\n## Apply Exact Arithmetic\n\nPass decimal strings to scaling operations. Use named rounding whenever an operation can produce fractional minor units.\n\n```ts\nimport { USD, divide, money, multiply, round, toDecimal } from '@vielzeug/coins';\n\nconst subtotal = money('19.99', USD);\nconst taxed = multiply(subtotal, '1.08', { rounding: 'halfEven' });\n\n// Extra currency precision must name its rounding policy.\nconst roundedInput = money('19.999', USD, { rounding: 'halfAwayFromZero' });\nconst split = divide(taxed, '3', { rounding: 'floor' });\nconst displayed = round(taxed, { fractionDigits: 0, rounding: 'halfAwayFromZero' });\n\nconsole.log(toDecimal(split), toDecimal(displayed));\n```\n\n## Aggregate and Allocate\n\n`sum` receives currency context, so empty collections produce a useful zero. `allocate` preserves every minor unit.\n\n```ts\nimport { USD, allocate, money, sum, toDecimal } from '@vielzeug/coins';\n\nconst total = sum([], { currency: USD });\nconst weighted = allocate(money('10.00', USD), ['1', '2', '1']);\nconst even = allocate(money(5n, USD, { unit: 'minor' }), 2);\n\nconsole.log(toDecimal(total));\nconsole.log(weighted.map(toDecimal));\nconsole.log(even.map((value) => value.amount)); // [3n, 2n]\n```\n\n## Convert Currency\n\nCreate a typed rate from currency definitions and an exact decimal string.\n\n```ts\nimport { EUR, USD, exchange, exchangeRate, format, money } from '@vielzeug/coins';\n\nconst usdToEur = exchangeRate({ from: USD, to: EUR, value: '0.9234' });\nconst euros = exchange(money('100.00', USD), usdToEur, { rounding: 'halfEven' });\n\nconsole.log(format(euros, { locale: 'de-DE' }));\n```\n\n## Serialize Money\n\nUse JSON helpers at storage and transport boundaries. Parsing validates the shape, unit, and currency code.\n\n```ts\nimport { USD, money, parseMoneyJSON, toJSON } from '@vielzeug/coins';\n\nconst encoded = toJSON(money('19.99', USD));\nconst restored = parseMoneyJSON(encoded);\n\n// Custom currencies require an explicit resolver at restore time.\nconst custom = parseMoneyJSON(customEncoded, { currency: resolveAppCurrency });\n```\n\n## Handle Errors\n\nUse `CoinsError.code` for stable recovery branches.\n\n```ts\nimport { CoinsError, USD, money } from '@vielzeug/coins';\n\ntry {\n money(1999n, USD);\n} catch (error) {\n if (error instanceof CoinsError && error.code === 'INVALID_MONEY') {\n console.log('Use { unit: \\'minor\\' } for bigint amounts.');\n }\n}\n```\n\n## Best Practices\n\n- Use decimal strings for exact external inputs.\n- Use bigint only with `{ unit: 'minor' }`.\n- Pass named rounding options for division, scaling, and exchange.\n- Keep currency definitions at application boundaries.\n- Use `sum(values, { currency })` for possibly empty collections.\n- Serialize with `toJSON` and validate with `parseMoneyJSON`.\n- Format only at presentation boundaries.\n",
|
|
7
7
|
"examples": "---\ntitle: Coins — Examples\ndescription: Practical examples and recipes for @vielzeug/coins.\n---\n\n## Examples\n\n- [Formatting](./examples/formatting.md)\n- [Exchange Rate Conversion](./examples/exchange.md)\n- [Allocation](./examples/allocation.md)\n"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { createContainer } from './container';\nexport {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';\nexport type { Container, FactoryOptions, InferTokens, Lifetime, ScopeToken, Token, ValueOptions } from './types';\nexport { scope, token } from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Conduit — Dependency Injection for TypeScript\ndescription: Dependency-first asynchronous dependency injection with typed tokens, lifecycle scopes, startup validation, and deterministic disposal.\npackage: conduit\ncategory: infrastructure\nkeywords: [dependency injection, container, token, lifecycle, scope]\nexports: [createContainer, token, scope]\nrelated: [courier, vault, rune]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"conduit\" />\n\n## Why Conduit?\n\nConduit makes service wiring explicit. Factory dependency tuples are source of truth for creation, startup validation, and disposal order.\n\n```ts\n// Before\nconst service = createService(createApi(config), logger);\n\n// After\ncontainer.factory(Service, [Api, Logger], (api, logger) => createService(api, logger));\n```\n\n| Feature | Conduit | Inversify | tsyringe |\n| --- | --- | --- | --- |\n| Dependencies | Explicit token tuples | Decorators/runtime metadata | Decorators/runtime metadata |\n| Async factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Lifecycle scopes | <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| Runtime dependencies | 0 | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Conduit when** application services need explicit wiring and owned lifecycle cleanup.\n\n**Consider direct imports when** dependencies are static, small, and need no replacement or disposal boundary.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/conduit\n```\n\n```sh [npm]\nnpm install @vielzeug/conduit\n```\n\n```sh [yarn]\nyarn add @vielzeug/conduit\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createContainer, token } from '@vielzeug/conduit';\n\nconst Config = token<{ baseUrl: string }>('Config');\nconst Client = token<{ url: string }>('Client');\nconst container = createContainer();\n\ncontainer.value(Config, { baseUrl: '/api' });\ncontainer.factory(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` }));\n\nconsole.log(await container.resolve(Client));\nawait container.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`token`**: typed dependency identity\n- **`factory`**: static dependency-first creation\n- **`validate`**: startup graph validation\n- **`scope`**: explicit request and job ownership\n- **`dispose`**: in-flight-safe resource cleanup\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- [Courier](/courier/) — inject HTTP clients into application services.\n- [Vault](/vault/) — inject persistence adapters with scoped ownership.\n- [Rune](/rune/) — provide application logging services.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Conduit — Dependency Injection for TypeScript\ndescription: Dependency-first asynchronous dependency injection with typed tokens, lifecycle scopes, startup validation, and deterministic disposal.\npackage: conduit\ncategory: infrastructure\nkeywords: [dependency injection, container, token, lifecycle, scope]\nexports: [createContainer, token, scope]\nrelated: [courier, vault, rune]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"conduit\" />\n\n## Why Conduit?\n\nConduit makes service wiring explicit. Factory dependency tuples are source of truth for creation, startup validation, and disposal order.\n\n```ts\n// Before\nconst service = createService(createApi(config), logger);\n\n// After\ncontainer.factory(Service, [Api, Logger], (api, logger) => createService(api, logger));\n```\n\n| Feature | Conduit | Inversify | tsyringe |\n| --- | --- | --- | --- |\n| Dependencies | Explicit token tuples | Decorators/runtime metadata | Decorators/runtime metadata |\n| Async factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Lifecycle scopes | <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| Runtime dependencies | 0 | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Conduit when** application services need explicit wiring and owned lifecycle cleanup.\n\n**Consider direct imports when** dependencies are static, small, and need no replacement or disposal boundary.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/conduit\n```\n\n```sh [npm]\nnpm install @vielzeug/conduit\n```\n\n```sh [yarn]\nyarn add @vielzeug/conduit\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createContainer, token } from '@vielzeug/conduit';\n\nconst Config = token<{ baseUrl: string }>('Config');\nconst Client = token<{ url: string }>('Client');\nconst container = createContainer();\n\ncontainer.value(Config, { baseUrl: '/api' });\ncontainer.factory(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` }));\n\nconsole.log(await container.resolve(Client));\nawait container.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`token`**: typed dependency identity\n- **`factory`**: static dependency-first creation\n- **`validate`**: startup graph validation\n- **`scope`**: explicit request and job ownership\n- **`dispose`**: in-flight-safe resource cleanup\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/) — inject HTTP clients into application services.\n- [Vault](/vault/) — inject persistence adapters with scoped ownership.\n- [Rune](/rune/) — provide application logging services.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Conduit — API Reference\ndescription: Reference for Conduit tokens, dependency-first factories, scopes, validation, and lifecycle disposal.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Mode | Common gotcha |\n| --- | --- | --- | --- |\n| `token` | Create typed dependency identity | Sync | Same description does not mean same token |\n| `scope` | Create named lifecycle identity | Sync | Must match factory lifetime |\n| `createContainer` | Create root registry | Sync | Dispose when application ends |\n| `value` | Register an existing value | Sync | One registration per token/container |\n| `factory` | Register static dependency factory | Sync | Tuple is copied and authoritative |\n| `has` | Check registration visibility | Sync | Walks parent containers |\n| `resolve` | Resolve one dependency | Async | Missing provider throws |\n| `validate` | Validate static graph | Sync | Run after registration |\n| `createScope` | Create child owner | Sync | Named scope required for scoped factories |\n| `dispose` | Release owned resources | Async | May throw `ConduitDisposeError` after cleanup attempts |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/conduit` | Complete Conduit API |\n\n## Tokens and Scopes\n\n```ts\ntoken<T>(description: string): Token<T>\nscope(name: string): ScopeToken\n```\n\nTokens and scopes are unique symbols. Descriptions exist only for diagnostics.\n\n## Container\n\n```ts\ncreateContainer(options?: { name?: string }): Container\n```\n\n### value\n\n```ts\ncontainer.value(token, value, options?)\n```\n\n`options.dispose` runs during container disposal.\n\n### has\n\n```ts\ncontainer.has(token): boolean\n```\n\nChecks local and parent registrations without creating a factory result.\n\n### factory\n\n```ts\ncontainer.factory(token, dependencies, create, options?)\n```\n\n```ts\ncontainer.factory(Service, [Api, Logger], (api, logger) => createService(api, logger));\n```\n\n`dependencies` is copied at registration and drives creation, validation, cycle detection, and teardown order. Factories may return a value or promise.\n\n`options.lifetime` accepts `'singleton'`, `'transient'`, or `ScopeToken`. A singleton cannot depend on a scoped resource.\n\n```ts\ntype FactoryOptions<T> = {\n dispose?: (value: T) => void | Promise<void>;\n lifetime?: 'singleton' | 'transient' | ScopeToken;\n};\n```\n\n### resolve\n\n```ts\ncontainer.resolve(token): Promise<T>\n```\n\nSingleton resolutions deduplicate concurrent callers.\n\n### validate\n\n```ts\ncontainer.validate(): Container\n```\n\nThrows for missing dependencies and circular factory tuples.\n\n### createScope\n\n```ts\ncontainer.createScope(scope?: ScopeToken, options?: { name?: string }): Container\n```\n\nA matching scope owns resources registered with its `ScopeToken` lifetime. Disposing a parent also disposes its active child scopes.\n\n### dispose\n\n```ts\ncontainer.dispose(): Promise<void>\ncontainer.disposalSignal: AbortSignal\ncontainer.disposed: boolean\n```\n\nDisposal blocks new work, aborts `disposalSignal`, disposes active child scopes, waits for in-flight creation, then disposes owned resources in reverse creation order. Cleanup failures are aggregated in `ConduitDisposeError.errors`.\n\n## Types\n\n```ts\ntype Token<T> = symbol;\ntype ScopeToken = symbol;\ntype Lifetime = 'singleton' | 'transient' | ScopeToken;\ntype InferTokens<Tokens> = { [K in keyof Tokens]: Tokens[K] extends Token<infer T> ? T : never };\n```\n\n## Errors\n\n- `ConduitError` — base class; `ConduitError.is(error)` narrows package errors.\n- `ConduitProviderNotFoundError` — dependency has no registration.\n- `ConduitCircularDependencyError` — static factory tuple graph contains a cycle.\n- `ConduitDuplicateRegistrationError` — token registered twice in one container.\n- `ConduitScopedResolutionError` — scoped factory resolved without matching scope.\n- `ConduitDisposedError` — operation attempted after disposal began.\n- `ConduitDisposeError` — one or more cleanup hooks failed.\n",
|
|
6
6
|
"usage": "---\ntitle: Conduit — Usage Guide\ndescription: Register static dependency tuples, resolve services asynchronously, create scopes, validate startup wiring, and dispose owned resources.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate tokens once, register values and factories, then resolve through one async API.\n\n```ts\nimport { createContainer, token } from '@vielzeug/conduit';\n\nconst Config = token<{ baseUrl: string }>('Config');\nconst Client = token<{ url: string }>('Client');\n\nconst container = createContainer();\ncontainer.value(Config, { baseUrl: '/api' });\ncontainer.factory(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` }));\n\nconsole.log(await container.resolve(Client));\nawait container.dispose();\n```\n\n## Define Dependencies\n\nFactory token tuples are authoritative. Conduit resolves tuple values in order, validates every edge, and disposes created services in reverse dependency order.\n\n```ts\nconst Logger = token<{ info(message: string): void }>('Logger');\nconst Api = token<{ get(path: string): Promise<unknown> }>('Api');\nconst Service = token<{ load(): Promise<unknown> }>('Service');\n\ncontainer.factory(Service, [Api, Logger], (api, logger) => ({\n async load() {\n logger.info('Loading data');\n return api.get('/data');\n },\n}));\n```\n\n## Choose Lifetimes\n\nFactories are singletons by default. Use transient lifetime for a new value on every resolution. Conduit retains a transient only when its factory has a `dispose` hook.\n\n```ts\nconst RequestId = token<{ id: string }>('RequestId');\n\ncontainer.factory(RequestId, [], () => ({ id: crypto.randomUUID() }), {\n lifetime: 'transient',\n});\n```\n\nConcurrent singleton resolutions share one in-flight factory result. A singleton cannot depend on a scoped resource; give dependent factory equal-or-shorter lifetime instead. Factory dependency tuples are copied at registration, so later caller mutation cannot change Conduit's graph.\n\n## Create Named Scopes\n\nUse a scope token when a resource belongs to a request, job, or test lifecycle.\n\n```ts\nimport { createContainer, scope, token } from '@vielzeug/conduit';\n\nconst Request = scope('request');\nconst Session = token<{ id: string }>('Session');\nconst root = createContainer();\n\nroot.factory(Session, [], () => ({ id: crypto.randomUUID() }), { lifetime: Request });\n\nconst request = root.createScope(Request);\nconst session = await request.resolve(Session);\nawait request.dispose();\nawait root.dispose();\n```\n\n## Validate Startup Wiring\n\nCall `validate()` after registration. It detects missing dependencies and cycles before service resolution. Parent singleton factories validate dependencies from their registration owner; child overrides do not satisfy them.\n\n```ts\ncontainer.validate();\n```\n\n## Dispose Resources\n\n`dispose()` rejects new work, aborts `disposalSignal`, disposes child scopes, waits for in-flight creation, then releases services in reverse creation order. A factory that finishes after disposal starts is immediately cleaned up and its resolver receives `ConduitDisposedError`.\n\n```ts\nawait container.dispose();\n```\n\n`ConduitDisposeError.errors` contains every cleanup failure after Conduit attempts all hooks, including cleanup from in-flight factories and child scopes.\n\n## Testing\n\nCreate a container per test and register explicit values for external dependencies.\n\n```ts\nconst Clock = token<{ now(): number }>('Clock');\nconst Service = token<{ timestamp: number }>('Service');\nconst container = createContainer();\n\ncontainer.value(Clock, { now: () => 123 });\ncontainer.factory(Service, [Clock], (clock) => ({ timestamp: clock.now() }));\n\nexpect(await container.resolve(Service)).toEqual({ timestamp: 123 });\nawait container.dispose();\n```\n\n## Best Practices\n\n- Create tokens at module scope.\n- Declare every factory dependency in its tuple.\n- Keep factories focused on one service.\n- Use scopes for request/job-owned resources.\n- Call `validate()` during startup.\n- Dispose every scope and root container.\n- Keep optional application fallback policy outside Conduit.\n- Use `await using container = createContainer()` when lexical async disposal fits application lifetime.\n",
|
|
7
7
|
"examples": "---\ntitle: Conduit — Examples\ndescription: Dependency-first container recipes.\n---\n\n## Examples\n\n- [Basic setup](./examples/basic-setup.md)\n- [Static async providers](./examples/async-providers.md)\n- [Lifetimes](./examples/lifetimes.md)\n- [Named scopes](./examples/named-scopes.md)\n- [Disposal lifecycle](./examples/dispose-lifecycle.md)\n- [Startup validation](./examples/startup-hardening.md)\n"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { createCourier, type Courier, type CourierOptions } from './courier';\nexport {\n CourierAbortError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n} from './errors';\nexport { withBearerAuth, withLogging, withRequestId } from './interceptors';\nexport type { FetchContext, Interceptor, TransportOptions } from './transport';\nexport type { HttpRequestConfig as RequestConfig, Params } from './url';\nexport type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';\nexport type { StreamEvent, StreamOptions } from './stream';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Courier — HTTP, queries, and streaming\ndescription: A framework-neutral fetch client with explicit cache keys, direct mutations, and abortable streams.\npackage: courier\ncategory: http\nkeywords: [http-client, fetch, caching, queries, mutations, sse, streaming, interceptors]\nrelated: [flux, ripple, spell]\nexports:\n [\n createCourier,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierTimeoutError,\n CourierAbortError,\n CourierSchemaValidationError,\n withBearerAuth,\n withRequestId,\n withLogging,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"courier\" />\n\n## Why Courier?\n\nNative `fetch` leaves request policy, cached reads, and stream lifecycles to each application. Courier keeps\nthose concerns in one client while making cache identity and fetch policy explicit at every cached read.\n\n```ts\n// Before\nconst response = await fetch(`/api/users/${userId}`);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst user = await response.json();\n\n// After\nawait courier.queries.fetch({\n key: ['users', userId],\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: userId }, signal }),\n});\n```\n\n| Feature | Courier | TanStack Query | ky |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"courier\" type=\"size\" /> | Framework adapter required | Separate package |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Native fetch transport | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Bring your own | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit cache keys | <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| SSE and NDJSON iteration | <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| External runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Courier when** one application client should own typed HTTP, explicit cached reads, direct writes, and\nabortable response streams.\n\n**Consider TanStack Query when** you need a maintained framework adapter or advanced cache features such as\ninfinite queries. **Consider ky when** you only need a compact fetch wrapper without caching or streams.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/courier\n```\n\n```sh [npm]\nnpm install @vielzeug/courier\n```\n\n```sh [yarn]\nyarn add @vielzeug/courier\n```\n\n:::\n\n## Quick Start\n\nCreate one client for an application or request scope, then fetch a cache entry by its explicit key.\n\n```ts\nimport { CourierHttpError, createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com', query: { staleTime: 30_000 } });\nconst key = ['users', 42] as const;\n\ntry {\n await courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: 42 }, signal }),\n });\n console.log(courier.queries.getSnapshot<User>(key)?.data);\n} catch (error) {\n if (CourierHttpError.is(error, 404)) console.log('User not found');\n else throw error;\n} finally {\n courier.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createCourier()`** — one lifecycle, interceptor pipeline, header store, and cancellation boundary.\n- **`get()` / `post()` / `request()`** — typed paths, query strings, request bodies, validation, and structured errors.\n- **`queries.fetch()`** — key-based cached reads, subscriptions, invalidation, and explicit revalidation.\n- **`mutate()`** — direct write operation with a cache callback, without hidden retries or a second state store.\n- **`events()` / `read()`** — abortable SSE, text, and NDJSON iteration with normalized request errors.\n- **`withBearerAuth()` / `withRequestId()` / `withLogging()`** — composable transport policies.\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- [Flux](/flux/) — adapts Courier cache entries and event iterators into composable streams.\n- [Ripple](/ripple/) — stores Courier snapshots in fine-grained reactive state.\n- [Spell](/spell/) — validates parsed HTTP payloads through Courier's schema option.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
4
|
+
"index": "---\ntitle: Courier — HTTP, queries, and streaming\ndescription: A framework-neutral fetch client with explicit cache keys, direct mutations, and abortable streams.\npackage: courier\ncategory: http\nkeywords: [http-client, fetch, caching, queries, mutations, sse, streaming, interceptors]\nrelated: [flux, ripple, spell]\nexports:\n [\n createCourier,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierTimeoutError,\n CourierAbortError,\n CourierSchemaValidationError,\n withBearerAuth,\n withRequestId,\n withLogging,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"courier\" />\n\n## Why Courier?\n\nNative `fetch` leaves request policy, cached reads, and stream lifecycles to each application. Courier keeps\nthose concerns in one client while making cache identity and fetch policy explicit at every cached read.\n\n```ts\n// Before\nconst response = await fetch(`/api/users/${userId}`);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst user = await response.json();\n\n// After\nawait courier.queries.fetch({\n key: ['users', userId],\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: userId }, signal }),\n});\n```\n\n| Feature | Courier | TanStack Query | ky |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"courier\" type=\"size\" /> | Framework adapter required | Separate package |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Native fetch transport | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Bring your own | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit cache keys | <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| SSE and NDJSON iteration | <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| External runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Courier when** one application client should own typed HTTP, explicit cached reads, direct writes, and\nabortable response streams.\n\n**Consider TanStack Query when** you need a maintained framework adapter or advanced cache features such as\ninfinite queries. **Consider ky when** you only need a compact fetch wrapper without caching or streams.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/courier\n```\n\n```sh [npm]\nnpm install @vielzeug/courier\n```\n\n```sh [yarn]\nyarn add @vielzeug/courier\n```\n\n:::\n\n## Quick Start\n\nCreate one client for an application or request scope, then fetch a cache entry by its explicit key.\n\n```ts\nimport { CourierHttpError, createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com', query: { staleTime: 30_000 } });\nconst key = ['users', 42] as const;\n\ntry {\n await courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: 42 }, signal }),\n });\n console.log(courier.queries.getSnapshot<User>(key)?.data);\n} catch (error) {\n if (CourierHttpError.is(error, 404)) console.log('User not found');\n else throw error;\n} finally {\n courier.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createCourier()`** — one lifecycle, interceptor pipeline, header store, and cancellation boundary.\n- **`get()` / `post()` / `request()`** — typed paths, query strings, request bodies, validation, and structured errors.\n- **`queries.fetch()`** — key-based cached reads, subscriptions, invalidation, and explicit revalidation.\n- **`mutate()`** — direct write operation with a cache callback, without hidden retries or a second state store.\n- **`events()` / `read()`** — abortable SSE, text, and NDJSON iteration with normalized request errors.\n- **`withBearerAuth()` / `withRequestId()` / `withLogging()`** — composable transport policies.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Flux](/flux/) — adapts Courier cache entries and event iterators into composable streams.\n- [Ripple](/ripple/) — stores Courier snapshots in fine-grained reactive state.\n- [Spell](/spell/) — validates parsed HTTP payloads through Courier's schema option.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
5
|
"api": "---\ntitle: Courier — API Reference\ndescription: Reference for Courier HTTP, cache, mutation, interceptor, and stream APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCourier()` | Creates unified application client | Sync | Dispose only when whole scope ends |\n| `Courier` HTTP methods | Sends and parses HTTP requests | Async | Direct calls never deduplicate |\n| `queries.fetch()` | Fetches one keyed cache entry | Async | Key must include all response identity inputs |\n| `mutate()` | Runs one write operation | Async | It never retries automatically |\n| `events()` / `read()` | Opens abortable response iterators | Async iteration | Breaking iteration aborts request |\n| `withBearerAuth()` | Adds authorization interceptor | Sync | Token provider runs per request |\n| `withRequestId()` | Adds request identifier interceptor | Sync | Default generator uses `uuid()` |\n| `withLogging()` | Logs request result metadata | Sync | URLs may contain sensitive query values |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/courier` | Client factory, errors, interceptors, and public types |\n| `@vielzeug/courier/devtools` | `debugCourier()` with logging preconfigured |\n\n## Client\n\n### `createCourier()`\n\n```ts\ncreateCourier(options?: CourierOptions): Courier;\n```\n\nReturns client sharing transport configuration, headers, interceptors, cancellation, cache, mutations, and streams.\n\n| `CourierOptions` field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `baseUrl` | `string` | `''` | Prefix for relative request paths |\n| `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Fetch implementation |\n| `headers` | `Record<string, string>` | `{}` | Global request headers |\n| `timeout` | `number` | `30_000` | Default HTTP timeout in milliseconds |\n| `query.staleTime` | `number` | `0` | Cache freshness duration |\n\n**Returns:** `Courier`.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n```\n\n| `Courier` member | Signature | Description |\n| --- | --- | --- |\n| `get` / `post` / `put` / `patch` / `delete` | `<T, P>(url: P, config?) => Promise<T>` | Sends one HTTP request |\n| `request` | `<T, P>(method, url: P, config?) => Promise<T>` | Sends custom HTTP method |\n| `headers` | `(updates) => void` | Updates global headers |\n| `getHeaders` | `() => Readonly<Record<string, string>>` | Returns header snapshot |\n| `use` | `(interceptor) => () => void` | Registers interceptor |\n| `cancelAll` | `() => void` | Aborts active HTTP, cache, and mutation work |\n| `queries` | `QueryCache` | Owns keyed cache entries |\n| `mutate` | `<T>(options) => Promise<T>` | Runs one write operation |\n| `events` | `<T, P>(url, options?) => AsyncIterableIterator<StreamEvent<T>>` | Opens SSE iterator |\n| `read` | `<T, P>(url, options?) => AsyncIterableIterator<T>` | Opens text or NDJSON iterator |\n| `dispose` | `() => void` | Final disposal; aborts work and clears cache |\n| `disposed` | `boolean` | Whether final disposal occurred |\n| `disposalSignal` | `AbortSignal` | Aborts on final disposal |\n\n---\n\n## Queries\n\n### `queries.fetch()`\n\n```ts\nfetch<T>(definition: QueryDefinition<T>, options?: { force?: boolean }): Promise<T>;\n```\n\nRegisters latest definition for `definition.key`, then returns fresh cached data or runs its fetch function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `definition.key` | `QueryKey` | Cache identity; include every response identity input |\n| `definition.fetch` | `(context: QueryContext) => Promise<T>` | Request function for this key |\n| `definition.staleTime` | `number` | Per-entry freshness duration |\n| `options.force` | `boolean` | Fetch even when cached data is fresh |\n\n**Returns:** Cached or fetched data.\n\n```ts\nconst key = ['profile', 1] as const;\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n```\n\n| `QueryCache` method | Returns | Description |\n| --- | --- | --- |\n| `get(key)` | `T \\| undefined` | Returns successful cached data |\n| `getSnapshot(key)` | `AsyncState<T> \\| null` | Returns snapshot by key |\n| `set(key, data, options?)` | `void` | Sets successful cache value |\n| `invalidate(key)` | `void` | Marks matching key prefixes stale |\n| `refetchStale()` | `void` | Starts stale successful entries in background |\n| `keys()` | `QueryKey[]` | Lists known keys |\n| `subscribe(key, listener)` | `Unsubscribe` | Subscribes to one key |\n| `clear()` | `void` | Removes every cache entry |\n\n---\n\n## Mutations\n\n### `mutate()`\n\n```ts\nmutate<T>(options: MutationOptions<T>): Promise<T>;\n```\n\nRuns `options.request` once, then calls `onSuccess` after successful completion.\n\n| `MutationOptions<T>` field | Type | Description |\n| --- | --- | --- |\n| `request` | `(context: MutationContext) => Promise<T>` | Write operation |\n| `onSuccess` | `(data, queries) => void \\| Promise<void>` | Cache update callback |\n| `signal` | `AbortSignal` | Caller-controlled cancellation |\n\n**Returns:** Request result.\n\n---\n\n## Streams\n\n### `events()` and `read()`\n\n```ts\nevents<T, P extends string>(url: P, options?: StreamOptions<P>): AsyncIterableIterator<StreamEvent<T>>;\nread<T, P extends string>(url: P, options?: StreamOptions<P> & { parse?: 'ndjson' | 'text' }): AsyncIterableIterator<T>;\n```\n\nBoth iterators abort request when `return()` runs or `for await` loop exits. `events()` parses `event` and `data`\nfields; it does not retain event IDs or reconnect.\n\n| `StreamOptions` field | Type | Description |\n| --- | --- | --- |\n| `body` | `unknown` | Request body |\n| `method` | `string` | Defaults to GET, or POST when body is present |\n| `params` / `query` | Path and query parameters | Builds URL |\n| `headers` / `fetchInit` | Request configuration | Adds per-request configuration |\n| `signal` | `AbortSignal` | Merges external cancellation |\n| `timeout` | `number` | Stream timeout; omitted means no timeout |\n\n**Returns:** Abortable async iterator.\n\n---\n\n## Interceptors\n\n### Interceptor helpers\n\n```ts\nwithBearerAuth(token: string | (() => string | Promise<string>)): Interceptor;\nwithRequestId(options?: { generate?: () => string; header?: string }): Interceptor;\nwithLogging(options?: {\n logger?: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): Interceptor;\n```\n\nEach helper returns an `Interceptor` accepted by `courier.use()`.\n\n## Types\n\n```ts\ntype AsyncState<T> =\n | { data: undefined; error: null; isFetching: boolean; status: 'loading'; updatedAt: undefined }\n | { data: T; error: null; isFetching: boolean; status: 'success'; updatedAt: number }\n | { data: T | undefined; error: Error; isFetching: false; status: 'error'; updatedAt: number };\n\ntype QueryContext = { readonly key: QueryKey; readonly signal: AbortSignal };\ntype QueryDefinition<T> = { fetch: (context: QueryContext) => Promise<T>; key: QueryKey; staleTime?: number };\ntype QueryKey = readonly [QueryKeyAtom, ...QueryKeyAtom[]];\ntype QueryKeyAtom = string | number | boolean | null | { readonly [key: string]: string | number | boolean | null };\ntype QueryCache = {\n clear(): void;\n fetch<T>(definition: QueryDefinition<T>, options?: { force?: boolean }): Promise<T>;\n get<T>(key: QueryKey): T | undefined;\n getSnapshot<T>(key: QueryKey): AsyncState<T> | null;\n invalidate(key: QueryKey): void;\n keys(): QueryKey[];\n refetchStale(): void;\n set<T>(key: QueryKey, data: T, options?: { updatedAt?: number }): void;\n subscribe(key: QueryKey, listener: () => void): Unsubscribe;\n};\ntype MutationContext = { readonly signal: AbortSignal };\ntype MutationOptions<T> = {\n onSuccess?: (data: T, queries: QueryCache) => void | Promise<void>;\n request: (context: MutationContext) => Promise<T>;\n signal?: AbortSignal;\n};\ntype StreamEvent<T = unknown> = { readonly data: T; readonly event: string };\ntype Unsubscribe = () => void;\n```\n\n```ts\ntype ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype Params = Record<string, ParamValue>;\ntype RequestConfig<P extends string = string, T = unknown> = {\n body?: unknown;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: Record<string, string>;\n params?: Record<string, string | number | boolean>;\n query?: Params;\n responseType?: 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';\n schema?: { parse(data: unknown): T };\n signal?: AbortSignal;\n timeout?: number;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `CourierError` | Base class for all Courier errors | `CourierError.is(error)` |\n| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url` |\n| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |\n| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |\n| `CourierAbortError` | Caller, client, or iterator cancellation | `method`, `url`, `cause` |\n| `CourierSchemaValidationError` | Response schema fails | `data`, `cause` |\n| `CourierParseError` | Response body cannot parse | — |\n| `CourierDisposedError` | Work starts after disposal | — |\n",
|
|
6
6
|
"usage": "---\ntitle: Courier — Usage Guide\ndescription: Use one Courier client for HTTP, explicit cached reads, direct mutations, and abortable streams.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one Courier client for an application or request scope. Its transport policy and disposal lifecycle apply\nto every request, cache entry, mutation, and stream.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com', query: { staleTime: 30_000 } });\nconst key = ['users', 1] as const;\n\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get<User>('/users/{id}', { params: { id: 1 }, signal }),\n});\nconsole.log(courier.queries.get<User>(key)?.name);\n```\n\n## HTTP Requests\n\nUse root methods for REST requests. Courier encodes path parameters, serializes plain-object bodies, and parses\nsuccessful response bodies. Each direct HTTP call is independent; use a query key when concurrent cached reads\nshould share work.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'Updated title' },\n params: { id: posts[0].id },\n});\n```\n\nCall `courier.headers({ authorization: 'Bearer token' })` to update subsequent calls.\n\n## Interceptors\n\nInterceptors apply to HTTP and streaming requests. Register a policy once, then remove it when its containing\nscope ends.\n\n```ts\nimport { withBearerAuth, withRequestId } from '@vielzeug/courier';\n\nconst removeAuth = courier.use(withBearerAuth(async () => sessionStorage.getItem('access-token') ?? ''));\nconst removeRequestId = courier.use(withRequestId());\n\nremoveRequestId();\nremoveAuth();\n```\n\nUse `debugCourier()` from `@vielzeug/courier/devtools` to create a logging-enabled client during local\ndevelopment. `withLogging()` includes full URLs, so sanitize query values before persistent logging.\n\n## Cached Queries\n\nPass a stable key and fetch definition to `queries.fetch()`. The cache owns data, snapshots, subscriptions, and\nin-flight deduplication for that key.\n\n```ts\nconst key = ['profile', 1] as const;\nconst definition = {\n key,\n fetch: ({ signal }) => courier.get<{ id: number; name: string }>('/profile/{id}', { params: { id: 1 }, signal }),\n staleTime: 60_000,\n};\n\nconst stop = courier.queries.subscribe(key, () => {\n const state = courier.queries.getSnapshot<{ id: number; name: string }>(key);\n if (state?.status === 'success') console.log(state.data.name);\n if (state?.status === 'error') console.error(state.error);\n});\n\nawait courier.queries.fetch(definition);\nstop();\n```\n\n`queries.fetch(definition)` reuses fresh data. Pass `{ force: true }` to fetch regardless of freshness.\n`invalidate(key)` marks matching keys stale but does not fetch. Call `queries.refetchStale()` when visible data\nmust refresh now.\n\n## Direct Mutations\n\nUse `mutate()` for a write operation and update the cache in `onSuccess`. Courier never retries writes: retry\nonly operations your application can prove idempotent.\n\n```ts\ntype User = { id: number; name: string };\n\nconst created = await courier.mutate({\n request: ({ signal }) => courier.post<User>('/users', { body: { name: 'Ada' }, signal }),\n onSuccess: (user, queries) => {\n queries.set(['users', user.id], user);\n queries.invalidate(['users']);\n queries.refetchStale();\n },\n});\n\nconsole.log(created.id);\n```\n\nPass an external `signal` when caller owns cancellation. Keep pending and error UI state in framework that owns\nthat UI.\n\n## Server-Sent Events\n\n`events()` returns an abortable `AsyncIterableIterator`. Breaking loop, calling `return()`, aborting a provided\nsignal, or disposing client stops its request immediately. Courier sends `Accept: text/event-stream` and\n`Cache-Control: no-cache` by default; pass headers to override either value.\n\n```ts\ntype Notification = { text: string };\n\nfor await (const event of courier.events<Notification>('/events')) {\n if (event.event !== 'message') continue;\n console.log(event.data.text);\n break;\n}\n```\n\nCourier parses valid JSON event data and otherwise returns text. It does not reconnect automatically or retain\nSSE event IDs; application owns reconnect policy.\n\n## HTTP Streaming\n\nUse `read()` for text chunks or NDJSON records.\n\n```ts\ntype ChatChunk = { done: boolean; delta: string };\n\nfor await (const chunk of courier.read<ChatChunk>('/chat', {\n body: { prompt: 'Explain cached queries.' },\n method: 'POST',\n parse: 'ndjson',\n})) {\n console.log(chunk.delta);\n if (chunk.done) break;\n}\n```\n\nStreams have no timeout unless `timeout` is supplied. HTTP, network, timeout, and cancellation failures use\nCourier error classes; starting a stream after disposal throws `CourierDisposedError`.\n\n## Framework Integration\n\nCreate Courier at application or route boundary. Views read a key snapshot synchronously, subscribe during\ntheir lifecycle, and let framework own rendering state.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useSyncExternalStore } from 'react';\nimport { createCourier } from '@vielzeug/courier';\nimport type { AsyncState, QueryDefinition } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nexport function Profile({ courier, definition }: { courier: ReturnType<typeof createCourier>; definition: QueryDefinition<User> }) {\n const state = useSyncExternalStore(\n (listener) => courier.queries.subscribe(definition.key, listener),\n () => courier.queries.getSnapshot<User>(definition.key),\n () => courier.queries.getSnapshot<User>(definition.key),\n ) as AsyncState<User> | null;\n\n useEffect(() => void courier.queries.fetch(definition), [courier, definition]);\n\n if (!state || state.status === 'loading') return <p>Loading...</p>;\n if (state.status === 'error') return <p role=\"alert\">{state.error.message}</p>;\n return <p>{state.data.name}</p>;\n}\n```\n\n```ts [Vue 3]\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createCourier } from '@vielzeug/courier';\nimport type { AsyncState, QueryDefinition } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nexport function useProfile(courier: ReturnType<typeof createCourier>, definition: QueryDefinition<User>) {\n const state = ref<AsyncState<User> | null>(courier.queries.getSnapshot(definition.key));\n const unsubscribe = courier.queries.subscribe(definition.key, () => {\n state.value = courier.queries.getSnapshot(definition.key);\n });\n\n onMounted(() => void courier.queries.fetch(definition));\n onUnmounted(unsubscribe);\n\n return { state };\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createCourier } from '@vielzeug/courier';\n import type { AsyncState, QueryDefinition } from '@vielzeug/courier';\n\n type User = { id: number; name: string };\n\n export let courier: ReturnType<typeof createCourier>;\n export let definition: QueryDefinition<User>;\n let state: AsyncState<User> | null = courier.queries.getSnapshot(definition.key);\n\n onMount(() => {\n const unsubscribe = courier.queries.subscribe(definition.key, () => (state = courier.queries.getSnapshot(definition.key)));\n void courier.queries.fetch(definition);\n return unsubscribe;\n });\n</script>\n\n{#if state?.status === 'success'}\n <p>{state.data.name}</p>\n{/if}\n```\n\n:::\n\nCourier exposes no framework-specific loading or error store. Render `AsyncState` in framework that owns view.\n\n## Working with Other Vielzeug Libraries\n\n### Flux\n\nUse Flux when cache snapshots or SSE events need filtering, composition, or subscription lifecycle separate from\nUI framework. Pass cache and query definition to `fromQuery()`.\n\n```ts\nimport { fromQuery, fromSse } from '@vielzeug/flux/courier';\n\nconst profile = {\n key: ['profile'] as const,\n fetch: ({ signal }: { signal: AbortSignal }) => courier.get<{ id: number; name: string }>('/profile', { signal }),\n};\nconst profile$ = fromQuery(courier.queries, profile);\nconst notifications$ = fromSse(courier.events<{ text: string }>('/events'), 'message');\n\nvoid courier.queries.fetch(profile);\n\nconst profileSubscription = profile$.subscribe((state) => console.log(state?.status));\nconst notificationSubscription = notifications$.subscribe((notification) => console.log(notification.text));\n\nnotificationSubscription.unsubscribe();\nprofileSubscription.unsubscribe();\n```\n\n### Ripple\n\nUse a Ripple signal when Courier data must participate in fine-grained reactive state outside a component. Mirror\nonly cache snapshot into signal.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst key = ['profile', 1] as const;\nconst profileState = signal(courier.queries.getSnapshot<{ id: number; name: string }>(key));\nconst unsubscribe = courier.queries.subscribe(key, () => (profileState.value = courier.queries.getSnapshot(key)));\n\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n\nunsubscribe();\n```\n\n## Best Practices\n\n- Create one Courier client per application or SSR request scope.\n- Use stable, complete cache keys for every cached response identity.\n- Fetch through `queries.fetch()` when work should deduplicate and cache.\n- Invalidate keys after writes, then refetch stale visible data when needed.\n- Keep retries outside mutations until operation idempotency is proven.\n- Dispose only at final application or request boundary.\n- Keep credentials out of URLs when using logging interceptors.\n",
|
|
7
7
|
"examples": "---\ntitle: Courier — Examples\ndescription: Practical examples and recipes for courier.\n---\n\n## Examples\n\n- [Authentication](./examples/authentication.md)\n- [CRUD Operations](./examples/crud-operations.md)\n- [Disposal](./examples/disposal.md)\n- [Error Handling Patterns](./examples/error-handling-patterns.md)\n- [File Uploads](./examples/file-uploads.md)\n- [Optimistic Updates](./examples/optimistic-updates.md)\n- [Polling](./examples/polling.md)\n- [Real-time Events](./examples/sse-events.md)\n- [AI Token Stream](./examples/ai-token-stream.md)\n"
|