@vielzeug/codex 26.10.0 → 26.10.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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/data/catalog.json +56 -60
  3. package/data/llms-full.txt +1473 -1069
  4. package/data/llms.txt +6 -6
  5. package/data/manifest.json +1 -1
  6. package/data/packages/arsenal.json +13 -13
  7. package/data/packages/assay.json +4 -4
  8. package/data/packages/clockwork.json +4 -4
  9. package/data/packages/codex.json +3 -3
  10. package/data/packages/coins.json +4 -4
  11. package/data/packages/conduit.json +4 -4
  12. package/data/packages/courier.json +4 -4
  13. package/data/packages/dnd.json +10 -10
  14. package/data/packages/familiar.json +4 -4
  15. package/data/packages/flux.json +4 -4
  16. package/data/packages/focus.json +8 -5
  17. package/data/packages/forge.json +4 -4
  18. package/data/packages/gesture.json +4 -4
  19. package/data/packages/herald.json +12 -12
  20. package/data/packages/illusionist.json +5 -5
  21. package/data/packages/keymap.json +9 -9
  22. package/data/packages/ledger.json +4 -4
  23. package/data/packages/lingua.json +4 -4
  24. package/data/packages/mesh.json +5 -5
  25. package/data/packages/necromancer.json +5 -5
  26. package/data/packages/orbit.json +7 -7
  27. package/data/packages/ore.json +5 -5
  28. package/data/packages/postmaster.json +4 -4
  29. package/data/packages/prism.json +50 -42
  30. package/data/packages/pulse.json +8 -8
  31. package/data/packages/refine.json +3 -3
  32. package/data/packages/ripple.json +4 -4
  33. package/data/packages/rune.json +9 -9
  34. package/data/packages/sandbox.json +4 -4
  35. package/data/packages/scout.json +7 -7
  36. package/data/packages/scroll.json +10 -10
  37. package/data/packages/sentinel.json +4 -4
  38. package/data/packages/sigil.json +6 -6
  39. package/data/packages/sourcerer.json +4 -4
  40. package/data/packages/spell.json +9 -9
  41. package/data/packages/tandem.json +5 -5
  42. package/data/packages/tavern.json +12 -12
  43. package/data/packages/tempo.json +4 -4
  44. package/data/packages/vault.json +13 -13
  45. package/data/packages/ward.json +9 -9
  46. package/data/packages/wayfinder.json +17 -17
  47. package/data/refine.json +259 -87
  48. package/data/search.json +249 -249
  49. package/dist/http.js +2 -2
  50. package/dist/http.js.map +1 -1
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +1 -1
  53. package/dist/refine.d.ts +1 -1
  54. package/dist/refine.js +1 -1
  55. package/dist/server.d.ts +1 -1
  56. package/dist/server.js +1 -1
  57. package/dist/tools/refine.js +1 -1
  58. package/package.json +2 -2
  59. package/skills/vielzeug/SKILL.md +57 -57
package/data/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vielzeug
2
2
 
3
- > 41 focused TypeScript packages. Version: 3.1.1
3
+ > 41 focused TypeScript packages. Version: 26.10.1
4
4
 
5
5
  Install any package independently: `pnpm add @vielzeug/<name>`
6
6
 
@@ -13,7 +13,7 @@ Install any package independently: `pnpm add @vielzeug/<name>`
13
13
  - [@vielzeug/coins](/coins/): Exact bigint monetary arithmetic with explicit currency definitions, decimal strings, allocation, exchange, formatting, and JSON boundaries.
14
14
  - [@vielzeug/conduit](/conduit/): Dependency-first asynchronous dependency injection with typed tokens, child-container scoping, startup validation, and deterministic disposal.
15
15
  - [@vielzeug/courier](/courier/): A framework-neutral HTTP client with explicit cached reads, prefetching, immutable middleware, typed paths, and structured errors.
16
- - [@vielzeug/dnd](/dnd/): Framework-agnostic drag-and-drop. Drop zones with MIME filtering, sortable lists with drag handles, and explicit connected scopes — zero third-party dependencies.
16
+ - [@vielzeug/dnd](/dnd/): Framework-agnostic drag-and-drop. Drop zones with MIME filtering, sortable lists with drag handles, and explicit connected scopes: zero third-party dependencies.
17
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, channels, and dependency-free structural bridges.
19
19
  - [@vielzeug/focus](/focus/): Framework-neutral list navigation and focus restoration primitives.
@@ -29,20 +29,20 @@ Install any package independently: `pnpm add @vielzeug/<name>`
29
29
  - [@vielzeug/orbit](/orbit/): Floating positioning with lifecycle-owned geometry and middleware.
30
30
  - [@vielzeug/ore](/ore/): Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.
31
31
  - [@vielzeug/postmaster](/postmaster/): Typed durable job outbox with leased processing, retries, and dead-letter recovery for browser applications.
32
- - [@vielzeug/prism](/prism/): Responsive SVG charts with explicit updates, accessible interactions, and CSS theming.
32
+ - [@vielzeug/prism](/prism/): Responsive SVG charts with explicit updates, keyboard and screen-reader support, and CSS theming.
33
33
  - [@vielzeug/pulse](/pulse/): Explicitly connected, typed WebSocket sessions with scoped channels, ref-counted rooms with reactive presence, reconnect restoration, and heartbeat.
34
34
  - [@vielzeug/refine](/refine/): Accessible, themeable web components built with Ore for framework and vanilla DOM apps.
35
35
  - [@vielzeug/ripple](/ripple/): Framework-agnostic signals, effects, async resources, scopes, structural bridges, and observability.
36
36
  - [@vielzeug/rune](/rune/): Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.
37
- - [@vielzeug/sandbox](/sandbox/): Isolated iframe runtime with a typed state bridge and explicit message trust boundary for safe execution of untrusted HTML — component previews, playgrounds, plugin sandboxes, and more.
37
+ - [@vielzeug/sandbox](/sandbox/): Isolated iframe runtime with a typed state bridge and explicit message trust boundary for safe execution of untrusted HTML: component previews, playgrounds, plugin sandboxes, and more.
38
38
  - [@vielzeug/scout](/scout/): Trigram-indexed fuzzy search with per-field weights, match highlighting, and an optional reactive layer.
39
39
  - [@vielzeug/scroll](/scroll/): Lightweight, dependency-free virtual list engine with variable heights, sticky headers, grids, and external-store integration.
40
40
  - [@vielzeug/sentinel](/sentinel/): Subscribable browser and DOM snapshots for viewport, network, media query, element size, intersection state, and screen wake lock.
41
- - [@vielzeug/sigil](/sigil/): QR code generation and scanning — pure matrix encoder, SVG and canvas renderers, native BarcodeDetector scanning
41
+ - [@vielzeug/sigil](/sigil/): QR code generation and scanning: pure matrix encoder, SVG and canvas renderers, native BarcodeDetector scanning
42
42
  - [@vielzeug/sourcerer](/sourcerer/): Framework-agnostic local and remote collection state with page, cursor, and infinite pagination.
43
43
  - [@vielzeug/spell](/spell/): Zero-dependency schema validation with explicit async checks, Standard Schema interoperability, and JSON Schema export.
44
44
  - [@vielzeug/tandem](/tandem/): Offline-first sync engine with rev baselines, tombstoned deletions, and idle-batched pushes for browser applications.
45
- - [@vielzeug/tavern](/tavern/): Table sessions over mesh — host-owned state replication with guest command forwarding
45
+ - [@vielzeug/tavern](/tavern/): Table sessions over mesh: host-owned state replication with guest command forwarding
46
46
  - [@vielzeug/tempo](/tempo/): Explicit Temporal parsing, timezone-safe arithmetic, and localized date/time formatting for TypeScript.
47
47
  - [@vielzeug/vault](/vault/): Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.
48
48
  - [@vielzeug/ward](/ward/): Zero-dependency authorization decisions with ordered rules, role helpers, ownership predicates, and default deny.
@@ -4,5 +4,5 @@
4
4
  "refine": "refine.json",
5
5
  "schemaVersion": 1,
6
6
  "search": "search.json",
