@vielzeug/codex 2.1.2 → 2.1.4

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 (46) hide show
  1. package/data/catalog.json +9 -5
  2. package/data/llms-full.txt +16 -44
  3. package/data/llms.txt +2 -2
  4. package/data/manifest.json +1 -1
  5. package/data/packages/arsenal.json +1 -1
  6. package/data/packages/assay.json +26 -26
  7. package/data/packages/clockwork.json +3 -3
  8. package/data/packages/codex.json +23 -23
  9. package/data/packages/coins.json +5 -5
  10. package/data/packages/conduit.json +8 -8
  11. package/data/packages/courier.json +8 -8
  12. package/data/packages/dnd.json +6 -6
  13. package/data/packages/familiar.json +9 -9
  14. package/data/packages/flux.json +9 -9
  15. package/data/packages/forge.json +2 -2
  16. package/data/packages/keymap.json +6 -6
  17. package/data/packages/lingua.json +22 -22
  18. package/data/packages/necromancer.json +2 -2
  19. package/data/packages/orbit.json +21 -21
  20. package/data/packages/ore.json +49 -49
  21. package/data/packages/prism.json +22 -22
  22. package/data/packages/pulse.json +17 -17
  23. package/data/packages/ripple.json +22 -16
  24. package/data/packages/rune.json +29 -29
  25. package/data/packages/scout.json +2 -2
  26. package/data/packages/scroll.json +15 -15
  27. package/data/packages/sourcerer.json +28 -28
  28. package/data/packages/spell.json +23 -23
  29. package/data/packages/tempo.json +14 -14
  30. package/data/packages/vault.json +4 -4
  31. package/data/packages/ward.json +25 -25
  32. package/data/packages/wayfinder.json +44 -44
  33. package/data/refine.json +4600 -4600
  34. package/data/search.json +33 -33
  35. package/dist/catalog.js.map +1 -1
  36. package/dist/cli.js +1 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/http.js +1 -1
  39. package/dist/http.js.map +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/snapshot.js.map +1 -1
  42. package/dist/tools/index.js.map +1 -1
  43. package/dist/tools/packages.js.map +1 -1
  44. package/dist/tools/refine.js.map +1 -1
  45. package/mcp-setup.json +5 -5
  46. package/package.json +20 -20
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export { clamp, allocate, sum } from './aggregate';\nexport { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';\nexport { decimal } from './decimal';\nexport { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';\nexport type { CoinsErrorCode } from './errors';\nexport { exchange, exchangeRate } from './exchange';\nexport { format, formatParts } from './format';\nexport {\n abs,\n add,\n compare,\n divide,\n isMoney,\n money,\n multiply,\n negate,\n parseMoney,\n round,\n subtract,\n toDecimal,\n} from './money';\nexport { parseMoneyJSON, toJSON } from './serialization';\nexport type {\n Currency,\n CurrencyCode,\n Decimal,\n ExchangeRate,\n FormatOptions,\n Money,\n MoneyFormatPart,\n MoneyJSON,\n RoundingMode,\n} from './types';\n",
2
+ "apiSource": "export { allocate, clamp, sum } from './aggregate';\nexport { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';\nexport { decimal } from './decimal';\nexport type { CoinsErrorCode } from './errors';\nexport { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';\nexport { exchange, exchangeRate } from './exchange';\nexport { format, formatParts } from './format';\nexport {\n abs,\n add,\n compare,\n divide,\n isMoney,\n money,\n multiply,\n negate,\n parseMoney,\n round,\n subtract,\n toDecimal,\n} from './money';\nexport { parseMoneyJSON, toJSON } from './serialization';\nexport type {\n Currency,\n CurrencyCode,\n Decimal,\n ExchangeRate,\n FormatOptions,\n Money,\n MoneyFormatPart,\n MoneyJSON,\n RoundingMode,\n} from './types';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Coins — Exact Money for TypeScript\ndescription: Exact bigint monetary arithmetic with explicit currency definitions, decimal strings, allocation, exchange, formatting, and JSON boundaries.\npackage: coins\ncategory: finance\nkeywords: [money, currency, bigint, decimal, exchange, formatting]\nexports: [money, currency, add, allocate, exchange, format]\nrelated: [vault, courier, spell]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"coins\" />\n\n## Why Coins?\n\nCoins keeps monetary values in bigint minor units, but makes units explicit at construction. Currency scale comes from deterministic definitions; `Intl` formats a known value without deciding its arithmetic representation.\n\n```ts\n// Before\nconst total = (19.99 + 7.25) * 1.08;\n\n// After\nimport { USD, add, money, multiply } from '@vielzeug/coins';\n\nconst total = multiply(add(money('19.99', USD), money('7.25', USD)), '1.08');\n```\n\n| Feature | Coins | decimal.js | Dinero.js |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"coins\" type=\"size\" /> | External dependency | External dependency |\n| Bigint minor units | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit currency scale | <ore-icon name=\"check\" size=\"16\"></ore-icon> | App-defined | Partial |\n| Exact allocation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Coins when** application values represent real money and every rounding boundary must be visible.\n\n**Consider native numbers when** values are estimates, analytics, or display-only approximations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/coins\n```\n\n```sh [npm]\nnpm install @vielzeug/coins\n```\n\n```sh [yarn]\nyarn add @vielzeug/coins\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { USD, add, format, money, multiply } from '@vielzeug/coins';\n\nconst subtotal = add(money('12.50', USD), money('7.25', USD));\nconst total = multiply(subtotal, '1.08', { rounding: 'halfEven' });\n\nconsole.log(format(total));\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`money`**: one constructor for decimal and explicit minor-unit values\n- **`currency`**: deterministic built-in currency definitions\n- **`add`**: exact same-currency arithmetic\n- **`allocate`**: split every minor unit without loss\n- **`exchange`**: typed source and target currency conversion\n- **`format`**: locale presentation for bigint values\n- **`parseMoneyJSON`**: validate persisted money values\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Vault](/vault/) — persist validated money JSON.\n- [Courier](/courier/) — retrieve exchange-rate data.\n- [Spell](/spell/) — validate external monetary payloads.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Coins — API Reference\ndescription: Exact money, currency definitions, exchange, formatting, serialization, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `money` | Construct validated money | Sync | Bigint requires `{ unit: 'minor' }` |\n| `currency` | Resolve supported definition | Sync | Unknown codes throw |\n| `defineCurrency` | Define an explicit scale | Sync | Code must be three uppercase letters |\n| `add` / `subtract` | Combine matching currencies | Sync | Mismatches throw |\n| `multiply` / `divide` | Exact decimal scaling | Sync | Use decimal strings |\n| `sum` | Aggregate with identity currency | Sync | Pass `{ currency }` |\n| `allocate` | Split without losing minor units | Sync | Weights must be non-negative |\n| `exchange` | Convert through an exact rate | Sync | Rate source must match value currency |\n| `format` | Present money with `Intl` | Sync | Formatting does not define currency scale |\n| `toJSON` / `parseMoneyJSON` | Cross JSON boundary | Sync | Persisted amount uses minor units |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/coins` | Complete public Coins API |\n\n## Construction\n\n### currency / defineCurrency\n\n```ts\ncurrency(code: string): Currency\ndefineCurrency({ code, minorUnit }): Currency\n```\n\nBuilt-ins: `USD`, `EUR`, `GBP`, `JPY`, `KRW`, `BHD`, `KWD`.\n\n### money\n\n```ts\nmoney(amount: string, currency: Currency): Money\nmoney(amount: string, currency: Currency, options: { rounding: RoundingMode }): Money\nmoney(amount: bigint, currency: Currency, options: { unit: 'minor' }): Money\n```\n\n```ts\nmoney('19.99', USD);\nmoney(1999n, USD, { unit: 'minor' });\n```\n\n### decimal\n\n```ts\ndecimal(value: string): Decimal\n```\n\nCreates an exact rational value for multiplication, division, or exchange rates.\n\n## Arithmetic\n\n```ts\nadd(left, right)\nsubtract(left, right)\nmultiply(value, factor, { rounding? })\ndivide(value, divisor, { rounding? })\ncompare(left, right)\nabs(value)\nnegate(value)\nround(value, { fractionDigits, rounding? })\n```\n\n`factor` and `divisor` are decimal strings. Matching currency is required for binary money operations.\n\n## Aggregation\n\n```ts\nsum(values, { currency })\nallocate(value, count)\nallocate(value, weights)\nclamp(value, { min, max })\n```\n\n`sum([], { currency: USD })` returns zero USD. `allocate` returns values whose minor-unit total exactly equals input.\n\n## Exchange\n\n```ts\nexchangeRate({ from, to, value }): ExchangeRate\nexchange(value, rate, { rounding? }): Money\n```\n\n```ts\nconst rate = exchangeRate({ from: USD, to: EUR, value: '0.9234' });\nexchange(money('100.00', USD), rate);\n```\n\n## Formatting\n\n```ts\nformat(value, options?): string\nformatParts(value, options?): MoneyFormatPart[]\n```\n\n`FormatOptions` uses `locale`, `style`, `minimumFractionDigits`, and `maximumFractionDigits`.\n\n## Serialization\n\n```ts\ntoDecimal(value): string\ntoJSON(value): MoneyJSON\nparseMoneyJSON(value: unknown, options?: { currency?: (code: string) => Currency }): Money\nparseMoney(value: unknown): Money\nisMoney(value: unknown): value is Money\n```\n\n## Types\n\n```ts\ntype Currency = { code: CurrencyCode; minorUnit: number };\ntype Money = { amount: bigint; currency: Currency };\ntype Decimal = { numerator: bigint; denominator: bigint };\ntype ExchangeRate = { from: Currency; to: Currency; value: Decimal };\ntype MoneyJSON = { amount: string; currency: string; unit: 'minor' };\ntype RoundingMode = 'awayFromZero' | 'ceil' | 'floor' | 'halfAwayFromZero' | 'halfEven' | 'towardZero';\n```\n\n## Errors\n\nEvery Coins failure extends `CoinsError` and exposes `code`.\n\n- `INVALID_CURRENCY`\n- `INVALID_DECIMAL`\n- `INVALID_MONEY`\n- `INVALID_ALLOCATION`\n- `INVALID_ROUNDING`\n- `DIVISION_BY_ZERO`\n- `CURRENCY_MISMATCH`\n\n`CurrencyMismatchError` and `InvalidCurrencyError` are specialized `CoinsError` subclasses.\n",
@@ -54,9 +54,9 @@
54
54
  }
55
55
  ],
