@vielzeug/codex 26.10.0 → 26.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/data/catalog.json +53 -59
- package/data/llms-full.txt +1322 -1069
- package/data/llms.txt +6 -6
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +13 -13
- package/data/packages/assay.json +4 -4
- package/data/packages/clockwork.json +4 -4
- package/data/packages/codex.json +3 -3
- package/data/packages/coins.json +4 -4
- package/data/packages/conduit.json +4 -4
- package/data/packages/courier.json +4 -4
- package/data/packages/dnd.json +10 -10
- package/data/packages/familiar.json +4 -4
- package/data/packages/flux.json +4 -4
- package/data/packages/focus.json +4 -4
- package/data/packages/forge.json +4 -4
- package/data/packages/gesture.json +4 -4
- package/data/packages/herald.json +12 -12
- package/data/packages/illusionist.json +5 -5
- package/data/packages/keymap.json +9 -9
- package/data/packages/ledger.json +4 -4
- package/data/packages/lingua.json +4 -4
- package/data/packages/mesh.json +5 -5
- package/data/packages/necromancer.json +5 -5
- package/data/packages/orbit.json +7 -7
- package/data/packages/ore.json +5 -5
- package/data/packages/postmaster.json +4 -4
- package/data/packages/prism.json +50 -42
- package/data/packages/pulse.json +8 -8
- package/data/packages/refine.json +3 -3
- package/data/packages/ripple.json +4 -4
- package/data/packages/rune.json +9 -9
- package/data/packages/sandbox.json +4 -4
- package/data/packages/scout.json +7 -7
- package/data/packages/scroll.json +10 -10
- package/data/packages/sentinel.json +4 -4
- package/data/packages/sigil.json +6 -6
- package/data/packages/sourcerer.json +4 -4
- package/data/packages/spell.json +9 -9
- package/data/packages/tandem.json +5 -5
- package/data/packages/tavern.json +12 -12
- package/data/packages/tempo.json +4 -4
- package/data/packages/vault.json +13 -13
- package/data/packages/ward.json +9 -9
- package/data/packages/wayfinder.json +17 -17
- package/data/refine.json +259 -87
- package/data/search.json +246 -246
- package/dist/http.js +2 -2
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/refine.d.ts +1 -1
- package/dist/refine.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/refine.js +1 -1
- package/package.json +2 -2
- package/skills/vielzeug/SKILL.md +57 -57
package/data/packages/codex.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "/**\n * `@vielzeug/codex
|
|
2
|
+
"apiSource": "/**\n * `@vielzeug/codex`: supported public contract.\n *\n * Exposes the generic catalog, snapshot loader, errors, and server hosts.\n * Refine catalog types and tools live behind `@vielzeug/codex/refine`.\n */\nexport { StdioServerTransport } from '@modelcontextprotocol/server/stdio';\nexport { type Catalog, CatalogError, type SearchHit, SnapshotCatalog } from './catalog.js';\nexport { CodexError } from './errors.js';\nexport { type HttpHost, type HttpHostOptions, startHttpHost } from './http.js';\nexport { createMcpServer } from './server.js';\nexport { type LoadedSnapshot, loadSnapshot } from './snapshot.js';\nexport type {\n DocPage,\n Example,\n PackageContent,\n PackageMeta,\n SnapshotManifest,\n} from './types.js';\n",
|
|
3
3
|
"docs": {
|
|
4
4
|
"index": "---\ntitle: Codex\ndescription: Local MCP access to Vielzeug documentation and package metadata.\npackage: codex\ncategory: AI\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports: [loadSnapshot, SnapshotCatalog, createMcpServer, startHttpHost, StdioServerTransport, CatalogError, CodexError]\nenvironments: [node]\n---\n\n<PackageHero package=\"codex\" />\n\n## Why Codex?\n\nCodex exposes current Vielzeug catalog data through MCP without scanning source at request time.\n\n## Installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## Quick Start\n\n```sh\nnpx -y @vielzeug/codex\n```\n\n## Features\n\n- `loadSnapshot` validates chunked snapshot metadata.\n- `SnapshotCatalog` loads package content only when requested.\n- `createMcpServer` registers generic package tools; `/refine` adds component tools explicitly.\n- Root and `/refine` entry points publish TypeScript declarations.\n- `startHttpHost` enforces loopback binding plus localhost Host and Origin validation.\n\n## Documentation\n\n- [Usage](./usage.md)\n- [API](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n## See Also\n\n- [Refine](../refine/) provides component metadata bundled by Codex.\n",
|
|
5
5
|
"api": "---\ntitle: Codex API\ndescription: Snapshot, catalog, MCP server, and local HTTP host APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `loadSnapshot` | Read validated snapshot metadata | Sync | Content chunks load lazily |\n| `SnapshotCatalog` | Query package corpus | Sync | Construct from loaded snapshot |\n| `createMcpServer` | MCP adapter factory (generic tools) | Sync | Requires catalog and version |\n| `startHttpHost` | Loopback Streamable HTTP host | Async | HTTP remains local-only |\n| `registerRefineTools` | Opt-in Refine tool registration | Sync | Import from `@vielzeug/codex/refine` |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/codex` | `Catalog`, `SnapshotCatalog`, `loadSnapshot`, MCP server, and HTTP host |\n| `@vielzeug/codex/refine` | Opt-in Refine component tools (`registerRefineTools`) |\n\n## Snapshot\n\n### `loadSnapshot`\n\n```ts\nloadSnapshot(snapshotRoot?: string, options?: { validateContents?: boolean }): LoadedSnapshot;\n```\n\nLoads catalog/search metadata and Refine metadata. `snapshotRoot` defaults to bundled `data/`. Package content chunks stay lazy unless `validateContents: true`, which validates every package content chunk during loading.\n\n### `SnapshotCatalog`\n\n```ts\nnew SnapshotCatalog(snapshot: LoadedSnapshot)\n```\n\nProvides generic package lookup, docs/source/example/signature access, and deterministic search. Use `SnapshotRefineCatalog` from `/refine` when component methods are needed.\n\n---\n\n## MCP\n\n### `createMcpServer`\n\n```ts\ncreateMcpServer(catalog: Catalog, options: { version: string; debug?: boolean }): Server;\n```\n\nRegisters generic package tools over `Catalog`. Refine tools are opt-in through `/refine`. The root also re-exports `StdioServerTransport` for connecting the server without a second package import.\n\n## Refine tools (`@vielzeug/codex/refine`)\n\n### `SnapshotRefineCatalog`\n\n```ts\nnew SnapshotRefineCatalog(snapshot: LoadedSnapshot)\n```\n\nExtends `SnapshotCatalog` with `getComponent()` and `listComponents()`.\n\n### `registerRefineTools`\n\n```ts\nregisterRefineTools(server: Server, catalog: RefineCatalog, debug?: boolean): void;\n```\n\nUpgrades a server created by `createMcpServer()` with the combined generic and `refine-*` tool set. `refineTools` is exported as a readonly registry.\n\n## HTTP\n\n### `startHttpHost`\n\n```ts\nstartHttpHost(options: HttpHostOptions): Promise<HttpHost>;\n```\n\nStarts Streamable HTTP on `127.0.0.1` by default. Runtime validation accepts only `127.0.0.1` or `::1`; every request must also pass localhost Host and Origin checks. `configureServer` runs once for each lazily created MCP request server and is not called during host startup.\n\n## Types\n\n```ts\ninterface SnapshotPointer {\n directory: string;\n}\n\ninterface SnapshotManifest {\n catalog: 'catalog.json';\n contentDirectory: 'packages';\n refine: 'refine.json' | null;\n schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;\n search: 'search.json';\n version: string;\n}\n```\n\nDev snapshots use `SnapshotPointer` (via `current.json`); published snapshots are static directories.\n\n```ts\ninterface LoadedSnapshot {\n catalog: CatalogFile;\n contentDirectory: string;\n manifest: SnapshotManifest;\n refineComponents: CemDeclaration[];\n search: SearchRecord[];\n}\n```\n\n```ts\ninterface CatalogFile {\n packages: PackageMeta[];\n version: string;\n}\n\ninterface SearchRecord {\n category: string;\n description: string;\n docs: Partial<Record<DocPage, string>>;\n examples: Array<{ id: string; text: string }>;\n exports: string;\n keywords: string;\n name: string;\n related: string;\n slug: string;\n source: string | null;\n}\n\ninterface PackageMeta {\n availableDocPages: DocPage[];\n category: string;\n description: string;\n exampleIds: string[];\n exports: string[];\n hasSource: boolean;\n keywords: string[];\n name: string;\n related: string[];\n slug: string;\n version: string;\n}\n\ninterface PackageContent {\n apiSource: string | null;\n docs: Partial<Record<DocPage, string>>;\n examples: Example[];\n typeSignatures: Record<string, string>;\n}\n\ninterface Example {\n code: string;\n id: string;\n name: string;\n}\n```\n\n```ts\ninterface Catalog {\n getContent(slug: string): PackageContent;\n getDocs(slug: string, page: DocPage): string;\n getExample(slug: string, exampleId: string): Example;\n getPackage(slug: string): PackageMeta;\n getSource(slug: string): string;\n getTypeSignature(slug: string, symbol: string): string;\n listExamples(slug: string): Array<Pick<Example, 'id' | 'name'>>;\n listPackages(): PackageMeta[];\n search(query: string): SearchHit[];\n}\n\ninterface RefineCatalog extends Catalog {\n getComponent(tagName: string): CemDeclaration;\n listComponents(): CemDeclaration[];\n}\n\ninterface SearchHit {\n matchedExamples?: string[];\n matchedIn: Array<'docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source'>;\n matchedPages?: DocPage[];\n name: string;\n slug: string;\n}\n```\n\n```ts\ninterface HttpHost {\n readonly disposalSignal: AbortSignal;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly host: string;\n readonly port: number;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\ninterface HttpHostOptions {\n catalog: Catalog;\n configureServer?: (server: Server) => void;\n debug?: boolean;\n host?: '127.0.0.1' | '::1';\n port: number;\n version: string;\n}\n```\n\n```ts\nconst DOC_PAGES = ['index', 'api', 'usage', 'examples'] as const;\ntype DocPage = (typeof DOC_PAGES)[number];\n\nconst SNAPSHOT_SCHEMA_VERSION = 1 as const;\n```\n\nCEM types and `RefineCatalog` are exported from `/refine`.\n\n```ts\ninterface CemDeclaration {\n attributes?: CemAttribute[];\n cssParts?: CemCssPart[];\n cssProperties?: CemCssProperty[];\n description?: string;\n events?: CemEvent[];\n members?: CemMember[];\n name?: string;\n slots?: CemSlot[];\n superclass?: { name: string; package?: string };\n tagName?: string;\n [key: string]: unknown;\n}\n\ninterface CemAttribute {\n default?: string;\n description?: string;\n fieldName?: string;\n name: string;\n type?: { text: string };\n}\n\ninterface CemCssPart {\n description?: string;\n name: string;\n}\n\ninterface CemCssProperty {\n default?: string;\n description?: string;\n name: string;\n}\n\ninterface CemEvent {\n description?: string;\n name: string;\n type?: { text: string };\n}\n\ninterface CemMember {\n description?: string;\n kind?: 'field' | 'method';\n name: string;\n type?: { text: string };\n}\n\ninterface CemSlot {\n description?: string;\n name: string;\n}\n```\n\n## Errors\n\n`CodexError` signals malformed snapshots or host failures. `CatalogError` adds `INVALID_ARG`, `NOT_FOUND`, or `UNAVAILABLE` for expected tool failures.\n",
|
|
6
|
-
"usage": "---\ntitle: Codex
|
|
7
|
-
"examples": "---\ntitle: Codex
|
|
6
|
+
"usage": "---\ntitle: 'Codex: Usage Guide'\ndescription: Install, connect, develop, and debug the Vielzeug MCP server.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRun local stdio server:\n\n```sh\nnpx -y @vielzeug/codex\n```\n\nUse shipped `mcp-setup.json` for machine-readable generic configuration. Client-specific configuration must use its documented MCP format.\n\n## HTTP Mode\n\nHTTP uses Streamable HTTP and binds loopback only:\n\n```sh\nnpx -y @vielzeug/codex --port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nResponse includes snapshot version. Runtime bind validation and Host/Origin allowlists restrict access to localhost; no remote host mode exists. Programmatic `configureServer` hooks run once per MCP request server, not at host startup. Dispose the host asynchronously and observe `disposed` or `disposalSignal` when coordinating shutdown.\n\n## Agent Skill\n\nCodex ships a `vielzeug` agent skill: a `SKILL.md` that tells an AI agent working in *your* project to discover packages through the MCP tools instead of guessing, to use the owning `@vielzeug/*` package instead of hand-rolling, and to build UI from Refine components, tokens, and theme whenever `@vielzeug/refine` is installed. Install it into the project:\n\n```sh\nnpx -y @vielzeug/codex skills install\nnpx -y @vielzeug/codex skills install --target=.claude/skills\nnpx -y @vielzeug/codex skills install --force # overwrite an existing copy\n```\n\nWithout `--target`, the first existing directory among `.agents/skills`, `.github/skills`, and `.claude/skills` is used; when none exists, `.agents/skills` is created. The command refuses to overwrite an installed skill unless `--force` is passed. Rerun after upgrading codex to pick up the current package list, which is generated from the published manifests. The skill pairs with the MCP server: its lookups (`get-docs`, `refine-get-component`, `refine-validate-usage`, …) assume codex is connected, and it documents `node_modules` fallbacks for when it is not.\n\n## Local Development\n\nRequires Node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## Debugging\n\n```sh\npnpm dev\nnode src/cli.ts --port=3100 --debug\ncurl http://127.0.0.1:3100/health\n```\n\n`--debug` logs tool durations and expected catalog errors to stderr. Build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## Programmatic Usage\n\n```ts\nimport { SnapshotCatalog, StdioServerTransport, createMcpServer, loadSnapshot } from '@vielzeug/codex';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotCatalog(snapshot);\nawait createMcpServer(catalog, { version: snapshot.manifest.version }).connect(new StdioServerTransport());\n```\n\nTo include Refine component tools, upgrade the server with the `@vielzeug/codex/refine` subpath:\n\n```ts\nimport { StdioServerTransport, createMcpServer, loadSnapshot } from '@vielzeug/codex';\nimport { SnapshotRefineCatalog, registerRefineTools } from '@vielzeug/codex/refine';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotRefineCatalog(snapshot);\nconst server = createMcpServer(catalog, { version: snapshot.manifest.version });\nregisterRefineTools(server, catalog);\nawait server.connect(new StdioServerTransport());\n```\n\n## Best Practices\n\n- Use `search-packages` for capability discovery before loading broad source.\n- Use `get-type-signature` before loading full source.\n- Published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n- Pass `validateContents: true` to `loadSnapshot()` for artifact verification; normal startup keeps package chunks lazy.\n- Treat `configureServer` as a per-request factory hook and avoid process-global side effects.\n- Keep HTTP local. Use stdio for normal client integration.\n- Run `pnpm test:unit` before `pnpm test:integration`.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Codex: Examples'\ndescription: Practical MCP tool-call examples for package discovery, docs lookup, and Refine component queries.\n---\n\n## Examples\n\n- [Listing Packages](./examples/listing-packages.md)\n- [Searching Packages](./examples/searching-packages.md)\n- [Package Metadata](./examples/package-metadata.md)\n- [Reading Docs](./examples/reading-docs.md)\n- [Running REPL Examples](./examples/running-repl-examples.md)\n- [Looking Up Components](./examples/looking-up-components.md)\n- [Inspector](./examples/inspector.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [],
|
|
10
10
|
"typeSignatures": {
|
package/data/packages/coins.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { allocate, sum } from './aggregate';\nexport { BHD, currency, EUR, GBP, isCurrency, JPY, KRW, KWD, USD } from './currency';\nexport type { CoinsErrorCode } from './errors';\nexport { CoinsError, CurrencyMismatchError, InvalidCurrencyError } from './errors';\nexport { exchange, exchangeRate, isExchangeRate } from './exchange';\nexport { format, formatParts } from './format';\nexport {\n abs,\n add,\n clamp,\n compare,\n decodeMoney,\n divide,\n isMoney,\n money,\n multiply,\n negate,\n round,\n subtract,\n toDecimal,\n} from './money';\nexport { toJSON } from './serialization';\nexport type {\n Currency,\n CurrencyCode,\n Decimal,\n ExchangeRate,\n FormatOptions,\n Money,\n MoneyFormatPart,\n MoneyJSON,\n RoundingMode,\n} from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Coins
|
|
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 built-in or construct custom | Sync | Unknown codes throw |\n| `add` / `subtract` | Combine matching currencies | Sync | Mismatches throw |\n| `multiply` / `divide` | Exact decimal scaling | Sync | Use decimal strings |\n| `sum` | Aggregate with inferred currency | Sync | Empty iterable requires `{ currency }` |\n| `allocate` | Split without losing minor units | Sync | Weights must be non-negative |\n| `clamp` | Bound to min/max range | Sync | Min must not exceed max |\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` / `decodeMoney` | Cross serialization or realm 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(code)` / `currency({ code, minorUnit })`\n\n```ts\nfunction currency<C extends string>(code: C): Currency<C>;\nfunction currency<C extends string>(definition: { code: C; minorUnit: number }): Currency<C>;\n```\n\nResolves one of the seven built-ins—`USD`, `EUR`, `GBP`, `JPY`, `KRW`, `BHD`, or `KWD`—or constructs an immutable custom currency. Custom currencies are local values with no global registry. Code must be exactly three uppercase letters; `minorUnit` must be an integer from 0 to 6. Arithmetic compares currency definitions by reference.\n\n```ts\ncurrency('USD');\ncurrency({ code: 'PTS', minorUnit: 0 });\n```\n\n### `isCurrency(value)`\n\n```ts\nfunction isCurrency(value: unknown): value is Currency;\n```\n\nType guard for canonical currency values (built-in or constructed via `currency()`).\n\n### `money(amount, currency, options?)`\n\n```ts\nfunction money<C extends Currency>(amount: string, currency: C, options?: { rounding?: RoundingMode }): Money<C>;\nfunction money<C extends Currency>(amount: bigint, currency: C, options: { unit: 'minor' }): Money<C>;\n```\n\n```ts\nmoney('19.99', USD);\nmoney(1999n, USD, { unit: 'minor' });\n```\n\nDecimal strings that exceed the currency's precision require a `rounding` mode. Inputs use plain base-10 syntax without exponent, leading plus, or whitespace; input length is capped at 1,000 characters and normalized fractional precision at 100 places. Bigint amounts require `{ unit: 'minor' }`.\n\n### `decodeMoney(value, options?)`\n\n```ts\nfunction decodeMoney(value: unknown, options?: { currency?: (code: string) => Currency }): Money;\n```\n\nValidates untrusted or cross-realm data and returns canonical money. Plain bigint input must have exactly `amount` and canonical `currency`; JSON must have exactly `amount`, currency code, and `unit: 'minor'`. Serialized amounts are bounded to 1,000 characters. A custom resolver must return a canonical currency whose code matches the envelope.\n\n### `isMoney(value)`\n\n```ts\nfunction isMoney(value: unknown): value is Money;\n```\n\nType guard for canonical Coins money values. Checks identity against the internal canonical set — forged frozen objects do not pass.\n\n## Arithmetic\n\n```ts\nfunction add<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): Money<C>;\nfunction subtract<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): Money<C>;\nfunction multiply<C extends Currency>(value: Money<C>, factor: string, options?: { rounding?: RoundingMode }): Money<C>;\nfunction divide<C extends Currency>(value: Money<C>, divisor: string, options?: { rounding?: RoundingMode }): Money<C>;\nfunction compare<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): -1 | 0 | 1;\nfunction clamp<C extends Currency>(\n value: Money<C>,\n options: { max: Money<NoInfer<C>>; min: Money<NoInfer<C>> },\n): Money<C>;\nfunction abs<C extends Currency>(value: Money<C>): Money<C>;\nfunction negate<C extends Currency>(value: Money<C>): Money<C>;\nfunction round<C extends Currency>(\n value: Money<C>,\n options: { fractionDigits: number; rounding?: RoundingMode },\n): Money<C>;\nfunction toDecimal(value: Money): string;\n```\n\n`factor` and `divisor` are plain decimal strings; exponent notation, whitespace, and a leading plus are invalid. Matching currency identity is required for binary operations. `round()` requires integer `fractionDigits` from 0 through the currency's `minorUnit`. Scaling and rounding default to `halfAwayFromZero`.\n\n## Aggregation\n\n```ts\nfunction sum<C extends Currency>(values: readonly Money<C>[]): Money<C>;\nfunction sum<C extends Currency>(values: Iterable<Money<C>>, options: { currency: C }): Money<C>;\nfunction allocate<C extends Currency>(value: Money<C>, count: number): Money<C>[];\nfunction allocate<C extends Currency>(value: Money<C>, weights: readonly string[]): Money<C>[];\n```\n\n`sum()` infers currency from non-empty values. An empty iterable requires a canonical `{ currency }` and returns canonical zero. `allocate()` accepts a positive integer count or dense, non-empty, non-negative decimal-string weights with a positive total. Either form is limited to 100,000 output parts. It distributes remainders by largest fractional share, breaks ties by input order, and preserves the exact signed minor-unit total.\n\n## Exchange\n\n```ts\nfunction exchangeRate<From extends Currency, To extends Currency>({\n from,\n to,\n value,\n}: {\n from: From;\n to: To;\n value: string;\n}): ExchangeRate<From, To>;\n\nfunction exchange<From extends Currency, To extends Currency>(\n value: Money<From>,\n rate: ExchangeRate<From, To>,\n options?: { rounding?: RoundingMode },\n): Money<To>;\n```\n\n```ts\nconst rate = exchangeRate({ from: USD, to: EUR, value: '0.9234' });\nexchange(money('100.00', USD), rate);\n```\n\nRates are exact positive decimal strings. The money currency must be the same canonical instance as `rate.from`. Conversion defaults to `halfAwayFromZero` rounding.\n\n### `isExchangeRate(value)`\n\n```ts\nfunction isExchangeRate(value: unknown): value is ExchangeRate;\n```\n\nType guard for canonical exchange rates created by `exchangeRate()`. Checks identity against the internal canonical set — forged frozen objects do not pass.\n\n## Formatting\n\n```ts\nfunction format(value: Money, options?: FormatOptions): string;\nfunction formatParts(value: Money, options?: FormatOptions): MoneyFormatPart[];\n```\n\nDefaults are locale `en-US`, style `symbol`, and the currency's minor-unit digits. Either fraction bound may be supplied independently; the omitted bound adjusts to remain compatible. Explicit bounds must satisfy `0 ≤ minimum ≤ maximum ≤ 20`. Exact decimal strings and mapped rounding modes are passed to `Intl.NumberFormat`, preserving locale digits, currency-name pluralization, and rounded negative signs. `formatParts()` keeps locale grouping inside its `integer` part.\n\n## Serialization\n\n```ts\nfunction toJSON(value: Money): MoneyJSON;\nfunction decodeMoney(value: unknown, options?: { currency?: (code: string) => Currency }): Money;\n```\n\n`toJSON()` produces exactly `{ amount, currency, unit: 'minor' }`. `amount` is a canonical integer string of at most 1,000 characters: no decimal point, exponent, leading plus, leading zero, whitespace, or negative zero. `decodeMoney()` resolves built-ins with `currency()` by default; custom codes require a resolver returning the same encoded code.\n\n## Types\n\n```ts\ntype CurrencyCode<C extends string = string> = C & { readonly [currencyBrand]: C };\n\ntype Currency<C extends string = string> = Readonly<{\n code: CurrencyCode<C>;\n minorUnit: number;\n}>;\n\ntype Decimal = Readonly<{\n readonly [decimalBrand]: true;\n denominator: bigint;\n numerator: bigint;\n}>;\n\ntype Money<C extends Currency = Currency> = Readonly<{\n amount: bigint;\n currency: C;\n readonly [moneyBrand]: C;\n}>;\n\ntype ExchangeRate<From extends Currency = Currency, To extends Currency = Currency> = Readonly<{\n from: From;\n to: To;\n value: Decimal;\n}>;\n\ntype FormatOptions = Readonly<{\n locale?: string;\n maximumFractionDigits?: number;\n minimumFractionDigits?: number;\n rounding?: RoundingMode;\n style?: 'code' | 'name' | 'narrowSymbol' | 'symbol';\n}>;\n\ntype MoneyFormatPart = Readonly<{\n type: 'currency' | 'decimal' | 'fraction' | 'integer' | 'literal' | 'minusSign' | 'plusSign';\n value: string;\n}>;\n\ntype MoneyJSON = Readonly<{\n amount: string;\n currency: string;\n unit: 'minor';\n}>;\n\ntype RoundingMode = 'awayFromZero' | 'ceil' | 'floor' | 'halfAwayFromZero' | 'halfEven' | 'towardZero';\n```\n\n`CurrencyCode`, `Decimal`, and `Money` carry phantom brand symbols that prevent unbranded values from being assigned where canonical values are required. `ExchangeRate.value` exposes a normalized exact `Decimal` numerator and denominator.\n\n| Rounding mode | Behavior |\n| --- | --- |\n| `awayFromZero` | Always increase a non-exact magnitude |\n| `ceil` | Toward positive infinity |\n| `floor` | Toward negative infinity |\n| `halfAwayFromZero` | Nearest; midpoint away from zero |\n| `halfEven` | Nearest; midpoint to an even retained unit |\n| `towardZero` | Truncate toward zero |\n\n## Errors\n\n```ts\ntype CoinsErrorCode =\n | 'CURRENCY_MISMATCH'\n | 'DIVISION_BY_ZERO'\n | 'FORMAT_ERROR'\n | 'INVALID_ALLOCATION'\n | 'INVALID_CURRENCY'\n | 'INVALID_DECIMAL'\n | 'INVALID_EXCHANGE_RATE'\n | 'INVALID_MONEY'\n | 'INVALID_RANGE'\n | 'INVALID_ROUNDING';\n```\n\nEvery Coins failure extends `CoinsError` and exposes `code`.\n\n```ts\nclass CoinsError extends Error {\n readonly code: CoinsErrorCode;\n}\n\nclass CurrencyMismatchError extends CoinsError {\n readonly expected: string;\n readonly received: string;\n}\n\nclass InvalidCurrencyError extends CoinsError {\n readonly value: unknown;\n}\n```\n\n`CurrencyMismatchError` and `InvalidCurrencyError` are specialized `CoinsError` subclasses. Use `instanceof CoinsError` to narrow any value to the Coins error hierarchy.\n",
|
|
6
|
-
"usage": "---\ntitle: Coins
|
|
7
|
-
"examples": "---\ntitle: Coins
|
|
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: [USD, EUR, GBP, JPY, KRW, BHD, KWD, money, decodeMoney, isMoney, currency, isCurrency, add, subtract, multiply, divide, compare, clamp, abs, negate, round, toDecimal, sum, allocate, exchangeRate, isExchangeRate, exchange, format, formatParts, toJSON]\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`**: resolve seven canonical built-ins, or create a local custom definition\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- **`decodeMoney`**: validate and re-canonicalize persisted, transported, or cross-realm 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
|
+
"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 built-in or construct custom | Sync | Unknown codes throw |\n| `add` / `subtract` | Combine matching currencies | Sync | Mismatches throw |\n| `multiply` / `divide` | Exact decimal scaling | Sync | Use decimal strings |\n| `sum` | Aggregate with inferred currency | Sync | Empty iterable requires `{ currency }` |\n| `allocate` | Split without losing minor units | Sync | Weights must be non-negative |\n| `clamp` | Bound to min/max range | Sync | Min must not exceed max |\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` / `decodeMoney` | Cross serialization or realm 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(code)` / `currency({ code, minorUnit })`\n\n```ts\nfunction currency<C extends string>(code: C): Currency<C>;\nfunction currency<C extends string>(definition: { code: C; minorUnit: number }): Currency<C>;\n```\n\nResolves one of the seven built-ins (`USD`, `EUR`, `GBP`, `JPY`, `KRW`, `BHD`, or `KWD`) or constructs an immutable custom currency. Custom currencies are local values with no global registry. Code must be exactly three uppercase letters; `minorUnit` must be an integer from 0 to 6. Arithmetic compares currency definitions by reference.\n\n```ts\ncurrency('USD');\ncurrency({ code: 'PTS', minorUnit: 0 });\n```\n\n### `isCurrency(value)`\n\n```ts\nfunction isCurrency(value: unknown): value is Currency;\n```\n\nType guard for canonical currency values (built-in or constructed via `currency()`).\n\n### `money(amount, currency, options?)`\n\n```ts\nfunction money<C extends Currency>(amount: string, currency: C, options?: { rounding?: RoundingMode }): Money<C>;\nfunction money<C extends Currency>(amount: bigint, currency: C, options: { unit: 'minor' }): Money<C>;\n```\n\n```ts\nmoney('19.99', USD);\nmoney(1999n, USD, { unit: 'minor' });\n```\n\nDecimal strings that exceed the currency's precision require a `rounding` mode. Inputs use plain base-10 syntax without exponent, leading plus, or whitespace; input length is capped at 1,000 characters and normalized fractional precision at 100 places. Bigint amounts require `{ unit: 'minor' }`.\n\n### `decodeMoney(value, options?)`\n\n```ts\nfunction decodeMoney(value: unknown, options?: { currency?: (code: string) => Currency }): Money;\n```\n\nValidates untrusted or cross-realm data and returns canonical money. Plain bigint input must have exactly `amount` and canonical `currency`; JSON must have exactly `amount`, currency code, and `unit: 'minor'`. Serialized amounts are bounded to 1,000 characters. A custom resolver must return a canonical currency whose code matches the envelope.\n\n### `isMoney(value)`\n\n```ts\nfunction isMoney(value: unknown): value is Money;\n```\n\nType guard for canonical Coins money values. Checks identity against the internal canonical set: forged frozen objects do not pass.\n\n## Arithmetic\n\n```ts\nfunction add<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): Money<C>;\nfunction subtract<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): Money<C>;\nfunction multiply<C extends Currency>(value: Money<C>, factor: string, options?: { rounding?: RoundingMode }): Money<C>;\nfunction divide<C extends Currency>(value: Money<C>, divisor: string, options?: { rounding?: RoundingMode }): Money<C>;\nfunction compare<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): -1 | 0 | 1;\nfunction clamp<C extends Currency>(\n value: Money<C>,\n options: { max: Money<NoInfer<C>>; min: Money<NoInfer<C>> },\n): Money<C>;\nfunction abs<C extends Currency>(value: Money<C>): Money<C>;\nfunction negate<C extends Currency>(value: Money<C>): Money<C>;\nfunction round<C extends Currency>(\n value: Money<C>,\n options: { fractionDigits: number; rounding?: RoundingMode },\n): Money<C>;\nfunction toDecimal(value: Money): string;\n```\n\n`factor` and `divisor` are plain decimal strings; exponent notation, whitespace, and a leading plus are invalid. Matching currency identity is required for binary operations. `round()` requires integer `fractionDigits` from 0 through the currency's `minorUnit`. Scaling and rounding default to `halfAwayFromZero`.\n\n## Aggregation\n\n```ts\nfunction sum<C extends Currency>(values: readonly Money<C>[]): Money<C>;\nfunction sum<C extends Currency>(values: Iterable<Money<C>>, options: { currency: C }): Money<C>;\nfunction allocate<C extends Currency>(value: Money<C>, count: number): Money<C>[];\nfunction allocate<C extends Currency>(value: Money<C>, weights: readonly string[]): Money<C>[];\n```\n\n`sum()` infers currency from non-empty values. An empty iterable requires a canonical `{ currency }` and returns canonical zero. `allocate()` accepts a positive integer count or dense, non-empty, non-negative decimal-string weights with a positive total. Either form is limited to 100,000 output parts. It distributes remainders by largest fractional share, breaks ties by input order, and preserves the exact signed minor-unit total.\n\n## Exchange\n\n```ts\nfunction exchangeRate<From extends Currency, To extends Currency>({\n from,\n to,\n value,\n}: {\n from: From;\n to: To;\n value: string;\n}): ExchangeRate<From, To>;\n\nfunction exchange<From extends Currency, To extends Currency>(\n value: Money<From>,\n rate: ExchangeRate<From, To>,\n options?: { rounding?: RoundingMode },\n): Money<To>;\n```\n\n```ts\nconst rate = exchangeRate({ from: USD, to: EUR, value: '0.9234' });\nexchange(money('100.00', USD), rate);\n```\n\nRates are exact positive decimal strings. The money currency must be the same canonical instance as `rate.from`. Conversion defaults to `halfAwayFromZero` rounding.\n\n### `isExchangeRate(value)`\n\n```ts\nfunction isExchangeRate(value: unknown): value is ExchangeRate;\n```\n\nType guard for canonical exchange rates created by `exchangeRate()`. Checks identity against the internal canonical set: forged frozen objects do not pass.\n\n## Formatting\n\n```ts\nfunction format(value: Money, options?: FormatOptions): string;\nfunction formatParts(value: Money, options?: FormatOptions): MoneyFormatPart[];\n```\n\nDefaults are locale `en-US`, style `symbol`, and the currency's minor-unit digits. Either fraction bound may be supplied independently; the omitted bound adjusts to remain compatible. Explicit bounds must satisfy `0 ≤ minimum ≤ maximum ≤ 20`. Exact decimal strings and mapped rounding modes are passed to `Intl.NumberFormat`, preserving locale digits, currency-name pluralization, and rounded negative signs. `formatParts()` keeps locale grouping inside its `integer` part.\n\n## Serialization\n\n```ts\nfunction toJSON(value: Money): MoneyJSON;\nfunction decodeMoney(value: unknown, options?: { currency?: (code: string) => Currency }): Money;\n```\n\n`toJSON()` produces exactly `{ amount, currency, unit: 'minor' }`. `amount` is a canonical integer string of at most 1,000 characters: no decimal point, exponent, leading plus, leading zero, whitespace, or negative zero. `decodeMoney()` resolves built-ins with `currency()` by default; custom codes require a resolver returning the same encoded code.\n\n## Types\n\n```ts\ntype CurrencyCode<C extends string = string> = C & { readonly [currencyBrand]: C };\n\ntype Currency<C extends string = string> = Readonly<{\n code: CurrencyCode<C>;\n minorUnit: number;\n}>;\n\ntype Decimal = Readonly<{\n readonly [decimalBrand]: true;\n denominator: bigint;\n numerator: bigint;\n}>;\n\ntype Money<C extends Currency = Currency> = Readonly<{\n amount: bigint;\n currency: C;\n readonly [moneyBrand]: C;\n}>;\n\ntype ExchangeRate<From extends Currency = Currency, To extends Currency = Currency> = Readonly<{\n from: From;\n to: To;\n value: Decimal;\n}>;\n\ntype FormatOptions = Readonly<{\n locale?: string;\n maximumFractionDigits?: number;\n minimumFractionDigits?: number;\n rounding?: RoundingMode;\n style?: 'code' | 'name' | 'narrowSymbol' | 'symbol';\n}>;\n\ntype MoneyFormatPart = Readonly<{\n type: 'currency' | 'decimal' | 'fraction' | 'integer' | 'literal' | 'minusSign' | 'plusSign';\n value: string;\n}>;\n\ntype MoneyJSON = Readonly<{\n amount: string;\n currency: string;\n unit: 'minor';\n}>;\n\ntype RoundingMode = 'awayFromZero' | 'ceil' | 'floor' | 'halfAwayFromZero' | 'halfEven' | 'towardZero';\n```\n\n`CurrencyCode`, `Decimal`, and `Money` carry phantom brand symbols that prevent unbranded values from being assigned where canonical values are required. `ExchangeRate.value` exposes a normalized exact `Decimal` numerator and denominator.\n\n| Rounding mode | Behavior |\n| --- | --- |\n| `awayFromZero` | Always increase a non-exact magnitude |\n| `ceil` | Toward positive infinity |\n| `floor` | Toward negative infinity |\n| `halfAwayFromZero` | Nearest; midpoint away from zero |\n| `halfEven` | Nearest; midpoint to an even retained unit |\n| `towardZero` | Truncate toward zero |\n\n## Errors\n\n```ts\ntype CoinsErrorCode =\n | 'CURRENCY_MISMATCH'\n | 'DIVISION_BY_ZERO'\n | 'FORMAT_ERROR'\n | 'INVALID_ALLOCATION'\n | 'INVALID_CURRENCY'\n | 'INVALID_DECIMAL'\n | 'INVALID_EXCHANGE_RATE'\n | 'INVALID_MONEY'\n | 'INVALID_RANGE'\n | 'INVALID_ROUNDING';\n```\n\nEvery Coins failure extends `CoinsError` and exposes `code`.\n\n```ts\nclass CoinsError extends Error {\n readonly code: CoinsErrorCode;\n}\n\nclass CurrencyMismatchError extends CoinsError {\n readonly expected: string;\n readonly received: string;\n}\n\nclass InvalidCurrencyError extends CoinsError {\n readonly value: unknown;\n}\n```\n\n`CurrencyMismatchError` and `InvalidCurrencyError` are specialized `CoinsError` subclasses. Use `instanceof CoinsError` to narrow any value to the Coins error hierarchy.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Coins: Usage Guide'\ndescription: Construct exact money, aggregate values, convert currencies, and format results with Coins.\n---\n\n[[toc]]\n\n## Basic Usage\n\nConstruct decimal values with a currency definition. Coins stores minor units internally and never accepts implicit floating-point input.\n\n```ts\nimport { USD, add, money, toDecimal } from '@vielzeug/coins';\n\nconst subtotal = add(money('12.50', USD), money('7.25', USD));\n\nconsole.log(toDecimal(subtotal)); // '19.75'\n```\n\nUse bigint only when data is already in minor units:\n\n```ts\nimport { USD, money } from '@vielzeug/coins';\n\nconst cents = money(1999n, USD, { unit: 'minor' });\n```\n\n## Define Currencies\n\nCoins exports seven built-in definitions: `USD`, `EUR`, `GBP`, `JPY`, `KRW`, `BHD`, and `KWD`. Construct other ISO or business currencies explicitly. Custom definitions are local canonical values; share one instance because arithmetic compares currencies by reference.\n\n```ts\nimport { EUR, USD, currency, money } from '@vielzeug/coins';\n\nconst rewards = currency({ code: 'PTS', minorUnit: 0 });\n\nmoney('10.00', USD);\nmoney('10.00', EUR);\nmoney('500', rewards);\n```\n\n## Apply Exact Arithmetic\n\nPass plain decimal strings to scaling operations; exponent notation, signs such as `+1`, whitespace, and numeric inputs are rejected. `multiply()`, `divide()`, `exchange()`, `round()`, and visible formatting default to `halfAwayFromZero`; pass a named mode when accounting policy requires another rule.\n\n```ts\nimport { USD, divide, money, multiply, round, toDecimal } from '@vielzeug/coins';\n\nconst subtotal = money('19.99', USD);\nconst taxed = multiply(subtotal, '1.08', { rounding: 'halfEven' });\n\n// Extra currency precision must name its rounding policy.\nconst roundedInput = money('19.999', USD, { rounding: 'halfAwayFromZero' });\nconst split = divide(taxed, '3', { rounding: 'floor' });\nconst displayed = round(taxed, { fractionDigits: 0, rounding: 'halfAwayFromZero' });\n\nconsole.log(toDecimal(split), toDecimal(displayed));\n```\n\n## Aggregate and Allocate\n\n`sum()` infers currency from non-empty values. Pass a canonical `{ currency }` for possibly empty iterables. `allocate()` preserves every minor unit, distributes remainders by largest fractional share with stable index ties, and applies the same allocation symmetrically to negative values. Counts and dense weight arrays are limited to 100,000 shares.\n\n```ts\nimport { USD, allocate, money, sum, toDecimal } from '@vielzeug/coins';\n\nconst total = sum([money('10.00', USD), money('5.00', USD)]);\nconst zero = sum([], { currency: USD });\nconst weighted = allocate(money('10.00', USD), ['1', '2', '1']);\nconst even = allocate(money(5n, USD, { unit: 'minor' }), 2);\n\nconsole.log(toDecimal(total));\nconsole.log(weighted.map(toDecimal));\nconsole.log(even.map((value) => value.amount)); // [3n, 2n]\n```\n\n## Convert Currency\n\nCreate a typed rate from currency definitions and a strictly positive exact decimal string.\n\n```ts\nimport { EUR, USD, exchange, exchangeRate, format, money } from '@vielzeug/coins';\n\nconst usdToEur = exchangeRate({ from: USD, to: EUR, value: '0.9234' });\nconst euros = exchange(money('100.00', USD), usdToEur, { rounding: 'halfEven' });\n\nconsole.log(format(euros, { locale: 'de-DE' }));\n```\n\n## Format Money\n\nFormat only at presentation boundaries. Coins keeps bigint arithmetic exact and uses `Intl.NumberFormat` only to obtain locale and currency layout.\n\n```ts\nimport { EUR, format, formatParts, money } from '@vielzeug/coins';\n\nconst value = money('1234.56', EUR);\nconsole.log(format(value, { locale: 'de-DE', style: 'code' }));\nconsole.log(formatParts(value, { maximumFractionDigits: 1, rounding: 'halfEven' }));\n```\n\nEither fraction bound may be supplied independently; the omitted bound adjusts to remain compatible. Explicit values must satisfy `0 ≤ minimumFractionDigits ≤ maximumFractionDigits ≤ 20`. Formatting passes the exact decimal string and rounding mode to `Intl`, including locale digits, currency-name pluralization, and rounded negative signs. It never mutates money.\n\n## Serialize Money\n\nUse `toJSON()` and `decodeMoney()` at storage, transport, worker, and realm boundaries. Decoding validates the envelope and returns a new canonical `Money` value.\n\n```ts\nimport { USD, currency, decodeMoney, money, toJSON } from '@vielzeug/coins';\n\nconst encoded = toJSON(money('19.99', USD));\nconst restored = decodeMoney(encoded);\n\nconst TOK = currency({ code: 'TOK', minorUnit: 2 });\nconst customEncoded = toJSON(money('1.00', TOK));\nconst custom = decodeMoney(customEncoded, {\n currency: (code) => (code === 'TOK' ? TOK : currency(code)),\n});\n```\n\n`MoneyJSON.amount` is a canonical integer string of at most 1,000 characters in minor units: no decimal point, exponent, leading plus, leading zero, whitespace, or negative zero. Built-in codes resolve automatically; custom codes require a resolver.\n\n`decodeMoney()` also accepts exactly `{ amount: bigint, currency: Currency }` when the currency is canonical. Custom resolvers must return the same code encoded in JSON. Use `isMoney()` only to test values already created or decoded in the current realm.\n\n## Handle Errors\n\nUse `CoinsError.code` for stable recovery branches.\n\n```ts\nimport { CoinsError, USD, money } from '@vielzeug/coins';\n\ntry {\n money('19.999', USD);\n} catch (error) {\n if (error instanceof CoinsError && error.code === 'INVALID_MONEY') {\n console.log('Over-precise decimal requires a rounding mode.');\n }\n}\n```\n\n## Best Practices\n\n- Use decimal strings for exact external inputs.\n- Use bigint only with `{ unit: 'minor' }`.\n- Pass named rounding options for division, scaling, and exchange.\n- Export each custom currency from one module and reuse that canonical instance.\n- Use `sum(values)` for non-empty collections; `sum(values, { currency })` for possibly empty ones.\n- Serialize with `toJSON()` and validate or re-canonicalize with `decodeMoney()`.\n- Format only at presentation boundaries.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Coins: Examples'\ndescription: Practical examples and recipes for @vielzeug/coins.\n---\n\n## Examples\n\n- [Formatting](./examples/formatting.md)\n- [Exchange Rate Conversion](./examples/exchange.md)\n- [Allocation](./examples/allocation.md)\n- [Serialization](./examples/serialization.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { createContainer } from './container.js';\nexport {\n ConduitCircularDependencyError,\n ConduitDisposedError,\n ConduitDisposeError,\n ConduitDuplicateRegistrationError,\n ConduitError,\n ConduitProviderNotFoundError,\n} from './errors.js';\nexport type {\n AnyProvider,\n Container,\n CreateScopeOptions,\n FactoryOptions,\n FactoryProvider,\n InferServices,\n InferTokens,\n Lifetime,\n Provider,\n ServiceMap,\n Token,\n ValueOptions,\n ValueProvider,\n} from './types.js';\nexport { disposalSignalToken, factoryProvider, token, valueProvider } from './types.js';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Conduit
|
|
5
|
-
"api": "---\ntitle: Conduit
|
|
6
|
-
"usage": "---\ntitle: Conduit
|
|
7
|
-
"examples": "---\ntitle: Conduit
|
|
4
|
+
"index": "---\ntitle: 'Conduit: Dependency Injection for TypeScript'\ndescription: Dependency-first asynchronous dependency injection with typed tokens, child-container scoping, startup validation, and deterministic disposal.\npackage: conduit\ncategory: infrastructure\nkeywords: [dependency injection, container, token, lifecycle, scope]\nexports: [createContainer, valueProvider, factoryProvider, disposalSignalToken, token]\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. Typed builders create immutable providers, static graphs are validated at construction, and dependency tuples drive creation and disposal. Resolve one root token or a typed composition map.\n\n```ts\n// Before\nconst service = createService(createApi(config), logger);\n\n// After\nconst container = createContainer([\n valueProvider(Config, config),\n factoryProvider(Api, [Config], (config) => createApi(config)),\n factoryProvider(Service, [Api, Logger], (api, logger) => createService(api, logger)),\n]);\nconst services = await container.resolve({ service: Service });\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, factoryProvider, token, valueProvider } from '@vielzeug/conduit';\n\nconst Config = token<{ baseUrl: string }>('Config');\nconst Client = token<{ url: string }>('Client');\n\nconst container = createContainer([\n valueProvider(Config, { baseUrl: '/api' }),\n factoryProvider(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` })),\n]);\n\nconst services = await container.resolve({ client: Client });\nconsole.log(services.client);\nawait container.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`token`**: typed dependency identity\n- **`valueProvider` / `factoryProvider`**: token-safe immutable registrations\n- **`createContainer`**: validated provider graph\n- **`resolve`**: direct service or typed composition map\n- **`createScope`**: child containers for 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
|
+
"api": "---\ntitle: 'Conduit: API Reference'\ndescription: Reference for Conduit tokens, immutable provider arrays, dependency-first factories, child containers, composition-root resolution, 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| `createContainer` | Build root container from an immutable provider array | Sync | Validates the full graph at construction |\n| `valueProvider` / `factoryProvider` | Build type-safe immutable registrations | Sync | Prefer over unannotated object literals |\n| `Provider` | Value or factory registration type | Sync | One local registration per token |\n| `has` | Check registration visibility | Sync | Walks parent containers |\n| `resolve` | Resolve one token or typed composition-root map | Async | Undeclared map tokens fail at resolution |\n| `createScope` | Create child container | Sync | Child providers shadow parent registrations |\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\n\n```ts\ntoken<T>(description: string): Token<T>\n```\n\nTokens are unique symbols. Descriptions exist only for diagnostics. `disposalSignalToken` is a built-in `Token<AbortSignal>` that resolves to the owning container's cancellation signal.\n\n## Container\n\n```ts\ncreateContainer(providers: readonly Provider[], options?: { name?: string }): Container\n```\n\n`providers` is an immutable array of value and factory providers. Construction validates the entire graph: duplicate tokens, missing dependencies, circular factory tuples, and singletons depending on transient factories all fail fast.\n\n### Providers\n\n```ts\ntype Provider<T, Dependencies> = ValueProvider<T> | FactoryProvider<T, Dependencies>;\n\nvalueProvider<T>(token: Token<T>, value: T, options?: ValueOptions<T>): ValueProvider<T>;\nfactoryProvider<T, Dependencies>(\n token: Token<T>,\n dependencies: Dependencies,\n factory: (...values: InferTokens<Dependencies>) => Promise<T> | T,\n options?: FactoryOptions<T>,\n): FactoryProvider<T, Dependencies>;\n```\n\nUse builders for token-driven value and dependency inference:\n\n```ts\nvalueProvider(Config, { baseUrl: '/api' }, { dispose: (value) => value.close() });\nfactoryProvider(Service, [Api, Logger], (api, logger) => createService(api, logger), {\n lifetime: 'singleton',\n dispose: (value) => value.dispose(),\n});\n```\n\nExplicit `ValueProvider<T>` and `FactoryProvider<T, Dependencies>` annotations remain available for reusable provider declarations. Avoid unannotated provider object literals because TypeScript cannot contextually infer one factory parameter from another object property.\n\n`dependencies` is copied at construction and drives creation, validation, cycle detection, and teardown order. Factories may return a value or promise.\n\n`lifetime` accepts `'singleton'` (default) or `'transient'`. Singletons cannot capture transient factories.\n\n```ts\ntype FactoryProvider<T, Dependencies> = {\n token: Token<T>;\n dependencies: Dependencies;\n factory: (...values: InferTokens<Dependencies>) => Promise<T> | T;\n dispose?: (value: T) => void | Promise<void>;\n lifetime?: 'singleton' | 'transient';\n};\n```\n\n### has\n\n```ts\ncontainer.has(token): boolean\n```\n\nChecks local and parent registrations without creating a factory result.\n\n### resolve\n\n```ts\ncontainer.resolve(token: Token<T>): Promise<T>\ncontainer.resolve(map: ServiceMap): Promise<InferServices<typeof map>>\n```\n\nResolves one typed token or a service object at a composition root. Map keys are defined as own properties, including `__proto__`, without changing the result prototype. Missing top-level tokens fail during resolution.\n\n```ts\nconst services = await container.resolve({ client: Client, config: Config });\n```\n\nSingleton resolutions deduplicate concurrent callers and cache successful values. A rejected creation attempt is evicted so a later resolution can retry.\n\n### createScope\n\n```ts\ncontainer.createScope(options?: { name?: string; providers?: readonly AnyProvider[] }): Container\n```\n\nA child container inherits parent registrations. Immutable local providers may shadow parent registrations for request or test overrides: this is how request-scoped values are spelled: one child per request, disposed when the request ends. Disposing a parent also disposes active children.\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 containers, waits for complete in-flight creation and late-result cleanup, then disposes owned resources in reverse creation order. Cleanup failures are aggregated in the readonly `ConduitDisposeError.errors` array.\n\n## Types\n\n```ts\ntype Token<T = unknown> = symbol;\ntype Lifetime = 'singleton' | 'transient';\n\ntype InferTokens<T extends readonly Token<unknown>[]> = {\n [K in keyof T]: T[K] extends Token<infer Value> ? Value : never;\n};\n\ntype ServiceMap = { readonly [key: string]: Token<unknown> };\n\ntype InferServices<M extends ServiceMap> = {\n readonly [K in keyof M]: M[K] extends Token<infer Value> ? Value : never;\n};\n\ninterface Container {\n createScope(options?: { name?: string; providers?: readonly AnyProvider[] }): Container;\n readonly disposalSignal: AbortSignal;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n has<T>(token: Token<T>): boolean;\n readonly name: string;\n resolve<T>(token: Token<T>): Promise<T>;\n resolve<M extends ServiceMap>(map: M): Promise<InferServices<M>>;\n [Symbol.asyncDispose](): Promise<void>;\n}\n```\n\n## Errors\n\n- `ConduitError`: base class; use `instanceof ConduitError` to narrow 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- `ConduitDisposedError`: operation attempted after disposal began.\n- `ConduitDisposeError`: one or more cleanup hooks failed.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Conduit: Usage Guide'\ndescription: Register an immutable provider array, resolve a typed service object at composition roots, scope work to child containers, and dispose owned resources.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate tokens once, build a container from an immutable provider array, then resolve a typed service object at your composition root.\n\n```ts\nimport { createContainer, factoryProvider, token, valueProvider } from '@vielzeug/conduit';\n\nconst Config = token<{ baseUrl: string }>('Config');\nconst Client = token<{ url: string }>('Client');\n\nconst container = createContainer([\n valueProvider(Config, { baseUrl: '/api' }),\n factoryProvider(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` })),\n]);\n\nconst services = await container.resolve({ config: Config, client: Client });\nconsole.log(services.client);\nawait container.dispose();\n```\n\n## Define Dependencies\n\nFactory `dependencies` tuples are authoritative. Conduit resolves tuple values in order, validates every edge at construction, and disposes created services in reverse dependency order.\n\n```ts\nconst Logger = token<{ info(message: string): void }>('Logger');\nconst Api = token<{ get(path: string): Promise<unknown> }>('Api');\nconst Service = token<{ load(): Promise<unknown> }>('Service');\n\nconst container = createContainer([\n valueProvider(Logger, { info: (message) => console.log(message) }),\n factoryProvider(Api, [], () => ({ get: async (path: string) => fetch(path) })),\n factoryProvider(Service, [Api, Logger], (api, logger) => ({\n async load() {\n logger.info('Loading data');\n return api.get('/data');\n },\n })),\n]);\n```\n\n## Choose Lifetimes\n\nFactories are singletons by default and cached on the registering container. Concurrent singleton resolutions share one in-flight attempt; a rejected attempt is evicted for retry. Use `'transient'` for one value per resolution. Singletons cannot depend on transient factories.\n\n```ts\nimport { createContainer, factoryProvider, token } from '@vielzeug/conduit';\n\nconst Clock = token<{ now(): number }>('Clock');\n\nconst root = createContainer([\n factoryProvider(Clock, [], () => ({ now: () => Date.now() }), { lifetime: 'transient' }),\n]);\n```\n\nFactory dependency tuples are copied at construction, so later caller mutation cannot change Conduit's graph. Add `disposalSignalToken` to a factory tuple when work must observe its owning container's cancellation.\n\n## Scope Work to Child Containers\n\nUse a child container when a resource belongs to a request, job, or test lifecycle. Pass immutable local providers through `createScope()` options to shadow parent registrations; the child owns and disposes them.\n\n```ts\nconst Session = token<{ id: string }>('Session');\nconst TraceId = token<string>('TraceId');\n\nconst request = root.createScope({\n providers: [\n factoryProvider(Session, [], () => ({ id: crypto.randomUUID() })),\n valueProvider(TraceId, 'request-trace'),\n ],\n});\nconst services = await request.resolve({ session: Session, traceId: TraceId });\nawait request.dispose();\nawait root.dispose();\n```\n\nEach child caches its own singleton, so two request children resolve distinct `Session` values while the root never sees request-owned work unless it registers it.\n\n## Resolve at Composition Roots\n\nResolve a map at composition roots to receive an explicitly typed service object. Direct `resolve(token)` remains available for one root service and focused tests; avoid scattering service-locator calls through application code.\n\n```ts\nconst client = await container.resolve(Client);\nconst services = await container.resolve({ client: Client, config: Config });\n// Both results are fully typed.\n```\n\n## Dispose Resources\n\n`dispose()` rejects new work, aborts `disposalSignal`, disposes child containers, waits for in-flight creation, then releases services in reverse creation order. A factory that finishes after disposal starts is immediately cleaned up and its resolver receives `ConduitDisposedError`.\n\n```ts\nawait container.dispose();\n```\n\n`ConduitDisposeError.errors` contains every cleanup failure after Conduit attempts all hooks, including cleanup from in-flight factories and child containers.\n\n## Testing\n\nBuild a container per test with explicit values for external dependencies. Because the provider array is immutable and validated at construction, a misconfigured test container fails immediately.\n\n```ts\nconst Clock = token<{ now(): number }>('Clock');\nconst Service = token<{ timestamp: number }>('Service');\n\nconst container = createContainer([\n valueProvider(Clock, { now: () => 123 }),\n factoryProvider(Service, [Clock], (clock) => ({ timestamp: clock.now() })),\n]);\n\nconst services = await container.resolve({ service: Service });\nexpect(services.service).toEqual({ timestamp: 123 });\nawait container.dispose();\n```\n\n## Best Practices\n\n- Create tokens at module scope.\n- Use `valueProvider()` and `factoryProvider()` so tokens, values, dependencies, and disposers remain type-safe.\n- Declare every factory dependency in its tuple.\n- Keep factories focused on one service.\n- Use child containers for request/job-owned resources.\n- Resolve once at a composition root; pass the typed service object downstream.\n- Dispose every child container and root container.\n- Keep optional application fallback policy outside Conduit.\n- Use `await using container = createContainer([...])` when lexical async disposal fits application lifetime.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Conduit: Examples'\ndescription: Immutable provider array recipes.\n---\n\n## Examples\n\n- [Basic setup](./examples/basic-setup.md)\n- [Static async providers](./examples/async-providers.md)\n- [Singleton lifetime](./examples/lifetimes.md)\n- [Child containers](./examples/child-containers.md)\n- [Disposal lifecycle](./examples/dispose-lifecycle.md)\n- [Startup validation](./examples/startup-hardening.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export {\n type Courier,\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n type CourierEvent,\n CourierHttpError,\n CourierNetworkError,\n type CourierOptions,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n createCourier,\n} from './courier.js';\nexport { withBearerAuth, withLogging, withRequestId } from './interceptors.js';\nexport type { ResponseType } from './response.js';\nexport type { FetchContext, Middleware, TransportOptions } from './transport.js';\nexport type {\n CourierCacheKey,\n CourierCacheKeyAtom,\n CourierCacheOptions,\n CourierReadCache,\n GetRequestConfig,\n Params,\n PrefetchConfig,\n RequestConfig,\n} from './url.js';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Courier
|
|
5
|
-
"api": "---\ntitle: Courier — API Reference\ndescription: Reference for Courier HTTP, cached read, prefetch, middleware, and error APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCourier()` | Creates transport client | Sync | Dispose only when whole scope ends |\n| `request()` | Sends one HTTP request of any method | Async | Direct calls never deduplicate |\n| `get()` | Sends a GET, optionally through the explicit cache | Async | One key must identify one representation |\n| `prefetch()` | Warms a cached GET | Async | Request failures resolve after surfacing through `tap()` |\n| `invalidateCache()` / `clearCache()` | Removes cached values | Sync | Invalidation matches structured key prefixes |\n| `post()` / `put()` / `patch()` / `delete()` | Common write method conveniences | Async | Use `request()` for custom methods |\n| `withBearerAuth()` | Adds authorization middleware | Sync | Token provider runs per request |\n| `withRequestId()` | Adds request identifier middleware | Sync | Default generator uses `crypto.randomUUID()` |\n| `withLogging()` | Logs request result metadata | Sync | Requires explicit logger; URLs may contain sensitive query values |\n| `tap()` | Observe structured transport events | Sync | Observation only; optional signal owns lifetime |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/courier` | Client factory, errors, middleware helpers, and public types |\n\n## Client\n\n### `createCourier()`\n\n```ts\ncreateCourier(options?: CourierOptions): Courier;\n```\n\nReturns an HTTP client sharing base URL, default headers, immutable middleware, optional parsed-value cache, timeout, and cancellation lifecycle.\n\n| `CourierOptions` field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `baseUrl` | `string` | `''` | Prefix for relative request paths |\n| `cache.capacity` | `number` | `100` | Maximum cached entries; positive integer or `Infinity` |\n| `cache.ttlMs` | `number` | `30_000` | Default cache freshness; non-negative milliseconds or `Infinity` |\n| `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Fetch implementation |\n| `headers` | `HeadersInit` | `{}` | Default request headers (immutable; merged lowercase) |\n| `middleware` | `readonly Middleware[]` | `[]` | Immutable middleware chain configured at construction |\n| `timeout` | `number` | `30_000` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity` |\n\n**Returns:** `Courier`.\n\n```ts\nimport { createCourier, withBearerAuth } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withBearerAuth('token')],\n});\n```\n\n| `Courier` member | Signature | Description |\n| --- | --- | --- |\n| `request` | `<T, P>(path, config?) => Promise<T>` | Sends one HTTP request; `config.method` defaults to `GET` |\n| `get` | `<T, P>(path, config?) => Promise<T>` | Sends GET; `cache` opts parsed responses into bounded caching |\n| `prefetch` | `<T, P>(path, config) => Promise<void>` | Warms one required structured cache key; failures are tap-observed |\n| `invalidateCache` | `(prefix: CourierCacheKey) => void` | Invalidates matching settled and pending key prefixes |\n| `clearCache` | `() => void` | Removes every cached and pending entry |\n| `post` | `<T, P>(path, config?) => Promise<T>` | Sends POST |\n| `put` | `<T, P>(path, config?) => Promise<T>` | Sends PUT |\n| `patch` | `<T, P>(path, config?) => Promise<T>` | Sends PATCH |\n| `delete` | `<T, P>(path, config?) => Promise<T>` | Sends DELETE |\n| `tap` | `(handler, options?: { signal?: AbortSignal }) => () => void` | Observes structured request and disposal events |\n| `cancelAll` | `() => void` | Aborts active requests; client remains usable |\n| `dispose` | `() => void` | Final teardown; aborts work and marks client unusable |\n| `disposed` | `boolean` | Whether final disposal occurred |\n| `disposalSignal` | `AbortSignal` | Aborts on final disposal |\n\n---\n\n## Requests\n\n### `request()`\n\n```ts\nrequest<T, P extends string>(path: P, config?: RequestConfig<P, T> & { method?: string }): Promise<T>;\n```\n\nSends one HTTP request. `config.method` defaults to `GET`; pass any method (including custom verbs) to support it consistently.\n\n| `RequestConfig<P, T>` field | Type | Description |\n| --- | --- | --- |\n| `method` | `string` | HTTP method; defaults to `GET` |\n| `params` | `Record<string, string \\| number \\| boolean>` | Path parameters for `{param}` placeholders |\n| `query` | `Params` | Query string parameters |\n| `body` | `unknown` | BodyInit values pass through; streams use Node duplex; GET/HEAD reject bodies; other values encode as JSON |\n| `headers` | `HeadersInit` | Per-request headers merged with (and overriding) defaults |\n| `responseType` | `ResponseType` | Response parsing strategy |\n| `schema` | `{ parse(data: unknown): T }` | Optional parsed-response validator; incompatible with `responseType: 'raw'` |\n| `signal` | `AbortSignal` | External abort signal merged with internal cancellation |\n| `timeout` | `number` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity`; overrides default |\n| `fetchInit` | `Omit<RequestInit, 'body' \\| 'headers' \\| 'method' \\| 'signal'>` | Raw fetch options for advanced use |\n\n**Returns:** Parsed response body (or `undefined` for empty bodies).\n\n```ts\nconst created = await courier.request<User>('/users', {\n method: 'POST',\n body: { name: 'Ada' },\n});\n```\n\n### `get()`\n\n```ts\nget<T, P extends string>(path: P, config?: GetRequestConfig<P, T>): Promise<T>;\n```\n\nConvenience for `request(path, { ...config, method: 'GET' })`. A `cache` descriptor opts a parsed response into the bounded cache. It requires a structured key and may override the default TTL. Raw responses cannot be cached.\n\n```ts\nconst user = await courier.get<User>('/users/{id}', {\n cache: { key: ['users', 1], ttlMs: 10_000 },\n params: { id: 1 },\n});\n```\n\n## Cache\n\n### `prefetch()`\n\n```ts\nprefetch<T, P extends string>(path: P, config: PrefetchConfig<P, T>): Promise<void>;\n```\n\nStarts the same cached GET used by `get()`. `config.cache` is required; `signal`, per-call `timeout`, and `responseType: 'raw'` are unavailable. Fresh values and pending loads are reused. The promise settles when the attempt finishes. Request failures emit `request-error` through `tap()` and resolve without caching data.\n\n### `invalidateCache()` and `clearCache()`\n\n```ts\ninvalidateCache(prefix: CourierCacheKey): void;\nclearCache(): void;\n```\n\n`invalidateCache()` removes every settled entry whose structured key starts with `prefix` and prevents matching pending loads from being stored. `clearCache()` applies that behavior to all entries. Existing waiters may still receive an invalidated in-flight result; `cancelAll()` or `dispose()` aborts physical work.\n\n---\n\n## Middleware\n\nMiddleware is an immutable chain configured at construction. Each middleware receives an immutable `FetchContext` and a `next` function.\n\n```ts\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n```\n\n### Middleware helpers\n\n```ts\nwithBearerAuth(token: string | (() => string | null | undefined | Promise<string | null | undefined>)): Middleware;\nwithRequestId(options?: { generate?: () => string; header?: string }): Middleware;\nwithLogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): Middleware;\n```\n\nEach helper returns a `Middleware` passed in the `middleware` option. `withLogging` requires an explicit `logger` function, isolates logger failures from requests, and has no default console output.\n\n```ts\nconst courier = createCourier({\n middleware: [\n withBearerAuth(() => tokenStore.getAccessToken()),\n withRequestId(),\n withLogging({ logger: (msg) => console.log(msg) }),\n ],\n});\n```\n\n## Types\n\n```ts\ntype TransportOptions = {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n headers?: HeadersInit;\n middleware?: readonly Middleware[];\n timeout?: number;\n};\n\ntype CourierCacheKeyAtom = string | number | boolean | null;\ntype CourierCacheKey = readonly [CourierCacheKeyAtom, ...CourierCacheKeyAtom[]];\n\ntype CourierCacheOptions = {\n capacity?: number;\n ttlMs?: number;\n};\n\ntype CourierReadCache = {\n key: CourierCacheKey;\n ttlMs?: number;\n};\n\ntype CourierOptions = TransportOptions & {\n cache?: CourierCacheOptions;\n};\n\ntype FetchContext = {\n readonly headers: Readonly<Record<string, string>>;\n readonly init: Readonly<Omit<RequestInit, 'headers'>>;\n readonly url: string;\n withHeaders(updates: Record<string, string | undefined>): FetchContext;\n withInit(updates: Partial<Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>>): FetchContext;\n};\n\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n\ntype CourierEvent =\n | { type: 'request-start'; method: string; url: string }\n | { type: 'request-success'; method: string; url: string; status: number; duration: number }\n | { type: 'request-error'; method: string; url: string; error: unknown }\n | { type: 'dispose' };\n```\n\n```ts\ntype ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype Params = Record<string, ParamValue>;\ntype ResponseType = 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';\ntype RequestConfig<P extends string = string, T = unknown> = {\n body?: unknown;\n method?: string;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: HeadersInit;\n params?: Record<string, string | number | boolean>;\n query?: Params;\n responseType?: ResponseType;\n schema?: { parse(data: unknown): T };\n signal?: AbortSignal;\n timeout?: number;\n};\n\ntype GetRequestConfig<P extends string = string, T = unknown> = Omit<\n RequestConfig<P, T>,\n 'body' | 'method' | 'responseType'\n> &\n ({ cache?: CourierReadCache; responseType?: Exclude<ResponseType, 'raw'> } | { cache?: never; responseType: 'raw' }) & {\n body?: never;\n };\n\ntype PrefetchConfig<P extends string = string, T = unknown> = Omit<\n GetRequestConfig<P, T>,\n 'cache' | 'responseType' | 'signal' | 'timeout'\n> & {\n cache: CourierReadCache;\n responseType?: Exclude<ResponseType, 'raw'>;\n signal?: never;\n timeout?: never;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `CourierError` | Base class for all Courier errors | Use `instanceof` to narrow |\n| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url`; `CourierHttpError.is(e, status?)` narrows by status |\n| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |\n| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |\n| `CourierAbortError` | Caller or client cancellation | `method`, `url`, `cause` |\n| `CourierSchemaValidationError` | Response schema fails | `data`, `cause` |\n| `CourierParseError` | Response body cannot parse | — |\n| `CourierConfigError` | Invalid request config (GET/HEAD body, raw + schema) | — |\n| `CourierDisposedError` | Work starts after disposal | — |\n\nErrors thrown directly by custom middleware are preserved rather than reclassified.\n",
|
|
6
|
-
"usage": "---\ntitle: Courier — Usage Guide\ndescription: Use one Courier client for typed HTTP, explicit cached reads, prefetching, immutable middleware, and structured errors.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one Courier client for an application or request scope. Its transport policy, middleware, and disposal lifecycle apply to every request.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\nconst user = await courier.get<User>('/users/{id}', { params: { id: 1 } });\nconsole.log(user.name);\n```\n\n## HTTP Requests\n\nUse `get()`, `post()`, `put()`, `patch()`, and `delete()` for standard methods. Use `request()` when the method is custom or selected dynamically. Courier preserves root-relative paths, encodes path parameters, serializes JSON-compatible bodies, and parses successful responses.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'Updated title' },\n params: { id: posts[0].id },\n});\n```\n\n`get()`, `post()`, `put()`, `patch()`, and `delete()` cover common methods. Use `request()` for custom verbs or when a method is selected dynamically.\n\n## Default Headers\n\nSet default headers at construction. They are merged (lowercased) into every request and cannot be mutated at runtime — middleware handles dynamic headers.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n headers: { 'x-client': 'web' },\n});\n```\n\n## Middleware\n\nMiddleware is an immutable chain configured once at construction. Each middleware receives an immutable `FetchContext` and a `next` function. Use `ctx.withHeaders()` or `ctx.withInit()` to derive a new context — never mutate `ctx` directly.\n\n```ts\nimport { withBearerAuth, withRequestId, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [\n withBearerAuth(() => sessionStorage.getItem('access-token') ?? ''),\n withRequestId(),\n ],\n});\n```\n\nA bearer token provider may return `null`, `undefined`, or an empty string to omit a stale authorization header. Use `withLogging()` with an explicit `logger` function to log requests during local development. `withLogging()` includes full URLs, so sanitize query values before persistent logging.\n\n```ts\nimport { withLogging, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (msg) => console.log(msg) })],\n});\n```\n\nUse `tap()` for multiple structured transport observers. It emits request-start, request-success, request-error, and dispose events; observer failures cannot alter requests. Pass `{ signal }` to own listener lifetime.\n\n```ts\nconst stop = courier.tap((event) => metrics.record(event), { signal: routeSignal });\n```\n\nBecause middleware is immutable, a client's behavior is locally understandable from its construction call. To change policy, create a new client.\n\n## Cached Reads and Prefetching\n\nOpt parsed GETs into caching with a structured key. Courier uses a finite capacity of 100 entries and a 30-second TTL by default; client options change those defaults, and each read may override TTL. Successful parsed and schema-validated values are cached, while errors are not.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n cache: { capacity: 200, ttlMs: 60_000 },\n});\nconst cache = { key: ['accounts', accountId, 'users', userId] as const, ttlMs: 10_000 };\n\nawait courier.prefetch<User>('/users/{id}', { cache, params: { id: userId } });\nconst user = await courier.get<User>('/users/{id}', { cache, params: { id: userId } });\n```\n\n`prefetch()` returns a promise that settles when warming finishes. Request failures are not cached or rejected from that promise; the physical request emits `request-error` through `tap()`. Cache, key, and URL configuration errors still throw before prefetch starts.\n\nInvalidate a structured prefix after writes. Every matching settled or pending key becomes ineligible for caching, while unrelated entries remain available.\n\n```ts\nawait courier.patch('/users/{id}', { body: update, params: { id: userId } });\ncourier.invalidateCache(['accounts', accountId, 'users']);\n```\n\nA cache key must identify one response representation, including a stable non-secret principal or tenant identifier, schema, and relevant request variants. Never place credentials in a key; call `clearCache()` when identity changes. Cached objects are shared by reference.\n\nConcurrent callers share physical work but own their own signal and timeout. One cancelled caller does not affect the others. When every caller cancels and no prefetch owns the load, Courier aborts the physical request. `cancelAll()` always aborts physical work; `dispose()` also clears settled cache data.\n\n## Response Parsing\n\nCourier parses successful 2xx response bodies automatically based on `content-type`, unless you set `responseType` explicitly (`'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'raw'` for the `Response` itself). Empty JSON bodies return `undefined` instead of throwing. Raw response bodies remain owned by Courier until consumed or cancelled, so `cancelAll()` and `dispose()` still abort them. Raw responses cannot be combined with `schema`. Always consume or cancel the body to release request ownership, and set `timeout: Infinity` for intentionally long-lived streams. Courier preserves URL, redirect, and type metadata on tracked responses and their clones.\n\n```ts\nconst buffer = await courier.request('/files/{id}', {\n method: 'GET',\n params: { id: 1 },\n responseType: 'arrayBuffer',\n});\n```\n\n## Schema Validation\n\nPass a `schema` with a `parse(data)` method to validate the parsed body. Any validator works — a `@vielzeug/spell` schema or a plain function wrapper. A failed parse throws `CourierSchemaValidationError`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst user = await courier.get('/users/1', {\n schema: s.object({ id: s.number(), name: s.string() }),\n});\n```\n\n## Cancellation and Disposal\n\nPass a `signal` to cancel one request. `cancelAll()` aborts every active request without disposing the client. `dispose()` aborts active work and marks the client unusable — call it only at the final application or request boundary.\n\n```ts\nconst controller = new AbortController();\nvoid courier.get('/slow', { signal: controller.signal });\ncontroller.abort(); // cancels that request only\n\ncourier.cancelAll(); // cancels all active requests\ncourier.dispose(); // final teardown\n```\n\n## Timeouts\n\nSet a client-wide `timeout` (default 30,000 ms) or a per-request timeout. Finite values must be integer milliseconds from 1 through 2,147,483,647. A timed-out request rejects with `CourierTimeoutError`; use `Infinity` for intentionally long-lived work.\n\n```ts\nconst courier = createCourier({ timeout: 10_000 });\nawait courier.get('/health', { timeout: 2_000 });\n```\n\n## Error Handling\n\nCourier classifies URL, request-construction, fetch, cancellation, HTTP, response-parsing, and schema failures into distinct error classes. Errors thrown directly by custom middleware are preserved.\n\n```ts\nimport {\n CourierAbortError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierTimeoutError,\n createCourier,\n} from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\nasync function loadUser(): Promise<string> {\n try {\n return (await courier.get<{ name: string }>('/users/1', { timeout: 2_000 })).name;\n } catch (error) {\n if (error instanceof CourierAbortError) return 'Cancelled';\n if (error instanceof CourierTimeoutError) return 'Timed out; retry.';\n if (CourierHttpError.is(error, 404)) return 'User not found.';\n if (error instanceof CourierNetworkError) return 'Check your connection.';\n if (error instanceof CourierParseError) return 'The server returned an unreadable response.';\n throw error;\n }\n}\n```\n\n- A `CourierHttpError` has a response (`status`, `data`, `headers`); a `CourierNetworkError` does not.\n- Do not display cancellation as an application error during navigation.\n- `CourierSchemaValidationError` carries the raw pre-validation `data`.\n\n## Composing with a State Layer\n\nCourier owns HTTP transport and optional parsed GET caching. Observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. Sourcerer, TanStack Query, or consumer code). Compose Courier calls into that layer instead of nesting state machines.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst users = createPageSource<User>({\n load: async ({ page, pageSize, signal }) => {\n const result = await courier.get<{ data: User[]; total: number }>('/users', {\n query: { page, pageSize },\n signal,\n });\n return { items: result.data, totalItems: result.total };\n },\n});\n\nawait users.reload();\n```\n\n## Framework Integration\n\nCreate Courier at an application or route boundary. Inject the client where requests are issued; let the framework own rendering and cache state.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useContext } from 'react';\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\ntype CourierClient = ReturnType<typeof createCourier>;\nconst CourierContext = createContext<CourierClient | null>(null);\n\nexport function CourierProvider({ children, courier }: { children: React.ReactNode; courier: CourierClient }) {\n return <CourierContext.Provider value={courier}>{children}</CourierContext.Provider>;\n}\n\nexport function useCourier() {\n const courier = useContext(CourierContext);\n if (!courier) throw new Error('CourierProvider missing');\n return courier;\n}\n\n// In a component:\n// const user = await useCourier().get<User>('/users/1');\n```\n\n```ts [Vue 3]\nimport { createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: '/api' });\n// Provide via app.provide('courier', courier) and inject in setup().\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createCourier } from '@vielzeug/courier';\n export let courier: ReturnType<typeof createCourier>;\n</script>\n```\n\n:::\n\nCourier exposes no loading, error, or cache store. Construct the client at the application or SSR-request boundary, pass it into framework providers, and dispose it at that same boundary. Render state in the framework that owns the view.\n\n## Gotchas\n\n### Root-relative paths stay root-relative\n\nWith no `baseUrl`, `'/api/users'` is passed to fetch unchanged while `'api/users'` remains document-relative. With a `baseUrl`, Courier appends either spelling to that base.\n\n### `baseUrl` should not include query parameters\n\n`buildUrl` joins `baseUrl` and path with `/`. A base URL like `https://api.example.com?token=abc` produces broken URLs (`https://api.example.com?token=abc/users`). Pass query parameters per-request via `config.query` instead.\n\n### Query parameters precede fragments\n\nWhen a path already contains `#fragment`, Courier inserts `config.query` before the fragment so values remain part of the HTTP query.\n\n### GET and HEAD bodies are rejected\n\nFetch does not support bodies for GET or HEAD. Courier rejects them as request parse errors before dispatch.\n\n### Middleware is immutable\n\nThere is no `use()` method. To change middleware, create a new client. This keeps each client's behavior locally understandable and avoids runtime mutation of the request chain.\n\n### Keep credentials out of URLs when using logging middleware\n\n`withLogging()` logs the full URL, including query parameters. If URLs may contain sensitive data, provide a custom `logger` that sanitizes the URL first.\n\n## Best Practices\n\n- Create one Courier client per application or SSR request scope.\n- Configure middleware once at construction; never mutate it at runtime.\n- Use method conveniences for GET/POST/PUT/PATCH/DELETE and `request()` for custom verbs.\n- Use explicit cache keys only for parsed GETs that should share data and in-flight work.\n- Clear cached data when the authenticated principal or tenant changes.\n- Let the owning state layer own observable queries and mutations.\n- Dispose only at the final application or request boundary.\n- Keep credentials out of URLs when using logging middleware.\n",
|
|
7
|
-
"examples": "---\ntitle: Courier
|
|
4
|
+
"index": "---\ntitle: 'Courier: HTTP client'\ndescription: A framework-neutral HTTP client with explicit cached reads, prefetching, immutable middleware, typed paths, and structured errors.\npackage: courier\ncategory: http\nkeywords: [http-client, fetch, cache, prefetch, middleware, interceptors, errors]\nrelated: [flux, spell, postmaster]\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, error classification, middleware composition, and repeated-read coordination to each application. Courier keeps those concerns in one client, including explicit cached GETs and warm-cache prefetching, while observable query and mutation state remain consumer-owned.\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\nconst user = await courier.get<User>('/users/{id}', { params: { id: userId } });\n```\n\n| Feature | Courier | ky | ofetch |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"courier\" type=\"size\" /> | Separate package | Separate package |\n| Zero 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| Native fetch transport | <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| Explicit parsed-value cache and prefetch | <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| Immutable middleware at construction | <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| Structured error taxonomy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Courier when** one client should own typed HTTP, explicit cached GETs and prefetching, immutable middleware, and structured errors, while a separate state layer owns observable queries and mutations.\n\n**Consider ky when** you only need a compact fetch wrapper. **Consider ofetch when** you want a bundled fetch utility with its own retry and caching conventions.\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 transport client for an application or request scope, then use method conveniences for standard HTTP methods and `request()` for custom methods.\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' });\n\ntry {\n const user = await courier.get<User>('/users/{id}', { params: { id: 42 } });\n console.log(user.name);\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, immutable middleware pipeline, header defaults, and cancellation boundary.\n- **`request()`**: one contract for every HTTP method; pass `method` in the config.\n- **Cached `get()`**: opt in with a structured key and optional per-read TTL; successful parsed values share bounded storage and in-flight work.\n- **`prefetch()`**: await cache warming while request failures remain observable through `tap()`.\n- **`invalidateCache()` / `clearCache()`**: invalidate structured key prefixes without query state.\n- **`get()` / `post()` / `put()` / `patch()` / `delete()`**: conveniences for standard methods.\n- **`withBearerAuth()` / `withRequestId()` / `withLogging()`**: composable middleware configured at construction.\n- **`tap()`**: signal-owned structured transport observation.\n- **Error taxonomy**: HTTP, network, timeout, abort, parse, and schema failures are distinct, actionable classes.\n- **`cancelAll()` / `dispose()`**: abort active requests and tear down the transport.\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/): composes async streams; pair with Courier transport calls in the owning state layer.\n- [Spell](/spell/): validates parsed HTTP payloads through Courier's `schema` option.\n- [Postmaster](/postmaster/): coordinates durable delivery of Courier requests through a job outbox.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Courier: API Reference'\ndescription: Reference for Courier HTTP, cached read, prefetch, middleware, and error APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCourier()` | Creates transport client | Sync | Dispose only when whole scope ends |\n| `request()` | Sends one HTTP request of any method | Async | Direct calls never deduplicate |\n| `get()` | Sends a GET, optionally through the explicit cache | Async | One key must identify one representation |\n| `prefetch()` | Warms a cached GET | Async | Request failures resolve after surfacing through `tap()` |\n| `invalidateCache()` / `clearCache()` | Removes cached values | Sync | Invalidation matches structured key prefixes |\n| `post()` / `put()` / `patch()` / `delete()` | Common write method conveniences | Async | Use `request()` for custom methods |\n| `withBearerAuth()` | Adds authorization middleware | Sync | Token provider runs per request |\n| `withRequestId()` | Adds request identifier middleware | Sync | Default generator uses `crypto.randomUUID()` |\n| `withLogging()` | Logs request result metadata | Sync | Requires explicit logger; URLs may contain sensitive query values |\n| `tap()` | Observe structured transport events | Sync | Observation only; optional signal owns lifetime |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/courier` | Client factory, errors, middleware helpers, and public types |\n\n## Client\n\n### `createCourier()`\n\n```ts\ncreateCourier(options?: CourierOptions): Courier;\n```\n\nReturns an HTTP client sharing base URL, default headers, immutable middleware, optional parsed-value cache, timeout, and cancellation lifecycle.\n\n| `CourierOptions` field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `baseUrl` | `string` | `''` | Prefix for relative request paths |\n| `cache.capacity` | `number` | `100` | Maximum cached entries; positive integer or `Infinity` |\n| `cache.ttlMs` | `number` | `30_000` | Default cache freshness; non-negative milliseconds or `Infinity` |\n| `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Fetch implementation |\n| `headers` | `HeadersInit` | `{}` | Default request headers (immutable; merged lowercase) |\n| `middleware` | `readonly Middleware[]` | `[]` | Immutable middleware chain configured at construction |\n| `timeout` | `number` | `30_000` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity` |\n\n**Returns:** `Courier`.\n\n```ts\nimport { createCourier, withBearerAuth } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withBearerAuth('token')],\n});\n```\n\n| `Courier` member | Signature | Description |\n| --- | --- | --- |\n| `request` | `<T, P>(path, config?) => Promise<T>` | Sends one HTTP request; `config.method` defaults to `GET` |\n| `get` | `<T, P>(path, config?) => Promise<T>` | Sends GET; `cache` opts parsed responses into bounded caching |\n| `prefetch` | `<T, P>(path, config) => Promise<void>` | Warms one required structured cache key; failures are tap-observed |\n| `invalidateCache` | `(prefix: CourierCacheKey) => void` | Invalidates matching settled and pending key prefixes |\n| `clearCache` | `() => void` | Removes every cached and pending entry |\n| `post` | `<T, P>(path, config?) => Promise<T>` | Sends POST |\n| `put` | `<T, P>(path, config?) => Promise<T>` | Sends PUT |\n| `patch` | `<T, P>(path, config?) => Promise<T>` | Sends PATCH |\n| `delete` | `<T, P>(path, config?) => Promise<T>` | Sends DELETE |\n| `tap` | `(handler, options?: { signal?: AbortSignal }) => () => void` | Observes structured request and disposal events |\n| `cancelAll` | `() => void` | Aborts active requests; client remains usable |\n| `dispose` | `() => void` | Final teardown; aborts work and marks client unusable |\n| `disposed` | `boolean` | Whether final disposal occurred |\n| `disposalSignal` | `AbortSignal` | Aborts on final disposal |\n\n---\n\n## Requests\n\n### `request()`\n\n```ts\nrequest<T, P extends string>(path: P, config?: RequestConfig<P, T> & { method?: string }): Promise<T>;\n```\n\nSends one HTTP request. `config.method` defaults to `GET`; pass any method (including custom verbs) to support it consistently.\n\n| `RequestConfig<P, T>` field | Type | Description |\n| --- | --- | --- |\n| `method` | `string` | HTTP method; defaults to `GET` |\n| `params` | `Record<string, string \\| number \\| boolean>` | Path parameters for `{param}` placeholders |\n| `query` | `Params` | Query string parameters |\n| `body` | `unknown` | BodyInit values pass through; streams use Node duplex; GET/HEAD reject bodies; other values encode as JSON |\n| `headers` | `HeadersInit` | Per-request headers merged with (and overriding) defaults |\n| `responseType` | `ResponseType` | Response parsing strategy |\n| `schema` | `{ parse(data: unknown): T }` | Optional parsed-response validator; incompatible with `responseType: 'raw'` |\n| `signal` | `AbortSignal` | External abort signal merged with internal cancellation |\n| `timeout` | `number` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity`; overrides default |\n| `fetchInit` | `Omit<RequestInit, 'body' \\| 'headers' \\| 'method' \\| 'signal'>` | Raw fetch options for advanced use |\n\n**Returns:** Parsed response body (or `undefined` for empty bodies).\n\n```ts\nconst created = await courier.request<User>('/users', {\n method: 'POST',\n body: { name: 'Ada' },\n});\n```\n\n### `get()`\n\n```ts\nget<T, P extends string>(path: P, config?: GetRequestConfig<P, T>): Promise<T>;\n```\n\nConvenience for `request(path, { ...config, method: 'GET' })`. A `cache` descriptor opts a parsed response into the bounded cache. It requires a structured key and may override the default TTL. Raw responses cannot be cached.\n\n```ts\nconst user = await courier.get<User>('/users/{id}', {\n cache: { key: ['users', 1], ttlMs: 10_000 },\n params: { id: 1 },\n});\n```\n\n## Cache\n\n### `prefetch()`\n\n```ts\nprefetch<T, P extends string>(path: P, config: PrefetchConfig<P, T>): Promise<void>;\n```\n\nStarts the same cached GET used by `get()`. `config.cache` is required; `signal`, per-call `timeout`, and `responseType: 'raw'` are unavailable. Fresh values and pending loads are reused. The promise settles when the attempt finishes. Request failures emit `request-error` through `tap()` and resolve without caching data.\n\n### `invalidateCache()` and `clearCache()`\n\n```ts\ninvalidateCache(prefix: CourierCacheKey): void;\nclearCache(): void;\n```\n\n`invalidateCache()` removes every settled entry whose structured key starts with `prefix` and prevents matching pending loads from being stored. `clearCache()` applies that behavior to all entries. Existing waiters may still receive an invalidated in-flight result; `cancelAll()` or `dispose()` aborts physical work.\n\n---\n\n## Middleware\n\nMiddleware is an immutable chain configured at construction. Each middleware receives an immutable `FetchContext` and a `next` function.\n\n```ts\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n```\n\n### Middleware helpers\n\n```ts\nwithBearerAuth(token: string | (() => string | null | undefined | Promise<string | null | undefined>)): Middleware;\nwithRequestId(options?: { generate?: () => string; header?: string }): Middleware;\nwithLogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): Middleware;\n```\n\nEach helper returns a `Middleware` passed in the `middleware` option. `withLogging` requires an explicit `logger` function, isolates logger failures from requests, and has no default console output.\n\n```ts\nconst courier = createCourier({\n middleware: [\n withBearerAuth(() => tokenStore.getAccessToken()),\n withRequestId(),\n withLogging({ logger: (msg) => console.log(msg) }),\n ],\n});\n```\n\n## Types\n\n```ts\ntype TransportOptions = {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n headers?: HeadersInit;\n middleware?: readonly Middleware[];\n timeout?: number;\n};\n\ntype CourierCacheKeyAtom = string | number | boolean | null;\ntype CourierCacheKey = readonly [CourierCacheKeyAtom, ...CourierCacheKeyAtom[]];\n\ntype CourierCacheOptions = {\n capacity?: number;\n ttlMs?: number;\n};\n\ntype CourierReadCache = {\n key: CourierCacheKey;\n ttlMs?: number;\n};\n\ntype CourierOptions = TransportOptions & {\n cache?: CourierCacheOptions;\n};\n\ntype FetchContext = {\n readonly headers: Readonly<Record<string, string>>;\n readonly init: Readonly<Omit<RequestInit, 'headers'>>;\n readonly url: string;\n withHeaders(updates: Record<string, string | undefined>): FetchContext;\n withInit(updates: Partial<Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>>): FetchContext;\n};\n\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n\ntype CourierEvent =\n | { type: 'request-start'; method: string; url: string }\n | { type: 'request-success'; method: string; url: string; status: number; duration: number }\n | { type: 'request-error'; method: string; url: string; error: unknown }\n | { type: 'dispose' };\n```\n\n```ts\ntype ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype Params = Record<string, ParamValue>;\ntype ResponseType = 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';\ntype RequestConfig<P extends string = string, T = unknown> = {\n body?: unknown;\n method?: string;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: HeadersInit;\n params?: Record<string, string | number | boolean>;\n query?: Params;\n responseType?: ResponseType;\n schema?: { parse(data: unknown): T };\n signal?: AbortSignal;\n timeout?: number;\n};\n\ntype GetRequestConfig<P extends string = string, T = unknown> = Omit<\n RequestConfig<P, T>,\n 'body' | 'method' | 'responseType'\n> &\n ({ cache?: CourierReadCache; responseType?: Exclude<ResponseType, 'raw'> } | { cache?: never; responseType: 'raw' }) & {\n body?: never;\n };\n\ntype PrefetchConfig<P extends string = string, T = unknown> = Omit<\n GetRequestConfig<P, T>,\n 'cache' | 'responseType' | 'signal' | 'timeout'\n> & {\n cache: CourierReadCache;\n responseType?: Exclude<ResponseType, 'raw'>;\n signal?: never;\n timeout?: never;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `CourierError` | Base class for all Courier errors | Use `instanceof` to narrow |\n| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url`; `CourierHttpError.is(e, status?)` narrows by status |\n| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |\n| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |\n| `CourierAbortError` | Caller or client cancellation | `method`, `url`, `cause` |\n| `CourierSchemaValidationError` | Response schema fails | `data`, `cause` |\n| `CourierParseError` | Response body cannot parse | N/A |\n| `CourierConfigError` | Invalid request config (GET/HEAD body, raw + schema) | N/A |\n| `CourierDisposedError` | Work starts after disposal | N/A |\n\nErrors thrown directly by custom middleware are preserved rather than reclassified.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Courier: Usage Guide'\ndescription: Use one Courier client for typed HTTP, explicit cached reads, prefetching, immutable middleware, and structured errors.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one Courier client for an application or request scope. Its transport policy, middleware, and disposal lifecycle apply to every request.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\nconst user = await courier.get<User>('/users/{id}', { params: { id: 1 } });\nconsole.log(user.name);\n```\n\n## HTTP Requests\n\nUse `get()`, `post()`, `put()`, `patch()`, and `delete()` for standard methods. Use `request()` when the method is custom or selected dynamically. Courier preserves root-relative paths, encodes path parameters, serializes JSON-compatible bodies, and parses successful responses.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'Updated title' },\n params: { id: posts[0].id },\n});\n```\n\n`get()`, `post()`, `put()`, `patch()`, and `delete()` cover common methods. Use `request()` for custom verbs or when a method is selected dynamically.\n\n## Default Headers\n\nSet default headers at construction. They are merged (lowercased) into every request and cannot be mutated at runtime: middleware handles dynamic headers.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n headers: { 'x-client': 'web' },\n});\n```\n\n## Middleware\n\nMiddleware is an immutable chain configured once at construction. Each middleware receives an immutable `FetchContext` and a `next` function. Use `ctx.withHeaders()` or `ctx.withInit()` to derive a new context: never mutate `ctx` directly.\n\n```ts\nimport { withBearerAuth, withRequestId, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [\n withBearerAuth(() => sessionStorage.getItem('access-token') ?? ''),\n withRequestId(),\n ],\n});\n```\n\nA bearer token provider may return `null`, `undefined`, or an empty string to omit a stale authorization header. Use `withLogging()` with an explicit `logger` function to log requests during local development. `withLogging()` includes full URLs, so sanitize query values before persistent logging.\n\n```ts\nimport { withLogging, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (msg) => console.log(msg) })],\n});\n```\n\nUse `tap()` for multiple structured transport observers. It emits request-start, request-success, request-error, and dispose events; observer failures cannot alter requests. Pass `{ signal }` to own listener lifetime.\n\n```ts\nconst stop = courier.tap((event) => metrics.record(event), { signal: routeSignal });\n```\n\nBecause middleware is immutable, a client's behavior is locally understandable from its construction call. To change policy, create a new client.\n\n## Cached Reads and Prefetching\n\nOpt parsed GETs into caching with a structured key. Courier uses a finite capacity of 100 entries and a 30-second TTL by default; client options change those defaults, and each read may override TTL. Successful parsed and schema-validated values are cached, while errors are not.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n cache: { capacity: 200, ttlMs: 60_000 },\n});\nconst cache = { key: ['accounts', accountId, 'users', userId] as const, ttlMs: 10_000 };\n\nawait courier.prefetch<User>('/users/{id}', { cache, params: { id: userId } });\nconst user = await courier.get<User>('/users/{id}', { cache, params: { id: userId } });\n```\n\n`prefetch()` returns a promise that settles when warming finishes. Request failures are not cached or rejected from that promise; the physical request emits `request-error` through `tap()`. Cache, key, and URL configuration errors still throw before prefetch starts.\n\nInvalidate a structured prefix after writes. Every matching settled or pending key becomes ineligible for caching, while unrelated entries remain available.\n\n```ts\nawait courier.patch('/users/{id}', { body: update, params: { id: userId } });\ncourier.invalidateCache(['accounts', accountId, 'users']);\n```\n\nA cache key must identify one response representation, including a stable non-secret principal or tenant identifier, schema, and relevant request variants. Never place credentials in a key; call `clearCache()` when identity changes. Cached objects are shared by reference.\n\nConcurrent callers share physical work but own their own signal and timeout. One cancelled caller does not affect the others. When every caller cancels and no prefetch owns the load, Courier aborts the physical request. `cancelAll()` always aborts physical work; `dispose()` also clears settled cache data.\n\n## Response Parsing\n\nCourier parses successful 2xx response bodies automatically based on `content-type`, unless you set `responseType` explicitly (`'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'raw'` for the `Response` itself). Empty JSON bodies return `undefined` instead of throwing. Raw response bodies remain owned by Courier until consumed or cancelled, so `cancelAll()` and `dispose()` still abort them. Raw responses cannot be combined with `schema`. Always consume or cancel the body to release request ownership, and set `timeout: Infinity` for intentionally long-lived streams. Courier preserves URL, redirect, and type metadata on tracked responses and their clones.\n\n```ts\nconst buffer = await courier.request('/files/{id}', {\n method: 'GET',\n params: { id: 1 },\n responseType: 'arrayBuffer',\n});\n```\n\n## Schema Validation\n\nPass a `schema` with a `parse(data)` method to validate the parsed body. Any validator works: a `@vielzeug/spell` schema or a plain function wrapper. A failed parse throws `CourierSchemaValidationError`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst user = await courier.get('/users/1', {\n schema: s.object({ id: s.number(), name: s.string() }),\n});\n```\n\n## Cancellation and Disposal\n\nPass a `signal` to cancel one request. `cancelAll()` aborts every active request without disposing the client. `dispose()` aborts active work and marks the client unusable: call it only at the final application or request boundary.\n\n```ts\nconst controller = new AbortController();\nvoid courier.get('/slow', { signal: controller.signal });\ncontroller.abort(); // cancels that request only\n\ncourier.cancelAll(); // cancels all active requests\ncourier.dispose(); // final teardown\n```\n\n## Timeouts\n\nSet a client-wide `timeout` (default 30,000 ms) or a per-request timeout. Finite values must be integer milliseconds from 1 through 2,147,483,647. A timed-out request rejects with `CourierTimeoutError`; use `Infinity` for intentionally long-lived work.\n\n```ts\nconst courier = createCourier({ timeout: 10_000 });\nawait courier.get('/health', { timeout: 2_000 });\n```\n\n## Error Handling\n\nCourier classifies URL, request-construction, fetch, cancellation, HTTP, response-parsing, and schema failures into distinct error classes. Errors thrown directly by custom middleware are preserved.\n\n```ts\nimport {\n CourierAbortError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierTimeoutError,\n createCourier,\n} from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\nasync function loadUser(): Promise<string> {\n try {\n return (await courier.get<{ name: string }>('/users/1', { timeout: 2_000 })).name;\n } catch (error) {\n if (error instanceof CourierAbortError) return 'Cancelled';\n if (error instanceof CourierTimeoutError) return 'Timed out; retry.';\n if (CourierHttpError.is(error, 404)) return 'User not found.';\n if (error instanceof CourierNetworkError) return 'Check your connection.';\n if (error instanceof CourierParseError) return 'The server returned an unreadable response.';\n throw error;\n }\n}\n```\n\n- A `CourierHttpError` has a response (`status`, `data`, `headers`); a `CourierNetworkError` does not.\n- Do not display cancellation as an application error during navigation.\n- `CourierSchemaValidationError` carries the raw pre-validation `data`.\n\n## Composing with a State Layer\n\nCourier owns HTTP transport and optional parsed GET caching. Observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. Sourcerer, TanStack Query, or consumer code). Compose Courier calls into that layer instead of nesting state machines.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst users = createPageSource<User>({\n load: async ({ page, pageSize, signal }) => {\n const result = await courier.get<{ data: User[]; total: number }>('/users', {\n query: { page, pageSize },\n signal,\n });\n return { items: result.data, totalItems: result.total };\n },\n});\n\nawait users.reload();\n```\n\n## Framework Integration\n\nCreate Courier at an application or route boundary. Inject the client where requests are issued; let the framework own rendering and cache state.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useContext } from 'react';\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\ntype CourierClient = ReturnType<typeof createCourier>;\nconst CourierContext = createContext<CourierClient | null>(null);\n\nexport function CourierProvider({ children, courier }: { children: React.ReactNode; courier: CourierClient }) {\n return <CourierContext.Provider value={courier}>{children}</CourierContext.Provider>;\n}\n\nexport function useCourier() {\n const courier = useContext(CourierContext);\n if (!courier) throw new Error('CourierProvider missing');\n return courier;\n}\n\n// In a component:\n// const user = await useCourier().get<User>('/users/1');\n```\n\n```ts [Vue 3]\nimport { createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: '/api' });\n// Provide via app.provide('courier', courier) and inject in setup().\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createCourier } from '@vielzeug/courier';\n export let courier: ReturnType<typeof createCourier>;\n</script>\n```\n\n:::\n\nCourier exposes no loading, error, or cache store. Construct the client at the application or SSR-request boundary, pass it into framework providers, and dispose it at that same boundary. Render state in the framework that owns the view.\n\n## Gotchas\n\n### Root-relative paths stay root-relative\n\nWith no `baseUrl`, `'/api/users'` is passed to fetch unchanged while `'api/users'` remains document-relative. With a `baseUrl`, Courier appends either spelling to that base.\n\n### `baseUrl` should not include query parameters\n\n`buildUrl` joins `baseUrl` and path with `/`. A base URL like `https://api.example.com?token=abc` produces broken URLs (`https://api.example.com?token=abc/users`). Pass query parameters per-request via `config.query` instead.\n\n### Query parameters precede fragments\n\nWhen a path already contains `#fragment`, Courier inserts `config.query` before the fragment so values remain part of the HTTP query.\n\n### GET and HEAD bodies are rejected\n\nFetch does not support bodies for GET or HEAD. Courier rejects them as request parse errors before dispatch.\n\n### Middleware is immutable\n\nThere is no `use()` method. To change middleware, create a new client. This keeps each client's behavior locally understandable and avoids runtime mutation of the request chain.\n\n### Keep credentials out of URLs when using logging middleware\n\n`withLogging()` logs the full URL, including query parameters. If URLs may contain sensitive data, provide a custom `logger` that sanitizes the URL first.\n\n## Best Practices\n\n- Create one Courier client per application or SSR request scope.\n- Configure middleware once at construction; never mutate it at runtime.\n- Use method conveniences for GET/POST/PUT/PATCH/DELETE and `request()` for custom verbs.\n- Use explicit cache keys only for parsed GETs that should share data and in-flight work.\n- Clear cached data when the authenticated principal or tenant changes.\n- Let the owning state layer own observable queries and mutations.\n- Dispose only at the final application or request boundary.\n- Keep credentials out of URLs when using logging middleware.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Courier: Examples'\ndescription: Practical examples and recipes for courier.\n---\n\n## Examples\n\n- [Authentication](./examples/authentication.md)\n- [CRUD Operations](./examples/crud-operations.md)\n- [Disposal](./examples/disposal.md)\n- [Error Handling Patterns](./examples/error-handling-patterns.md)\n- [File Uploads](./examples/file-uploads.md)\n- [Optimistic Updates](./examples/optimistic-updates.md)\n- [SSE Events](./examples/sse-events.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|