7
- "version": "3.1.1"
7
+ "version": "26.10.1"
8
8
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "apiSource": "export * 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/abortable';\nexport * from './async/abortError';\nexport * from './async/attempt';\nexport * from './async/backoff';\nexport * from './async/parallel';\nexport * from './async/retry';\nexport * from './async/sleep';\nexport * from './async/taskPool';\nexport * from './async/waitFor';\nexport * from './cache/cache';\nexport * from './cache/memo';\nexport * from './encoding/base64url';\nexport * from './errors';\nexport * from './function/debounce';\nexport * from './function/once';\nexport * from './function/pipe';\nexport * from './function/tapper';\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/allocate';\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/random';\nexport * from './random/source';\nexport * from './string/camelCase';\nexport * from './types';\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: [groupBy, retry, debounce, isEqual, taskPool, cache, fuzzyFilter, tryParseJson, allocate, hash]\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 provides typed, zero-dependency utilities that reduce repeated application code: collection transforms, numeric operations, retry and cancellation, caching, safe-path traversal, and deterministic serialization. The root stays curated while category entry points expose the broader toolkit. Exact platform aliases remain excluded.\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 { groupBy, retry } from '@vielzeug/arsenal';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { taskPool } from '@vielzeug/arsenal/async';\n\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(byRole, health, profile);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\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- **`allocate`**: lossless proportional distribution from `/math`\n- **`chunk`, `zip`, `compact`**: typed collection transforms from `/array`\n- **`range`, `average`, `median`**: common numeric operations from `/math`\n- **`assert`, `tap`**: typed control-flow helpers from `/function`\n- **`isEqual`**: structural equality from package root\n- **`hash`**: deterministic serialization for stable cache keys from `/object`\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
- "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| `groupBy` | Group values by key (prototype-pollution guarded) | Sync | Root export |\n| `chunk` / `zip` / `compact` | Typed collection transforms | Sync | Available from `/array` |\n| `range` / `average` / `median` | Numeric generation and aggregation | Sync | Available from `/math` |\n| `assert` | Assert and narrow a condition | Sync | Available from `/function` |\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 (safe-path guarded) | Sync | Available from `/object` |\n| `allocate` | Lossless proportional distribution | Sync | Available from `/math` |\n| `isEqual` | Structural equality | Sync | Root export |\n| `hash` | Deterministic serialization for cache keys | Sync | Available from `/object` |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/arsenal` | Curated common utilities |\n| `@vielzeug/arsenal/array` | Immutable transforms, set operations, 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` | Assertions, composition, timing, teardown |\n| `@vielzeug/arsenal/guards` | Type guards, predicate combinators, equality guards |\n| `@vielzeug/arsenal/math` | Ranges, aggregation, statistics, interpolation, exact allocation |\n| `@vielzeug/arsenal/object` | Paths, transforms, hash, JSON parse result |\n| `@vielzeug/arsenal/random` | Cryptographic-entropy selection and shuffle |\n| `@vielzeug/arsenal/string` | Unicode-aware case transforms and similarity |\n\n## Array\n\n### Typed transforms\n\n```ts\nchunk<T>(input: readonly T[], size?: number): T[][]\ncompact<T>(array: readonly T[]): Array<Exclude<T, false | '' | 0 | 0n | null | undefined>>\nfirst<T>(array: readonly T[], fallback?: T): T | undefined\nlast<T>(array: readonly T[], fallback?: T): T | undefined\nreplace<T>(array: readonly T[], predicate: (value: T) => boolean, value: T): T[]\nrotate<T>(array: readonly T[], positions: number, options?: { wrap?: boolean }): T[]\nzip(...arrays): Array<tuple>\nunzip(rows): tupleOfArrays\n```\n\n`take`, `takeLast`, `drop`, and `dropLast` return new arrays and normalize negative counts to zero. Set operations accept optional selectors for object identity.\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 entries(): ReadonlyArray<readonly [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`, `set`, and `entries`. `entries()` trims expired values and returns a new insertion-ordered snapshot without exposing internal storage. The `size` getter does not evict expired entries.\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## Encoding\n\nURL-safe base64 (`base64url`) codecs without `btoa`/`Buffer`, so the same code runs in browsers, workers, and Node.\n\n```ts\nimport { base64UrlToBytes, base64UrlToText, bytesToBase64Url, textToBase64Url, utf8Bytes } from '@vielzeug/arsenal';\n\ntextToBase64Url('{\"build\":\"daeron\"}'); // never contains '+', '/', or '='\nbase64UrlToText(textToBase64Url('Primal — The Awakening')); // round-trips any UTF-8 text\nbytesToBase64Url(Uint8Array.from([0, 1, 250])); // byte-level codec for binary payloads\nbase64UrlToBytes(code); // throws on characters outside the alphabet\nutf8Bytes('Hunter äöü'); // 11 — byte length of a UTF-8 string\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### hash\n\n```ts\nhash(value: unknown, options?: HashOptions): string\n```\n\nProduces a deterministic, order-independent serialization. Handles `Date`, `RegExp`, `Set`, `Map`, and `bigint`. Circular references produce a sentinel. Useful for stable cache keys.\n\n---\n\n## Math\n\n### Numeric toolkit\n\n```ts\nrange(stop: number): number[]\nrange(start: number, stop: number, step?: number): number[]\nclamp(value: number, min?: number, max?: number): number\nsum<T>(values: readonly T[], select?: (value: T) => number): number\naverage<T>(values: readonly T[], select?: (value: T) => number): number | undefined\nmedian<T>(values: readonly T[], select?: (value: T) => number): number | undefined\nvariance<T>(values: readonly T[], select?: (value: T) => number): number\nstandardDeviation<T>(values: readonly T[], select?: (value: T) => number): number\n```\n\n`sum` and `average` reject non-finite values. `gcd` and `lcm` require safe integers. `linspace` requires finite bounds and a positive integer point count. `variance` and `standardDeviation` compute population statistics.\n\n### allocate\n\n```ts\nallocate(amount: number, ratios: number[] | number): number[]\nallocate(amount: bigint, ratios: number[] | number): bigint[]\n```\n\nDistributes an amount proportionally across ratios. The indivisible remainder is applied to the last bucket so the sum equals the original amount exactly — critical for financial operations.\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
- "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 utilities. Move to a category entry point for the broader typed toolkit. Arsenal keeps helpers that improve readability, narrowing, immutable updates, or edge-case consistency; exact platform aliases stay out.\n\n```ts\nimport { 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 byRole = groupBy(users, (user) => user.role);\nconst health = await retry(() => fetch('/health').then((response) => response.json()));\n\nconsole.log(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\nconsole.log(profiles.entries());\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, randomFloat, type RandomSource } from '@vielzeug/arsenal/random';\n\nconst source: RandomSource = { next: () => 0.5 };\n\nrandomFloat(source); // 0.5\nrandom(1, 4, source); // 3\n```\n\n## Encode Text for URLs and QR Codes\n\nShare codes ride in URLs and QR payloads, where `+`, `/`, and padding `=` break consumers. The base64url codecs are environment-independent — no `btoa`, no `Buffer` — so the same encode/decode pair works in the browser, in workers, and in Node.\n\n```ts\nimport { base64UrlToText, textToBase64Url } from '@vielzeug/arsenal';\n\nconst code = textToBase64Url(JSON.stringify({ hunterId: 'daeron', version: 2 }));\n// eyJodW50ZXJJZCI6ImRhZXJvbiIsInZlcnNpb24iOjJ9 — safe in a query string or QR\n\nconst payload = JSON.parse(base64UrlToText(code));\n```\n\nDecoding throws on characters outside the base64url alphabet, so corrupted input fails loudly instead of decoding to garbage.\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 utilities from package root.\n- Import the broader toolkit from category entry points.\n- Prefer Arsenal when a helper improves typing, intent, immutable updates, or edge-case behavior.\n- Use the platform directly for exact aliases such as `crypto.randomUUID()` and `Array.prototype.flat()`.\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
- "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`, `hash`, `diff`, `deepMerge`\n- [Random utilities](./examples/random.md)\n- [String utilities](./examples/string.md)\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: [groupBy, retry, debounce, isEqual, taskPool, cache, fuzzyFilter, tryParseJson, allocate, hash]\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 provides typed, zero-dependency utilities that reduce repeated application code: collection transforms, numeric operations, retry and cancellation, caching, safe-path traversal, and deterministic serialization. The root stays curated while category entry points expose the broader toolkit. Exact platform aliases remain excluded.\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 { groupBy, retry } from '@vielzeug/arsenal';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { taskPool } from '@vielzeug/arsenal/async';\n\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(byRole, health, profile);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\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- **`allocate`**: lossless proportional distribution from `/math`\n- **`chunk`, `zip`, `compact`**: typed collection transforms from `/array`\n- **`range`, `average`, `median`**: common numeric operations from `/math`\n- **`assert`, `tap`**: typed control-flow helpers from `/function`\n- **`isEqual`**: structural equality from package root\n- **`hash`**: deterministic serialization for stable cache keys from `/object`\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
+ "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| `groupBy` | Group values by key (prototype-pollution guarded) | Sync | Root export |\n| `chunk` / `zip` / `compact` | Typed collection transforms | Sync | Available from `/array` |\n| `range` / `average` / `median` | Numeric generation and aggregation | Sync | Available from `/math` |\n| `assert` | Assert and narrow a condition | Sync | Available from `/function` |\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 (safe-path guarded) | Sync | Available from `/object` |\n| `allocate` | Lossless proportional distribution | Sync | Available from `/math` |\n| `isEqual` | Structural equality | Sync | Root export |\n| `hash` | Deterministic serialization for cache keys | Sync | Available from `/object` |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/arsenal` | Curated common utilities |\n| `@vielzeug/arsenal/array` | Immutable transforms, set operations, 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` | Assertions, composition, timing, teardown |\n| `@vielzeug/arsenal/guards` | Type guards, predicate combinators, equality guards |\n| `@vielzeug/arsenal/math` | Ranges, aggregation, statistics, interpolation, exact allocation |\n| `@vielzeug/arsenal/object` | Paths, transforms, hash, JSON parse result |\n| `@vielzeug/arsenal/random` | Cryptographic-entropy selection and shuffle |\n| `@vielzeug/arsenal/string` | Unicode-aware case transforms and similarity |\n\n## Array\n\n### Typed transforms\n\n```ts\nchunk<T>(input: readonly T[], size?: number): T[][]\ncompact<T>(array: readonly T[]): Array<Exclude<T, false | '' | 0 | 0n | null | undefined>>\nfirst<T>(array: readonly T[], fallback?: T): T | undefined\nlast<T>(array: readonly T[], fallback?: T): T | undefined\nreplace<T>(array: readonly T[], predicate: (value: T) => boolean, value: T): T[]\nrotate<T>(array: readonly T[], positions: number, options?: { wrap?: boolean }): T[]\nzip(...arrays): Array<tuple>\nunzip(rows): tupleOfArrays\n```\n\n`take`, `takeLast`, `drop`, and `dropLast` return new arrays and normalize negative counts to zero. Set operations accept optional selectors for object identity.\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 entries(): ReadonlyArray<readonly [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`, `set`, and `entries`. `entries()` trims expired values and returns a new insertion-ordered snapshot without exposing internal storage. The `size` getter does not evict expired entries.\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## Encoding\n\nURL-safe base64 (`base64url`) codecs without `btoa`/`Buffer`, so the same code runs in browsers, workers, and Node.\n\n```ts\nimport { base64UrlToBytes, base64UrlToText, bytesToBase64Url, textToBase64Url, utf8Bytes } from '@vielzeug/arsenal';\n\ntextToBase64Url('{\"build\":\"hero\"}'); // never contains '+', '/', or '='\nbase64UrlToText(textToBase64Url('League of Legends')); // round-trips any UTF-8 text\nbytesToBase64Url(Uint8Array.from([0, 1, 250])); // byte-level codec for binary payloads\nbase64UrlToBytes(code); // throws on characters outside the alphabet\nutf8Bytes('Hunter äöü'); // 11: byte length of a UTF-8 string\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### hash\n\n```ts\nhash(value: unknown, options?: HashOptions): string\n```\n\nProduces a deterministic, order-independent serialization. Handles `Date`, `RegExp`, `Set`, `Map`, and `bigint`. Circular references produce a sentinel. Useful for stable cache keys.\n\n---\n\n## Math\n\n### Numeric toolkit\n\n```ts\nrange(stop: number): number[]\nrange(start: number, stop: number, step?: number): number[]\nclamp(value: number, min?: number, max?: number): number\nsum<T>(values: readonly T[], select?: (value: T) => number): number\naverage<T>(values: readonly T[], select?: (value: T) => number): number | undefined\nmedian<T>(values: readonly T[], select?: (value: T) => number): number | undefined\nvariance<T>(values: readonly T[], select?: (value: T) => number): number\nstandardDeviation<T>(values: readonly T[], select?: (value: T) => number): number\n```\n\n`sum` and `average` reject non-finite values. `gcd` and `lcm` require safe integers. `linspace` requires finite bounds and a positive integer point count. `variance` and `standardDeviation` compute population statistics.\n\n### allocate\n\n```ts\nallocate(amount: number, ratios: number[] | number): number[]\nallocate(amount: bigint, ratios: number[] | number): bigint[]\n```\n\nDistributes an amount proportionally across ratios. The indivisible remainder is applied to the last bucket so the sum equals the original amount exactly: critical for financial operations.\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
+ "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 utilities. Move to a category entry point for the broader typed toolkit. Arsenal keeps helpers that improve readability, narrowing, immutable updates, or edge-case consistency; exact platform aliases stay out.\n\n```ts\nimport { 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 byRole = groupBy(users, (user) => user.role);\nconst health = await retry(() => fetch('/health').then((response) => response.json()));\n\nconsole.log(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) =>\n product.published ? `${product.id}: ${product.price}` : undefined,\n);\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\nconsole.log(profiles.entries());\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, randomFloat, type RandomSource } from '@vielzeug/arsenal/random';\n\nconst source: RandomSource = { next: () => 0.5 };\n\nrandomFloat(source); // 0.5\nrandom(1, 4, source); // 3\n```\n\n## Encode Text for URLs and QR Codes\n\nShare codes ride in URLs and QR payloads, where `+`, `/`, and padding `=` break consumers. The base64url codecs are environment-independent: no `btoa`, no `Buffer`, so the same encode/decode pair works in the browser, in workers, and in Node.\n\n```ts\nimport { base64UrlToText, textToBase64Url } from '@vielzeug/arsenal';\n\nconst code = textToBase64Url(JSON.stringify({ seat: 4, version: 2 }));\n// eyJzZWF0Ijo0LCJ2ZXJzaW9uIjoyfQ: safe in a query string or QR\n\nconst payload = JSON.parse(base64UrlToText(code));\n```\n\nDecoding throws on characters outside the base64url alphabet, so corrupted input fails loudly instead of decoding to garbage.\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 utilities from package root.\n- Import the broader toolkit from category entry points.\n- Prefer Arsenal when a helper improves typing, intent, immutable updates, or edge-case behavior.\n- Use the platform directly for exact aliases such as `crypto.randomUUID()` and `Array.prototype.flat()`.\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
+ "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`, `hash`, `diff`, `deepMerge`\n- [Random utilities](./examples/random.md)\n- [String utilities](./examples/string.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {
@@ -44,7 +44,7 @@
44
44
  },