56
56
  "typeSignatures": {
57
- "clamp": "export { clamp, allocate, sum } from './aggregate';",
58
- "allocate": "export { clamp, allocate, sum } from './aggregate';",
59
- "sum": "export { clamp, allocate, sum } from './aggregate';",
57
+ "allocate": "export { allocate, clamp, sum } from './aggregate';",
58
+ "clamp": "export { allocate, clamp, sum } from './aggregate';",
59
+ "sum": "export { allocate, clamp, sum } from './aggregate';",
60
60
  "BHD": "export { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';",
61
61
  "currency": "export { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';",
62
62
  "defineCurrency": "export { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';",
@@ -68,10 +68,10 @@
68
68
  "KWD": "export { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';",
69
69
  "USD": "export { BHD, currency, defineCurrency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';",
70
70
  "decimal": "export { decimal } from './decimal';",
71
+ "CoinsErrorCode": "export type { CoinsErrorCode } from './errors';",
71
72
  "CoinsError": "export { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';",
72
73
  "CurrencyMismatchError": "export { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';",
73
74
  "InvalidCurrencyError": "export { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';",
74
- "CoinsErrorCode": "export type { CoinsErrorCode } from './errors';",
75
75
  "exchange": "export { exchange, exchangeRate } from './exchange';",
76
76
  "exchangeRate": "export { exchange, exchangeRate } from './exchange';",
77
77
  "format": "export { format, formatParts } from './format';",
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export { createContainer } from './container';\nexport {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';\nexport type { Container, FactoryOptions, InferTokens, Lifetime, ScopeToken, Token, ValueOptions } from './types';\nexport { scope, token } from './types';\n",
2
+ "apiSource": "export { createContainer } from './container';\nexport {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';\nexport type { Container, FactoryOptions, InferTokens, Lifetime, ScopeToken, Token, ValueOptions } from './types';\nexport { scope, token } from './types';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Conduit — Dependency Injection for TypeScript\ndescription: Dependency-first asynchronous dependency injection with typed tokens, lifecycle scopes, startup validation, and deterministic disposal.\npackage: conduit\ncategory: infrastructure\nkeywords: [dependency injection, container, token, lifecycle, scope]\nexports: [createContainer, token, scope]\nrelated: [courier, vault, rune]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"conduit\" />\n\n## Why Conduit?\n\nConduit makes service wiring explicit. Factory dependency tuples are source of truth for creation, startup validation, and disposal order.\n\n```ts\n// Before\nconst service = createService(createApi(config), logger);\n\n// After\ncontainer.factory(Service, [Api, Logger], (api, logger) => createService(api, logger));\n```\n\n| Feature | Conduit | Inversify | tsyringe |\n| --- | --- | --- | --- |\n| Dependencies | Explicit token tuples | Decorators/runtime metadata | Decorators/runtime metadata |\n| Async factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Lifecycle scopes | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Runtime dependencies | 0 | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Conduit when** application services need explicit wiring and owned lifecycle cleanup.\n\n**Consider direct imports when** dependencies are static, small, and need no replacement or disposal boundary.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/conduit\n```\n\n```sh [npm]\nnpm install @vielzeug/conduit\n```\n\n```sh [yarn]\nyarn add @vielzeug/conduit\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createContainer, token } from '@vielzeug/conduit';\n\nconst Config = token<{ baseUrl: string }>('Config');\nconst Client = token<{ url: string }>('Client');\nconst container = createContainer();\n\ncontainer.value(Config, { baseUrl: '/api' });\ncontainer.factory(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` }));\n\nconsole.log(await container.resolve(Client));\nawait container.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`token`**: typed dependency identity\n- **`factory`**: static dependency-first creation\n- **`validate`**: startup graph validation\n- **`scope`**: explicit request and job ownership\n- **`dispose`**: in-flight-safe resource cleanup\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/) — inject HTTP clients into application services.\n- [Vault](/vault/) — inject persistence adapters with scoped ownership.\n- [Rune](/rune/) — provide application logging services.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Conduit — API Reference\ndescription: Reference for Conduit tokens, dependency-first factories, scopes, validation, and lifecycle disposal.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Mode | Common gotcha |\n| --- | --- | --- | --- |\n| `token` | Create typed dependency identity | Sync | Same description does not mean same token |\n| `scope` | Create named lifecycle identity | Sync | Must match factory lifetime |\n| `createContainer` | Create root registry | Sync | Dispose when application ends |\n| `value` | Register an existing value | Sync | One registration per token/container |\n| `factory` | Register static dependency factory | Sync | Tuple is copied and authoritative |\n| `has` | Check registration visibility | Sync | Walks parent containers |\n| `resolve` | Resolve one dependency | Async | Missing provider throws |\n| `validate` | Validate static graph | Sync | Run after registration |\n| `createScope` | Create child owner | Sync | Named scope required for scoped factories |\n| `dispose` | Release owned resources | Async | May throw `ConduitDisposeError` after cleanup attempts |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/conduit` | Complete Conduit API |\n\n## Tokens and Scopes\n\n```ts\ntoken<T>(description: string): Token<T>\nscope(name: string): ScopeToken\n```\n\nTokens and scopes are unique symbols. Descriptions exist only for diagnostics.\n\n## Container\n\n```ts\ncreateContainer(options?: { name?: string }): Container\n```\n\n### value\n\n```ts\ncontainer.value(token, value, options?)\n```\n\n`options.dispose` runs during container disposal.\n\n### has\n\n```ts\ncontainer.has(token): boolean\n```\n\nChecks local and parent registrations without creating a factory result.\n\n### factory\n\n```ts\ncontainer.factory(token, dependencies, create, options?)\n```\n\n```ts\ncontainer.factory(Service, [Api, Logger], (api, logger) => createService(api, logger));\n```\n\n`dependencies` is copied at registration and drives creation, validation, cycle detection, and teardown order. Factories may return a value or promise.\n\n`options.lifetime` accepts `'singleton'`, `'transient'`, or `ScopeToken`. A singleton cannot depend on a scoped resource.\n\n```ts\ntype FactoryOptions<T> = {\n dispose?: (value: T) => void | Promise<void>;\n lifetime?: 'singleton' | 'transient' | ScopeToken;\n};\n```\n\n### resolve\n\n```ts\ncontainer.resolve(token): Promise<T>\n```\n\nSingleton resolutions deduplicate concurrent callers.\n\n### validate\n\n```ts\ncontainer.validate(): Container\n```\n\nThrows for missing dependencies and circular factory tuples.\n\n### createScope\n\n```ts\ncontainer.createScope(scope?: ScopeToken, options?: { name?: string }): Container\n```\n\nA matching scope owns resources registered with its `ScopeToken` lifetime. Disposing a parent also disposes its active child scopes.\n\n### dispose\n\n```ts\ncontainer.dispose(): Promise<void>\ncontainer.disposalSignal: AbortSignal\ncontainer.disposed: boolean\n```\n\nDisposal blocks new work, aborts `disposalSignal`, disposes active child scopes, waits for in-flight creation, then disposes owned resources in reverse creation order. Cleanup failures are aggregated in `ConduitDisposeError.errors`.\n\n## Types\n\n```ts\ntype Token<T> = symbol;\ntype ScopeToken = symbol;\ntype Lifetime = 'singleton' | 'transient' | ScopeToken;\ntype InferTokens<Tokens> = { [K in keyof Tokens]: Tokens[K] extends Token<infer T> ? T : never };\n```\n\n## Errors\n\n- `ConduitError` — base class; `ConduitError.is(error)` narrows package errors.\n- `ConduitProviderNotFoundError` — dependency has no registration.\n- `ConduitCircularDependencyError` — static factory tuple graph contains a cycle.\n- `ConduitDuplicateRegistrationError` — token registered twice in one container.\n- `ConduitScopedResolutionError` — scoped factory resolved without matching scope.\n- `ConduitDisposedError` — operation attempted after disposal began.\n- `ConduitDisposeError` — one or more cleanup hooks failed.\n",
@@ -40,13 +40,13 @@
40
40
  ],
41
41
  "typeSignatures": {
42
42
  "createContainer": "export { createContainer } from './container';",
43
- "ConduitCircularDependencyError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
44
- "ConduitDisposeError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
45
- "ConduitDisposedError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
46
- "ConduitDuplicateRegistrationError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
47
- "ConduitError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
48
- "ConduitProviderNotFoundError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
49
- "ConduitScopedResolutionError": "export {\n ConduitCircularDependencyError,\n ConduitDisposeError,\n ConduitDisposedError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
43
+ "ConduitCircularDependencyError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
44
+ "ConduitDisposedError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
45
+ "ConduitDisposeError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
46
+ "ConduitDuplicateRegistrationError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
47
+ "ConduitError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
48
+ "ConduitProviderNotFoundError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
49
+ "ConduitScopedResolutionError": "export {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n ConduitScopedResolutionError,\n} from './errors';",
50
50
  "Container": "export type { Container, FactoryOptions, InferTokens, Lifetime, ScopeToken, Token, ValueOptions } from './types';",
51
51
  "FactoryOptions": "export type { Container, FactoryOptions, InferTokens, Lifetime, ScopeToken, Token, ValueOptions } from './types';",
52
52
  "InferTokens": "export type { Container, FactoryOptions, InferTokens, Lifetime, ScopeToken, Token, ValueOptions } from './types';",
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export { createCourier, type Courier, type CourierOptions } from './courier';\nexport {\n CourierAbortError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n} from './errors';\nexport { withBearerAuth, withLogging, withRequestId } from './interceptors';\nexport type { FetchContext, Interceptor, TransportOptions } from './transport';\nexport type { HttpRequestConfig as RequestConfig, Params } from './url';\nexport type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';\nexport type { StreamEvent, StreamOptions } from './stream';\n",
2
+ "apiSource": "export { type Courier, type CourierOptions, createCourier } from './courier';\nexport {\n CourierAbortError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n} from './errors';\nexport { withBearerAuth, withLogging, withRequestId } from './interceptors';\nexport type { StreamEvent, StreamOptions } from './stream';\nexport type { FetchContext, Interceptor, TransportOptions } from './transport';\nexport type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';\nexport type { HttpRequestConfig as RequestConfig, Params } from './url';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Courier — HTTP, queries, and streaming\ndescription: A framework-neutral fetch client with explicit cache keys, direct mutations, and abortable streams.\npackage: courier\ncategory: http\nkeywords: [http-client, fetch, caching, queries, mutations, sse, streaming, interceptors]\nrelated: [flux, ripple, spell]\nexports:\n [\n createCourier,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierTimeoutError,\n CourierAbortError,\n CourierSchemaValidationError,\n withBearerAuth,\n withRequestId,\n withLogging,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"courier\" />\n\n## Why Courier?\n\nNative `fetch` leaves request policy, cached reads, and stream lifecycles to each application. Courier keeps\nthose concerns in one client while making cache identity and fetch policy explicit at every cached read.\n\n```ts\n// Before\nconst response = await fetch(`/api/users/${userId}`);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst user = await response.json();\n\n// After\nawait courier.queries.fetch({\n key: ['users', userId],\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: userId }, signal }),\n});\n```\n\n| Feature | Courier | TanStack Query | ky |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"courier\" type=\"size\" /> | Framework adapter required | Separate package |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Native fetch transport | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Bring your own | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit cache keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| SSE and NDJSON iteration | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| External runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Courier when** one application client should own typed HTTP, explicit cached reads, direct writes, and\nabortable response streams.\n\n**Consider TanStack Query when** you need a maintained framework adapter or advanced cache features such as\ninfinite queries. **Consider ky when** you only need a compact fetch wrapper without caching or streams.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/courier\n```\n\n```sh [npm]\nnpm install @vielzeug/courier\n```\n\n```sh [yarn]\nyarn add @vielzeug/courier\n```\n\n:::\n\n## Quick Start\n\nCreate one client for an application or request scope, then fetch a cache entry by its explicit key.\n\n```ts\nimport { CourierHttpError, createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com', query: { staleTime: 30_000 } });\nconst key = ['users', 42] as const;\n\ntry {\n await courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: 42 }, signal }),\n });\n console.log(courier.queries.getSnapshot<User>(key)?.data);\n} catch (error) {\n if (CourierHttpError.is(error, 404)) console.log('User not found');\n else throw error;\n} finally {\n courier.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createCourier()`** — one lifecycle, interceptor pipeline, header store, and cancellation boundary.\n- **`get()` / `post()` / `request()`** — typed paths, query strings, request bodies, validation, and structured errors.\n- **`queries.fetch()`** — key-based cached reads, subscriptions, invalidation, and explicit revalidation.\n- **`mutate()`** — direct write operation with a cache callback, without hidden retries or a second state store.\n- **`events()` / `read()`** — abortable SSE, text, and NDJSON iteration with normalized request errors.\n- **`withBearerAuth()` / `withRequestId()` / `withLogging()`** — composable transport policies.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Flux](/flux/) — adapts Courier cache entries and event iterators into composable streams.\n- [Ripple](/ripple/) — stores Courier snapshots in fine-grained reactive state.\n- [Spell](/spell/) — validates parsed HTTP payloads through Courier's schema option.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Courier — API Reference\ndescription: Reference for Courier HTTP, cache, mutation, interceptor, and stream APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCourier()` | Creates unified application client | Sync | Dispose only when whole scope ends |\n| `Courier` HTTP methods | Sends and parses HTTP requests | Async | Direct calls never deduplicate |\n| `queries.fetch()` | Fetches one keyed cache entry | Async | Key must include all response identity inputs |\n| `mutate()` | Runs one write operation | Async | It never retries automatically |\n| `events()` / `read()` | Opens abortable response iterators | Async iteration | Breaking iteration aborts request |\n| `withBearerAuth()` | Adds authorization interceptor | Sync | Token provider runs per request |\n| `withRequestId()` | Adds request identifier interceptor | Sync | Default generator uses `uuid()` |\n| `withLogging()` | Logs request result metadata | Sync | URLs may contain sensitive query values |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/courier` | Client factory, errors, interceptors, and public types |\n| `@vielzeug/courier/devtools` | `debugCourier()` with logging preconfigured |\n\n## Client\n\n### `createCourier()`\n\n```ts\ncreateCourier(options?: CourierOptions): Courier;\n```\n\nReturns client sharing transport configuration, headers, interceptors, cancellation, cache, mutations, and streams.\n\n| `CourierOptions` field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `baseUrl` | `string` | `''` | Prefix for relative request paths |\n| `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Fetch implementation |\n| `headers` | `Record<string, string>` | `{}` | Global request headers |\n| `timeout` | `number` | `30_000` | Default HTTP timeout in milliseconds |\n| `query.staleTime` | `number` | `0` | Cache freshness duration |\n\n**Returns:** `Courier`.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n```\n\n| `Courier` member | Signature | Description |\n| --- | --- | --- |\n| `get` / `post` / `put` / `patch` / `delete` | `<T, P>(url: P, config?) => Promise<T>` | Sends one HTTP request |\n| `request` | `<T, P>(method, url: P, config?) => Promise<T>` | Sends custom HTTP method |\n| `headers` | `(updates) => void` | Updates global headers |\n| `getHeaders` | `() => Readonly<Record<string, string>>` | Returns header snapshot |\n| `use` | `(interceptor) => () => void` | Registers interceptor |\n| `cancelAll` | `() => void` | Aborts active HTTP, cache, and mutation work |\n| `queries` | `QueryCache` | Owns keyed cache entries |\n| `mutate` | `<T>(options) => Promise<T>` | Runs one write operation |\n| `events` | `<T, P>(url, options?) => AsyncIterableIterator<StreamEvent<T>>` | Opens SSE iterator |\n| `read` | `<T, P>(url, options?) => AsyncIterableIterator<T>` | Opens text or NDJSON iterator |\n| `dispose` | `() => void` | Final disposal; aborts work and clears cache |\n| `disposed` | `boolean` | Whether final disposal occurred |\n| `disposalSignal` | `AbortSignal` | Aborts on final disposal |\n\n---\n\n## Queries\n\n### `queries.fetch()`\n\n```ts\nfetch<T>(definition: QueryDefinition<T>, options?: { force?: boolean }): Promise<T>;\n```\n\nRegisters latest definition for `definition.key`, then returns fresh cached data or runs its fetch function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `definition.key` | `QueryKey` | Cache identity; include every response identity input |\n| `definition.fetch` | `(context: QueryContext) => Promise<T>` | Request function for this key |\n| `definition.staleTime` | `number` | Per-entry freshness duration |\n| `options.force` | `boolean` | Fetch even when cached data is fresh |\n\n**Returns:** Cached or fetched data.\n\n```ts\nconst key = ['profile', 1] as const;\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n```\n\n| `QueryCache` method | Returns | Description |\n| --- | --- | --- |\n| `get(key)` | `T \\| undefined` | Returns successful cached data |\n| `getSnapshot(key)` | `AsyncState<T> \\| null` | Returns snapshot by key |\n| `set(key, data, options?)` | `void` | Sets successful cache value |\n| `invalidate(key)` | `void` | Marks matching key prefixes stale |\n| `refetchStale()` | `void` | Starts stale successful entries in background |\n| `keys()` | `QueryKey[]` | Lists known keys |\n| `subscribe(key, listener)` | `Unsubscribe` | Subscribes to one key |\n| `clear()` | `void` | Removes every cache entry |\n\n---\n\n## Mutations\n\n### `mutate()`\n\n```ts\nmutate<T>(options: MutationOptions<T>): Promise<T>;\n```\n\nRuns `options.request` once, then calls `onSuccess` after successful completion.\n\n| `MutationOptions<T>` field | Type | Description |\n| --- | --- | --- |\n| `request` | `(context: MutationContext) => Promise<T>` | Write operation |\n| `onSuccess` | `(data, queries) => void \\| Promise<void>` | Cache update callback |\n| `signal` | `AbortSignal` | Caller-controlled cancellation |\n\n**Returns:** Request result.\n\n---\n\n## Streams\n\n### `events()` and `read()`\n\n```ts\nevents<T, P extends string>(url: P, options?: StreamOptions<P>): AsyncIterableIterator<StreamEvent<T>>;\nread<T, P extends string>(url: P, options?: StreamOptions<P> & { parse?: 'ndjson' | 'text' }): AsyncIterableIterator<T>;\n```\n\nBoth iterators abort request when `return()` runs or `for await` loop exits. `events()` parses `event` and `data`\nfields; it does not retain event IDs or reconnect.\n\n| `StreamOptions` field | Type | Description |\n| --- | --- | --- |\n| `body` | `unknown` | Request body |\n| `method` | `string` | Defaults to GET, or POST when body is present |\n| `params` / `query` | Path and query parameters | Builds URL |\n| `headers` / `fetchInit` | Request configuration | Adds per-request configuration |\n| `signal` | `AbortSignal` | Merges external cancellation |\n| `timeout` | `number` | Stream timeout; omitted means no timeout |\n\n**Returns:** Abortable async iterator.\n\n---\n\n## Interceptors\n\n### Interceptor helpers\n\n```ts\nwithBearerAuth(token: string | (() => string | Promise<string>)): Interceptor;\nwithRequestId(options?: { generate?: () => string; header?: string }): Interceptor;\nwithLogging(options?: {\n logger?: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): Interceptor;\n```\n\nEach helper returns an `Interceptor` accepted by `courier.use()`.\n\n## Types\n\n```ts\ntype AsyncState<T> =\n | { data: undefined; error: null; isFetching: boolean; status: 'loading'; updatedAt: undefined }\n | { data: T; error: null; isFetching: boolean; status: 'success'; updatedAt: number }\n | { data: T | undefined; error: Error; isFetching: false; status: 'error'; updatedAt: number };\n\ntype QueryContext = { readonly key: QueryKey; readonly signal: AbortSignal };\ntype QueryDefinition<T> = { fetch: (context: QueryContext) => Promise<T>; key: QueryKey; staleTime?: number };\ntype QueryKey = readonly [QueryKeyAtom, ...QueryKeyAtom[]];\ntype QueryKeyAtom = string | number | boolean | null | { readonly [key: string]: string | number | boolean | null };\ntype QueryCache = {\n clear(): void;\n fetch<T>(definition: QueryDefinition<T>, options?: { force?: boolean }): Promise<T>;\n get<T>(key: QueryKey): T | undefined;\n getSnapshot<T>(key: QueryKey): AsyncState<T> | null;\n invalidate(key: QueryKey): void;\n keys(): QueryKey[];\n refetchStale(): void;\n set<T>(key: QueryKey, data: T, options?: { updatedAt?: number }): void;\n subscribe(key: QueryKey, listener: () => void): Unsubscribe;\n};\ntype MutationContext = { readonly signal: AbortSignal };\ntype MutationOptions<T> = {\n onSuccess?: (data: T, queries: QueryCache) => void | Promise<void>;\n request: (context: MutationContext) => Promise<T>;\n signal?: AbortSignal;\n};\ntype StreamEvent<T = unknown> = { readonly data: T; readonly event: string };\ntype Unsubscribe = () => void;\n```\n\n```ts\ntype ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype Params = Record<string, ParamValue>;\ntype RequestConfig<P extends string = string, T = unknown> = {\n body?: unknown;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: Record<string, string>;\n params?: Record<string, string | number | boolean>;\n query?: Params;\n responseType?: 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';\n schema?: { parse(data: unknown): T };\n signal?: AbortSignal;\n timeout?: number;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `CourierError` | Base class for all Courier errors | `CourierError.is(error)` |\n| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url` |\n| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |\n| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |\n| `CourierAbortError` | Caller, client, or iterator cancellation | `method`, `url`, `cause` |\n| `CourierSchemaValidationError` | Response schema fails | `data`, `cause` |\n| `CourierParseError` | Response body cannot parse | — |\n| `CourierDisposedError` | Work starts after disposal | — |\n",
@@ -24,9 +24,9 @@
24
24
  }
25
25
  ],
