@vielzeug/codex 26.10.5 → 26.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +81 -42
- package/data/llms-full.txt +1474 -18
- package/data/llms.txt +2 -1
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +76 -0
- package/data/packages/arsenal.json +18 -4
- package/data/packages/courier.json +3 -3
- package/data/packages/prism.json +55 -52
- package/data/packages/refine.json +2 -2
- package/data/packages/tandem.json +1 -1
- package/data/refine.json +150 -0
- package/data/search.json +35 -14
- package/package.json +2 -2
- package/skills/vielzeug/SKILL.md +2 -1
|
@@ -2,9 +2,9 @@
|
|
|
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
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"
|
|
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). With\n`responseType: 'raw'` the resolved value is the `Response` itself; a `204`/`205`\nanswer whose transport carried an unexpected empty body stream is normalized to\n`body: null` — the stray stream is cancelled and headers, status, statusText,\nURL, redirect and type metadata are preserved (on the response and its clones).\nA null-body raw response owns no pending resource, so no consumption is required\nto release the request.\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. A `204`/`205` raw answer is normalized to `body: null` — a stray empty transport stream is cancelled for you — so a null-body raw response never carries pending body ownership and needs no consumption.\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- [Service Discovery and Custom Fetch](./examples/service-discovery-and-custom-fetch.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
|
{
|