45
45
  {
46
46
  "id": "async-attempt",
47
- "code": "import { attempt, isFail, isOk, retry } from '@vielzeug/arsenal/async'\n\n// attempt() wraps any async function — never throws, always returns { ok, value|error }\nconst ok = await attempt(async () => {\n await new Promise(r => setTimeout(r, 10))\n return { id: 1, title: 'Buy groceries' }\n})\n\nif (isOk(ok)) {\n console.log('Success:', ok.value)\n}\n\n// Failure path — errors are captured, not thrown\nconst fail = await attempt(async () => {\n throw new Error('network timeout')\n})\n\nif (isFail(fail)) {\n console.log('Caught:', fail.error.message) // 'network timeout'\n}\n\n// Combine with retry() for resilient operations\nlet calls = 0\nconst result = await attempt(() =>\n retry(async () => {\n calls++\n if (calls < 3) throw new Error('not ready')\n return 'ready'\n }, { times: 5, delay: 10 })\n)\n\nconsole.log('ok?', result.ok) // true\nconsole.log('value:', result.value) // 'ready'\nconsole.log('calls:', calls) // 3",
47
+ "code": "import { attempt, isFail, isOk, retry } from '@vielzeug/arsenal/async'\n\n// attempt() wraps any async function: never throws, always returns { ok, value|error }\nconst ok = await attempt(async () => {\n await new Promise(r => setTimeout(r, 10))\n return { id: 1, title: 'Buy groceries' }\n})\n\nif (isOk(ok)) {\n console.log('Success:', ok.value)\n}\n\n// Failure path: errors are captured, not thrown\nconst fail = await attempt(async () => {\n throw new Error('network timeout')\n})\n\nif (isFail(fail)) {\n console.log('Caught:', fail.error.message) // 'network timeout'\n}\n\n// Combine with retry() for resilient operations\nlet calls = 0\nconst result = await attempt(() =>\n retry(async () => {\n calls++\n if (calls < 3) throw new Error('not ready')\n return 'ready'\n }, { times: 5, delay: 10 })\n)\n\nconsole.log('ok?', result.ok) // true\nconsole.log('value:', result.value) // 'ready'\nconsole.log('calls:', calls) // 3",
48
48
  "name": "attempt - Safe async execution with isFail/isOk helpers"
49
49
  },
50
50
  {
@@ -79,7 +79,7 @@
79
79
  },
80
80
  {
81
81
  "id": "function-memo",
82
- "code": "import { memo } from '@vielzeug/arsenal/cache'\n\n// LRU cache capped at 3 entries — oldest evicted when full\nlet callCount = 0\nconst compute = memo(\n (n) => { callCount++; return n * n },\n { maxSize: 3 }\n)\n\nconsole.log(compute(2)) // 4 — computed\nconsole.log(compute(3)) // 9 — computed\nconsole.log(compute(4)) // 16 — computed\nconsole.log(compute(2)) // 4 — cache hit\nconsole.log('calls so far:', callCount) // 3\nconsole.log('cached entries:', compute.size) // 3\n\n// Adding a 4th entry evicts the oldest (key 2)\nconsole.log(compute(5)) // 25 — computed, evicts 2\nconsole.log('after 4th entry, size:', compute.size) // 3\n\n// Invalidate a specific entry\ncompute.invalidate(3)\nconsole.log('after invalidate(3), size:', compute.size) // 2\nconsole.log(compute(3)) // 9 — recomputed\nconsole.log('total calls:', callCount) // 5",
82
+ "code": "import { memo } from '@vielzeug/arsenal/cache'\n\n// LRU cache capped at 3 entries: oldest evicted when full\nlet callCount = 0\nconst compute = memo(\n (n) => { callCount++; return n * n },\n { maxSize: 3 }\n)\n\nconsole.log(compute(2)) // 4: computed\nconsole.log(compute(3)) // 9: computed\nconsole.log(compute(4)) // 16: computed\nconsole.log(compute(2)) // 4: cache hit\nconsole.log('calls so far:', callCount) // 3\nconsole.log('cached entries:', compute.size) // 3\n\n// Adding a 4th entry evicts the oldest (key 2)\nconsole.log(compute(5)) // 25: computed, evicts 2\nconsole.log('after 4th entry, size:', compute.size) // 3\n\n// Invalidate a specific entry\ncompute.invalidate(3)\nconsole.log('after invalidate(3), size:', compute.size) // 2\nconsole.log(compute(3)) // 9: recomputed\nconsole.log('total calls:', callCount) // 5",
83
83
  "name": "memo - LRU cache with size tracking and invalidation"
84
84
  },
85
85
  {
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "id": "function-runAll",
92
- "code": "import { runAll } from '@vielzeug/arsenal/function'\n\n// Run every teardown function — collect errors instead of stopping on first failure\nconst log = []\n\nconst teardowns = [\n () => { log.push('cleanup A'); },\n () => { log.push('cleanup B'); throw new Error('B failed'); },\n () => { log.push('cleanup C'); },\n]\n\ntry {\n runAll(teardowns, { reverse: true }) // LIFO order matches setup-teardown semantics\n} catch (err) {\n console.log('errors collected:', err instanceof AggregateError) // true\n console.log('error count:', err.errors.length) // 1\n console.log('still ran:', log) // ['cleanup C', 'cleanup B', 'cleanup A']\n}\n\n// Without failures — just runs all in order\nconst steps = []\nrunAll([() => steps.push(1), () => steps.push(2), () => steps.push(3)])\nconsole.log('steps:', steps) // [1, 2, 3]",
92
+ "code": "import { runAll } from '@vielzeug/arsenal/function'\n\n// Run every teardown function: collect errors instead of stopping on first failure\nconst log = []\n\nconst teardowns = [\n () => { log.push('cleanup A'); },\n () => { log.push('cleanup B'); throw new Error('B failed'); },\n () => { log.push('cleanup C'); },\n]\n\ntry {\n runAll(teardowns, { reverse: true }) // LIFO order matches setup-teardown semantics\n} catch (err) {\n console.log('errors collected:', err instanceof AggregateError) // true\n console.log('error count:', err.errors.length) // 1\n console.log('still ran:', log) // ['cleanup C', 'cleanup B', 'cleanup A']\n}\n\n// Without failures: just runs all in order\nconst steps = []\nrunAll([() => steps.push(1), () => steps.push(2), () => steps.push(3)])\nconsole.log('steps:', steps) // [1, 2, 3]",
93
93
  "name": "runAll - Run all callbacks, collect errors"
94
94
  },