26
26
  "typeSignatures": {
27
- "createCourier": "export { createCourier, type Courier, type CourierOptions } from './courier';",
28
- "Courier": "export { createCourier, type Courier, type CourierOptions } from './courier';",
29
- "CourierOptions": "export { createCourier, type Courier, type CourierOptions } from './courier';",
27
+ "Courier": "export { type Courier, type CourierOptions, createCourier } from './courier';",
28
+ "CourierOptions": "export { type Courier, type CourierOptions, createCourier } from './courier';",
29
+ "createCourier": "export { type Courier, type CourierOptions, createCourier } from './courier';",
30
30
  "CourierAbortError": "export {\n CourierAbortError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n} from './errors';",
31
31
  "CourierDisposedError": "export {\n CourierAbortError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n} from './errors';",
32
32
  "CourierError": "export {\n CourierAbortError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n} from './errors';",
@@ -38,11 +38,11 @@
38
38
  "withBearerAuth": "export { withBearerAuth, withLogging, withRequestId } from './interceptors';",
39
39
  "withLogging": "export { withBearerAuth, withLogging, withRequestId } from './interceptors';",
40
40
  "withRequestId": "export { withBearerAuth, withLogging, withRequestId } from './interceptors';",
41
+ "StreamEvent": "export type { StreamEvent, StreamOptions } from './stream';",
42
+ "StreamOptions": "export type { StreamEvent, StreamOptions } from './stream';",
41
43
  "FetchContext": "export type { FetchContext, Interceptor, TransportOptions } from './transport';",
42
44
  "Interceptor": "export type { FetchContext, Interceptor, TransportOptions } from './transport';",
43
45
  "TransportOptions": "export type { FetchContext, Interceptor, TransportOptions } from './transport';",
44
- "RequestConfig": "export type { HttpRequestConfig as RequestConfig, Params } from './url';",
45
- "Params": "export type { HttpRequestConfig as RequestConfig, Params } from './url';",
46
46
  "AsyncState": "export type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';",
47
47
  "MutationContext": "export type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';",
48
48
  "MutationOptions": "export type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';",
@@ -52,7 +52,7 @@
52
52
  "QueryKey": "export type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';",
53
53
  "QueryKeyAtom": "export type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';",
54
54
  "Unsubscribe": "export type {\n AsyncState,\n MutationContext,\n MutationOptions,\n QueryCache,\n QueryContext,\n QueryDefinition,\n QueryKey,\n QueryKeyAtom,\n Unsubscribe,\n} from './types';",
55
- "StreamEvent": "export type { StreamEvent, StreamOptions } from './stream';",
56
- "StreamOptions": "export type { StreamEvent, StreamOptions } from './stream';"
55
+ "RequestConfig": "export type { HttpRequestConfig as RequestConfig, Params } from './url';",
56
+ "Params": "export type { HttpRequestConfig as RequestConfig, Params } from './url';"
57
57
  }
