@defjs/core · function ```ts function createClient(...options: ClientOption[]): DerivableClient; ``` Creates a Defjs [DerivableClient](/api/core/derivableclient-type) from option helpers. Options apply in order: a later option overwrites an earlier one for the same setting, and interceptors accumulate. Prefer creating the client inside the request boundary when interceptors close over auth or tenants. Default HTTP/SSE fetch availability is checked when an operation invokes transport. The returned factory is a function. Calling it with [RequestOptions](/api/core/requestoptions-type) returns a [Client](/api/core/client-type) value for one call and performs no I/O; see [DerivableClient](/api/core/derivableclient-type) for the snapshot rules. The factory itself is also a `Client` and carries no request options, so it can be passed to an operation directly. The base configuration is shared by every client value and is not frozen at runtime. It is fixed by convention: nothing in the library changes it, and applications should not either. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | ...options | `ClientOption[]` | optional | Client option helpers such as `withEndpoint`. | ## Returns A callable factory that carries the resolved base configuration. ## Examples ```ts import { createClient, withEndpoint } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); ``` ## See also [DerivableClient](/api/core/derivableclient-type) · [Client](/api/core/client-type) · [ClientOption](/api/core/clientoption-type) · [RequestOptions](/api/core/requestoptions-type) ## Overview [Overview](/api/client) @defjs/core · function ```ts function withEndpoint(endpoint: string): ClientOption; ``` Sets the valid absolute base URL prepended to relative request paths. The default is an empty string, so execution requires an explicit endpoint. Its pathname is kept as a directory; its query and hash are discarded. For WebSocket operations, an `http:` base becomes `ws:` and `https:` becomes `wss:`. A missing, relative, or invalid endpoint, or an illegal request path, fails with `REQ_BUILD_FAILED` during building, before any interceptor or transport runs, even when an interceptor would short-circuit. HTTP, SSE, and WebSocket share this rule. The resolved request must keep the base origin and stay under its directory; C0 control characters (including tab, CR, and LF), backslashes, `?`, `#`, and directory escapes in a path are rejected. Scheme-like and protocol-relative forms are rejected even with leading spaces; a plain inner space is allowed and sent as `%20`. An interceptor's final `req.endpoint` is checked against its final `req.baseEndpoint` with the same rule, but it may deliberately replace the base with another valid absolute URL. Deterministic URL errors are never retried. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | endpoint | `string` | required | Absolute base endpoint. Missing or invalid endpoints fail request building. | ## Returns An option that writes `config.endpoint`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withInterceptors(...interceptors: Interceptor[]): ClientOption; ``` Appends interceptors to the client interceptor chain. Calling it again appends more; it never replaces earlier ones. The first registered interceptor is the outermost layer. HTTP, SSE, and WebSocket interceptors can be mixed in one call. When an operation runs, only the interceptors of that transport run, in their registration order. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | ...interceptors | `Interceptor[]` | optional | Interceptors applied in registration order. | ## Returns An option that pushes onto `config.interceptors`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withQuerySerializer(serializer: QuerySerializer): ClientOption; ``` Replaces query encoding for HTTP, SSE, and WebSocket requests. `serializer(query)` receives the encoded wire object: aliases applied, `Date` as an ISO string, `bigint` as a string. Nested objects and arrays are kept as they are. It must return the query string without a leading `?`. The string is used verbatim. It is not called when the request has no query. A builder that returns `URLSearchParams` skips it. Without this option, queries use `URLSearchParams`: arrays repeat keys and a nested object fails. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | serializer | `QuerySerializer` | required | Function that turns an encoded query object into a string. | ## Returns An option that sets `config.querySerializer`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withHTTPHandle(fetchImpl: FetchHandle): ClientOption; ``` Replaces the Fetch-compatible function used for HTTP requests. The default is the platform `fetch`. `fetchImpl` is a [FetchHandle](/api/core/fetchhandle-type), so a test double does not need host extras such as `fetch.preconnect`. It only affects HTTP. SSE uses [withSSEHandle](/api/core/withssehandle). ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fetchImpl | `FetchHandle` | required | `(input, init?) => Promise`. Not `typeof fetch`, so a mock does not need host extras such as `preconnect`. | ## Returns An option that sets `config.http.handle`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withSSEHandle(fetchImpl: FetchHandle): ClientOption; ``` Replaces the Fetch-compatible function used to open SSE event streams. The default is the platform `fetch`. `fetchImpl` is a [FetchHandle](/api/core/fetchhandle-type). It only affects SSE. HTTP uses [withHTTPHandle](/api/core/withhttphandle). ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fetchImpl | `FetchHandle` | required | `(input, init?) => Promise`. Not `typeof fetch`, so a mock does not need host extras such as `preconnect`. | ## Returns An option that sets `config.sse.handle`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withWebSocketHandle( WebSocketImpl: WebSocketHandleConstructor, ): ClientOption; ``` Replaces the WebSocket constructor used for socket connections. The default is `globalThis.WebSocket`. `WebSocketImpl` is a [WebSocketHandleConstructor](/api/core/websockethandleconstructor-type) that returns a [WebSocketHandle](/api/core/websockethandle-type). If no constructor is available, a WebSocket operation fails with an `ENV_UNSUPPORTED` fault. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | WebSocketImpl | `WebSocketHandleConstructor` | required | Constructor that returns a `WebSocketHandle`. | ## Returns An option that sets `config.webSocket.handle`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withCredentials(value: boolean): ClientOption; ``` Sets whether HTTP and SSE requests include credentials. When `value` is `true`, Fetch uses `credentials: 'include'`, so cookies and other credentials are sent with cross-origin requests. When it is `false` or never set, `credentials` is left unset and Fetch uses its default. The WebSocket constructor does not receive this setting. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | value | `boolean` | required | When `true`, credentials are sent with cross-origin requests. | ## Returns An option that sets `config.withCredentials`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withXSRF( options?: HttpClientOptions["xsrf"], ): ClientOption; ``` Enables XSRF cookie-to-header token forwarding for HTTP requests. SSE and WebSocket requests do not get the header. `cookieName` defaults to `XSRF-TOKEN` and `headerName` to `X-XSRF-TOKEN` when names are omitted. - The header is set only when all of these hold: the method is not `GET`, `HEAD`, `OPTIONS`, or `TRACE`; the request does not already have that header; the client has an endpoint; and, whenever `location` exists (Window or Worker), the resolved request URL has the same origin as `location.origin`. A cross-origin request does not call the provider. - Without `tokenProvider`, the token is read from `document.cookie` and URL-decoded only when `document` exists. Workers and servers need a provider to supply a token. - With `tokenProvider`, the cookie is never read. The provider receives the request, and its result is the token. An empty string, `null`, or `undefined` means no header. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | options | `HttpClientOptions["xsrf"]` | optional | Cookie/header names and optional token provider. | ## Returns An option that sets `config.xsrf`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withSSEReconnect( options: ClientSSEOptions["reconnect"], ): ClientOption; ``` Configures automatic reconnection after an SSE stream fails. Reconnection is opt-in: without this option a failed stream is not retried. Passing `undefined` clears an earlier setting. ```ts type SSEReconnect = { attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; lastEventId: string; method: string; open?: { response: { status: number; statusText: string; url: string }; url: string; }; }) => boolean | Promise; }; ``` - `attempts` is the retry budget for each outage. It defaults to `3` when `reconnect` is set, and `0` disables retries. A successful handshake resets the budget, callback `attempt`, and backoff exponent. Omitting it does not give unlimited retries within one outage. Repeated successful opens followed immediately by retryable stream failures can keep reconnecting at the current base delay (subject to jitter and caps), because each open resets the budget; use `shouldReconnect` to stop them. Normal EOF remains terminal. - Without `shouldReconnect`, only `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` reconnect. With it, its result decides for every method, `POST` included; `method` in the context tells endpoints apart. Return `false` to stop for this failure. It runs before the attempts limit is checked. - A request with a `ReadableStream` body never reconnects, and `shouldReconnect` is not called for it. - `delayMs`, `factor`, `jitter` (0 to 1), and `maxDelayMs` shape the backoff. `factor` defaults to `1` and `jitter` to `0`. `maxDelayMs` caps the final delay after jitter and rounding. Without a configured cap, the platform timer limit of `2_147_483_647` ms still applies. A zero base stays zero; valid-number overflow saturates at the caps. - `delayMs` is only the starting base delay (`1000` by default). A server `retry:` field (digits only, in milliseconds) replaces it for later attempts. - The last event id is sent again as UTF-8 bytes in `Last-Event-ID` on the next attempt. An empty `id:` clears it; an ID containing NUL is ignored. - Reconnect options are validated before interceptors or Fetch: `attempts` must be a non-negative safe integer; `delayMs` must be finite and non-negative; `maxDelayMs` must be a non-negative finite number or positive `Infinity`, which removes the configured cap but not the platform limit of `2_147_483_647` ms; `factor` must be finite and positive; `jitter` must be finite and within 0–1. Invalid values return `REQ_OPTIONS_INVALID` when the operation runs. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | options | `ClientSSEOptions["reconnect"]` | required | Retry budget, backoff, and `shouldReconnect` gate. | ## Returns An option that sets `config.sse.reconnect`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withSSEOnInvalidEvent( handler: ClientSSEOptions["onInvalidEvent"], ): ClientOption; ``` Observes SSE events that have no matching event struct or fail validation. The handler may be async and is awaited. The callback receives: ```ts { reason: 'missing-struct' | 'validation-failed' message: { id: string; event: string; data: string } cause?: unknown signal: AbortSignal } ``` - The invalid event is **always dropped**; it is never delivered to the stream. The callback only observes it. - The return value is ignored. An error thrown by the callback is swallowed and does not close the stream, unless `signal` has already aborted. - `cause` is set for `validation-failed`. Without a handler the invalid event is dropped silently. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | handler | `ClientSSEOptions["onInvalidEvent"]` | required | Observer callback for invalid events. | ## Returns An option that sets `config.sse.onInvalidEvent`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withWebSocketBeforeConnect( beforeConnect: ClientWebSocketOptions["beforeConnect"], ): ClientOption; ``` Runs a hook before the initial WebSocket connect and before every reconnect. The hook type is: ```ts (context: { attempt: number; signal: AbortSignal }) => void | Promise ``` - `attempt` is `0` for the first connect and grows by one for each reconnect. - Pass `signal` into token refreshes and other async work so cancelling the session stops them. - If the hook throws, the attempt fails with an `EXT_HOOK_FAILED` fault. - A `beforeConnect` in the request options replaces this one for that call. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | beforeConnect | `((context: { attempt: number; signal: AbortSignal; }) => void \| Promise) \| undefined` | required | Hook receiving attempt index and abort signal. | ## Returns An option that sets `config.webSocket.beforeConnect`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withWebSocketProtocols( protocols: readonly string[], ): ClientOption; ``` Sets the WebSocket subprotocols offered during the handshake. They are passed as the second argument to the `WebSocket` constructor only when the list is not empty, so the platform can run the RFC 6455 subprotocol negotiation. They matter only when the server requires a specific subprotocol, for example `['json']` or `['v1']`. Precedence, highest first: the `protocols` request option, this client option, then `protocols` on the endpoint definition. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | protocols | `readonly string[]` | required | Subprotocol list for the WebSocket constructor. | ## Returns An option that sets `config.webSocket.protocols`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withWebSocketHeartbeat( options: ClientWebSocketOptions["heartbeat"], ): ClientOption; ``` Enables WebSocket heartbeat messages with optional acknowledgement detection. Passing `undefined` clears an earlier setting. The option type is: ```ts { intervalMs: number isAck?: (message: unknown) => boolean message?: () => T | unknown timeoutMs?: number } ``` - Every `intervalMs`, `message()` is called and its result is sent through the endpoint's `outgoing` struct. No message is sent when `message` is omitted or returns `undefined`. - With `timeoutMs`, an acknowledgement is expected after each heartbeat. `isAck` receives the decoded incoming message; when it returns `true`, the timer is cleared and that message is **not** delivered to `receive`. - If no ack arrives within `timeoutMs`, or a heartbeat cannot be sent, the session fails. This does not trigger a reconnect. - Heartbeat uses the same JSON guard as `session.send`: a value JSON cannot represent after struct encoding causes `TypeError` and ends the logical session. - `intervalMs` and `timeoutMs` must be positive, finite, and at most `2_147_483_647`; otherwise the operation fails with `REQ_OPTIONS_INVALID`. - A `heartbeat` in the request options replaces this one for that call. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | options | `ClientWebSocketOptions["heartbeat"]` | required | Heartbeat interval, payload, timeout, and ACK predicate. | ## Returns An option that sets `config.webSocket.heartbeat`. ## Overview [Overview](/api/client) @defjs/core · function ```ts function withWebSocketReconnect( options: ClientWebSocketOptions["reconnect"], ): ClientOption; ``` Configures automatic reconnection when the socket closes for any reason other than a manual `session.close()`. Reconnection is opt-in: without this option a close ends the session. Passing `undefined` clears an earlier setting. The option type is: ```ts { attempts?: number delayMs?: number factor?: number jitter?: number maxDelayMs?: number shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean }) => boolean } ``` - `attempts` is the retry budget for each outage. It defaults to `3`, and `0` disables reconnection. Successful `open` resets the budget and backoff exponent; the next outage starts at retry 1. Omitting it does not give unlimited retries within one outage. A socket that successfully opens and is immediately closed again by the server can keep reconnecting at the base delay (subject to jitter and caps), because every open resets the budget; use `shouldReconnect` to stop it. - `delayMs` defaults to `1000`, `factor` to `2`, `maxDelayMs` to `30000`, and `jitter` (0 to 1) to `0`. Retry _n_ starts with `delayMs * factor^(n-1)`, capped at `maxDelayMs`; jitter and rounding follow, then the final delay is capped again at `maxDelayMs` and `2_147_483_647` ms. A zero base stays zero; valid-number overflow saturates at those caps. - `shouldReconnect` can return `false` to end the session instead. Without it every close is retried while the budget lasts. It is not called for a manual `session.close()`. - Reconnect options are validated before connecting: `attempts` must be a non-negative safe integer; `delayMs` must be finite and non-negative; `maxDelayMs` must be a non-negative finite number or positive `Infinity`, which removes the configured cap but not the platform limit of `2_147_483_647` ms; `factor` must be finite and positive; `jitter` must be finite and within 0–1. Invalid values make the operation fail with `REQ_OPTIONS_INVALID` before connecting. `attempts: 0` disables subsequent retries, but all explicitly supplied reconnect fields are still validated before the initial connection. - A `reconnect` in the request options replaces this one for that call. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | options | `ClientWebSocketOptions["reconnect"]` | required | Retry budget, backoff, and `shouldReconnect` gate. | ## Returns An option that sets `config.webSocket.reconnect`. ## Overview [Overview](/api/client) @defjs/core · type ```ts interface Client { readonly [CLIENT]: ClientState; } ``` Client value that operations accept. A client value carries the base configuration and one call's request options. Operations accept it. It is not callable; create one by calling the factory returned by `createClient`. Calling a client value throws a `TypeError` at runtime and fails to compile. Create a client value per call. Reusing a value that carries `signal` shares that signal between calls. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | [CLIENT] | `ClientState` | required | Internal state. Not part of the call surface. | ## See also [createClient](/api/core/createclient) · [RequestOptions](/api/core/requestoptions-type) ## Overview [Overview](/api/client) @defjs/core · type ```ts type RequestOptions = { signal?: AbortSignal; onUploadProgress?: HttpProgressFn; onDownloadProgress?: HttpProgressFn; beforeConnect?: ClientWebSocketOptions["beforeConnect"]; protocols?: readonly string[]; heartbeat?: ClientWebSocketOptions["heartbeat"]; reconnect?: ClientWebSocketOptions["reconnect"]; }; ``` Flat per-call options. Each protocol reads the fields it understands and ignores the rest. SSE reads only `signal` from request options. SSE reconnect comes from client configuration (`withSSEReconnect`), so `client({ reconnect })` affects WebSocket only. `signal` cancels HTTP, SSE, and WebSocket calls. `onUploadProgress` and `onDownloadProgress` apply to HTTP. `beforeConnect`, `protocols`, `heartbeat`, and `reconnect` apply to WebSocket and replace the client setting for that call as a whole value. There is no `timeout` or `abort` field. Pass `AbortSignal.timeout(ms)` as `signal` when a call needs a deadline. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | signal | `AbortSignal \| undefined` | optional | Aborts the call. An already aborted signal fails the call before transport. | | onUploadProgress | `HttpProgressFn \| undefined` | optional | Observes HTTP upload progress. | | onDownloadProgress | `HttpProgressFn \| undefined` | optional | Observes HTTP download progress. | | beforeConnect | `((context: { attempt: number; signal: AbortSignal; }) => void \| Promise) \| undefined` | optional | Runs before each WebSocket connect attempt for this call. | | protocols | `readonly string[] \| undefined` | optional | WebSocket subprotocols for this call. Replaces the client list; it is not appended. | | heartbeat | `{ intervalMs: number; isAck?: (message: unknown) => boolean; message?: () => T \| unknown; timeoutMs?: number; } \| undefined` | optional | WebSocket heartbeat for this call. Replaces the client heartbeat as a whole value. | | reconnect | `{ attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean; }) => boolean; } \| undefined` | optional | WebSocket reconnect policy for this call. Replaces the client policy as a whole value. | ## Overview [Overview](/api/client) @defjs/core · type ```ts type ClientOption = (config: ClientConfig) => void; ``` Function type of the option helpers accepted by `createClient`. Mutates the [ClientConfig](/api/core/clientconfig-type) that `createClient` is building. Every `with*` helper returns one. `createClient` applies the options in order, so a later option overwrites an earlier one for the same setting, except `withInterceptors`, which appends. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | config | `ClientConfig` | required | | ## See also [ClientConfig](/api/core/clientconfig-type) ## Overview [Overview](/api/client) @defjs/core · type ```ts interface ClientOptions extends HttpClientOptions { sse?: ClientSSEOptions; webSocket?: ClientWebSocketOptions; } ``` Input-shaped client options, including SSE and WebSocket, that mirror what the option helpers configure. The user-facing shape of the settings before defaults are applied. `createClient` does not accept this object; configure a client with option helpers such as `withEndpoint`. [ClientConfig](/api/core/clientconfig-type) is the resolved counterpart. The HTTP members come from the non-exported `HttpClientOptions` base and are shown inline. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | sse | `ClientSSEOptions \| undefined` | optional | SSE transport and reconnect options. | | webSocket | `ClientWebSocketOptions \| undefined` | optional | WebSocket transport, heartbeat, and reconnect options. | | endpoint | `string` | required | Base URL prepended to relative request paths. | | headers | `Headers \| undefined` | optional | Static default headers merged under headers returned by the builder for HTTP/SSE. | | http | `{ handle?: FetchHandle; } \| undefined` | optional | HTTP transport overrides; the default resolves global `fetch` on each call. | | interceptors | `Interceptor[] \| undefined` | optional | Interceptors applied in registration order. | | querySerializer | `QuerySerializer \| undefined` | optional | Serializes encoded query objects. Omit to use `URLSearchParams`. | | xsrf | `{ cookieName?: string; headerName?: string; tokenProvider?: (context: { request: HttpRequest; }) => string \| null \| undefined; } \| undefined` | optional | XSRF cookie/header mapping and optional token provider. | | withCredentials | `boolean \| undefined` | optional | When `true`, credentials are included with requests. | ## Overview [Overview](/api/client) @defjs/core · type ```ts interface ClientConfig extends HttpClientConfig { sse: ClientSSEConfig; webSocket: ClientWebSocketOptions; } ``` Resolved client configuration after option helpers have been applied. The snapshot that `createClient` builds, that every option helper mutates, and that instances from `createClient` hold. The HTTP members come from a non-exported `HttpClientConfig` base and are shown inline. Before any option applies, `endpoint` is `''`, `interceptors` is empty, and `http.handle` and `sse.handle` share a default wrapper that resolves `globalThis.fetch` on each call. Replacing Fetch after import affects both defaults. Core can be imported without Fetch; a default HTTP/SSE execution fails with `ENV_UNSUPPORTED` if it is still missing. Custom handles do not require a global Fetch. `webSocket.handle` is `globalThis.WebSocket`, and the other `webSocket` settings are unset. `withXSRF` fills `xsrf.cookieName` with `XSRF-TOKEN` and `xsrf.headerName` with `X-XSRF-TOKEN` when they are omitted. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | sse | `ClientSSEConfig` | required | Resolved SSE configuration. | | webSocket | `ClientWebSocketOptions` | required | Resolved WebSocket configuration. | | endpoint | `string` | required | Base URL prepended to relative request paths. | | headers | `Headers \| undefined` | optional | Static default headers merged under headers returned by the builder for HTTP/SSE. | | http | `{ handle: FetchHandle; }` | required | HTTP transport with a required fetch handle. | | interceptors | `Interceptor[]` | required | Interceptors applied in registration order. | | querySerializer | `QuerySerializer \| undefined` | optional | Serializes encoded query objects. Omit to use `URLSearchParams`. | | xsrf | `{ cookieName: string; headerName: string; tokenProvider?: (context: { request: HttpRequest; }) => string \| null \| undefined; } \| undefined` | optional | Resolved XSRF settings when enabled. | | withCredentials | `boolean \| undefined` | optional | When `true`, credentials are included with requests. | ## Overview [Overview](/api/client) @defjs/core · type ```ts interface ClientSSEOptions { handle?: FetchHandle; onInvalidEvent?: (context: { reason: "missing-struct" | "validation-failed"; message: { id: string; event: string; data: string }; cause?: unknown; signal: AbortSignal; }) => unknown; reconnect?: { attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; lastEventId: string; method: string; open?: { response: { status: number; statusText: string; url: string }; url: string; }; }) => boolean | Promise; }; } ``` Optional SSE settings applied when creating a client: fetch handle, invalid-event hook, and reconnect policy. - **handle** — Fetch implementation that opens the stream. The default resolves `globalThis.fetch` on each attempt, including replacements installed after import. Every SSE handle receives a `Request`. Missing global Fetch fails default execution with `ENV_UNSUPPORTED`; a custom handle remains usable. - **onInvalidEvent** — Called when an event has no matching struct and no `default` struct (`'missing-struct'`), or its data fails validation (`'validation-failed'`). The event is skipped. An error thrown by the hook does not close the stream. - **reconnect** — Opt-in. Without it the stream never reconnects. `attempts` defaults to **3** per outage; `attempts: 0` disables retry. A successful handshake resets the budget, callback `attempt`, and backoff exponent. `delayMs` is the first base delay, 1000 by default, and a server-sent `retry:` value replaces it. `factor` defaults to 1. `jitter` is a 0–1 multiplicative factor and defaults to 0. `maxDelayMs` caps the final delay after jitter and rounding; without a configured cap, the platform limit of `2_147_483_647` ms still applies. A zero base stays zero; valid-number overflow saturates at the caps. Without `shouldReconnect` only `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` reconnect; with it, its result decides for every method and its context carries `method`. A `ReadableStream` request body never reconnects. Reconnect sends the latest ID as UTF-8 bytes in `Last-Event-ID`; an empty ID clears it and an ID containing NUL is ignored. Reconnect options are validated before interceptors or Fetch: `attempts` is a non-negative safe integer; `delayMs` must be finite and non-negative; `maxDelayMs` must be a non-negative finite number or positive `Infinity`, which removes the configured cap but not the platform limit of `2_147_483_647` ms; `factor` is finite and positive; `jitter` is finite and within 0–1. Invalid values return `REQ_OPTIONS_INVALID` when the operation runs. Set these with `withSSEHandle`, `withSSEOnInvalidEvent`, and `withSSEReconnect`. Omitting `withSSEReconnect` disables retry. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | handle | `FetchHandle \| undefined` | optional | Fetch implementation used to open the event stream; defaults to resolving global `fetch` on each call. | | onInvalidEvent | `((context: { reason: "missing-struct" \| "validation-failed"; message: { id: string; event: string; data: string; }; cause?: unknown; signal: AbortSignal; }) => unknown) \| undefined` | optional | Called when an event has no matching struct or fails validation. | | reconnect | `{ attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; lastEventId: string; method: string; open?: { response: { status: number; statusText: string; url: string; }; url: string; }; }) => boolean \| Promise; } \| undefined` | optional | Automatic reconnect policy after stream failures. | ## Overview [Overview](/api/client) @defjs/core · type ```ts interface ClientSSEConfig extends ClientSSEOptions { handle: FetchHandle; } ``` Resolved SSE configuration with a required fetch handle. The type of `ClientConfig.sse`. It is [ClientSSEOptions](/api/core/clientsseoptions-type) with `handle` always present; the default wrapper resolves the current global `fetch` on each attempt, rather than capturing it at import time. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | handle | `FetchHandle` | required | Fetch implementation used to open the event stream. | | onInvalidEvent | `((context: { reason: "missing-struct" \| "validation-failed"; message: { id: string; event: string; data: string; }; cause?: unknown; signal: AbortSignal; }) => unknown) \| undefined` | optional | Called when an event has no matching struct or fails validation. | | reconnect | `{ attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; lastEventId: string; method: string; open?: { response: { status: number; statusText: string; url: string; }; url: string; }; }) => boolean \| Promise; } \| undefined` | optional | Automatic reconnect policy after stream failures. | ## Overview [Overview](/api/client) @defjs/core · type ```ts interface ClientWebSocketOptions { handle?: WebSocketHandleConstructor; beforeConnect?: (context: { attempt: number; signal: AbortSignal; }) => void | Promise; heartbeat?: { intervalMs: number; isAck?: (message: unknown) => boolean; message?: () => T | unknown; timeoutMs?: number; }; protocols?: readonly string[]; reconnect?: { attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean; }) => boolean; }; } ``` Optional WebSocket settings applied when creating a client: constructor, `beforeConnect` hook, heartbeat, protocols, and reconnect policy. - **handle** — WebSocket constructor; defaults to `globalThis.WebSocket`. - **beforeConnect** — Runs before each connect and reconnect attempt. - **heartbeat** — Sends `message()` every `intervalMs`. With `timeoutMs` and `isAck`, a missing acknowledgement marks the connection unhealthy. `intervalMs` and `timeoutMs` must be positive and at most 2147483647. - **protocols** — Subprotocols passed to the constructor. - **reconnect** — Opt-in. `attempts` defaults to **3**; `0` disables retry, and omitting `attempts` is not unlimited. Other defaults: `delayMs` 1000, `factor` 2, `jitter` 0 (a 0-1 multiplicative factor), `maxDelayMs` 30000. `shouldReconnect` receives the close `code`, `reason`, and `wasClean` and can veto a retry. The retry budget applies per outage. Successful `open` resets the budget, callback attempt, and backoff exponent; the next outage starts at retry 1. `maxDelayMs` caps the final delay after jitter and rounding, also bounded by `2_147_483_647` ms. See [withWebSocketReconnect](/api/core/withwebsocketreconnect) for validation rules and overflow behavior. Set these with `withWebSocketHandle`, `withWebSocketBeforeConnect`, `withWebSocketHeartbeat`, `withWebSocketProtocols`, and `withWebSocketReconnect`. `WebSocketExecuteOptions` can override `beforeConnect`, `heartbeat`, `protocols`, and `reconnect` per call. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | handle | `WebSocketHandleConstructor \| undefined` | optional | WebSocket constructor; defaults to `globalThis.WebSocket`. | | beforeConnect | `((context: { attempt: number; signal: AbortSignal; }) => void \| Promise) \| undefined` | optional | Hook before each connect/reconnect attempt. | | heartbeat | `{ intervalMs: number; isAck?: (message: unknown) => boolean; message?: () => T \| unknown; timeoutMs?: number; } \| undefined` | optional | Periodic heartbeat ping and optional ACK matching. | | protocols | `readonly string[] \| undefined` | optional | Subprotocols passed to the WebSocket constructor. | | reconnect | `{ attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean; }) => boolean; } \| undefined` | optional | Automatic reconnect policy after unexpected closes. | ## Overview [Overview](/api/client) @defjs/core · type ```ts type QuerySerializer = (query: { [key: string]: unknown }) => string; ``` Serializes an encoded query object into a query string. `query` is the wire object: aliases already applied, `Date` values already ISO strings, `bigint` values already strings. Nested objects and arrays are preserved. The string is used verbatim and is not passed through `URLSearchParams`. Interceptors see `queryParams` as `new URLSearchParams(queryString)`. Set it with `withQuerySerializer`. Without one, queries use `URLSearchParams`: arrays repeat keys and a nested object fails. A builder that returns `URLSearchParams` skips the serializer. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | query | `{ [key: string]: unknown; }` | required | Encoded query object. | ## Returns The query string without a leading `?`. ## Overview [Overview](/api/client) @defjs/core · type ```ts type WebSocketHandle = Omit< WebSocket, | "binaryType" | "CLOSED" | "CLOSING" | "CONNECTING" | "dispatchEvent" | "onclose" | "onerror" | "onmessage" | "onopen" | "OPEN" | "readyState" > & { readonly readyState: number }; ``` Minimal WebSocket instance surface used by the client transport. It is the DOM `WebSocket` without its event-handler properties, `binaryType`, `dispatchEvent`, and the state constants. A custom implementation therefore needs `url`, `protocol`, `extensions`, `bufferedAmount`, `send`, `close`, `addEventListener`, `removeEventListener`, and a numeric `readyState`. The transport sets `binaryType = 'arraybuffer'` when the property exists and ignores its absence. If the setter throws, it requests best-effort closure of the constructed handle before failing that attempt with `NET_UNREACHABLE`; the socket is not left outside cleanup. This property is an optimization, and the decoder also accepts Blob frames. This is the instance type produced by a [WebSocketHandleConstructor](/api/core/websockethandleconstructor-type). ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | readyState | `number` | required | | ## Overview [Overview](/api/client) @defjs/core · type ```ts type WebSocketHandleConstructor = { readonly OPEN: number; } & (new (...args: never[]) => WebSocketHandle); ``` Constructor type accepted by `withWebSocketHandle`. A class or function that builds a [WebSocketHandle](/api/core/websockethandle-type) and exposes the numeric `OPEN` ready-state constant. The transport compares `readyState` with `OPEN`. It is called as `new WebSocketImpl(url)`, or `new WebSocketImpl(url, protocols)` when subprotocols are set. The default is `globalThis.WebSocket`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | OPEN | `number` | required | | ## Overview [Overview](/api/client) @defjs/core · function ```ts function defineRequest< const M extends string, const P extends string, TInput extends AnyStruct | undefined = undefined, TOutput extends AnyStruct | undefined = undefined, TError extends AnyStruct | undefined = undefined, >( definition: ResponseDeclaration & { input?: TInput; method: M; operation?: string; path: P; } & BuilderField, HttpRequestShape, NoInfer>>, ): RequestEndpoint< EndpointArgs, RequestSuccessData, RequestErrorData, DeclaredResponse >; ``` Declares a typed HTTP request endpoint. Pass method, path, an optional input struct, and the single `output` and `error` structs. Call the returned operation with a client and, when declared, the input. The contract is `method`, `path`, an optional `input` struct, optional `output` and `error` structs, and an optional `operation`. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. `ResponseDeclaration` and `BuilderField` are internal helper types: `output` and `error` are required only when the matching type parameter is a struct, and `builder` is `(input: BuilderInput) => HttpRequestShape` when `input` is declared. `output` is one struct for the 2xx body; `error` is one struct for every non-2xx body. Neither is keyed by status. A declaration is an assertion: omitting one means that body is never read. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | definition | `ResponseDeclaration & { input?: TInput; method: M; operation?: string; path: P; } & BuilderField, HttpRequestShape, NoInfer>>` | required | Request contract (method, path, input, builder, output). | ## Returns An operation `(client, ...input) => Promise`. The checked example file passes the whole input object as `path`. This snippet maps individual fields into `path` and `query` instead: ## Examples ```ts const getUser = defineRequest({ method: 'GET', path: '/users/:id', input: struct.object({ id: struct.number(), verbose: struct.boolean() }), builder: (input) => ({ path: { id: input.id }, query: { verbose: input.verbose } }), output: struct.object({ id: struct.number(), name: struct.string() }), }) ``` ```ts import { defineRequest, struct } from "@defjs/core"; const getUser = defineRequest({ method: "GET", path: "/users/:id", input: struct.object({ id: struct.number() }), builder: (input) => ({ path: input }), output: struct.object({ id: struct.number(), name: struct.string() }), error: struct.object({ message: struct.string() }), }); ``` ## Overview [Overview](/api/http) @defjs/core · function ```ts function fetchHandler( httpRequest: HttpRequest, fetchImpl?: FetchHandle, ): Promise>; ``` Default HTTP transport: perform `httpRequest` with `fetch` and return an `HttpResponse`. The client always uses it; `withHTTPHandle` only supplies the `fetchImpl` it calls. Also usable in tests and custom handlers. `fetchImpl` defaults to the global `fetch` resolved at send time. It rejects, rather than resolving with a synthetic response, when no response reached the client. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | httpRequest | `HttpRequest` | required | Normalized request (URL, headers, body, signal, progress hooks). | | fetchImpl | `FetchHandle` | optional | Fetch implementation; defaults to global `fetch`. The library default handle is called as `(url, init)` while the global is untouched, and its body is read in one piece unless download progress is observed. Any other handle receives a `Request` and its body is read chunk by chunk. | ## Returns Parsed `HttpResponse`. Rejects when no response reached the client, when reading the body failed, or when an upload observer threw (`EXT_OBSERVER_FAILED`). Cancellation still wins. ## Overview [Overview](/api/http) @defjs/core · function ```ts function makeResponse( options?: MakeResponseOptions, ): HttpResponse; ``` Builds an `HttpResponse` without a network call, for interceptors that short-circuit `next` and for tests. Default status is `0`, so pass a 2xx `status` for a success. `ok` is true for 2xx. The value it returns goes through the same media-type check and the same declared struct as one off the wire; there is no trusted short-circuit. For a JSON body, set a JSON `content-type` header. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | options | `MakeResponseOptions` | optional | Status, headers, body, and an optional request to copy headers/url from; defaults yield status `0`. | ## Returns An `HttpResponse` with `ok` derived from the status code. ## Overview [Overview](/api/http) @defjs/core · type ```ts type RequestDefinition< TInput extends AnyStruct | undefined = undefined, TOutput extends AnyStruct | undefined = undefined, TError extends AnyStruct | undefined = undefined, > = ResponseDeclaration & { builder?: OutboundBuilder; input?: TInput; method: string; operation?: string; path: string; }; ``` Contract for an HTTP endpoint: method, path, optional input, and the structs that decode its bodies. Declaring `input` requires an explicit `builder` to place the parsed value on the request. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. `output` declares the body for a 2xx response, `error` the body for anything else. Only one of the two ever runs, and omitting one means that body is never read. `ResponseDeclaration` is an internal helper type: `output` and `error` are required only when the matching type parameter is a struct. There is no `responseType`: the struct kind selects how a declared body is read. Pass to `defineRequest` to get a typed endpoint. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | output | `TOutput \| undefined` | optional | | | error | `TError \| undefined` | optional | | | builder | `OutboundBuilder \| undefined` | optional | | | input | `TInput \| undefined` | optional | | | method | `string` | required | | | operation | `string \| undefined` | optional | | | path | `string` | required | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type RequestEndpoint< Args extends unknown[], Data, ErrorData, Response extends HttpMeta, > = ( client: Client, ...args: Args ) => Promise< | [error: null, result: Data, response: Response] | [error: FaultOf, result: undefined, response: undefined] >; ``` Operation returned by `defineRequest`. The first argument is a [Client](/api/core/client-type). Further arguments are the input, and only the input: none when the definition declares no `input`, optional when the input struct accepts `undefined`, and required otherwise. Calling it starts the request and returns the error-first tuple. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | client | `Client` | required | | | ...args | `Args` | optional | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type BuilderInput = S extends CarriedStruct ? NullishBuilderInput : S extends UnionStruct ? BuilderInput : S extends DiscriminatedUnionStruct ? BuilderInput : S extends IntersectionStruct ? IntersectBuilderInputs : S extends RecordStruct ? { [key: string]: BuilderInput } : S extends TupleStruct ? { [K in keyof Items]: Items[K] extends StructLike< unknown, unknown, boolean > ? BuilderInput : Items[K]; } : S extends ArrayStruct ? BuilderInput[] : S extends ObjectStruct ? { [K in keyof Shape]: BuilderInput } & Typed : Infer; ``` Parsed endpoint input that a request builder receives. Object nodes are intersected with the internal `Typed` brand, so they carry the struct that produced them and alias, `Date`, and `bigint` encoding follow the value into the request. Record entries and tuple items are branded the same way. At runtime a record container is also tagged when the parsed value is an object, whether or not it was merged. Array and tuple containers are not. `null` and `undefined` are never tagged. Nullish modifiers recover the struct they wrapped. Union options are distributed; intersection options are combined. Any other struct falls back to `Infer`. `CarriedStruct`, `NullishBuilderInput`, `IntersectBuilderInputs`, and `Typed` are internal helper types. ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpRequestShape

= { body?: Uppercase extends "GET" | "HEAD" ? never : BodyValue; headers?: HeadersInit | FlatRecord; path?: PathValue

; query?: URLSearchParams | QueryRecord; }; ``` Request shape an HTTP builder returns: path, query, headers, and body. `P` is the endpoint path and `M` its method. `GET` and `HEAD` cannot return a body. `BodyValue` is a wire value Fetch sends as is (`string`, `Blob`, `ArrayBuffer`, a typed array or `DataView`, `FormData`, `URLSearchParams`, `ReadableStream`) or JSON data (`number`, `boolean`, `bigint`, `Date`, `null`, an array, or an object type with string keys); an `interface` value or class instance needs `{ ...value }`. `path` values must be scalars. A header value may be a scalar or an array of scalars, and `HeadersInit` is accepted. `query` may nest, and a nested object needs `withQuerySerializer` at runtime. `FlatRecord`, `QueryRecord`, `PathValue`, and `BodyValue` are internal helper types. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | body | `(Uppercase extends "GET" \| "HEAD" ? never : BodyValue) \| undefined` | optional | Request body. Not allowed for GET or HEAD. Other methods send a native body or encoded JSON, depending on the value. | | headers | `FlatRecord \| HeadersInit \| undefined` | optional | HeadersInit or a flat record of scalars or scalar arrays. Put cross-request policies on the Client. | | path | `PathValue