95
95
  {
@@ -114,7 +114,7 @@
114
114
  },
115
115
  {
116
116
  "id": "object-diffArrays",
117
- "code": "import { diffArrays } from '@vielzeug/arsenal/object'\n\n// Default 'set' strategy — order independent\nconst v1 = [1, 2, 3]\nconst v2 = [2, 3, 4]\nconst setDiff = diffArrays(v1, v2)\nconsole.log('set diff:', setDiff) // { added: [4], removed: [1] }\n\n// 'lcs' strategy — ordered minimal diff\nconst before = [1, 2, 3, 4, 5]\nconst after = [1, 3, 4, 5, 6]\nconst lcsDiff = diffArrays(before, after, { strategy: 'lcs' })\nconsole.log('lcs diff:', lcsDiff) // { added: [6], removed: [2] }\n\n// With custom compareFn for objects\nconst oldUsers = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]\nconst newUsers = [{ id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]\nconst userDiff = diffArrays(oldUsers, newUsers, { compareFn: (a, b) => a.id === b.id })\nconsole.log('users:', userDiff) // { added: [{id:3,...}], removed: [{id:1,...}] }",
117
+ "code": "import { diffArrays } from '@vielzeug/arsenal/object'\n\n// Default 'set' strategy: order independent\nconst v1 = [1, 2, 3]\nconst v2 = [2, 3, 4]\nconst setDiff = diffArrays(v1, v2)\nconsole.log('set diff:', setDiff) // { added: [4], removed: [1] }\n\n// 'lcs' strategy: ordered minimal diff\nconst before = [1, 2, 3, 4, 5]\nconst after = [1, 3, 4, 5, 6]\nconst lcsDiff = diffArrays(before, after, { strategy: 'lcs' })\nconsole.log('lcs diff:', lcsDiff) // { added: [6], removed: [2] }\n\n// With custom compareFn for objects\nconst oldUsers = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]\nconst newUsers = [{ id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]\nconst userDiff = diffArrays(oldUsers, newUsers, { compareFn: (a, b) => a.id === b.id })\nconsole.log('users:', userDiff) // { added: [{id:3,...}], removed: [{id:1,...}] }",
118
118
  "name": "diffArrays - Set and LCS strategies"
119
119
  },
120
120
  {
@@ -124,7 +124,7 @@
124
124
  },
125
125
  {
126
126
  "id": "object-hash",
127
- "code": "import { hash } from '@vielzeug/arsenal/object'\n\n// Stable cache key regardless of object key insertion order\nconst key1 = hash({ sort: 'asc', filter: { role: 'admin' } })\nconst key2 = hash({ filter: { role: 'admin' }, sort: 'asc' })\nconsole.log('Same key?', key1 === key2) // true\nconsole.log('Key:', key1) // '{\"filter\":{\"role\":\"admin\"},\"sort\":\"asc\"}'\n\n// Handles Date, RegExp, Set, Map, bigint\nconsole.log(hash(new Date('2024-01-01T00:00:00Z'))) // '[Date:2024-01-01T00:00:00.000Z]'\nconsole.log(hash(new Set([3, 1, 2]))) // '[Set:1,2,3]' — sorted\nconsole.log(hash(new Map([['b', 2], ['a', 1]]))) // '[Map:\"a\"=>1,\"b\"=>2]' — sorted\nconsole.log(hash(42n)) // '42n'\nconsole.log(hash(/foo/gi)) // '[RegExp:foo/gi]'\n\n// Circular references produce a sentinel — no stack overflow\nconst obj = { x: 1 }\nobj.self = obj\nconsole.log(hash(obj)) // '{\"self\":[Circular],\"x\":1}'\n\n// Class instances coerce to String(instance) by default\nclass Point {\n constructor(x, y) { this.x = x; this.y = y }\n toString() { return `Point(${this.x},${this.y})` }\n}\nconsole.log(hash(new Point(1, 2))) // 'Point(1,2)'",
127
+ "code": "import { hash } from '@vielzeug/arsenal/object'\n\n// Stable cache key regardless of object key insertion order\nconst key1 = hash({ sort: 'asc', filter: { role: 'admin' } })\nconst key2 = hash({ filter: { role: 'admin' }, sort: 'asc' })\nconsole.log('Same key?', key1 === key2) // true\nconsole.log('Key:', key1) // '{\"filter\":{\"role\":\"admin\"},\"sort\":\"asc\"}'\n\n// Handles Date, RegExp, Set, Map, bigint\nconsole.log(hash(new Date('2024-01-01T00:00:00Z'))) // '[Date:2024-01-01T00:00:00.000Z]'\nconsole.log(hash(new Set([3, 1, 2]))) // '[Set:1,2,3]': sorted\nconsole.log(hash(new Map([['b', 2], ['a', 1]]))) // '[Map:\"a\"=>1,\"b\"=>2]': sorted\nconsole.log(hash(42n)) // '42n'\nconsole.log(hash(/foo/gi)) // '[RegExp:foo/gi]'\n\n// Circular references produce a sentinel: no stack overflow\nconst obj = { x: 1 }\nobj.self = obj\nconsole.log(hash(obj)) // '{\"self\":[Circular],\"x\":1}'\n\n// Class instances coerce to String(instance) by default\nclass Point {\n constructor(x, y) { this.x = x; this.y = y }\n toString() { return `Point(${this.x},${this.y})` }\n}\nconsole.log(hash(new Point(1, 2))) // 'Point(1,2)'",
128
128
  "name": "hash - Deterministic cache key from any value"
129
129
  },
130
130
  {
@@ -154,7 +154,7 @@
154
154
  },
155
155
  {
156
156
  "id": "typed-is",
157
- "code": "import { allOf, anyOf, isEmpty, isEqual, isMatch, isPlainObject, noneOf, shallowEqual } from '@vielzeug/arsenal/guards'\n\nconst values = ['hello', 42, [1, 2, 3], {}, null]\n\nvalues.forEach(value => {\n console.log({\n empty: isEmpty(value),\n plainObject: isPlainObject(value),\n })\n})\n\n// Deep structural equality (handles nested objects, arrays, Dates, Maps, Sets)\nconsole.log('deep equal:', isEqual({ a: [1, 2] }, { a: [1, 2] })) // true\n\n// Shallow equality (one level, uses Object.is so NaN === NaN)\nconsole.log('shallow equal:', shallowEqual([1, 2, 3], [1, 2, 3])) // true\n\n// Partial deep match — source properties must be present and equal\nconsole.log('is match:', isMatch({ a: 1, b: 2, c: 3 }, { a: 1 })) // true\n\n// Predicate combinators with vacuous-truth semantics\nconst isWorkingAge = allOf<number>(\n (n) => n >= 18,\n (n) => n < 65,\n)\nconst isSpecial = anyOf<number>(\n (n) => n === 0,\n (n) => n === 100,\n)\nconst isOdd = noneOf((n) => n % 2 === 0)\n\nconsole.log('working age:', isWorkingAge(30)) // true\nconsole.log('special:', isSpecial(100)) // true\nconsole.log('odd:', [1, 2, 3, 4].filter(isOdd)) // [1, 3]",
157
+ "code": "import { allOf, anyOf, isEmpty, isEqual, isMatch, isPlainObject, noneOf, shallowEqual } from '@vielzeug/arsenal/guards'\n\nconst values = ['hello', 42, [1, 2, 3], {}, null]\n\nvalues.forEach(value => {\n console.log({\n empty: isEmpty(value),\n plainObject: isPlainObject(value),\n })\n})\n\n// Deep structural equality (handles nested objects, arrays, Dates, Maps, Sets)\nconsole.log('deep equal:', isEqual({ a: [1, 2] }, { a: [1, 2] })) // true\n\n// Shallow equality (one level, uses Object.is so NaN === NaN)\nconsole.log('shallow equal:', shallowEqual([1, 2, 3], [1, 2, 3])) // true\n\n// Partial deep match: source properties must be present and equal\nconsole.log('is match:', isMatch({ a: 1, b: 2, c: 3 }, { a: 1 })) // true\n\n// Predicate combinators with vacuous-truth semantics\nconst isWorkingAge = allOf<number>(\n (n) => n >= 18,\n (n) => n < 65,\n)\nconst isSpecial = anyOf<number>(\n (n) => n === 0,\n (n) => n === 100,\n)\nconst isOdd = noneOf((n) => n % 2 === 0)\n\nconsole.log('working age:', isWorkingAge(30)) // true\nconsole.log('special:', isSpecial(100)) // true\nconsole.log('odd:', [1, 2, 3, 4].filter(isOdd)) // [1, 3]",
158
158
  "name": "Guards, equality, and predicate combinators"
159
159
  }
160
160
  ],
@@ -177,7 +177,7 @@
177
177
  "backoff": "export function backoff(attempt: number, maxMs = 30_000): number {\n const n = Math.max(0, Number.isNaN(attempt) ? 0 : Math.floor(attempt));\n\n return Math.min(1000 * 2 ** n, maxMs);\n}",