58
58
  }
@@ -57,21 +57,21 @@
57
57
  "DndError": "export { DndError, DndScopeError } from './errors';",
58
58
  "DndScopeError": "export { DndError, DndScopeError } from './errors';",
59
59
  "matchesAccept": "export function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}",
60
- "DropZoneOptions": "export interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files, { signal }) => {\n * const ok = await checkServerQuota(files, { signal });\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[], context: DropValidationContext) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}",
60
+ "DropZoneOptions": "export interface DropZoneOptions {\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files, { signal }) => {\n * const ok = await checkServerQuota(files, { signal });\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[], context: DropValidationContext) => boolean | Promise<boolean>;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n}",
61
61
  "DropValidationContext": "export interface DropValidationContext {\n /** Aborts when the zone is disposed. Pass this to validation requests. */\n readonly signal: AbortSignal;\n}",
62
62
  "DropZone": "export interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}",
63
63
  "createDropZone": "export function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n const validationControllers = new Set<AbortController>();\n\n const setValidating = (next: boolean): void => {\n if (validating === next) return;\n\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(() => {\n for (const controller of validationControllers) controller.abort();\n\n validationControllers.clear();\n resetCounter();\n });\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n const onValidate = options.onValidate;\n const validationController = onValidate && accepted.length > 0 ? new AbortController() : null;\n\n if (validationController) {\n validationControllers.add(validationController);\n setValidating(true);\n }\n\n const finishValidation = (): void => {\n if (!validationController) return;\n\n validationControllers.delete(validationController);\n\n if (!disposable.disposed) setValidating(validationControllers.size > 0);\n };\n\n let validation: boolean | Promise<boolean>;\n\n try {\n validation =\n validationController && onValidate ? onValidate(accepted, { signal: validationController.signal }) : true;\n } catch (error) {\n validation = Promise.reject(error);\n }\n\n void Promise.resolve(validation)\n .then((valid) => {\n finishValidation();\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n finishValidation();\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}",
64
- "SortableScope": "export interface SortableScope extends Disposable {\n /** `true` while any sortable in this scope is actively dragging. */\n readonly isDragging: boolean;\n readonly [SCOPE_BRAND]: true;\n /**\n * Calls the revert function registered for the most recent cross-container move.\n * A no-op when no move registered a revert function.\n */\n revert(): void;\n}",
65
- "AutoScrollOptions": "export interface AutoScrollOptions {\n /** Distance in pixels from an edge that triggers auto-scroll. @default 32 */\n edgeThreshold?: number;\n /** Pixels scrolled per dragover frame while near an edge. @default 18 */\n speed?: number;\n /** Scroll the sortable container while dragging near its edges. @default true */\n container?: boolean;\n /** Scroll the viewport while dragging near the window edges. @default false */\n viewport?: boolean;\n}",
64
+ "SortableScope": "export interface SortableScope extends Disposable {\n /** `true` while any sortable in this scope is actively dragging. */\n readonly isDragging: boolean;\n /**\n * Calls the revert function registered for the most recent cross-container move.\n * A no-op when no move registered a revert function.\n */\n revert(): void;\n readonly [SCOPE_BRAND]: true;\n}",
65
+ "AutoScrollOptions": "export interface AutoScrollOptions {\n /** Scroll the sortable container while dragging near its edges. @default true */\n container?: boolean;\n /** Distance in pixels from an edge that triggers auto-scroll. @default 32 */\n edgeThreshold?: number;\n /** Pixels scrolled per dragover frame while near an edge. @default 18 */\n speed?: number;\n /** Scroll the viewport while dragging near the window edges. @default false */\n viewport?: boolean;\n}",
66
66
  "ReorderEvent": "export interface ReorderEvent {\n /** The new ordered list of item keys after the reorder. */\n ids: string[];\n /**\n * Register a revert function that will be called when `sortable.revert()` is invoked.\n * Useful for rolling back optimistic UI updates on server error.\n * Only the most recent `setRevert` registration is retained — a new reorder overwrites it.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n setRevert(fn: () => void): void;\n}",
67
- "SortableMoveEvent": "export interface SortableMoveEvent {\n /** Stable identity of the moved item. */\n readonly itemId: string;\n /** Source container before the move. */\n readonly source: HTMLElement;\n /** Ordered source item IDs after the move. */\n readonly sourceIds: string[];\n /** Target container after the move. */\n readonly target: HTMLElement;\n /** Ordered target item IDs after the move. */\n readonly targetIds: string[];\n /** Registers a rollback for the most recent scope move. */\n setRevert(fn: () => void): void;\n}",
67
+ "SortableMoveEvent": "export interface SortableMoveEvent {\n /** Stable identity of the moved item. */\n readonly itemId: string;\n /** Registers a rollback for the most recent scope move. */\n setRevert(fn: () => void): void;\n /** Source container before the move. */\n readonly source: HTMLElement;\n /** Ordered source item IDs after the move. */\n readonly sourceIds: string[];\n /** Target container after the move. */\n readonly target: HTMLElement;\n /** Ordered target item IDs after the move. */\n readonly targetIds: string[];\n}",
68
68
  "SortableScopeOptions": "export interface SortableScopeOptions {\n /**\n * Called exactly once for every successful cross-container move.\n * Local reorders continue to use each sortable's `onReorder` callback.\n */\n onMove?: (event: SortableMoveEvent) => void;\n /**\n * Enables touch input for sortable items registered to this scope.\n * The controller ignores unrelated document draggables.\n */\n touch?: boolean | TouchInputOptions;\n}",
69
69
  "SortableTouchOptions": "export type SortableTouchOptions = TouchInputOptions;",
70
- "SortableOptions": "export interface SortableOptions {\n /** Container element whose direct-child items are sortable. */\n element: HTMLElement;\n /** Shared scope for connected sortable containers. Containers only exchange items within the same scope. */\n scope?: SortableScope;\n /**\n * Selector for the drag handle inside each item.\n * When omitted the whole item is the handle.\n */\n handle?: string;\n /**\n * Enables keyboard-based reordering using arrow keys plus Home/End.\n * @default true\n */\n keyboard?: boolean;\n /**\n * Returns the identity key for a given item element.\n * This separates the \"what is this item?\" concern (yours) from the \"which children\n * are sortable?\" concern (ours — marked with `data-dnd-item`).\n *\n * @example\n * ```ts\n * getKey: (el) => el.dataset.taskId!\n * ```\n */\n getKey: (element: HTMLElement) => string;\n /** Sorting axis used to compute insertion position. @default 'vertical' */\n axis?: 'vertical' | 'horizontal';\n /** Auto-scrolls the container (and viewport) near edges while dragging. @default true */\n autoScroll?: boolean | AutoScrollOptions;\n /** Optional custom drag preview element. */\n dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);\n /** CSS class applied to the placeholder element. @default 'dnd-placeholder' */\n placeholderClass?: string;\n /**\n * Called with a {@link ReorderEvent} after a successful reorder, only when the order changed.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n onReorder?: (event: ReorderEvent) => void;\n /**\n * Called just before a successful drag commit with the before and after order snapshots.\n * Use this hook to set up FLIP animations — the source items are still in their\n * pre-commit positions at the time of the call.\n *\n * @example\n * ```ts\n * onBeforeReorder: (from, to) => {\n * // record element positions here, then animate after the next microtask\n * }\n * ```\n */\n onBeforeReorder?: (from: string[], to: string[]) => void;\n /**\n * When `true`, drag interactions are ignored.\n *\n * Note: if `disabled` transitions to `true` while a drag is in progress the\n * drag is treated as a cancellation — the item snaps back to its original\n * position rather than committing the last placeholder location.\n */\n disabled?: boolean;\n /** Called when the user starts dragging an item. */\n onDragStart?: (id: string, event: DragEvent) => void;\n /** Called when a drag ends (whether dropped or cancelled). */\n onDragEnd?: (id: string, event: DragEvent) => void;\n /**\n * Hotspot offset `[x, y]` passed to `setDragImage`.\n * Controls which point of the preview image follows the cursor.\n * @default [0, 0]\n */\n dragImageOffset?: [number, number];\n}",
70
+ "SortableOptions": "export interface SortableOptions {\n /** Auto-scrolls the container (and viewport) near edges while dragging. @default true */\n autoScroll?: boolean | AutoScrollOptions;\n /** Sorting axis used to compute insertion position. @default 'vertical' */\n axis?: 'vertical' | 'horizontal';\n /**\n * When `true`, drag interactions are ignored.\n *\n * Note: if `disabled` transitions to `true` while a drag is in progress the\n * drag is treated as a cancellation — the item snaps back to its original\n * position rather than committing the last placeholder location.\n */\n disabled?: boolean;\n /** Optional custom drag preview element. */\n dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);\n /**\n * Hotspot offset `[x, y]` passed to `setDragImage`.\n * Controls which point of the preview image follows the cursor.\n * @default [0, 0]\n */\n dragImageOffset?: [number, number];\n /** Container element whose direct-child items are sortable. */\n element: HTMLElement;\n /**\n * Returns the identity key for a given item element.\n * This separates the \"what is this item?\" concern (yours) from the \"which children\n * are sortable?\" concern (ours — marked with `data-dnd-item`).\n *\n * @example\n * ```ts\n * getKey: (el) => el.dataset.taskId!\n * ```\n */\n getKey: (element: HTMLElement) => string;\n /**\n * Selector for the drag handle inside each item.\n * When omitted the whole item is the handle.\n */\n handle?: string;\n /**\n * Enables keyboard-based reordering using arrow keys plus Home/End.\n * @default true\n */\n keyboard?: boolean;\n /**\n * Called just before a successful drag commit with the before and after order snapshots.\n * Use this hook to set up FLIP animations — the source items are still in their\n * pre-commit positions at the time of the call.\n *\n * @example\n * ```ts\n * onBeforeReorder: (from, to) => {\n * // record element positions here, then animate after the next microtask\n * }\n * ```\n */\n onBeforeReorder?: (from: string[], to: string[]) => void;\n /** Called when a drag ends (whether dropped or cancelled). */\n onDragEnd?: (id: string, event: DragEvent) => void;\n /** Called when the user starts dragging an item. */\n onDragStart?: (id: string, event: DragEvent) => void;\n /**\n * Called with a {@link ReorderEvent} after a successful reorder, only when the order changed.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n onReorder?: (event: ReorderEvent) => void;\n /** CSS class applied to the placeholder element. @default 'dnd-placeholder' */\n placeholderClass?: string;\n /** Shared scope for connected sortable containers. Containers only exchange items within the same scope. */\n scope?: SortableScope;\n}",
71
71
  "Sortable": "export interface Sortable extends Disposable {\n readonly isDragging: boolean;\n /**\n * Calls the revert function registered via `setRevert` in the last `onReorder` invocation (if any) and clears it.\n * A no-op when no revert function was registered or has already been consumed.\n *\n * Works for both drag-based and keyboard-based reorders.\n * Note: only the most recent reorder can be reverted; a new reorder overwrites the stored function.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * // later, on server error:\n * sortable.revert();\n * ```\n */\n revert(): void;\n /**\n * Re-reads the container's children and reapplies `draggable`, ARIA roles,\n * and handle attributes. Call this after programmatically adding, removing,\n * or replacing items — e.g. after a framework render that replaces DOM nodes.\n *\n * Not needed when items are only reordered via drag or keyboard.\n */\n sync(): void;\n}",