\| undefined` | optional | Explicit values for path placeholders. Anonymous objects use template keys; typed objects apply their struct aliases. | | query | `QueryRecord \| URLSearchParams \| undefined` | optional | URLSearchParams passes through unchanged; objects are encoded first. Configure the Client's query serializer for nested query values. | ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpExecuteOptions = UseRequestConfig; ``` HTTP fields read from [RequestOptions](/api/core/requestoptions-type) for one call. Progress callbacks and `signal`. There is no `timeout` or `abort`. ## Overview [Overview](/api/http) @defjs/core · type ```ts type UseRequestConfig = { signal?: AbortSignal; onDownloadProgress?: HttpProgressFn; onUploadProgress?: HttpProgressFn; }; ``` HTTP fields of [RequestOptions](/api/core/requestoptions-type): progress callbacks and `signal`. `onDownloadProgress` and `onUploadProgress` are [HttpProgressFn](/api/core/httpprogressfn-type) callbacks. They receive an [HttpProgressEvent](/api/core/httpprogressevent-type) with `loaded`, `total`, and `lengthComputable`. Other request fields are ignored by HTTP. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | signal | `AbortSignal \| undefined` | optional | Aborts the call. | | onDownloadProgress | `HttpProgressFn \| undefined` | optional | Observes download progress. | | onUploadProgress | `HttpProgressFn \| undefined` | optional | Observes upload progress. | ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpAwaitResult = | [ error: null, result: TData, response: [TData] extends [undefined] ? HttpMeta : DecodedResponse, ] | [error: FaultOf, result: undefined, response: undefined]; ``` Await-result tuple for an HTTP call: success is `[null, data, response]`, and failure is `[fault, undefined, undefined]`. `TData` and `TErrorData` are decoded body types. On success the third slot carries `body` only when `output` was declared. On failure it is `undefined`, because the fault already holds whatever response metadata existed. ## Overview [Overview](/api/http) @defjs/core · type ```ts type RequestSuccessData = DeclaredBody; ``` Decoded success body for the declared `output` struct, or `undefined` when none is declared. ## Overview [Overview](/api/http) @defjs/core · type ```ts type RequestErrorData = DeclaredBody; ``` Decoded error body for the declared `error` struct, or `undefined` when none is declared. ## Overview [Overview](/api/http) @defjs/core · type ```ts interface HttpRequest { signal?: AbortSignal; baseEndpoint?: string; body?: | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream | object | string | number | boolean | null; bodyContentType?: string | null; downloadProgress?: HttpProgressFn; endpoint: string; headers?: Headers; method: string; operation?: string; queryParams?: URLSearchParams; queryString?: string; readonly responseType?: HttpResponseType; readonly errorResponseType?: HttpResponseType; uploadProgress?: HttpProgressFn; withCredentials?: boolean; xsrf?: { cookieName: string; headerName: string; tokenProvider?: (context: { request: HttpRequest; }) => string | null | undefined; }; } ``` Normalized outgoing HTTP request seen by transports, handlers, interceptors, and `fetchHandler` after the builder runs. `body` is already the wire value. For JSON that is a **JSON text string**; sign HMAC or digests against that string (or other final bytes), not the original struct input. - `bodyContentType` holds the inferred type of the builder's body. `null` suppresses that header. FormData owns its multipart boundary. HTTP inference follows a direct write to `body` and is not copied by object spread; an explicit `bodyContentType` assignment is copied. A replacement body is detected again, so declare its type with a `content-type` header. For the unchanged body, `null` suppresses library inference when there is no explicit header, but native Fetch may add its own string type. - HTTP never writes inferred Content-Type into `headers`; interceptors see only declared headers. An explicit `content-type` header always wins over `bodyContentType` and detection, whether set by the builder, client defaults, an interceptor, or a raw `fetchHandler` request. `body: undefined` has no library-inferred Content-Type. Signers should read `headers.get('content-type') ?? request.bodyContentType`; do not configure internal fields. - `endpoint` is the path with path parameters filled in; `baseEndpoint` is the client endpoint. - Interceptors see `queryParams` as `new URLSearchParams(queryString)`. A builder that returns `URLSearchParams` passes that instance through. - `operation` is a static, low-cardinality endpoint identity for telemetry and diagnostics. - `downloadProgress` and `uploadProgress` are the `onDownloadProgress` and `onUploadProgress` hooks of `HttpExecuteOptions`. - `responseType` and `errorResponseType` are readonly: they are derived from the endpoint's `output` and `error` struct kinds and must not be changed. Each is set only when its struct is declared. An interceptor that short-circuits with `makeResponse` reads them to match the transport. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | signal | `AbortSignal \| undefined` | optional | | | baseEndpoint | `string \| undefined` | optional | Trusted absolute base; its origin and directory bound the resolved endpoint path. | | body | `string \| number \| boolean \| object \| ArrayBuffer \| Blob \| FormData \| ReadableStream> \| URLSearchParams \| null \| undefined` | optional | | | bodyContentType | `string \| null \| undefined` | optional | Current body media type; null suppresses the header. FormData owns its boundary. HTTP inference follows direct body mutations and is not copied by object spread; explicit assignments are copied. | | downloadProgress | `HttpProgressFn \| undefined` | optional | | | endpoint | `string` | required | Relative path validated after building and again when an interceptor calls the transport. | | headers | `Headers \| undefined` | optional | HTTP interceptors see declared headers; detected Content-Type is applied at the Fetch boundary. | | method | `string` | required | | | operation | `string \| undefined` | optional | Static, low-cardinality endpoint identity for telemetry and diagnostics. | | queryParams | `URLSearchParams \| undefined` | optional | | | queryString | `string \| undefined` | optional | | | responseType | `HttpResponseType \| undefined` | optional | Representation for a 2xx body, derived from the endpoint `output` struct kind. Set only when `output` is declared. Interceptors that short-circuit with `makeResponse` read this to match the transport. Must not be changed. `undefined` leaves the 2xx body unread. | | errorResponseType | `HttpResponseType \| undefined` | optional | Representation for a non-2xx body, derived from the endpoint `error` struct kind. Set only when `error` is declared. Must not be changed. `undefined` leaves the non-2xx body unread and does not fall back to `responseType`. | | uploadProgress | `HttpProgressFn \| undefined` | optional | | | withCredentials | `boolean \| undefined` | optional | | | xsrf | `{ cookieName: string; headerName: string; tokenProvider?: (context: { request: HttpRequest; }) => string \| null \| undefined; } \| undefined` | optional | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpMeta = { readonly headers: Headers; readonly ok: boolean; readonly status: number; readonly statusText: string; readonly url: string; }; ``` Response metadata, available whenever a response reached the client. It carries no `body`: a body only exists once a declared struct decoded it. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | headers | `Headers` | required | | | ok | `boolean` | required | | | status | `number` | required | | | statusText | `string` | required | | | url | `string` | required | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type DecodedResponse = HttpMeta & { readonly body: TBody }; ``` Response metadata plus a body that decoded against a declared struct. Holding this type is the proof that decoding happened; a failed decode is reported as a fault carrying `HttpMeta` alone. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | headers | `Headers` | required | | | ok | `boolean` | required | | | status | `number` | required | | | statusText | `string` | required | | | url | `string` | required | | | body | `TBody` | required | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpResponse = { readonly [HTTP_RESPONSE]: true; readonly url: string; readonly status: number; readonly statusText: string; readonly headers: Headers; readonly body: R | null | undefined; readonly ok: boolean; }; ``` Wire-shape HTTP response that transports produce and interceptors accept. `body` is parsed JSON, text, a `Blob`, or an `ArrayBuffer`, according to `responseType`, not a decoded struct value. `body` is `undefined` when no body was present; a JSON `null` stays `null`. For a JSON representation, a string is a JSON string root and is not parsed again; bytes and blobs are raw and are parsed once. `ok` is true for status codes in the 2xx range. A transport failure is a fault, never a response. `DecodedResponse` is what reaches the caller. The symbol key is an internal brand. Build values with `makeResponse`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | url | `string` | required | | | status | `number` | required | | | statusText | `string` | required | | | headers | `Headers` | required | | | body | `R \| null \| undefined` | required | Absent when no body was present. JSON `null` stays `null`. | | ok | `boolean` | required | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpResponseType = "arraybuffer" | "blob" | "json" | "text"; ``` Representation used to read an HTTP body before struct validation. Chosen from the declared struct kind: `string` is `text`, `blob` and `file` are `blob`, `arrayBuffer` is `arraybuffer` (bytes), and every other kind is `json`. Not a field on `defineRequest`. It appears on `HttpRequest` as `responseType` and `errorResponseType`. ## Overview [Overview](/api/http) @defjs/core · type ```ts interface HttpProgressEvent { readonly lengthComputable: boolean; readonly loaded: number; readonly total: number; } ``` Progress snapshot passed to upload and download callbacks. Mirrors the `ProgressEvent` fields `loaded`, `total`, and `lengthComputable`. `loaded` counts bytes read from the upload or download stream, not bytes acknowledged by the server. Download bytes may already be decompressed. Unknown lengths use `total: 0` and `lengthComputable: false`. A download total is computable only with a positive `Content-Length` and an absent or `identity` `Content-Encoding`; compressed downloads, other encoded responses, and bytes exceeding `Content-Length` have an unknown decoded length. A streamed FormData upload also has an unknown total unless a valid explicit `Content-Length` supplies it. Other unknown-length streams use the same `0` / `false` fields. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | lengthComputable | `boolean` | required | | | loaded | `number` | required | | | total | `number` | required | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type HttpProgressFn = (event: HttpProgressEvent) => Promise | void; ``` Callback invoked as request bytes are uploaded or response bytes are downloaded. Passed as `onDownloadProgress` or `onUploadProgress` in `HttpExecuteOptions`. Callbacks may be async and are awaited before the next chunk is processed. A callback that throws or rejects fails the call with `EXT_OBSERVER_FAILED`, with the thrown value as `cause`, for downloads and uploads alike. Caller cancellation still takes precedence and reports `NET_ABORTED`, or `NET_TIMEOUT` when the abort reason is a timeout. Upload progress for a `Blob`, `File`, `ArrayBuffer`, or `FormData` body is reported only where the runtime can stream request bodies (Bun, Node). In browsers that body is sent unchanged and `onUploadProgress` is not called. A caller-supplied `ReadableStream` still reports progress wherever the runtime accepts it. Download progress is unaffected. On server runtimes, FormData with `onUploadProgress` reads its serialized stream directly, keeps its multipart boundary, and is sent without a library-generated `Content-Length` (chunked over HTTP/1.1), so progress and abort work without buffering the whole body. It reports an unknown total unless a valid explicit `Content-Length` supplies it. For an endpoint that requires `Content-Length`, such as some presigned upload targets, omit `onUploadProgress`; platform Fetch then sends FormData with its length. Non-identity encoded downloads also report `total: 0` and `lengthComputable: false`, since `loaded` counts decoded stream bytes. Cancellation releases the wrapped upload stream. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | event | `HttpProgressEvent` | required | | ## Overview [Overview](/api/http) @defjs/core · type ```ts type MakeResponseOptions = { status?: number; statusText?: string; url?: string; headers?: Headers; body?: R | null; request?: HttpRequest; }; ``` Fields accepted by `makeResponse` when building a synthetic `HttpResponse`, for example in interceptors. `status` defaults to `0` and `statusText` to `''`. `url` and `headers` default to values taken from `request`: its resolved URL (base endpoint, path and query) and a copy of its headers. A request without `baseEndpoint` gives its `endpoint` as written. Without a request, `url` is `''` and `headers` is empty. An omitted `body` stays `undefined`; only an explicit `null` is a JSON `null`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | status | `number \| undefined` | optional | | | statusText | `string \| undefined` | optional | | | url | `string \| undefined` | optional | | | headers | `Headers \| undefined` | optional | | | body | `R \| null \| undefined` | optional | | | request | `HttpRequest \| undefined` | optional | | ## Overview [Overview](/api/http) @defjs/core · constant Facade object for building protocol-boundary structs and parsing values with its constructors and `struct.parse`. Prefer this object over importing the constructor functions one by one. Compose shapes with `struct.object`, then parse with `struct.parse` or let HTTP/SSE/WebSocket endpoints apply the same contracts at the wire boundary. `struct` is a plain object of helpers. ```ts import { struct } from "@defjs/core"; const User = struct.object({ id: struct.number(), displayName: struct.string().alias("display_name"), }); function readUser() { const [error, user] = struct.parse(User, { id: 1, displayName: "Ada" }); if (error) { console.error(error.message); return; } console.log(user.displayName); } readUser(); ``` ### Primitives and containers | Helper | Role | | -------------------------------------------------------------------------------- | --------------------------------------------------------- | | [struct.string](/api/core/struct-string)() | Text | | [struct.number](/api/core/struct-number)() | Number, `NaN` rejected | | [struct.boolean](/api/core/struct-boolean)() | Boolean | | [struct.bigint](/api/core/struct-bigint)() | `bigint`, or a string that parses as one | | [struct.date](/api/core/struct-date)() | `Date`, from a `Date`, timestamp, or string | | [struct.literal](/api/core/struct-literal)(value) | Exact literal | | [struct.null](/api/core/struct-null)() | Only `null` | | [struct.any](/api/core/struct-any)() / unknown() ([struct.unknown](/api/core/struct-unknown)) | Any value except `null` and `undefined` | | [struct.array](/api/core/struct-array)(item) | Array of one item struct | | [struct.tuple](/api/core/struct-tuple)(items) | Fixed-length array; `items` is one array of structs | | [struct.record](/api/core/struct-record)(value) | Plain object with string keys and one value struct | | [struct.object](/api/core/struct-object)(shape) | Plain object with fixed fields; unknown keys are stripped | | [struct.or](/api/core/struct-or)(...options) | First matching option | | [struct.intersection](/api/core/struct-intersection)(...structs) | Every struct must match; object results are merged | | [struct.discriminatedUnion](/api/core/struct-discriminatedunion)(tag, options) | Object option picked by the value of the `tag` field | | [struct.enum](/api/core/struct-enum)(values) | String array, or the values of a `{ Name: value }` object | ### Body codecs and binary values | Helper | Role | | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | [struct.formData](/api/core/struct-formdata)(shape) / urlencoded(shape) ([struct.urlencoded](/api/core/struct-urlencoded)) | Object struct plus a codec tag. The tag applies only when that value is the request body root | | [struct.arrayBuffer](/api/core/struct-arraybuffer)() / blob() ([struct.blob](/api/core/struct-blob)) / file() ([struct.file](/api/core/struct-file)) | Binary values. As a body root they are sent as bytes; as `output` / `error` they select that representation | | [struct.string](/api/core/struct-string)() | Text. As a body root or an inbound `string` struct, the bytes stay text | ### Parsing and modifiers [struct.parse](/api/core/struct-parse)(struct, value, options?) parses an unknown value and returns `[null, value]` or `[StructError, undefined]`. Every struct has the chainable modifiers `.optional()`, `.null()` / `.nullable()`, `.nullish()`, and `.alias(name)`. See [StructMethods](/api/core/structmethods-type). ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | any | `() => Struct` | required | Struct that accepts any value except null and undefined, without validation. The value is returned unchanged, and nothing inside it is validated or stripped. - Chain `.null()`, `.optional()`, or `.nullish()` to accept `null` or `undefined`. - The output type is `any`. Prefer [struct.unknown](/api/core/struct-unknown)() when the caller should narrow the value. | | array | `>(item: S) => ArrayStruct` | required | Struct that parses an array with one item struct. - Parses every element with `item` and outputs a new array. A non-array fails with `invalid_type` (expected `array`). - Parsing stops at the first failing element, and `issue.path` holds its index. - Throws `TypeError` when `item` is not a struct. `ArrayStruct` is not exported. It is a `Struct[], Infer[]>`. | | arrayBuffer | `() => Struct` | required | Struct that accepts an ArrayBuffer. - Accepts an `ArrayBuffer` only. Typed arrays and other views fail with `invalid_type`. - As a body root it is sent as bytes with `application/octet-stream`. - As an `output` or `error` struct it makes the response body be read as an `ArrayBuffer`. | | bigint | `() => Struct` | required | Struct that accepts a bigint or a bigint string and outputs a bigint. - Accepts a `bigint`, or a string that `BigInt()` can parse. A `number` is rejected. - Output is always a `bigint`; an unparsable string fails with `invalid_type`. - When encoded for the wire, the value becomes a decimal string. | | blob | `() => Struct` | required | Struct that accepts a Blob. - Accepts a `Blob` (a `File` is a `Blob`). Anything else fails with `invalid_type`. - As a body root it is sent as is, with the blob's own type or `application/octet-stream`. - As an `output` or `error` struct it makes the response body be read as a `Blob`. | | boolean | `() => Struct` | required | Struct that accepts boolean values. Accepts `true` and `false` only. Other values, including `'true'` and `1`, fail with `invalid_type`. | | date | `() => Struct` | required | Struct that accepts a Date, timestamp, or date string and outputs a Date. - Accepts a `Date`, a millisecond timestamp, or a string. A valid `Date` is returned as the same object. A number or string is converted with `new Date(value)`. - Output is a `Date`. A value that produces an invalid date fails with `invalid_type`. - When encoded for the wire, the value becomes an ISO 8601 string (`toISOString()`). | | discriminatedUnion | `, ...ObjectStruct[]]>(discriminator: TDiscriminator, options: TOptions & RequiredDiscriminatorOptions) => DiscriminatedUnionStruct` | required | Struct that picks one object struct by the value of a tag field. ```ts const Event = struct.discriminatedUnion("kind", [ struct.object({ kind: struct.literal("created"), id: struct.number() }), struct.object({ kind: struct.literal("deleted"), id: struct.number() }), ]); ``` - Every option must be an object struct whose `discriminator` field is a required [struct.literal](/api/core/struct-literal)(). A missing field, a non-literal field, an optional field, or a duplicate tag value throws `TypeError` when the struct is created. - The input must be a plain object. The tag value selects the option, and that option parses the whole object. - A missing tag fails with `missing_key`, and an unknown tag fails with `invalid_union`. Both report the tag field's path. Other failures come from the selected option. - With `struct.parse(..., { aliases: true })` the tag is read from the field's alias. `RequiredDiscriminatorOptions` and `DiscriminatedUnionStruct` are not exported. The type check rejects an option whose tag field is missing or may be `undefined`. | | enum | `{ (value: T): Struct, EnumValue>; (value: T): Struct, EnumValue>; }` | required | Struct that accepts one value from a list of strings or from the values of an object. - With a non-empty string array, the accepted values are the array items. - With an object, the accepted values are the object's string and number **values**, not its keys. `{ A: 'a', B: 2 }` accepts `'a'` and `2`. - A TypeScript numeric `enum` object also accepts its member names, because the reverse mapping is part of its values. - A value outside the set fails with `invalid_enum`. An object with no string or number value throws `TypeError` when the struct is created. | | file | `() => Struct` | required | Struct that accepts a File. - Accepts a `File` only; a plain `Blob` fails with `invalid_type`. - Use it for a field of [struct.formData](/api/core/struct-formdata)(), or for a raw file body. - As an `output` or `error` struct it makes the response body be read as a blob. | | formData | `(shape: T) => ObjectStruct` | required | Object struct that is encoded as multipart FormData when it is the request body. Parses like [struct.object](/api/core/struct-object)(), and carries a `formData` codec tag. - The tag applies only when this struct's value is the request body root. The body is then sent as `FormData` with a platform multipart boundary. - Arrays repeat the field name, `undefined` fields are skipped, and `Blob` / `File` values are appended as files. A nested object fails the request with `REQ_BUILD_FAILED`. - `struct.parse` ignores the tag. It does not accept a `FormData` instance, only a plain object. | | intersection | `, ...StructLike[]]>(...structs: T) => T extends readonly [unknown] ? T[0] : T extends readonly [unknown, unknown, ...unknown[]] ? IntersectionStruct : T[0] \| IntersectionStruct` | required | Struct that requires a value to satisfy every given struct. `struct.intersection` is variadic: pass each struct as its own argument. - Parses the input with every struct and merges the results. - When every struct is an object struct, the output is one object with the fields of all of them. Keys that none of them declares are removed. - When two structs produce incompatible values for the same key, it fails with `invalid_intersection`. - Accepts `null` only when every struct accepts it, including a nested intersection marked `.null()`. - A single struct is returned unchanged, so it keeps its own identity and type, including `.optional()` and `.null()`. Calling it with no structs throws `TypeError`, and so does a non-struct argument. - A nonempty variadic tuple must account for that singleton. Spreading a tuple that may contain only one member infers both the singleton and multi-member results. Its object fields may therefore be optional. A tuple known to contain at least two members keeps the required intersection semantics. `IntersectionStruct` is not exported. It is a `Struct` whose input and output are the intersections of the member types. | | literal | `(value: T) => Struct` | required | Struct that accepts exactly one boolean, null, number, or string value. - Accepts only `value`. The comparison uses `Object.is`, so any other value fails with `invalid_literal`. - `value` must be a `boolean`, `null`, `number`, or `string`. - Use it for the tag field of [struct.discriminatedUnion](/api/core/struct-discriminatedunion)(). | | null | `() => Struct` | required | Struct that accepts only null. Accepts `null` only. Every other value fails with `invalid_type`. That includes `undefined` at the root or in an array or tuple; a missing object or record field reports `missing_key` instead. This is not the `.null()` modifier. The modifier adds `null` to the values another struct accepts. | | number | `() => NumberStruct` | required | Struct that accepts number values except NaN. - Accepts `number` values, including `Infinity`. `NaN` and non-numbers fail with `invalid_type`. - Strings are not coerced. | | object | `(shape: T) => ObjectStruct` | required | Struct that parses a plain object with a fixed set of fields. `shape` maps field names to structs. - Every field is **required** by default. A missing key fails with `missing_key`. - `.optional()` and `.nullish()` allow a missing key. An absent optional field stays absent in the output. - `.null()` / `.nullable()` allow `null`. - `.alias(name)` sets the field's wire key. `struct.parse` reads logical keys by default; `{ aliases: true }` and HTTP JSON decoding read wire keys, and HTTP JSON encoding writes them. Parsed output uses logical keys. - Keys that are not in `shape` are removed from the output. The output is a new object with a `null` prototype. - The input must be a plain object. Arrays, `Map`, and class instances fail with `invalid_type` (expected `object`). - Parsing stops at the first failing field, and `issue.path` holds its key. - Throws `TypeError` when `shape` is not a plain object. `ObjectShape` is `{ [key: string]: any }`; each value must be a struct. | | or | `(...options: T) => UnionStruct` | required | Struct that accepts a value matching any one of several structs. `struct.or` is variadic: pass each option as its own argument. - Tries the options in order and returns the result of the first one that parses. - If none matches, it fails with one `invalid_union` issue at the union's own path. The issues of the individual options are not reported. - Throws `TypeError` when an option is not a struct. `UnionStruct` is not exported. It is a `Struct` whose input and output are the unions of the option types, without `undefined`. For tagged objects prefer [struct.discriminatedUnion](/api/core/struct-discriminatedunion)(), which reports the issue of the selected option. | | parse | `>(struct: S, value: unknown, options?: { aliases?: boolean; errorMap?: ErrorMap; }) => ParseResult` | required | Parses an unknown value with a struct and returns an error-first tuple. - **struct** is the struct to parse with. A value that is not a struct throws `TypeError`. - **value** is the unknown input. - **options.aliases** defaults to `false`: object keys are read by their **logical** names (`displayName`). With `true` they are read by their wire names (`display_name`), the way HTTP/JSON codecs read them. The output always uses logical names. - **options.errorMap** is an [ErrorMap](/api/core/errormap-type) that rewrites issue messages for this call only, including bigint/date conversion failures from a struct created by another Core copy. Any string, including `''`, replaces the message; only `undefined` keeps the default. Custom text is used verbatim. - Parsing is fail-fast: it stops at the first failure, so the error holds one issue. Unknown object keys are stripped from the output, and the input is not modified. - At most **256 nested containers** (`object`, `array`, `record`, or `tuple`) are visited along a path, counting the root container as the first. The next container fails with `invalid_type` at its logical path, with `expected: 'value nested at most 256 levels deep'`. In a union, later candidates may still succeed; if none match, the union reports `invalid_union`. Cycles through declared container fields also reach this limit; shared non-cyclic children are allowed. Union and intersection branches do not add a container level, and `any`/`unknown` values remain opaque rather than being traversed. ```ts import { struct } from "@defjs/core"; const User = struct.object({ id: struct.number(), displayName: struct.string() }); function readUser() { const [error, user] = struct.parse(User, { id: 1, displayName: "Ada" }); if (error) { console.error(error.prettify()); return; } console.log(user.displayName); } readUser(); ``` `any` and `unknown` do not traverse their values, but an intersection may recursively merge two distinct object or array values returned by those leaves. This merge shares the same 256-container budget with its enclosing logical path. Exceeding it produces `invalid_intersection` at the rejected container's logical path. Identical references are retained without traversal, including a shared cyclic value. | | record | `>(value: S) => RecordStruct` | required | Struct that parses an object with string keys and one value struct. - Parses every own key of a plain object with `value`. Anything else fails with `invalid_type` (expected `record`). - Parsing stops at the first failing value, and `issue.path` holds its key. - The output is a new object with a `null` prototype. - Throws `TypeError` when `value` is not a struct. `RecordStruct` is not exported. It is a `Struct<{ [key: string]: StructInput }, { [key: string]: Output }>`, where `Output` is `Infer` without `undefined`. When `S` is an optional struct, a value that is `undefined` omits its key from the output. | | string | `() => StringStruct` | required | Struct that accepts string values. - Accepts `string` values only; anything else fails with `invalid_type`. - As a body root, or as an inbound `output` / `error` struct, the bytes stay text instead of being parsed as JSON. | | tuple | `, ...StructLike[]]>(items: T) => TupleStruct` | required | Struct that parses a fixed-length array with one struct per position. `struct.tuple` takes **one array** of structs, not separate arguments: ```ts const Pair = struct.tuple([struct.string(), struct.number()]); ``` - Each position is parsed with the struct at the same index. The input must be an array of exactly the same length, otherwise it fails with `invalid_type` (expected `tuple of length N`). - Passing the structs as separate arguments, `struct.tuple(a, b)`, throws a `TypeError`. - Throws `TypeError` when an item is not a struct. `TupleStruct` is not exported. It is a `Struct` whose input and output are tuples of the item types. | | unknown | `() => UnknownStruct` | required | Struct that accepts any value except null and undefined, typed as unknown. The value is returned unchanged, and nothing inside it is validated or stripped. - Chain `.null()`, `.optional()`, or `.nullish()` to accept `null` or `undefined`. - The output type is `unknown`, so the caller must narrow it before use. | | urlencoded | `(shape: T) => ObjectStruct` | required | Object struct that is encoded as an URL-encoded form when it is the request body. Parses like [struct.object](/api/core/struct-object)(), and carries a `urlencoded` codec tag. - The tag applies only when this struct's value is the request body root. The body is then sent as `URLSearchParams` with `application/x-www-form-urlencoded;charset=UTF-8`. - Arrays repeat the field name and `undefined` fields are skipped. A nested object or a `Blob` fails the request with `REQ_BUILD_FAILED`. - `struct.parse` ignores the tag. It does not accept a `URLSearchParams` instance, only a plain object. | ## Examples ```ts const User = struct.object({ id: struct.number(), displayName: struct.string().alias('display_name'), }) const getUser = defineRequest({ method: 'GET', path: '/users/:id', input: struct.object({ id: struct.number() }), builder: (input) => ({ path: input }), output: User, }) function readUser() { const [error, user] = struct.parse(User, { id: 1, displayName: 'Ada' }) if (error) { console.error(error.message) return } console.log(user.displayName) } readUser() ``` ## Overview [Overview](/api/struct) @defjs/core · function ```ts function any(): Struct; ``` Struct that accepts any value except null and undefined, without validation. The value is returned unchanged, and nothing inside it is validated or stripped. - Chain `.null()`, `.optional()`, or `.nullish()` to accept `null` or `undefined`. - The output type is `any`. Prefer [struct.unknown](/api/core/struct-unknown)() when the caller should narrow the value. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function array< S extends StructLike, >(item: S): ArrayStruct; ``` Struct that parses an array with one item struct. - Parses every element with `item` and outputs a new array. A non-array fails with `invalid_type` (expected `array`). - Parsing stops at the first failing element, and `issue.path` holds its index. - Throws `TypeError` when `item` is not a struct. `ArrayStruct` is not exported. It is a `Struct[], Infer[]>`. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | item | `S` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function arrayBuffer(): Struct; ``` Struct that accepts an ArrayBuffer. - Accepts an `ArrayBuffer` only. Typed arrays and other views fail with `invalid_type`. - As a body root it is sent as bytes with `application/octet-stream`. - As an `output` or `error` struct it makes the response body be read as an `ArrayBuffer`. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function bigint(): Struct; ``` Struct that accepts a bigint or a bigint string and outputs a bigint. - Accepts a `bigint`, or a string that `BigInt()` can parse. A `number` is rejected. - Output is always a `bigint`; an unparsable string fails with `invalid_type`. - When encoded for the wire, the value becomes a decimal string. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function blob(): Struct; ``` Struct that accepts a Blob. - Accepts a `Blob` (a `File` is a `Blob`). Anything else fails with `invalid_type`. - As a body root it is sent as is, with the blob's own type or `application/octet-stream`. - As an `output` or `error` struct it makes the response body be read as a `Blob`. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function boolean(): Struct; ``` Struct that accepts boolean values. Accepts `true` and `false` only. Other values, including `'true'` and `1`, fail with `invalid_type`. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function date(): Struct; ``` Struct that accepts a Date, timestamp, or date string and outputs a Date. - Accepts a `Date`, a millisecond timestamp, or a string. A valid `Date` is returned as the same object. A number or string is converted with `new Date(value)`. - Output is a `Date`. A value that produces an invalid date fails with `invalid_type`. - When encoded for the wire, the value becomes an ISO 8601 string (`toISOString()`). ## Overview [Overview](/api/struct) @defjs/core · function ```ts function discriminatedUnion< const TDiscriminator extends string, const TOptions extends readonly [ ObjectStruct, ...ObjectStruct[], ], >( discriminator: TDiscriminator, options: TOptions & RequiredDiscriminatorOptions, ): DiscriminatedUnionStruct; ``` Struct that picks one object struct by the value of a tag field. ```ts const Event = struct.discriminatedUnion("kind", [ struct.object({ kind: struct.literal("created"), id: struct.number() }), struct.object({ kind: struct.literal("deleted"), id: struct.number() }), ]); ``` - Every option must be an object struct whose `discriminator` field is a required [struct.literal](/api/core/struct-literal)(). A missing field, a non-literal field, an optional field, or a duplicate tag value throws `TypeError` when the struct is created. - The input must be a plain object. The tag value selects the option, and that option parses the whole object. - A missing tag fails with `missing_key`, and an unknown tag fails with `invalid_union`. Both report the tag field's path. Other failures come from the selected option. - With `struct.parse(..., { aliases: true })` the tag is read from the field's alias. `RequiredDiscriminatorOptions` and `DiscriminatedUnionStruct` are not exported. The type check rejects an option whose tag field is missing or may be `undefined`. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | discriminator | `TDiscriminator` | required | | | options | `TOptions & RequiredDiscriminatorOptions` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function enum( value: T, ): Struct, EnumValue>; ``` ```ts function enum( value: T, ): Struct, EnumValue>; ``` Struct that accepts one value from a list of strings or from the values of an object. - With a non-empty string array, the accepted values are the array items. - With an object, the accepted values are the object's string and number **values**, not its keys. `{ A: 'a', B: 2 }` accepts `'a'` and `2`. - A TypeScript numeric `enum` object also accepts its member names, because the reverse mapping is part of its values. - A value outside the set fails with `invalid_enum`. An object with no string or number value throws `TypeError` when the struct is created. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | value | `T` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function file(): Struct; ``` Struct that accepts a File. - Accepts a `File` only; a plain `Blob` fails with `invalid_type`. - Use it for a field of [struct.formData](/api/core/struct-formdata)(), or for a raw file body. - As an `output` or `error` struct it makes the response body be read as a blob. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function formData( shape: T, ): ObjectStruct; ``` Object struct that is encoded as multipart FormData when it is the request body. Parses like [struct.object](/api/core/struct-object)(), and carries a `formData` codec tag. - The tag applies only when this struct's value is the request body root. The body is then sent as `FormData` with a platform multipart boundary. - Arrays repeat the field name, `undefined` fields are skipped, and `Blob` / `File` values are appended as files. A nested object fails the request with `REQ_BUILD_FAILED`. - `struct.parse` ignores the tag. It does not accept a `FormData` instance, only a plain object. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | shape | `T` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function intersection< const T extends readonly [ StructLike, ...StructLike[], ], >( ...structs: T ): T extends readonly [unknown] ? T[0] : T extends readonly [unknown, unknown, ...unknown[]] ? IntersectionStruct : T[0] | IntersectionStruct; ``` Struct that requires a value to satisfy every given struct. `struct.intersection` is variadic: pass each struct as its own argument. - Parses the input with every struct and merges the results. - When every struct is an object struct, the output is one object with the fields of all of them. Keys that none of them declares are removed. - When two structs produce incompatible values for the same key, it fails with `invalid_intersection`. - Accepts `null` only when every struct accepts it, including a nested intersection marked `.null()`. - A single struct is returned unchanged, so it keeps its own identity and type, including `.optional()` and `.null()`. Calling it with no structs throws `TypeError`, and so does a non-struct argument. - A nonempty variadic tuple must account for that singleton. Spreading a tuple that may contain only one member infers both the singleton and multi-member results. Its object fields may therefore be optional. A tuple known to contain at least two members keeps the required intersection semantics. `IntersectionStruct` is not exported. It is a `Struct` whose input and output are the intersections of the member types. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | ...structs | `T` | optional | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function literal( value: T, ): Struct; ``` Struct that accepts exactly one boolean, null, number, or string value. - Accepts only `value`. The comparison uses `Object.is`, so any other value fails with `invalid_literal`. - `value` must be a `boolean`, `null`, `number`, or `string`. - Use it for the tag field of [struct.discriminatedUnion](/api/core/struct-discriminatedunion)(). ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | value | `T` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function null(): Struct; ``` Struct that accepts only null. Accepts `null` only. Every other value fails with `invalid_type`. That includes `undefined` at the root or in an array or tuple; a missing object or record field reports `missing_key` instead. This is not the `.null()` modifier. The modifier adds `null` to the values another struct accepts. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function number(): NumberStruct; ``` Struct that accepts number values except NaN. - Accepts `number` values, including `Infinity`. `NaN` and non-numbers fail with `invalid_type`. - Strings are not coerced. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function object( shape: T, ): ObjectStruct; ``` Struct that parses a plain object with a fixed set of fields. `shape` maps field names to structs. - Every field is **required** by default. A missing key fails with `missing_key`. - `.optional()` and `.nullish()` allow a missing key. An absent optional field stays absent in the output. - `.null()` / `.nullable()` allow `null`. - `.alias(name)` sets the field's wire key. `struct.parse` reads logical keys by default; `{ aliases: true }` and HTTP JSON decoding read wire keys, and HTTP JSON encoding writes them. Parsed output uses logical keys. - Keys that are not in `shape` are removed from the output. The output is a new object with a `null` prototype. - The input must be a plain object. Arrays, `Map`, and class instances fail with `invalid_type` (expected `object`). - Parsing stops at the first failing field, and `issue.path` holds its key. - Throws `TypeError` when `shape` is not a plain object. `ObjectShape` is `{ [key: string]: any }`; each value must be a struct. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | shape | `T` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function or( ...options: T ): UnionStruct; ``` Struct that accepts a value matching any one of several structs. `struct.or` is variadic: pass each option as its own argument. - Tries the options in order and returns the result of the first one that parses. - If none matches, it fails with one `invalid_union` issue at the union's own path. The issues of the individual options are not reported. - Throws `TypeError` when an option is not a struct. `UnionStruct` is not exported. It is a `Struct` whose input and output are the unions of the option types, without `undefined`. For tagged objects prefer [struct.discriminatedUnion](/api/core/struct-discriminatedunion)(), which reports the issue of the selected option. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | ...options | `T` | optional | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function parse< S extends StructLike, >( struct: S, value: unknown, options?: { aliases?: boolean; errorMap?: ErrorMap }, ): ParseResult; ``` Parses an unknown value with a struct and returns an error-first tuple. - **struct** is the struct to parse with. A value that is not a struct throws `TypeError`. - **value** is the unknown input. - **options.aliases** defaults to `false`: object keys are read by their **logical** names (`displayName`). With `true` they are read by their wire names (`display_name`), the way HTTP/JSON codecs read them. The output always uses logical names. - **options.errorMap** is an [ErrorMap](/api/core/errormap-type) that rewrites issue messages for this call only, including bigint/date conversion failures from a struct created by another Core copy. Any string, including `''`, replaces the message; only `undefined` keeps the default. Custom text is used verbatim. - Parsing is fail-fast: it stops at the first failure, so the error holds one issue. Unknown object keys are stripped from the output, and the input is not modified. - At most **256 nested containers** (`object`, `array`, `record`, or `tuple`) are visited along a path, counting the root container as the first. The next container fails with `invalid_type` at its logical path, with `expected: 'value nested at most 256 levels deep'`. In a union, later candidates may still succeed; if none match, the union reports `invalid_union`. Cycles through declared container fields also reach this limit; shared non-cyclic children are allowed. Union and intersection branches do not add a container level, and `any`/`unknown` values remain opaque rather than being traversed. ```ts import { struct } from "@defjs/core"; const User = struct.object({ id: struct.number(), displayName: struct.string() }); function readUser() { const [error, user] = struct.parse(User, { id: 1, displayName: "Ada" }); if (error) { console.error(error.prettify()); return; } console.log(user.displayName); } readUser(); ``` `any` and `unknown` do not traverse their values, but an intersection may recursively merge two distinct object or array values returned by those leaves. This merge shares the same 256-container budget with its enclosing logical path. Exceeding it produces `invalid_intersection` at the rejected container's logical path. Identical references are retained without traversal, including a shared cyclic value. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | struct | `S` | required | | | value | `unknown` | required | | | options | `{ aliases?: boolean; errorMap?: ErrorMap; }` | optional | | ## Returns `[null, output]` or `[StructError, undefined]`. Validation failures do not throw. Exceptions from caller-provided getters and `errorMap` callbacks propagate unchanged. ## See also [Struct](/api/core/struct-type) · [ParseResult](/api/core/parseresult-type) ## Overview [Overview](/api/struct) @defjs/core · function ```ts function record< S extends StructLike, >(value: S): RecordStruct; ``` Struct that parses an object with string keys and one value struct. - Parses every own key of a plain object with `value`. Anything else fails with `invalid_type` (expected `record`). - Parsing stops at the first failing value, and `issue.path` holds its key. - The output is a new object with a `null` prototype. - Throws `TypeError` when `value` is not a struct. `RecordStruct` is not exported. It is a `Struct<{ [key: string]: StructInput }, { [key: string]: Output }>`, where `Output` is `Infer` without `undefined`. When `S` is an optional struct, a value that is `undefined` omits its key from the output. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | value | `S` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function string(): StringStruct; ``` Struct that accepts string values. - Accepts `string` values only; anything else fails with `invalid_type`. - As a body root, or as an inbound `output` / `error` struct, the bytes stay text instead of being parsed as JSON. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function tuple< const T extends readonly [ StructLike, ...StructLike[], ], >(items: T): TupleStruct; ``` Struct that parses a fixed-length array with one struct per position. `struct.tuple` takes **one array** of structs, not separate arguments: ```ts const Pair = struct.tuple([struct.string(), struct.number()]); ``` - Each position is parsed with the struct at the same index. The input must be an array of exactly the same length, otherwise it fails with `invalid_type` (expected `tuple of length N`). - Passing the structs as separate arguments, `struct.tuple(a, b)`, throws a `TypeError`. - Throws `TypeError` when an item is not a struct. `TupleStruct` is not exported. It is a `Struct` whose input and output are tuples of the item types. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | items | `T` | required | | ## Overview [Overview](/api/struct) @defjs/core · function ```ts function unknown(): UnknownStruct; ``` Struct that accepts any value except null and undefined, typed as unknown. The value is returned unchanged, and nothing inside it is validated or stripped. - Chain `.null()`, `.optional()`, or `.nullish()` to accept `null` or `undefined`. - The output type is `unknown`, so the caller must narrow it before use. ## Overview [Overview](/api/struct) @defjs/core · function ```ts function urlencoded( shape: T, ): ObjectStruct; ``` Object struct that is encoded as an URL-encoded form when it is the request body. Parses like [struct.object](/api/core/struct-object)(), and carries a `urlencoded` codec tag. - The tag applies only when this struct's value is the request body root. The body is then sent as `URLSearchParams` with `application/x-www-form-urlencoded;charset=UTF-8`. - Arrays repeat the field name and `undefined` fields are skipped. A nested object or a `Blob` fails the request with `REQ_BUILD_FAILED`. - `struct.parse` ignores the tag. It does not accept a `URLSearchParams` instance, only a plain object. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | shape | `T` | required | | ## Overview [Overview](/api/struct) @defjs/core · type ```ts type Infer = StructOutput; ``` Infers the parsed output type of a struct, as an alias of the internal output brand. The type a successful parse produces. Use `Infer` for the decoded value of a struct, and `StructInput` for the accepted input type. A type that is not a struct yields `never`. ```ts const User = struct.object({ id: struct.number(), name: struct.string() }); type User = Infer; // { id: number; name: string } ``` ## Overview [Overview](/api/struct) @defjs/core · type ```ts type StructInput = T extends { readonly _struct: { readonly input: unknown }; } ? T["_struct"]["input"] : never; ``` Type helper that extracts the accepted input type of a struct. The type a struct accepts, as opposed to [Infer](/api/core/infer-type), which is what a successful parse returns. `T` is a struct, or any [StructLike](/api/core/structlike-type). Anything else gives `never`. The two differ when a struct converts its input: ```ts const Created = struct.date(); type In = StructInput; // Date | number | string type Out = Infer; // Date ``` ## Overview [Overview](/api/struct) @defjs/core · type ```ts type AnyStruct = Struct; ``` Broad struct constraint that accepts any struct, for endpoint and client generics. Used by generics such as `defineRequest` and `Fault`. Prefer a concrete `Struct<…>` when you need precise input and output inference. ## Overview [Overview](/api/struct) @defjs/core · type ```ts type Struct< Input = unknown, Output = Input, OptionalOut extends boolean = false, > = StructMethods & StructLike; ``` Public struct contract: input and output types, modifier methods, and the `_struct` type brand. A parseable shape. `Input` is what it accepts ([StructInput](/api/core/structinput-type)), `Output` is what a successful parse returns ([Infer](/api/core/infer-type)), and `OptionalOut` marks a field that may be absent from object output. Build structs with the [struct](/api/core/struct) facade (`struct.string()`, `struct.object()`, …). Every modifier in [StructMethods](/api/core/structmethods-type) returns a new struct and leaves the original unchanged. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | alias | `(name: string) => Struct` | required | Override the external wire name for this field without changing the TypeScript property name. | | null | `() => Struct` | required | Allow `null` in addition to the current input and output types. | | nullable | `() => Struct` | required | Alias for StructMethods.null. | | nullish | `() => Struct` | required | Allow `null` or `undefined`, marking the field optional in object output. | | optional | `() => Struct` | required | Allow `undefined`, marking the field optional in object shapes. | | _struct | `StructTypes` | required | Phantom payload. `optionalOut` is `true` only when this struct's output fields may be absent. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts interface StructLike< I = unknown, O = unknown, OO extends boolean = boolean, > { readonly _struct: StructTypes; } ``` Minimal structural brand carried by every struct instance. Anything with a `_struct` type payload. `_struct` holds input and output phantom types: [StructInput](/api/core/structinput-type) and [Infer](/api/core/infer-type) read `input` and `output` from it, and object structs use `optionalOut` to decide which output fields may be absent. `_struct` exists only in the type system. Structs have no `_struct` property at runtime, and a plain object with that type is not a valid struct. `input` and `output` are the phantom types. `optionalOut` is `true` when the third type argument is `true`, and `undefined` otherwise. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | _struct | `StructTypes` | required | Phantom payload. `optionalOut` is `true` only when this struct's output fields may be absent. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts interface StructMethods { alias(name: string): this; null(): Struct; nullable(): Struct; nullish(): Struct; optional(): Struct; } ``` Chainable modifiers available on every `Struct` instance. Each method returns a new struct and leaves the original unchanged. - `alias(name)` sets the wire name of an object field. `struct.parse` reads logical names unless `aliases: true` is passed; JSON and flat request codecs write the alias. A non-string `name` throws `TypeError`. - `null()` and `nullable()` are the same: they also accept `null`. - `optional()` also accepts `undefined`. In an object, the key may be missing, and an absent field stays absent in the output. - `nullish()` accepts both `null` and `undefined`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | alias | `(name: string) => StructMethods` | required | Override the external wire name for this field without changing the TypeScript property name. | | null | `() => Struct` | required | Allow `null` in addition to the current input and output types. | | nullable | `() => Struct` | required | Alias for StructMethods.null. | | nullish | `() => Struct` | required | Allow `null` or `undefined`, marking the field optional in object output. | | optional | `() => Struct` | required | Allow `undefined`, marking the field optional in object shapes. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts interface ObjectStruct extends StructMethods, ObjectOutput, false>, StructLike, ObjectOutput, false> { readonly _struct: ObjectStructTypes; null(): PreservedStruct< ObjectStruct, ObjectInput | null, ObjectOutput | null, false >; nullable(): PreservedStruct< ObjectStruct, ObjectInput | null, ObjectOutput | null, false >; nullish(): PreservedStruct< ObjectStruct, ObjectInput | null | undefined, ObjectOutput | null | undefined, true >; optional(): PreservedStruct< ObjectStruct, ObjectInput | undefined, ObjectOutput | undefined, true >; } ``` Object-shaped struct type produced by `struct.object(shape)`. Fields are required by default. A field marked `optional()` or `nullish()` becomes an optional key, so field presence follows those modifiers. Unknown keys are stripped when parsing. Wire names can differ from TypeScript keys via `.alias()`. A failed parse returns `[StructError, undefined]`. `ObjectInput`, `ObjectOutput`, `ObjectStructTypes`, and `PreservedStruct` are internal helper types. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | _struct | `ObjectStructTypes` | required | Phantom payload. `optionalOut` is `true` only when this struct's output fields may be absent. | | null | `() => PreservedStruct, ObjectInput \| null, ObjectOutput \| null, false>` | required | Allow `null` in addition to the current input and output types. | | nullable | `() => PreservedStruct, ObjectInput \| null, ObjectOutput \| null, false>` | required | Alias for StructMethods.null. | | nullish | `() => PreservedStruct, ObjectInput \| null \| undefined, ObjectOutput \| null \| undefined, true>` | required | Allow `null` or `undefined`, marking the field optional in object output. | | optional | `() => PreservedStruct, ObjectInput \| undefined, ObjectOutput \| undefined, true>` | required | Allow `undefined`, marking the field optional in object shapes. | | alias | `(name: string) => ObjectStruct` | required | Override the external wire name for this field without changing the TypeScript property name. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts type ParseResult = [error: null, value: O] | [error: StructError, value: undefined]; ``` Error-first tuple returned by `struct.parse`. On success the error is `null` and `value` is the parsed output `O`. On failure the error is a `StructError` and `value` is always `undefined`. ## Overview [Overview](/api/struct) @defjs/core · type ```ts type ErrorMap = (issue: StructIssue) => string | undefined; ``` Optional callback that rewrites a [StructIssue](/api/core/structissue-type) message before it is stored. Passed as the `errorMap` option of `struct.parse`. It is called for each issue (`code`, `expected`, `message`, `path`, `received`) before the issue is recorded. Any string, including `''`, replaces the default message; only `undefined` keeps it. The map belongs to this parse call and also handles bigint/date conversion failures from a struct created by another Core copy. Default messages omit received values; custom text is used verbatim, so avoid including sensitive `received` values. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | issue | `StructIssue` | required | Issue about to be recorded. | ## Returns Replacement message, or `undefined` to keep the default. ## Overview [Overview](/api/struct) @defjs/core · type ```ts class StructError extends Error { readonly issues: StructIssue[]; constructor(issues: StructIssue[]); format(): FormattedStructError; flatten(): FlattenedStructError; prettify(): string; } ``` Error returned by a failed struct parse, aggregating one or more [StructIssue](/api/core/structissue-type)s and their formatters. `name` is `'StructError'`. Construct it with `constructor(issues: StructIssue[])`. `issues` is the `readonly` list of those issues. `message` is the issue's message for a single issue, `N struct issues: ` for several, and `Struct parse failed` for none. Default issue messages name the received kind and omit the value, so `message` and `prettify()` can be logged. For example, `Expected number at id, received string`. `message` and the formatters use that text. The raw value stays on `StructIssue.received`; custom `errorMap` text is used verbatim and can reintroduce sensitive values. Use `format()`, `flatten()`, or `prettify()` to present issues to callers. - `struct.parse` stops at the first failure, so `issues` holds one [StructIssue](/api/core/structissue-type) there. - `format()` returns a [FormattedStructError](/api/core/formattedstructerror-type) tree. Each node has an `_errors` array, and nested keys follow `issue.path`. - `flatten()` returns a [FlattenedStructError](/api/core/flattenedstructerror-type): messages with an empty path go to `formErrors`, the others are grouped by the first path segment in `fieldErrors`. - `prettify()` returns one `× path: message` line per issue. An empty path is shown as ``. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | issues | `StructIssue[]` | required | Issues collected for this failure, in encounter order. | | format | `() => FormattedStructError` | required | Build a nested error tree keyed by path segments. | | flatten | `() => FlattenedStructError` | required | Split issues into root `formErrors` and first-segment `fieldErrors`. | | prettify | `() => string` | required | Render a multi-line, human-readable summary of all issues. Each line uses the issue message, which names the received kind and omits the value. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts interface StructIssue { code: | "custom" | "invalid_enum" | "invalid_intersection" | "invalid_literal" | "invalid_type" | "invalid_union" | "missing_key"; expected: string; message: string; path: Path; received: unknown; } ``` A single validation failure produced while parsing a struct. - `code` is the machine-readable category. The parser produces `invalid_type`, `invalid_literal`, `invalid_enum`, `invalid_union`, `invalid_intersection`, and `missing_key`; `custom` is part of the type, but the built-in structs never produce it. - `expected` describes the expected type or value. - `message` is the final text, after an optional [ErrorMap](/api/core/errormap-type) rewrite. - `path` runs from the parse root to the failing location: field names and array indices. It is empty for a root-level failure. - `received` is the value that failed at `path`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `"custom" \| "invalid_enum" \| "invalid_intersection" \| "invalid_literal" \| "invalid_type" \| "invalid_union" \| "missing_key"` | required | Machine-readable failure category. | | expected | `string` | required | Human-readable description of the expected type or value. | | message | `string` | required | Final message after optional `ErrorMap` rewriting. The default names the received kind, not the value. | | path | `Path` | required | Path from the parse root to the failing location. | | received | `unknown` | required | Value that failed validation at `path`. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts interface FormattedStructError { _errors: string[]; [key: string]: FormattedStructError | string[]; } ``` Nested error tree returned by `StructError.format()`. Leaf messages live under `_errors`; nested keys mirror the failing path. A path segment literally named `_errors` is stored as `\_errors`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | _errors | `string[]` | required | | ## Overview [Overview](/api/struct) @defjs/core · type ```ts interface FlattenedStructError { formErrors: string[]; fieldErrors: { [key: string]: string[] }; } ``` Flat field and form error bags returned by `StructError.flatten()`. `formErrors` holds messages of issues with an empty path (root-level issues). `fieldErrors` holds messages keyed by the first path segment, so nested issues are grouped under their top-level field. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | formErrors | `string[]` | required | Messages whose path is empty (root-level issues). | | fieldErrors | `{ [key: string]: string[]; }` | required | Messages keyed by the first path segment. | ## Overview [Overview](/api/struct) @defjs/core · type ```ts type FaultCode = | "CAP_BUFFER_EXCEEDED" | "CAP_QUEUE_OVERFLOW" | "ENV_UNSUPPORTED" | "EXT_HOOK_FAILED" | "EXT_INTERCEPTOR_FAILED" | "EXT_OBSERVER_FAILED" | "HTTP_STATUS" | "NET_ABORTED" | "NET_BODY_INCOMPLETE" | "NET_TIMEOUT" | "NET_UNREACHABLE" | "REQ_BUILD_FAILED" | "REQ_INPUT_INVALID" | "REQ_OPTIONS_INVALID" | "RES_DECODE_FAILED" | "RES_MEDIA_TYPE_INVALID" | "RES_STRUCT_MISMATCH"; ``` Every failure Defjs reports, as one closed set of codes. The prefix before the first `_` is the class; there is no separate `kind` field. Read the prefix for coarse triage and the whole code for an exact case: | Class | Meaning | | ------ | -------------------------------------------------------------- | | `HTTP` | The peer answered with a non-2xx status. | | `REQ` | The request never left: input, request options, or build. | | `NET` | No usable response: cancel, timeout, connect, truncated body. | | `RES` | A response arrived but could not be read as declared. | | `EXT` | Code you attached to the pipeline threw. | | `CAP` | A declared buffer or queue limit was exceeded. | | `ENV` | The host runtime lacks a required capability. | The set is closed on purpose: it keeps `switch (fault.code)` exhaustive. Extensions report their own detail through `cause`, not through a new code. ## Overview [Overview](/api/errors) @defjs/core · type ```ts type FaultClass = C extends `${infer TClass}_${string}` ? TClass : never; ``` Extracts the class prefix from a fault code: the segment before its first `_`. `FaultClass` is `'CAP' | 'ENV' | 'EXT' | 'HTTP' | 'NET' | 'REQ' | 'RES'`. ## Overview [Overview](/api/errors) @defjs/core · type ```ts type Fault = FaultOf< ErrorBody >; ``` Union of every failure an operation can return, discriminated by `code`. `Fault` is [FaultOf](/api/core/faultof-type) applied to the decoded body type, so it is the union of [DecodeFault](/api/core/decodefault-type), [HttpStatusFault](/api/core/httpstatusfault-type)``, and [PreflightFault](/api/core/preflightfault-type). `ErrorBody` is `undefined` when `TErr` is `undefined`, and `Infer` otherwise. Switch on `fault.code`. Every variant is a native `Error` named `DefjsFault`, so `String(fault)` produces `DefjsFault: `. `code` and the variant metadata — `status`, `response`, `data` — are enumerable own properties. The native `cause` chain is non-enumerable. Default Struct messages say `received `, never the received value. A JSON `SyntaxError` behind `RES_DECODE_FAILED` gives the message `Response body could not be decoded`. Structural paths retain record keys, which may be sensitive. Raw `StructIssue.received` and parser causes remain sensitive; custom `errorMap` and extension messages are used verbatim and need review before logging. Call `format()`, `flatten()`, or `prettify()` only after narrowing `fault.cause` to `StructError`; those helpers are not copied onto the fault. ## Examples ```ts import { StructError, type Fault } from "@defjs/core"; function logFault(fault: Fault): void { console.error(String(fault), { code: fault.code }); if (fault.cause instanceof StructError) { console.error(fault.cause.prettify()); } } ``` ## Overview [Overview](/api/errors) @defjs/core · type ```ts type FaultOf = DecodeFault | HttpStatusFaultOf | PreflightFault; ``` Any failure returned by an operation, expressed by its decoded error-body type. `Fault` is `FaultOf` applied to `Infer`, and `HttpAwaitResult` uses `FaultOf`. Pass a decoded type, not a struct: write `FaultOf>`, or use `Fault`. ## Overview [Overview](/api/errors) @defjs/core · type ```ts type HttpStatusFault = HttpStatusFaultOf>; ``` Fault for a non-2xx response, typed by the declared `error` struct. `TErr` is the endpoint's declared `error` struct, or `undefined` when it declares none. Any non-2xx status. When the endpoint declared `error` and the body decoded, `data` is that body and `response` carries it. Omitting `error` skips decoding, which leaves `data` as `undefined` and `response` without a `body` field. `ErrorBody` is that decoded body: `undefined` when no error struct was declared, otherwise `Infer`. [HttpStatusFaultOf](/api/core/httpstatusfaultof-type) is the member shape. ## Overview [Overview](/api/errors) @defjs/core · type ```ts type HttpStatusFaultOf = Error & { code: "HTTP_STATUS"; data: TData; response: [TData] extends [undefined] ? HttpMeta : DecodedResponse; status: number; }; ``` Non-2xx fault expressed by its decoded body type instead of the struct that produced it. Factories work in decoded values, so they use this form; endpoint types use `HttpStatusFault` and let `Infer` bridge the two. When `TData` is `undefined`, the body was never read and `response` is metadata alone. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `"HTTP_STATUS"` | required | | | data | `TData` | required | | | response | `[TData] extends [undefined] ? HttpMeta : DecodedResponse` | required | | | status | `number` | required | | ## Overview [Overview](/api/errors) @defjs/core · type ```ts type DecodeFault = Error & { cause: unknown; code: DecodeFaultCode; response: HttpMeta; status: number; }; ``` A response arrived but could not be read as declared. `code` is one of `RES_DECODE_FAILED`, `RES_MEDIA_TYPE_INVALID`, or `RES_STRUCT_MISMATCH`. `response` is metadata only: a body is a decoded value, and decoding is what failed, so there is no `body` field to reach for. The offending detail lives on `cause` (a `StructError` for a struct mismatch, the parser failure for an unreadable representation). ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `DecodeFaultCode` | required | | | response | `HttpMeta` | required | | | status | `number` | required | | ## Overview [Overview](/api/errors) @defjs/core · type ```ts type PreflightFault = Error & { cause?: unknown; code: PreflightFaultCode; response?: HttpMeta; }; ``` A failure that may have happened before any response existed. Covers `REQ_*`, `NET_*`, `EXT_*`, `CAP_*`, and `ENV_UNSUPPORTED`. `response` is present only where the transport already had metadata to report, such as a body that truncated mid-download. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `PreflightFaultCode` | required | | | response | `HttpMeta \| undefined` | optional | | ## Overview [Overview](/api/errors) @defjs/core · type ```ts type AnyFault = | DecodeFault | HttpStatusFaultOf | HttpStatusFaultOf | PreflightFault; ``` Any fault at all, whatever its decoded error-body type. Use it for handlers that classify faults without caring which endpoint produced them. Prefer `Fault` where the body type matters. ## Overview [Overview](/api/errors) @defjs/core · function ```ts function createHttpStatusFault( response: DecodedResponse, ): HttpStatusFaultOf; ``` Builds an `HTTP_STATUS` fault for a non-2xx response whose body decoded against the declared `error` struct. The fault is a native `Error` named `DefjsFault` with `code: 'HTTP_STATUS'`, `data` set to `response.body`, `status` copied from the response, and `response` as passed. The message is `Http failure response: `, followed by ` - ` when there is one. Use [createUndecodedHttpStatusFault](/api/core/createundecodedhttpstatusfault) when no body was decoded. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | response | `DecodedResponse` | required | The response, carrying the decoded body. | ## Returns An `HTTP_STATUS` fault whose `data` is that body. ## Overview [Overview](/api/errors) @defjs/core · function ```ts function createUndecodedHttpStatusFault( response: HttpMeta, ): HttpStatusFaultOf; ``` Builds an `HTTP_STATUS` fault for a non-2xx response that was never decoded, because the endpoint declared no `error` struct. The fault is a native `Error` named `DefjsFault` with `code: 'HTTP_STATUS'`, `data: undefined`, `status` copied from the response, and `response` as passed. Use [createHttpStatusFault](/api/core/createhttpstatusfault) when the body was decoded. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | response | `HttpMeta` | required | Metadata of the response; no body was read. | ## Returns An `HTTP_STATUS` fault whose `data` is `undefined`. ## Overview [Overview](/api/errors) @defjs/core · function ```ts function createDecodeFault( code: DecodeFaultCode, cause: unknown, response: HttpMeta, ): DecodeFault; ``` Builds a decode fault for a response that arrived but could not be read as declared. `code` is `RES_DECODE_FAILED`, `RES_MEDIA_TYPE_INVALID`, or `RES_STRUCT_MISMATCH`. The fault carries metadata only: a body is a decoded value, and decoding is what failed, so there is no `body`. `status` is copied from `response`. The result is a native `Error` named `DefjsFault`. `cause` is the underlying parser or struct failure and is kept as `cause`. Its message normally becomes the fault message (`String(cause)` when it is not an `Error`). For `RES_DECODE_FAILED` with an `Error` named `SyntaxError`, the message is `Response body could not be decoded`, so `String(fault)` does not include a body fragment. Default Struct messages contain received kinds, not values; raw values and parser text remain on the cause. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `DecodeFaultCode` | required | Which read step failed. | | cause | `unknown` | required | Underlying parser or struct failure. | | response | `HttpMeta` | required | Metadata of the response that could not be read. | ## Returns A decode fault with no body. ## Overview [Overview](/api/errors) @defjs/core · function ```ts function createNetworkFault( cause: unknown, response?: HttpMeta, ): PreflightFault; ``` Builds a `NET_*` `PreflightFault`, choosing the code from the shape of `cause`. Optional `response` metadata is attached when the transport already had some to report. The cause is kept as `cause`. - `NET_ABORTED` when [isAbortCause](/api/core/isabortcause) recognizes an abort-branded `Error` (including `ERR_ABORTED` from another Core copy) or a `DOMException` named `AbortError`. The message is `Request was aborted`. - `NET_TIMEOUT` when [isTimeoutCause](/api/core/istimeoutcause) recognizes a timeout-branded `Error` (including `ERR_TIMEOUT` from another Core copy) or a `DOMException` or `Error` named `TimeoutError`. The message is the cause's message, or `Request timed out` when that is empty. - `NET_UNREACHABLE` for anything else, with the cause's message. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | cause | `unknown` | required | Underlying cancellation, timeout, or transport failure. | | response | `HttpMeta` | optional | Metadata, only where the transport already had some to report. | ## Returns A `NET_ABORTED`, `NET_TIMEOUT`, or `NET_UNREACHABLE` fault. ## Overview [Overview](/api/errors) @defjs/core · function ```ts function createPreflightFault( code: PreflightFaultCode, cause?: unknown, response?: HttpMeta, ): PreflightFault; ``` Builds a fault for a failure that may have happened before any response existed. When `cause` is given, its message becomes the fault message; otherwise the code is the message. The result is a native `Error` named `DefjsFault` with `code`, plus `response` when one was given. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `PreflightFaultCode` | required | The specific failure. | | cause | `unknown` | optional | Underlying value, when there is one; the code becomes the message otherwise. | | response | `HttpMeta` | optional | Metadata, only where the transport already had some to report. | ## Returns A preflight fault. ## Overview [Overview](/api/errors) @defjs/core · constant ```ts const ERR_ABORTED: Error; ``` Shared Error sentinel that marks a request as aborted. Its `message` is `Request was aborted`. Shared `cause` and message value for caller cancellation. `createNetworkFault` maps it, and a `DOMException` named `AbortError`, to `NET_ABORTED`. An HTTP interceptor that throws it makes the call resolve with a `NET_ABORTED` fault. SSE and WebSocket interceptors that throw it are reported as `EXT_INTERCEPTOR_FAILED`. The sentinel carries a non-enumerable `Symbol.for('defjs.cause.aborted')` brand. [isAbortCause](/api/core/isabortcause) recognizes this sentinel from any Core copy without comparing object identity. Classification is independent of unrelated earlier executions. ## Overview [Overview](/api/errors) @defjs/core · constant ```ts const ERR_TIMEOUT: Error; ``` Shared Error sentinel that marks a request as timed out. Its `message` is `Request timed out`. Shared `cause` and message value for timeouts. `createNetworkFault` maps it, and any error named `TimeoutError`, to `NET_TIMEOUT`. An HTTP interceptor that throws it makes the call resolve with a `NET_TIMEOUT` fault. SSE and WebSocket interceptors that throw it are reported as `EXT_INTERCEPTOR_FAILED`. The sentinel carries a non-enumerable `Symbol.for('defjs.cause.timeout')` brand. [isTimeoutCause](/api/core/istimeoutcause) recognizes this sentinel from any Core copy without comparing object identity. Classification is independent of unrelated earlier executions. ## Overview [Overview](/api/errors) @defjs/core · function ```ts function isAbortCause(cause: unknown): boolean; ``` Whether `cause` is caller cancellation, across Core copies and platform abort errors. Returns `true` for an `Error` carrying the non-enumerable `Symbol.for('defjs.cause.aborted')` brand, including `ERR_ABORTED` from another Core copy, or a `DOMException` named `AbortError`. An ordinary unbranded `Error` with that name does not match. Other values return `false`. Pass the underlying `fault.cause`; this function does not unwrap faults. `createNetworkFault` uses this check to select `NET_ABORTED`. OpenTelemetry uses it to suppress caller-cancellation logs while preserving timeout logs. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | cause | `unknown` | required | | ## See also [ERR_ABORTED](/api/core/err-aborted-type) · [isTimeoutCause](/api/core/istimeoutcause) ## Overview [Overview](/api/errors) @defjs/core · function ```ts function isTimeoutCause(cause: unknown): boolean; ``` Whether `cause` is a timeout, across Core copies and platform timeout errors. Returns `true` for an `Error` carrying the non-enumerable `Symbol.for('defjs.cause.timeout')` brand, including `ERR_TIMEOUT` from another Core copy, or a `DOMException` or `Error` named `TimeoutError`. Other values return `false`. Pass the underlying `fault.cause`; this function does not unwrap faults. `createNetworkFault` uses this check to select `NET_TIMEOUT`. OpenTelemetry keeps logging timeouts even when the abort signal is also set. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | cause | `unknown` | required | | ## See also [ERR_TIMEOUT](/api/core/err-timeout-type) · [isAbortCause](/api/core/isabortcause) ## Overview [Overview](/api/errors) @defjs/core · function ```ts function createHttpInterceptor(fn: InterceptorFn): HttpInterceptor; ``` Wraps an `InterceptorFn` as a tagged `HttpInterceptor`. Returns `{ kind: 'http', fn }`. Call `next(req)` (optionally with a modified request) or short-circuit with `makeResponse`. `next` runs only the interceptors registered after this one and then the fetch handler. HTTP interceptors do not run for SSE or WebSocket operations. A deterministic URL error from `next(req)` rejects the returned promise, so `.then` or `.catch` recovery runs; if it is not recovered, the operation still reports `REQ_BUILD_FAILED`. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fn | `InterceptorFn` | required | HTTP interceptor function. | ## Returns An `HttpInterceptor` with `kind: 'http'`. ## Overview [Overview](/api/interceptors) @defjs/core · function ```ts function createSSEInterceptor(fn: SSEInterceptorFn): SSEInterceptor; ``` Wraps an `SSEInterceptorFn` as a tagged `SSEInterceptor`. Returns `{ kind: 'sse', fn }`. SSE interceptors run only for SSE operations, in registration order; `next(req)` opens the stream through the remaining interceptors. A deterministic URL error from `next(req)` rejects the returned promise, so `.then` or `.catch` recovery runs; if it is not recovered, the operation still reports `REQ_BUILD_FAILED`. `EventStreamHandle` extends `AsyncDisposable`. A structural short-circuit implementation must provide `[Symbol.asyncDispose](): PromiseLike` and connect it to the same close lifecycle. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fn | `SSEInterceptorFn` | required | SSE interceptor function. | ## Returns An `SSEInterceptor` with `kind: 'sse'`. ## Overview [Overview](/api/interceptors) @defjs/core · function ```ts function createWebSocketInterceptor( fn: WebSocketInterceptorFn, ): WebSocketInterceptor; ``` Wraps a `WebSocketInterceptorFn` as a tagged `WebSocketInterceptor`. Returns `{ kind: 'web-socket', fn }`. WebSocket interceptors run only for WebSocket operations. `next` and the interceptor return a [WebSocketSessionLike](/api/core/websocketsessionlike-type), the interceptor-facing session. A wrapper or short-circuit session must forward its `[Symbol.asyncDispose]()` teardown. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fn | `WebSocketInterceptorFn` | required | WebSocket interceptor function. | ## Returns A `WebSocketInterceptor` with `kind: 'web-socket'`. ## Overview [Overview](/api/interceptors) @defjs/core · function ```ts function basicAuthHttpInterceptor( fn: () => BasicCredential, options?: BasicAuthInterceptorOptions, ): HttpInterceptor; ``` HTTP interceptor that sets an `Authorization: Basic …` header from credentials. It sets `Authorization: Basic ` on a copy of the request headers, replacing any existing `Authorization` header. The default encoder is ``btoa(`${username}:${password}`)``, so credentials must be Latin-1. Without `encode`, calling `basicAuthHttpInterceptor` throws `Error: BasicAuthInterceptor is not supported in this environment` when `btoa` is not available in the runtime. Use [basicAuthSSEInterceptor](/api/core/basicauthsseinterceptor) for SSE operations. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fn | `() => BasicCredential` | required | Factory returning username/password for each request. | | options | `BasicAuthInterceptorOptions` | optional | Optional custom encoder (defaults to `btoa`). | ## Returns An `HttpInterceptor` that injects basic auth. ## Overview [Overview](/api/interceptors) @defjs/core · function ```ts function basicAuthSSEInterceptor( fn: () => BasicCredential, options?: BasicAuthInterceptorOptions, ): SSEInterceptor; ``` SSE interceptor that sets an `Authorization: Basic …` header from credentials. It sets `Authorization: Basic ` on a copy of the request headers, replacing any existing `Authorization` header. Encoding and the `btoa` requirement are the same as for [basicAuthHttpInterceptor](/api/core/basicauthhttpinterceptor), including the synchronous throw when `btoa` is missing. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | fn | `() => BasicCredential` | required | Factory returning username/password for each request. | | options | `BasicAuthInterceptorOptions` | optional | Optional custom encoder (defaults to `btoa`). | ## Returns An `SSEInterceptor` that injects basic auth. ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type Interceptor = HttpInterceptor | SSEInterceptor | WebSocketInterceptor; ``` Any tagged interceptor accepted by client interceptor options. `kind` (`'http'`, `'sse'`, or `'web-socket'`) selects which operations an interceptor runs for. `withInterceptors` accepts any mix; each transport runs only its own kind, in registration order. ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type InterceptorFn = ( req: HttpRequest, next: HttpInterceptorNext, ) => Promise>; ``` HTTP interceptor function: may inspect or modify the request, then call `next`. Call `next(req)` or short-circuit with `makeResponse`. Wrap it with `createHttpInterceptor` to register it. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | req | `HttpRequest` | required | | | next | `HttpInterceptorNext` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts interface HttpInterceptor { kind: "http"; fn: InterceptorFn; } ``` Tagged HTTP interceptor accepted by client interceptor options. Created by `createHttpInterceptor`. `kind` is the discriminant that lets `withInterceptors` accept HTTP, SSE, and WebSocket interceptors in one list. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | kind | `"http"` | required | | | fn | `InterceptorFn` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type HttpInterceptorNext = ( req: HttpRequest, ) => Promise>; ``` Continuation that runs the rest of the HTTP interceptor chain, or the handler. `next(req)` only goes inward: the interceptors registered after this one, then the Fetch handler. It is not a new operation call from outside the chain. Call `next` again for a safe replay (for example one 401 refresh) from the same interceptor; outer layers do not run a second time. Once the chain has settled, calling `next` rejects with `HTTP interceptor next() cannot be called after the chain has settled`. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | req | `HttpRequest` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts interface SSEInterceptor { kind: "sse"; fn: SSEInterceptorFn; } ``` Tagged SSE interceptor for client option lists, accepted by `withInterceptors`. Created by `createSSEInterceptor`. The `kind` tag lets `withInterceptors` accept HTTP, SSE, and WebSocket interceptors in one list; only `kind: 'sse'` interceptors run for SSE operations. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | kind | `"sse"` | required | | | fn | `SSEInterceptorFn` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type SSEInterceptorFn = ( req: HttpRequest, next: SSEHandler, ) => Promise>; ``` SSE interceptor function: may inspect, modify, or replace the request, then call `next`. Call `next` (an [SSEHandler](/api/core/ssehandler-type)) to continue the chain, or return your own `EventStreamHandle` to short-circuit. Wrap it with `createSSEInterceptor` to register it through `withInterceptors`. A short-circuit handle must provide `[Symbol.asyncDispose](): PromiseLike` and connect it to the same close lifecycle. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | req | `HttpRequest` | required | | | next | `SSEHandler` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type SSEHandler = ( req: HttpRequest, ) => Promise>; ``` Terminal `next` function an SSE interceptor receives; it opens an event stream for the request. `next(req)` goes inward only: through the interceptors registered after this one, then the transport that opens the stream. Calling it after the chain has settled rejects. `EventStreamHandle` extends `AsyncDisposable`. A short-circuit implementation that builds its own handle must provide `[Symbol.asyncDispose](): PromiseLike` and connect it to the same close lifecycle. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | req | `HttpRequest` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts interface WebSocketInterceptor { kind: "web-socket"; fn: WebSocketInterceptorFn; } ``` Tagged WebSocket interceptor for client option lists, accepted by `withInterceptors`. Created by `createWebSocketInterceptor`. The `kind` tag lets `withInterceptors` accept HTTP, SSE, and WebSocket interceptors in one list; only `kind: 'web-socket'` interceptors run for WebSocket operations. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | kind | `"web-socket"` | required | | | fn | `WebSocketInterceptorFn` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type WebSocketInterceptorFn = ( req: HttpRequest, next: WebSocketHandler, ) => Promise; ``` WebSocket interceptor function: may inspect, modify, or replace the request, then call `next`. Call `next` (a [WebSocketHandler](/api/core/websockethandler-type)) to continue the chain, or return your own [WebSocketSessionLike](/api/core/websocketsessionlike-type) to short-circuit. Wrap it with `createWebSocketInterceptor` to register it through `withInterceptors`. The chain runs once per operation. Reconnects happen inside the session it returns and do not run it again. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | req | `HttpRequest` | required | | | next | `WebSocketHandler` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type WebSocketHandler = ( req: HttpRequest, ) => Promise; ``` Terminal `next` function a WebSocket interceptor receives; it opens a session for the request. `next(req)` goes inward only: through the interceptors registered after this one, then the transport that opens the session. It resolves to a [WebSocketSessionLike](/api/core/websocketsessionlike-type). It may be called once per execute; a second call, or a call after the chain has settled, rejects. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | req | `HttpRequest` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts interface WebSocketSessionLike extends AsyncDisposable { readonly bufferedAmount: number; readonly connection: { extensions?: string; generation: number; protocol?: string; url?: string; }; readonly closed: Promise; readonly receive: AsyncIterable; readonly state: WebSocketState; close(code?: number, reason?: string): void; [Symbol.asyncDispose](): PromiseLike; onRuntimeError(listener: (error: unknown) => void): () => void; onStateChange(listener: (state: WebSocketState) => void): () => void; send(message: unknown): void; } ``` Minimal WebSocket session surface that interceptors see. Structurally compatible with [WebSocketSession](/api/core/websocketsession-type) without importing that type, which avoids a circular dependency between interceptors and WebSocket. Messages are typed `unknown`. A wrapper or short-circuit session must forward the same `[Symbol.asyncDispose]()` teardown as its `close` / `closed` lifecycle; returning an unrelated resolved promise is not sufficient. Requiring this member is a compile-time breaking change for structural implementations. Code that only accepts sessions returned by Defjs has no additional runtime call requirement. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | bufferedAmount | `number` | required | | | connection | `{ extensions?: string; generation: number; protocol?: string; url?: string; }` | required | | | closed | `Promise` | required | | | receive | `AsyncIterable` | required | | | state | `WebSocketState` | required | | | close | `(code?: number, reason?: string) => void` | required | | | [Symbol.asyncDispose] | `() => PromiseLike` | required | | | onRuntimeError | `(listener: (error: unknown) => void) => () => void` | required | | | onStateChange | `(listener: (state: WebSocketState) => void) => () => void` | required | | | send | `(message: unknown) => void` | required | | ## Overview [Overview](/api/interceptors) @defjs/core · type ```ts type BasicAuthInterceptorOptions = { encode?: (credential: BasicCredential) => string; }; ``` Options for basic-auth interceptors, including a custom credential encoder. `encode` replaces only the credential encoder. The default is ``btoa(`${username}:${password}`)``. The interceptor always prefixes the returned string with `Basic `, so return the credential part alone: returning `'Basic abc'` produces the header `Basic Basic abc`. Accepted by [basicAuthHttpInterceptor](/api/core/basicauthhttpinterceptor) and [basicAuthSSEInterceptor](/api/core/basicauthsseinterceptor). ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | encode | `((credential: BasicCredential) => string) \| undefined` | optional | | ## Overview [Overview](/api/interceptors) @defjs/core · function ```ts function defineEventStream< const P extends string, const M extends string = "GET", TInput extends AnyStruct | undefined = undefined, TEvents extends EventStructs = EventStructs, TError extends AnyStruct | undefined = undefined, >( definition: Omit, "method" | "path"> & EventStreamErrorField & { input?: TInput; method?: M; path: P; } & BuilderField< NoInfer, EventStreamRequestShape, NoInfer> >, ): EventStreamEndpoint, EventStreamData, TError>; ``` Declare a typed Server-Sent Events endpoint. Pass path, event structs, and buffer/queue limits. Call the returned operation with a client and, when declared, the input. - **definition** — `path`, an `events` map, and the required `maxBufferSize` and `maxQueueSize` limits (positive safe integers). Optional `method` (default `'GET'`), `operation`, `input`, and `error`. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. `EventStreamDefinitionBase`, `EventStreamErrorField`, and `BuilderField` are internal helper types: `error` is required only when `TError` is a struct, and `builder` is `(input: BuilderInput) => EventStreamRequestShape` when `input` is declared. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | definition | `Omit, "method" \| "path"> & EventStreamErrorField & { input?: TInput; method?: M; path: P; } & BuilderField< NoInfer, EventStreamRequestShape, NoInfer> >` | required | SSE contract (path, events, buffer limits, optional input). | ## Returns An operation `(client, ...input) => Promise`. ## Examples ```ts import { defineEventStream, struct } from "@defjs/core"; const ticks = defineEventStream({ maxBufferSize: 64 * 1024, maxQueueSize: 100, path: "/ticks", events: { message: struct.object({ text: struct.string() }) }, }); ``` ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStreamDefinition< TInput extends AnyStruct | undefined = undefined, TEvents extends EventStructs = EventStructs, TError extends AnyStruct | undefined = undefined, > = EventStreamDefinitionBase & EventStreamErrorField & { builder?: OutboundBuilder; input?: TInput; }; ``` Contract that describes an SSE endpoint for defineEventStream. The composed members are: - `events`, `maxBufferSize`, and `maxQueueSize` (both limits are required positive safe integers), optional `method`, `operation`, and a required `path`. - `error`, required only when `TError` is a struct. It decodes the body of a failed handshake (a non-2xx response). The kind rule is the same as for HTTP: `string` is text, `blob` and `file` are blobs, `arrayBuffer` is bytes, and everything else is JSON. - `input` and `builder`. The request shape matches HTTP, including `body`; `GET` and `HEAD` cannot return a body. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. `EventStreamDefinitionBase` and `EventStreamErrorField` are internal helper types. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | events | `TEvents` | required | | | maxBufferSize | `number` | required | | | maxQueueSize | `number` | required | | | method | `string \| undefined` | optional | | | operation | `string \| undefined` | optional | | | path | `string` | required | | | error | `TError \| undefined` | optional | | | builder | `OutboundBuilder \| undefined` | optional | | | input | `TInput \| undefined` | optional | | ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStreamEndpoint< Args extends unknown[], Event, TError extends AnyStruct | undefined = undefined, > = ( client: Client, ...args: Args ) => Promise>; ``` Operation returned by `defineEventStream`. The first argument is a [Client](/api/core/client-type). Further arguments are the input only. Calling it opens the stream and returns the existing error-first tuple. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | client | `Client` | required | | | ...args | `Args` | optional | | ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStreamRequestShape< P extends string, M extends string, > = HttpRequestShape; ``` Request shape an SSE builder returns: the same fields as [HttpRequestShape](/api/core/httprequestshape-type) for method `M` and path `P`. ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStreamExecuteOptions = { signal?: AbortSignal; }; ``` SSE field of [RequestOptions](/api/core/requestoptions-type). HTTP progress and WebSocket fields are ignored. `signal` covers the whole stream, including after it opens. There is no startup-only deadline. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | signal | `AbortSignal \| undefined` | optional | Aborts the stream, including after it is open. | ## Overview [Overview](/api/sse) @defjs/core · type ```ts type StreamAwaitResult< TEvent, TError extends AnyStruct | undefined = undefined, > = | [error: null, stream: EventStreamHandle, open: EventStreamOpenInfo] | [ error: Fault, stream: undefined, open: EventStreamOpenInfo | undefined, ]; ``` Await-result tuple from opening an SSE stream. - On success: `[null, stream, open]`. `open` is the returned connection's first successful handshake snapshot for native Defjs handles or wrappers forwarding the same live `open` object; custom handles or wrappers overriding `open` use their current public `open` even after reconnect, while `stream.open` stays live. - On failure: `[fault, undefined, open]`. `open` carries the response snapshot when the failure happened after a response arrived, for example a non-2xx handshake, and is `undefined` otherwise. - `TError` is the declared `error` struct. It types the decoded error body on the fault. ## Overview [Overview](/api/sse) @defjs/core · type ```ts interface EventStreamHandle extends AsyncIterable, AsyncDisposable { readonly open: EventStreamOpenInfo; readonly closed: Promise; close(reason?: unknown): void; [Symbol.asyncDispose](): PromiseLike; } ``` Open SSE stream: iterate events, read open metadata, and close early. `EventStreamMessage` is the raw `{ id: string; event: string; data: string }` message. Handles returned by an event-stream operation carry parsed [EventStreamData](/api/core/eventstreamdata-type). `open` is the startup snapshot ([EventStreamOpenInfo](/api/core/eventstreamopeninfo-type)); after a reconnect it can change, so read `stream.open` again. `closed` resolves with an [EventStreamCloseInfo](/api/core/eventstreamcloseinfo-type) when the stream ends. Prefer `await using stream = handle` for owned scope cleanup. The disposer stops Defjs reading and reconnect work and waits for its reader lock to be released; it does not promise that a provider-controlled, stuck `ReadableStream.cancel()` promise will finish. Manual `close()` / `closed` remains available. ```ts async function consume(handle: EventStreamHandle): Promise { await using stream = handle; for await (const event of stream) console.log(event); } ``` Structural implementations of `EventStreamHandle` must implement `[Symbol.asyncDispose]()` and return the same owned teardown path. This is a compile-time requirement for custom handles; code that only receives handles returned by Defjs has no additional runtime call requirement. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | open | `EventStreamOpenInfo` | required | | | closed | `Promise` | required | | | close | `(reason?: unknown) => void` | required | | | [Symbol.asyncDispose] | `() => PromiseLike` | required | | ## Overview [Overview](/api/sse) @defjs/core · type ```ts interface EventStreamOpenInfo { response: HttpResponse; url: string; } ``` Metadata available once an SSE response has opened. `response` is the handshake response and `url` is the resolved request URL. It is the third element of a successful event-stream result and the value of `EventStreamHandle.open`. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | response | `HttpResponse` | required | | | url | `string` | required | | ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStreamCloseInfo = | (EventStreamCloseInfoBase & { kind: "eof" }) | (EventStreamCloseInfoBase & { kind: "aborted" }) | (EventStreamCloseInfoBase & { code: EventStreamFaultCode; kind: "error" }); ``` How an SSE stream ended: clean EOF, abort, or an error carrying its fault `code`. The discriminant is `kind` so that `code` can mean the same thing it means on a fault. The value of `EventStreamHandle.closed`. `eof` means the response ended normally. `aborted` means the stream was closed with `close()` or disposal, or its caller signal aborted with a non-timeout reason; `reason` and `cause` carry the value passed to `close`. If the caller signal aborts after open with a `TimeoutError` or `ERR_TIMEOUT`, the iterator throws `ERR_TIMEOUT` and `closed` is `{ kind: 'error', code: 'NET_TIMEOUT', ... }`. Explicit `close(timeoutError)` still reports `aborted`. `error` carries the fault `code` (see `EventStreamFaultCode`) and the failure as `cause`. ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStructs = { [key: string]: AnyStruct }; ``` Map from SSE event names to the structs that decode their data. An event with no `event:` field is named `message`. An event whose name has no own entry uses the `default` struct; with neither, the event is skipped and `onInvalidEvent` is called. Declare the event name `__proto__` with a computed own key. A `string` event struct keeps the raw `data` text. Every other kind is JSON text: `JSON.parse`, then the struct. A bare `struct.literal` therefore needs JSON text, not an unquoted token. Reconnect lives on the client (`withSSEReconnect`). SSE `jitter` is a **0-1 multiplicative factor**, same as WebSocket. ## Overview [Overview](/api/sse) @defjs/core · type ```ts type EventStreamData = TEvents extends EventStructs ? KnownEventUnion | DefaultEventUnion : never; ``` Union of parsed SSE events inferred from an EventStructs map. For each declared event name except `default`, the union has `{ event: ; data: Infer; id?: string }`. When a `default` struct is declared, it adds `{ event: string; data: Infer; id?: string }` for every event name that has no struct of its own. `KnownEventUnion` and `DefaultEventUnion` are internal helper types. ## Overview [Overview](/api/sse) @defjs/core · function ```ts function defineWebSocket< const P extends string, TInput extends AnyStruct | undefined = undefined, TIncoming extends AnyStruct = AnyStruct, TOutgoing extends AnyStruct | undefined = undefined, >( definition: Omit, "path"> & { input?: TInput; path: P; } & BuilderField, WebSocketRequestShape>>, ): WebSocketEndpoint< EndpointArgs, WebSocketIncomingData, WebSocketOutgoingData >; ``` Declare a typed WebSocket endpoint. Pass path, incoming (and optional outgoing) message structs, and queue limits. Call the returned operation with a client and, when declared, the input. - **definition** — `path`, one `incoming` struct, and the required `maxIncomingQueueSize` (a positive safe integer). Optional `outgoing` and `input` structs, static `protocols`, `maxOutgoingQueueSize` (a non-negative safe integer), and `operation`. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. A WebSocket `builder` may return `path` and `query` only; `headers` and `body` are rejected. `WebSocketDefinitionBase` and `BuilderField` are internal helper types. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | definition | `Omit, "path"> & { input?: TInput; path: P; } & { builder?: (() => WebSocketRequestShape

) \| undefined; }` | required | WebSocket contract (path, structs, queue limits, optional input). | ## Returns An operation `(client, ...input) => Promise`. ## Examples ```ts import { defineWebSocket, struct } from "@defjs/core"; const chat = defineWebSocket({ maxIncomingQueueSize: 100, path: "/chat", protocols: ["chat.v1"], incoming: struct.object({ text: struct.string(), type: struct.literal("message"), }), outgoing: struct.object({ text: struct.string(), type: struct.literal("message"), }), }); ``` ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketDefinition< TInput extends AnyStruct | undefined = undefined, TIncoming extends AnyStruct = AnyStruct, TOutgoing extends AnyStruct | undefined = undefined, > = WebSocketDefinitionBase & { builder?: OutboundBuilder; input?: TInput; }; ``` Contract object passed to defineWebSocket. - `incoming` is the struct every received frame is parsed with. Frames are JSON text. - `outgoing` is the struct `session.send` validates and encodes with. Without it, `send` throws. - `maxIncomingQueueSize` is required and must be a positive safe integer. `maxOutgoingQueueSize` must be a non-negative safe integer; it defaults to `0`, which disables queueing of messages sent while the session is `reconnecting`. - `protocols` is the static default subprotocol list. `RequestOptions.protocols` and [withWebSocketProtocols](/api/core/withwebsocketprotocols) override it. - `operation` is a static, low-cardinality name for telemetry. - `input` is the struct for the operation input. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. The builder may return `path` and `query` only; see [WebSocketRequestShape](/api/core/websocketrequestshape-type). Invalid queue limits are reported when the operation runs, as a `REQ_OPTIONS_INVALID` fault. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | incoming | `TIncoming` | required | | | maxIncomingQueueSize | `number` | required | | | maxOutgoingQueueSize | `number \| undefined` | optional | | | operation | `string \| undefined` | optional | | | outgoing | `TOutgoing \| undefined` | optional | | | path | `string` | required | | | protocols | `readonly string[] \| undefined` | optional | | | builder | `OutboundBuilder \| undefined` | optional | | | input | `TInput \| undefined` | optional | | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketEndpoint< Args extends unknown[], Incoming, Outgoing = never, > = ( client: Client, ...args: Args ) => Promise>; ``` Operation returned by `defineWebSocket`. The first argument is a [Client](/api/core/client-type). Further arguments are the input only. Calling it opens the session and returns the existing error-first tuple. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | client | `Client` | required | | | ...args | `Args` | optional | | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketRequestShape

= { body?: never; headers?: never; path?: PathValue

; query?: URLSearchParams | QueryRecord; }; ``` Request shape the `builder` of a [WebSocketDefinition](/api/core/websocketdefinition-type) may return, where `P` is the endpoint path. - `path` supplies the `:name` segments of `P`: one scalar value per name. It is `never` when the path has none. - `query` is a `URLSearchParams` or a record of scalars, arrays, and nested records. - `headers` and `body` are `never`. A WebSocket handshake takes neither from the builder; put credentials in `beforeConnect` or an interceptor. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | body | `undefined` | optional | WebSocket handshakes cannot have a body. Use session.send after connecting. | | headers | `undefined` | optional | WebSocket builders cannot set handshake headers. | | path | `PathValue

\| undefined` | optional | Explicit values for path placeholders. Anonymous objects use template keys; typed objects apply their struct aliases. | | query | `QueryRecord \| URLSearchParams \| undefined` | optional | URLSearchParams passes through unchanged; objects are encoded first. Configure the Client's query serializer for nested query values. | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketExecuteOptions< TIncoming = unknown, TOutgoing = unknown, > = UseWebSocketConfig; ``` WebSocket fields read from [RequestOptions](/api/core/requestoptions-type) for one call. See [UseWebSocketConfig](/api/core/usewebsocketconfig-type). `heartbeat` callbacks are not inferred from the operation; narrow `unknown` messages yourself. ## See also [WebSocketHeartbeatConfig](/api/core/websocketheartbeatconfig-type) ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type UseWebSocketConfig< TIncoming = unknown, TOutgoing = unknown, > = UseWebSocketBaseConfig & { signal?: AbortSignal; }; ``` WebSocket fields of [RequestOptions](/api/core/requestoptions-type), plus `signal`. `beforeConnect`, `heartbeat`, `protocols`, and `reconnect` override the client-level options for one call. `signal` covers the whole session, including after it opens. There is no startup-only deadline and no `abort` field. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | beforeConnect | `((context: { attempt: number; signal: AbortSignal; }) => void \| Promise) \| undefined` | optional | | | heartbeat | `WebSocketHeartbeatConfig \| undefined` | optional | | | protocols | `readonly string[] \| undefined` | optional | | | reconnect | `{ attempts?: number; delayMs?: number; factor?: number; jitter?: number; maxDelayMs?: number; shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean; }) => boolean; } \| undefined` | optional | | | signal | `AbortSignal \| undefined` | optional | Aborts the session, including after it is open. | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type SocketAwaitResult = | [ error: null, socket: WebSocketSession, connection: WebSocketConnectionInfo, ] | [ error: Fault, socket: undefined, connection: WebSocketConnectionInfo | undefined, ]; ``` Await-result tuple from opening a WebSocket. - On success: `[null, session, connection]`. `session.connection` moves forward on reconnect. The tuple's `connection` preserves the first successful snapshot only for a native Defjs session or a wrapper forwarding the same live `connection` object. Custom sessions and wrappers that transform `connection` instead return their current public `connection`. Sharing `closed` cannot bypass URL sanitization or metadata filtering, and Defjs does not reconstruct first-connection metadata from arbitrary transformations. - On failure: `[fault, undefined, connection]`. `connection` may be `undefined`, for example when the constructor failed before the socket opened. - Only startup failures arrive here. Once the session is returned, later errors surface through `onRuntimeError`, `receive`, and `closed`. ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts interface WebSocketSession< TIncoming = unknown, TOutgoing = never, > extends AsyncDisposable { readonly bufferedAmount: number; readonly connection: WebSocketConnectionInfo; readonly closed: Promise; readonly receive: AsyncIterable; readonly state: WebSocketState; close(code?: number, reason?: string): void; [Symbol.asyncDispose](): PromiseLike; onRuntimeError(listener: (error: unknown) => void): () => void; onStateChange(listener: (state: WebSocketState) => void): () => void; send(message: TOutgoing): void; } ``` Typed WebSocket session: send and receive messages, observe state, and close. - `state` is the logical [WebSocketState](/api/core/websocketstate-type); `connection` is the latest [WebSocketConnectionInfo](/api/core/websocketconnectioninfo-type). `bufferedAmount` is the native unsent byte count, or `0` with no physical socket. - `receive` yields incoming messages already parsed with the `incoming` struct. It is one-consumer: asking for a second iterator throws a `TypeError`. A frame that cannot be parsed is reported to `onRuntimeError` listeners and skipped. - `send(message)` validates and encodes the message with the `outgoing` struct, then sends JSON text. It sends immediately while `open`, queues while `reconnecting` when `maxOutgoingQueueSize` allows, and throws a `DOMException` named `InvalidStateError` when the session is not writable. It also throws when no `outgoing` struct is declared, the message fails the struct, or the queue is disabled or full. - After encoding, a value JSON cannot faithfully represent throws `TypeError` before sending or entering the queue. This includes non-finite numbers, functions, symbols, `Map`, `Set`, streams, and class instances without a JSON representation, at the root or nested. The same guard protects heartbeat messages; a heartbeat serialization failure ends the session. Struct validation may reject a value first. - `close(code?, reason?)` requests closure and stops the heartbeat. `code` must be `1000` or in `3000..4999`, and `reason` at most 123 UTF-8 bytes; invalid arguments throw before the state changes. - `closed` resolves with the logical terminal [WebSocketCloseInfo](/api/core/websocketcloseinfo-type). - `onStateChange` and `onRuntimeError` return an unsubscribe function. Prefer `await using ownedSession = session` for owned scope cleanup. Manual `session.close()` and `await session.closed` remain available. ```ts async function consume( session: WebSocketSession, ): Promise { await using ownedSession = session; for await (const message of ownedSession.receive) console.log(message); } ``` The async disposer requests native close and waits for Defjs-owned teardown, but it bounds a missing close event at one second and may reject with a `DOMException` named `TimeoutError`. It cannot prove that the physical TCP connection closed. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | bufferedAmount | `number` | required | | | connection | `WebSocketConnectionInfo` | required | | | closed | `Promise` | required | | | receive | `AsyncIterable` | required | | | state | `WebSocketState` | required | | | close | `(code?: number, reason?: string) => void` | required | | | [Symbol.asyncDispose] | `() => PromiseLike` | required | | | onRuntimeError | `(listener: (error: unknown) => void) => () => void` | required | | | onStateChange | `(listener: (state: WebSocketState) => void) => () => void` | required | | | send | `(message: TOutgoing) => void` | required | | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketState = | "aborted" | "closed" | "closing" | "connecting" | "error" | "idle" | "open" | "reconnecting"; ``` Lifecycle state of a managed WebSocket session. | State | Meaning | | -------------- | ---------------------------------------------------------------------- | | `idle` | Before the first connect attempt | | `connecting` | Preparing and opening the first connection | | `open` | The physical socket is open | | `reconnecting` | Preparing or waiting for a replacement socket after a close | | `closing` | Closure was requested with `session.close()` or disposal | | `closed` | Terminal: the session ended without an error or cancellation | | `aborted` | Terminal: the session was cancelled through `signal` | | `error` | Terminal: the session failed, including a startup failure or `timeout` | Observe it with `session.state` and `session.onStateChange`. ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts interface WebSocketConnectionInfo { extensions?: string; generation: number; protocol?: string; url?: string; } ``` Connection metadata for the current WebSocket generation. - `url` is the URL the socket reports. - `protocol` is the negotiated subprotocol and `extensions` the negotiated extensions. Both are `undefined` when the socket reports an empty string. - `generation` counts the physical sockets that reached `open`: `1` after the first open, one more after each reconnect. A snapshot taken before any open has `0`. `session.connection` exposes the session's current public connection info. The `connection` in the operation result preserves the first successful snapshot only for a native Defjs session or a wrapper forwarding the same live `connection` object. Custom sessions and wrappers that transform `connection` instead return their current public `connection`. Sharing `closed` cannot bypass URL sanitization or metadata filtering, and Defjs does not reconstruct first-connection metadata from arbitrary transformations. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | extensions | `string \| undefined` | optional | | | generation | `number` | required | | | protocol | `string \| undefined` | optional | | | url | `string \| undefined` | optional | | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketCloseInfo = | { kind: "closed"; code?: number; reason?: string; wasClean?: boolean } | { kind: "aborted"; cause?: unknown; code?: number; reason?: string; wasClean?: boolean; } | { kind: "error"; cause: unknown; code?: number; reason?: string; wasClean?: boolean; }; ``` Logical terminal outcome of a WebSocket session. What `session.closed` resolves with, once the session has ended for good. - `closed`: the session ended normally, including a manual `session.close()`. - `aborted`: the session was cancelled; `cause` is the abort reason. - `error`: the session failed; `cause` is required. - `code`, `reason`, and `wasClean` come from the native close event when one was observed. A manual `session.close(code, reason)` fills `code` and `reason` when the close event does not. ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type ManualSocketCloseReason = { code?: number; kind: "manual-web-socket-close"; reason?: string; }; ``` Close code and reason recorded when a session is closed with session.close. Records the `code` and `reason` passed to `session.close(code?, reason?)`. They fill `code` and `reason` of the final `{ kind: 'closed' }` close info when the peer's close event carries none. `close` throws a `DOMException` when a provided `code` is not the integer `1000` or an integer from `3000` through `4999`, and when `reason` exceeds 123 UTF-8 bytes. Omitting `code` is allowed. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | code | `number \| undefined` | optional | | | kind | `"manual-web-socket-close"` | required | | | reason | `string \| undefined` | optional | | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts interface WebSocketHeartbeatConfig< TIncoming = unknown, TOutgoing = unknown, > { intervalMs: number; isAck?: (message: TIncoming) => boolean; message?: () => TOutgoing; timeoutMs?: number; } ``` Heartbeat ping and acknowledgement settings for a live WebSocket session. - `intervalMs` is the delay between heartbeat messages. - `message()` builds the payload. It is sent through the `outgoing` struct. Nothing is sent when it is omitted or returns `undefined`. - After struct encoding, non-JSON values fail with `TypeError` through the same serializer as `session.send`; that heartbeat failure ends the logical session without reconnecting. - `isAck(message)` receives each decoded incoming message. When it returns `true`, the message is treated as the acknowledgement and is not delivered to `receive`. - `timeoutMs` is how long to wait for an acknowledgement after a heartbeat. Missing it fails the session. - `intervalMs` and `timeoutMs` must be positive, finite, and at most `2_147_483_647`. Set it with [withWebSocketHeartbeat](/api/core/withwebsocketheartbeat) or the `heartbeat` request option. ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | intervalMs | `number` | required | | | isAck | `((message: TIncoming) => boolean) \| undefined` | optional | | | message | `(() => TOutgoing) \| undefined` | optional | | | timeoutMs | `number \| undefined` | optional | | ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketIncomingData = Infer; ``` Decoded type of a received WebSocket message. The type of each message yielded by `session.receive`: the [Infer](/api/core/infer-type) output of the endpoint's `incoming` struct. Aliased fields use their logical names. ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type WebSocketOutgoingData = TOutgoing extends AnyStruct ? StructInput : never; ``` Type accepted by WebSocket session send. The type `session.send` accepts: the [StructInput](/api/core/structinput-type) of the endpoint's `outgoing` struct, or `never` when the endpoint declares none, so `send` cannot be called with a value. ## Overview [Overview](/api/web-socket) @defjs/core · type ```ts type SocketLifecycleOutcome = { closeInfo: WebSocketCloseSnapshot; connection?: WebSocketConnectionInfo; cause?: unknown; opened: boolean; }; ``` The outcome of a **single connect attempt**, not the final result of the session. The reconnect logic uses it to decide whether to try again. - `closeInfo` holds the close details of that attempt: `{ cause?, code?, reason?, wasClean? }`. - `connection` is the connection snapshot, when the attempt got one. - `cause` is the error that ended the attempt, if any. - `opened` tells whether the socket reached `open` before it ended. The `shouldReconnect` option does not receive this object. It gets `attempt`, `cause`, `code`, `reason`, and `wasClean`, taken from it. The final result of the session is [WebSocketCloseInfo](/api/core/websocketcloseinfo-type). ## Members | Name | Type | Required | Description | | --- | --- | --- | --- | | closeInfo | `WebSocketCloseSnapshot` | required | | | connection | `WebSocketConnectionInfo \| undefined` | optional | | | cause | `unknown` | optional | | | opened | `boolean` | required | | ## Overview [Overview](/api/web-socket) @defjs/vue · function ```ts function createClientPlugin(client: DerivableClient): Plugin; ``` Create a Vue plugin that provides an existing Defjs client via [HTTP_CLIENT](/api/vue/http-client). Does not create, cache, or dispose the client — pass the client returned by `createClient`. Installing it calls `app.provide(HTTP_CLIENT, client)`, so components read the client with `injectClient()`. The client is stored as given: it is not wrapped in `ref`, `reactive`, or `markRaw`. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | client | `DerivableClient` | required | The client returned by `createClient` to provide to the app. | ## Returns A Vue `Plugin` for `app.use(...)`. ## Examples ```ts import { createClient, withEndpoint } from "@defjs/core"; import { createClientPlugin } from "@defjs/vue"; import { createApp } from "vue"; const client = createClient(withEndpoint("https://api.example.com")); const app = createApp(App); app.use(createClientPlugin(client)); ``` ## See also [DerivableClient](/api/core/derivableclient-type) ## Overview [Overview](/api/vue) @defjs/vue · function ```ts function injectClient( key?: InjectionKey, ): DerivableClient; ``` Vue composable that injects the Defjs client for a key, defaulting to [HTTP_CLIENT](/api/vue/http-client). `injectClient(key)` reads the nearest provider for that key; omitted or `undefined` uses `HTTP_CLIENT`. Call in a valid Vue injection context: synchronous `setup()`, compiler-restored ` ``` `createClientPlugin(client)` provides the exact object you pass. No clone, no disposal hook. Configure core options and interceptors when you create the client. `onCleanup` runs before the watcher re-runs and when it stops. Register it before starting async work. The error-first tuple stays application data. ## Inject and override [#inject-and-override] `injectClient(key)` reads the nearest provider for that key; omitted or `undefined` uses `HTTP_CLIENT`. Call in a valid Vue injection context: synchronous `setup()` or `app.runWithContext()` (Vue 3.3+). The latter supplies only synchronous app-level context, not subtree overrides or context across ordinary `await`. Compiler-restored contexts after top-level `await` in ` ``` Nearest provider wins. Descendants get `childClient`; siblings outside the subtree keep the app-level client. The providing component itself still reads from its ancestors. ## Own HTTP work outside a watcher [#own-http-work-outside-a-watcher] For work started by a composable or component outside a watcher, use `AbortController` + `onScopeDispose`. Abort startup and active work; check the signal before assigning reactive state. A plugin or injection scope does not infer who owns an operation. For list pages, call the operation from Vue Query (or similar) — `@defjs/vue` stays DI only and does **not** ship `useRequest`. When a scope owns a client, keep it request-independent for browser-wide reuse. If it captures headers, cookies, users, tenants, or credentials, create it in the relevant app/SSR request boundary and provide that instance there. ## Clean up realtime scope [#clean-up-realtime-scope] Close a stream or session even when the scope disappears mid-connect. Abort startup, close a late-arriving handle, consume the single iterator, await the terminal promise: ```vue ``` Fatal stream errors and queue overflow reject the iterator, so the loop has a `catch`; otherwise that rejection is unhandled. The example keeps only the last 100 messages and keys rows by an id, because identical payloads would give duplicate keys. Stopping the scope ends iteration normally for SSE; an aborted WebSocket session instead rejects `receive` with `AbortError`, so guard the `catch` the same way. WebSocket: same sequence — abort prep, close a late session, consume `session.receive`, unsubscribe `onStateChange` / `onRuntimeError`, close, await `session.closed`. Keep cleanup idempotent; disposal and iterator completion can meet. ## SSR scope [#ssr-scope] `createClientPlugin(client)` provides one instance to one Vue app. In the browser, share it when endpoint, interceptors, and captured state are safe to share. During SSR, create and install a separate client per request when headers, cookies, users, tenants, or credentials differ. App unmount, plugin removal, and component scope disposal do **not** abort HTTP, close SSE/WebSocket, unsubscribe listeners, or dispose the core client. The owner that starts the work must finish it. ## Reference [#reference] Public exports from `@defjs/vue`: ```typescript import { HTTP_CLIENT, createClientPlugin, injectClient } from "@defjs/vue"; type VueApi = { HTTP_CLIENT: typeof HTTP_CLIENT; createClientPlugin: typeof createClientPlugin; injectClient: typeof injectClient; }; const api: VueApi = { HTTP_CLIENT, createClientPlugin, injectClient }; void api; ``` - `HTTP_CLIENT` — `InjectionKey` for native `provide` / `inject` - `createClientPlugin(client)` — Vue `Plugin` that provides that client - `injectClient` — nearest client for the selected key (default `HTTP_CLIENT`); throws if missing Create clients and options in `@defjs/core`. See [Client](/docs/client), [HTTP](/docs/http), [Interceptors](/docs/interceptors), [SSE](/docs/sse), and [WebSocket](/docs/web-socket). ## Related recipes [#related-recipes] - [GET with a declared 404](/recipes/get-declared-404) - [Cancel an HTTP call](/recipes/cancel-http) - [Consume an SSE stream](/recipes/consume-sse) - [Open a WebSocket session](/recipes/websocket-session) # Interceptors [#interceptors] Interceptors let applications configure execution independently of the SDK's operation definitions. An SDK may export an authentication interceptor or a client factory as an optional helper; applications choose where to install it and supply their current credentials. Add auth headers, short-circuit maintenance windows, or retry safe reads — without touching operation validation. Each transport has its own chain. You get an `HttpRequest` in; you return that transport’s result (`HttpResponse`, event-stream handle, or WebSocket session). Input validation runs before the chain; status dispatch and decoded results after. ## Basic Setup [#basic-setup] ```typescript import { createClient, createHttpInterceptor, withEndpoint, withInterceptors, } from "@defjs/core"; const audit = createHttpInterceptor(async (request, next) => { const started = performance.now(); const response = await next(request); console.info( request.operation ?? request.method, response.status, Math.round(performance.now() - started), ); return response; }); const client = createClient( withEndpoint("https://api.example.com"), withInterceptors(audit), ); void client; ``` ## Onion order [#onion-order] `withInterceptors(...items)` accepts mixed interceptors. The client filters by `kind` for the selected transport and keeps relative registration order. Each interceptor may run before and after `next`: | Factory | Request | Result from `next` | | ---------------------------- | ------------- | ------------------------------------- | | `createHttpInterceptor` | `HttpRequest` | `Promise>` | | `createSSEInterceptor` | `HttpRequest` | `Promise>` | | `createWebSocketInterceptor` | `HttpRequest` | `Promise` | ```typescript import { createHttpInterceptor } from "@defjs/core"; const order: string[] = []; const first = createHttpInterceptor(async (request, next) => { order.push("first:before"); const response = await next(request); order.push("first:after"); return response; }); const second = createHttpInterceptor(async (request, next) => { order.push("second:before"); const response = await next(request); order.push("second:after"); return response; }); // Request: first:before → second:before → transport // Return: second:after → first:after void [first, second, order]; ``` Multiple `withInterceptors(...)` calls append. Put broad observation outside narrower mutation/retry when the outer layer must see the final result. ## Clone and add request headers [#clone-and-add-request-headers] Treat the incoming `HttpRequest` as owned by the chain. Clone `Headers` before changing them; pass a new request to `next`: ```typescript import { createHttpInterceptor } from "@defjs/core"; function readAccessToken(): string | undefined { return undefined; } const bearer = createHttpInterceptor((request, next) => { const token = readAccessToken(); if (!token) return next(request); const headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${token}`); return next({ ...request, headers }); }); ``` Same pattern for SSE. Browser WebSocket can’t add arbitrary handshake headers — changing `request.headers` won’t authenticate a browser socket. Use protocol, URL/query policy, or a server-supported handshake instead. HTTP never writes an inferred Content-Type into `request.headers`; interceptors see only declared headers. `request.bodyContentType` holds the inferred type of the builder's body as a plain field, so it survives `{ ...req }` copies. An explicit `content-type` header always wins over that field and detection, whether declared by the builder, client defaults, an interceptor, or a raw `fetchHandler` request. Signers should read `headers.get('content-type') ?? request.bodyContentType`. When replacing an HTTP body, replace `body` on the copied request and declare its type with a `content-type` header. `bodyContentType` applies only while `body` is the builder's value; replacement bodies are detected again, so a replacement string is text unless the header declares otherwise. Setting `body: undefined` leaves no library-inferred Content-Type. For the unchanged builder body, `bodyContentType: null` suppresses library inference when there is no explicit header, though native Fetch may still add its own type for a string. FormData always uses its actual multipart boundary. Don’t reuse a consumed `ReadableStream` body. ## Short-circuit a request [#short-circuit-a-request] You can skip `next`, but you must return the expected result type. For HTTP, `makeResponse(...)` builds a compatible wrapper: ```typescript import { createHttpInterceptor, makeResponse } from "@defjs/core"; function isMaintenanceWindow(): boolean { return false; } const maintenanceGate = createHttpInterceptor(async (_request, next) => { if (isMaintenanceWindow()) { return makeResponse({ status: 503, statusText: "Service Unavailable", headers: new Headers({ "content-type": "application/json" }), body: { message: "Temporarily unavailable" }, }); } return next(_request); }); ``` The operation still dispatches by status. Declare one `error` struct for the non-2xx side when callers need typed `error.data`. It also still checks the declared side: a JSON `output` or `error` needs a JSON `content-type` header on the synthetic response, and `body` should already be the parsed value, the way the transport would deliver it (`request.responseType` and `request.errorResponseType` name the representation). Short-circuiting SSE or WebSocket needs a complete compatible handle/session (closure promises, live state, ownership, and `[Symbol.asyncDispose]`). Partial objects aren’t valid policy. Structural `EventStreamHandle` and `WebSocketSessionLike` implementations now need that standard disposer at compile time; consumers that only receive Defjs handles have no new runtime call requirement. ## Retry safe reads [#retry-safe-reads] Retries change behavior. Keep the policy narrow — this example retries replayable `GET` / `HEAD` / `OPTIONS` for statuses `429`, `502`, `503`, `504`, caps `Retry-After` at 30s, and stops after two retries or on abort: ```typescript import { createHttpInterceptor, type HttpRequest, type HttpResponse, } from "@defjs/core"; const retryableMethods = new Set(["GET", "HEAD", "OPTIONS"]); const retryableStatuses = new Set([429, 502, 503, 504]); function isReplayable(request: HttpRequest): boolean { return ( typeof ReadableStream === "undefined" || !(request.body instanceof ReadableStream) ); } function retryAfterMs(response: HttpResponse): number { const value = response.headers.get("retry-after"); if (!value) return 250; const seconds = Number(value); if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1_000, 30_000); const date = Date.parse(value); return Number.isNaN(date) ? 250 : Math.min(Math.max(0, date - Date.now()), 30_000); } function waitForRetryAfter( response: HttpResponse, signal?: AbortSignal, ): Promise { const delay = retryAfterMs(response); return new Promise((resolve, reject) => { if (signal?.aborted) { reject(signal.reason); return; } const timer = setTimeout(done, delay); const onAbort = () => { clearTimeout(timer); signal?.removeEventListener("abort", onAbort); reject(signal?.reason); }; function done() { signal?.removeEventListener("abort", onAbort); resolve(); } signal?.addEventListener("abort", onAbort, { once: true }); if (signal?.aborted) onAbort(); }); } const retrySafeReads = createHttpInterceptor(async (request, next) => { if ( !retryableMethods.has(request.method.toUpperCase()) || !isReplayable(request) ) return next(request); for (let attempt = 0; ; attempt += 1) { const response = await next(request); if (!retryableStatuses.has(response.status) || attempt >= 2) return response; await waitForRetryAfter(response, request.signal); } }); ``` Thrown interceptor/Fetch errors aren’t retried by this loop — a transport failure rejects instead of producing a status this loop could match on. Retrying `POST` / `PUT` / `PATCH` / `DELETE` needs replayable bytes, server support, an idempotency contract, and a reviewed status policy. You own retry — Core does not export `retryAfter()`. An interceptor that throws (or rejects) outside this sample is returned to the caller as `EXT_INTERCEPTOR_FAILED`, with the thrown value on `cause` — see [Errors](/docs/errors). So is one that resolves anything but its transport's result: `undefined` from a forgotten `return`, or a native `Response` where an `HttpResponse` belongs. In an HTTP interceptor, throwing `ERR_ABORTED` or `ERR_TIMEOUT` is how you express cancellation instead: the call resolves with `NET_ABORTED` or `NET_TIMEOUT`. SSE wraps a non-transport throw as `EXT_INTERCEPTOR_FAILED`. A WebSocket throw is returned unchanged when it is already a Fault (an `Error` with a string `code`); any other throw is `EXT_INTERCEPTOR_FAILED`. `next(request)` can reject. A transport failure is a fault, not a status-0 response, so an interceptor that wants to observe one uses `try` / `catch` around `next`. Defjs still attributes it correctly: a rejection that came from the transport stays a `NET_*` fault rather than being blamed on your interceptor. ## Wrap WebSocket sessions [#wrap-websocket-sessions] A WebSocket interceptor may call `next` at most once. If you wrap the session, delegate live getters and lifecycle members explicitly: ```typescript import { createWebSocketInterceptor } from "@defjs/core"; const preserveSession = createWebSocketInterceptor(async (request, next) => { const session = await next(request); return { get bufferedAmount() { return session.bufferedAmount; }, get connection() { return session.connection; }, get state() { return session.state; }, closed: session.closed, receive: session.receive, close(code?: number, reason?: string) { session.close(code, reason); }, [Symbol.asyncDispose]() { return session[Symbol.asyncDispose](); }, onRuntimeError(listener) { return session.onRuntimeError(listener); }, onStateChange(listener) { return session.onStateChange(listener); }, send(message: unknown) { session.send(message); }, }; }); ``` Spreading a session snapshots `state` / `connection` / `bufferedAmount` once. Preserve `closed`, `receive`, `close`, the exact `[Symbol.asyncDispose]()` delegation, and listener cleanup unless you’re deliberately changing ownership. A wrapper must return the inner session’s teardown promise, not an unrelated resolved promise. If a chain creates a session that isn’t delivered, Core settles and closes it; if a successful interceptor returns a different session, Core discards the created one. ## Reference [#reference] Factories return tagged transport values: - `createHttpInterceptor(fn)` → `{ kind: 'http', fn }` - `createSSEInterceptor(fn)` → `{ kind: 'sse', fn }` - `createWebSocketInterceptor(fn)` → `{ kind: 'web-socket', fn }` - `basicAuthHttpInterceptor(provider, options?)` — Basic credentials on HTTP - `basicAuthSSEInterceptor(provider, options?)` — Basic credentials on SSE `HttpRequest` may include `endpoint`, `baseEndpoint`, `method`, `headers`, `body`, `queryParams`, `queryString`, `signal`, and static `operation`. It’s a transport integration value — not the caller’s parsed input. `queryParams` is `new URLSearchParams(queryString)` after serialization. A configured query serializer is not called when the builder produced no query, and a builder `URLSearchParams` is used as-is. Keep input validation, output validation, and domain error mapping in their layers. URL validation starts during request building: a missing, relative, or invalid base endpoint or an illegal path fails with `REQ_BUILD_FAILED` before any interceptor or transport runs, including a short-circuiting interceptor. Before dispatch, the final `req.endpoint` is validated against the final `req.baseEndpoint`: the resolved URL must keep that base's origin and stay under its directory; C0 control characters (including tab, CR, and LF), backslashes, `?`, `#`, and directory escapes in the path are rejected. Scheme-like and protocol-relative forms are rejected even with leading spaces; a plain inner space is allowed and sent as `%20`. An interceptor may deliberately replace `baseEndpoint` with another valid absolute URL. HTTP, SSE, and WebSocket share the rule, and deterministic URL errors are never retried. SSE/WebSocket observers are lifecycle hooks, not control flow. Unsubscribe WebSocket listeners when the owner ends. Observer failures follow the transport contract; an interceptor itself can throw or reject. Log a reviewed allowlist: static `operation`, method, status, duration, stable error code. Don’t log resolved URLs, query strings, auth headers, bodies, raw causes, SSE event IDs, or WebSocket payloads by default. Basic credentials are base64, not encrypted. Use TLS, keep credential providers request-scoped on a server, never log the generated header. Default encoder is `globalThis.btoa`; pass `BasicAuthInterceptorOptions.encode` when the runtime lacks `btoa` or needs a reviewed encoder. An interceptor can enforce transport policy. It is not input validation, authorization, or resource ownership. The code that starts long-lived work still uses `await using` or manually closes and awaits the terminal promise. Ordinary HTTP remains request-scoped and is managed with its `AbortSignal`; `Client` is not `AsyncDisposable`. ## Related recipes [#related-recipes] - [Refresh a Bearer token once on 401](/recipes/refresh-bearer-once) - [Build and publish an SDK](/recipes/publish-http-sdk) - [Test with a local Fetch handle](/recipes/test-with-handle) - [Cancel an HTTP call](/recipes/cancel-http) # Server-Sent Events [#server-sent-events] An SDK exports an event-stream operation with input, event contracts, and buffer limits. Applications configure authentication and optional reconnection on their own clients, then call the imported operation when they need a stream. Open a stream, iterate it, then release the owned handle with `await using`. Manual `close()` and `closed` remain available; clients and plugins don’t dispose a returned stream for you. ## Basic Setup [#basic-setup] ```typescript import { createClient, defineEventStream, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); const notifications = defineEventStream({ maxBufferSize: 64 * 1024, maxQueueSize: 100, path: "/notifications", events: { message: struct.object({ text: struct.string() }), }, }); async function main() { const [error, stream] = await notifications(client); if (error) { console.error(error.code); return; } await using ownedStream = stream; for await (const event of ownedStream) { if (event.event === "message") console.log(event.data.text); } } void main(); ``` ## Define the stream [#define-the-stream] `defineEventStream(...)` needs `events`, positive safe-integer `maxBufferSize`, positive safe-integer `maxQueueSize`, and a relative `path`. Method defaults to `GET`. The request shape matches HTTP: `builder` returning `path`, `query`, `headers`, and `body`. `GET` / `HEAD` cannot return a body. A declared `error` struct types the startup fault and decodes a non-2xx handshake body by struct kind (`string` text, `blob` / `file` blob, `arrayBuffer` bytes, otherwise JSON). Defjs sends `Accept: text/event-stream` when you didn’t already set `Accept`. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. A `ReadableStream` body follows the HTTP rules: on a runtime that cannot stream request bodies, such as Firefox, startup fails with `ENV_UNSUPPORTED` before anything is sent. In Chromium it needs HTTP/2 or later; over HTTP/1.1 startup fails with `NET_UNREACHABLE`. A stream body is sent once and never reconnected (see Reconnect). One logical stream can span several physical Fetch attempts **when** you opt into reconnect. Without `withSSEReconnect`, transient network and stream-read failures end the logical stream. You still get one handle and one async iterator when the open succeeds. ## Open and inspect [#open-and-inspect] The operation resolves only after status, content-type, and body checks pass: ```typescript import { createClient, defineEventStream, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); const notifications = defineEventStream({ maxBufferSize: 64 * 1024, maxQueueSize: 100, path: "/notifications", events: { message: struct.string() }, }); async function main() { const [error, stream, startupOpen] = await notifications(client); if (error) { console.error(error.code, startupOpen?.response.status); return; } console.log( stream.open.response.status, startupOpen.response.status, stream.open.url, ); stream.close("example-finished"); await stream.closed; } void main(); ``` Response must be successful, media type essence `text/event-stream`, and have a body. Non-2xx startup → `HTTP_STATUS`. When `error` is declared, that fault carries the decoded handshake body; a bad media type is `RES_MEDIA_TYPE_INVALID` and a body that cannot be read as the declared kind is `RES_DECODE_FAILED`. A response snapshot can still sit in the third tuple slot when validation fails after the response arrives. A missing, relative, or invalid base endpoint or an illegal path fails with `REQ_BUILD_FAILED` during building, before any interceptor or transport runs, even if an interceptor would short-circuit. The resolved URL must keep the base origin and stay under its directory; C0 control characters (including tab, CR, and LF), backslashes, `?`, `#`, and directory escapes in the path are rejected. Scheme-like and protocol-relative forms are rejected even with leading spaces; a plain inner space is allowed and sent as `%20`. An interceptor's final `req.endpoint` is checked against its final `req.baseEndpoint` with the same rule; deliberately replacing the base with another valid absolute URL is allowed. Deterministic URL errors are never retried. `startupOpen` is the initial snapshot for that call; a new call never reuses a previous call's snapshot, even if it fails before a response arrives. `stream.open` is live and changes on later physical opens. Keep the tuple value when the first response matters. `startupOpen` is the returned connection's first successful handshake snapshot only for a native Defjs handle or a wrapper forwarding the same live `open` object; custom handles and wrappers overriding `open` retain their current public `open` even after reconnect, so shared `closed` cannot bypass URL sanitization or header filtering, and Defjs does not reconstruct first-handshake metadata from arbitrary transformations. ```typescript import type { EventStreamHandle, EventStreamOpenInfo, Fault, } from "@defjs/core"; type StreamResult = | [error: null, stream: EventStreamHandle, open: EventStreamOpenInfo] | [error: Fault, stream: undefined, open: EventStreamOpenInfo | undefined]; const result: StreamResult | undefined = undefined; void result; ``` ## Decode events [#decode-events] Wire event name → `events[eventName]`; else `events.default`. No matching Struct → event not delivered. Missing SSE `event` field → logical name `message`. SSE `data` starts as text. A `string` struct keeps that text. Every other kind is JSON: parse the text, then decode with the struct. `struct.object`, `struct.number`, `struct.boolean`, `struct.any`, and `struct.unknown` are JSON, not trimmed text. A bare token such as `ping` does not satisfy `struct.literal('ping')`; the data has to be JSON. A server that sends the JSON string `"abc"` still satisfies `struct.string()`: the delivered value is that raw text, including the quotes, and is not JSON-decoded to `abc`. Parsing follows the WHATWG SSE line and field rules: CR, LF, and CRLF end lines; a blank line dispatches a data-bearing event; multiple `data:` lines join with `\n`. Comments and unknown fields are ignored, and an unfinished event at EOF is not dispatched. Only one leading UTF-8 BOM at the start of each physical stream is removed. U+FEFF inside `data` or `id` values is preserved; a later BOM-prefixed field name such as `data` is unknown and ignored. Emitted value: `event`, decoded `data`, optional non-empty `id`. With `default`, unknown event names are `string` in the inferred union. ## Observe invalid events [#observe-invalid-events] Invalid/undeclared events are dropped, not queued. `withSSEOnInvalidEvent(...)` can observe raw ID, name, text data, plus `missing-struct` or `validation-failed` and an optional cause. ```ts import { createClient, withEndpoint, withSSEOnInvalidEvent } from "@defjs/core"; const client = createClient( withEndpoint("https://api.example.com"), withSSEOnInvalidEvent(({ reason, message, cause, signal }) => { if (signal.aborted) return; console.info("Dropped SSE event", { reason, event: message.event, hasCause: cause !== undefined, }); }), ); ``` Observer runs at the transform boundary. Its failure is isolated unless the active attempt signal is aborted. Keep it short; don’t treat raw event data as trusted. ## Reconnect [#reconnect] **Behavior change:** SSE reconnect is opt-in. Without `withSSEReconnect(...)`, network and stream-read failures are **not** retried (same as `attempts: 0`). Pass `withSSEReconnect({ attempts: 3 })` (or another reviewed budget) to retry those failures. Normal EOF is not retried. Status/content-type validation, parser limits, message transform failures, queue overflow, and normal EOF stay terminal for the logical stream. ```ts import { createClient, withEndpoint, withSSEReconnect } from "@defjs/core"; const client = createClient( withEndpoint("https://api.example.com"), withSSEReconnect({ attempts: 3, delayMs: 1_000, factor: 2, maxDelayMs: 30_000, jitter: 0.5, shouldReconnect({ lastEventId, method }) { // A POST is replayed only when the server can resume it from Last-Event-ID. return method === "GET" || lastEventId !== ""; }, }), ); ``` `attempts` is the retry budget for each outage; it defaults to `3` when a reconnect object is set. A successful handshake resets the budget, callback attempt, and backoff exponent, so separate outages do not spend a lifetime budget. `attempts: 0` disables retry. Omitting `withSSEReconnect` also disables retry. `delayMs` is the initial interval (default `1000`); `factor` grows it (default `1`, a constant delay); `maxDelayMs` is a hard cap on the final delay, after jitter and rounding (default: no configured cap). SSE `jitter` is a **0–1 multiplicative factor**, same as WebSocket. The platform timer limit is `2_147_483_647` ms even without a configured cap. A zero base stays zero; overflow from otherwise valid numbers saturates at the caps. A digits-only stream `retry:` field updates the current base interval, including after recovery. Policy callback returning false / throwing / rejecting ends the logical stream. Repeated successful opens followed immediately by retryable stream failures can keep reconnecting at the current base delay (subject to jitter and caps), because each open resets the budget; use `shouldReconnect` to stop them. Normal EOF remains terminal. Reconnect options are checked before interceptors or Fetch: `attempts` must be a non-negative safe integer; `delayMs` must be finite and non-negative; `maxDelayMs` must be a non-negative finite number or positive `Infinity`, which removes the configured cap but not the platform limit of `2_147_483_647` ms; `factor` must be finite and positive; `jitter` must be finite and within 0–1. Invalid values return `REQ_OPTIONS_INVALID` without opening a stream. Latest parsed event ID becomes `Last-Event-ID` on a later attempt, encoded as UTF-8 bytes on the wire, including non-ASCII IDs. An empty `id:` clears the header; an ID containing NUL is ignored. Apart from that header, a later attempt resends the request built for the first one: the builder and interceptors run once per call, so method, URL, headers, and body stay the same. A header that must be fresh on every attempt, such as a short-lived token, belongs in the Fetch passed to `withSSEHandle`, which runs once per attempt. If a credential still expires, a non-2xx reconnect handshake after the stream has opened ends it with `RES_DECODE_FAILED`; inspect `stream.open.response.status` for the status. A non-2xx initial handshake instead returns a startup `HTTP_STATUS` fault when its declared error body decodes. Reconnect resends a request only when that is safe: - `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` are idempotent (RFC 9110 §9.2.2) and reconnect within the budget. - Any other method, such as `POST` or `PATCH`, reconnects only when `shouldReconnect` returns `true`. A failed connection cannot tell whether the server already acted: a `POST` that starts an LLM generation would run, and be billed, again, and its new events would follow the old ones in the same stream. Passing `shouldReconnect` replaces this default for every method, so check `method` in its context when one client serves both kinds of endpoint. - A `ReadableStream` body is consumed by the first attempt. Any failure, before or after the stream opens, ends the stream with the original error; `shouldReconnect` is not called and no delay is spent. Use a string, bytes, `Blob`, form, or JSON body when the request must reconnect. ## Buffer and queue limits [#buffer-and-queue-limits] Both must be positive safe integers. Overflow is fatal — no silent discard of older events. | Limit | Protects | Terminal code | | --------------- | -------------------------------------------------- | --------------------- | | `maxBufferSize` | Incomplete/oversized SSE line/event while parsing | `CAP_BUFFER_EXCEEDED` | | `maxQueueSize` | Events produced faster than the one consumer reads | `CAP_QUEUE_OVERFLOW` | Fatal stream also clears buffered events, cancels the active body, rejects the iterator, and resolves `stream.closed` with `kind: 'error'`. ## Close and dispose [#close-and-dispose] `EventStreamHandle`: one live opening snapshot, one terminal promise, one `close`, one async iterator, and one standard async disposer. ```typescript import type { EventStreamCloseInfo, EventStreamHandle, EventStreamOpenInfo, } from "@defjs/core"; interface StreamApi extends AsyncIterable, AsyncDisposable { readonly open: EventStreamOpenInfo; readonly closed: Promise; close(reason?: unknown): void; [Symbol.asyncDispose](): PromiseLike; } const handle = null as unknown as EventStreamHandle; const api: StreamApi = handle; void api; ``` Terminal kinds (`closed.kind`): `eof`, `aborted`, or `error`. An `error` result also carries `closed.code`, an `EventStreamFaultCode`: `CAP_BUFFER_EXCEEDED`, `CAP_QUEUE_OVERFLOW`, `EXT_HOOK_FAILED`, `EXT_OBSERVER_FAILED`, `NET_TIMEOUT`, `NET_UNREACHABLE`, `RES_DECODE_FAILED`, or `RES_MEDIA_TYPE_INVALID`. `close(reason)` aborts the active attempt, closes the queue, settles as `aborted`, even when the reason is a `TimeoutError`. The `signal` passed to the operation stays attached after the stream is open and covers the whole connection. Aborting it normally ends the iterator and reports `aborted`. If the abort reason is a `TimeoutError` or `ERR_TIMEOUT` (for example `AbortSignal.timeout(...)`), the iterator throws `ERR_TIMEOUT` and `closed` reports `kind: 'error'` with `code: 'NET_TIMEOUT'`. There is no startup-only deadline on the client. An application that wants one creates its own `AbortController`, aborts it with `new DOMException("...", "TimeoutError")` from a timer, and clears the timer after the await returns: ```ts import { createClient, defineEventStream, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); const notifications = defineEventStream({ maxBufferSize: 64 * 1024, maxQueueSize: 100, path: "/notifications", events: { message: struct.string() }, }); async function openDuringStartup(abortAfterMs: number) { const controller = new AbortController(); const timer = setTimeout(() => { controller.abort(new DOMException("startup deadline", "TimeoutError")); }, abortAfterMs); const [error, stream] = await notifications( client({ signal: controller.signal }), ); clearTimeout(timer); if (error) { console.error(error.code); return; } stream.close("opened"); await stream.closed; } void openDuringStartup; ``` Loop `break` / `return` / throw invokes iterator return and closes with `iterator-return`. The code that starts the operation owns closure. `await using` calls the same owned close path and waits until Defjs reading/reconnect work has stopped and the active reader lock has been released. It is intentionally bounded around a provider-controlled `ReadableStream.cancel()` promise that never settles, so it does not guarantee that such a provider promise completed. Explicit `stream.close(reason)` plus `await stream.closed` is still valid when you need a reason or the terminal result. Custom structural `EventStreamHandle` implementations must now provide `[Symbol.asyncDispose](): PromiseLike` and connect it to the same lifecycle. That is a compile-time breaking change for implementers; consumers that only receive a Defjs handle do not need to invoke anything new at runtime. The supported minimum lib set is `ES2022`, `ESNext.Disposable`, `DOM`, and `DOM.Iterable`. Repository declaration tooling uses TypeScript 7, but there is no automated packed-consumer or minimum-lib verification gate, and older compilers are not promised. This set supplies types, not runtime Web API polyfills. A `Client` is not `AsyncDisposable`; manage each request with its `AbortSignal`. Keep credentials, event data, event IDs, causes, and stream URLs out of routine logs. `withCredentials(true)` affects Fetch cookies for SSE; it does not configure WebSocket auth. ## Related recipes [#related-recipes] - [Consume an SSE stream](/recipes/consume-sse) - [Cancel an HTTP call](/recipes/cancel-http) # Getting started: your first SDK operation [#getting-started-one-http-request] You’ll write two files: an SDK module that exports `GET /users/:id`, and an application that imports it and owns the client. The local Fetch handle keeps the first run offline. The same API definition will then work with a client pointed at your service. ## Best practice [#best-practice] A definition stays independent of the client. Call the operation with the application's client, as in `getUser(client, { id: 7 })`. There is no third options argument. Calling the operation starts it; `await` only waits. The application also owns the service address and credentials. Inspect the error-first tuple first. When `error` is present, report `error.code` and return from the function before reading the success value. ## Step 1 — Install [#step-1--install] `@defjs/core` is ESM and runs on Node.js or Bun. Node.js 22.18+ (or 24+) runs the `.ts` file directly through type stripping; older 22.x releases need `--experimental-strip-types`. Keep `"type": "module"` in package.json. In a browser you still need your bundler and Fetch. ```sh bun add @defjs/core ``` ## Step 2 — Define the request [#step-2--define-the-request] Create `src/define-request.ts`. This is the SDK author's module: it exports `getUser` without creating a client. The input struct is the caller's value. `builder` places that value on the request. Here the whole input is the path object, so alias-free scalar fields fill `:id`. ```ts define-request.ts import { defineRequest, struct } from "@defjs/core"; const User = struct.object({ id: struct.number(), name: struct.string(), }); const NotFound = struct.object({ message: struct.string(), }); export const getUser = defineRequest({ method: "GET", path: "/users/:id", input: struct.object({ id: struct.number() }), builder: (input) => ({ path: input }), output: User, error: NotFound, }); ``` `output` is the 2xx body. `error` is every other status. One struct each. Use `struct.or(...)` or a discriminated union when those bodies differ. `defineRequest(...)` returns an operation. Calling `getUser(client, input)` starts the request. The `builder` above is the function that places input; it is not the operation. Where `input` is declared, `builder` is required, synchronous, and pure. ## Step 3 — Call it locally [#step-3--call-it-locally] Create `src/execute-locally.ts` beside the definition. This is the consuming application: it imports `getUser`, creates its own client, and supplies a local Fetch handle. Defjs still validates input, builds the `Request`, dispatches by status, and parses the body. ```ts execute-locally.ts import { createClient, withEndpoint, withHTTPHandle, type FetchHandle, } from "@defjs/core"; import { getUser } from "./define-request.ts"; const handle: FetchHandle = async (input, init) => { const request = new Request(input, init); const id = new URL(request.url).pathname.split("/").at(-1); if (id === "7") { return Response.json({ id: 7, name: "Ada" }, { status: 200 }); } return Response.json({ message: "User not found" }, { status: 404 }); }; const client = createClient( withEndpoint("https://api.example.test"), withHTTPHandle(handle), ); async function main() { const [error, user, response] = await getUser( client({ signal: AbortSignal.timeout(5_000) }), { id: 7 }, ); if (error?.code === "HTTP_STATUS" && error.status === 404) { console.log(error.data.message); return; } if (error) { console.error(error.code); return; } console.log(`Loaded ${user.name} from ${response.status}`); } void main(); ``` Run it: ```sh bun src/execute-locally.ts ``` ```txt Loaded Ada from 200 ``` Try a missing user — change the path id to `8` and run again: ```txt User not found ``` On success: `error` is `null`, `user` is the `200` Struct output, `response` is a `DecodedResponse`. On a declared `404`: `error.code` is `'HTTP_STATUS'`, `error.status` is `404`, and `error.data` is typed `NotFound`. The second tuple item is `undefined` on failure. ## Step 4 — Point at a real API [#step-4--point-at-a-real-api] In `src/real-api.ts`, import the same `getUser` and create a client with your service's base URL. Run this version only after replacing the example address with a service that implements `GET /v1/users/:id` with those bodies. ```ts import { createClient, withEndpoint } from "@defjs/core"; import { getUser } from "./define-request.ts"; const client = createClient(withEndpoint("https://api.example.com/v1")); async function main() { const [error, user] = await getUser(client, { id: 7 }); if (error) { console.error(error.code); return; } console.log(user.name); } void main(); ``` The API definition stays in the SDK module. Service addresses, credentials, interceptors, and execution lifetime stay with the application. Both clients use the same Defjs endpoint contract. ## Publish the definition [#publish-the-definition] Move the definition into an SDK package when more applications need it. Export `getUser`, include its TypeScript declarations, and declare the compatible Core version. Consumers then import from the package instead of `./define-request.ts`; their client configuration remains their own. Follow [Build and publish an SDK](/recipes/publish-http-sdk) for the complete package setup. ## When the result differs [#when-the-result-differs] - Bad input → `REQ_INPUT_INVALID`; a failing `builder` → `REQ_BUILD_FAILED` - A non-2xx status → `HTTP_STATUS`, with typed `error.data` when `error` was declared and its body decodes - A declared JSON body with another media type → `RES_MEDIA_TYPE_INVALID`, reported before the body is read - A declared body (`output` for 2xx, `error` otherwise) that won't decode → `RES_DECODE_FAILED` or `RES_STRUCT_MISMATCH`, whatever the status - Fetch fail / cancel / deadline → `NET_UNREACHABLE` / `NET_ABORTED` / `NET_TIMEOUT` A deadline is `client({ signal: AbortSignal.timeout(ms) })` and classifies as `NET_TIMEOUT`. A manual abort is `NET_ABORTED`. There is no client-level default deadline. Derive a client when one call needs its own `signal`; see [Client](/docs/client). Cancellation tells you what the caller saw — not whether a server write committed. ## Next recipes [#next-recipes] - [Build and publish an SDK](/recipes/publish-http-sdk) - [GET with a declared 404](/recipes/get-declared-404) - [POST JSON](/recipes/post-json) - [Cancel an HTTP call](/recipes/cancel-http) - [Consume an SSE stream](/recipes/consume-sse) - [Open a WebSocket session](/recipes/websocket-session) - [Test with a local Fetch handle](/recipes/test-with-handle) # Struct [#struct] Structs express the data contract your SDK promises: caller input, service responses, declared errors, and realtime messages. Reuse them across exported operations so consumers get consistent TypeScript types and runtime validation, independently of the client they configure. You get TypeScript types via `Infer`, and runtime checks via `struct.parse(...)` — no throw, error-first tuple. ## Basic Setup [#basic-setup] ```typescript import { defineRequest, struct, type Infer } from "@defjs/core"; const User = struct.object({ id: struct.number(), name: struct.string(), active: struct.boolean(), }); type User = Infer; const createUser = defineRequest({ method: "POST", path: "/users", input: struct.object({ name: struct.string(), email: struct.string(), }), builder: (input) => ({ body: input }), output: User, }); function readUser() { const [parseError, user] = struct.parse(User, { id: 7, name: "Ada", active: true, }); if (parseError) { console.error(parseError.message); return; } console.log(user.name); } readUser(); void createUser; ``` Parsed output keeps only declared fields. Missing required fields, wrong primitives, bad nested values, wrong tuple length, or disallowed `null` → `StructError`, no partial value. Structs are immutable; `.optional()` and friends return a new Struct. ## Required, optional, null [#required-optional-null] Presence and nullability are separate: | Declaration | Missing / `undefined` | `null` | Valid value | | ---------------------------- | -------------------------------- | ------ | ------------------- | | `struct.string()` | Reject | Reject | Accept string | | `struct.string().optional()` | Accept; omit absent object field | Reject | Accept string | | `struct.string().null()` | Reject | Accept | Accept string | | `struct.string().nullish()` | Accept; omit absent object field | Accept | Accept string | | `struct.null()` | Reject | Accept | Reject other values | ```typescript import { struct } from "@defjs/core"; const Profile = struct.object({ name: struct.string(), nickname: struct.string().optional(), biography: struct.string().null(), note: struct.string().nullish(), }); function showProfile() { const [error, profile] = struct.parse(Profile, { name: "Ada", biography: null, note: undefined, }); if (error) { console.error(error.message); return; } console.log(profile.name, profile.nickname, profile.biography, profile.note); } showProfile(); ``` `struct.any()` and `struct.unknown()` reject `null` and `undefined` too. Chain `.null()`, `.optional()`, or `.nullish()` to accept them. At the root, optional can be `undefined`. Inside an object, omitted optional/nullish fields stay absent. A nested object field follows the same rule: mark it `.optional()` when the caller may omit it. A required object field stays required even when every field inside it is optional. ## Request placement [#request-placement] Path, query, headers, and body are fields the builder returns, not a request struct. A declared `input` always requires an explicit `builder`. `input` only defines the parameters; the builder alone decides where each value goes. Only definitions without `input` may omit `builder`. `struct.urlencoded(shape)` and `struct.formData(shape)` are object structs with a codec tag. The tag runs only when that value is the body root: keys use aliases, and arrays repeat the field name. ```typescript import { defineRequest, struct, type Client } from "@defjs/core"; const createUser = defineRequest({ method: "POST", path: "/organizations/:organizationId/users", input: struct.object({ organizationId: struct.string(), notify: struct.boolean().optional(), requestId: struct.string(), user: struct.object({ displayName: struct.string().alias("display_name"), }), }), builder: (input) => ({ path: { organizationId: input.organizationId }, query: { notify: input.notify }, headers: { "x-request-id": input.requestId }, body: input.user, }), }); async function submit(client: Client) { const [error] = await createUser(client, { organizationId: "acme", notify: true, requestId: "request-42", user: { displayName: "Ada" }, }); if (error) return; } void submit; ``` | Body root | Sent as | Default content type | | --------------------------------------------------- | ----------------- | ------------------------------------------------- | | typed object, array, finite number, boolean, `null` | JSON text | `application/json` | | `string` struct or a raw string | text | `text/plain;charset=UTF-8` | | `struct.urlencoded(shape)` at the root | `URLSearchParams` | `application/x-www-form-urlencoded;charset=UTF-8` | | `struct.formData(shape)` at the root | `FormData` | platform multipart boundary | | `Blob` / `File` | the blob | blob type or `application/octet-stream` | | `ArrayBuffer` / view / stream | bytes | `application/octet-stream` | `NaN` and `±Infinity`, including inside an array or object, fail with `REQ_BUILD_FAILED`. `struct.file()` is a value struct for a form field and for a raw file body. WebSocket builders cannot return `headers` or `body`. HTTP and SSE can. `GET` and `HEAD` cannot. ## Aliases [#aliases] `.alias(...)` separates logical names from wire names. `struct.parse(...)` uses logical keys unless you pass `{ aliases: true }`. JSON and flat request codecs encode aliases; JSON response decoding maps wire keys back to logical fields. ```typescript import { struct } from "@defjs/core"; const User = struct.object({ displayName: struct.string().alias("display_name"), }); function readDisplayName() { const [parseError, user] = struct.parse(User, { displayName: "Ada" }); if (parseError) { console.error(parseError.message); return; } console.log(user.displayName); } readDisplayName(); const [wireError] = struct.parse(User, { display_name: "Ada" }); // Expected failure: public parse reads logical keys, not wire keys. if (!wireError) throw new Error("struct.parse must read logical keys"); console.log(wireError.issues[0]?.path); ``` | Boundary | Field | | --------------------------------------------- | ---------------------------- | | `struct.parse(User, ...)` and operation input | Logical `displayName` | | Typed object returned from the builder | Wire `display_name` | | JSON / SSE / WebSocket decoding | Wire → logical `displayName` | | Anonymous object written in the builder | The key you wrote | The builder itself reads logical names (`input.user.displayName`); the wire name appears only when the serializer encodes the typed object the builder returns. Aliases work on nested fields, arrays, objects, unions, and discriminators. Keep logical names in app code; put the external name on the struct once. Spreading a typed object keeps that encoding. Setting both the logical field and its wire key in the same object is `REQ_BUILD_FAILED`. `JSON.stringify` of a value yourself leaves this path: the serializer sees logical names. The guide is chosen at every node, not only at the root. A new object under an anonymous parent keeps the keys you wrote. The same object under a typed parent uses that field's struct. ```ts body: { row: { id: '42' } } // Anonymous root: emits id body: { ...input, row: { id: '42' } } // Typed root, row field declares alias: emits wire_id ``` When a union guide matches more than one branch, each branch is encoded and compared. Identical wire output is kept; a difference is `REQ_BUILD_FAILED`. A mixed intersection that contains a non-object part is tagged, when parsed, with the conjunction of the tags from the parts that matched; a newly written mixed-intersection value that was not parsed is an anonymous object. ## Parse failures [#parse-failures] `struct.parse(...)` returns `[null, value]` or `[StructError, undefined]`. `StructError` extends `Error` and exposes `issues`, plus `format()`, `flatten()`, and `prettify()`. ```typescript import { struct, StructError } from "@defjs/core"; const User = struct.object({ id: struct.number(), name: struct.string() }); const [error, value] = struct.parse(User, { id: "not-a-number" }); if (error) { console.log(error instanceof StructError); console.log(error.issues[0]?.code, error.issues[0]?.path); console.log(error.flatten().fieldErrors); console.log(error.format(), error.prettify()); } void value; ``` A `StructIssue` has `code`, `expected`, `message`, `path`, and `received`. `code` is `custom`, `invalid_enum`, `invalid_intersection`, `invalid_literal`, `invalid_type`, `invalid_union`, or `missing_key`. Default messages name the received kind, never the value: `Expected number at id, received string`. Issue paths are structural and retain record keys: `struct.record(...)` can produce `Expected number at alice@example.com, received string`, so keys may still be sensitive in default messages, `prettify()`, and `String(fault)`. `message` and `prettify()` therefore omit received payloads; `received` keeps the original value and needs redaction before logging or returning. `struct.parse(..., { errorMap })` rewrites messages for that call only, including bigint/date conversion failures in a struct from another Core copy. Any returned string, including `''`, replaces the default; only `undefined` keeps it. Custom text is used verbatim and may expose values if the map includes them. Struct validation is structural only. No public range, format, refinement, auth, or state-transition rules. Do those checks before you call an operation. ## Reference [#reference] Public constructors on `@defjs/core` (internals are not facade APIs): ```typescript import { struct } from "@defjs/core"; const Any = struct.any(); const ArrayOfStrings = struct.array(struct.string()); const Bytes = struct.arrayBuffer(); const BigIntValue = struct.bigint(); const BlobValue = struct.blob(); const BooleanValue = struct.boolean(); const DateValue = struct.date(); const Discriminated = struct.discriminatedUnion("kind", [ struct.object({ kind: struct.literal("created"), id: struct.number() }), struct.object({ kind: struct.literal("deleted"), id: struct.number() }), ]); const Status = struct.enum(["draft", "published"]); const FileValue = struct.file(); const Form = struct.formData({ file: struct.file() }); const Combined = struct.intersection( struct.object({ id: struct.number() }), struct.object({ name: struct.string() }), ); const Literal = struct.literal("ready"); const NullValue = struct.null(); const NumberValue = struct.number(); const ObjectValue = struct.object({ id: struct.number() }); const Union = struct.or(struct.string(), struct.number()); const RecordValue = struct.record(struct.number()); const StringValue = struct.string(); const Tuple = struct.tuple([struct.string(), struct.number()]); const Unknown = struct.unknown(); const FormUrlEncoded = struct.urlencoded({ name: struct.string() }); void [ Any, ArrayOfStrings, Bytes, BigIntValue, BlobValue, BooleanValue, DateValue, Discriminated, Status, FileValue, Form, Combined, ]; void [ Literal, NullValue, NumberValue, ObjectValue, Union, RecordValue, StringValue, Tuple, Unknown, FormUrlEncoded, ]; ``` | Constructor | Input | Inferred output | | -------------------------------- | ------------------------------------------------ | ------------------------------- | | `struct.number()` | Number other than `NaN` | `number`, including ±`Infinity` | | `struct.date()` | `Date`, number, or date string | Valid `Date` | | `struct.bigint()` | `bigint` or string accepted by `BigInt(...)` | `bigint` | | `struct.enum(...)` | Declared string or number member | That literal union | | `struct.discriminatedUnion(...)` | Object with required literal discriminator | Selected object branch | | `struct.or(...)` | First matching branch; encoding checks ambiguity | Union of branch outputs | | `struct.intersection(...)` | Values accepted by every member | Intersection of outputs | | `struct.record(value)` | Plain object whose values match `value` | Record of parsed values | | `struct.tuple(items)` | Array of exactly the declared length | Fixed-length tuple | Every Struct supports `.alias(name)`, `.optional()`, `.null()`, and `.nullish()`. `struct.discriminatedUnion` needs object options with a required literal discriminator and rejects duplicates. Import `struct`, `Infer`, `Struct`, `StructError`, and related public types from `@defjs/core`. Use `struct.parse(...)` as the parser. Don’t import `createObjectStruct`, definition symbols, codec internals, or `packages/core/src`. Facade non-promises: - Object/record outputs use a null prototype — don’t assume `Object.prototype` methods. - Unknown object keys are dropped. - `struct.number()` rejects `NaN`, accepts infinities. - `struct.or(...)` tries branches in order; rejects ambiguous encodings when branches disagree. - `struct.intersection(...)` parses members in declaration order. - An intersection deep-merges member outputs (`Object.is`-equal values and equal-time dates stay, plain objects and equal-length arrays merge recursively, any other clash is `invalid_intersection` at that path), `unknown`/`any` contribute nothing, and one logical key may map to several wire keys. Only unequal values written to the same wire key conflict. - A Struct validates a boundary; it doesn’t cache, authorize, or own a transport resource. ## Related recipes [#related-recipes] - [POST JSON](/recipes/post-json) - [GET with a declared 404](/recipes/get-declared-404) # WebSocket [#websocket] The SDK defines a WebSocket operation's path, inputs, incoming and outgoing messages, and queue limit. The consuming application configures its client, calls the operation, and chooses call-specific connection options on a derived client. Export the operation alongside your SDK's HTTP or SSE operations. Start → receive → send → release with `await using`. You own unsubscribe and disposal. Manual `close()` / `closed` remains available; clients, providers, and interceptors don’t auto-close sessions. ## Basic Setup [#basic-setup] ```typescript import { createClient, defineWebSocket, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://chat.example.com")); const room = defineWebSocket({ maxIncomingQueueSize: 100, path: "/chat", incoming: struct.object({ type: struct.literal("message"), text: struct.string(), }), outgoing: struct.object({ type: struct.literal("send"), text: struct.string(), }), }); async function main() { const [error, session, startupConnection] = await room(client); if (error) { console.error(error.code, startupConnection?.generation); return; } await using ownedSession = session; const unsubscribe = ownedSession.onRuntimeError((cause) => console.error("runtime", cause), ); try { ownedSession.send({ type: "send", text: "Hello" }); // wire: {"type":"send","text":"Hello"} for await (const message of ownedSession.receive) { console.log(message.type, message.text); break; } } finally { unsubscribe(); } } void main(); ``` ## One struct per direction [#one-struct-per-direction] `incoming` is one struct for every text frame. `outgoing` is one struct for `session.send(...)`. Model several kinds with `struct.discriminatedUnion`. Alias on those fields is the wire name in both directions. There is no separate type envelope and no normalizer. ```json { "type": "message", "text": "Hello" } ``` Incoming text, `ArrayBuffer`, typed-array, and `Blob` frames are read as UTF-8, JSON-parsed, then checked against `incoming`. Malformed JSON and struct failures go to `session.onRuntimeError` and are not queued. The session stays up. Frames that are not text, bytes, or a `Blob` are ignored. There is no `withWebSocketOnInvalidEvent`. `session.send(...)` validates and serializes synchronously: struct encode, then `JSON.stringify`. It sends immediately when open, queues during `reconnecting` when an outgoing queue is enabled, and throws `InvalidStateError` when not writable. It also throws when there’s no outgoing struct, the payload fails the struct, the outgoing queue is disabled or full, or the native send fails. A static `protocols` list on the definition is the endpoint contract (for example `graphql-transport-ws`). A derived client's `protocols` and `withWebSocketProtocols` can still override it. Only those options choose subprotocols: the builder, `beforeConnect`, and interceptors cannot change them, so a token-in-subprotocol is passed as `protocols` on the derived client for that call. The builder may return only `path` and `query`. After struct encoding, values that JSON cannot faithfully represent throw `TypeError` before sending or entering the reconnect queue: non-finite numbers, functions, symbols, `Map`, `Set`, streams, and class instances without a JSON representation. Nested values follow the same rule as HTTP JSON bodies. `struct.number()` still accepts infinities and rejects `NaN`; accepting a value at the struct layer does not make it valid JSON. The heartbeat uses the same serializer; its serialization failure ends the logical session. `receive` is one-consumer. Asking for a second iterator throws a `TypeError`. ## State snapshots [#state-snapshots] | Member | Meaning | | -------------------------- | --------------------------------------------------------------------------------------------- | | `state` | `idle`, `connecting`, `open`, `reconnecting`, `closing`, `closed`, `aborted`, or `error` | | `connection` | Latest physical connection: `generation`, URL, negotiated protocol, extensions when available | | `bufferedAmount` | Native unsent byte count, or `0` with no physical socket | | `receive` | One-consumer async iterable of validated incoming messages | | `onStateChange(listener)` | Subscribe to logical state transitions; returns unsubscribe | | `onRuntimeError(listener)` | Subscribe to non-startup runtime errors; returns unsubscribe | | `closed` | Promise for the logical terminal close outcome | `open` = physical socket open. `reconnecting` includes preparation + delay before a replacement. `connection.generation` increments each physical socket that reaches `open`. `session.connection` moves forward on reconnect. Tuple `startupConnection` preserves the first successful snapshot only for a native Defjs session or a wrapper forwarding the same live `connection` object. Custom sessions and wrappers that transform `connection` instead use the returned session's current public `connection` for the tuple. Sharing `closed` cannot bypass URL sanitization or metadata filtering, and Defjs does not reconstruct first-connection metadata from arbitrary transformations. Startup failure → `[error, undefined, connection?]`. Pre-open constructor failure may have no connection; an abort or close during startup may still provide a snapshot. After the session returns, runtime errors travel through observers, `receive`, and `closed` — not a second result tuple. ```typescript import type { Fault, WebSocketConnectionInfo, WebSocketSession, } from "@defjs/core"; type SocketResult = | [ error: null, session: WebSocketSession, connection: WebSocketConnectionInfo, ] | [ error: Fault, session: undefined, connection: WebSocketConnectionInfo | undefined, ]; const result: SocketResult | undefined = undefined; void result; ``` ## Reconnect [#reconnect] Reconnect is opt-in. No `reconnect` object → physical close ends the logical session. When configured, defaults are `attempts: 3`, `delayMs: 1000`, `factor: 2`, `maxDelayMs: 30000`, `jitter: 0`. `attempts` is the retry budget for each outage; a successful `open` resets the budget and backoff exponent. Each later outage therefore starts again with retry 1 and the `delayMs` base. `attempts: 0` disables. Default predicate accepts every non-manual close outcome within that outage's budget. Repeated successful opens do not have a lifetime retry limit. A socket that successfully opens and is immediately closed again by the server can keep reconnecting at the base delay (subject to jitter and caps), because every open resets the budget; use `shouldReconnect` to stop it. ```ts import { createClient, defineWebSocket, struct, withEndpoint, withWebSocketReconnect, } from "@defjs/core"; const client = createClient( withEndpoint("https://chat.example.com"), withWebSocketReconnect({ attempts: 3, delayMs: 500, factor: 2, maxDelayMs: 10_000, jitter: 0.2, shouldReconnect({ attempt, code, wasClean }) { return attempt <= 3 && (wasClean !== true || code === 1006); }, }), ); const room = defineWebSocket({ maxIncomingQueueSize: 100, path: "/chat", incoming: struct.object({ type: struct.literal("ready"), ok: struct.boolean(), }), }); async function main() { const [error, session] = await room(client); if (error) { console.error(error.code); return; } console.log(session.state); session.close(1000, "done"); } void main(); ``` `shouldReconnect` gets next retry attempt, close cause, code, reason, and `wasClean`. Manual `session.close(...)` does not enter the predicate. Throwing preparation/policy ends the logical session with an error. WebSocket backoff jitter is **multiplicative** (`jitter: 0.2` scales the capped base by `0.8x`–`1.2x` before rounding). The final delay is capped again at `maxDelayMs` and the platform timer limit of `2_147_483_647` ms, so the upper part of that range can be truncated. A zero base stays zero; overflow from valid numbers is clamped instead of throwing. SSE uses the same 0–1 jitter rule. Reconnect options are validated before the constructor: `attempts` is a non-negative safe integer; `delayMs` must be finite and non-negative; `maxDelayMs` must be a non-negative finite number or positive `Infinity`, which removes the configured cap but not the platform limit of `2_147_483_647` ms; `factor` is finite and positive; `jitter` is finite and within 0–1. Invalid values return `REQ_OPTIONS_INVALID`. WebSocket `attempts: 0` disables subsequent retries, but all explicitly supplied reconnect fields are still validated before the initial connection, just as in SSE. `beforeConnect({ attempt, signal })` runs before the initial constructor and every reconnect. Pass its signal into token refresh so cancel stops both prep and connect. ## Heartbeat [#heartbeat] Opt-in on the client or on a derived client. Interval sends `message()` through the outgoing struct. Optional `isAck(message)` recognizes an ack — that message clears the acknowledgement wait and is **not** delivered to `receive`. `heartbeat` is replaced as a whole value. `timeoutMs` is how long to wait for that acknowledgement. It is not a request deadline. ```ts import { createClient, defineWebSocket, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://chat.example.com")); const room = defineWebSocket({ maxIncomingQueueSize: 100, path: "/chat", incoming: struct.discriminatedUnion("type", [ struct.object({ type: struct.literal("pong"), ok: struct.boolean() }), ]), outgoing: struct.object({ type: struct.literal("ping") }), }); async function main() { const [error, session] = await room( client({ heartbeat: { intervalMs: 30_000, timeoutMs: 10_000, message: () => ({ type: "ping" as const }), isAck: (message) => typeof message === "object" && message !== null && "type" in message && message.type === "pong", }, }), ); if (error) { console.error(error.code); return; } console.log(session.state); session.close(1000, "done"); } void main(); ``` `intervalMs` and `timeoutMs` must be positive finite timers ≤ `2_147_483_647`. Heartbeat message must be valid for the outgoing struct. Serialization, native send, ack classification, and timeout failures are fatal to the logical session — they don’t become ordinary reconnects. ## Queues [#queues] | Setting | Required value | Behavior | | ---------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `maxIncomingQueueSize` | Positive safe integer | Bounds parsed messages waiting for `receive` and raw frames waiting for transform. Overflow → `state: 'error'` | | `maxOutgoingQueueSize` | Optional non-negative safe integer; default `0` | FIFO only while `state === 'reconnecting'`. Full/disabled → `send(...)` throws | Queued outgoing frames flush before the replacement socket publishes `open`. Frames already sent on an earlier socket are never auto-replayed. Reconnect queues are for messages you send while reconnecting — not for reconstructing app state. Incoming overflow clears the pending sequence, fails `receive`, stops the session, resolves `session.closed` with `kind: 'error'`. Keep the consumer fast enough or raise the bound from measured size/memory. ## Protocols and authentication [#protocols-and-authentication] Definition `protocols`, client `withWebSocketProtocols(...)`, and `protocols` on the derived client set the constructor subprotocol list. Precedence: derived client → client → definition. The derived list replaces the client list; it is not appended. The first defined list is copied for the logical session and reused on reconnect. Browser WebSocket constructors don’t accept arbitrary handshake headers, and the builder cannot return `headers` or `body`. Defjs converts `http:` → `ws:` and `https:` → `wss:`, encodes path placeholders once, and uses the same query rules as HTTP: scalars and repeated keys by default, `withQuerySerializer` for nested objects, and no serializer call when there is no query. As with HTTP and SSE, a missing, relative, or invalid base endpoint or an illegal path fails with `REQ_BUILD_FAILED` during building, before any interceptor or transport runs, even if an interceptor would short-circuit. The resolved URL must keep the base origin and stay under its directory; C0 control characters (including tab, CR, and LF), backslashes, `?`, `#`, and directory escapes in the path are rejected. Scheme-like and protocol-relative forms are rejected even with leading spaces; a plain inner space is allowed and sent as `%20`. An interceptor's final `req.endpoint` is validated against its final `req.baseEndpoint` with the same rule, but it may deliberately replace the base with another valid absolute URL. Deterministic URL errors are never retried. `withCredentials(true)` is Fetch credentials for HTTP/SSE — not WebSocket auth. Use reviewed cookie/session policy, subprotocol, or a short-lived connection ticket. Don’t put general credentials or long-lived secrets in the query string. ## Closure and ownership [#closure-and-ownership] `session.close(code?, reason?)` requests terminal closure and stops heartbeat. Code must be `1000` or `3000..4999`; reason ≤ 123 UTF-8 bytes. Invalid close args throw before changing state. Use it with `await session.closed` when you need a manual close reason or the logical terminal result. The `signal` passed to the operation stays attached to the opened session and covers the whole connection, including after it is open. A timeout-shaped abort (`TimeoutError` or `ERR_TIMEOUT`, including `AbortSignal.timeout(...)`) ends it as `'error'` (`NET_TIMEOUT`). Any other reason ends it as `'aborted'`, and `receive` rejects with that reason itself, not a new `AbortError`. There is no startup-only deadline. An application that wants one creates its own `AbortController`, aborts it with `new DOMException("...", "TimeoutError")` from a timer, and clears the timer after the await returns. See [SSE](/docs/sse#close-and-dispose). ```typescript import type { WebSocketSession } from "@defjs/core"; async function observeSession( session: WebSocketSession, ): Promise { await using ownedSession = session; console.log(ownedSession.state); } void observeSession; ``` `session.closed` is the logical terminal snapshot: `'closed'`, `'aborted'`, or `'error'`, with optional native `code` / `reason` / `wasClean` and a `cause` for aborted/error. Observed native close fields win over the owner’s requested fallback. The standard async disposer requests best-effort native close, then waits for Defjs-owned lifecycle, message-pump, timer, listener, queue, and socket-reference teardown. If a close event is not observed within one second, teardown is forced and the disposer may reject with a `DOMException` named `TimeoutError`; this does not prove that the physical TCP connection closed. Repeated disposer calls share the same teardown. Custom structural session implementations must now provide the same `[Symbol.asyncDispose](): PromiseLike` contract. ## GraphQL boundary [#graphql-boundary] Defjs supplies one incoming struct, one outgoing struct, and a logical session lifecycle. It does **not** implement a WebSocket application protocol. GraphQL-over-WebSocket features — connection init, operation IDs, `next`/`error`/`complete`, disposal, subscription replay — are outside the core contract. Use a protocol client like `graphql-ws` when the server requires that protocol, or model the frames with `defineWebSocket(...)`. Put a static subprotocol such as `graphql-transport-ws` on `protocols`. A struct alone does not negotiate GraphQL semantics. ## Related recipes [#related-recipes] - [Open a WebSocket session](/recipes/websocket-session) - [Consume an SSE stream](/recipes/consume-sse) # Defjs [#defjs] **Designed for better SDKs.** Defjs helps teams turn their APIs into reusable TypeScript SDKs. `def` stands for `define`: describe the operations your service offers, export them from a module or npm package, and let each application configure its own client to call them. ## Define the SDK, configure the application [#define-the-sdk-configure-the-application] | SDK author | SDK consumer | | ---------------------------------------------------------------- | -------------------------------------------------------------------- | | Define operations, input, request mapping, responses, and errors | Choose the service address, credentials, interceptors, and transport | | Export typed HTTP, SSE, and WebSocket endpoints | Import those endpoints and call them with a Defjs client | | Maintain and publish the API contract | Handle results and own cancellation, streams, and sessions | An SDK definition does not capture an application client. The same exported endpoint can serve a browser application, a server, or a local test with different client configurations. Both sides still use the Defjs execution contract and compatible Core versions. The SDK exports an operation: ```ts get-health.ts import { defineRequest, struct } from "@defjs/core"; export const getHealth = defineRequest({ method: "GET", path: "/health", output: struct.object({ ok: struct.boolean() }), }); ``` The application imports it and owns execution: ```ts app.ts import { createClient, withEndpoint } from "@defjs/core"; import { getHealth } from "./get-health.ts"; const client = createClient(withEndpoint("https://api.example.com")); async function main() { const [error, result] = await getHealth(client); if (error) { console.error(error.code); return; } console.log(result.ok); } void main(); ``` Calling `getHealth(client)` starts the request. When you publish the definition, consumers import it from your SDK package instead of the local file. ## Best practice [#best-practice] A definition stays independent of the client. Pass the application's client as the first argument, as in `getHealth(client)`. The application also owns the service address and credentials. Inspect the error-first tuple first. When `error` is present, report `error.code` and return from the function before reading the success value. ## Build your SDK [#build-your-sdk] 1. [Define and run your first operation](/docs/start) in separate SDK and application files. 2. Describe the [data contracts](/docs/struct) and [HTTP operations](/docs/http) your SDK exports. 3. [Test and publish an SDK package](/recipes/publish-http-sdk) with JavaScript, declarations, and a compatible Core dependency. 4. Configure the consuming application's [Client](/docs/client), then add framework or observability integration where needed. The [design decisions](/docs/design-decisions) explain why API definitions and execution have separate owners. You can use this same structure inside one application before publishing a package. ## Pick a transport [#pick-a-transport] | You need | Start with | Successful result | | ---------------------------------- | ----------------------------- | ------------------------------------------------------------------------ | | Request + status-specific response | [HTTP](/docs/http) | Decoded data + `DecodedResponse` (`HttpMeta` when no output is declared) | | Long-lived server event feed | [SSE](/docs/sse) | One stream + startup `open` snapshot | | Bidirectional session | [WebSocket](/docs/web-socket) | One session + startup `connection` snapshot | All three transports follow the same sequence: define an operation, then call it with the application's client and the caller's input. ## Pick a package [#pick-a-package] | Package | When | | ----------------------------- | ------------------------------------------------------------------------------------------------------ | | `@defjs/core` | Define SDK operations and contracts; create clients and call HTTP, SSE, and WebSocket operations | | `@defjs/react` | `ClientProvider` / `useClient` — see [React](/docs/integrations/react) | | `@defjs/vue` | Plugin + `injectClient` — see [Vue](/docs/integrations/vue) | | `@defjs/opentelemetry-server` | Outbound spans, metrics and logs — see [OpenTelemetry Server](/docs/integrations/opentelemetry-server) | ## Result shapes [#result-shapes] All three transports return an error-first three-item tuple. Positions match; meanings don’t: - HTTP → `[error, data, response]` - SSE → `[error, stream, open]` - WebSocket → `[error, session, connection]` On failure the second item is `undefined`. For HTTP the third item is `undefined` too, because the fault carries the response metadata. For SSE and WebSocket the third item is the startup snapshot when one was captured before the failure. See [Errors](/docs/errors). ## Ownership in one breath [#ownership-in-one-breath] Abort HTTP when it’s stale. Close SSE and `await stream.closed`. Close WebSocket and `await session.closed`. On a server, create the client inside the request boundary when options capture cookies, auth, or tenant data. Redact URLs, headers, and bodies before you log them. Defjs provides definitions, validation, and execution. Applications choose caching and retry policies; stream reconnection is opt-in. Handwritten SDK packages are supported today. CLI tooling and OpenAPI/SDK generation remain planned. ## Related recipes [#related-recipes] - [Build and publish an SDK](/recipes/publish-http-sdk) - [GET with a declared 404](/recipes/get-declared-404) - [POST JSON](/recipes/post-json) - [Cancel an HTTP call](/recipes/cancel-http) - [Consume an SSE stream](/recipes/consume-sse) - [Open a WebSocket session](/recipes/websocket-session) - [Test with a local Fetch handle](/recipes/test-with-handle) # Cancel an HTTP call [#cancel-an-http-call] Combine the caller's signal with a deadline on `signal`. `AbortSignal.any([controller.signal, AbortSignal.timeout(5_000)])` is one signal. A manual abort classifies as `NET_ABORTED`. A deadline classifies as `NET_TIMEOUT`. See [HTTP](/docs/http#cancel-the-work) for details. ```ts cancel-report.ts import { createClient, defineRequest, withEndpoint } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); const getReport = defineRequest({ method: "GET", path: "/report" }); const controller = new AbortController(); const pending = getReport( client({ signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5_000)]), }), ); controller.abort("screen closed"); async function main() { const [error] = await pending; if (error?.code === "NET_ABORTED") { console.log("caller cancelled"); return; } if (error?.code === "NET_TIMEOUT") { console.log("timed out"); return; } if (error) { console.error(error.code); return; } console.log("got the report"); } void main(); ``` ```txt caller cancelled ``` Cancellation tells you what the caller observed. It does not prove a server-side write rolled back — keep mutation retries behind an idempotency contract. # Consume an SSE stream [#consume-an-sse-stream] One async iterator per stream. Put `close` + `await stream.closed` in `finally` so early exits don’t leak the Fetch attempt. See [SSE](/docs/sse) for details. ```ts notifications.ts import { createClient, defineEventStream, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); const notifications = defineEventStream({ maxBufferSize: 64 * 1024, maxQueueSize: 100, path: "/notifications", events: { message: struct.object({ text: struct.string() }), }, }); async function main() { const [error, stream] = await notifications(client); if (error) { console.error(error.code); return; } try { for await (const event of stream) { if (event.event === "message") { console.log(event.data.text); break; } } } finally { stream.close("consumer-finished"); await stream.closed; } } void main(); ``` `api.example.com` is a placeholder. With a server whose first event is `message` with the data `{ "text": "hello from the server" }`, the snippet prints: ```txt hello from the server ``` SSE does not retry network/read failures unless you enable `withSSEReconnect(...)`. With a reconnect object, `attempts` defaults to `3` per outage; a successful handshake resets the budget and backoff exponent. `attempts: 0` disables retry. Invalid numeric reconnect options fail before any request with `REQ_OPTIONS_INVALID`; see [validation rules](/api/core/withssereconnect). Without `shouldReconnect`, only `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` requests reconnect, and a `ReadableStream` request body never does. # Revalidate with ETag and a 304 [#revalidate-with-etag-and-a-304] `ok` means 2xx, so a `304` arrives as an `HTTP_STATUS` fault. A redirect status is not an error body either, so leave `error` undeclared: the body is never read, the fault carries the status, and you return the cached product yourself. See [HTTP](/docs/http). ```ts etag-cache.ts import { createClient, type DerivableClient, defineRequest, struct, type Infer, withEndpoint, withHTTPHandle, } from "@defjs/core"; const Product = struct.object({ sku: struct.string(), name: struct.string(), priceCents: struct.number(), }); type Product = Infer; const readProduct = defineRequest({ method: "GET", path: "/catalog/:sku", input: struct.object({ sku: struct.string(), etag: struct.string().optional(), }), builder: (input) => ({ path: { sku: input.sku }, headers: { "If-None-Match": input.etag }, }), output: Product, }); function createReader(client: DerivableClient) { const cache = new Map(); return async (sku: string): Promise => { const cached = cache.get(sku); const [error, product, response] = await readProduct(client, { sku, etag: cached?.etag, }); if (error?.code === "HTTP_STATUS" && error.status === 304 && cached) { return cached.product; } // This reader returns Product, so a non-304 fault leaves here by throwing. if (error) throw error; const etag = response.headers.get("etag"); if (!etag) throw new Error("missing ETag"); cache.set(sku, { etag, product }); return product; }; } const etag = '"v7"'; const client = createClient( withEndpoint("https://catalog.example.test"), withHTTPHandle(async (input, init) => { if (new Request(input, init).headers.get("if-none-match") === etag) { return new Response(null, { status: 304, headers: { etag } }); } return Response.json( { sku: "SKU-1", name: "Flask", priceCents: 3200 }, { status: 200, headers: { etag } }, ); }), ); const read = createReader(client); await read("SKU-1"); const product = await read("SKU-1"); console.log(product.name); ``` ```txt Flask ``` `throw error` after the 304 branch is this reader's boundary. The function still returns `Promise`; a non-304 fault is not returned as a Defjs tuple from that boundary. Do not rewrite `304` into a fake `200` inside Core. Cache ownership stays in application code around the operation call. # GET with a declared 404 [#get-with-a-declared-404] Declare both success and 404 bodies. Branch on `error.code` and `error.status` — you get typed `error.data` for the declared miss. See [HTTP](/docs/http) and [Errors](/docs/errors) for details. ```ts get-user.ts import { createClient, defineRequest, struct, withEndpoint } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com")); const getUser = defineRequest({ method: "GET", path: "/users/:id", input: struct.object({ id: struct.number() }), builder: (input) => ({ path: { id: input.id } }), output: struct.object({ id: struct.number(), name: struct.string() }), error: struct.object({ message: struct.string() }), }); async function main() { const [error, user, response] = await getUser(client, { id: 7 }); if (error?.code === "HTTP_STATUS" && error.status === 404) { console.log(error.data.message); return; } if (error) { console.error(error.code); return; } console.log(`Loaded ${user.name} from ${response.status}`); } void main(); ``` `api.example.com` is a placeholder. With a server that answers `GET /users/7` with `200` and `{ "id": 7, "name": "Ada" }`, the snippet prints: ```txt Loaded Ada from 200 ``` A non-2xx status is `HTTP_STATUS`. Declaring `error` gives its body a type; omitting `error` leaves `error.data` as `undefined` and the body unread. If a declared `error` body cannot be read (wrong media type, invalid JSON, wrong shape), the result is a `RES_*` fault instead. One `error` Struct covers every non-2xx status, so use `struct.or(...)` or a discriminated union when the shapes differ. # Unwrap a GraphQL HTTP envelope [#unwrap-a-graphql-http-envelope] Defjs does not implement GraphQL `errorPolicy`. Model the wire as an HTTP POST whose builder places the query document and variables. Declare envelope `data` as `.nullish()` so both missing `data` and `data: null` reach your helper, then unwrap `errors[]` before checking `data`. See [Design Decisions](/docs/design-decisions). ```ts graphql-viewer.ts import { createClient, defineRequest, struct, withEndpoint, withHTTPHandle, type Client, } from "@defjs/core"; const VIEWER_QUERY = "query Viewer { viewer { id login } }" as const; const runViewer = defineRequest({ method: "POST", path: "/graphql", builder: () => ({ body: { query: VIEWER_QUERY, operationName: "Viewer" }, }), output: struct.object({ data: struct .object({ viewer: struct .object({ id: struct.string(), login: struct.string() }) .null(), }) .nullish(), errors: struct .array(struct.object({ message: struct.string() })) .optional(), }), }); async function loadViewer(client: Client) { const [error, result] = await runViewer(client); // A Defjs fault is outside the HTTP 200 envelope this helper returns. if (error) throw error; if (result.errors?.length) { throw new Error(result.errors.map((item) => item.message).join("; ")); } if (!result.data) throw new Error("GraphQL response contained neither data nor errors"); return result.data.viewer; } let calls = 0; const responses = [ { data: { viewer: { id: "v-1", login: "ada" } } }, { errors: [{ message: "viewer unavailable" }] }, { data: null, errors: [{ message: "viewer unavailable" }] }, ]; const client = createClient( withEndpoint("https://api.example.test"), withHTTPHandle(async () => { calls += 1; return Response.json(responses[calls - 1], { status: 200 }); }), ); const viewer = await loadViewer(client); const graphQlErrors: string[] = []; for (let attempt = 0; attempt < 2; attempt += 1) { try { await loadViewer(client); } catch (error) { graphQlErrors.push(error instanceof Error ? error.message : String(error)); } } console.log(viewer?.login, ...graphQlErrors); ``` ```txt ada viewer unavailable viewer unavailable ``` HTTP success only means the envelope arrived. Application errors stay in `errors[]` until your unwrap throws or returns, including a response with `data: null`. The example handles both missing `data` and `data: null` with `errors[]` as application errors, not Struct mismatches. `if (error) throw error` adapts a Defjs fault to that helper. HTTP 200 GraphQL failures stay application errors in `errors[]` and are thrown on their own. Start with [Build and publish an SDK](/recipes/publish-http-sdk) for the full path from exported definition to npm package and consuming application. The recipes below show individual operation contracts and application policies you can compose around that SDK. ## HTTP [#http] - [Send JSON](/recipes/post-json) - [Handle a declared 404](/recipes/get-declared-404) - [Cancellation and timeouts](/recipes/cancel-http) - [ETag conditional requests](/recipes/etag-revalidate) - [GraphQL requests](/recipes/graphql-http-envelope) ## Authentication [#authentication] - [Refresh a token after 401](/recipes/refresh-bearer-once) ## Realtime [#realtime] - [Consume SSE events](/recipes/consume-sse) - [Manage WebSocket sessions](/recipes/websocket-session) ## SDK testing and publishing [#testing-and-packaging] - [Local HTTP tests](/recipes/test-with-handle) - [Build and publish an SDK](/recipes/publish-http-sdk) # POST JSON [#post-json] Hand the typed body to the builder. Logical field names stay in TypeScript; wire aliases on the struct become JSON keys. An object body is `application/json`. See [HTTP](/docs/http) and [Client](/docs/client) for details. ```ts create-user.ts import { createClient, defineRequest, struct, withEndpoint } from "@defjs/core"; const client = createClient(withEndpoint("https://api.example.com/v1")); const User = struct.object({ id: struct.number(), name: struct.string(), email: struct.string(), }); const ApiError = struct.object({ code: struct.string(), message: struct.string(), }); const NewUser = struct.object({ name: struct.string(), email: struct.string(), }); const createUser = defineRequest({ method: "POST", path: "/users", input: NewUser, builder: (input) => ({ body: input }), output: User, error: ApiError, }); async function main() { const [error, user, response] = await createUser(client, { name: "Ada", email: "ada@example.com", }); if (error?.code === "HTTP_STATUS") { console.error(error.status, error.data.message); return; } if (error) { console.error(error.code); return; } console.log(`Created ${user.name} → ${response.status}`); } void main(); ``` `api.example.com` is a placeholder. With a server that answers `201` and `{ "id": 1, "name": "Ada", "email": "ada@example.com" }`, the snippet prints: ```txt Created Ada → 201 ``` # Build and publish an SDK [#publish-an-http-sdk-without-hiding-execute] An SDK packages the API your team offers. With Defjs, export endpoint definitions and their data contracts; consuming applications supply the client configuration and decide when to call them. This recipe uses HTTP, and the same package boundary works for SSE and WebSocket definitions. ## 1. Define the SDK's operations [#1-define-the-sdks-operations] Create `src/users.ts` in your SDK project. Keep paths, input mapping, responses, and service errors here. No client instance, deployment address, or current credential is needed to define this operation. ```ts title="src/users.ts" import { defineRequest, struct } from "@defjs/core"; export const getUser = defineRequest({ method: "GET", path: "/users/:id", input: struct.object({ id: struct.number() }), builder: (input) => ({ path: { id: input.id } }), output: struct.object({ id: struct.number(), name: struct.string() }), error: struct.object({ message: struct.string() }), }); ``` Expose it from `src/index.ts`: ```ts export { getUser } from "./users"; ``` Add other operation modules and re-export their endpoints as your SDK grows. ## 2. Package JavaScript and declarations [#2-package-javascript-and-declarations] The SDK project has its own package boundary: ```txt users-sdk/ package.json tsconfig.json src/ index.ts users.ts ``` Use this `package.json`, replacing the example name with a package scope you own: ```json { "name": "@company/users-sdk", "version": "0.1.0", "type": "module", "sideEffects": false, "files": ["dist"], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsdown --platform neutral" }, "peerDependencies": { "@defjs/core": "^0.6.0" }, "devDependencies": { "@defjs/core": "^0.6.0", "tsdown": "^0.23.3", "typescript": "^7.0.2" } } ``` Core remains an external runtime dependency shared with the application. The peer range states which Core releases your SDK supports; the development dependency lets you build and test against that range. This example matches the current `0.4.x` contract. Choose and test the range for your SDK before publishing; declarations and runtime behavior both depend on it. Add `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022", "ESNext.Disposable", "DOM", "DOM.Iterable"], "strict": true, "noEmit": true }, "include": ["src/**/*.ts"] } ``` These library declarations provide types, not runtime polyfills. The application still needs the Web APIs used by its chosen transports. `tsdown` bundles the entry into `dist/index.js` and emits `dist/index.d.ts`. Build and inspect the package: ```sh bun install bun run build bun pm pack --dry-run bun pm pack ``` Check that the tarball contains `dist/index.js` and `dist/index.d.ts`. Install that tarball into a separate application and verify both its types and execution before publishing. The SDK's README should document the service API, compatible Core versions, expected authentication, and any optional defaults. ## 3. Call it from the consuming application [#3-call-it-from-the-consuming-application] The local example keeps the definition in `users-sdk.ts` beside `app.ts` so it can run offline. Its import creates no client; the application configures a local Fetch handle and calls the imported operation: ```ts title="app.ts" import { createClient, withEndpoint, withHTTPHandle, type FetchHandle, } from "@defjs/core"; import { getUser } from "./users-sdk.ts"; const handle: FetchHandle = async () => Response.json({ id: 7, name: "Ada" }, { status: 200 }); const client = createClient( withEndpoint("https://api.example.test"), withHTTPHandle(handle), ); async function main() { const [error, user] = await getUser(client, { id: 7 }); if (error) { console.error(error.code); return; } console.log(user.name); } void main(); ``` ```txt Ada ``` For the packaged SDK, install it together with a compatible Core release: ```sh bun add @company/users-sdk @defjs/core@^0.6.0 ``` Replace the example's relative import with: ```ts import { getUser } from "@company/users-sdk"; ``` Configure your service address and authentication on the application client. Keep `withHTTPHandle(...)` for local tests; remove it to use the platform Fetch implementation. A second application can import the same endpoint and choose different client settings without changing the SDK definition. ## 4. Verify and publish [#4-verify-and-publish] Use [client-local tests](/recipes/test-with-handle) to exercise the exported definition, request mapping, response validation, and declared errors. Verify the same endpoint with two clients configured for different service addresses and credentials. Keep typed `HTTP_STATUS` handling available at the call site, and test cancellation and stream cleanup for the transports your SDK exports. When the package has passed your checks, publish from the SDK directory with your npm account: ```sh bun publish --access public ``` This command publishes a public package. Use your organization's registry and access policy for a private SDK. Defjs does not generate or publish the package for you. Client factories and convenience methods are optional additions. They can provide service defaults or domain behavior while accepting caller configuration and preserving direct endpoint exports. Document any error mapping or lifecycle policy they introduce. Neither is required for a reusable SDK. See [HTTP](/docs/http), [Client](/docs/client), and [Design decisions](/docs/design-decisions) for the underlying contracts. # Refresh a Bearer token once on 401 [#refresh-a-bearer-token-once-on-401] `next()` is the rest of **this** onion — not `axios(config)` re-dispatch from the outside. Put attach + single-flight refresh + one replay in **one** HTTP interceptor so outer layers do not run twice. See [Interceptors](/docs/interceptors). ```ts refresh-bearer.ts import { createClient, createHttpInterceptor, defineRequest, struct, withEndpoint, withHTTPHandle, withInterceptors, } from "@defjs/core"; const getInvoice = defineRequest({ method: "GET", path: "/v1/invoice", output: struct.object({ total: struct.number() }), error: struct.object({ code: struct.literal("expired_token") }), }); type Credential = { accessToken: string }; function refreshingBearer( read: () => Credential, refresh: (stale: Credential) => Promise, ) { let flight: Promise | undefined; return createHttpInterceptor(async (request, next) => { const authorize = (credential: Credential) => { const headers = new Headers(request.headers); headers.set("authorization", `Bearer ${credential.accessToken}`); return { ...request, headers }; }; const used = read(); const response = await next(authorize(used)); const body = response.body; if ( response.status !== 401 || typeof body !== "object" || body === null || !("code" in body) || body.code !== "expired_token" ) { return response; } const current = read(); if (current.accessToken !== used.accessToken) return next(authorize(current)); flight ??= refresh(used).finally(() => { flight = undefined; }); const refreshed = await flight; return next(authorize(refreshed)); }); } let credential: Credential = { accessToken: "v1" }; const client = createClient( withEndpoint("https://billing.example.test"), withHTTPHandle(async (input, init) => { const auth = new Request(input, init).headers.get("authorization"); if (auth === "Bearer v1") return Response.json({ code: "expired_token" }, { status: 401 }); return Response.json({ total: 42 }, { status: 200 }); }), withInterceptors( refreshingBearer( () => credential, async () => { credential = { accessToken: "v2" }; return credential; }, ), ), ); const outcome = await getInvoice(client); const [error, invoice] = outcome; function reportInvoice(result: typeof outcome) { const [fault, body] = result; if (fault) { console.error(fault.code); return; } console.log(body.total); } reportInvoice(outcome); ``` ```txt 42 ``` Calling `next` again only re-enters interceptors **inside** this layer (and the Fetch handler). It does not re-run outer timing/auth wrappers — that is why 401 layout belongs in one interceptor, not a second call of the operation. # Test with a local Fetch handle [#test-with-a-local-fetch-handle] `withHTTPHandle(...)` replaces Fetch for that client only. You still get URL building, interceptors, status dispatch, and Struct decoding — without DNS, TLS, or a real server. See [Client](/docs/client) for details. ```ts get-user.test.ts import { createClient, defineRequest, struct, withEndpoint, withHTTPHandle, } from "@defjs/core"; const getUser = defineRequest({ method: "GET", path: "/users/:id", input: struct.object({ id: struct.number() }), builder: (input) => ({ path: { id: input.id } }), output: struct.object({ id: struct.number(), name: struct.string() }), error: struct.object({ message: struct.string() }), }); const client = createClient( withEndpoint("https://fixture.invalid"), withHTTPHandle(async (input, init) => { const request = new Request(input, init); const id = new URL(request.url).pathname.split("/").at(-1); if (id === "7") return Response.json({ id: 7, name: "Ada" }, { status: 200 }); return Response.json({ message: "User not found" }, { status: 404 }); }), ); async function main() { const [error, user, response] = await getUser(client, { id: 7 }); if (error) { // The test boundary rejects failures so the test process cannot pass. throw error; } console.log(user.name, response.status); } await main(); ``` ```txt Ada 200 ``` Use the same `getUser` operation against a real `withEndpoint(...)` client in production — only the handle changes. # Open a WebSocket session [#open-a-websocket-session] Each direction is one struct. You own unsubscribe, `close`, and `await session.closed`. Invalid frames go to `onRuntimeError`. See [WebSocket](/docs/web-socket) for details. ```ts chat.ts import { createClient, defineWebSocket, struct, withEndpoint, } from "@defjs/core"; const client = createClient(withEndpoint("https://chat.example.com")); const room = defineWebSocket({ maxIncomingQueueSize: 100, path: "/chat", incoming: struct.object({ type: struct.literal("message"), text: struct.string(), }), outgoing: struct.object({ type: struct.literal("send"), text: struct.string(), }), }); async function main() { const [error, session] = await room(client); if (error) { console.error(error.code); return; } const unsubscribe = session.onRuntimeError((cause) => console.error("runtime", cause), ); try { session.send({ type: "send", text: "Hello" }); for await (const message of session.receive) { console.log(message.type, message.text); break; } } finally { unsubscribe(); session.close(1000, "consumer-finished"); await session.closed; } } void main(); ``` `chat.example.com` is a placeholder. With a server that answers `{ "type": "send", "text": "Hello" }` with `{ "type": "message", "text": "Hello" }`, the snippet prints: ```txt message Hello ``` Reconnect is opt-in (`withWebSocketReconnect`). Heartbeat is opt-in too. Browser WebSockets can’t take arbitrary handshake headers — use cookies, subprotocols, or a short-lived ticket.