178
178
  "parallel": "export async function parallel<T, R>(\n array: readonly T[],\n callback: (item: T, index: number, array: readonly T[]) => Promise<R> | R,\n options: { abortOnError?: boolean; limit?: number; signal?: AbortSignal } = {},\n): Promise<R[]> {\n const { abortOnError = false, limit = Infinity, signal } = options;\n\n if (limit < 1) {\n throw new RangeError(`parallel: limit must be at least 1, got ${limit}`);\n }\n\n if (signal?.aborted) {\n throw abortError(signal);\n }\n\n if (array.length === 0) {\n return [];\n }\n\n const results: R[] = new Array(array.length);\n let currentIndex = 0;\n const failFastController = abortOnError ? new AbortController() : undefined;\n\n const worker = async (): Promise<void> => {\n while (true) {\n if (signal?.aborted) {\n throw abortError(signal);\n }\n\n if (failFastController?.signal.aborted) {\n return;\n }\n\n if (currentIndex >= array.length) {\n return;\n }\n\n const index = currentIndex++;\n\n try {\n results[index] = await callback(array[index]!, index, array);\n } catch (err) {\n failFastController?.abort();\n throw err;\n }\n }\n };\n\n const workerCount = Math.min(limit, array.length);\n const workers = Array.from({ length: workerCount }, () => worker());\n\n await Promise.all(workers);\n\n return results;\n}",
179
179
  "RetryOptions": "export type RetryOptions = {\n delay?: number | ((attempt: number) => number);\n shouldRetry?: (error: unknown, attempt: number) => boolean;\n signal?: AbortSignal;\n timeout?: number;\n times?: number;\n};",
180
- "retry": "export async function retry<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n { delay = 250, shouldRetry, signal, timeout, times = 3 }: RetryOptions = {},\n): Promise<T> {\n if (!Number.isInteger(times) || times < 1) {\n throw new RangeError(`retry: times must be a positive integer, got ${times}`);\n }\n\n for (let tryCount = 1; tryCount <= times; tryCount++) {\n if (signal?.aborted) throw abortError(signal);\n\n const callSignal = buildSignal(timeout, signal);\n\n try {\n return await fn(callSignal);\n } catch (err) {\n if (tryCount === times) throw err;\n\n if (shouldRetry && !shouldRetry(err, tryCount - 1)) throw err;\n\n const ms = typeof delay === 'function' ? delay(tryCount - 1) : delay;\n\n if (ms > 0) await sleep(ms, signal);\n }\n }\n\n /* unreachable — the loop always returns or throws before exhausting all attempts */\n return undefined as never;\n}",
180
+ "retry": "export async function retry<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n { delay = 250, shouldRetry, signal, timeout, times = 3 }: RetryOptions = {},\n): Promise<T> {\n if (!Number.isInteger(times) || times < 1) {\n throw new RangeError(`retry: times must be a positive integer, got ${times}`);\n }\n\n for (let tryCount = 1; tryCount <= times; tryCount++) {\n if (signal?.aborted) throw abortError(signal);\n\n const callSignal = buildSignal(timeout, signal);\n\n try {\n return await fn(callSignal);\n } catch (err) {\n if (tryCount === times) throw err;\n\n if (shouldRetry && !shouldRetry(err, tryCount - 1)) throw err;\n\n const ms = typeof delay === 'function' ? delay(tryCount - 1) : delay;\n\n if (ms > 0) await sleep(ms, signal);\n }\n }\n\n /* unreachable: the loop always returns or throws before exhausting all attempts */\n return undefined as never;\n}",
181
181
  "sleep": "export async function sleep(timeout: number, signal?: AbortSignal): Promise<void> {\n if (!signal) {\n return new Promise((resolve) => setTimeout(resolve, timeout));\n }\n\n if (signal.aborted) {\n return Promise.reject(abortError(signal));\n }\n\n return new Promise<void>((resolve, reject) => {\n const id = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, timeout);\n\n const onAbort = () => {\n clearTimeout(id);\n signal.removeEventListener('abort', onAbort);\n reject(abortError(signal));\n };\n\n signal.addEventListener('abort', onAbort);\n });\n}",
182
182
  "TaskPool": "export interface TaskPool {\n readonly active: number;\n readonly disposalSignal: AbortSignal;\n dispose(reason?: unknown): void;\n readonly disposed: boolean;\n idle(): Promise<void>;\n readonly pending: number;\n run<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>;\n [Symbol.dispose](): void;\n}",
183
183
  "taskPool": "export function taskPool({ concurrency = 1 }: { concurrency?: number } = {}): TaskPool {\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new RangeError('taskPool: concurrency must be a positive integer');\n }\n\n const controller = new AbortController();\n const pending: PendingTask[] = [];\n let active = 0;\n let disposed = false;\n let idleDeferred: ReturnType<typeof Promise.withResolvers<void>> | undefined;\n\n const resolveIdle = (): void => {\n if (active === 0 && pending.length === 0) {\n idleDeferred?.resolve();\n idleDeferred = undefined;\n }\n };\n\n const start = (): void => {\n while (active < concurrency && pending.length > 0) {\n const next = pending.shift()!;\n\n active++;\n void Promise.resolve()\n .then(() => next.run(controller.signal))\n .then(next.resolve, next.reject)\n .finally(() => {\n active--;\n start();\n resolveIdle();\n });\n }\n };\n\n return {\n get active(): number {\n return active;\n },\n get disposalSignal(): AbortSignal {\n return controller.signal;\n },\n dispose: (reason: unknown = new DOMException('Task pool disposed', 'AbortError')): void => {\n if (disposed) return;\n\n disposed = true;\n controller.abort(reason);\n\n for (const task of pending.splice(0)) task.reject(reason);\n\n resolveIdle();\n },\n get disposed(): boolean {\n return disposed;\n },\n idle: (): Promise<void> => {\n if (active === 0 && pending.length === 0) return Promise.resolve();\n\n idleDeferred ??= Promise.withResolvers<void>();\n\n return idleDeferred.promise;\n },\n get pending(): number {\n return pending.length;\n },\n run: <T>(task: (signal: AbortSignal) => Promise<T>): Promise<T> => {\n const deferred = Promise.withResolvers<T>();\n\n if (disposed) {\n deferred.reject(controller.signal.reason);\n\n return deferred.promise;\n }\n\n pending.push({\n reject: deferred.reject,\n resolve: deferred.resolve as (value: unknown) => void,\n run: task as (signal: AbortSignal) => Promise<unknown>,\n });\n start();\n\n return deferred.promise;\n },\n [Symbol.dispose](): void {\n this.dispose();\n },\n };\n}",
@@ -198,7 +198,7 @@
198
198
  "ArsenalSerializationError": "export class ArsenalSerializationError extends ArsenalError {}",
199
199
  "DebounceOptions": "export type DebounceOptions = {\n /** Invoke on the leading edge of the delay window. Default: `false`. */\n leading?: boolean;\n /** Invoke on the trailing edge of the delay window. Default: `true`. */\n trailing?: boolean;\n};",
200
200
  "Debounced": "export type Debounced<T extends Fn> = ((...args: Parameters<T>) => ReturnType<T> | undefined) & {\n cancel(): void;\n flush(): ReturnType<T> | undefined;\n pending(): boolean;\n};",
201
- "debounce": "export function debounce<T extends Fn>(\n fn: T,\n delay = 300,\n options: DebounceOptions = { leading: false, trailing: true },\n): Debounced<T> {\n const leading = options.leading ?? false;\n const trailing = options.trailing ?? true;\n\n if (!leading && !trailing) {\n warn('debounce: both `leading` and `trailing` are false — the wrapped function will never be invoked.');\n }\n\n let timerId: ReturnType<typeof setTimeout> | undefined;\n let lastArgs: Parameters<T> | undefined;\n let lastResult: ReturnType<T> | undefined;\n let leadingFired = false;\n\n const clearTimer = () => {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n timerId = undefined;\n }\n };\n\n const invokeTrailing = () => {\n clearTimer();\n leadingFired = false;\n\n if (!lastArgs) return undefined;\n\n const args = lastArgs;\n\n lastArgs = undefined;\n lastResult = fn(...args) as ReturnType<T>;\n\n return lastResult;\n };\n\n return Object.assign(\n (...args: Parameters<T>): ReturnType<T> | undefined => {\n lastArgs = args;\n\n if (leading && !leadingFired) {\n leadingFired = true;\n lastResult = fn(...args) as ReturnType<T>;\n // Clear args regardless of `trailing` — the leading call already consumed this\n // invocation. The trailing timer below only re-invokes `fn` if a *further* call\n // arrives during the cooldown window and repopulates `lastArgs`.\n lastArgs = undefined;\n\n if (!trailing) {\n // Leading-only: start cooldown — no trailing call needed\n timerId = setTimeout(() => {\n timerId = undefined;\n leadingFired = false;\n }, delay);\n\n return lastResult;\n }\n }\n\n // When leading-only, ignore calls within the cooldown window\n if (leading && !trailing && leadingFired) return lastResult;\n\n clearTimer();\n timerId = setTimeout(invokeTrailing, delay);\n\n return lastResult;\n },\n {\n cancel: () => {\n clearTimer();\n lastArgs = undefined;\n leadingFired = false;\n },\n flush: (): ReturnType<T> | undefined => invokeTrailing(),\n pending: () => timerId !== undefined,\n },\n );\n}",
201
+ "debounce": "export function debounce<T extends Fn>(\n fn: T,\n delay = 300,\n options: DebounceOptions = { leading: false, trailing: true },\n): Debounced<T> {\n const leading = options.leading ?? false;\n const trailing = options.trailing ?? true;\n\n if (!leading && !trailing) {\n warn('debounce: both `leading` and `trailing` are false: the wrapped function will never be invoked.');\n }\n\n let timerId: ReturnType<typeof setTimeout> | undefined;\n let lastArgs: Parameters<T> | undefined;\n let lastResult: ReturnType<T> | undefined;\n let leadingFired = false;\n\n const clearTimer = () => {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n timerId = undefined;\n }\n };\n\n const invokeTrailing = () => {\n clearTimer();\n leadingFired = false;\n\n if (!lastArgs) return undefined;\n\n const args = lastArgs;\n\n lastArgs = undefined;\n lastResult = fn(...args) as ReturnType<T>;\n\n return lastResult;\n };\n\n return Object.assign(\n (...args: Parameters<T>): ReturnType<T> | undefined => {\n lastArgs = args;\n\n if (leading && !leadingFired) {\n leadingFired = true;\n lastResult = fn(...args) as ReturnType<T>;\n // Clear args regardless of `trailing`: the leading call already consumed this\n // invocation. The trailing timer below only re-invokes `fn` if a *further* call\n // arrives during the cooldown window and repopulates `lastArgs`.\n lastArgs = undefined;\n\n if (!trailing) {\n // Leading-only: start cooldown: no trailing call needed\n timerId = setTimeout(() => {\n timerId = undefined;\n leadingFired = false;\n }, delay);\n\n return lastResult;\n }\n }\n\n // When leading-only, ignore calls within the cooldown window\n if (leading && !trailing && leadingFired) return lastResult;\n\n clearTimer();\n timerId = setTimeout(invokeTrailing, delay);\n\n return lastResult;\n },\n {\n cancel: () => {\n clearTimer();\n lastArgs = undefined;\n leadingFired = false;\n },\n flush: (): ReturnType<T> | undefined => invokeTrailing(),\n pending: () => timerId !== undefined,\n },\n );\n}",
202
202
  "Once": "export type Once<T extends Fn> = T & { reset: () => void };",