72
72
  "createSortableScope": "export function createSortableScope(options: SortableScopeOptions = {}): SortableScope {\n const state: SortableScopeState = {\n active: null,\n commitMove(event): void {\n options.onMove?.({\n ...event,\n setRevert(fn): void {\n state.lastRevert = fn;\n },\n });\n },\n disposables: new Set(),\n handles: new Set(),\n lastRevert: null,\n touch: null,\n };\n const disposable = createDisposable(() => {\n state.touch?.dispose();\n\n // Dispose all registered sortables (each dispose() call is idempotent)\n for (const disposeFn of state.disposables) {\n disposeFn();\n }\n });\n\n const scope = {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get isDragging() {\n return state.active !== null;\n },\n revert() {\n state.lastRevert?.();\n state.lastRevert = null;\n },\n [SCOPE_BRAND]: true as const,\n [Symbol.dispose]: disposable[Symbol.dispose],\n } as SortableScope;\n\n sortableScopeStates.set(scope, state);\n\n if (options.touch) {\n state.touch = createScopeTouchController(options.touch === true ? {} : options.touch, (target) => {\n for (const handle of state.handles) {\n const dragTarget = handle.resolveTouchTarget(target);\n\n if (dragTarget) return dragTarget;\n }\n\n return null;\n });\n }\n\n return scope;\n}",
73
73
  "createSortable": "export function createSortable(options: SortableOptions): Sortable {\n const {\n autoScroll = true,\n axis = 'vertical',\n element,\n getKey,\n handle,\n keyboard = true,\n placeholderClass = 'dnd-placeholder',\n scope = createSortableScope(),\n } = options;\n const autoScrollOptions = resolveAutoScrollOptions(autoScroll);\n const scopeState = getSortableScopeState(scope);\n\n if (handle !== undefined && handle.trim() === '') {\n warn(\n 'handle option is an empty string — no handle elements will be found. Provide a valid CSS selector or omit the option.',\n );\n }\n\n const getItems = (): HTMLElement[] =>\n Array.from(element.children).filter((c) => (c as HTMLElement).hasAttribute(ITEM_ATTR)) as HTMLElement[];\n\n const getOrderedIds = (): string[] => getItems().map((el) => getKey(el));\n const managedElements = new Map<HTMLElement, ManagedElementState>();\n const originalContainerRole = element.getAttribute('role');\n\n const rememberElement = (managedElement: HTMLElement): ManagedElementState => {\n const existing = managedElements.get(managedElement);\n\n if (existing) return existing;\n\n const state: ManagedElementState = {\n dataDndHandle: managedElement.getAttribute(HANDLE_ATTR),\n dataDndItem: managedElement.getAttribute(ITEM_ATTR),\n draggable: managedElement.getAttribute('draggable'),\n role: managedElement.getAttribute('role'),\n tabIndex: managedElement.getAttribute('tabindex'),\n touchAction: managedElement.style.touchAction,\n };\n\n managedElements.set(managedElement, state);\n\n return state;\n };\n\n const restoreAttribute = (managedElement: HTMLElement, name: string, value: string | null): void => {\n if (value === null) {\n managedElement.removeAttribute(name);\n } else {\n managedElement.setAttribute(name, value);\n }\n };\n\n const syncItems = (): void => {\n getItems().forEach((el) => {\n const itemState = rememberElement(el);\n\n if (itemState.role === null) el.setAttribute('role', 'listitem');\n\n if (itemState.tabIndex === null) el.tabIndex = 0;\n\n if (handle) {\n el.querySelectorAll<HTMLElement>(handle).forEach((handleEl) => {\n rememberElement(handleEl);\n handleEl.setAttribute(HANDLE_ATTR, '');\n handleEl.setAttribute('draggable', 'true');\n handleEl.style.touchAction = 'none';\n });\n } else {\n el.setAttribute('draggable', 'true');\n // A native mouse drag has no competing gesture to arbitrate; touch does. Without this,\n // a mobile browser can decide the very first bit of finger movement is a page\n // scroll/pan — a decision it makes independently of, and before, this library's own\n // touch-shim threshold/`preventDefault()` logic ever runs — and hand the rest of the\n // gesture to native scrolling. Once that happens the item never receives the\n // `dragover` sequence needed to update the drop target, so the session ends up\n // committing back to wherever it started: indistinguishable from the drop \"reverting\".\n // `touch-action: none` opts the element out of every default touch gesture from\n // `touchstart` onward, leaving the whole interaction to this library's own JS.\n el.style.touchAction = 'none';\n }\n });\n };\n\n const markItems = (): void => {\n const seenKeys = new Set<string>();\n\n // Mark all children that have a key as sortable items\n Array.from(element.children).forEach((child) => {\n const el = child as HTMLElement;\n\n try {\n const key = getKey(el);\n\n if (key) {\n rememberElement(el);\n\n if (seenKeys.has(key)) {\n warn(\n `getKey returned the duplicate key \"${key}\" for two sibling items — onReorder's ids and applyReorder may become inconsistent. Ensure getKey returns a unique value per item.`,\n );\n } else {\n seenKeys.add(key);\n }\n\n el.setAttribute(ITEM_ATTR, '');\n }\n } catch (err) {\n warn(\n `getKey threw for a child element — the item will not be sortable. Check your getKey implementation. ${String(err)}`,\n );\n }\n });\n\n syncItems();\n };\n\n const cleanupItems = (): void => {\n for (const [managedElement, state] of managedElements) {\n restoreAttribute(managedElement, HANDLE_ATTR, state.dataDndHandle);\n restoreAttribute(managedElement, ITEM_ATTR, state.dataDndItem);\n restoreAttribute(managedElement, 'draggable', state.draggable);\n restoreAttribute(managedElement, 'role', state.role);\n restoreAttribute(managedElement, 'tabindex', state.tabIndex);\n managedElement.style.touchAction = state.touchAction;\n }\n\n managedElements.clear();\n };\n\n const createPlaceholder = (source: HTMLElement): HTMLElement => {\n const p = document.createElement('div');\n\n p.className = placeholderClass;\n p.setAttribute('aria-hidden', 'true');\n\n if (axis === 'horizontal') {\n p.style.width = `${source.offsetWidth}px`;\n } else {\n p.style.height = `${source.offsetHeight}px`;\n }\n\n return p;\n };\n\n let lastRevert: (() => void) | null = null;\n\n const handle_: ContainerHandle = {\n commitReorder: (orderedIds) => {\n if (!options.onReorder) return;\n\n const event: ReorderEvent = {\n ids: orderedIds,\n setRevert(fn) {\n lastRevert = fn;\n },\n };\n\n options.onReorder(event);\n },\n element,\n getOrderedIds,\n isDisabled: () => resolveDisabled(options.disabled),\n notifyBeforeReorder: (from, to) => options.onBeforeReorder?.(from, to),\n notifyDragEnd: (id, event) => options.onDragEnd?.(id, event),\n notifyDragStart: (id, event) => options.onDragStart?.(id, event),\n resolveTouchTarget: (target) => {\n if (resolveDisabled(options.disabled) || !element.contains(target)) return null;\n\n const item = target.closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item || !element.contains(item)) return null;\n\n if (!handle) return item;\n\n const handleTarget = target.closest<HTMLElement>(handle);\n\n return handleTarget && item.contains(handleTarget) ? handleTarget : null;\n },\n };\n\n scopeState.handles.add(handle_);\n\n const handleDragStart = (e: DragEvent): void => {\n if (scopeState.active) return;\n\n if (handle_.isDisabled()) return;\n\n const target = e.target as HTMLElement;\n const item = target.closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item) return;\n\n if (handle && !target.closest(handle)) return;\n\n const originalParent = item.parentElement;\n\n if (!originalParent) return;\n\n const placeholder = createPlaceholder(item);\n const originalNextSibling = item.nextSibling;\n const activeId = getKey(item);\n\n // Snapshot only the source handle at drag start; targets are snapshotted lazily.\n const initialOrders = new Map<ContainerHandle, string[]>();\n\n initialOrders.set(handle_, handle_.getOrderedIds());\n item.setAttribute('data-dragging', '');\n originalParent.insertBefore(placeholder, originalNextSibling);\n\n const session: DragSession = {\n draggedEl: item,\n draggedId: activeId,\n hideFrame: null,\n initialOrders,\n originalDisplay: item.style.display,\n originalNextSibling,\n originalParent,\n placeholder,\n source: handle_,\n target: handle_,\n };\n\n if (!isTouchDragEvent(e) || e.__dndTouchPreview) scheduleHide(session);\n\n scopeState.active = session;\n\n if (e.dataTransfer) {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', activeId);\n\n if (options.dragImage) {\n const preview =\n typeof options.dragImage === 'function' ? options.dragImage(activeId, item, e) : options.dragImage;\n const [offsetX, offsetY] = options.dragImageOffset ?? [0, 0];\n\n if (preview) e.dataTransfer.setDragImage(preview, offsetX, offsetY);\n }\n }\n\n handle_.notifyDragStart(session.draggedId, e);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n const session = scopeState.active;\n\n if (!session) return;\n\n if (session.source.isDisabled() || handle_.isDisabled()) return;\n\n e.preventDefault();\n maybeAutoScroll(e, element, axis, autoScrollOptions);\n\n // Lazily snapshot this handle's order the first time it becomes a target.\n snapshotOrder(session, handle_);\n\n const { draggedEl, placeholder } = session;\n const target = (e.target as HTMLElement).closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!target) {\n // Only append placeholder when it isn't already inside this container.\n // Moving it to the end on every over-empty-space event causes the\n // placeholder to oscillate between positions as the cursor moves.\n if (placeholder.parentElement !== element) {\n element.appendChild(placeholder);\n }\n\n session.target = handle_;\n\n return;\n }\n\n if (target === draggedEl || target === placeholder) return;\n\n const rect = target.getBoundingClientRect();\n const insertAfter =\n axis === 'vertical' ? e.clientY >= rect.top + rect.height / 2 : e.clientX >= rect.left + rect.width / 2;\n\n element.insertBefore(placeholder, insertAfter ? target.nextSibling : target);\n session.target = handle_;\n };\n\n const handleDrop = (e: DragEvent): void => {\n const session = scopeState.active;\n\n if (!session) return;\n\n if (session.source.isDisabled() || handle_.isDisabled()) return;\n\n e.preventDefault();\n // Record the drop target; the actual commit happens in handleDragEnd where\n // dataTransfer.dropEffect tells us whether the browser accepted the operation.\n session.target = handle_;\n };\n\n const handleDragEnd = (e: DragEvent): void => {\n if (scopeState.active?.source !== handle_) return;\n\n finishSession(scopeState, e, false);\n };\n\n const handleKeydown = (e: KeyboardEvent): void => {\n if (!keyboard || handle_.isDisabled()) return;\n\n const tagName = (e.target as HTMLElement | null)?.tagName;\n\n if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return;\n\n const item = (e.target as HTMLElement).closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item || !element.contains(item)) return;\n\n const prevOrder = getOrderedIds();\n const newOrder = applyKeyboardReorder(item, element, getItems, getOrderedIds, e.key, axis);\n\n // null means unrecognized key or boundary — let the browser handle it (e.g. page scroll)\n if (newOrder === null) return;\n\n e.preventDefault();\n handle_.notifyBeforeReorder(prevOrder, newOrder);\n handle_.commitReorder(newOrder);\n };\n\n markItems();\n\n const disposable = createDisposable(() => {\n scopeState.disposables.delete(disposable.dispose);\n\n if (scopeState.active && (scopeState.active.source === handle_ || scopeState.active.target === handle_)) {\n finishSession(scopeState, new Event('dragend') as DragEvent, true);\n }\n\n scopeState.handles.delete(handle_);\n restoreAttribute(element, 'role', originalContainerRole);\n cleanupItems();\n });\n\n if (originalContainerRole === null) element.setAttribute('role', 'list');\n\n element.addEventListener('dragstart', handleDragStart, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n element.addEventListener('dragend', handleDragEnd, { signal: disposable.disposalSignal });\n element.addEventListener('keydown', handleKeydown, { signal: disposable.disposalSignal });\n\n // Register with scope so scope.dispose() can tear this down\n scopeState.disposables.add(disposable.dispose);\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get isDragging() {\n return scopeState.active?.source === handle_;\n },\n revert: () => {\n lastRevert?.();\n lastRevert = null;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n sync: () => {\n markItems();\n },\n };\n}",
