@vielzeug/codex 3.1.1 → 26.10.0
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 +124 -36
- package/data/llms-full.txt +2335 -679
- package/data/llms.txt +5 -3
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +30 -6
- package/data/packages/assay.json +11 -19
- package/data/packages/clockwork.json +30 -27
- package/data/packages/codex.json +4 -4
- package/data/packages/conduit.json +33 -34
- package/data/packages/courier.json +15 -14
- package/data/packages/dnd.json +2 -2
- package/data/packages/familiar.json +24 -3
- package/data/packages/flux.json +9 -6
- package/data/packages/focus.json +16 -6
- package/data/packages/gesture.json +10 -4
- package/data/packages/herald.json +7 -8
- package/data/packages/illusionist.json +1 -1
- package/data/packages/keymap.json +11 -7
- package/data/packages/ledger.json +25 -25
- package/data/packages/lingua.json +2 -2
- package/data/packages/mesh.json +29 -31
- package/data/packages/necromancer.json +3 -2
- package/data/packages/orbit.json +4 -3
- package/data/packages/ore.json +4 -3
- package/data/packages/postmaster.json +24 -24
- package/data/packages/pulse.json +28 -28
- package/data/packages/refine.json +6 -4
- package/data/packages/ripple.json +2 -2
- package/data/packages/sandbox.json +2 -2
- package/data/packages/scout.json +13 -7
- package/data/packages/scroll.json +7 -7
- package/data/packages/sentinel.json +26 -15
- package/data/packages/sigil.json +3 -3
- package/data/packages/spell.json +25 -30
- package/data/packages/tandem.json +29 -0
- package/data/packages/tavern.json +23 -0
- package/data/packages/tempo.json +44 -42
- package/data/packages/vault.json +9 -19
- package/data/packages/ward.json +26 -29
- package/data/packages/wayfinder.json +20 -13
- package/data/refine.json +746 -65
- package/data/search.json +198 -163
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -12
- package/skills/vielzeug/SKILL.md +2 -0
- package/dist/advanced.d.ts +0 -12
- package/dist/advanced.js +0 -13
- package/dist/advanced.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export {\n type Courier,\n CourierAbortError,\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",
|
|
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| `CourierDisposedError` | Work starts after disposal | — |\n\nErrors thrown directly by custom middleware are preserved rather than reclassified.\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| `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
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
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
|
},
|
|
@@ -24,18 +24,19 @@
|
|
|
24
24
|
}
|
|
25
25
|
],
|
|
26
26
|
"typeSignatures": {
|
|
27
|
-
"Courier": "export {\n type Courier,\n CourierAbortError,\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';",
|
|
28
|
-
"CourierAbortError": "export {\n type Courier,\n CourierAbortError,\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';",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
27
|
+
"Courier": "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';",
|
|
28
|
+
"CourierAbortError": "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';",
|
|
29
|
+
"CourierConfigError": "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';",
|
|
30
|
+
"CourierDisposedError": "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';",
|
|
31
|
+
"CourierError": "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';",
|
|
32
|
+
"CourierEvent": "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';",
|
|
33
|
+
"CourierHttpError": "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';",
|
|
34
|
+
"CourierNetworkError": "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';",
|
|
35
|
+
"CourierOptions": "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';",
|
|
36
|
+
"CourierParseError": "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';",
|
|
37
|
+
"CourierSchemaValidationError": "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';",
|
|
38
|
+
"CourierTimeoutError": "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';",
|
|
39
|
+
"createCourier": "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';",
|
|
39
40
|
"withBearerAuth": "export { withBearerAuth, withLogging, withRequestId } from './interceptors.js';",
|
|
40
41
|
"withLogging": "export { withBearerAuth, withLogging, withRequestId } from './interceptors.js';",
|
|
41
42
|
"withRequestId": "export { withBearerAuth, withLogging, withRequestId } from './interceptors.js';",
|
package/data/packages/dnd.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "//
|
|
2
|
+
"apiSource": "// Full API aggregate and IIFE bundle entry. For tree-shaken imports prefer the\n// `@vielzeug/dnd/drop` and `@vielzeug/dnd/sortable` subpaths.\nexport * from './drop-zone.js';\nexport { DndError, DndScopeError } from './errors.js';\nexport * from './sortable.js';\nexport * from './types.js';\n",
|
|
3
3
|
"docs": {
|
|
4
4
|
"index": "---\ntitle: Dnd — Drag-and-drop primitives for the DOM\ndescription: Framework-agnostic drag-and-drop. Drop zones with MIME filtering, sortable lists with drag handles, and explicit connected scopes — zero third-party dependencies.\npackage: dnd\ncategory: ui-interaction\nkeywords: [drag-drop, sortable, file-upload, drop-zone, dnd, reorder]\nrelated: [gesture, ore, scroll, refine]\nexports: [createDropZone, createSortable, createSortableScope, applyReorder, matchesAccept]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"dnd\" />\n\n## Why Dnd?\n\nThe HTML5 Drag & Drop API requires careful counter tracking to avoid hover state flicker, has no MIME type pre-filtering, and provides no sortable list abstraction.\n\n```ts\n// Before — raw HTML5 Drag & Drop\nlet enterCount = 0;\ndropzone.addEventListener('dragenter', () => {\n enterCount++;\n dropzone.classList.add('over');\n});\ndropzone.addEventListener('dragleave', () => {\n if (--enterCount === 0) dropzone.classList.remove('over');\n});\ndropzone.addEventListener('dragover', (e) => e.preventDefault());\ndropzone.addEventListener('drop', (e) => {\n e.preventDefault();\n enterCount = 0;\n const files = [...e.dataTransfer!.files];\n if (!files.every((f) => f.type.startsWith('image/'))) return showError('Images only');\n uploadFiles(files);\n});\n\n// After — Dnd\nimport { createDropZone } from '@vielzeug/dnd/drop';\nconst zone = createDropZone({\n element: dropzone,\n accept: ['image/*'],\n onDrop: (files) => uploadFiles(files),\n onDropRejected: (files) => showError(`${files.length} file(s) not accepted`),\n onHoverChange: (hovered) => dropzone.classList.toggle('over', hovered),\n});\n```\n\n| Feature | DND | SortableJS | dnd-kit |\n| ------------------- | -------------------------------------------------------- | ------------------------------------------ | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"dnd\" type=\"size\" /> | ~15 kB | ~30 kB |\n| Framework agnostic | <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| MIME type filtering | <ore-icon name=\"check\" size=\"16\"></ore-icon> Pre-validated | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Counter-based hover | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | N/A |\n| Sortable lists | <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| Drag handles | <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| `using` support | <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| Touch support | <ore-icon name=\"check\" size=\"16\"></ore-icon> Scoped opt-in | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Zero third-party 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\n<div class=\"decision-callout\">\n\n**Use Dnd when** an item is picked up and dropped: file drop zones, sortable collections, keyboard reordering, or connected lists.\n\n**Use Gesture instead** when a surface follows free or axis-locked pointer movement and application code owns rendering and completion, such as direct manipulation, swipe reveal, carousel navigation, or drawer dismissal.\n\n**Consider dnd-kit** if you are building a React app and need complex multi-container drag interactions or accessibility-first sortable trees.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/dnd\n```\n\n```sh [npm]\nnpm install @vielzeug/dnd\n```\n\n```sh [yarn]\nyarn add @vielzeug/dnd\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createDropZone } from '@vielzeug/dnd/drop';\nimport { createSortable } from '@vielzeug/dnd/sortable';\n\n// File drop zone — with async validation and paste support\nconst dropzone = document.getElementById('dropzone')!;\n\nusing zone = createDropZone({\n element: dropzone,\n accept: ['image/*', '.pdf'],\n paste: true,\n onValidate: (files) => files.every((file) => file.size <= 5_000_000),\n onDrop: (files) => console.log('Upload', files),\n onDropRejected: (files) => {\n console.warn(`${files.length} file(s) rejected`);\n },\n onHoverChange: (hovered) => {\n dropzone.classList.toggle('drag-over', hovered);\n },\n});\n\n// Sortable list — with application-owned rollback for optimistic updates\nlet currentOrder = ['a', 'b', 'c'];\nconst history: Array<{ before: readonly string[] }> = [];\n\nusing sortable = createSortable({\n element: document.getElementById('list')!,\n keyboard: true,\n onBeforeReorder: (from, to) => {\n // record positions here before the DOM commits (for FLIP animations)\n },\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ before, after }) => {\n history.push({ before });\n currentOrder = after;\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Counter-based hover state** — `onHoverChange` stays accurate when dragging over child elements; hover only activates when the drag payload passes the `accept` filter, with symmetric enter/leave pairing to prevent flicker\n- **MIME type pre-validation** — queries `dataTransfer.items` during drag to set `dropEffect='none'` before the drop; confirmed against `File.type` on drop\n- **Flexible accept patterns** — MIME types (`image/png`), wildcards (`image/*`), and file extensions (`.pdf`)\n- **`maxFiles` limit** — cap the number of accepted files per drop; excess files are forwarded to `onDropRejected`\n- **`onValidate` async gating** — optional cancellable async step after type filtering; `zone.validating` remains `true` until every pending validation settles\n- **Clipboard paste support** — `paste: true` routes pasted files through the same `accept`, `maxFiles`, and `onValidate` pipeline; `onPaste` provides a separate callback; paste rejections are forwarded to `onDropRejected` with the same `(files: File[]) => void` signature as drop rejections\n- **`onDropRejected`** — separate callback for files that didn't match `accept`, exceeded `maxFiles`, or were rejected by `onValidate`\n- **Sortable lists** — reorders DOM children with a placeholder indicator; fires `onReorder` only when the order actually changes\n- **Drag handles** — scope dragging to a child selector via `handle`; whole item is draggable when omitted\n- **Custom drag preview** — pass an element or a `(id, item, event) => element | null` factory; control hotspot with `dragImageOffset`\n- **`onBeforeReorder` FLIP hook** — fires before commit for both drag and keyboard moves; pair it with [`captureLayout()`](/necromancer/api.md#capturelayout) for lifecycle-owned FLIP animation\n- **Application-owned rollback** — `onReorder` emits `{ before, after, item }` so application history can own undo/rollback instead of the drag controller\n- **Boundary-safe keyboard reordering** — arrow keys at the first/last item no longer suppress `preventDefault`, so the browser can scroll the page normally\n- **Transactional connected scopes** — one `onMove` callback receives each cross-list transfer with both final orders\n- **Scoped touch support** — `createSortableScope({ touch: true })` handles only items registered to that scope and uses an inert outline preview\n- **Explicit DOM refresh** — call `sortable.refresh()` after DOM mutations, or provide an `items()` provider for explicit item ownership\n- **`[Symbol.dispose]`** — both primitives support the `using` keyword for automatic cleanup\n- **Reactive-friendly options** — `disabled` is re-read on each event (reassign `options.disabled = true` to toggle); `accept` is normalized and snapshotted at construction; recreate the zone to change accepted types\n- **Shared pointer recognition** — touch sorting uses `@vielzeug/gesture`; Dnd retains previews, hit-testing, drag events, and sortable transactions\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- [Orbit](/orbit/) — floating element positioning; use alongside Dnd to anchor drag previews and drop-zone indicators to precise positions\n- [Gesture](/gesture/) — pointer recognition used by Dnd touch sorting and directly by swipe, reveal, carousel, and drawer interactions\n- [Ore](/ore/) — web-component authoring framework for application-owned draggable and sortable surfaces\n- [Refine](/refine/) — accessible web components; Dnd powers Refine's file-drop input behavior\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Dnd — API Reference\ndescription: Complete API reference for Dnd.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------------- | -------------------------------------------- | -------------- | ----------------------------------------------------------------- |\n| `createDropZone()` | Create a typed drop-zone controller | Sync | Dispose the controller during teardown |\n| `createSortable()` | Add sortable drag-and-drop behavior to lists | Sync | Provide stable item identity for reorder operations |\n| `createSortableScope()` | Create a shared scope for connected lists | Sync | Each set of connected containers needs its own scope instance |\n| `applyReorder()` | Apply ordered IDs to data arrays | Sync | Unknown IDs are skipped; non-mentioned items are appended |\n| `DropZoneOptions.accept` | Filter file types before processing | Sync | Mismatch between MIME and extension can reject files unexpectedly |\n| `DropZoneOptions.maxFiles` | Cap accepted files per drop | Sync | Excess accepted files become rejected; `onDropRejected` is called |\n| `matchesAccept()` | Test a single `File` against an accept list | Sync | Extension patterns are case-insensitive; empty list accepts all |\n| `DndError` | Base class for Dnd errors | Sync | Use `instanceof DndError` to narrow unknown errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ------------------- | -------------------------------- |\n| `@vielzeug/dnd` | Backward-compat root barrel |\n| `@vielzeug/dnd/drop` | Drop-zone APIs plus `DndError` and `Disposable` |\n| `@vielzeug/dnd/sortable` | Sortable APIs plus `DndError`, `DndScopeError`, and `Disposable` |\n\nDnd has no third-party runtime dependencies. Its sortable touch adapter uses `@vielzeug/gesture` for shared pointer recognition.\n\n## Types\n\n### `Disposable`\n\n```ts\ninterface Disposable {\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n [Symbol.dispose](): void;\n}\n```\n\n### `DropZoneOptions`\n\n```ts\ninterface DropZoneOptions {\n element: HTMLElement;\n accept?: readonly string[];\n maxFiles?: number;\n onValidate?: (files: File[], context: DropValidationContext) => boolean | Promise<boolean>;\n disabled?: boolean;\n dropEffect?: DataTransfer['dropEffect'];\n onDrop?: (files: File[]) => void;\n onDropRejected?: (files: File[]) => void;\n onHoverChange?: (hovered: boolean) => void;\n onValidatingChange?: (validating: boolean) => void;\n paste?: boolean;\n onPaste?: (files: File[]) => void;\n}\n```\n\n### `DropZone`\n\n```ts\ninterface DropZone extends Disposable {\n readonly hovered: boolean;\n readonly validating: boolean;\n}\n```\n\n### `DropValidationContext`\n\n```ts\ninterface DropValidationContext {\n readonly signal: AbortSignal;\n}\n```\n\n### `SortableOptions`\n\n```ts\ninterface SortableOptions {\n element: HTMLElement;\n getKey: (element: HTMLElement) => string;\n scope?: SortableScope;\n handle?: string;\n keyboard?: boolean;\n axis?: 'vertical' | 'horizontal';\n autoScroll?: boolean | AutoScrollOptions;\n dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);\n dragImageOffset?: [number, number];\n placeholderClass?: string;\n disabled?: boolean;\n onDragStart?: (id: string, event: DragEvent) => void;\n onDragEnd?: (id: string, event: DragEvent) => void;\n onInteraction?: (event: SortableInteractionEvent) => void;\n onBeforeReorder?: (from: readonly string[], to: readonly string[]) => void;\n onReorder?: (event: ReorderEvent) => void;\n items?: () => readonly HTMLElement[];\n}\n```\n\n### `AutoScrollOptions`\n\n```ts\ninterface AutoScrollOptions {\n edgeThreshold?: number;\n speed?: number;\n container?: boolean;\n viewport?: boolean;\n}\n```\n\n### `ReorderEvent`\n\n```ts\ninterface ReorderEvent {\n before: readonly string[];\n after: readonly string[];\n item: string;\n}\n```\n\n### `Sortable`\n\n```ts\ninterface Sortable extends Disposable {\n readonly isDragging: boolean;\n refresh(): void;\n}\n```\n\n### `SortableScope`\n\n```ts\ninterface SortableScope extends Disposable {\n readonly isDragging: boolean;\n}\n```\n\n### `SortableScopeOptions`\n\n```ts\ninterface SortableScopeOptions {\n onMove?: (event: SortableMoveEvent) => void;\n touch?: boolean | SortableTouchOptions;\n}\n```\n\n### `SortableMoveEvent`\n\n```ts\ninterface SortableMoveEvent {\n readonly itemId: string;\n readonly source: HTMLElement;\n readonly sourceBeforeIds: readonly string[];\n readonly sourceIds: readonly string[];\n readonly target: HTMLElement;\n readonly targetBeforeIds: readonly string[];\n readonly targetIds: readonly string[];\n}\n```\n\n### `SortableInteractionEvent`\n\nStructured accessibility/observability event for sortable interactions. Drag emits `pickup` on dragstart, `drop` on commit, `cancel` on cancel. Keyboard emits `move` on each successful arrow/Home/End reorder (direct-commit model).\n\n```ts\ntype SortableInteractionEvent =\n | { readonly type: 'pickup'; readonly itemId: string; readonly index: number; readonly total: number }\n | {\n readonly type: 'move';\n readonly itemId: string;\n readonly previousIndex: number;\n readonly index: number;\n readonly total: number;\n }\n | { readonly type: 'drop'; readonly itemId: string; readonly index: number; readonly total: number }\n | { readonly type: 'cancel'; readonly itemId: string; readonly index: number; readonly total: number };\n```\n\nWire a consumer-side announcer to provide screen-reader feedback:\n\n```ts\nimport { createSortable } from '@vielzeug/dnd/sortable';\n\ncreateSortable({\n element: listEl,\n getKey: (el) => el.dataset.id!,\n onInteraction(event) {\n switch (event.type) {\n case 'pickup':\n announce(`Picked up ${event.itemId}, position ${event.index + 1} of ${event.total}`);\n break;\n case 'move':\n announce(`Moved ${event.itemId} to position ${event.index + 1} of ${event.total}`);\n break;\n case 'drop':\n announce(`Dropped ${event.itemId} at position ${event.index + 1} of ${event.total}`);\n break;\n case 'cancel':\n announce(`Cancelled, ${event.itemId} returned to position ${event.index + 1} of ${event.total}`);\n break;\n }\n },\n});\n```\n\n### `SortableTouchOptions`\n\n```ts\ntype SortableTouchOptions = Readonly<{\n activationDistance?: number;\n preview?: false | ((item: HTMLElement) => HTMLElement | null);\n}>\n```\n\n`activationDistance` is the non-negative finite movement threshold in pixels before touch sorting starts (default `6`). Invalid values throw `DndError`.\n\n`preview` returns a template that Dnd clones before mounting it as a transient touch preview, so returning an element from the sortable item does not reparent or remove caller-owned DOM. Return `false` to disable the preview.\n\nTouch sorting delegates one-pointer activation, movement, identity, and cancellation to Gesture's `createDragGesture()` with pointer capture disabled for document hit-testing. Dnd retains preview rendering, hit-testing, synthetic drag events, and sortable transactions. Non-primary pointers are ignored, and cancellation restores the pre-drag order without firing `onReorder`.\n\n## `createDropZone()`\n\n```ts\ndeclare function createDropZone(options: DropZoneOptions): DropZone;\n```\n\nAttaches drag-and-drop file handling to a DOM element. Returns a `DropZone` handle.\n\n| Option | Type | Default | Description |\n| ---------------- | ------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `element` | `HTMLElement` | — | **Required.** The element to attach drag listeners to. |\n| `accept` | `string[]` | `[]` | Accepted file types, normalized and snapshotted at construction. Empty array accepts everything; invalid MIME, wildcard, or extension patterns throw `DndError`. |\n| `maxFiles` | `number` | — | Non-negative safe-integer file limit. Excess files are passed to `onDropRejected`; invalid values throw `DndError` during construction. |\n| `onValidate` | `(files, { signal }) => boolean \\| Promise<boolean>` | — | Optional async gating step. Return or resolve `false` to reject all accepted files. `validating` remains true until every operation settles; `signal` aborts on disposal. |\n| `disabled` | `boolean` | — | When `true`, all drag and paste events are ignored. A disabled zone does not call `preventDefault` on `dragenter`, `dragover`, `drop`, or `paste`, so underlying elements (text editors, etc.) receive them normally. |\n| `dropEffect` | `'copy' \\| 'move' \\| 'link' \\| 'none'` | `'copy'` | The `dropEffect` set on `dataTransfer` during `dragover`. Controls the cursor indicator. |\n| `onDrop` | `(files: File[]) => void` | — | Called with accepted files only. Not called if all dropped files are rejected. Also receives paste events when `paste: true` and `onPaste` is omitted. |\n| `onDropRejected` | `(files: File[]) => void` | — | Called with files that did not match `accept`, exceeded `maxFiles`, or were rejected by `onValidate`. |\n| `onHoverChange` | `(hovered: boolean) => void` | — | Called when hover state toggles. Use this callback for drag-over styling. |\n| `onValidatingChange` | `(validating: boolean) => void` | — | Called whenever the aggregate async validation state changes. |\n| `paste` | `boolean` | `false` | When `true`, attaches a `paste` listener to `element`. Pasted files run through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files. |\n| `onPaste` | `(files: File[]) => void` | — | Called when files are pasted from the clipboard. Falls back to `onDrop` when omitted. Only active when `paste: true`. |\n\n**Returns:** `DropZone`\n\nNotes:\n\n- Extension accept patterns are approximate during pre-check (`DataTransferItem` has no filename); exact filtering is applied at drop time.\n- Hover state (`hovered`) only becomes `true` when the dragged payload passes the `accept` filter. Drags carrying rejected file types enter and leave the zone without triggering `onHoverChange`.\n- Hover state is reset on element drop and also global `window` `drop`/`dragend` to avoid stuck hover state when drags leave the viewport.\n- Disposal aborts validation and transitions `validating` to `false` before teardown completes.\n- Consumer callback failures are reported through Dnd's development warning channel and never reclassify accepted files as rejected.\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n accept: ['image/*', '.pdf'],\n onDrop: (files) => {\n upload(files);\n },\n onDropRejected: (files) => {\n showError(`${files.length} rejected`);\n },\n onHoverChange: (hovered) => {\n dropEl.classList.toggle('drag-over', hovered);\n },\n});\n```\n\n## `DropZone` Interface\n\n### `zone.hovered`\n\n`readonly hovered: boolean`\n\n`true` when a drag is currently over the zone. Updated synchronously by the internal counter — safe to read at any time.\n\n### `zone.validating`\n\n`readonly validating: boolean`\n\n`true` while an `onValidate` promise is pending. Use this to render a loading indicator between file selection and the acceptance/rejection callbacks firing.\n\n```ts\nconsole.log(zone.validating); // true between drop and onValidate resolution\n```\n\n### `zone.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called. Safe to read at any time.\n\n### `zone.disposalSignal`\n\n`readonly disposalSignal: AbortSignal`\n\nAn `AbortSignal` that fires when `dispose()` is called. Use it to cancel in-flight requests tied to the zone's lifetime.\n\n### `zone.dispose()`\n\n`dispose(): void`\n\nRemoves all event listeners, aborts pending validation, and resets `hovered` and `validating`. Idempotent — safe to call multiple times.\n\n```ts\nzone.dispose();\n```\n\n### `zone[Symbol.dispose]()`\n\n`[Symbol.dispose](): void`\n\nAlias for `dispose()`. Called automatically when used with the `using` keyword.\n\n```ts\n{\n using zone = createDropZone({ element: dropEl, onDrop: handleFiles });\n} // zone.dispose() runs here\n```\n\n## `createSortable()`\n\n```ts\ndeclare function createSortable(options: SortableOptions): Sortable;\n```\n\nMakes the direct children of a container element reorderable via drag. Returns a `Sortable` handle.\n\n`createSortable` adds drag and keyboard defaults only when callers have not already supplied semantics. Every changed attribute and inline style is restored to its prior value on disposal.\n\n- `element`: `HTMLElement`, required. The container whose children become sortable.\n- `getKey`: `(element: HTMLElement) => string`, required. Maps each item element to its stable string identity. Children for which `getKey` returns a falsy value are skipped.\n- `scope`: `SortableScope`, default private scope. Connects sortable lists explicitly; containers only exchange items when they share the same scope instance.\n- `handle`: `string`. Valid CSS selector for a drag handle inside each item. Invalid selectors throw `DndError`; an empty selector warns and disables dragging.\n- `keyboard`: `boolean`, default `true`. Enables keyboard reordering with arrow keys plus `Home` and `End`. Events from interactive descendants are ignored unless they match the configured handle.\n- `axis`: `'vertical' | 'horizontal'`, default `'vertical'`. Controls midpoint calculation for placeholder insertion.\n- `autoScroll`: `boolean | AutoScrollOptions`, default `true`. Scrolls near edges; threshold must be finite and non-negative and speed finite and positive.\n- `dragImage`: `HTMLElement | ((id, item, event) => HTMLElement | null | undefined)`. Custom native drag preview passed to `dataTransfer.setDragImage()`. A `null` or `undefined` return skips `setDragImage` entirely.\n- `dragImageOffset`: `[number, number]`, default `[0, 0]`. The `[x, y]` hotspot offset passed to `setDragImage`. Controls which point of the preview image follows the cursor.\n- `placeholderClass`: `string`, default `'dnd-placeholder'`. CSS class applied to the generated placeholder element.\n- `disabled`: `boolean`. Blocks drag interactions. If a list becomes disabled mid-drag, Dnd cancels the drag and restores the original order.\n- `onDragStart`: `(id: string, event: DragEvent) => void`. Called when a drag starts.\n- `onDragEnd`: `(id: string, event: DragEvent) => void`. Called when a drag ends, whether completed or cancelled.\n- `onInteraction`: `(event: SortableInteractionEvent) => void`. Structured accessibility event for pickup/move/drop/cancel — wire to a consumer-side announcer for screen-reader feedback. See [`SortableInteractionEvent`](#sortableinteractionevent).\n- `onBeforeReorder`: `(from: readonly string[], to: readonly string[]) => void`. Called with the before/after order snapshots just before a successful reorder commits — for both drag and keyboard. Items are still in their pre-commit positions at the time of the call, making it ideal for [`captureLayout()`](/necromancer/api.md#capturelayout) setup.\n- `items`: `() => readonly HTMLElement[]`. Returns direct children owned by this sortable. `refresh()` restores elements no longer returned before marking the current set.\n- `onReorder`: `(event: ReorderEvent) => void`. Called after a successful reorder (drag or keyboard), only when the order changed. The event carries `before`, `after`, and `item` so application history can own rollback.\n\nOrder arrays and interaction events are frozen snapshots. Callback failures are reported through Dnd's development warning channel and cannot interrupt session completion or corrupt later events. Native touch scrolling is preserved unless the shared scope enables touch input.\n\n**Returns:** `Sortable`\n\n```ts\nconst boardScope = createSortableScope({\n onMove: ({ itemId, sourceIds, targetIds }) => saveMove(itemId, sourceIds, targetIds),\n touch: true,\n});\n\nconst sortable = createSortable({\n element: listEl,\n getKey: (el) => el.dataset.id!,\n handle: '.drag-handle',\n onDragStart: (id) => {\n listEl.classList.add('sorting');\n },\n onDragEnd: (id) => {\n listEl.classList.remove('sorting');\n },\n onReorder: ({ before, after }) => {\n history.push({ revert: () => saveOrder(before) });\n saveOrder(after);\n },\n scope: boardScope,\n});\n```\n\n### `createSortableScope()`\n\n```ts\ndeclare function createSortableScope(options?: SortableScopeOptions): SortableScope;\n```\n\nUse one scope per connected set of containers. `onMove` fires once for cross-list moves with both final orders; local reorders continue to call the sortable's `onReorder`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `SortableScopeOptions` | Optional cross-list move callback and scope-owned touch configuration |\n\n**Returns:** `SortableScope`.\n\n```ts\nimport { createSortableScope } from '@vielzeug/dnd/sortable';\n\nconst scope = createSortableScope({\n onMove: ({ itemId, sourceIds, targetIds }) => {\n persistMove(itemId, sourceIds, targetIds);\n },\n touch: true,\n});\n```\n\n## `Sortable` Interface\n\n### `sortable.isDragging`\n\n`readonly isDragging: boolean`\n\n`true` while an item drag is in progress.\n\n### `sortable.refresh()`\n\n`refresh(): void`\n\nRe-reads items from the `items` provider (or the container's direct children) and reapplies `draggable`, `role`, and handle attributes. Call it after adding, removing, or replacing sortable children.\n\n### `sortable.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called.\n\n### `sortable.disposalSignal`\n\n`readonly disposalSignal: AbortSignal`\n\nAn `AbortSignal` that fires when `dispose()` is called.\n\n### `sortable.dispose()`\n\n`dispose(): void`\n\nRemoves all event listeners from the container, strips sortable attributes from items and handles, and cancels any in-progress drag by restoring the original order. Idempotent — safe to call multiple times.\n\n### `sortable[Symbol.dispose]()`\n\n`[Symbol.dispose](): void`\n\nAlias for `dispose()`.\n\n## `SortableScope` Interface\n\n### `scope.isDragging`\n\n`readonly isDragging: boolean`\n\n`true` while any sortable registered to the scope is dragging.\n\n### `scope.dispose()`\n\n`dispose(): void`\n\nDisposes scope-owned touch input and all registered sortables. Registering a new sortable with a disposed scope throws `DndScopeError`.\n\n## DOM Attributes\n\nDnd reads and writes the following DOM attributes:\n\n- `data-dnd-item`: internal marker applied by `createSortable` to children that return a truthy key from `getKey`. Restored on `dispose()`.\n- `draggable`, roles, tabindex, and `touchAction`: managed only as needed and restored to their exact prior values on `dispose()`.\n- `data-dragging`: set during drag, removed on `dragend` or `dispose()`. Use it as your styling hook for drag state.\n- `data-dnd-handle`: internal marker set by `createSortable` and `sortable.refresh()`, removed by `dispose()`. Lets Dnd clean up only the handle attributes it applied.\n- `aria-hidden=\"true\"`: set on placeholder creation and removed with the placeholder. Applied to the `.dnd-placeholder` element.\n- `style.touchAction = 'none'` (inline style): set by `createSortable` and `sortable.refresh()` on the item (or the handle, when `handle` is set), then restored on `dispose()`.\n\n## CSS Classes\n\n| Class | Applied to | When |\n| ----------------- | ---------------------------- | ------------------------------------------------------------- |\n| `dnd-placeholder` | `<div>` inserted by sortable | While an item is being dragged, in the placeholder's position |\n\n## `matchesAccept()`\n\n```ts\ndeclare function matchesAccept(file: File, accept: readonly string[]): boolean;\n```\n\nTests whether a `File` matches an accept pattern list. Each pattern can be:\n\n- A MIME type: `'image/png'`\n- A MIME wildcard: `'image/*'`\n- A file extension: `'.pdf'`\n\nAn empty list accepts everything. Extension matching is case-insensitive.\n\n**Returns:** `true` when the file matches at least one pattern, or when `accept` is empty.\n\n```ts\nimport { matchesAccept } from '@vielzeug/dnd/drop';\n\nmatchesAccept(file, ['image/*', '.pdf']); // true or false\n```\n\n## `applyReorder()`\n\n```ts\ndeclare function applyReorder<T>(items: readonly T[], ids: readonly string[], getKey: (item: T) => string): T[];\n```\n\nApplies a DOM reorder result (`orderedIds`) to your backing array.\n\n- IDs missing from `items` are ignored.\n- Items not listed in `ids` are appended in original order.\n- Duplicate IDs in `ids` — first occurrence wins, later occurrences are ignored.\n- Duplicate keys in `items` throw `DndError`; data is never silently discarded.\n\n**Returns:** A new array ordered by `ids`, with omitted items appended in their original order.\n\n```ts\nconst next = applyReorder(items, orderedIds, (item) => item.id);\n```\n\n## Errors\n\n| Error | Trigger | Notable property |\n| --- | --- | --- |\n| `DndError` | Invalid configuration, duplicate item data, or duplicate sortable registration in one scope | Base class for package errors |\n| `DndScopeError` | A sortable receives an invalid or disposed scope | Extends `DndError` |\n",
|
|
5
|
+
"api": "---\ntitle: Dnd — API Reference\ndescription: Complete API reference for Dnd.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------------- | -------------------------------------------- | -------------- | ----------------------------------------------------------------- |\n| `createDropZone()` | Create a typed drop-zone controller | Sync | Dispose the controller during teardown |\n| `createSortable()` | Add sortable drag-and-drop behavior to lists | Sync | Provide stable item identity for reorder operations |\n| `createSortableScope()` | Create a shared scope for connected lists | Sync | Each set of connected containers needs its own scope instance |\n| `applyReorder()` | Apply ordered IDs to data arrays | Sync | Unknown IDs are skipped; non-mentioned items are appended |\n| `DropZoneOptions.accept` | Filter file types before processing | Sync | Mismatch between MIME and extension can reject files unexpectedly |\n| `DropZoneOptions.maxFiles` | Cap accepted files per drop | Sync | Excess accepted files become rejected; `onDropRejected` is called |\n| `matchesAccept()` | Test a single `File` against an accept list | Sync | Extension patterns are case-insensitive; empty list accepts all |\n| `DndError` | Base class for Dnd errors | Sync | Use `instanceof DndError` to narrow unknown errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ------------------- | -------------------------------- |\n| `@vielzeug/dnd` | Backward-compat root barrel |\n| `@vielzeug/dnd/drop` | Drop-zone APIs plus `DndError` and `Disposable` |\n| `@vielzeug/dnd/sortable` | Sortable APIs plus `DndError`, `DndScopeError`, and `Disposable` |\n\nDnd has no third-party runtime dependencies. Its sortable touch adapter uses `@vielzeug/gesture` for shared pointer recognition.\n\n## Types\n\n### `Disposable`\n\n```ts\ninterface Disposable {\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n [Symbol.dispose](): void;\n}\n```\n\n### `DropZoneOptions`\n\n```ts\ninterface DropZoneOptions {\n element: HTMLElement;\n accept?: readonly string[];\n maxFiles?: number;\n onValidate?: (files: File[], context: DropValidationContext) => boolean | Promise<boolean>;\n disabled?: boolean;\n dropEffect?: DataTransfer['dropEffect'];\n onDrop?: (files: File[]) => void;\n onDropRejected?: (files: File[]) => void;\n onHoverChange?: (hovered: boolean) => void;\n onValidatingChange?: (validating: boolean) => void;\n paste?: boolean;\n onPaste?: (files: File[]) => void;\n}\n```\n\n### `DropZone`\n\n```ts\ninterface DropZone extends Disposable {\n readonly hovered: boolean;\n readonly validating: boolean;\n}\n```\n\n### `DropValidationContext`\n\n```ts\ninterface DropValidationContext {\n readonly signal: AbortSignal;\n}\n```\n\n### `SortableOptions`\n\n```ts\ninterface SortableOptions {\n element: HTMLElement;\n getKey: (element: HTMLElement) => string;\n scope?: SortableScope;\n handle?: string;\n keyboard?: boolean;\n axis?: 'vertical' | 'horizontal';\n autoScroll?: boolean | AutoScrollOptions;\n dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);\n dragImageOffset?: [number, number];\n placeholderClass?: string;\n disabled?: boolean;\n onDragStart?: (id: string, event: DragEvent) => void;\n onDragEnd?: (id: string, event: DragEvent) => void;\n onInteraction?: (event: SortableInteractionEvent) => void;\n onBeforeReorder?: (from: readonly string[], to: readonly string[]) => void;\n onReorder?: (event: ReorderEvent) => void;\n items?: () => readonly HTMLElement[];\n}\n```\n\n### `AutoScrollOptions`\n\n```ts\ninterface AutoScrollOptions {\n edgeThreshold?: number;\n speed?: number;\n container?: boolean;\n viewport?: boolean;\n}\n```\n\n### `ReorderEvent`\n\n```ts\ninterface ReorderEvent {\n before: readonly string[];\n after: readonly string[];\n item: string;\n}\n```\n\n### `Sortable`\n\n```ts\ninterface Sortable extends Disposable {\n readonly isDragging: boolean;\n refresh(): void;\n}\n```\n\n### `SortableScope`\n\n```ts\ninterface SortableScope extends Disposable {\n readonly isDragging: boolean;\n}\n```\n\n### `SortableScopeOptions`\n\n```ts\ninterface SortableScopeOptions {\n onMove?: (event: SortableMoveEvent) => void;\n touch?: boolean | SortableTouchOptions;\n}\n```\n\n### `SortableMoveEvent`\n\n```ts\ninterface SortableMoveEvent {\n readonly itemId: string;\n readonly source: HTMLElement;\n readonly sourceBeforeIds: readonly string[];\n readonly sourceIds: readonly string[];\n readonly target: HTMLElement;\n readonly targetBeforeIds: readonly string[];\n readonly targetIds: readonly string[];\n}\n```\n\n### `SortableInteractionEvent`\n\nStructured accessibility/observability event for sortable interactions. Drag emits `pickup` on dragstart, `drop` on commit, `cancel` on cancel. Keyboard emits `move` on each successful arrow/Home/End reorder (direct-commit model).\n\n```ts\ntype SortableInteractionEvent =\n | { readonly type: 'pickup'; readonly itemId: string; readonly index: number; readonly total: number }\n | {\n readonly type: 'move';\n readonly itemId: string;\n readonly previousIndex: number;\n readonly index: number;\n readonly total: number;\n }\n | { readonly type: 'drop'; readonly itemId: string; readonly index: number; readonly total: number }\n | { readonly type: 'cancel'; readonly itemId: string; readonly index: number; readonly total: number };\n```\n\nWire a consumer-side announcer to provide screen-reader feedback:\n\n```ts\nimport { createSortable } from '@vielzeug/dnd/sortable';\n\ncreateSortable({\n element: listEl,\n getKey: (el) => el.dataset.id!,\n onInteraction(event) {\n switch (event.type) {\n case 'pickup':\n announce(`Picked up ${event.itemId}, position ${event.index + 1} of ${event.total}`);\n break;\n case 'move':\n announce(`Moved ${event.itemId} to position ${event.index + 1} of ${event.total}`);\n break;\n case 'drop':\n announce(`Dropped ${event.itemId} at position ${event.index + 1} of ${event.total}`);\n break;\n case 'cancel':\n announce(`Cancelled, ${event.itemId} returned to position ${event.index + 1} of ${event.total}`);\n break;\n }\n },\n});\n```\n\n### `SortableTouchOptions`\n\n```ts\ntype SortableTouchOptions = Readonly<{\n activationDistance?: number;\n preview?: false | ((item: HTMLElement) => HTMLElement | null);\n}>\n```\n\n`activationDistance` is the non-negative finite movement threshold in pixels before touch sorting starts (default `6`). Invalid values throw `DndError`.\n\n`preview` returns a template that Dnd clones before mounting it as a transient touch preview, so returning an element from the sortable item does not reparent or remove caller-owned DOM. Return `false` to disable the preview.\n\nTouch sorting delegates one-pointer activation, movement, identity, and cancellation to Gesture's `createDragGesture()` with pointer capture disabled for document hit-testing. Dnd retains preview rendering, hit-testing, synthetic drag events, and sortable transactions. Non-primary pointers are ignored, and cancellation restores the pre-drag order without firing `onReorder`.\n\n## `createDropZone()`\n\n```ts\ndeclare function createDropZone(options: DropZoneOptions): DropZone;\n```\n\nAttaches drag-and-drop file handling to a DOM element. Returns a `DropZone` handle.\n\n| Option | Type | Default | Description |\n| ---------------- | ------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `element` | `HTMLElement` | — | **Required.** The element to attach drag listeners to. |\n| `accept` | `string[]` | `[]` | Accepted file types, normalized and snapshotted at construction. Empty array accepts everything; invalid MIME, wildcard, or extension patterns throw `DndError`. |\n| `maxFiles` | `number` | — | Non-negative safe-integer file limit. Excess files are passed to `onDropRejected`; invalid values throw `DndError` during construction. |\n| `onValidate` | `(files, { signal }) => boolean \\| Promise<boolean>` | — | Optional async gating step. Return or resolve `false` to reject all accepted files. `validating` remains true until every operation settles; `signal` aborts on disposal. |\n| `disabled` | `boolean` | — | When `true`, all drag and paste events are ignored. A disabled zone does not call `preventDefault` on `dragenter`, `dragover`, `drop`, or `paste`, so underlying elements (text editors, etc.) receive them normally. |\n| `dropEffect` | `'copy' \\| 'move' \\| 'link' \\| 'none'` | `'copy'` | The `dropEffect` set on `dataTransfer` during `dragover`. Controls the cursor indicator. |\n| `onDrop` | `(files: File[]) => void` | — | Called with accepted files only. Not called if all dropped files are rejected. Also receives paste events when `paste: true` and `onPaste` is omitted. |\n| `onDropRejected` | `(files: File[]) => void` | — | Called with files that did not match `accept`, exceeded `maxFiles`, or were rejected by `onValidate`. |\n| `onHoverChange` | `(hovered: boolean) => void` | — | Called when hover state toggles. Use this callback for drag-over styling. |\n| `onValidatingChange` | `(validating: boolean) => void` | — | Called whenever the aggregate async validation state changes. |\n| `paste` | `boolean` | `false` | When `true`, attaches a `paste` listener to `element`. Pasted files run through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files. |\n| `onPaste` | `(files: File[]) => void` | — | Called when files are pasted from the clipboard. Falls back to `onDrop` when omitted. Only active when `paste: true`. |\n\n**Returns:** `DropZone`\n\nNotes:\n\n- Extension accept patterns are approximate during pre-check (`DataTransferItem` has no filename); exact filtering is applied at drop time.\n- Hover state (`hovered`) only becomes `true` when the dragged payload passes the `accept` filter. Drags carrying rejected file types enter and leave the zone without triggering `onHoverChange`.\n- Hover state is reset on element drop and also global `window` `drop`/`dragend` to avoid stuck hover state when drags leave the viewport.\n- Disposal aborts validation and transitions `validating` to `false` before teardown completes.\n- Consumer callback failures are reported through Dnd's development warning channel and never reclassify accepted files as rejected.\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n accept: ['image/*', '.pdf'],\n onDrop: (files) => {\n upload(files);\n },\n onDropRejected: (files) => {\n showError(`${files.length} rejected`);\n },\n onHoverChange: (hovered) => {\n dropEl.classList.toggle('drag-over', hovered);\n },\n});\n```\n\n## `DropZone` Interface\n\n### `zone.hovered`\n\n`readonly hovered: boolean`\n\n`true` when a drag is currently over the zone. Updated synchronously by the internal counter — safe to read at any time.\n\n### `zone.validating`\n\n`readonly validating: boolean`\n\n`true` while an `onValidate` promise is pending. Use this to render a loading indicator between file selection and the acceptance/rejection callbacks firing.\n\n```ts\nconsole.log(zone.validating); // true between drop and onValidate resolution\n```\n\n### `zone.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called. Safe to read at any time.\n\n### `zone.disposalSignal`\n\n`readonly disposalSignal: AbortSignal`\n\nAn `AbortSignal` that fires when `dispose()` is called. Use it to cancel in-flight requests tied to the zone's lifetime.\n\n### `zone.dispose()`\n\n`dispose(): void`\n\nRemoves all event listeners, aborts pending validation, and resets `hovered` and `validating`. Idempotent — safe to call multiple times.\n\n```ts\nzone.dispose();\n```\n\n### `zone[Symbol.dispose]()`\n\n`[Symbol.dispose](): void`\n\nAlias for `dispose()`. Called automatically when used with the `using` keyword.\n\n```ts\n{\n using zone = createDropZone({ element: dropEl, onDrop: handleFiles });\n} // zone.dispose() runs here\n```\n\n## `createSortable()`\n\n```ts\ndeclare function createSortable(options: SortableOptions): Sortable;\n```\n\nMakes the direct children of a container element reorderable via drag. Returns a `Sortable` handle.\n\n`createSortable` adds drag and keyboard defaults only when callers have not already supplied semantics. Every changed attribute and inline style is restored to its prior value on disposal.\n\n- `element`: `HTMLElement`, required. The container whose children become sortable.\n- `getKey`: `(element: HTMLElement) => string`, required. Maps each item element to its stable string identity. Children for which `getKey` returns a falsy value are skipped.\n- `scope`: `SortableScope`, default private scope. Connects sortable lists explicitly; containers only exchange items when they share the same scope instance.\n- `handle`: `string`. Valid CSS selector for a drag handle inside each item. Invalid selectors throw `DndError`; an empty selector warns and disables dragging.\n- `keyboard`: `boolean`, default `true`. Enables keyboard reordering with arrow keys plus `Home` and `End`, matched through `matchKey` from `@vielzeug/keymap` — modifier chords (e.g. Ctrl+ArrowDown, browser back/forward) are ignored. Events from interactive descendants are ignored unless they match the configured handle.\n- `axis`: `'vertical' | 'horizontal'`, default `'vertical'`. Controls midpoint calculation for placeholder insertion.\n- `autoScroll`: `boolean | AutoScrollOptions`, default `true`. Scrolls near edges; threshold must be finite and non-negative and speed finite and positive.\n- `dragImage`: `HTMLElement | ((id, item, event) => HTMLElement | null | undefined)`. Custom native drag preview passed to `dataTransfer.setDragImage()`. A `null` or `undefined` return skips `setDragImage` entirely.\n- `dragImageOffset`: `[number, number]`, default `[0, 0]`. The `[x, y]` hotspot offset passed to `setDragImage`. Controls which point of the preview image follows the cursor.\n- `placeholderClass`: `string`, default `'dnd-placeholder'`. CSS class applied to the generated placeholder element.\n- `disabled`: `boolean`. Blocks drag interactions. If a list becomes disabled mid-drag, Dnd cancels the drag and restores the original order.\n- `onDragStart`: `(id: string, event: DragEvent) => void`. Called when a drag starts.\n- `onDragEnd`: `(id: string, event: DragEvent) => void`. Called when a drag ends, whether completed or cancelled.\n- `onInteraction`: `(event: SortableInteractionEvent) => void`. Structured accessibility event for pickup/move/drop/cancel — wire to a consumer-side announcer for screen-reader feedback. See [`SortableInteractionEvent`](#sortableinteractionevent).\n- `onBeforeReorder`: `(from: readonly string[], to: readonly string[]) => void`. Called with the before/after order snapshots just before a successful reorder commits — for both drag and keyboard. Items are still in their pre-commit positions at the time of the call, making it ideal for [`captureLayout()`](/necromancer/api.md#capturelayout) setup.\n- `items`: `() => readonly HTMLElement[]`. Returns direct children owned by this sortable. `refresh()` restores elements no longer returned before marking the current set.\n- `onReorder`: `(event: ReorderEvent) => void`. Called after a successful reorder (drag or keyboard), only when the order changed. The event carries `before`, `after`, and `item` so application history can own rollback.\n\nOrder arrays and interaction events are frozen snapshots. Callback failures are reported through Dnd's development warning channel and cannot interrupt session completion or corrupt later events. Native touch scrolling is preserved unless the shared scope enables touch input.\n\n**Returns:** `Sortable`\n\n```ts\nconst boardScope = createSortableScope({\n onMove: ({ itemId, sourceIds, targetIds }) => saveMove(itemId, sourceIds, targetIds),\n touch: true,\n});\n\nconst sortable = createSortable({\n element: listEl,\n getKey: (el) => el.dataset.id!,\n handle: '.drag-handle',\n onDragStart: (id) => {\n listEl.classList.add('sorting');\n },\n onDragEnd: (id) => {\n listEl.classList.remove('sorting');\n },\n onReorder: ({ before, after }) => {\n history.push({ revert: () => saveOrder(before) });\n saveOrder(after);\n },\n scope: boardScope,\n});\n```\n\n### `createSortableScope()`\n\n```ts\ndeclare function createSortableScope(options?: SortableScopeOptions): SortableScope;\n```\n\nUse one scope per connected set of containers. `onMove` fires once for cross-list moves with both final orders; local reorders continue to call the sortable's `onReorder`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `SortableScopeOptions` | Optional cross-list move callback and scope-owned touch configuration |\n\n**Returns:** `SortableScope`.\n\n```ts\nimport { createSortableScope } from '@vielzeug/dnd/sortable';\n\nconst scope = createSortableScope({\n onMove: ({ itemId, sourceIds, targetIds }) => {\n persistMove(itemId, sourceIds, targetIds);\n },\n touch: true,\n});\n```\n\n## `Sortable` Interface\n\n### `sortable.isDragging`\n\n`readonly isDragging: boolean`\n\n`true` while an item drag is in progress.\n\n### `sortable.refresh()`\n\n`refresh(): void`\n\nRe-reads items from the `items` provider (or the container's direct children) and reapplies `draggable`, `role`, and handle attributes. Call it after adding, removing, or replacing sortable children.\n\n### `sortable.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called.\n\n### `sortable.disposalSignal`\n\n`readonly disposalSignal: AbortSignal`\n\nAn `AbortSignal` that fires when `dispose()` is called.\n\n### `sortable.dispose()`\n\n`dispose(): void`\n\nRemoves all event listeners from the container, strips sortable attributes from items and handles, and cancels any in-progress drag by restoring the original order. Idempotent — safe to call multiple times.\n\n### `sortable[Symbol.dispose]()`\n\n`[Symbol.dispose](): void`\n\nAlias for `dispose()`.\n\n## `SortableScope` Interface\n\n### `scope.isDragging`\n\n`readonly isDragging: boolean`\n\n`true` while any sortable registered to the scope is dragging.\n\n### `scope.dispose()`\n\n`dispose(): void`\n\nDisposes scope-owned touch input and all registered sortables. Registering a new sortable with a disposed scope throws `DndScopeError`.\n\n## DOM Attributes\n\nDnd reads and writes the following DOM attributes:\n\n- `data-dnd-item`: internal marker applied by `createSortable` to children that return a truthy key from `getKey`. Restored on `dispose()`.\n- `draggable`, roles, tabindex, and `touchAction`: managed only as needed and restored to their exact prior values on `dispose()`.\n- `data-dragging`: set during drag, removed on `dragend` or `dispose()`. Use it as your styling hook for drag state.\n- `data-dnd-handle`: internal marker set by `createSortable` and `sortable.refresh()`, removed by `dispose()`. Lets Dnd clean up only the handle attributes it applied.\n- `aria-hidden=\"true\"`: set on placeholder creation and removed with the placeholder. Applied to the `.dnd-placeholder` element.\n- `style.touchAction = 'none'` (inline style): set by `createSortable` and `sortable.refresh()` on the item (or the handle, when `handle` is set), then restored on `dispose()`.\n\n## CSS Classes\n\n| Class | Applied to | When |\n| ----------------- | ---------------------------- | ------------------------------------------------------------- |\n| `dnd-placeholder` | `<div>` inserted by sortable | While an item is being dragged, in the placeholder's position |\n\n## `matchesAccept()`\n\n```ts\ndeclare function matchesAccept(file: File, accept: readonly string[]): boolean;\n```\n\nTests whether a `File` matches an accept pattern list. Each pattern can be:\n\n- A MIME type: `'image/png'`\n- A MIME wildcard: `'image/*'`\n- A file extension: `'.pdf'`\n\nAn empty list accepts everything. Extension matching is case-insensitive.\n\n**Returns:** `true` when the file matches at least one pattern, or when `accept` is empty.\n\n```ts\nimport { matchesAccept } from '@vielzeug/dnd/drop';\n\nmatchesAccept(file, ['image/*', '.pdf']); // true or false\n```\n\n## `applyReorder()`\n\n```ts\ndeclare function applyReorder<T>(items: readonly T[], ids: readonly string[], getKey: (item: T) => string): T[];\n```\n\nApplies a DOM reorder result (`orderedIds`) to your backing array.\n\n- IDs missing from `items` are ignored.\n- Items not listed in `ids` are appended in original order.\n- Duplicate IDs in `ids` — first occurrence wins, later occurrences are ignored.\n- Duplicate keys in `items` throw `DndError`; data is never silently discarded.\n\n**Returns:** A new array ordered by `ids`, with omitted items appended in their original order.\n\n```ts\nconst next = applyReorder(items, orderedIds, (item) => item.id);\n```\n\n## Errors\n\n| Error | Trigger | Notable property |\n| --- | --- | --- |\n| `DndError` | Invalid configuration, duplicate item data, or duplicate sortable registration in one scope | Base class for package errors |\n| `DndScopeError` | A sortable receives an invalid or disposed scope | Extends `DndError` |\n",
|
|
6
6
|
"usage": "---\ntitle: Dnd — Usage Guide\ndescription: Drop zones, sortable lists, explicit connected scopes, keyboard sorting, and cleanup patterns with Dnd.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`createDropZone` attaches drag-and-drop behavior to any DOM element and keeps hover state stable with a counter.\n\n```ts\nimport { createDropZone } from '@vielzeug/dnd/drop';\n\nconst dropzone = document.getElementById('dropzone')!;\n\nconst zone = createDropZone({\n element: dropzone,\n onDrop: (files) => {\n console.log('Accepted files:', files);\n },\n});\n```\n\n### Accept filtering\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n accept: ['image/*', '.pdf', 'application/json'],\n onDrop: (files) => {\n // accepted files only\n },\n onDropRejected: (files) => {\n showToast(`${files.length} file(s) not accepted`);\n },\n});\n```\n\nThe `accept` list is normalized and snapshotted at construction. MIME matching is case-insensitive; invalid patterns throw `DndError`. Recreate the zone to change accepted types.\n\n### Hover state\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n onHoverChange: (hovered) => {\n dropEl.classList.toggle('drag-over', hovered);\n },\n});\n```\n\nRead zone state imperatively:\n\n```ts\nconsole.log(zone.hovered);\nconsole.log(zone.validating);\n```\n\n### Drop effect\n\n```ts\ncreateDropZone({\n element: dropEl,\n dropEffect: 'move',\n onDrop: (files) => {\n // ...\n },\n});\n```\n\n### Disabled state\n\n```ts\nconst options = { disabled: false, element: dropEl, onDrop: handleFiles };\nconst zone = createDropZone(options);\n\n// options.disabled is read live on each event — mutate to toggle:\noptions.disabled = isReadOnly;\n```\n\n### File limit\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n accept: ['image/*'],\n maxFiles: 5,\n onDrop: (files) => {\n // 1-5 accepted files\n },\n onDropRejected: (files) => {\n showToast(`Only 5 files at a time. ${files.length} were ignored.`);\n },\n});\n```\n\n### Cleanup\n\n```ts\nzone.dispose();\n// or:\nusing zone = createDropZone({ element: dropEl, onDrop: handleFiles });\n```\n\n`maxFiles` must be a non-negative safe integer.\n\n### Async validation\n\nGate drops behind an async check with `onValidate`. The zone remains `validating: true` until every pending validation settles, and disposal aborts each validation signal.\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n accept: ['image/*'],\n onValidate: async (files, { signal }) => {\n const ok = await checkServerQuota(files, { signal });\n return ok; // false → all files forwarded to onDropRejected\n },\n onDrop: (files) => uploadFiles(files),\n onDropRejected: (files) => showError('Quota exceeded'),\n});\n\n// show a spinner while checking\nconsole.log(zone.validating); // true during pending check\n```\n\nA synchronous boolean return skips the microtask queue entirely:\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n onValidate: (files) => files.every((f) => f.size < 5_000_000), // sync\n onDrop: handleFiles,\n});\n```\n\n### Clipboard paste\n\nSet `paste: true` to accept files pasted through the drop-zone element. The focused paste target must be the element or its descendant; the same `accept`, `maxFiles`, and `onValidate` pipeline applies.\n\n```ts\nconst zone = createDropZone({\n element: dropEl,\n paste: true,\n accept: ['image/*'],\n onPaste: (files) => {\n uploadFiles(files);\n },\n onDropRejected: (files) => {\n showError(`${files.length} file(s) not accepted`);\n },\n});\n```\n\nWhen `onPaste` is omitted, accepted pasted files fall through to `onDrop`.\n\n## Sortable\n\n`createSortable` makes direct children of a container reorderable via drag.\n\n### Setup\n\n```html\n<ul id=\"task-list\">\n <li data-sort-id=\"task-1\">Design</li>\n <li data-sort-id=\"task-2\">Develop</li>\n <li data-sort-id=\"task-3\">Review</li>\n</ul>\n```\n\n```ts\nconst sortable = createSortable({\n element: document.getElementById('task-list')!,\n getKey: (el) => el.dataset.sortId!,\n axis: 'vertical',\n onReorder: ({ after }) => {\n saveTaskOrder(after);\n },\n});\n```\n\nDnd automatically sets:\n\n- `draggable=\"true\"` on sortable nodes (or handles)\n- `role=\"listitem\"` on each item\n- `role=\"list\"` on the container\n- `tabindex=\"0\"` on each item for keyboard reordering\n\n### Drag handles\n\n```ts\ncreateSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n handle: '.drag-handle',\n onReorder: ({ after }) => saveOrder(after),\n});\n```\n\n### Keyboard reordering\n\nFocus an item and use arrow keys to move it. `Home` and `End` move to the boundary positions.\n\nWhen an item is already at the first or last position, the boundary key press is not consumed — the browser handles it normally (for example, scrolling the page). Only keys that actually move an item call `preventDefault`. Arrow keys from buttons, links, form fields, and editable descendants remain native unless that descendant matches the configured drag handle. Wire `onInteraction` to an application live region for screen-reader announcements.\n\n### Connected lists\n\nCreate a shared scope when items should move between containers:\n\n```ts\nconst boardScope = createSortableScope({\n onMove: ({ itemId, sourceIds, targetIds }) => {\n persistMove(itemId, sourceIds, targetIds);\n },\n touch: true,\n});\n\ncreateSortable({\n element: todoEl,\n getKey: (el) => el.dataset.sortId!,\n scope: boardScope,\n});\ncreateSortable({\n element: doneEl,\n getKey: (el) => el.dataset.sortId!,\n scope: boardScope,\n});\n```\n\n### Auto-scroll and drag preview\n\n```ts\ncreateSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n autoScroll: { edgeThreshold: 40, speed: 24, viewport: true },\n dragImage: (id, item) => item,\n dragImageOffset: [8, 8],\n});\n```\n\nViewport scrolling is opt-in. Container scrolling stays enabled by default.\n\n### Lifecycle hooks\n\n```ts\ncreateSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onDragStart: (id) => {\n listEl.classList.add('sorting');\n },\n onDragEnd: (id) => {\n listEl.classList.remove('sorting');\n },\n onReorder: ({ after }) => saveOrder(after),\n});\n```\n\n### Custom identity function\n\n```ts\ncreateSortable({\n element: listEl,\n getKey: (el) => el.getAttribute('data-id')!,\n onReorder: ({ after }) => saveOrder(after),\n});\n```\n\n### Dynamic lists\n\nCall `sortable.refresh()` after adding, removing, or replacing sortable items. It restores Dnd-managed semantics on elements no longer returned by `items()` before marking the current direct children.\n\n```ts\nconst item = document.createElement('li');\nitem.dataset.sortId = 'task-4';\nitem.textContent = 'Deploy';\nlistEl.appendChild(item);\nsortable.refresh();\n```\n\n### Disabled state\n\n```ts\nimport { createSortable, type SortableOptions } from '@vielzeug/dnd/sortable';\n\nconst options: SortableOptions = {\n disabled: false,\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => saveOrder(after),\n};\nconst sortable = createSortable(options);\n\n// options.disabled is read live on each event — mutate to toggle:\noptions.disabled = isLocked;\n```\n\n### Placeholder styling\n\n```css\n.dnd-placeholder {\n background: var(--color-primary-50);\n border: 2px dashed var(--color-primary-300);\n border-radius: 4px;\n box-sizing: border-box;\n}\n\n[data-dragging] {\n opacity: 0.35;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n```\n\n### Mapping DOM order back to data\n\n```ts\nimport { applyReorder, createSortable } from '@vielzeug/dnd/sortable';\n\nlet items = [\n { id: 'task-1', title: 'Design' },\n { id: 'task-2', title: 'Develop' },\n { id: 'task-3', title: 'Review' },\n];\n\ncreateSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => {\n items = applyReorder(items, after, (item) => item.id);\n },\n});\n```\n\n### Cleanup\n\n```ts\nsortable.dispose();\n// or:\nusing sortable = createSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => saveOrder(after),\n});\n```\n\n### FLIP animation hook\n\n`onBeforeReorder` fires just before the DOM reorder commits, for both drag and keyboard moves. Pair it with [`captureLayout()`](/necromancer/api.md#capturelayout) to animate the resulting layout without managing rectangles, transforms, or animation frames yourself.\n\n```ts\nimport { captureLayout, type LayoutTransition } from '@vielzeug/necromancer';\n\nlet layout: LayoutTransition<HTMLElement> | undefined;\n\nconst sortable = createSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onBeforeReorder: () => {\n layout = captureLayout(listEl.querySelectorAll<HTMLElement>('[data-sort-id]'), {\n getKey: (el) => el.dataset.sortId!,\n });\n },\n onReorder: ({ after }) => {\n saveOrder(after); // Commit a framework render here when needed.\n layout?.animate({\n duration: 200,\n easing: 'ease-out',\n elements: listEl.querySelectorAll('[data-sort-id]'),\n });\n layout = undefined;\n },\n});\n```\n\nIf `saveOrder()` triggers a render that replaces list items, call `layout?.animate({ elements: committedItems })` after that render commits. When DnD's own reordered elements remain in the DOM, call `layout?.animate()` directly. Dnd does not depend on Necromancer: the application installs it only when FLIP animation is required. Dnd's sole runtime dependency is Gesture for touch pointer recognition.\n\n### Optimistic updates and rollback\n\nThe `onReorder` event carries `before`, `after`, and `item`. Application history owns rollback — use `before` to record an undo entry.\n\n```ts\nconst history: Array<{ before: readonly string[] }> = [];\n\nconst sortable = createSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ before, after }) => {\n history.push({ before });\n setOrder(after); // optimistic update\n },\n});\n\n// On server error:\ntry {\n await api.saveOrder(currentOrder);\n} catch {\n const entry = history.pop();\n if (entry) setOrder(entry.before);\n}\n```\n\n## Touch Support\n\nThe HTML Drag and Drop API has no reliable native touch path. Enable touch on a sortable scope; it only recognizes items registered to that scope, never unrelated `draggable` elements.\n\n```ts\nimport { createSortable, createSortableScope } from '@vielzeug/dnd/sortable';\n\nusing scope = createSortableScope({ touch: true });\nusing sortable = createSortable({ element: listEl, getKey: (el) => el.dataset.id!, scope });\n```\n\nGesture tracks the primary touch pointer for the active drag. Additional contacts cannot move, finish, or replace it. Pointer cancellation restores the original item order and removes the transient preview.\n\n### Touch preview\n\nTouch uses an inert outline by default, avoiding cloned application DOM. Provide a preview factory or opt out when your item styling supplies its own feedback.\n\n```ts\nconst scope = createSortableScope({\n touch: {\n activationDistance: 8,\n // The returned element is cloned before Dnd mounts it as a transient preview.\n preview: (item) => item.querySelector<HTMLElement>('.drag-preview'),\n },\n});\n```\n\n### Why touch-enabled items get `touch-action: none`\n\nWhen a sortable scope enables touch input, `createSortable` sets `touch-action: none` on the managed item or handle. This prevents a mobile browser from claiming the gesture as page scrolling before the scope controller starts the drag. Sortables without touch input preserve native touch scrolling.\n\n## Testing\n\nTest observable callbacks and controller state with your DOM test runner. Construct the zone in each test, dispatch a real `drop` event, then dispose it during teardown.\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { createDropZone } from '@vielzeug/dnd/drop';\n\nconst zones: Array<{ dispose(): void }> = [];\n\nafterEach(() => zones.splice(0).forEach((zone) => zone.dispose()));\n\nit('forwards accepted files', async () => {\n const element = document.createElement('div');\n const onDrop = vi.fn();\n const zone = createDropZone({ element, onDrop });\n zones.push(zone);\n const file = new File(['content'], 'readme.txt', { type: 'text/plain' });\n const event = new Event('drop') as DragEvent;\n\n Object.defineProperty(event, 'dataTransfer', { value: { files: [file] } });\n element.dispatchEvent(event);\n\n await Promise.resolve();\n\n expect(onDrop).toHaveBeenCalledWith([file]);\n expect(zone.disposed).toBe(false);\n});\n```\n\n## Framework Integration\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createSortable, applyReorder } from '@vielzeug/dnd/sortable';\n\nfunction SortableList({ initialItems }: { initialItems: { id: string; text: string }[] }) {\n const listRef = useRef<HTMLUListElement>(null);\n const items = useRef(initialItems);\n\n useEffect(() => {\n const sortable = createSortable({\n element: listRef.current!,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => {\n items.current = applyReorder(items.current, after, (i) => i.id);\n },\n });\n return () => sortable.dispose();\n }, []);\n\n return (\n <ul ref={listRef}>\n {initialItems.map((item) => (\n <li key={item.id} data-sort-id={item.id}>\n {item.text}\n </li>\n ))}\n </ul>\n );\n}\n```\n\n```ts [Vue 3]\nimport { ref, onMounted, onUnmounted } from 'vue';\nimport { createSortable, applyReorder, type Sortable } from '@vielzeug/dnd/sortable';\n\nfunction useSortable(items: { id: string; text: string }[]) {\n const listRef = ref<HTMLElement | null>(null);\n const orderedItems = ref(items);\n let sortable: Sortable | null = null;\n\n onMounted(() => {\n sortable = createSortable({\n element: listRef.value!,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => {\n orderedItems.value = applyReorder(orderedItems.value, after, (i) => i.id);\n },\n });\n });\n\n onUnmounted(() => sortable?.dispose());\n return { listRef, orderedItems };\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createSortable, applyReorder } from '@vielzeug/dnd/sortable';\n\n export let initialItems: { id: string; text: string }[] = [];\n let items = initialItems;\n let listEl: HTMLUListElement;\n\n onMount(() => {\n const sortable = createSortable({\n element: listEl,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => { items = applyReorder(items, after, (i) => i.id); },\n });\n return () => sortable.dispose();\n });\n</script>\n\n<ul bind:this={listEl}>\n {#each items as item (item.id)}\n <li data-sort-id={item.id}>{item.text}</li>\n {/each}\n</ul>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Ore\n\nUse Dnd in custom web components by attaching behavior in component lifecycle hooks.\n\n```ts\nimport { createSortable } from '@vielzeug/dnd/sortable';\nimport { define, getHost, html, onMounted } from '@vielzeug/ore';\n\ndefine('task-list', {\n setup(_props) {\n const el = getHost();\n\n onMounted(() => {\n const sortable = createSortable({\n element: el,\n getKey: (el) => el.dataset.sortId!,\n onReorder: ({ after }) => save(after),\n });\n return () => sortable.dispose();\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\n### With Gesture\n\nKeep direct manipulation separate from drag-and-drop semantics. Use Gesture for one-axis surfaces that follow the pointer and let application code decide the outcome; use Dnd when an item enters a drag lifecycle and is dropped into a zone or reordered collection. Dnd touch sorting uses Gesture's two-dimensional pointer recognizer, then adds previews, hit-testing, synthetic drag events, and sortable transactions. Direct-manipulation consumers use Gesture without Dnd.\n\n## Best Practices\n\n- Attach `createDropZone` and `createSortable` after the container element is in the DOM — use `onMounted` in component frameworks.\n- Call `.dispose()` in the cleanup phase of your framework (useEffect return, onUnmounted, onDestroy) to prevent memory leaks.\n- Use `data-sort-id` attributes that match your data's identity field — do not use DOM index as an identifier.\n- Prefer `applyReorder()` over manual array splicing, and guarantee unique backing keys; duplicates throw `DndError` rather than dropping data.\n- Use `createSortableScope()` only when items should genuinely move between containers.\n- Use drag handles (`.handle` selector) when the full item surface area conflicts with other interactions such as text selection.\n- Test keyboard reordering explicitly — Dnd sets `tabindex` on items and supports arrow keys by default.\n- Enable `touch: true` only on scopes that own touch-sortable lists.\n",
|
|
7
7
|
"examples": "---\ntitle: Dnd — Examples\ndescription: Practical examples and recipes for dnd.\n---\n\n## Examples\n\n- [Sortable List](./examples/sortable-list.md)\n- [Touch-Enabled Sortable List](./examples/touch-enabled-sortable-list.md)\n- [File Upload Drop Zone](./examples/file-upload-drop-zone.md)\n- [Optimistic Reorder with Revert and FLIP Animation](./examples/optimistic-reorder-with-revert.md)\n- [Combined Sortable With Inline Editing](./examples/combined-sortable-with-inline-editing.md)\n- [Connected Kanban Keyboard Sorting](./examples/connected-kanban-keyboard-sorting.md)\n- [Web Component With Ore](./examples/web-component-with-craft.md)\n- [Using `using` for scoped cleanup](./examples/using-using-for-scoped-cleanup.md)\n"
|
|
8
8
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export
|
|
2
|
+
"apiSource": "export { type BatchOptions, runBatch } from './_pool.js';\nexport {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';\nexport type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';\nexport { createStreamWorker, createWorker } from './worker.js';\n",
|
|
3
3
|
"docs": {
|
|
4
4
|
"index": "---\ntitle: Familiar — Typed module-worker pools\ndescription: Typed ES module Worker pools with cancellation, priority scheduling, streaming, and test utilities.\npackage: familiar\ncategory: workers\nkeywords: [web-workers, module-workers, pool, concurrency, timeout, cancellation, streaming]\nrelated: [ripple, herald]\nexports: [createWorker, createStreamWorker, runBatch, FamiliarError, FamiliarInvalidOptionsError, FamiliarTimeoutError, FamiliarTaskError, FamiliarQueueFullError, FamiliarTerminatedError, FamiliarRuntimeError]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"familiar\" />\n\n## Why Familiar?\n\nRaw workers force every application to maintain its own message contract, lifecycle, cancellation, and pool scheduler. Familiar provides those boundaries while keeping worker code in normal typed ES modules.\n\n```ts\n// Before\nconst worker = new Worker(new URL('./sum.worker.ts', import.meta.url), { type: 'module' });\nworker.postMessage([1, 2, 3]);\n\n// After\nconst pool = createWorker<number[], number>(new URL('./sum.worker.ts', import.meta.url));\nawait pool.run([1, 2, 3]);\n```\n\n| Feature | Familiar | Raw Worker | Comlink |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"familiar\" type=\"size\" /> | built-in | ~2 kB |\n| Module-worker contract | <ore-icon name=\"check\" size=\"16\"></ore-icon> | manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Pool scheduling | <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| AbortSignal cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | manual | manual |\n| Versioned protocol | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | implementation-specific |\n| Zero 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\n<div class=\"decision-callout\">\n\n**Use Familiar when** worker jobs need bounded concurrency, typed errors, cancellation, or queue policy.\n\n**Consider raw Worker when** one isolated worker and custom messaging are enough.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/familiar\n```\n\n```sh [npm]\nnpm install @vielzeug/familiar\n```\n\n```sh [yarn]\nyarn add @vielzeug/familiar\n```\n\n:::\n\n## Quick Start\n\nRegister task logic inside a worker module.\n\n```ts\n// double.worker.ts\nimport { exposeTask } from '@vielzeug/familiar/protocol';\n\nexposeTask((value: number) => value * 2);\n```\n\nCreate pool from module URL and dispose it after use.\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst worker = createWorker<number, number>(new URL('./double.worker.ts', import.meta.url));\n\ntry {\n console.log(await worker.run(21));\n} finally {\n worker.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createWorker()` — versioned task protocol over ES module workers\n- `createStreamWorker()` — stream-only worker capability\n- `run()` — validated priority scheduling, transferables, timeout, and cancellation\n- `runBatch()` — ordered progressive results with shared fail-fast cancellation\n- `stats` — active, queued, completed, and failed counters\n- `createTestWorker()` — faithful in-process task-pool testing\n- `dispose()` and `drain()` — immediate or draining teardown, with `using` support\n- Application-owned promise composition — domain task groups without pool-owned orchestration state\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — expose worker results through reactive state.\n- [Herald](/herald/) — publish application events after worker jobs settle.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Familiar — API Reference\ndescription: API reference for module-worker pools and worker-side protocol registration.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createWorker()` | Create single-result module-worker pool | Sync | Worker must call `exposeTask()` |\n| `createStreamWorker()` | Create stream-only module-worker pool | Sync | Worker must call `exposeStream()` |\n| `runBatch()` | Yield ordered results with shared cancellation | Async iterable | Transferables need a per-input selector |\n| `createTestWorker()` | Create an in-process task-pool test double | Sync | Task modules are not executed |\n| `exposeTask()` | Register worker task handler | Sync | Worker-only import |\n| `exposeStream()` | Register worker stream handler | Sync | Worker-only import |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/familiar` | Pool factories, types, and errors |\n| `@vielzeug/familiar/protocol` | Versioned worker protocol and registration helpers |\n| `@vielzeug/familiar/testing` | Task-pool testing adapter |\n\n## Pool Factories\n\n### `createWorker()`\n\n```ts\nfunction createWorker<TInput, TOutput>(url: URL | string, options?: WorkerOptions): WorkerPool<TInput, TOutput>;\n```\n\nCreates a task pool for a worker module registered with `exposeTask()`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `URL \\| string` | Module-worker URL, usually `new URL('./task.worker.ts', import.meta.url)` |\n| `options` | `WorkerOptions` | Pool concurrency, queue, timeout, and worker-error policy |\n\n**Returns:** `WorkerPool<TInput, TOutput>`.\n\n**Example:**\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker<number, number>(new URL('./double.worker.ts', import.meta.url));\n\ntry {\n console.log(await pool.run(21));\n} finally {\n pool.dispose();\n}\n```\n\n### `createStreamWorker()`\n\n```ts\nfunction createStreamWorker<TInput, TChunk>(url: URL | string, options?: WorkerOptions): StreamWorkerPool<TInput, TChunk>;\n```\n\nCreates a stream-only pool for a worker module registered with `exposeStream()`.\n\n**Returns:** `StreamWorkerPool<TInput, TChunk>`.\n\n### `runBatch()`\n\n```ts\nfunction runBatch<TInput, TOutput>(\n pool: WorkerPool<TInput, TOutput>,\n inputs: readonly TInput[],\n options?: BatchOptions<TInput>,\n): AsyncIterable<TOutput>;\n```\n\nStarts related pool tasks concurrently and yields results in input order. The first failure aborts siblings, early iterator exit cancels unfinished work, and cleanup waits for every task to settle. The iterable is one-shot once consumption begins; an unused iterator may be discarded.\n\nUse `getTransferables(input, index)` to return a fresh transfer list for each task. A shared transfer list is intentionally unsupported because transferred values can be detached only once.\n\n**Returns:** `AsyncIterable<TOutput>`.\n\n## Testing\n\n### `createTestWorker()`\n\n```ts\nfunction createTestWorker<TInput, TOutput>(\n handler: (input: TInput) => TOutput | Promise<TOutput>,\n options?: TestWorkerOptions,\n): TestWorkerHandle<TInput, TOutput>;\n```\n\nCreates an in-process task-pool double. It structured-clones values, records settlement, and matches pool timeout and cancellation results without loading a worker module. Cancellation rejects the test task but cannot stop side effects inside an already-running in-process handler.\n\n**Returns:** `TestWorkerHandle<TInput, TOutput>`.\n\n## Worker Protocol\n\n### `exposeTask()`\n\n```ts\nfunction exposeTask<TInput, TOutput>(handler: TaskHandler<TInput, TOutput>): void;\n```\n\nRegisters one single-result handler in a module worker.\n\n### `exposeStream()`\n\n```ts\nfunction exposeStream<TInput, TChunk>(handler: StreamHandler<TInput, TChunk>): void;\n```\n\nRegisters one chunk-producing handler in a module worker.\n\n### `PROTOCOL_VERSION`\n\n```ts\nconst PROTOCOL_VERSION: 1;\n```\n\nVersion included in every host request and worker response. Identifiers must be non-negative safe integers. Task/stream capability mismatches return a protocol-category error that maps to `FamiliarRuntimeError`; malformed host responses reject active work with the same class.\n\n## Types\n\n### `BatchOptions`\n\n```ts\ntype BatchOptions<TInput> = Pick<RunOptions, 'priority' | 'signal' | 'timeout'> & {\n getTransferables?: (input: TInput, index: number) => readonly Transferable[];\n};\n```\n\n### `WorkerOptions`\n\n```ts\ntype WorkerOptions = {\n concurrency?: number | 'auto';\n maxQueue?: number;\n onFull?: 'reject' | 'wait';\n timeout?: number;\n onSlotError?: (error: FamiliarRuntimeError) => void;\n};\n```\n\n`concurrency` is limited to 512; `\"auto\"` clamps the reported hardware concurrency to that limit. `maxQueue` must be a positive integer and `onFull` must be `\"reject\"` or `\"wait\"`. Timeout values must be integer milliseconds from 1 through 2,147,483,647. `onSlotError` runs after active work settles, and callback failures cannot interrupt slot replacement.\n\n### `RunOptions`\n\n```ts\ntype RunOptions = {\n priority?: number;\n signal?: AbortSignal;\n timeout?: number;\n transferables?: readonly Transferable[];\n};\n```\n\n`signal` cancels capacity waits, queued work, and executing work. Executing cancellation terminates and replaces its worker slot without incrementing `failed`. `priority` must be finite and timeout values use the same bounds as pool defaults. The transfer list is copied at submission. Queued inputs remain caller-owned until a slot dispatches them, so do not mutate input objects while work is queued.\n\n### `WorkerPool`\n\n```ts\ninterface WorkerPool<TInput, TOutput> {\n [Symbol.asyncDispose](): Promise<void>;\n [Symbol.dispose](): void;\n run(input: TInput, options?: RunOptions): Promise<TOutput>;\n drain(options?: DrainOptions): Promise<void>;\n dispose(): void;\n readonly stats: WorkerStats;\n readonly status: WorkerStatus;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n}\n```\n\n### `StreamWorkerPool`\n\n```ts\ninterface StreamWorkerPool<TInput, TChunk> {\n [Symbol.asyncDispose](): Promise<void>;\n [Symbol.dispose](): void;\n runStream(input: TInput, options?: RunOptions): AsyncIterable<TChunk>;\n drain(options?: DrainOptions): Promise<void>;\n dispose(): void;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n readonly stats: WorkerStats;\n readonly status: WorkerStatus;\n}\n```\n\n`runStream()` returns a one-shot iterable. Consumption begins on the first iterator operation, so an unused iterator can be discarded. A second active iterator rejects with `FamiliarRuntimeError`, and `return()` immediately cancels a pending chunk request. Caller abort listeners are owned by the active iterator rather than an unconsumed iterable. Incoming chunks are buffered without protocol backpressure.\n\n### `WorkerStats`\n\n```ts\ntype WorkerStats = {\n readonly active: number;\n readonly completed: number;\n readonly failed: number;\n readonly queued: number;\n};\n```\n\n`queued` includes admitted queue entries and calls waiting for queue capacity. Cancellation is not counted as failure even when a caller supplies a custom abort reason.\n\n### `RunningStream`\n\n```ts\ntype RunningStream<TChunk> = {\n done: Promise<void>;\n iterable: AsyncIterable<TChunk>;\n};\n```\n\n### `WorkerStatus`\n\n```ts\ntype WorkerStatus = 'idle' | 'running' | 'terminated';\n```\n\n### `DrainOptions`\n\n```ts\ntype DrainOptions = {\n timeout?: number;\n};\n```\n\n`drain()` accepts no argument for an unbounded graceful drain. A supplied timeout must be an integer from 1 through 2,147,483,647; expiry disposes the pool and rejects with `FamiliarTimeoutError`.\n\n### `TestWorkerOptions`\n\n```ts\ntype TestWorkerOptions = Omit<WorkerOptions, 'concurrency' | 'onSlotError'> & {\n concurrency?: number;\n};\n```\n\n### `TestWorkerCall`\n\n```ts\ntype TestWorkerCall<TInput, TOutput> =\n | { input: TInput; status: 'fulfilled'; value: TOutput }\n | { input: TInput; reason: unknown; status: 'rejected' };\n```\n\n### `TestWorkerHandle`\n\n```ts\ntype TestWorkerHandle<TInput, TOutput> = WorkerPool<TInput, TOutput> & {\n readonly calls: ReadonlyArray<TestWorkerCall<TInput, TOutput>>;\n};\n```\n\n### `SerializedError`\n\n```ts\ntype SerializedError = {\n category?: 'protocol';\n message: string;\n name: string;\n stack?: string;\n};\n```\n\n### `WorkerRequest`\n\n```ts\ntype WorkerRequest<TInput> =\n | { id: number; input: TInput; kind: 'run'; version: 1 }\n | { id: number; input: TInput; kind: 'stream'; version: 1 };\n```\n\n### `WorkerResponse`\n\n```ts\ntype WorkerResponse<TOutput> =\n | { id: number; kind: 'chunk'; value: TOutput; version: 1 }\n | { error: SerializedError; id: number; kind: 'error'; version: 1 }\n | { id: number; kind: 'result'; value: TOutput; version: 1 };\n```\n\n### `TaskHandler` and `StreamHandler`\n\n```ts\ntype TaskHandler<TInput, TOutput> = (input: TInput) => TOutput | Promise<TOutput>;\ntype StreamHandler<TInput, TChunk> = (input: TInput) => AsyncIterable<TChunk> | Promise<AsyncIterable<TChunk>>;\n```\n\n## Errors\n\n| Error | Trigger | Notable property |\n| --- | --- | --- |\n| `FamiliarError` | Base class for all Familiar errors | Use `instanceof FamiliarError` to narrow |\n| `FamiliarInvalidOptionsError` | Invalid factory or test options | — |\n| `FamiliarQueueFullError` | Queue limit reached with `onFull: 'reject'` | `maxQueue` |\n| `FamiliarTaskError` | Worker handler throws or payload cannot clone | `cause` |\n| `FamiliarTimeoutError` | Task or drain deadline expires | `timeoutMs` |\n| `FamiliarTerminatedError` | Pool is disposed or draining | — |\n| `FamiliarRuntimeError` | Worker API or worker process fails | `cause` |\n",
|
|
5
|
+
"api": "---\ntitle: Familiar — API Reference\ndescription: API reference for module-worker pools and worker-side protocol registration.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createWorker()` | Create single-result module-worker pool | Sync | Worker must call `exposeTask()` |\n| `createStreamWorker()` | Create stream-only module-worker pool | Sync | Worker must call `exposeStream()` |\n| `runBatch()` | Yield ordered results with shared cancellation | Async iterable | Transferables need a per-input selector |\n| `createTestWorker()` | Create an in-process task-pool test double | Sync | Task modules are not executed |\n| `exposeTask()` | Register worker task handler | Sync | Worker-only import |\n| `exposeStream()` | Register worker stream handler | Sync | Worker-only import |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/familiar` | Pool factories, types, and errors |\n| `@vielzeug/familiar/protocol` | Versioned worker protocol and registration helpers |\n| `@vielzeug/familiar/testing` | Task-pool testing adapter |\n\n## Pool Factories\n\n### `createWorker()`\n\n```ts\nfunction createWorker<TInput, TOutput>(url: URL | string, options?: WorkerOptions): WorkerPool<TInput, TOutput>;\n```\n\nCreates a task pool for a worker module registered with `exposeTask()`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `URL \\| string` | Module-worker URL, usually `new URL('./task.worker.ts', import.meta.url)` |\n| `options` | `WorkerOptions` | Pool concurrency, queue, timeout, and worker-error policy |\n\n**Returns:** `WorkerPool<TInput, TOutput>`.\n\n**Example:**\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker<number, number>(new URL('./double.worker.ts', import.meta.url));\n\ntry {\n console.log(await pool.run(21));\n} finally {\n pool.dispose();\n}\n```\n\n### `createStreamWorker()`\n\n```ts\nfunction createStreamWorker<TInput, TChunk>(url: URL | string, options?: WorkerOptions): StreamWorkerPool<TInput, TChunk>;\n```\n\nCreates a stream-only pool for a worker module registered with `exposeStream()`.\n\n**Returns:** `StreamWorkerPool<TInput, TChunk>`.\n\n### `runBatch()`\n\n```ts\nfunction runBatch<TInput, TOutput>(\n pool: WorkerPool<TInput, TOutput>,\n inputs: readonly TInput[],\n options?: BatchOptions<TInput>,\n): AsyncIterable<TOutput>;\n```\n\nStarts related pool tasks concurrently and yields results in input order. The first failure aborts siblings, early iterator exit cancels unfinished work, and cleanup waits for every task to settle. The iterable is one-shot once consumption begins; an unused iterator may be discarded.\n\nUse `getTransferables(input, index)` to return a fresh transfer list for each task. A shared transfer list is intentionally unsupported because transferred values can be detached only once.\n\n**Returns:** `AsyncIterable<TOutput>`.\n\n## Testing\n\n### `createTestWorker()`\n\n```ts\nfunction createTestWorker<TInput, TOutput>(\n handler: (input: TInput) => TOutput | Promise<TOutput>,\n options?: TestWorkerOptions,\n): TestWorkerHandle<TInput, TOutput>;\n```\n\nCreates an in-process task-pool double. It structured-clones values, records settlement, and matches pool timeout and cancellation results without loading a worker module. Cancellation rejects the test task but cannot stop side effects inside an already-running in-process handler.\n\n**Returns:** `TestWorkerHandle<TInput, TOutput>`.\n\n## Worker Protocol\n\n### `exposeTask()`\n\n```ts\nfunction exposeTask<TInput, TOutput>(handler: TaskHandler<TInput, TOutput>): void;\n```\n\nRegisters one single-result handler in a module worker.\n\n### `exposeStream()`\n\n```ts\nfunction exposeStream<TInput, TChunk>(handler: StreamHandler<TInput, TChunk>): void;\n```\n\nRegisters one chunk-producing handler in a module worker.\n\n### `PROTOCOL_VERSION`\n\n```ts\nconst PROTOCOL_VERSION: 1;\n```\n\nVersion included in every host request and worker response. Identifiers must be non-negative safe integers. Task/stream capability mismatches return a protocol-category error that maps to `FamiliarRuntimeError`; malformed host responses reject active work with the same class.\n\n## Types\n\n### `BatchOptions`\n\n```ts\ntype BatchOptions<TInput> = Pick<RunOptions, 'priority' | 'signal' | 'timeout'> & {\n getTransferables?: (input: TInput, index: number) => readonly Transferable[];\n};\n```\n\n### `WorkerOptions`\n\n```ts\ntype WorkerOptions = {\n concurrency?: number | 'auto';\n maxQueue?: number;\n onFull?: 'reject' | 'wait';\n timeout?: number;\n};\n```\n\n`concurrency` is limited to 512; `\"auto\"` clamps the reported hardware concurrency to that limit. `maxQueue` must be a positive integer and `onFull` must be `\"reject\"` or `\"wait\"`. Timeout values must be integer milliseconds from 1 through 2,147,483,647. Unhandled worker runtime errors are reported through `tap()`; a throwing tap handler cannot interrupt slot replacement.\n\n### `RunOptions`\n\n```ts\ntype RunOptions = {\n priority?: number;\n signal?: AbortSignal;\n timeout?: number;\n transferables?: readonly Transferable[];\n};\n```\n\n`signal` cancels capacity waits, queued work, and executing work. Executing cancellation terminates and replaces its worker slot without incrementing `failed`. `priority` must be finite and timeout values use the same bounds as pool defaults. The transfer list is copied at submission. Queued inputs remain caller-owned until a slot dispatches them, so do not mutate input objects while work is queued.\n\n### `WorkerPool`\n\n```ts\ninterface WorkerPool<TInput, TOutput> {\n [Symbol.asyncDispose](): Promise<void>;\n [Symbol.dispose](): void;\n run(input: TInput, options?: RunOptions): Promise<TOutput>;\n drain(options?: DrainOptions): Promise<void>;\n dispose(): void;\n tap(handler: (event: FamiliarTapEvent) => void, options?: { signal?: AbortSignal }): () => void;\n readonly stats: WorkerStats;\n readonly status: WorkerStatus;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n}\n```\n\n### `StreamWorkerPool`\n\n```ts\ninterface StreamWorkerPool<TInput, TChunk> {\n [Symbol.asyncDispose](): Promise<void>;\n [Symbol.dispose](): void;\n runStream(input: TInput, options?: RunOptions): AsyncIterable<TChunk>;\n drain(options?: DrainOptions): Promise<void>;\n dispose(): void;\n tap(handler: (event: FamiliarTapEvent) => void, options?: { signal?: AbortSignal }): () => void;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n readonly stats: WorkerStats;\n readonly status: WorkerStatus;\n}\n```\n\n`runStream()` returns a one-shot iterable. Consumption begins on the first iterator operation, so an unused iterator can be discarded. A second active iterator rejects with `FamiliarRuntimeError`, and `return()` immediately cancels a pending chunk request. Caller abort listeners are owned by the active iterator rather than an unconsumed iterable. Incoming chunks are buffered without protocol backpressure.\n\n### `WorkerStats`\n\n```ts\ntype WorkerStats = {\n readonly active: number;\n readonly completed: number;\n readonly failed: number;\n readonly queued: number;\n};\n```\n\n`queued` includes admitted queue entries and calls waiting for queue capacity. Cancellation is not counted as failure even when a caller supplies a custom abort reason.\n\n### `FamiliarTapEvent`\n\n```ts\ntype FamiliarTapEvent =\n | { error: FamiliarRuntimeError; type: 'worker-error' }\n | { type: 'dispose' };\n```\n\n`tap()` observes the pool: `worker-error` fires when a slot fails with an unhandled runtime error (the slot is replaced regardless), and `dispose` fires once when the pool terminates. Handler errors are swallowed; `tap()` returns an unsubscribe function and honors an optional `signal`.\n\n### `WorkerStatus`\n\n```ts\ntype WorkerStatus = 'idle' | 'running' | 'terminated';\n```\n\n### `DrainOptions`\n\n```ts\ntype DrainOptions = {\n timeout?: number;\n};\n```\n\n`drain()` accepts no argument for an unbounded graceful drain. A supplied timeout must be an integer from 1 through 2,147,483,647; expiry disposes the pool and rejects with `FamiliarTimeoutError`.\n\n### `TestWorkerOptions`\n\n```ts\ntype TestWorkerOptions = Omit<WorkerOptions, 'concurrency'> & {\n concurrency?: number;\n};\n```\n\n### `TestWorkerCall`\n\n```ts\ntype TestWorkerCall<TInput, TOutput> =\n | { input: TInput; status: 'fulfilled'; value: TOutput }\n | { input: TInput; reason: unknown; status: 'rejected' };\n```\n\n### `TestWorkerHandle`\n\n```ts\ntype TestWorkerHandle<TInput, TOutput> = WorkerPool<TInput, TOutput> & {\n readonly calls: ReadonlyArray<TestWorkerCall<TInput, TOutput>>;\n};\n```\n\n### `SerializedError`\n\n```ts\ntype SerializedError = {\n category?: 'protocol';\n message: string;\n name: string;\n stack?: string;\n};\n```\n\n### `WorkerRequest`\n\n```ts\ntype WorkerRequest<TInput> =\n | { id: number; input: TInput; kind: 'run'; version: 1 }\n | { id: number; input: TInput; kind: 'stream'; version: 1 };\n```\n\n### `WorkerResponse`\n\n```ts\ntype WorkerResponse<TOutput> =\n | { id: number; kind: 'chunk'; value: TOutput; version: 1 }\n | { error: SerializedError; id: number; kind: 'error'; version: 1 }\n | { id: number; kind: 'result'; value: TOutput; version: 1 };\n```\n\n### `TaskHandler` and `StreamHandler`\n\n```ts\ntype TaskHandler<TInput, TOutput> = (input: TInput) => TOutput | Promise<TOutput>;\ntype StreamHandler<TInput, TChunk> = (input: TInput) => AsyncIterable<TChunk> | Promise<AsyncIterable<TChunk>>;\n```\n\n## Errors\n\n| Error | Trigger | Notable property |\n| --- | --- | --- |\n| `FamiliarError` | Base class for all Familiar errors | Use `instanceof FamiliarError` to narrow |\n| `FamiliarInvalidOptionsError` | Invalid factory or test options | — |\n| `FamiliarQueueFullError` | Queue limit reached with `onFull: 'reject'` | `maxQueue` |\n| `FamiliarTaskError` | Worker handler throws or payload cannot clone | `cause` |\n| `FamiliarTimeoutError` | Task or drain deadline expires | `timeoutMs` |\n| `FamiliarTerminatedError` | Pool is disposed or draining | — |\n| `FamiliarRuntimeError` | Worker API or worker process fails | `cause` |\n",
|
|
6
6
|
"usage": "---\ntitle: Familiar — Usage Guide\ndescription: Run task and stream module workers with bounded concurrency, cancellation, and test parity.\n---\n\n[[toc]]\n\n## Basic Usage\n\nPut task logic in a worker module. Imports and helpers stay normal module code.\n\n```ts\n// normalize.worker.ts\nimport { exposeTask } from '@vielzeug/familiar/protocol';\n\nimport { normalize } from './normalize';\n\nexposeTask((text: string) => normalize(text));\n```\n\nCreate one long-lived pool at its owner boundary.\n\n```ts\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker<string, string>(new URL('./normalize.worker.ts', import.meta.url), {\n concurrency: 2,\n timeout: 2_000,\n});\n\ntry {\n const normalized = await pool.run(' Familiar ');\n console.log(normalized);\n} finally {\n pool.dispose();\n}\n```\n\n## Cancellation and Timeouts\n\nPass one signal to stop capacity waits, queued work, or active work. Cancelling active work terminates and lazily replaces its slot without incrementing `stats.failed`, regardless of the abort reason. Timeouts must be integer milliseconds from 1 through 2,147,483,647.\n\n```ts\nconst controller = new AbortController();\nconst result = pool.run('input', { signal: controller.signal, timeout: 500 });\n\ncontroller.abort();\nawait result.catch((error) => console.log(error.name)); // AbortError\n```\n\n## Queue Policy and Priority\n\nUse `maxQueue` to bound the admitted queue. Higher priorities dispatch first once a slot opens.\n\n```ts\nconst pool = createWorker<Job, Result>(new URL('./job.worker.ts', import.meta.url), {\n concurrency: 2,\n maxQueue: 100,\n onFull: 'wait',\n});\n\nawait pool.run(criticalJob, { priority: 10 });\n```\n\nWith `onFull: 'wait'`, overflow calls wait for admission outside the bounded queue. `stats.queued` includes both admitted tasks and capacity waiters, and priority applies to both.\n\n## Batch Composition\n\nUse `runBatch()` for ordered progressive results with one shared cancellation lifetime. It starts pool work concurrently, cancels siblings on the first failure, and cancels unfinished work when iteration stops early.\n\n```ts\nimport { runBatch } from '@vielzeug/familiar';\n\nfor await (const result of runBatch(pool, inputs, { signal, timeout: 5_000 })) {\n consume(result);\n}\n```\n\nUse `getTransferables(input, index)` when each task needs its own transfer list. See [Cancellable Batch](./examples/cancellable-batch.md).\n\n## Streaming\n\nStream workers have their own capability and registration helper.\n\n```ts\n// tokenize.worker.ts\nimport { exposeStream } from '@vielzeug/familiar/protocol';\n\nexposeStream(async function* (text: string) {\n for (const token of text.split(/\\s+/)) yield token;\n});\n```\n\n```ts\nimport { createStreamWorker } from '@vielzeug/familiar';\n\nconst pool = createStreamWorker<string, string>(new URL('./tokenize.worker.ts', import.meta.url));\nfor await (const token of pool.runStream('typed module workers')) {\n console.log(token);\n}\npool.dispose();\n```\n\nEach value returned by `runStream()` is one-shot once an iterator operation begins. Unused iterators can be discarded, and `return()` immediately cancels a pending chunk request. Abort listeners are attached when iteration begins and removed when it settles. Worker messages do not provide backpressure, so keep chunks and production rates bounded when consumers may be slow.\n\n## Testing\n\nUse `createTestWorker()` when testing consumer code that depends on a task pool. It clones input/output, wraps task failures, and honors cancellation and timeout behavior.\n\n```ts\nimport { createTestWorker } from '@vielzeug/familiar/testing';\n\nconst pool = createTestWorker((value: number) => value * 2);\nawait expect(pool.run(21)).resolves.toBe(42);\nexpect(pool.calls).toEqual([{ input: 21, status: 'fulfilled', value: 42 }]);\npool.dispose();\n```\n\nTest worker-module business logic directly when possible. `createTestWorker()` does not run module files or support stream pools. Cancellation rejects an active test task but cannot stop side effects in an already-running in-process handler.\n\n## Framework Integration\n\nCreate a pool at a stable owner boundary. Abort obsolete requests during component cleanup and dispose the pool at the same boundary that created it.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect } from 'react';\nimport type { WorkerPool } from '@vielzeug/familiar';\n\nfunction SortedView({ pool }: { pool: WorkerPool<Input, Output> }) {\n useEffect(() => {\n const controller = new AbortController();\n void pool.run(input, { signal: controller.signal }).then(renderOutput, (error: unknown) => {\n if (!(error instanceof DOMException && error.name === 'AbortError')) reportError(error);\n });\n return () => controller.abort();\n }, [input, pool]);\n\n return null;\n}\n```\n\nCreate and dispose the injected React pool at the application boundary, outside Strict Mode effect replay.\n\n```ts [Vue]\nimport { onUnmounted } from 'vue';\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker(new URL('./sort.worker.ts', import.meta.url));\n\nonUnmounted(() => pool.dispose());\n```\n\n```ts [Svelte]\nimport { onDestroy } from 'svelte';\nimport { createWorker } from '@vielzeug/familiar';\n\nconst pool = createWorker(new URL('./sort.worker.ts', import.meta.url));\n\nonDestroy(() => pool.dispose());\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nKeep worker module protocol registration in `@vielzeug/familiar/protocol`.\n\n## Best Practices\n\n- Put every task handler in its own module-worker boundary.\n- Reuse pools for repeated work; dispose owner-scoped pools.\n- Abort work made obsolete by navigation or newer input.\n- Transfer large binary buffers instead of cloning them.\n- Treat submitted inputs as immutable until dispatch; queued inputs are structured-cloned only when a worker slot becomes available.\n- Set explicit timeouts for work with a bounded latency budget.\n- Keep worker handlers deterministic and data-only.\n- Test module logic directly; test pool consumers with `createTestWorker()`.\n",
|
|
7
7
|
"examples": "---\ntitle: Familiar — Examples\ndescription: Module-worker recipes for familiar.\n---\n\n## Examples\n\n- [Fibonacci With Pool And Timeout](./examples/fibonacci-with-pool-and-timeout.md)\n- [Data Transformation Pipeline](./examples/data-transformation-pipeline.md)\n- [Image Processing](./examples/image-processing.md)\n- [Using Transferables](./examples/using-transferables.md)\n- [Cancellable Batch](./examples/cancellable-batch.md)\n- [Priority Queue](./examples/priority-queue.md)\n- [Streaming With Stream Worker](./examples/streaming-with-runstream.md)\n- [Module Worker](./examples/module-worker.md)\n- [Typed Error Handling](./examples/typed-error-handling.md)\n- [React Integration](./examples/react-integration.md)\n- [Testing With createTestWorker](./examples/testing-with-createtestworker.md)\n"
|
|
8
8
|
},
|
|
@@ -13,5 +13,26 @@
|
|
|
13
13
|
"name": "Familiar Error Contracts"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
|
-
"typeSignatures": {
|
|
16
|
+
"typeSignatures": {
|
|
17
|
+
"BatchOptions": "export { type BatchOptions, runBatch } from './_pool.js';",
|
|
18
|
+
"runBatch": "export { type BatchOptions, runBatch } from './_pool.js';",
|
|
19
|
+
"FamiliarError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
20
|
+
"FamiliarInvalidOptionsError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
21
|
+
"FamiliarQueueFullError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
22
|
+
"FamiliarRuntimeError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
23
|
+
"FamiliarTaskError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
24
|
+
"FamiliarTerminatedError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
25
|
+
"FamiliarTimeoutError": "export {\n FamiliarError,\n FamiliarInvalidOptionsError,\n FamiliarQueueFullError,\n FamiliarRuntimeError,\n FamiliarTaskError,\n FamiliarTerminatedError,\n FamiliarTimeoutError,\n} from './errors.js';",
|
|
26
|
+
"DrainOptions": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
27
|
+
"FamiliarTapEvent": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
28
|
+
"PoolBase": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
29
|
+
"RunOptions": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
30
|
+
"StreamWorkerPool": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
31
|
+
"WorkerOptions": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
32
|
+
"WorkerPool": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
33
|
+
"WorkerStats": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
34
|
+
"WorkerStatus": "export type {\n DrainOptions,\n FamiliarTapEvent,\n PoolBase,\n RunOptions,\n StreamWorkerPool,\n WorkerOptions,\n WorkerPool,\n WorkerStats,\n WorkerStatus,\n} from './types.js';",
|
|
35
|
+
"createStreamWorker": "export { createStreamWorker, createWorker } from './worker.js';",
|
|
36
|
+
"createWorker": "export { createStreamWorker, createWorker } from './worker.js';"
|
|
37
|
+
}
|
|
17
38
|
}
|