203
203
  "once": "export const once = <T extends Fn>(fn: T): Once<T> => {\n let result: ReturnType<T> | undefined;\n let called = false;\n\n const wrappedFn = ((...args: Parameters<T>): ReturnType<T> => {\n if (!called) {\n result = fn(...args) as ReturnType<T>;\n called = true;\n }\n\n return result as ReturnType<T>;\n }) as Once<T>;\n\n wrappedFn.reset = () => {\n result = undefined;\n called = false;\n };\n\n return wrappedFn;\n};",
204
204
  "pipe": "export function pipe(): <T>(x: T) => T;\n\nexport function pipe<T extends readonly [Fn, ...Fn[]]>(...fns: T): (...args: FirstParameters<T>) => LastReturnType<T>;\n\nexport function pipe(...fns: Fn[]): Fn {\n if (fns.length === 0) return <T>(x: T): T => x;\n\n const [firstFn, ...restFns] = fns;\n\n return (...args: never[]) =>\n restFns.reduce<unknown>((prev, fn) => (fn as (x: unknown) => unknown)(prev), firstFn(...args));\n}",
@@ -221,7 +221,7 @@
221
221
  "getPath": "export function getPath<T extends Obj, P extends string>(item: T, path: P): PathValue<T, P> | undefined {\n return readPath(item, path) as PathValue<T, P> | undefined;\n}",
222
222
  "getPathOr": "export function getPathOr<T extends Obj, P extends string, F>(item: T, path: P, fallback: F): PathValue<T, P> | F {\n const value = readPath(item, path);\n\n return (value === undefined ? fallback : value) as PathValue<T, P> | F;\n}",
223
223
  "requirePath": "export function requirePath<T extends Obj, P extends string>(item: T, path: P): Exclude<PathValue<T, P>, undefined> {\n const value = readPath(item, path);\n\n if (value === undefined) throw new TypeError(`Path does not exist: '${path}'`);\n\n return value as Exclude<PathValue<T, P>, undefined>;\n}",
224
- "HashOptions": "export type HashOptions = {\n /**\n * What to do when a class instance is encountered (an object whose prototype is\n * neither `Object.prototype` nor `null`).\n *\n * - `'coerce'` (default) — calls `String(value)`.\n * - `'throw'` — throws an `ArsenalSerializationError`.\n */\n onClassInstance?: 'coerce' | 'throw';\n};",
224
+ "HashOptions": "export type HashOptions = {\n /**\n * What to do when a class instance is encountered (an object whose prototype is\n * neither `Object.prototype` nor `null`).\n *\n * - `'coerce'` (default): calls `String(value)`.\n * - `'throw'`: throws an `ArsenalSerializationError`.\n */\n onClassInstance?: 'coerce' | 'throw';\n};",
225
225
  "hash": "export function hash(value: unknown, options?: HashOptions): string {\n return _hash(value, options, new Set<object>());\n}",
226
226
  "omit": "export function omit<T extends Obj, K extends keyof T>(obj: T, omittedKeys: readonly K[]): Omit<T, K> {\n const blacklist = new Set<PropertyKey>(omittedKeys as readonly PropertyKey[]);\n const out = {} as Omit<T, K>;\n\n for (const key of Object.keys(obj) as Array<keyof T>) {\n if (isUnsafeKey(key as PropertyKey)) continue;\n\n if (!blacklist.has(key)) {\n (out as T)[key] = obj[key];\n }\n }\n\n return out;\n}",
227
227
  "pick": "export function pick<T extends Obj, K extends keyof T>(obj: T, selectedKeys: readonly K[]): Pick<T, K> {\n const out = {} as Pick<T, K>;\n\n for (const key of selectedKeys) {\n if (isUnsafeKey(key as PropertyKey)) continue;\n\n if (Object.hasOwn(obj, key)) {\n out[key] = obj[key];\n }\n }\n\n return out;\n}",