74
74
  "applyReorder": "export function applyReorder<T>(items: T[], ids: string[], getKey: (item: T) => string): T[] {\n const byId = new Map(items.map((item) => [getKey(item), item] as const));\n const ordered: T[] = [];\n\n for (const id of ids) {\n if (!byId.has(id)) continue;\n\n const item = byId.get(id) as T;\n\n ordered.push(item);\n byId.delete(id);\n }\n\n for (const item of byId.values()) ordered.push(item);\n\n return ordered;\n}",
75
- "Disposable": "export interface Disposable {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n}"
75
+ "Disposable": "export interface Disposable {\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n [Symbol.dispose](): void;\n}"
76
76
  }
77
77
  }
@@ -14,6 +14,15 @@
14
14
  }
15
15
  ],
16
16
  "typeSignatures": {
17
+ "batch": "export { batch, createTaskGroup } from './_pool';",
18
+ "createTaskGroup": "export { batch, createTaskGroup } from './_pool';",
19
+ "FamiliarError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
20
+ "FamiliarInvalidOptionsError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
21
+ "FamiliarQueueFullError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
22
+ "FamiliarRuntimeError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
23
+ "FamiliarTaskError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
24
+ "FamiliarTerminatedError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
25
+ "FamiliarTimeoutError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
17
26
  "BatchOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
18
27
  "DrainOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
19
28
  "RunOptions": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
@@ -24,15 +33,6 @@
24
33
  "WorkerPool": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
25
34
  "WorkerStats": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
26
35
  "WorkerStatus": "export type {\n BatchOptions,\n DrainOptions,\n RunOptions,\n StreamWorkerPool,\n TaskGroup,\n TaskGroupOptions,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types';",
27
- "FamiliarError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
28
- "FamiliarInvalidOptionsError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
29
- "FamiliarQueueFullError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
30
- "FamiliarRuntimeError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
31
- "FamiliarTaskError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
32
- "FamiliarTerminatedError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
33
- "FamiliarTimeoutError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors';",
34
- "batch": "export { batch, createTaskGroup } from './_pool';",
35
- "createTaskGroup": "export { batch, createTaskGroup } from './_pool';",
36
36
  "RunningStream": "export type RunningStream<TChunk> = {\n done: Promise<void>;\n iterable: AsyncIterable<TChunk>;\n};",
37
37
  "createWorker": "export function createWorker<TInput, TOutput>(\n url: URL | string,\n options: WorkerOptions = {},\n): WorkerPool<TInput, TOutput> {\n const resolved = resolveOptions(options);\n\n return createPool(slots<TInput, TOutput>(url, resolved), {\n concurrency: resolved.concurrency,\n defaultTimeout: resolved.timeout,\n maxQueue: resolved.maxQueue,\n onFull: resolved.onFull,\n });\n}",
38
38
  "createStreamWorker": "export function createStreamWorker<TInput, TChunk>(\n url: URL | string,\n options: WorkerOptions = {},\n): StreamWorkerPool<TInput, TChunk> {\n const resolved = resolveOptions(options);\n\n return createStreamPool(slots<TInput, TChunk>(url, resolved), {\n concurrency: resolved.concurrency,\n defaultTimeout: resolved.timeout,\n maxQueue: resolved.maxQueue,\n onFull: resolved.onFull,\n });\n}"
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export { toAsyncIterable } from './async';\nexport { stream } from './core';\nexport { FluxError, FluxTimeoutError } from './errors';\nexport { combineLatest, concat, merge } from './operators/combination';\nexport { from, fromEvent, interval, of, timer } from './operators/creation';\nexport type { IntervalOptions, TimerOptions } from './operators/creation';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering';\nexport type { DebounceOptions, TimeoutOptions } from './operators/filtering';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';\nexport type { ConcatMapOptions } from './operators/transformation';\nexport { first, last, retry, toArray } from './operators/utility';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';\nexport { pipe } from './pipe';\nexport type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';\n",
2
+ "apiSource": "export { toAsyncIterable } from './async';\nexport { stream } from './core';\nexport { FluxError, FluxTimeoutError } from './errors';\nexport { combineLatest, concat, merge } from './operators/combination';\nexport type { IntervalOptions, TimerOptions } from './operators/creation';\nexport { from, fromEvent, interval, of, timer } from './operators/creation';\nexport type { DebounceOptions, TimeoutOptions } from './operators/filtering';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering';\nexport type { ConcatMapOptions } from './operators/transformation';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';\nexport { first, last, retry, toArray } from './operators/utility';\nexport { pipe } from './pipe';\nexport type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Flux — Explicit push streams for TypeScript\ndescription: Reusable push streams with subscription-owned cancellation, bounded buffering, and optional ecosystem adapters.\npackage: flux\ncategory: reactive\nkeywords: [streams, reactive, operators, cancellation, buffering, channels]\nrelated: [ripple, herald, pulse, courier]\nexports: [stream, pipe, of, from, fromEvent, interval, timer, map, filter, scan, switchMap, mergeMap, concatMap, take, takeUntil, debounce, timeout, merge, concat, combineLatest, retry, toArray, first, last, toAsyncIterable]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"flux\" />\n\n## Why Flux?\n\nUse Flux when an API pushes many values over time and consumers need independent cancellation. Streams describe reusable work; subscriptions own cleanup. Explicit queue capacity keeps async iteration from silently growing memory.\n\n```ts\n// Before\nconst controller = new AbortController();\nconst render = (value: string) => console.log(value);\nconst handler = (event: Event) => render((event.target as HTMLInputElement).value);\ninput.addEventListener('input', handler);\nsetTimeout(() => controller.abort(), 5_000);\n\n// After\nimport { fromEvent, map, pipe, takeUntil } from '@vielzeug/flux';\n\nconst updates = pipe(\n fromEvent<InputEvent>(input, 'input'),\n map((event) => (event.target as HTMLInputElement).value),\n takeUntil(controller.signal),\n);\n\nupdates.subscribe({ error: console.error, next: render });\n```\n\n| Feature | Flux | RxJS | TC39 Observable |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"flux\" type=\"size\" /> | Varies by imported operators | Native proposal / polyfill |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Subscription-owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit async queue policy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Operator-dependent | No standard policy |\n| Vielzeug adapters | Ripple, Courier, Herald, Pulse | Manual adapters | Manual adapters |\n\n<div class=\"decision-callout\">\n\n**Use Flux when** you need a small TypeScript stream primitive, explicit cancellation, and first-party Vielzeug adapters.\n\n**Consider RxJS when** you need its larger operator catalog or third-party Observable integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/flux\n```\n\n```sh [npm]\nnpm install @vielzeug/flux\n```\n\n```sh [yarn]\nyarn add @vielzeug/flux\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { toArray, interval, map, pipe, take } from '@vielzeug/flux';\n\nconst firstThree = pipe(\n interval({ every: 100 }),\n map((value) => value * 2),\n take(3),\n);\n\ntry {\n console.log(await toArray(firstThree, { maxItems: 3 })); // [0, 2, 4]\n} catch (reason) {\n console.error('Stream failed', reason);\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `stream()` — define cold reusable work with one teardown function\n- `pipe()` — compose any number of typed operators\n- `Subscription` — own cancellation through `unsubscribe()` or `AbortSignal`\n- `createChannel()` — mutable multicast state with bounded replay\n- `toAsyncIterable()` — explicit capacity and overflow policy for pull consumers\n- `retry()` — retry failures with optional backoff\n- `fromSignal()` / `toSignal()` — bridge Ripple signals\n- `fromQuery()` / `fromSse()` — adapt Courier state and events\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — adapt reactive signal state through `@vielzeug/flux/ripple`.\n- [Courier](/courier/) — adapt query snapshots and SSE events through `@vielzeug/flux/courier`.\n- [Herald](/herald/) — adapt typed bus events through `@vielzeug/flux/herald`.\n- [Pulse](/pulse/) — adapt connection and presence events through `@vielzeug/flux/pulse`.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Flux — API Reference\ndescription: Complete reference for @vielzeug/flux streams, operators, channels, and adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `stream()` | Create cold stream | Lazy | Return one teardown function |\n| `pipe()` | Compose operators | Lazy | Source is first argument |\n| `of()` / `from()` | Convert known values | Sync / mixed | `from()` promise cannot be aborted |\n| `fromEvent()` | Adapt event target | Async | Unsubscribe removes listener |\n| `interval()` / `timer()` | Create timed values | Async | Use `take()` or unsubscribe for intervals |\n| `map()` / `filter()` / `scan()` | Transform values | Sync | Callback throws terminate stream |\n| `switchMap()` / `mergeMap()` / `concatMap()` | Flatten streams | Mixed | `concatMap()` queue is bounded |\n| `take()` / `takeUntil()` | Stop values | Mixed | Notifier emission completes output |\n| `debounce()` / `timeout()` / `retry()` | Control time and failures | Async | `timeout()` measures inactivity |\n| `merge()` / `concat()` / `combineLatest()` | Combine streams | Mixed | `combineLatest()` waits for every source |\n| `toArray()` / `first()` / `last()` | Consume finite values | Async | Bound `toArray()` with `maxItems` |\n| `toAsyncIterable()` | Use `for await` | Async | Capacity and overflow required |\n| `createChannel()` | Imperative multicast boundary | Sync | Dispose to complete subscribers |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/flux` | Core streams, operators, consumers, errors, and types |\n| `@vielzeug/flux/async` | `toAsyncIterable()` only |\n| `@vielzeug/flux/subjects` | `createChannel()` and channel types |\n| `@vielzeug/flux/ripple` | Ripple signal adapters |\n| `@vielzeug/flux/courier` | Courier query and SSE adapters |\n| `@vielzeug/flux/herald` | Herald bus adapters |\n| `@vielzeug/flux/pulse` | Pulse event and presence adapters |\n\n## Core\n\n### `stream()`\n\n```ts\nstream<T>(producer: Producer<T>): Stream<T>\n```\n\nCreates cold reusable work. Producer runs once for every subscription.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `producer` | `Producer<T>` | Emits through sink and returns optional teardown |\n\n**Returns:** `Stream<T>`.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst ticks = stream<number>((sink) => {\n const id = setInterval(() => sink.next(Date.now()), 1_000);\n return () => clearInterval(id);\n});\n```\n\n---\n\n### `pipe()`\n\n```ts\npipe<Input, Operators>(source: Stream<Input>, ...operators: Operators): Stream<Output>\n```\n\nApplies operators left to right while inferring output value type.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `Stream<Input>` | Source stream |\n| `operators` | `Operator[]` | Operators applied in order |\n\n**Returns:** transformed `Stream<Output>`.\n\n```ts\nimport { map, of, pipe } from '@vielzeug/flux';\n\nconst labels = pipe(of(1, 2), map((value) => `#${value}`));\n```\n\n## Creation\n\n### `of()`\n\n```ts\nof<T>(...values: T[]): Stream<T>\n```\n\nEmits every value synchronously, then completes.\n\n```ts\nimport { of } from '@vielzeug/flux';\n\nof(1, 2, 3).subscribe(console.log);\n```\n\n---\n\n### `from()`\n\n```ts\nfrom<T>(source: Iterable<T> | AsyncIterable<T> | Promise<T>): Stream<T>\n```\n\nConverts iterable, async iterable, or promise into a stream. Cancellation stops iterable consumption and calls `return()` when available.\n\n```ts\nimport { from } from '@vielzeug/flux';\n\nfrom(Promise.resolve('ready')).subscribe({ error: console.error, next: console.log });\n```\n\n---\n\n### `fromEvent()`\n\n```ts\nfromEvent<T = Event>(target, type: string): Stream<T>\n```\n\nEmits target events until subscription ends.\n\n```ts\nimport { fromEvent } from '@vielzeug/flux';\n\nfromEvent<MouseEvent>(document, 'click').subscribe(console.log);\n```\n\n---\n\n### `interval()`\n\n```ts\ninterval(options: IntervalOptions): Stream<number>\n```\n\nEmits incrementing values starting at zero.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `every` | `number` | Non-negative interval duration in milliseconds |\n\n---\n\n### `timer()`\n\n```ts\ntimer(options: TimerOptions): Stream<number>\n```\n\nEmits zero after `delay`; optionally continues at `interval`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `delay` | `number` | Non-negative initial delay in milliseconds |\n| `interval` | `number` | Optional non-negative repeat duration |\n\n## Transformation Operators\n\n### `map()`\n\n```ts\nmap<A, B>(project: (value: A) => B): Operator<A, B>\n```\n\nMaps every value. A thrown callback error terminates output.\n\n---\n\n### `filter()`\n\n```ts\nfilter<T>(predicate: (value: T) => boolean): Operator<T, T>\n```\n\nForwards values matching predicate.\n\n---\n\n### `scan()`\n\n```ts\nscan<T, A>(reducer: (state: A, value: T) => A, initial: A): Operator<T, A>\n```\n\nEmits accumulated state after every source value.\n\n---\n\n### `switchMap()`\n\n```ts\nswitchMap<A, B>(project: (value: A) => Stream<B>): Operator<A, B>\n```\n\nCancels previous inner stream when source emits.\n\n---\n\n### `mergeMap()`\n\n```ts\nmergeMap<A, B>(project: (value: A) => Stream<B>): Operator<A, B>\n```\n\nRuns every inner stream concurrently.\n\n---\n\n### `concatMap()`\n\n```ts\nconcatMap<A, B>(project: (value: A) => Stream<B>, options: ConcatMapOptions): Operator<A, B>\n```\n\nRuns inner streams in order. Exceeding capacity errors output.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive maximum queued source values |\n\n## Control Operators\n\n### `take()`\n\n```ts\ntake<T>(count: number): Operator<T, T>\n```\n\nForwards `count` values, cancels upstream, then completes. Count must be non-negative integer.\n\n---\n\n### `takeUntil()`\n\n```ts\ntakeUntil<T>(notifier: AbortSignal | Stream<unknown>): Operator<T, T>\n```\n\nCompletes when notifier aborts or emits.\n\n---\n\n### `debounce()`\n\n```ts\ndebounce<T>(options: DebounceOptions): Operator<T, T>\n```\n\nEmits latest value after configured silence. Pending value flushes on source completion.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `for` | `number` | Non-negative silence duration in milliseconds |\n\n---\n\n### `timeout()`\n\n```ts\ntimeout<T>(options: TimeoutOptions): Operator<T, T>\n```\n\nErrors with `FluxTimeoutError` when source is silent too long.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `after` | `number` | Non-negative inactivity duration in milliseconds |\n\n---\n\n### `retry()`\n\n```ts\nretry<T>(options: RetryOptions): Operator<T, T>\n```\n\nResubscribes after source errors until attempts are exhausted.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `attempts` | `number` | Non-negative retry count |\n| `delay` | `number \\| (attempt: number) => number` | Optional delay or backoff function |\n\n## Combination\n\n### `merge()`\n\n```ts\nmerge<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nForwards values from all sources and completes after every source completes.\n\n---\n\n### `concat()`\n\n```ts\nconcat<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nSubscribes to each source only after previous source completes.\n\n---\n\n### `combineLatest()`\n\n```ts\ncombineLatest<T extends readonly Stream<unknown>[]>(...sources: T): Stream<{ [K in keyof T]: T[K] extends Stream<infer V> ? V : never }>\n```\n\nEmits latest tuple after every source emits once. Completes without emission when a source completes before first value.\n\n## Value Consumers\n\n### `toArray()`\n\n```ts\ntoArray<T>(source: Stream<T>, options: ToArrayOptions): Promise<T[]>\n```\n\nCollects finite output. Rejects on source error, abort, or `maxItems` overflow.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `maxItems` | `number` | Non-negative maximum toArrayed values |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n---\n\n### `first()`\n\n```ts\nfirst<T>(source: Stream<T>, options?: ValueOptions): Promise<T>\n```\n\nResolves first value and cancels source. Rejects on source error or abort.\n\n---\n\n### `last()`\n\n```ts\nlast<T>(source: Stream<T>, options?: ValueOptions): Promise<T | undefined>\n```\n\nResolves last value on completion, or `undefined` when source completes empty.\n\n## Async Conversion\n\n### `toAsyncIterable()`\n\n```ts\ntoAsyncIterable<T>(source: Stream<T>, options: AsyncIterableOptions): AsyncIterable<T>\n```\n\nConverts push stream to async iterable with bounded queue.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive queue capacity |\n| `overflow` | `OverflowPolicy` | `error`, `drop-oldest`, or `drop-newest` |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n## Channels\n\n### `createChannel()`\n\n```ts\ncreateChannel<T>(options?: ChannelOptions<T>): Channel<T>\n```\n\nCreates imperative multicast boundary. Disposal completes subscribers.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `initial` | `T` | Optional initial replay value |\n| `replay` | `number` | Non-negative retained value count |\n\n## Adapters\n\n### `@vielzeug/flux/ripple`\n\n```ts\nfromSignal<T>(source: Readable<T>): Stream<T>\ntoSignal<T>(source: Stream<T>, options: ToSignalOptions<T>): SignalBinding<T>\n```\n\n`fromSignal()` emits current value first. `toSignal()` preserves final value then disposes binding when source completes, errors, or supplied signal aborts.\n\n### `@vielzeug/flux/courier`\n\n```ts\nfromQuery<T extends { key: readonly unknown[]; fetch: (...args: never[]) => Promise<unknown> }>(\n cache: { getSnapshot<T>(key: readonly unknown[]): T | null; subscribe(key: readonly unknown[], listener: () => void): () => void },\n definition: T,\n): Stream<AsyncState<Awaited<ReturnType<T['fetch']>>> | null>\nfromSse<T>(source: AsyncIterable<{ data: T; event: string }>, event: string): Stream<T>\n```\n\n`fromQuery()` infers data from `definition.fetch` and emits Courier-compatible `AsyncState` snapshots.\n\n### `@vielzeug/flux/herald`\n\n```ts\nfromBus<T extends EventMap, K extends EventKey<T>>(bus: Bus<T>, event: K): Stream<T[K]>\ntoBus<T extends EventMap, K extends EventKey<T>>(bus: Bus<T>, event: K): Operator<T[K], T[K]>\n```\n\n### `@vielzeug/flux/pulse`\n\n```ts\nfromPulse<T extends MessageMap, K extends EventKey<T>>(pulse: Pulse<T>, event: K): Stream<T[K]>\nfromPresence<T>(presence: PresenceChannel<T>): Stream<ReadonlyMap<string, T>>\n```\n\n## Types\n\n```ts\ntype Teardown = () => void;\n\ntype Subscription = {\n [Symbol.dispose](): void;\n readonly closed: boolean;\n unsubscribe(): void;\n};\n\ntype Observer<T> = {\n complete?: () => void;\n error?: (reason: unknown) => void;\n next: (value: T) => void;\n};\n\ntype SubscribeOptions = { signal?: AbortSignal };\n\ntype Sink<T> = {\n complete(): void;\n error(reason: unknown): void;\n next(value: T): void;\n};\n\ntype Producer<T> = (sink: Sink<T>, signal: AbortSignal) => Teardown | void;\ntype Operator<A = unknown, B = unknown> = (source: Stream<A>) => Stream<B>;\n\ninterface Stream<T> {\n subscribe(observer: Observer<T> | ((value: T) => void), options?: SubscribeOptions): Subscription;\n}\n\ntype OverflowPolicy = 'drop-newest' | 'drop-oldest' | 'error';\ntype AsyncIterableOptions = { capacity: number; overflow: OverflowPolicy; signal?: AbortSignal };\ntype IntervalOptions = { every: number };\ntype TimerOptions = { delay: number; interval?: number };\ntype DebounceOptions = { for: number };\ntype TimeoutOptions = { after: number };\ntype ConcatMapOptions = { capacity: number };\ntype RetryOptions = { attempts: number; delay?: number | ((attempt: number) => number) };\ntype ToArrayOptions = { maxItems: number; signal?: AbortSignal };\ntype ValueOptions = { signal?: AbortSignal };\ntype ChannelOptions<T> = { initial?: T; replay?: number };\n```\n\n## Errors\n\n### `FluxError`\n\nBase Flux error. Use `FluxError.is(reason)` to narrow unknown values.\n\n### `FluxTimeoutError`\n\nRaised by `timeout()`. `ms` contains configured inactivity duration.\n",
@@ -51,33 +51,33 @@
51
51
  "combineLatest": "export { combineLatest, concat, merge } from './operators/combination';",
52
52
  "concat": "export { combineLatest, concat, merge } from './operators/combination';",
53
53
  "merge": "export { combineLatest, concat, merge } from './operators/combination';",
54
+ "IntervalOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
55
+ "TimerOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
54
56
  "from": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
55
57
  "fromEvent": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
56
58
  "interval": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
57
59
  "of": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
58
60
  "timer": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
59
- "IntervalOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
60
- "TimerOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
61
+ "DebounceOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
62
+ "TimeoutOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
61
63
  "debounce": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
62
64
  "take": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
63
65
  "takeUntil": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
64
66
  "timeout": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
65
- "DebounceOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
66
- "TimeoutOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
67
+ "ConcatMapOptions": "export type { ConcatMapOptions } from './operators/transformation';",
67
68
  "concatMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
68
69
  "filter": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
69
70
  "map": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
70
71
  "mergeMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
71
72
  "scan": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
72
73
  "switchMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
73
- "ConcatMapOptions": "export type { ConcatMapOptions } from './operators/transformation';",
74
+ "RetryOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
75
+ "ToArrayOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
76
+ "ValueOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
74
77
  "first": "export { first, last, retry, toArray } from './operators/utility';",
75
78
  "last": "export { first, last, retry, toArray } from './operators/utility';",
76
79
  "retry": "export { first, last, retry, toArray } from './operators/utility';",
77
80
  "toArray": "export { first, last, retry, toArray } from './operators/utility';",
78
- "RetryOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
79
- "ToArrayOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
80
- "ValueOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
81
81
  "pipe": "export { pipe } from './pipe';",
82
82
  "AsyncIterableOptions": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
83
83
  "Observer": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport * from './types';\nexport { createForm } from './form';\nexport { toFormData } from './adapters/form-data';\n",
2
+ "apiSource": "export { toFormData } from './adapters/form-data';\nexport { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport { createForm } from './form';\nexport * from './types';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Forge — Immutable form state for TypeScript\ndescription: Framework-agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form-state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createForm, toFormData, bindField, customValidator, saveForm, loadForm]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"forge\" />\n\n## Why Forge?\n\nNative form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// Before\nconst values = { email: '', password: '' };\nconst errors: Record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'Invalid email';\n errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';\n}\n\n// After\nconst form = createForm({\n initialValues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'Invalid email',\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n },\n }),\n});\n```\n\n| Feature | Forge | Native form state | Framework-owned form state |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"forge\" type=\"size\" /> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Varies |\n| Zero external dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Immutable nested values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Typed object field handles | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Framework-independent state | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Forge when** form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.\n\n**Consider framework-owned form state when** application only needs a single UI framework's native input bindings.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\nInstall `@vielzeug/spell` or `@vielzeug/vault` only when importing Forge's matching optional adapter.\n\n## Quick Start\n\nCreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: JSON.stringify(value),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n });\n\n return response.ok;\n});\n\nif (!result.ok && result.type === 'validation') console.log(result.errors);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `form.value` exposes one immutable nested value tree.\n- `form.field(key)` selects typed object branches without string paths.\n- `field.set(updater)` replaces array values without index handles.\n- `form.validate()` returns valid, invalid, or aborted results.\n- `form.submit(handler)` touches, validates, and invokes the handler when valid.\n- `bindField()` connects one DOM element without owning validation timing.\n- `customValidator()` maps Spell schema errors into Forge fields.\n- `saveForm()` and `loadForm()` persist explicit Vault draft records.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — adapt a Spell schema through `customValidator()`.\n- [Vault](/vault/) — save and restore explicit Forge draft records.\n- [Courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
5
  "api": "---\ntitle: Forge — API Reference\ndescription: Complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createForm()` | Create immutable form state | Sync | `initialValues` cannot contain mutable class instances |\n| `form.field()` | Select a top-level or object child field | Sync | Arrays have no index field handles |\n| `form.validate()` | Validate complete value | Async | Handle `aborted` separately |\n| `form.submit()` | Touch, validate, then invoke handler | Async | Concurrent calls reject |\n| `form.reset()` | Restore or replace baseline | Sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | Observe form metadata | Sync | Throws after disposal |\n| `toFormData()` | Serialize values for multipart transport | Sync | `FileList` is transport-only |\n| `debugForm()` | Log public state transitions | Sync | Import from `/devtools` |\n| `bindField()` | Bind one DOM element | Sync | Does not schedule validation |\n| `customValidator()` | Adapt a Spell schema | Async | Does not transform `form.value` |\n| `saveForm()` / `loadForm()` | Persist explicit Vault records | Async | FormDraftCodec owns record shape |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/forge` | Core form factory, serialization helper, types, and errors |\n| `@vielzeug/forge/devtools` | `debugForm()` |\n| `@vielzeug/forge/dom` | `bindField()` and DOM binding types |\n| `@vielzeug/forge/spell` | `customValidator()` |\n| `@vielzeug/forge/vault` | `saveForm()`, `loadForm()`, and `FormDraftCodec` |\n\n## Core Functions\n\n### `createForm(options)`\n\n```ts\nfunction createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues>;\n```\n\nCreates a form with immutable initial values and an optional full-form validator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.initialValues` | `TValues` | Initial value and reset baseline. Supports primitives, plain objects, arrays, `File`, and `Blob`. |\n| `options.validate` | `FormValidator<TValues>` | Optional validator for the entire current value. |\n| `options.onSubscriberError` | `(error: unknown) => void` | Optional subscriber failure reporter. |\n\n**Returns:** `Form<TValues>`.\n\n**Example:**\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n```\n\n---\n\n### `toFormData(values)`\n\n```ts\nfunction toFormData(values: Record<string, unknown>): FormData;\n```\n\nConverts nested values into `FormData` with dot-separated object keys and repeated array keys.\n\n**Returns:** a populated `FormData` instance.\n\n**Example:**\n\n```ts\nimport { toFormData } from '@vielzeug/forge';\n\nconst body = toFormData({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## Form Handles\n\n### `Form<TValues>`\n\n`createForm()` returns this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<TValues>` | Current immutable value. |\n| `state` | `FormState<TValues>` | Submission, validation, touch, and error metadata. |\n| `field(key)` | `Field<TValues[K]>` | Select a top-level field. |\n| `set(next)` | `void` | Replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | Restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `Promise<ValidationResult<TValues>>` | Run full-form validation. |\n| `submit(handler)` | `Promise<SubmitResult<TResult, TValues>>` | Touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe form state; throws after disposal. |\n| `dispose()` | `void` | Abort validation and clear subscribers. |\n| `disposed` | `boolean` | Whether the form has been disposed. |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal. |\n\n### `Field<V>`\n\n`form.field(key)` and object-field `.field(key)` return this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<V>` | Current immutable branch value. |\n| `error` | `string \\| undefined` | Current field error. |\n| `dirty` | `boolean` | Whether branch differs from baseline. |\n| `touched` | `boolean` | Whether field was touched. |\n| `field(key)` | `Field<V[K]>` | Select child object field only. |\n| `set(next)` | `void` | Replace branch or derive a replacement. |\n| `reset()` | `void` | Restore exact baseline branch. |\n| `touch()` | `void` | Mark field touched. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe field transitions; throws after disposal. |\n\n## Validation Results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n```\n\nRuns the configured validator against the complete value. A newer validation aborts the older run.\n\n**Returns:** `ValidationResult<TValues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formError);\n```\n\n### `form.submit(handler)`\n\n```ts\nfunction submit<TResult>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;\n```\n\nTouches all fields, validates once, and invokes `handler` when validation is valid.\n\n**Returns:** `SubmitResult<TResult, TValues>`. Handler failures reject normally.\n\n```ts\nconst result = await form.submit((value) => Promise.resolve(value));\n```\n\n## Devtools and Adapters\n\n### `debugForm(form, options?)`\n\n```ts\nfunction debugForm<TValues extends Record<string, unknown>>(\n form: Form<TValues>,\n options?: ForgeDevtoolsOptions,\n): Unsubscribe;\n```\n\nLogs public validity, validation, and submission transitions through `console.debug`.\n\n**Example:**\n\n```ts\nimport { debugForm } from '@vielzeug/forge/devtools';\n\nconst stop = debugForm(form, { label: 'checkout' });\nstop();\n```\n\n---\n\n### `bindField(element, field, options)`\n\n```ts\nfunction bindField<Element extends HTMLElement, V>(\n element: Element,\n field: Field<V>,\n options: FieldBindingOptions<Element, V>,\n): Unsubscribe;\n```\n\nBinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**Example:**\n\n```ts\nimport { bindField } from '@vielzeug/forge/dom';\n\nconst stop = bindField(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n---\n\n### `customValidator(schema)`\n\n```ts\nfunction customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues>,\n): FormValidator<TValues>;\n```\n\nAdapts a Spell schema. Every failing union maps its closest branch while preserving unrelated errors. Array item issues map to the parent array field; duplicate paths retain the first message.\n\n**Example:**\n\n```ts\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n---\n\n### `saveForm()` and `loadForm()`\n\n```ts\nfunction saveForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, codec: FormDraftCodec<TValues, S, K>,\n): Promise<void>;\n\nfunction loadForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, key: KeyOf<S, K>, codec: FormDraftCodec<TValues, S, K>,\n): Promise<boolean>;\n```\n\nPersists or restores a codec-defined Vault record. `loadForm()` calls `form.reset()` when the codec decodes a record.\n\n**Returns:** `loadForm()` returns `false` for a missing or rejected record.\n\n## Types\n\n```ts\ntype Unsubscribe = () => void;\ntype MaybePromise<T> = T | PromiseLike<T>;\ntype ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;\n\ntype FormErrors<T> = T extends readonly unknown[]\n ? string\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;\n\ntype ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;\n\ntype FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>, signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;\n\ntype FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<TValues>;\n}>;\n\ntype SubscribeOptions = Readonly<{ immediate?: boolean }>;\n\ntype FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;\n\ntype FormState<TValues extends Record<string, unknown> = Record<string, unknown>> = Readonly<{\n error: string | undefined;\n errors: FormErrors<TValues> | undefined;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n valid: boolean;\n validating: boolean;\n}>;\n\ntype ValidationResult<TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;\n\ntype SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ ok: true; value: TResult }>\n | Readonly<{ ok: false; type: 'aborted' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; ok: false; type: 'validation' }>;\n```\n\n```ts\ntype Field<V> = {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]>;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n};\n\ntype Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly state: FormState<TValues>;\n readonly value: ReadonlyDeep<TValues>;\n dispose(): void;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n};\n\ntype ForgeDevtoolsOptions = Readonly<{ label?: string }>;\n\ntype FieldBindingOptions<Element extends HTMLElement, V> = Readonly<{\n event?: keyof HTMLElementEventMap;\n read(element: Element): V;\n write?: (element: Element, value: ReadonlyDeep<V>) => void;\n}>;\n\ntype FormDraftCodec<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string> = Readonly<{\n fromRecord(record: RecordOf<S, K>): TValues | undefined;\n toRecord(values: ReadonlyDeep<TValues>): RecordOf<S, K>;\n}>;\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `ForgeError` | Base Forge error | `ForgeError.is(error)` narrows unknown values. |\n| `ForgeConfigError` | Unsafe key or unsupported form value | Extends `ForgeError`. |\n| `ForgeDisposedError` | Operation or subscription after disposal | Message names the attempted operation. |\n| `ForgeSubmitError` | Concurrent `submit()` call | Extends `ForgeError`. |\n| `ForgeValidationError` | Validator throws unexpectedly | Preserves original error as `cause`. |\n",
@@ -59,13 +59,13 @@
59
59
  }
60
60
  ],
61
61
  "typeSignatures": {
62
+ "toFormData": "export { toFormData } from './adapters/form-data';",
62
63
  "ForgeConfigError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
63
64
  "ForgeDisposedError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
64
65
  "ForgeError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
65
66
  "ForgeSubmitError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
66
67
  "ForgeValidationError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
67
68
  "createForm": "export { createForm } from './form';",
68
- "toFormData": "export { toFormData } from './adapters/form-data';",
69
69
  "Unsubscribe": "export type Unsubscribe = () => void;",
70
70
  "MaybePromise": "export type MaybePromise<T> = T | PromiseLike<T>;",
71
71
  "ReadonlyDeep": "export type ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;",