@@ -1,10 +1,10 @@
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} from './events';\nexport { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';\nexport {\n type DelayOptions,\n delay,\n type EventuallyOptions,\n eventually,\n type WaitOptions,\n waitForEvent,\n waitUntil,\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 eventually,\n waitForEvent,\n delay,\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`, `eventually`, and `waitForEvent` provide bounded, abortable async waiting.\n- `delay` models explicit cancellable timer 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
- "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\n| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |\n| `waitUntil` / `eventually` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |\n| `delay` | Schedules a cancellable timer | Async | Pass a signal to cancel a pending wait |\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, 'item-added', { detail: { id: '42' } });\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?)` dispatches a `CustomEvent` with the given type. 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 eventually(() => expect(spy).toHaveBeenCalled(), { signal, timeout: 1000 });\nawait waitForEvent(target, 'ready', { signal, timeout: 1000 });\nawait delay(100, { signal });\n```\n\n| Function | Success condition | Options |\n| -------------------------------------- | ------------------------ | ------------------------------------------ |\n| `waitUntil(predicate, options?)` | Predicate returns `true` | `timeout`, `interval`, `signal` |\n| `eventually(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\n\n`waitUntil()`, `eventually()`, and `waitForEvent()` reject with `AssayTimeoutError` when their shared deadline expires. `waitUntil()` and `eventually()` remain bounded while an asynchronous callback is pending. Abort rejects with the signal reason and removes Assay-owned timers and listeners. Callback work itself must cooperate with application cancellation.\n\n`timeout` and delay milliseconds must be finite, non-negative, and no greater than 2,147,483,647 ms; `interval` must be finite, positive, and within the same timer limit. Invalid durations throw `RangeError`. `eventually()` also accepts `message` for diagnostic timeout context.\n\n## Types\n\n```ts\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n```\n\nScoped query helpers returned by `within(root)`.\n\n```ts\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nexport interface DelayOptions {\n /** Cancel the pending delay. */\n signal?: AbortSignal;\n}\n```\n\n`WaitOptions` configures `waitUntil()`. `EventuallyOptions` configures `eventually()`. `DelayOptions` configures `delay()`.\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\nUse `instanceof AssayError` to narrow any value to the Assay error hierarchy.\n",
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, 'value-change', { detail: { value: 42 } });\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, eventually, waitForEvent, waitUntil } from '@vielzeug/assay';\n\nawait waitUntil(() => panel.querySelector('.status')?.textContent === 'Ready');\n\nawait eventually(() => {\n expect(onSave).toHaveBeenCalledOnce();\n}, { message: 'save callback did not run' });\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()` polls a boolean predicate. `eventually()` retries a throwing assertion and can include a diagnostic `message`. Both enforce one hard deadline while asynchronous callbacks are pending. Abort rejects immediately with the signal reason, but cannot stop callback-owned work; forward an application signal into that work when needed.\n\nTimeouts reject with `AssayTimeoutError`. Timeout and delay values must be finite and non-negative; polling intervals must be finite and positive. All durations must fit the platform timer limit of 2,147,483,647 ms.\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 `eventually()` for assertions.\n- Add a diagnostic `message` to waits whose final assertion does not identify the tested workflow.\n",
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"
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 eventually,\n waitForEvent,\n delay,\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`, `eventually`, and `waitForEvent` provide bounded, abortable async waiting.\n- `delay` models explicit cancellable timer 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
+ "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\n| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |\n| `waitUntil` / `eventually` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |\n| `delay` | Schedules a cancellable timer | Async | Pass a signal to cancel a pending wait |\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, 'item-added', { detail: { id: '42' } });\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?)` dispatches a `CustomEvent` with the given type. 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 eventually(() => expect(spy).toHaveBeenCalled(), { signal, timeout: 1000 });\nawait waitForEvent(target, 'ready', { signal, timeout: 1000 });\nawait delay(100, { signal });\n```\n\n| Function | Success condition | Options |\n| -------------------------------------- | ------------------------ | ------------------------------------------ |\n| `waitUntil(predicate, options?)` | Predicate returns `true` | `timeout`, `interval`, `signal` |\n| `eventually(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\n\n`waitUntil()`, `eventually()`, and `waitForEvent()` reject with `AssayTimeoutError` when their shared deadline expires. `waitUntil()` and `eventually()` remain bounded while an asynchronous callback is pending. Abort rejects with the signal reason and removes Assay-owned timers and listeners. Callback work itself must cooperate with application cancellation.\n\n`timeout` and delay milliseconds must be finite, non-negative, and no greater than 2,147,483,647 ms; `interval` must be finite, positive, and within the same timer limit. Invalid durations throw `RangeError`. `eventually()` also accepts `message` for diagnostic timeout context.\n\n## Types\n\n```ts\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n```\n\nScoped query helpers returned by `within(root)`.\n\n```ts\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nexport interface DelayOptions {\n /** Cancel the pending delay. */\n signal?: AbortSignal;\n}\n```\n\n`WaitOptions` configures `waitUntil()`. `EventuallyOptions` configures `eventually()`. `DelayOptions` configures `delay()`.\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\nUse `instanceof AssayError` to narrow any value to the Assay error hierarchy.\n",
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, 'value-change', { detail: { value: 42 } });\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, eventually, waitForEvent, waitUntil } from '@vielzeug/assay';\n\nawait waitUntil(() => panel.querySelector('.status')?.textContent === 'Ready');\n\nawait eventually(() => {\n expect(onSave).toHaveBeenCalledOnce();\n}, { message: 'save callback did not run' });\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()` polls a boolean predicate. `eventually()` retries a throwing assertion and can include a diagnostic `message`. Both enforce one hard deadline while asynchronous callbacks are pending. Abort rejects immediately with the signal reason, but cannot stop callback-owned work; forward an application signal into that work when needed.\n\nTimeouts reject with `AssayTimeoutError`. Timeout and delay values must be finite and non-negative; polling intervals must be finite and positive. All durations must fit the platform timer limit of 2,147,483,647 ms.\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 `eventually()` for assertions.\n- Add a diagnostic `message` to waits whose final assertion does not identify the tested workflow.\n",
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"
8
8
  },
9
9
  "examples": [],
10
10
  "typeSignatures": {
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "apiSource": "export {\n ClockworkDefinitionError,\n ClockworkError,\n ClockworkSnapshotError,\n ClockworkTransitionLimitError,\n} from './errors.js';\nexport { defineMachine } from './interpret.js';\nexport type {\n Actor,\n ActorErrorContext,\n ActorOptions,\n ActorTapEvent,\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';\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]\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()`** — defines one flat, typed machine and rejects the footguns types cannot express.\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 after commit and stable subscriber notification.\n- **`invoke`** — runs cancellable asynchronous work on state entry.\n- **`after`** — schedules cancellable delayed transitions.\n- **`actor.snapshot`** — exposes the current shallow-frozen 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
- "api": "---\ntitle: Clockwork — API Reference\ndescription: Reference for Clockwork machine definitions, actors, and types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `defineMachine()` | Define a typed flat machine | 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| `Actor.subscribe()` | Observe committed snapshots | Sync | Observes only; it does not trace sends or errors |\n| `Actor.tap()` | Observe transitions, ignores, errors, disposal | Sync | Handler errors are swallowed; it cannot control disposition |\n| `ClockworkError` | Report definition, snapshot, and transition-limit failures | Sync | Narrow with the error subtypes, not message text |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/clockwork` | Machine factory, actor runtime, errors, and types |\n\n## Exported Surface\n\nCallback parameter types are inferred for inline definitions. Clockwork also exports reusable callback, transition, state-node, actor-option, result, and error-context types for shared helpers.\n\n| Primary export | Kind |\n| --- | --- |\n| `defineMachine` | Function |\n| `Machine` | Type |\n| `Actor` | Type |\n| `MachineSnapshot` | Type |\n| `MachineConfig` | Type |\n| `ClockworkError` | Class |\n| `ClockworkDefinitionError` | Class |\n| `ClockworkSnapshotError` | Class |\n| `ClockworkTransitionLimitError` | Class |\n\nAdditional exported types: `ActorErrorContext`, `ActorOptions`, `ActorTapEvent`, `After`, `Effect`, `EffectArgs`, `EventByType`, `EventType`, `Guard`, `Invoke`, `InvokeArgs`, `MachineEvent`, `Reducer`, `StateNode`, `Transition`, `TransitionInput`, and `TransitionResult`.\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 accepts a typed flat machine definition. The definition is trusted as typed: targets, callback shapes, and state keys are compiler-checked. `defineMachine()` rejects only what types cannot express — timer delays outside `0..2,147,483,647` ms, empty transition arrays, and a context that is not a plain record (arrays, `null`, and class instances fail). Omit `context` only when the context type has no keys.\n\nThe curried signature is required so TypeScript can infer `State` from the `states` object while you explicitly provide `Context` and `Event`. TypeScript does not support partial type argument inference, so the state-key union cannot be inferred in a single non-curried call when the context and event generics are explicit.\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 `ClockworkDefinitionError` when a definition carries a timer delay outside the platform range, an empty transition array, or a context that is not a plain record.\n\n---\n\n### `Actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nSubscribes to future committed actor snapshots. It does not run immediately; read `actor.snapshot` for the initial value. Returns an unsubscribe function. Subscriber failures are reported through `tap()` as `error` events with `phase: 'subscriber'` without stopping remaining subscribers, declared effects, or the actor.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\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 stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\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>): 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\n**Returns:** Disposable `Actor`.\n\n**Example:**\n\n```ts\nconst actor = machine.createActor({ snapshot: { context: {}, state: 'idle' } });\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. For active actors, malformed events without a string `type` log a development warning and are ignored. Valid but unhandled event types are ignored without a warning. 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. A guard failure is reported as a fatal transition error, disposes the actor, and returns `false`. 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, tappers and listeners, emits a final `dispose` tap event, and aborts `disposalSignal`.\n\n**Returns:** Nothing. Idempotent.\n\n### `actor.tap()`\n\n```ts\ntap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { signal?: AbortSignal },\n): () => void;\n```\n\nObserves actor lifecycle without affecting machine behavior: `{ type: 'transition' | 'ignored' }` carries the event and resulting snapshot, `{ type: 'error' }` carries the error with its `phase`/`state`/`event` context, and `{ type: 'dispose' }` is the final event before tappers are cleared. Handler errors are swallowed. Returns an unsubscribe function.\n\n```ts\nactor.tap((event) => {\n if (event.type === 'error') console.error(event.phase, event.state, event.error);\n});\n```\n\n## Error Handling Policy\n\nMachine-execution failures are fail-stop: guard, reducer, effect, invoke, timer, and queued-transition-limit failures dispose the actor. The queued-transition limit is a fixed internal guard against runaway loops, not a configurable option. Subscriber failures are observational: Clockwork reports them through `tap()`, continues the stable subscriber snapshot and declared effects, and keeps the actor active. Tap handlers cannot control disposition.\n\nSubscriber iteration uses a stable snapshot: listeners added during notification begin with the next transition.\n\n## Types\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\nClockwork shallow-clones and freezes each snapshot and its context record. Nested context values remain caller-owned and are not deep-frozen.\n\n### `MachineConfig<State, Context, Event>`\n\n```ts\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. The `StateNode`, `Transition`, `After`, `Invoke`, `Effect`, `Guard`, and `Reducer` shapes are inferred inline and exported for reusable helpers. `After.delay` must be finite and between 0 and 2,147,483,647 milliseconds; delayed guards and reducers receive `event: undefined`.\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 tap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { readonly signal?: AbortSignal },\n ): () => void;\n};\n```\n\nAn actor's `snapshot` is the current plain readonly snapshot.\n\n### `ActorTapEvent<State, Context, Event>`\n\n```ts\ntype ActorTapEvent<State, Context, Event> =\n | (ActorErrorContext<State, Event> & { readonly error: unknown; readonly type: 'error' })\n | {\n readonly event: Event | undefined;\n readonly snapshot: MachineSnapshot<State, Context>;\n readonly type: 'transition' | 'ignored';\n }\n | { readonly type: 'dispose' };\n\ntype ActorErrorContext<State, Event> = {\n readonly event?: Event;\n readonly phase: 'effect' | 'invoke' | 'subscriber' | 'transition';\n readonly state: State;\n};\n```\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>): 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 reusable machine. Its state and event lookups are own-property guarded, so unknown or prototype-colliding names such as `__proto__` or `constructor` are safely ignored when no transition exists.\n\n## Errors\n\n### `ClockworkError`\n\n`ClockworkError` is the base class for every Clockwork failure, with `details` and standard `Error` fields. Use `instanceof ClockworkError` to narrow an unknown error. Three subtypes carry the failure category:\n\n| Subtype | Raised when |\n| --- | --- |\n| `ClockworkDefinitionError` | `defineMachine()` rejects a definition footgun types cannot express: a timer delay outside `0..2,147,483,647` ms, an empty transition array, or a context that is not a plain record |\n| `ClockworkSnapshotError` | A snapshot, its context, or a reducer result is not a plain object record, or names an undeclared state |\n| `ClockworkTransitionLimitError` | An actor exceeds the fixed queued-transition limit and disposes itself |\n\n```ts\nif (error instanceof ClockworkDefinitionError) {\n console.error(error.message, 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\nCall `defineMachine<Context, Event>()` first to bind context and event types; the returned definition function infers state labels from `states`. Context is optional only when its type has no keys. 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 shallow-clones and freezes each snapshot and context record; nested values are not deep-cloned, so make explicit nested replacements.\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 target-state timers and invokes, notifies a stable subscriber snapshot, then runs exit, transition, and entry effects. Subscriber failures are reported but cannot skip declared 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. Delay must be finite and between 0 and 2,147,483,647 milliseconds. Guards and reducers 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 shallow-frozen `MachineSnapshot`; 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\nGuard, reducer, effect, invoke, timer, and transition-limit failures dispose the actor. The transition limit is a fixed internal guard against runaway loops. Subscriber failures are reported but do not dispose the actor or interrupt remaining subscribers and declared effects.\n\nUse `tap()` to observe either class without controlling disposition. The `error` event carries the error and its `phase` and `state`; handler failures are swallowed so observation cannot alter actor behavior.\n\n```ts\nconst actor = machine.createActor();\nactor.tap((event) => {\n if (event.type === 'error') console.error({ error: event.error, event: event.event, phase: event.phase, state: event.state });\n});\n```\n\nFor fatal machine failures, the `error` event is emitted before disposal. Without a tap handler, fatal failures dispose silently.\n\n## Debugging\n\nUse `actor.subscribe()` to observe committed snapshots during development. It observes snapshots only; it does not trace dispatched events or runtime errors.\n\n```ts\nconst actor = machine.createActor();\nconst stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\nactor.dispose();\n```\n\nFor richer inspection, route snapshots to 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 one 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<Value>(actor: { readonly snapshot: Value; subscribe(listener: (snapshot: Value) => void): () => void }) {\n return useSyncExternalStore(\n (notify) => actor.subscribe(() => notify()),\n () => actor.snapshot,\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 the `tap()` observer for logging and diagnostics.\n",
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"
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]\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()`**: defines one flat, typed machine and rejects the footguns types cannot express.\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 after commit and stable subscriber notification.\n- **`invoke`**: runs cancellable asynchronous work on state entry.\n- **`after`**: schedules cancellable delayed transitions.\n- **`actor.snapshot`**: exposes the current shallow-frozen 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
+ "api": "---\ntitle: 'Clockwork: API Reference'\ndescription: Reference for Clockwork machine definitions, actors, and types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `defineMachine()` | Define a typed flat machine | 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| `Actor.subscribe()` | Observe committed snapshots | Sync | Observes only; it does not trace sends or errors |\n| `Actor.tap()` | Observe transitions, ignores, errors, disposal | Sync | Handler errors are swallowed; it cannot control disposition |\n| `ClockworkError` | Report definition, snapshot, and transition-limit failures | Sync | Narrow with the error subtypes, not message text |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/clockwork` | Machine factory, actor runtime, errors, and types |\n\n## Exported Surface\n\nCallback parameter types are inferred for inline definitions. Clockwork also exports reusable callback, transition, state-node, actor-option, result, and error-context types for shared helpers.\n\n| Primary export | Kind |\n| --- | --- |\n| `defineMachine` | Function |\n| `Machine` | Type |\n| `Actor` | Type |\n| `MachineSnapshot` | Type |\n| `MachineConfig` | Type |\n| `ClockworkError` | Class |\n| `ClockworkDefinitionError` | Class |\n| `ClockworkSnapshotError` | Class |\n| `ClockworkTransitionLimitError` | Class |\n\nAdditional exported types: `ActorErrorContext`, `ActorOptions`, `ActorTapEvent`, `After`, `Effect`, `EffectArgs`, `EventByType`, `EventType`, `Guard`, `Invoke`, `InvokeArgs`, `MachineEvent`, `Reducer`, `StateNode`, `Transition`, `TransitionInput`, and `TransitionResult`.\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 accepts a typed flat machine definition. The definition is trusted as typed: targets, callback shapes, and state keys are compiler-checked. `defineMachine()` rejects only what types cannot express: timer delays outside `0..2,147,483,647` ms, empty transition arrays, and a context that is not a plain record (arrays, `null`, and class instances fail). Omit `context` only when the context type has no keys.\n\nThe curried signature is required so TypeScript can infer `State` from the `states` object while you explicitly provide `Context` and `Event`. TypeScript does not support partial type argument inference, so the state-key union cannot be inferred in a single non-curried call when the context and event generics are explicit.\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 `ClockworkDefinitionError` when a definition carries a timer delay outside the platform range, an empty transition array, or a context that is not a plain record.\n\n---\n\n### `Actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nSubscribes to future committed actor snapshots. It does not run immediately; read `actor.snapshot` for the initial value. Returns an unsubscribe function. Subscriber failures are reported through `tap()` as `error` events with `phase: 'subscriber'` without stopping remaining subscribers, declared effects, or the actor.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\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 stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\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>): 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\n**Returns:** Disposable `Actor`.\n\n**Example:**\n\n```ts\nconst actor = machine.createActor({ snapshot: { context: {}, state: 'idle' } });\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. For active actors, malformed events without a string `type` log a development warning and are ignored. Valid but unhandled event types are ignored without a warning. 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. A guard failure is reported as a fatal transition error, disposes the actor, and returns `false`. 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, tappers and listeners, emits a final `dispose` tap event, and aborts `disposalSignal`.\n\n**Returns:** Nothing. Idempotent.\n\n### `actor.tap()`\n\n```ts\ntap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { signal?: AbortSignal },\n): () => void;\n```\n\nObserves actor lifecycle without affecting machine behavior: `{ type: 'transition' | 'ignored' }` carries the event and resulting snapshot, `{ type: 'error' }` carries the error with its `phase`/`state`/`event` context, and `{ type: 'dispose' }` is the final event before tappers are cleared. Handler errors are swallowed. Returns an unsubscribe function.\n\n```ts\nactor.tap((event) => {\n if (event.type === 'error') console.error(event.phase, event.state, event.error);\n});\n```\n\n## Error Handling Policy\n\nMachine-execution failures are fail-stop: guard, reducer, effect, invoke, timer, and queued-transition-limit failures dispose the actor. The queued-transition limit is a fixed internal guard against runaway loops, not a configurable option. Subscriber failures are observational: Clockwork reports them through `tap()`, continues the stable subscriber snapshot and declared effects, and keeps the actor active. Tap handlers cannot control disposition.\n\nSubscriber iteration uses a stable snapshot: listeners added during notification begin with the next transition.\n\n## Types\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\nClockwork shallow-clones and freezes each snapshot and its context record. Nested context values remain caller-owned and are not deep-frozen.\n\n### `MachineConfig<State, Context, Event>`\n\n```ts\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. The `StateNode`, `Transition`, `After`, `Invoke`, `Effect`, `Guard`, and `Reducer` shapes are inferred inline and exported for reusable helpers. `After.delay` must be finite and between 0 and 2,147,483,647 milliseconds; delayed guards and reducers receive `event: undefined`.\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 tap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { readonly signal?: AbortSignal },\n ): () => void;\n};\n```\n\nAn actor's `snapshot` is the current plain readonly snapshot.\n\n### `ActorTapEvent<State, Context, Event>`\n\n```ts\ntype ActorTapEvent<State, Context, Event> =\n | (ActorErrorContext<State, Event> & { readonly error: unknown; readonly type: 'error' })\n | {\n readonly event: Event | undefined;\n readonly snapshot: MachineSnapshot<State, Context>;\n readonly type: 'transition' | 'ignored';\n }\n | { readonly type: 'dispose' };\n\ntype ActorErrorContext<State, Event> = {\n readonly event?: Event;\n readonly phase: 'effect' | 'invoke' | 'subscriber' | 'transition';\n readonly state: State;\n};\n```\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>): 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 reusable machine. Its state and event lookups are own-property guarded, so unknown or prototype-colliding names such as `__proto__` or `constructor` are safely ignored when no transition exists.\n\n## Errors\n\n### `ClockworkError`\n\n`ClockworkError` is the base class for every Clockwork failure, with `details` and standard `Error` fields. Use `instanceof ClockworkError` to narrow an unknown error. Three subtypes carry the failure category:\n\n| Subtype | Raised when |\n| --- | --- |\n| `ClockworkDefinitionError` | `defineMachine()` rejects a definition footgun types cannot express: a timer delay outside `0..2,147,483,647` ms, an empty transition array, or a context that is not a plain record |\n| `ClockworkSnapshotError` | A snapshot, its context, or a reducer result is not a plain object record, or names an undeclared state |\n| `ClockworkTransitionLimitError` | An actor exceeds the fixed queued-transition limit and disposes itself |\n\n```ts\nif (error instanceof ClockworkDefinitionError) {\n console.error(error.message, 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\nCall `defineMachine<Context, Event>()` first to bind context and event types; the returned definition function infers state labels from `states`. Context is optional only when its type has no keys. 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 shallow-clones and freezes each snapshot and context record; nested values are not deep-cloned, so make explicit nested replacements.\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 target-state timers and invokes, notifies a stable subscriber snapshot, then runs exit, transition, and entry effects. Subscriber failures are reported but cannot skip declared 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. Delay must be finite and between 0 and 2,147,483,647 milliseconds. Guards and reducers 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 shallow-frozen `MachineSnapshot`; 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\nGuard, reducer, effect, invoke, timer, and transition-limit failures dispose the actor. The transition limit is a fixed internal guard against runaway loops. Subscriber failures are reported but do not dispose the actor or interrupt remaining subscribers and declared effects.\n\nUse `tap()` to observe either class without controlling disposition. The `error` event carries the error and its `phase` and `state`; handler failures are swallowed so observation cannot alter actor behavior.\n\n```ts\nconst actor = machine.createActor();\nactor.tap((event) => {\n if (event.type === 'error') console.error({ error: event.error, event: event.event, phase: event.phase, state: event.state });\n});\n```\n\nFor fatal machine failures, the `error` event is emitted before disposal. Without a tap handler, fatal failures dispose silently.\n\n## Debugging\n\nUse `actor.subscribe()` to observe committed snapshots during development. It observes snapshots only; it does not trace dispatched events or runtime errors.\n\n```ts\nconst actor = machine.createActor();\nconst stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\nactor.dispose();\n```\n\nFor richer inspection, route snapshots to 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 one 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<Value>(actor: { readonly snapshot: Value; subscribe(listener: (snapshot: Value) => void): () => void }) {\n return useSyncExternalStore(\n (notify) => actor.subscribe(() => notify()),\n () => actor.snapshot,\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 the `tap()` observer for logging and diagnostics.\n",
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": [
10
10
  {