@defjs/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +25 -71
  2. package/index.d.ts +363 -516
  3. package/index.js +701 -963
  4. package/index.min.js +20 -20
  5. package/package.json +1 -1
package/index.js CHANGED
@@ -1,23 +1,3 @@
1
- // src/client/command.ts
2
- const COMMAND_TYPE = Symbol.for("defjs.command.type");
3
- const HTTP_COMMAND = Symbol.for("defjs.command.http");
4
- const EVENT_STREAM_COMMAND = Symbol.for("defjs.command.event-stream");
5
- const WEB_SOCKET_COMMAND = Symbol.for("defjs.command.web-socket");
6
- /** The command's own runner, so `client.execute` never imports a transport the app does not use. */
7
- const COMMAND_EXECUTE = Symbol.for("defjs.command.execute");
8
- function commandTypeOf(value) {
9
- if (typeof value !== "object" || value === null || !(COMMAND_TYPE in value)) return;
10
- return value[COMMAND_TYPE];
11
- }
12
- function isHttpCommand(value) {
13
- return commandTypeOf(value) === HTTP_COMMAND;
14
- }
15
- function isEventStreamCommand(value) {
16
- return commandTypeOf(value) === EVENT_STREAM_COMMAND;
17
- }
18
- function isWebSocketCommand(value) {
19
- return commandTypeOf(value) === WEB_SOCKET_COMMAND;
20
- }
21
1
  // src/client/config.ts
22
2
  const PLATFORM_FETCH = globalThis.fetch;
23
3
  const PLATFORM_FETCH_HANDLE = PLATFORM_FETCH?.bind(globalThis);
@@ -61,142 +41,482 @@ function createClientConfig() {
61
41
  };
62
42
  }
63
43
  // src/client/client.ts
44
+ /**
45
+ * Internal symbol key for a client's base configuration and derived request options.
46
+ *
47
+ * Application code calls the client and passes it to an operation. It does not read this property.
48
+ *
49
+ * @group client
50
+ */
64
51
  const CLIENT = Symbol("Client");
52
+ function overlay(base, patch) {
53
+ if (patch === void 0) return { ...base };
54
+ return {
55
+ abort: patch.abort ?? base.abort,
56
+ onUploadProgress: patch.onUploadProgress ?? base.onUploadProgress,
57
+ onDownloadProgress: patch.onDownloadProgress ?? base.onDownloadProgress,
58
+ beforeConnect: patch.beforeConnect ?? base.beforeConnect,
59
+ protocols: patch.protocols ?? base.protocols,
60
+ heartbeat: patch.heartbeat ?? base.heartbeat,
61
+ reconnect: patch.reconnect ?? base.reconnect
62
+ };
63
+ }
64
+ function createDerivedClient(config, options) {
65
+ const state = {
66
+ config,
67
+ options
68
+ };
69
+ function derive(patch) {
70
+ return createDerivedClient(config, overlay(options, patch));
71
+ }
72
+ derive[CLIENT] = state;
73
+ return derive;
74
+ }
65
75
  /**
66
76
  * Creates a Defjs client from option helpers.
67
77
  *
68
- * 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 the interceptor chain invokes transport.
78
+ * 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.
79
+ *
80
+ * The returned client is a function. Call it with {@link RequestOptions} to derive a client for one operation. The seven known fields are copied from that argument into a fresh options object. Nested `heartbeat`, `protocols`, and `reconnect` values are kept by reference. The base configuration is shared by every derived client and is not frozen at runtime. It is fixed by convention: nothing in the library changes it, and applications should not either. A getter that throws escapes the call; it is not converted into a fault.
69
81
  *
70
82
  * @param options - Client option helpers such as `withEndpoint`.
71
- * @returns A client that can `execute` HTTP, SSE, and WebSocket commands.
83
+ * @returns A callable client that carries the resolved base configuration.
72
84
  *
73
85
  * @group client
74
86
  * @see {@link Client}
75
87
  * @see {@link ClientOption}
88
+ * @see {@link RequestOptions}
76
89
  * @exampleFile examples/api/create-client.ts#example
77
90
  */
78
91
  function createClient(...options) {
79
- const conf = createClientConfig();
80
- for (const option of options) option(conf);
81
- function execute(command, options) {
82
- if (isHttpCommand(command) || isEventStreamCommand(command) || isWebSocketCommand(command)) return command[COMMAND_EXECUTE](conf, command, options);
83
- return Promise.reject(/* @__PURE__ */ new Error("Unsupported command"));
84
- }
85
- return {
86
- [CLIENT]: conf,
87
- execute
88
- };
92
+ const config = createClientConfig();
93
+ for (const option of options) option(config);
94
+ return createDerivedClient(config, {});
89
95
  }
90
- // src/struct/symbols.ts
91
- const DEFINITION = Symbol.for("defjs.struct.definition");
92
- const OMIT = Symbol("defjs.struct.omit");
96
+ // src/client/option.ts
93
97
  /**
94
- * Stands in for a parsed value when parsing fails; the issue waits in the slot that `failure()` fills.
95
- * Not in the symbol registry, so no input value can ever equal it.
98
+ * Sets the valid absolute base URL prepended to relative request paths.
99
+ *
100
+ * 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:`.
101
+ *
102
+ * 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.
103
+ *
104
+ * @param endpoint - Absolute base endpoint. Missing or invalid endpoints fail request building.
105
+ * @returns An option that writes `config.endpoint`.
106
+ * @group client
96
107
  */
97
- const FAILED = Symbol("defjs.struct.failed");
108
+ function withEndpoint(endpoint) {
109
+ return (config) => {
110
+ config.endpoint = endpoint;
111
+ };
112
+ }
98
113
  /**
99
- * Type-only export of the internal enumerable tag key on objects produced while parsing endpoint input, used by the outbound encoder.
114
+ * Sets static default headers for HTTP and SSE requests built by this client.
100
115
  *
101
- * `STRUCT` is exported as a **type** only (`export type { STRUCT }`). `@defjs/core` has no runtime `STRUCT` export, so `import { STRUCT } from '@defjs/core'` fails to compile and gives no value.
116
+ * WebSocket connections do not use them. The `HeadersInit` is copied into a `Headers` object when the option is applied. A later call replaces the earlier headers instead of merging.
102
117
  *
103
- * It marks those objects so the outbound encoder knows which struct produced them. Response decoding and `struct.parse` leave it unset. The symbol is internal to the outbound encoder; application code should not depend on it.
118
+ * Headers returned by a request builder replace a default of the same name. The first value for that name replaces the default; later values in the same array append only to that replacement. A scalar or a `Headers` instance also replaces. Other default names stay.
104
119
  *
105
- * @internal
106
- * @group struct
120
+ * Per-request or computed credentials, such as a refreshed Bearer token or an HMAC signature, belong in an interceptor, because they need the serialized body or a refreshed credential.
121
+ *
122
+ * @param headers - `HeadersInit` bag cloned onto the client config.
123
+ * @returns An option that writes `config.headers`.
124
+ * @group client
107
125
  */
108
- const STRUCT = Symbol.for("defjs.struct.STRUCT");
109
- // src/struct/guards.ts
110
- const KNOWN_KINDS = /* @__PURE__ */ new Set([
111
- "any",
112
- "array",
113
- "arrayBuffer",
114
- "bigint",
115
- "blob",
116
- "boolean",
117
- "date",
118
- "discriminatedUnion",
119
- "enum",
120
- "file",
121
- "intersection",
122
- "literal",
123
- "null",
124
- "number",
125
- "object",
126
- "or",
127
- "record",
128
- "string",
129
- "tuple",
130
- "unknown"
131
- ]);
132
- function isStruct(value) {
133
- if (typeof value !== "object" || value === null || !Object.hasOwn(value, DEFINITION)) return false;
134
- const definition = value[DEFINITION];
135
- if (typeof definition !== "object" || definition === null) return false;
136
- if (!KNOWN_KINDS.has(definition.kind)) return false;
137
- const flags = definition.flags;
138
- return typeof flags === "object" && flags !== null && typeof flags.nullable === "boolean" && typeof flags.optional === "boolean";
126
+ function withHeaders(headers) {
127
+ return (config) => {
128
+ config.headers = new Headers(headers);
129
+ };
139
130
  }
140
- // src/struct/guide.ts
141
- const GUIDE_SET = Symbol.for("defjs.struct.guideSet");
142
- function isGuideSet(value) {
143
- return typeof value === "object" && value !== null && value[GUIDE_SET] === true;
131
+ /**
132
+ * Appends interceptors to the client interceptor chain.
133
+ *
134
+ * 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.
135
+ *
136
+ * @param interceptors - Interceptors applied in registration order.
137
+ * @returns An option that pushes onto `config.interceptors`.
138
+ * @group client
139
+ */
140
+ function withInterceptors(...interceptors) {
141
+ return (config) => {
142
+ config.interceptors.push(...interceptors);
143
+ };
144
144
  }
145
- function guideTag(members) {
146
- const unique = [];
147
- for (const member of members) if (!unique.includes(member)) unique.push(member);
148
- if (unique.length === 0) return;
149
- if (unique.length === 1) return unique[0];
150
- return {
151
- [GUIDE_SET]: true,
152
- members: unique
145
+ /**
146
+ * Replaces query encoding for HTTP, SSE, and WebSocket requests.
147
+ *
148
+ * `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.
149
+ *
150
+ * 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.
151
+ *
152
+ * @param serializer - Function that turns an encoded query object into a string.
153
+ * @returns An option that sets `config.querySerializer`.
154
+ * @group client
155
+ */
156
+ function withQuerySerializer(serializer) {
157
+ return (config) => {
158
+ config.querySerializer = serializer;
153
159
  };
154
160
  }
155
- function guideMembers(guide) {
156
- return isGuideSet(guide) ? guide.members : [guide];
161
+ /**
162
+ * Replaces the Fetch-compatible function used for HTTP requests.
163
+ *
164
+ * The default is the platform `fetch`. `fetchImpl` is a {@link FetchHandle}, so a test double does not need host extras such as `fetch.preconnect`. It only affects HTTP. SSE uses {@link withSSEHandle}.
165
+ *
166
+ * @param fetchImpl - `(input, init?) => Promise<Response>`. Not `typeof fetch`, so
167
+ * a mock does not need host extras such as `preconnect`.
168
+ * @returns An option that sets `config.http.handle`.
169
+ * @group client
170
+ */
171
+ function withHTTPHandle(fetchImpl) {
172
+ return (config) => {
173
+ config.http.handle = fetchImpl;
174
+ };
157
175
  }
158
- function combineGuides(left, right) {
159
- if (!left) return right;
160
- if (!right) return left;
161
- return guideTag([...guideMembers(left), ...guideMembers(right)]);
176
+ /**
177
+ * Replaces the Fetch-compatible function used to open SSE event streams.
178
+ *
179
+ * The default is the platform `fetch`. `fetchImpl` is a {@link FetchHandle}. It only affects SSE. HTTP uses {@link withHTTPHandle}.
180
+ *
181
+ * @param fetchImpl - `(input, init?) => Promise<Response>`. Not `typeof fetch`, so
182
+ * a mock does not need host extras such as `preconnect`.
183
+ * @returns An option that sets `config.sse.handle`.
184
+ * @group client
185
+ */
186
+ function withSSEHandle(fetchImpl) {
187
+ return (config) => {
188
+ config.sse.handle = fetchImpl;
189
+ };
162
190
  }
163
- function readOwnGuide(value, guides) {
164
- const tag = guides?.get(value) ?? value[STRUCT];
165
- if (isGuideSet(tag)) return tag;
166
- if (!isStruct(tag)) return;
167
- const kind = tag[DEFINITION].kind;
168
- if (kind === "object" || kind === "record") return tag;
191
+ /**
192
+ * Replaces the WebSocket constructor used for socket connections.
193
+ *
194
+ * The default is `globalThis.WebSocket`. `WebSocketImpl` is a {@link WebSocketHandleConstructor} that returns a {@link WebSocketHandle}. If no constructor is available, a WebSocket operation fails with an `ENV_UNSUPPORTED` fault.
195
+ *
196
+ * @param WebSocketImpl - Constructor that returns a `WebSocketHandle`.
197
+ * @returns An option that sets `config.webSocket.handle`.
198
+ * @group client
199
+ */
200
+ function withWebSocketHandle(WebSocketImpl) {
201
+ return (config) => {
202
+ config.webSocket.handle = WebSocketImpl;
203
+ };
169
204
  }
170
- /** Tag a null-prototype node this parse just created. Enumerable so object spread keeps its guide. */
171
- function tagOwnNode(value, tag) {
172
- value[STRUCT] = tag;
205
+ /**
206
+ * Runs a hook before the initial WebSocket connect and before every reconnect.
207
+ *
208
+ * The hook type is:
209
+ *
210
+ * ```ts
211
+ * (context: { attempt: number; signal: AbortSignal }) => void | Promise<void>
212
+ * ```
213
+ *
214
+ * - `attempt` is `0` for the first connect and grows by one for each reconnect.
215
+ * - Pass `signal` into token refreshes and other async work so cancelling the session stops them.
216
+ * - If the hook throws, the attempt fails with an `EXT_HOOK_FAILED` fault.
217
+ * - A `beforeConnect` in the request options replaces this one for that call.
218
+ *
219
+ * @param beforeConnect - Hook receiving attempt index and abort signal.
220
+ * @returns An option that sets `config.webSocket.beforeConnect`.
221
+ * @group client
222
+ */
223
+ function withWebSocketBeforeConnect(beforeConnect) {
224
+ return (config) => {
225
+ config.webSocket.beforeConnect = beforeConnect;
226
+ };
173
227
  }
174
228
  /**
175
- * Deep-merge two intersection outputs.
229
+ * Sets the WebSocket subprotocols offered during the handshake.
176
230
  *
177
- * `Object.is` keeps NaN and distinguishes signed zeroes. Equal-time Dates are one instant even
178
- * though the instances differ. Plain objects are copied onto a null prototype so `__proto__` stays
179
- * an own data key. Anything else, including arrays of different lengths, is a conflict.
231
+ * 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']`.
180
232
  *
181
- * `tagNodes` folds each side's parse tag into the new object at this level. One-sided children are
182
- * copied with the tag their own parser already wrote, so a later walk is not required. Null and
183
- * undefined never enter this branch. `depth` is the caller's logical path length, not a fresh merge budget.
184
- * Distinct containers consume a level even when they came from opaque leaves; identical references do not recurse.
233
+ * Precedence, highest first: the `protocols` request option, this client option, then `protocols` on the endpoint definition.
234
+ *
235
+ * @param protocols - Subprotocol list for the WebSocket constructor.
236
+ * @returns An option that sets `config.webSocket.protocols`.
237
+ * @group client
185
238
  */
186
- function mergeIntersectionValues(left, right, tagNodes, depth, context) {
187
- if (Object.is(left, right)) return {
188
- ok: true,
189
- value: left
239
+ function withWebSocketProtocols(protocols) {
240
+ return (config) => {
241
+ config.webSocket.protocols = protocols;
190
242
  };
191
- if (left instanceof Date && right instanceof Date && left.getTime() === right.getTime()) return {
192
- ok: true,
193
- value: left
243
+ }
244
+ /**
245
+ * Enables WebSocket heartbeat messages with optional acknowledgement detection.
246
+ *
247
+ * Passing `undefined` clears an earlier setting. The option type is:
248
+ *
249
+ * ```ts
250
+ * {
251
+ * intervalMs: number
252
+ * isAck?: (message: unknown) => boolean
253
+ * message?: <T = unknown>() => T | unknown
254
+ * timeoutMs?: number
255
+ * }
256
+ * ```
257
+ *
258
+ * - 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`.
259
+ * - 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`.
260
+ * - If no ack arrives within `timeoutMs`, or a heartbeat cannot be sent, the session fails. This does not trigger a reconnect.
261
+ * - Heartbeat uses the same JSON guard as `session.send`: a value JSON cannot represent after struct encoding causes `TypeError` and ends the logical session.
262
+ * - `intervalMs` and `timeoutMs` must be positive, finite, and at most `2_147_483_647`; otherwise the operation fails with `REQ_OPTIONS_INVALID`.
263
+ * - A `heartbeat` in the request options replaces this one for that call.
264
+ *
265
+ * @param options - Heartbeat interval, payload, timeout, and ACK predicate.
266
+ * @returns An option that sets `config.webSocket.heartbeat`.
267
+ * @group client
268
+ */
269
+ function withWebSocketHeartbeat(options) {
270
+ return (config) => {
271
+ config.webSocket.heartbeat = options;
194
272
  };
195
- if (isPlainObject(left) && isPlainObject(right)) {
196
- if (depth >= 256) return {
197
- ok: false,
198
- path: [],
199
- received: right
273
+ }
274
+ /**
275
+ * Configures automatic reconnection when the socket closes for any reason other than a manual `session.close()`.
276
+ *
277
+ * Reconnection is opt-in: without this option a close ends the session. Passing `undefined` clears an earlier setting. The option type is:
278
+ *
279
+ * ```ts
280
+ * {
281
+ * attempts?: number
282
+ * delayMs?: number
283
+ * factor?: number
284
+ * jitter?: number
285
+ * maxDelayMs?: number
286
+ * shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean }) => boolean
287
+ * }
288
+ * ```
289
+ *
290
+ * - `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.
291
+ * - `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.
292
+ * - `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()`.
293
+ * - 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.
294
+ * - A `reconnect` in the request options replaces this one for that call.
295
+ *
296
+ * @param options - Retry budget, backoff, and `shouldReconnect` gate.
297
+ * @returns An option that sets `config.webSocket.reconnect`.
298
+ * @group client
299
+ */
300
+ function withWebSocketReconnect(options) {
301
+ return (config) => {
302
+ config.webSocket.reconnect = options;
303
+ };
304
+ }
305
+ /**
306
+ * Observes SSE events that have no matching event struct or fail validation.
307
+ *
308
+ * The handler may be async and is awaited. The callback receives:
309
+ *
310
+ * ```ts
311
+ * {
312
+ * reason: 'missing-struct' | 'validation-failed'
313
+ * message: { id: string; event: string; data: string }
314
+ * cause?: unknown
315
+ * signal: AbortSignal
316
+ * }
317
+ * ```
318
+ *
319
+ * - The invalid event is **always dropped**; it is never delivered to the stream. The callback only observes it.
320
+ * - The return value is ignored. An error thrown by the callback is swallowed and does not close the stream, unless `signal` has already aborted.
321
+ * - `cause` is set for `validation-failed`. Without a handler the invalid event is dropped silently.
322
+ *
323
+ * @param handler - Observer callback for invalid events.
324
+ * @returns An option that sets `config.sse.onInvalidEvent`.
325
+ * @group client
326
+ */
327
+ function withSSEOnInvalidEvent(handler) {
328
+ return (config) => {
329
+ config.sse.onInvalidEvent = handler;
330
+ };
331
+ }
332
+ /**
333
+ * Configures automatic reconnection after an SSE stream fails.
334
+ *
335
+ * Reconnection is opt-in: without this option a failed stream is not retried. Passing `undefined` clears an earlier setting.
336
+ *
337
+ * ```ts
338
+ * type SSEReconnect = {
339
+ * attempts?: number;
340
+ * delayMs?: number;
341
+ * factor?: number;
342
+ * jitter?: number;
343
+ * maxDelayMs?: number;
344
+ * shouldReconnect?: (context: {
345
+ * attempt: number;
346
+ * cause?: unknown;
347
+ * lastEventId: string;
348
+ * method: string;
349
+ * open?: {
350
+ * response: { status: number; statusText: string; url: string };
351
+ * url: string;
352
+ * };
353
+ * }) => boolean | Promise<boolean>;
354
+ * };
355
+ * ```
356
+ *
357
+ * - `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.
358
+ * - 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.
359
+ * - A request with a `ReadableStream` body never reconnects, and `shouldReconnect` is not called for it.
360
+ * - `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.
361
+ * - `delayMs` is only the starting base delay (`1000` by default). A server `retry:` field (digits only, in milliseconds) replaces it for later attempts.
362
+ * - 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.
363
+ * - 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.
364
+ *
365
+ * @param options - Retry budget, backoff, and `shouldReconnect` gate.
366
+ * @returns An option that sets `config.sse.reconnect`.
367
+ * @group client
368
+ */
369
+ function withSSEReconnect(options) {
370
+ return (config) => {
371
+ config.sse.reconnect = options;
372
+ };
373
+ }
374
+ /**
375
+ * Enables XSRF cookie-to-header token forwarding for HTTP requests.
376
+ *
377
+ * SSE and WebSocket requests do not get the header. `cookieName` defaults to `XSRF-TOKEN` and `headerName` to `X-XSRF-TOKEN` when names are omitted.
378
+ *
379
+ * - 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.
380
+ * - 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.
381
+ * - 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.
382
+ *
383
+ * @param options - Cookie/header names and optional token provider.
384
+ * @returns An option that sets `config.xsrf`.
385
+ * @group client
386
+ */
387
+ function withXSRF(options = {}) {
388
+ return (config) => {
389
+ config.xsrf = {
390
+ cookieName: options.cookieName ?? "XSRF-TOKEN",
391
+ headerName: options.headerName ?? "X-XSRF-TOKEN",
392
+ tokenProvider: options.tokenProvider
393
+ };
394
+ };
395
+ }
396
+ /**
397
+ * Sets whether HTTP and SSE requests include credentials.
398
+ *
399
+ * 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.
400
+ *
401
+ * @param value - When `true`, credentials are sent with cross-origin requests.
402
+ * @returns An option that sets `config.withCredentials`.
403
+ * @group client
404
+ */
405
+ function withCredentials(value) {
406
+ return (config) => {
407
+ config.withCredentials = value;
408
+ };
409
+ }
410
+ // src/struct/symbols.ts
411
+ const DEFINITION = Symbol.for("defjs.struct.definition");
412
+ const OMIT = Symbol("defjs.struct.omit");
413
+ /**
414
+ * Stands in for a parsed value when parsing fails; the issue waits in the slot that `failure()` fills.
415
+ * Not in the symbol registry, so no input value can ever equal it.
416
+ */
417
+ const FAILED = Symbol("defjs.struct.failed");
418
+ /**
419
+ * Type-only export of the internal enumerable tag key on objects produced while parsing endpoint input, used by the outbound encoder.
420
+ *
421
+ * `STRUCT` is exported as a **type** only (`export type { STRUCT }`). `@defjs/core` has no runtime `STRUCT` export, so `import { STRUCT } from '@defjs/core'` fails to compile and gives no value.
422
+ *
423
+ * It marks those objects so the outbound encoder knows which struct produced them. Response decoding and `struct.parse` leave it unset. The symbol is internal to the outbound encoder; application code should not depend on it.
424
+ *
425
+ * @internal
426
+ * @group struct
427
+ */
428
+ const STRUCT = Symbol.for("defjs.struct.STRUCT");
429
+ // src/struct/guards.ts
430
+ const KNOWN_KINDS = /* @__PURE__ */ new Set([
431
+ "any",
432
+ "array",
433
+ "arrayBuffer",
434
+ "bigint",
435
+ "blob",
436
+ "boolean",
437
+ "date",
438
+ "discriminatedUnion",
439
+ "enum",
440
+ "file",
441
+ "intersection",
442
+ "literal",
443
+ "null",
444
+ "number",
445
+ "object",
446
+ "or",
447
+ "record",
448
+ "string",
449
+ "tuple",
450
+ "unknown"
451
+ ]);
452
+ function isStruct(value) {
453
+ if (typeof value !== "object" || value === null || !Object.hasOwn(value, DEFINITION)) return false;
454
+ const definition = value[DEFINITION];
455
+ if (typeof definition !== "object" || definition === null) return false;
456
+ if (!KNOWN_KINDS.has(definition.kind)) return false;
457
+ const flags = definition.flags;
458
+ return typeof flags === "object" && flags !== null && typeof flags.nullable === "boolean" && typeof flags.optional === "boolean";
459
+ }
460
+ // src/struct/guide.ts
461
+ const GUIDE_SET = Symbol.for("defjs.struct.guideSet");
462
+ function isGuideSet(value) {
463
+ return typeof value === "object" && value !== null && value[GUIDE_SET] === true;
464
+ }
465
+ function guideTag(members) {
466
+ const unique = [];
467
+ for (const member of members) if (!unique.includes(member)) unique.push(member);
468
+ if (unique.length === 0) return;
469
+ if (unique.length === 1) return unique[0];
470
+ return {
471
+ [GUIDE_SET]: true,
472
+ members: unique
473
+ };
474
+ }
475
+ function guideMembers(guide) {
476
+ return isGuideSet(guide) ? guide.members : [guide];
477
+ }
478
+ function combineGuides(left, right) {
479
+ if (!left) return right;
480
+ if (!right) return left;
481
+ return guideTag([...guideMembers(left), ...guideMembers(right)]);
482
+ }
483
+ function readOwnGuide(value, guides) {
484
+ const tag = guides?.get(value) ?? value[STRUCT];
485
+ if (isGuideSet(tag)) return tag;
486
+ if (!isStruct(tag)) return;
487
+ const kind = tag[DEFINITION].kind;
488
+ if (kind === "object" || kind === "record") return tag;
489
+ }
490
+ /** Tag a null-prototype node this parse just created. Enumerable so object spread keeps its guide. */
491
+ function tagOwnNode(value, tag) {
492
+ value[STRUCT] = tag;
493
+ }
494
+ /**
495
+ * Deep-merge two intersection outputs.
496
+ *
497
+ * `Object.is` keeps NaN and distinguishes signed zeroes. Equal-time Dates are one instant even
498
+ * though the instances differ. Plain objects are copied onto a null prototype so `__proto__` stays
499
+ * an own data key. Anything else, including arrays of different lengths, is a conflict.
500
+ *
501
+ * `tagNodes` folds each side's parse tag into the new object at this level. One-sided children are
502
+ * copied with the tag their own parser already wrote, so a later walk is not required. Null and
503
+ * undefined never enter this branch. `depth` is the caller's logical path length, not a fresh merge budget.
504
+ * Distinct containers consume a level even when they came from opaque leaves; identical references do not recurse.
505
+ */
506
+ function mergeIntersectionValues(left, right, tagNodes, depth, context) {
507
+ if (Object.is(left, right)) return {
508
+ ok: true,
509
+ value: left
510
+ };
511
+ if (left instanceof Date && right instanceof Date && left.getTime() === right.getTime()) return {
512
+ ok: true,
513
+ value: left
514
+ };
515
+ if (isPlainObject(left) && isPlainObject(right)) {
516
+ if (depth >= 256) return {
517
+ ok: false,
518
+ path: [],
519
+ received: right
200
520
  };
201
521
  const output = nullObject();
202
522
  for (const key of Object.keys(left)) output[key] = left[key];
@@ -452,7 +772,7 @@ function appendRecordToHeaders(headers, value) {
452
772
  /**
453
773
  * Resolves a relative definition path under the base endpoint's origin and directory into a URL.
454
774
  *
455
- * During `client.execute`, a missing, relative, or invalid base or an illegal
775
+ * During an operation, a missing, relative, or invalid base or an illegal
456
776
  * path becomes `REQ_BUILD_FAILED` before any interceptor or transport runs,
457
777
  * including a short-circuiting interceptor, for HTTP, SSE, and WebSocket alike.
458
778
  * An interceptor's final `req.endpoint` is also validated against its final
@@ -774,527 +1094,32 @@ function createPreflightFault(code, cause, response) {
774
1094
  /**
775
1095
  * Builds a `NET_*` `PreflightFault`, choosing the code from the shape of `cause`.
776
1096
  *
777
- * Optional `response` metadata is attached when the transport already had some
778
- * to report. The cause is kept as `cause`.
779
- *
780
- * - `NET_ABORTED` when {@link isAbortCause} recognizes an abort-branded `Error`
781
- * (including `ERR_ABORTED` from another Core copy) or a `DOMException` named
782
- * `AbortError`. The message is `Request was aborted`.
783
- * - `NET_TIMEOUT` when {@link isTimeoutCause} recognizes a timeout-branded
784
- * `Error` (including `ERR_TIMEOUT` from another Core copy) or a `DOMException`
785
- * or `Error` named `TimeoutError`. The message is the cause's message, or
786
- * `Request timed out` when that is empty.
787
- * - `NET_UNREACHABLE` for anything else, with the cause's message.
788
- *
789
- * @param cause - Underlying cancellation, timeout, or transport failure.
790
- * @param response - Metadata, only where the transport already had some to report.
791
- * @returns A `NET_ABORTED`, `NET_TIMEOUT`, or `NET_UNREACHABLE` fault.
792
- * @group errors
793
- */
794
- function createNetworkFault(cause, response) {
795
- if (isAbortCause(cause)) return withResponse(createFault(ERR_ABORTED.message, cause), "NET_ABORTED", response);
796
- if (isTimeoutCause(cause)) return withResponse(createFault(cause instanceof Error && cause.message ? cause.message : ERR_TIMEOUT.message, cause), "NET_TIMEOUT", response);
797
- return withResponse(createFault(describeCause(cause), cause), "NET_UNREACHABLE", response);
798
- }
799
- function withResponse(fault, code, response) {
800
- fault.code = code;
801
- if (response !== void 0) fault.response = response;
802
- return fault;
803
- }
804
- // src/internal/abort.ts
805
- const ABORT_TIMEOUT_CONFLICT_MESSAGE = "abort and timeout cannot be used together";
806
- const MAX_TIMER_DELAY_MS$2 = 2147483647;
807
- function snapshotCancellationConfig(config) {
808
- return {
809
- abort: config.abort,
810
- signal: config.signal,
811
- timeout: config.timeout
812
- };
813
- }
814
- function hasAbortTimeoutConflict(config) {
815
- return config !== void 0 && config.abort !== void 0 && config.timeout !== void 0;
816
- }
817
- /** The cancellation cause for an already-aborted signal, or `undefined` while it is still live. */
818
- function resolveAbortCause(signal) {
819
- if (!signal.aborted) return;
820
- return resolveAbortedCause(signal);
821
- }
822
- /** `ERR_TIMEOUT` or `ERR_ABORTED`, chosen from the signal's abort reason, including another copy's timeout sentinel. */
823
- function resolveAbortedCause(signal) {
824
- return isTimeoutCause(signal.reason) ? ERR_TIMEOUT : ERR_ABORTED;
825
- }
826
- /** `REQ_OPTIONS_INVALID` for execute options that pass both `abort` and `timeout`. */
827
- function createAbortTimeoutConflictFault() {
828
- return createPreflightFault("REQ_OPTIONS_INVALID", /* @__PURE__ */ new Error(ABORT_TIMEOUT_CONFLICT_MESSAGE));
829
- }
830
- /** The cancellation fault for an already-aborted signal, or `undefined` while it is still live. */
831
- function resolveAbortFault(signal) {
832
- if (!signal.aborted) return;
833
- return resolveAbortedFault(signal);
834
- }
835
- /** `NET_ABORTED` or `NET_TIMEOUT`, chosen from the signal's abort reason. */
836
- function resolveAbortedFault(signal) {
837
- return createNetworkFault(resolveAbortedCause(signal));
838
- }
839
- function validateTransportTimeout(timeout) {
840
- if (typeof timeout !== "undefined" && (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout > MAX_TIMER_DELAY_MS$2)) throw new RangeError(`Request timeout must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS$2}`);
841
- }
842
- async function awaitWithSignal(run, signal) {
843
- signal.throwIfAborted();
844
- let rejectAbort;
845
- const aborted = new Promise((_resolve, reject) => {
846
- rejectAbort = reject;
847
- });
848
- const onAbort = () => rejectAbort(signal.reason);
849
- signal.addEventListener("abort", onAbort, { once: true });
850
- const task = Promise.resolve().then(() => {
851
- signal.throwIfAborted();
852
- return run();
853
- });
854
- task.catch(() => void 0);
855
- try {
856
- const value = await Promise.race([task, aborted]);
857
- signal.throwIfAborted();
858
- return value;
859
- } finally {
860
- signal.removeEventListener("abort", onAbort);
861
- }
862
- }
863
- /**
864
- * Arm `timeout` before synchronous parse and builder work.
865
- *
866
- * A timer task cannot run until that work yields. `expiredFault` also watches
867
- * the wall clock so a microtask transport cannot succeed after the deadline.
868
- * `release` clears the timer and makes a later wall-clock check a no-op, so a
869
- * settled startup can no longer abort the request signal.
870
- */
871
- function armTransportDeadline(timeout) {
872
- if (typeof timeout !== "number") return {
873
- expiredFault: () => void 0,
874
- release: () => void 0,
875
- releaseOnAbort: () => void 0,
876
- signal: void 0
877
- };
878
- const startedAt = Date.now();
879
- const controller = new AbortController();
880
- let released = false;
881
- let watched;
882
- const timer = setTimeout(() => {
883
- controller.abort(ERR_TIMEOUT);
884
- }, timeout);
885
- const release = () => {
886
- if (released) return;
887
- released = true;
888
- clearTimeout(timer);
889
- watched?.removeEventListener("abort", release);
890
- };
891
- return {
892
- release,
893
- releaseOnAbort(signal) {
894
- watched = signal;
895
- signal.addEventListener("abort", release, { once: true });
896
- },
897
- signal: controller.signal,
898
- expiredFault() {
899
- if (released || Date.now() - startedAt < timeout) return;
900
- controller.abort(ERR_TIMEOUT);
901
- return resolveAbortedFault(controller.signal);
902
- }
903
- };
904
- }
905
- function resolveTransportCancellation(deadline, signal, fallback) {
906
- return resolveAbortFault(signal) ?? deadline.expiredFault() ?? fallback;
907
- }
908
- const forwardingSignals = /* @__PURE__ */ new WeakMap();
909
- /** Release listeners owned by a signal returned from `mergeAbortSignals`. */
910
- function releaseAbortSignal(signal) {
911
- forwardingSignals.get(signal)?.release();
912
- }
913
- function mergeAbortSignals(controller, signals, timeout) {
914
- validateTransportTimeout(timeout);
915
- const hasTimeout = typeof timeout === "number";
916
- if (!hasTimeout && signals.length > 0 && signals.every((signal) => signal === void 0 || signal === controller)) return controller;
917
- const merged = [];
918
- const seen = /* @__PURE__ */ new Set();
919
- const append = (signal) => {
920
- const state = forwardingSignals.get(signal);
921
- if (state?.active) {
922
- for (const source of state.sources) append(source);
923
- return;
924
- }
925
- if (!seen.has(signal)) {
926
- seen.add(signal);
927
- merged.push(signal);
928
- }
929
- };
930
- append(controller);
931
- for (const signal of signals) if (signal) append(signal);
932
- if (!hasTimeout && merged.length === 1) return merged[0];
933
- let timeoutTimer;
934
- const combinedController = new AbortController();
935
- const listeners = [];
936
- let abortWithReason;
937
- const state = {
938
- active: true,
939
- release() {
940
- if (!state.active) return;
941
- state.active = false;
942
- for (const [source, listener] of listeners) source.removeEventListener("abort", listener);
943
- if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
944
- },
945
- sources: merged
946
- };
947
- forwardingSignals.set(combinedController.signal, state);
948
- abortWithReason = (reason) => {
949
- if (combinedController.signal.aborted) return;
950
- combinedController.abort(reason);
951
- state.release();
952
- };
953
- if (hasTimeout) timeoutTimer = setTimeout(() => {
954
- abortWithReason(ERR_TIMEOUT);
955
- }, timeout);
956
- const alreadyAborted = merged.find((signal) => signal.aborted);
957
- if (alreadyAborted) {
958
- abortWithReason(alreadyAborted.reason);
959
- return combinedController.signal;
960
- }
961
- for (const source of merged) {
962
- const listener = () => abortWithReason(source.reason);
963
- listeners.push([source, listener]);
964
- source.addEventListener("abort", listener, { once: true });
965
- }
966
- return combinedController.signal;
967
- }
968
- // src/client/option.ts
969
- /**
970
- * Sets the valid absolute base URL prepended to relative request paths.
971
- *
972
- * 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 commands, an `http:` base becomes `ws:` and `https:` becomes `wss:`.
973
- *
974
- * 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.
975
- *
976
- * @param endpoint - Absolute base endpoint. Missing or invalid endpoints fail request building.
977
- * @returns An option that writes `config.endpoint`.
978
- * @group client
979
- */
980
- function withEndpoint(endpoint) {
981
- return (config) => {
982
- config.endpoint = endpoint;
983
- };
984
- }
985
- /**
986
- * Sets static default headers for HTTP and SSE requests built by this client.
987
- *
988
- * WebSocket connections do not use them. The `HeadersInit` is copied into a `Headers` object when the option is applied. A later call replaces the earlier headers instead of merging.
989
- *
990
- * Headers returned by a request builder replace a default of the same name. The first value for that name replaces the default; later values in the same array append only to that replacement. A scalar or a `Headers` instance also replaces. Other default names stay.
991
- *
992
- * Per-request or computed credentials, such as a refreshed Bearer token or an HMAC signature, belong in an interceptor, because they need the serialized body or a refreshed credential.
993
- *
994
- * @param headers - `HeadersInit` bag cloned onto the client config.
995
- * @returns An option that writes `config.headers`.
996
- * @group client
997
- */
998
- function withHeaders(headers) {
999
- return (config) => {
1000
- config.headers = new Headers(headers);
1001
- };
1002
- }
1003
- /**
1004
- * Sets the default timeout, in milliseconds, for HTTP `execute` only.
1005
- *
1006
- * SSE and WebSocket ignore it. `ms` must be a positive safe integer no greater than `2_147_483_647`. Anything else throws `RangeError` immediately, when `withTimeout` is called, not when the client executes.
1007
- *
1008
- * The timer starts before the interceptor chain runs. When it fires, the call returns a `NET_TIMEOUT` fault. Execute `{ timeout }` replaces this default for that call. Execute `{ abort }` combines with the default: an abort gives `NET_ABORTED`, the timer gives `NET_TIMEOUT`. Passing both `abort` and `timeout` to the same `execute` call is still rejected.
1009
- *
1010
- * @param ms - Positive safe integer timeout in milliseconds.
1011
- * @returns An option that writes `config.timeout`.
1012
- * @group client
1013
- */
1014
- function withTimeout(ms) {
1015
- validateTransportTimeout(ms);
1016
- return (config) => {
1017
- config.timeout = ms;
1018
- };
1019
- }
1020
- /**
1021
- * Appends interceptors to the client interceptor chain.
1022
- *
1023
- * 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. At execute time only the interceptors of the command's transport run, in their registration order.
1024
- *
1025
- * @param interceptors - Interceptors applied in registration order.
1026
- * @returns An option that pushes onto `config.interceptors`.
1027
- * @group client
1028
- */
1029
- function withInterceptors(...interceptors) {
1030
- return (config) => {
1031
- config.interceptors.push(...interceptors);
1032
- };
1033
- }
1034
- /**
1035
- * Replaces query encoding for HTTP, SSE, and WebSocket requests.
1036
- *
1037
- * `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.
1038
- *
1039
- * 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.
1040
- *
1041
- * @param serializer - Function that turns an encoded query object into a string.
1042
- * @returns An option that sets `config.querySerializer`.
1043
- * @group client
1044
- */
1045
- function withQuerySerializer(serializer) {
1046
- return (config) => {
1047
- config.querySerializer = serializer;
1048
- };
1049
- }
1050
- /**
1051
- * Replaces the Fetch-compatible function used for HTTP requests.
1052
- *
1053
- * The default is the platform `fetch`. `fetchImpl` is a {@link FetchHandle}, so a test double does not need host extras such as `fetch.preconnect`. It only affects HTTP. SSE uses {@link withSSEHandle}.
1054
- *
1055
- * @param fetchImpl - `(input, init?) => Promise<Response>`. Not `typeof fetch`, so
1056
- * a mock does not need host extras such as `preconnect`.
1057
- * @returns An option that sets `config.http.handle`.
1058
- * @group client
1059
- */
1060
- function withHTTPHandle(fetchImpl) {
1061
- return (config) => {
1062
- config.http.handle = fetchImpl;
1063
- };
1064
- }
1065
- /**
1066
- * Replaces the Fetch-compatible function used to open SSE event streams.
1067
- *
1068
- * The default is the platform `fetch`. `fetchImpl` is a {@link FetchHandle}. It only affects SSE. HTTP uses {@link withHTTPHandle}.
1069
- *
1070
- * @param fetchImpl - `(input, init?) => Promise<Response>`. Not `typeof fetch`, so
1071
- * a mock does not need host extras such as `preconnect`.
1072
- * @returns An option that sets `config.sse.handle`.
1073
- * @group client
1074
- */
1075
- function withSSEHandle(fetchImpl) {
1076
- return (config) => {
1077
- config.sse.handle = fetchImpl;
1078
- };
1079
- }
1080
- /**
1081
- * Replaces the WebSocket constructor used for socket connections.
1082
- *
1083
- * The default is `globalThis.WebSocket`. `WebSocketImpl` is a {@link WebSocketHandleConstructor} that returns a {@link WebSocketHandle}. If no constructor is available, executing a WebSocket command fails with an `ENV_UNSUPPORTED` fault.
1084
- *
1085
- * @param WebSocketImpl - Constructor that returns a `WebSocketHandle`.
1086
- * @returns An option that sets `config.webSocket.handle`.
1087
- * @group client
1088
- */
1089
- function withWebSocketHandle(WebSocketImpl) {
1090
- return (config) => {
1091
- config.webSocket.handle = WebSocketImpl;
1092
- };
1093
- }
1094
- /**
1095
- * Runs a hook before the initial WebSocket connect and before every reconnect.
1096
- *
1097
- * The hook type is:
1098
- *
1099
- * ```ts
1100
- * (context: { attempt: number; signal: AbortSignal }) => void | Promise<void>
1101
- * ```
1102
- *
1103
- * - `attempt` is `0` for the first connect and grows by one for each reconnect.
1104
- * - Pass `signal` into token refreshes and other async work so cancelling the session stops them.
1105
- * - If the hook throws, the attempt fails with an `EXT_HOOK_FAILED` fault.
1106
- * - A `beforeConnect` in the execute options replaces this one for that call.
1107
- *
1108
- * @param beforeConnect - Hook receiving attempt index and abort signal.
1109
- * @returns An option that sets `config.webSocket.beforeConnect`.
1110
- * @group client
1111
- */
1112
- function withWebSocketBeforeConnect(beforeConnect) {
1113
- return (config) => {
1114
- config.webSocket.beforeConnect = beforeConnect;
1115
- };
1116
- }
1117
- /**
1118
- * Sets the WebSocket subprotocols offered during the handshake.
1119
- *
1120
- * 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']`.
1121
- *
1122
- * Precedence, highest first: the `protocols` execute option, this client option, then `protocols` on the endpoint definition.
1123
- *
1124
- * @param protocols - Subprotocol list for the WebSocket constructor.
1125
- * @returns An option that sets `config.webSocket.protocols`.
1126
- * @group client
1127
- */
1128
- function withWebSocketProtocols(protocols) {
1129
- return (config) => {
1130
- config.webSocket.protocols = protocols;
1131
- };
1132
- }
1133
- /**
1134
- * Enables WebSocket heartbeat messages with optional acknowledgement detection.
1135
- *
1136
- * Passing `undefined` clears an earlier setting. The option type is:
1137
- *
1138
- * ```ts
1139
- * {
1140
- * intervalMs: number
1141
- * isAck?: (message: unknown) => boolean
1142
- * message?: <T = unknown>() => T | unknown
1143
- * timeoutMs?: number
1144
- * }
1145
- * ```
1146
- *
1147
- * - 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`.
1148
- * - 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`.
1149
- * - If no ack arrives within `timeoutMs`, or a heartbeat cannot be sent, the session fails. This does not trigger a reconnect.
1150
- * - Heartbeat uses the same JSON guard as `session.send`: a value JSON cannot represent after struct encoding causes `TypeError` and ends the logical session.
1151
- * - `intervalMs` and `timeoutMs` must be positive, finite, and at most `2_147_483_647`; otherwise execute fails with `REQ_OPTIONS_INVALID`.
1152
- * - A `heartbeat` in the execute options replaces this one for that call.
1153
- *
1154
- * @param options - Heartbeat interval, payload, timeout, and ACK predicate.
1155
- * @returns An option that sets `config.webSocket.heartbeat`.
1156
- * @group client
1157
- */
1158
- function withWebSocketHeartbeat(options) {
1159
- return (config) => {
1160
- config.webSocket.heartbeat = options;
1161
- };
1162
- }
1163
- /**
1164
- * Configures automatic reconnection when the socket closes for any reason other than a manual `session.close()`.
1165
- *
1166
- * Reconnection is opt-in: without this option a close ends the session. Passing `undefined` clears an earlier setting. The option type is:
1167
- *
1168
- * ```ts
1169
- * {
1170
- * attempts?: number
1171
- * delayMs?: number
1172
- * factor?: number
1173
- * jitter?: number
1174
- * maxDelayMs?: number
1175
- * shouldReconnect?: (context: { attempt: number; cause?: unknown; code?: number; reason?: string; wasClean?: boolean }) => boolean
1176
- * }
1177
- * ```
1178
- *
1179
- * - `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.
1180
- * - `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.
1181
- * - `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()`.
1182
- * - 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 execute 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.
1183
- * - A `reconnect` in the execute options replaces this one for that call.
1184
- *
1185
- * @param options - Retry budget, backoff, and `shouldReconnect` gate.
1186
- * @returns An option that sets `config.webSocket.reconnect`.
1187
- * @group client
1188
- */
1189
- function withWebSocketReconnect(options) {
1190
- return (config) => {
1191
- config.webSocket.reconnect = options;
1192
- };
1193
- }
1194
- /**
1195
- * Observes SSE events that have no matching event struct or fail validation.
1196
- *
1197
- * The handler may be async and is awaited. The callback receives:
1198
- *
1199
- * ```ts
1200
- * {
1201
- * reason: 'missing-struct' | 'validation-failed'
1202
- * message: { id: string; event: string; data: string }
1203
- * cause?: unknown
1204
- * signal: AbortSignal
1205
- * }
1206
- * ```
1207
- *
1208
- * - The invalid event is **always dropped**; it is never delivered to the stream. The callback only observes it.
1209
- * - The return value is ignored. An error thrown by the callback is swallowed and does not close the stream, unless `signal` has already aborted.
1210
- * - `cause` is set for `validation-failed`. Without a handler the invalid event is dropped silently.
1211
- *
1212
- * @param handler - Observer callback for invalid events.
1213
- * @returns An option that sets `config.sse.onInvalidEvent`.
1214
- * @group client
1215
- */
1216
- function withSSEOnInvalidEvent(handler) {
1217
- return (config) => {
1218
- config.sse.onInvalidEvent = handler;
1219
- };
1220
- }
1221
- /**
1222
- * Configures automatic reconnection after an SSE stream fails.
1223
- *
1224
- * Reconnection is opt-in: without this option a failed stream is not retried. Passing `undefined` clears an earlier setting.
1225
- *
1226
- * ```ts
1227
- * type SSEReconnect = {
1228
- * attempts?: number;
1229
- * delayMs?: number;
1230
- * factor?: number;
1231
- * jitter?: number;
1232
- * maxDelayMs?: number;
1233
- * shouldReconnect?: (context: {
1234
- * attempt: number;
1235
- * cause?: unknown;
1236
- * lastEventId: string;
1237
- * method: string;
1238
- * open?: {
1239
- * response: { status: number; statusText: string; url: string };
1240
- * url: string;
1241
- * };
1242
- * }) => boolean | Promise<boolean>;
1243
- * };
1244
- * ```
1245
- *
1246
- * - `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.
1247
- * - 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.
1248
- * - A request with a `ReadableStream` body never reconnects, and `shouldReconnect` is not called for it.
1249
- * - `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.
1250
- * - `delayMs` is only the starting base delay (`1000` by default). A server `retry:` field (digits only, in milliseconds) replaces it for later attempts.
1251
- * - 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.
1252
- * - 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` at execute time.
1253
- *
1254
- * @param options - Retry budget, backoff, and `shouldReconnect` gate.
1255
- * @returns An option that sets `config.sse.reconnect`.
1256
- * @group client
1257
- */
1258
- function withSSEReconnect(options) {
1259
- return (config) => {
1260
- config.sse.reconnect = options;
1261
- };
1262
- }
1263
- /**
1264
- * Enables XSRF cookie-to-header token forwarding for HTTP requests.
1265
- *
1266
- * SSE and WebSocket requests do not get the header. `cookieName` defaults to `XSRF-TOKEN` and `headerName` to `X-XSRF-TOKEN` when names are omitted.
1267
- *
1268
- * - 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.
1269
- * - 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.
1270
- * - 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.
1271
- *
1272
- * @param options - Cookie/header names and optional token provider.
1273
- * @returns An option that sets `config.xsrf`.
1274
- * @group client
1275
- */
1276
- function withXSRF(options = {}) {
1277
- return (config) => {
1278
- config.xsrf = {
1279
- cookieName: options.cookieName ?? "XSRF-TOKEN",
1280
- headerName: options.headerName ?? "X-XSRF-TOKEN",
1281
- tokenProvider: options.tokenProvider
1282
- };
1283
- };
1284
- }
1285
- /**
1286
- * Sets whether HTTP and SSE requests include credentials.
1287
- *
1288
- * 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.
1097
+ * Optional `response` metadata is attached when the transport already had some
1098
+ * to report. The cause is kept as `cause`.
1289
1099
  *
1290
- * @param value - When `true`, credentials are sent with cross-origin requests.
1291
- * @returns An option that sets `config.withCredentials`.
1292
- * @group client
1100
+ * - `NET_ABORTED` when {@link isAbortCause} recognizes an abort-branded `Error`
1101
+ * (including `ERR_ABORTED` from another Core copy) or a `DOMException` named
1102
+ * `AbortError`. The message is `Request was aborted`.
1103
+ * - `NET_TIMEOUT` when {@link isTimeoutCause} recognizes a timeout-branded
1104
+ * `Error` (including `ERR_TIMEOUT` from another Core copy) or a `DOMException`
1105
+ * or `Error` named `TimeoutError`. The message is the cause's message, or
1106
+ * `Request timed out` when that is empty.
1107
+ * - `NET_UNREACHABLE` for anything else, with the cause's message.
1108
+ *
1109
+ * @param cause - Underlying cancellation, timeout, or transport failure.
1110
+ * @param response - Metadata, only where the transport already had some to report.
1111
+ * @returns A `NET_ABORTED`, `NET_TIMEOUT`, or `NET_UNREACHABLE` fault.
1112
+ * @group errors
1293
1113
  */
1294
- function withCredentials(value) {
1295
- return (config) => {
1296
- config.withCredentials = value;
1297
- };
1114
+ function createNetworkFault(cause, response) {
1115
+ if (isAbortCause(cause)) return withResponse(createFault(ERR_ABORTED.message, cause), "NET_ABORTED", response);
1116
+ if (isTimeoutCause(cause)) return withResponse(createFault(cause instanceof Error && cause.message ? cause.message : ERR_TIMEOUT.message, cause), "NET_TIMEOUT", response);
1117
+ return withResponse(createFault(describeCause(cause), cause), "NET_UNREACHABLE", response);
1118
+ }
1119
+ function withResponse(fault, code, response) {
1120
+ fault.code = code;
1121
+ if (response !== void 0) fault.response = response;
1122
+ return fault;
1298
1123
  }
1299
1124
  // src/interceptor/interceptor.ts
1300
1125
  /**
@@ -1303,10 +1128,10 @@ function withCredentials(value) {
1303
1128
  * Returns `{ kind: 'http', fn }`. Call `next(req)` (optionally with a modified
1304
1129
  * request) or short-circuit with `makeResponse`. `next` runs only the
1305
1130
  * interceptors registered after this one and then the fetch handler. HTTP
1306
- * interceptors do not run for SSE or WebSocket commands.
1131
+ * interceptors do not run for SSE or WebSocket operations.
1307
1132
  *
1308
1133
  * A deterministic URL error from `next(req)` rejects the returned promise, so
1309
- * `.then` or `.catch` recovery runs; if it is not recovered, `client.execute`
1134
+ * `.then` or `.catch` recovery runs; if it is not recovered, the operation
1310
1135
  * still reports `REQ_BUILD_FAILED`.
1311
1136
  *
1312
1137
  * @param fn - HTTP interceptor function.
@@ -1322,12 +1147,12 @@ function createHttpInterceptor(fn) {
1322
1147
  /**
1323
1148
  * Wraps an `SSEInterceptorFn` as a tagged `SSEInterceptor`.
1324
1149
  *
1325
- * Returns `{ kind: 'sse', fn }`. SSE interceptors run only for SSE commands, in
1150
+ * Returns `{ kind: 'sse', fn }`. SSE interceptors run only for SSE operations, in
1326
1151
  * registration order; `next(req)` opens the stream through the remaining
1327
1152
  * interceptors.
1328
1153
  *
1329
1154
  * A deterministic URL error from `next(req)` rejects the returned promise, so
1330
- * `.then` or `.catch` recovery runs; if it is not recovered, `client.execute`
1155
+ * `.then` or `.catch` recovery runs; if it is not recovered, the operation
1331
1156
  * still reports `REQ_BUILD_FAILED`.
1332
1157
  *
1333
1158
  * `EventStreamHandle` extends `AsyncDisposable`. A structural short-circuit
@@ -1348,7 +1173,7 @@ function createSSEInterceptor(fn) {
1348
1173
  * Wraps a `WebSocketInterceptorFn` as a tagged `WebSocketInterceptor`.
1349
1174
  *
1350
1175
  * Returns `{ kind: 'web-socket', fn }`. WebSocket interceptors run only for
1351
- * WebSocket commands. `next` and the interceptor return a
1176
+ * WebSocket operations. `next` and the interceptor return a
1352
1177
  * {@link WebSocketSessionLike}, the interceptor-facing session. A wrapper or
1353
1178
  * short-circuit session must forward its `[Symbol.asyncDispose]()` teardown.
1354
1179
  *
@@ -1377,6 +1202,99 @@ function resolveSSEInterceptors(interceptors) {
1377
1202
  function resolveWebSocketInterceptors(interceptors) {
1378
1203
  return interceptors.filter((i) => i.kind === "web-socket").map((i) => i.fn);
1379
1204
  }
1205
+ // src/internal/abort.ts
1206
+ /** The cancellation cause for an already-aborted signal, or `undefined` while it is still live. */
1207
+ function resolveAbortCause(signal) {
1208
+ if (!signal.aborted) return;
1209
+ return resolveAbortedCause(signal);
1210
+ }
1211
+ /** `ERR_TIMEOUT` or `ERR_ABORTED`, chosen from the signal's abort reason, including another copy's timeout sentinel. */
1212
+ function resolveAbortedCause(signal) {
1213
+ return isTimeoutCause(signal.reason) ? ERR_TIMEOUT : ERR_ABORTED;
1214
+ }
1215
+ /** The cancellation fault for an already-aborted signal, or `undefined` while it is still live. */
1216
+ function resolveAbortFault(signal) {
1217
+ if (!signal.aborted) return;
1218
+ return resolveAbortedFault(signal);
1219
+ }
1220
+ /** `NET_ABORTED` or `NET_TIMEOUT`, chosen from the signal's abort reason. */
1221
+ function resolveAbortedFault(signal) {
1222
+ return createNetworkFault(resolveAbortedCause(signal));
1223
+ }
1224
+ async function awaitWithSignal(run, signal) {
1225
+ signal.throwIfAborted();
1226
+ let rejectAbort;
1227
+ const aborted = new Promise((_resolve, reject) => {
1228
+ rejectAbort = reject;
1229
+ });
1230
+ const onAbort = () => rejectAbort(signal.reason);
1231
+ signal.addEventListener("abort", onAbort, { once: true });
1232
+ const task = Promise.resolve().then(() => {
1233
+ signal.throwIfAborted();
1234
+ return run();
1235
+ });
1236
+ task.catch(() => void 0);
1237
+ try {
1238
+ const value = await Promise.race([task, aborted]);
1239
+ signal.throwIfAborted();
1240
+ return value;
1241
+ } finally {
1242
+ signal.removeEventListener("abort", onAbort);
1243
+ }
1244
+ }
1245
+ const forwardingSignals = /* @__PURE__ */ new WeakMap();
1246
+ /** Release listeners owned by a signal returned from `mergeAbortSignals`. */
1247
+ function releaseAbortSignal(signal) {
1248
+ forwardingSignals.get(signal)?.release();
1249
+ }
1250
+ function mergeAbortSignals(controller, signals) {
1251
+ if (signals.length > 0 && signals.every((signal) => signal === void 0 || signal === controller)) return controller;
1252
+ const merged = [];
1253
+ const seen = /* @__PURE__ */ new Set();
1254
+ const append = (signal) => {
1255
+ const state = forwardingSignals.get(signal);
1256
+ if (state?.active) {
1257
+ for (const source of state.sources) append(source);
1258
+ return;
1259
+ }
1260
+ if (!seen.has(signal)) {
1261
+ seen.add(signal);
1262
+ merged.push(signal);
1263
+ }
1264
+ };
1265
+ append(controller);
1266
+ for (const signal of signals) if (signal) append(signal);
1267
+ if (merged.length === 1) return merged[0];
1268
+ const combinedController = new AbortController();
1269
+ const listeners = [];
1270
+ let abortWithReason;
1271
+ const state = {
1272
+ active: true,
1273
+ release() {
1274
+ if (!state.active) return;
1275
+ state.active = false;
1276
+ for (const [source, listener] of listeners) source.removeEventListener("abort", listener);
1277
+ },
1278
+ sources: merged
1279
+ };
1280
+ forwardingSignals.set(combinedController.signal, state);
1281
+ abortWithReason = (reason) => {
1282
+ if (combinedController.signal.aborted) return;
1283
+ combinedController.abort(reason);
1284
+ state.release();
1285
+ };
1286
+ const alreadyAborted = merged.find((signal) => signal.aborted);
1287
+ if (alreadyAborted) {
1288
+ abortWithReason(alreadyAborted.reason);
1289
+ return combinedController.signal;
1290
+ }
1291
+ for (const source of merged) {
1292
+ const listener = () => abortWithReason(source.reason);
1293
+ listeners.push([source, listener]);
1294
+ source.addEventListener("abort", listener, { once: true });
1295
+ }
1296
+ return combinedController.signal;
1297
+ }
1380
1298
  // src/internal/bytes.ts
1381
1299
  const textDecoder = new TextDecoder();
1382
1300
  function decodeBytes(value) {
@@ -1774,7 +1692,9 @@ var StructError = class extends Error {
1774
1692
  }
1775
1693
  };
1776
1694
  function createFormattedError() {
1777
- return Object.assign(Object.create(null), { _errors: [] });
1695
+ const node = Object.create(null);
1696
+ node._errors = [];
1697
+ return node;
1778
1698
  }
1779
1699
  function formatErrorTreeKey(segment) {
1780
1700
  const key = String(segment);
@@ -2706,7 +2626,6 @@ function createBaseTransportRequest(method, path, input, builder, options) {
2706
2626
  operation: options.operation,
2707
2627
  queryParams,
2708
2628
  queryString,
2709
- timeout: options.timeout,
2710
2629
  withCredentials: options.withCredentials ?? false
2711
2630
  };
2712
2631
  resolveRequestUrl(request);
@@ -2725,7 +2644,6 @@ function createHttpRequest(method, path, input, builder, options) {
2725
2644
  inputGuides: options.inputGuides,
2726
2645
  operation: options.operation,
2727
2646
  querySerializer: options.querySerializer,
2728
- timeout: options.timeout,
2729
2647
  transport: "http",
2730
2648
  withCredentials: options.withCredentials
2731
2649
  }),
@@ -2743,7 +2661,7 @@ function createHttpRequest(method, path, input, builder, options) {
2743
2661
  *
2744
2662
  * An upload observer can fail before any response exists, so `meta` is absent then. Download
2745
2663
  * observers, truncated reads, and representation checks already have a response. Those steps
2746
- * are indistinguishable once they collapse into a bare cause, so command execution maps
2664
+ * are indistinguishable once they collapse into a bare cause, so operation execution maps
2747
2665
  * `code` straight through.
2748
2666
  *
2749
2667
  * Caller cancellation is deliberately never tagged: it stays the raw abort reason so the
@@ -3272,8 +3190,7 @@ function convertBodyToUploadStream(body) {
3272
3190
  * Declares a typed HTTP request endpoint.
3273
3191
  *
3274
3192
  * Pass method, path, an optional input struct, and the single `output` and
3275
- * `error` structs. Call the returned endpoint with input to get an `HttpCommand`
3276
- * for `client.execute`.
3193
+ * `error` structs. Call the returned operation with a client and, when declared, the input.
3277
3194
  *
3278
3195
  * The contract is `method`, `path`, an optional `input` struct, optional
3279
3196
  * `output` and `error` structs, and an optional `operation`. A declared `input`
@@ -3289,7 +3206,7 @@ function convertBodyToUploadStream(body) {
3289
3206
  * omitting one means that body is never read.
3290
3207
  *
3291
3208
  * @param definition - Request contract (method, path, input, builder, output).
3292
- * @returns An endpoint that creates `HttpCommand` values from input.
3209
+ * @returns An operation `(client, ...input) => Promise<error-first tuple>`.
3293
3210
  *
3294
3211
  * The checked example file passes the whole input object as `path`. This snippet maps individual fields into `path` and `query` instead:
3295
3212
  *
@@ -3308,43 +3225,10 @@ function convertBodyToUploadStream(body) {
3308
3225
  */
3309
3226
  function defineRequest(definition) {
3310
3227
  const endpoint = { ...definition };
3311
- function create(input) {
3312
- return {
3313
- [COMMAND_EXECUTE]: executeHttpCommand,
3314
- [COMMAND_TYPE]: HTTP_COMMAND,
3315
- definition: endpoint,
3316
- input
3317
- };
3318
- }
3319
- return ((input) => create(input));
3228
+ return (async (client, input) => executeHttpCommand(client[CLIENT].config, endpoint, input, client[CLIENT].options));
3320
3229
  }
3321
- /**
3322
- * Runs an `HttpCommand` against client config: validate input, send the request, and parse the response.
3323
- *
3324
- * Low-level entry used by `client.execute`. Prefer `client.execute(command, options)`
3325
- * in application code. `HttpClientConfig` is the HTTP part of `ClientConfig`, so
3326
- * a `ClientConfig` is accepted. `HttpExecuteAwaitResult` is the internal result
3327
- * tuple.
3328
- *
3329
- * The result is `[null, data, response]` or `[fault, undefined, undefined]`.
3330
- * `data` is the decoded `output` body, or `undefined` when none is declared.
3331
- * `response` is `DecodedResponse` when `output` is declared and `HttpMeta`
3332
- * otherwise. The fault is `Fault<TError>`. A non-2xx status is `HTTP_STATUS`
3333
- * when no `error` struct is declared (its `data` is `undefined`) or when the
3334
- * declared body decodes (its `data` is that body). When a declared `error` body
3335
- * cannot be read, the fault is a `RES_*` decode fault instead. On failure the
3336
- * tuple's third slot is `undefined`; the fault carries the response metadata.
3337
- * `options.timeout` falls back to the client's `withTimeout` default when omitted.
3338
- *
3339
- * @param clientConfig - Resolved HTTP client configuration (endpoint, interceptors, handler).
3340
- * @param command - Command from a `defineRequest` endpoint.
3341
- * @param options - Per-request progress and cancellation options.
3342
- * @returns An await-result tuple of success body or typed request error.
3343
- * @group http
3344
- */
3345
- async function executeHttpCommand(clientConfig, command, options) {
3346
- const { definition, input } = command;
3347
- const config = options ?? {};
3230
+ async function executeHttpCommand(clientConfig, definition, input, options) {
3231
+ const config = options;
3348
3232
  const fail = (fault) => {
3349
3233
  return [
3350
3234
  fault,
@@ -3352,34 +3236,10 @@ async function executeHttpCommand(clientConfig, command, options) {
3352
3236
  void 0
3353
3237
  ];
3354
3238
  };
3355
- let cancellation;
3356
- try {
3357
- cancellation = snapshotCancellationConfig({
3358
- abort: config.abort,
3359
- signal: config.signal,
3360
- timeout: config.timeout !== void 0 ? config.timeout : clientConfig.timeout
3361
- });
3362
- } catch (error) {
3363
- return fail(createPreflightFault("REQ_OPTIONS_INVALID", error));
3364
- }
3365
- if (hasAbortTimeoutConflict(config)) return fail(createAbortTimeoutConflictFault());
3366
- try {
3367
- validateTransportTimeout(cancellation.timeout);
3368
- } catch (error) {
3369
- return fail(createPreflightFault("REQ_OPTIONS_INVALID", error));
3370
- }
3371
- const controller = new AbortController();
3372
- const preAbortedSignal = [cancellation.abort, cancellation.signal].find((signal) => signal?.aborted);
3373
- if (preAbortedSignal) return fail(resolveAbortedFault(preAbortedSignal));
3374
- const deadline = armTransportDeadline(cancellation.timeout);
3375
- const requestSignal = cancellation.abort === void 0 && cancellation.signal === void 0 && deadline.signal ? deadline.signal : mergeAbortSignals(controller.signal, [
3376
- cancellation.abort,
3377
- cancellation.signal,
3378
- deadline.signal
3379
- ]);
3380
- deadline.releaseOnAbort(requestSignal);
3239
+ const callerAbort = config.abort;
3240
+ if (callerAbort?.aborted) return fail(resolveAbortedFault(callerAbort));
3241
+ const requestSignal = mergeAbortSignals(new AbortController().signal, [callerAbort]);
3381
3242
  const stopBeforeTransport = (fault) => {
3382
- deadline.release();
3383
3243
  releaseAbortSignal(requestSignal);
3384
3244
  return fail(fault);
3385
3245
  };
@@ -3390,9 +3250,9 @@ async function executeHttpCommand(clientConfig, command, options) {
3390
3250
  try {
3391
3251
  parsedInput = parseEndpointInput(definition.input, input, inputGuides);
3392
3252
  } catch (error) {
3393
- return stopBeforeTransport(resolveTransportCancellation(deadline, requestSignal, createPreflightFault("REQ_INPUT_INVALID", error)));
3253
+ return stopBeforeTransport(resolveAbortFault(requestSignal) ?? createPreflightFault("REQ_INPUT_INVALID", error));
3394
3254
  }
3395
- const parsedCancellation = resolveTransportCancellation(deadline, requestSignal);
3255
+ const parsedCancellation = resolveAbortFault(requestSignal);
3396
3256
  if (parsedCancellation) return stopBeforeTransport(parsedCancellation);
3397
3257
  let request;
3398
3258
  try {
@@ -3407,17 +3267,15 @@ async function executeHttpCommand(clientConfig, command, options) {
3407
3267
  operation: definition.operation,
3408
3268
  querySerializer: clientConfig.querySerializer,
3409
3269
  responseType,
3410
- timeout: cancellation.timeout,
3411
3270
  uploadProgress: config.onUploadProgress,
3412
3271
  withCredentials: clientConfig.withCredentials,
3413
3272
  xsrf: clientConfig.xsrf
3414
3273
  });
3415
3274
  } catch (error) {
3416
- return stopBeforeTransport(resolveTransportCancellation(deadline, requestSignal, createPreflightFault("REQ_BUILD_FAILED", error)));
3275
+ return stopBeforeTransport(resolveAbortFault(requestSignal) ?? createPreflightFault("REQ_BUILD_FAILED", error));
3417
3276
  }
3418
- const builtCancellation = resolveTransportCancellation(deadline, requestSignal);
3277
+ const builtCancellation = resolveAbortFault(requestSignal);
3419
3278
  if (builtCancellation) return stopBeforeTransport(builtCancellation);
3420
- const releaseRequestTimeout = deadline.release;
3421
3279
  const transportOrigin = createTransportOrigin();
3422
3280
  const transportController = new AbortController();
3423
3281
  let chainSettled = false;
@@ -3437,7 +3295,7 @@ async function executeHttpCommand(clientConfig, command, options) {
3437
3295
  return rejected;
3438
3296
  }
3439
3297
  const transportAbort = nextRequest.abort === void 0 || nextRequest.abort === requestSignal ? transportController.signal : mergeAbortSignals(transportController.signal, [nextRequest.abort]);
3440
- if (resolveTransportCancellation(deadline, transportAbort)) {
3298
+ if (resolveAbortFault(transportAbort)) {
3441
3299
  const rejected = Promise.reject(transportOrigin.mark(resolveAbortCause(transportAbort)));
3442
3300
  releaseAbortSignal(transportAbort);
3443
3301
  rejected.catch(() => void 0);
@@ -3469,7 +3327,7 @@ async function executeHttpCommand(clientConfig, command, options) {
3469
3327
  response = settled;
3470
3328
  } catch (error) {
3471
3329
  const fromTransport = transportOrigin.has(error);
3472
- const cancelled = resolveTransportCancellation(deadline, requestSignal);
3330
+ const cancelled = resolveAbortFault(requestSignal);
3473
3331
  if (cancelled) {
3474
3332
  abortTransport();
3475
3333
  if (inflightTransport) await Promise.allSettled([inflightTransport]);
@@ -3486,19 +3344,13 @@ async function executeHttpCommand(clientConfig, command, options) {
3486
3344
  requestSignal.removeEventListener("abort", abortTransport);
3487
3345
  chainSettled = true;
3488
3346
  if (!transportController.signal.aborted) transportController.abort(/* @__PURE__ */ new Error("HTTP interceptor chain settled"));
3489
- if (!response) {
3490
- releaseRequestTimeout();
3491
- releaseAbortSignal(requestSignal);
3492
- }
3347
+ if (!response) releaseAbortSignal(requestSignal);
3493
3348
  }
3494
3349
  const settledResponse = response;
3495
3350
  const meta = toHttpMeta(settledResponse);
3496
3351
  const struct = settledResponse.ok ? definition.output : definition.error;
3497
3352
  if (!struct) {
3498
- const cancelled = resolveTransportCancellation(deadline, requestSignal);
3499
- releaseRequestTimeout();
3500
3353
  releaseAbortSignal(requestSignal);
3501
- if (cancelled) return fail(cancelled);
3502
3354
  if (settledResponse.ok) return [
3503
3355
  null,
3504
3356
  void 0,
@@ -3509,16 +3361,13 @@ async function executeHttpCommand(clientConfig, command, options) {
3509
3361
  let decoded;
3510
3362
  try {
3511
3363
  decoded = await decodeDeclaredBody(struct, settledResponse, requestSignal);
3512
- const cancelled = resolveTransportCancellation(deadline, requestSignal);
3513
- if (cancelled) return fail(cancelled);
3514
3364
  } catch (error) {
3515
- const cancelled = resolveTransportCancellation(deadline, requestSignal);
3365
+ const cancelled = resolveAbortFault(requestSignal);
3516
3366
  if (cancelled) return fail(cancelled);
3517
3367
  if (error instanceof JsonMediaTypeError) return fail(createDecodeFault("RES_MEDIA_TYPE_INVALID", error, meta));
3518
3368
  if (error instanceof StructError) return fail(createDecodeFault("RES_STRUCT_MISMATCH", error, meta));
3519
3369
  return fail(createDecodeFault("RES_DECODE_FAILED", error, meta));
3520
3370
  } finally {
3521
- releaseRequestTimeout();
3522
3371
  releaseAbortSignal(requestSignal);
3523
3372
  }
3524
3373
  const decodedResponse = {
@@ -3608,7 +3457,7 @@ function createBasicAuthModifier(fn, options) {
3608
3457
  * `encode`, calling `basicAuthHttpInterceptor` throws
3609
3458
  * `Error: BasicAuthInterceptor is not supported in this environment` when `btoa`
3610
3459
  * is not available in the runtime. Use {@link basicAuthSSEInterceptor} for SSE
3611
- * commands.
3460
+ * operations.
3612
3461
  *
3613
3462
  * @param fn - Factory returning username/password for each request.
3614
3463
  * @param options - Optional custom encoder (defaults to `btoa`).
@@ -4298,15 +4147,15 @@ function toCloseReason(reason) {
4298
4147
  /**
4299
4148
  * Declare a typed Server-Sent Events endpoint.
4300
4149
  *
4301
- * Pass path, event structs, and buffer/queue limits. Call the returned endpoint
4302
- * with input to get an `EventStreamCommand` for `client.execute`.
4150
+ * Pass path, event structs, and buffer/queue limits. Call the returned operation
4151
+ * with a client and, when declared, the input.
4303
4152
  *
4304
4153
  * - **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`.
4305
4154
  *
4306
4155
  * `EventStreamDefinitionBase`, `EventStreamErrorField`, and `BuilderField` are internal helper types: `error` is required only when `TError` is a struct, and `builder` is `(input: BuilderInput<TInput>) => EventStreamRequestShape` when `input` is declared.
4307
4156
  *
4308
4157
  * @param definition - SSE contract (path, events, buffer limits, optional input).
4309
- * @returns An endpoint that creates `EventStreamCommand` values from input.
4158
+ * @returns An operation `(client, ...input) => Promise<error-first tuple>`.
4310
4159
  *
4311
4160
  * @group sse
4312
4161
  * @exampleFile examples/api/define-event-stream.ts#example
@@ -4316,61 +4165,12 @@ function defineEventStream(definition) {
4316
4165
  ...definition,
4317
4166
  method: definition.method ?? "GET"
4318
4167
  };
4319
- function create(input) {
4320
- return {
4321
- [COMMAND_EXECUTE]: executeEventStreamCommand,
4322
- [COMMAND_TYPE]: EVENT_STREAM_COMMAND,
4323
- endpoint,
4324
- input
4325
- };
4326
- }
4327
- return ((input) => create(input));
4328
- }
4329
- /**
4330
- * Open an SSE command against the given client config.
4331
- *
4332
- * Prefer `client.execute(command)` in application code; this is the low-level entry used by the client.
4333
- *
4334
- * The result is `[null, stream, open]` or `[fault, undefined, open | undefined]`. The fault is `Fault<TError>`: the decoded handshake body is on `data` when `error` is declared, and `Fault<undefined>` otherwise.
4335
- *
4336
- * @param clientConfig - Resolved client configuration.
4337
- * @param command - SSE command from an `EventStreamEndpoint`.
4338
- * @param options - Optional execute options (abort, timeout).
4339
- * @returns Await-result tuple of `[error, stream, open]`.
4340
- * @group sse
4341
- */
4342
- async function executeEventStreamCommand(clientConfig, command, options) {
4343
- const { endpoint, input } = command;
4344
- return runEventStreamCommand(clientConfig, endpoint, input, options ?? {}, new AbortController());
4168
+ return (async (client, input) => runEventStreamCommand(client[CLIENT].config, endpoint, input, client[CLIENT].options, new AbortController()));
4345
4169
  }
4346
4170
  async function runEventStreamCommand(clientConfig, endpoint, input, config, controller) {
4347
- let cancellation;
4348
- try {
4349
- cancellation = snapshotCancellationConfig(config);
4350
- } catch (error) {
4351
- return [
4352
- createPreflightFault("REQ_OPTIONS_INVALID", error),
4353
- void 0,
4354
- void 0
4355
- ];
4356
- }
4357
- if (hasAbortTimeoutConflict(cancellation)) return [
4358
- createAbortTimeoutConflictFault(),
4359
- void 0,
4360
- void 0
4361
- ];
4362
- try {
4363
- validateTransportTimeout(cancellation.timeout);
4364
- } catch (error) {
4365
- return [
4366
- createPreflightFault("REQ_OPTIONS_INVALID", error),
4367
- void 0,
4368
- void 0
4369
- ];
4370
- }
4371
- const preAbortedSignal = [cancellation.abort, cancellation.signal].find((signal) => signal?.aborted);
4372
- if (preAbortedSignal) return [
4373
- resolveAbortedFault(preAbortedSignal),
4171
+ const callerAbort = config.abort;
4172
+ if (callerAbort?.aborted) return [
4173
+ resolveAbortedFault(callerAbort),
4374
4174
  void 0,
4375
4175
  void 0
4376
4176
  ];
@@ -4390,15 +4190,8 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4390
4190
  void 0
4391
4191
  ];
4392
4192
  }
4393
- const deadline = armTransportDeadline(cancellation.timeout);
4394
- const requestSignal = mergeAbortSignals(controller.signal, [
4395
- cancellation.abort,
4396
- cancellation.signal,
4397
- deadline.signal
4398
- ]);
4399
- deadline.releaseOnAbort(requestSignal);
4193
+ const requestSignal = mergeAbortSignals(controller.signal, [callerAbort]);
4400
4194
  const stopBeforeTransport = (fault) => {
4401
- deadline.release();
4402
4195
  releaseAbortSignal(requestSignal);
4403
4196
  return [
4404
4197
  fault,
@@ -4411,9 +4204,9 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4411
4204
  try {
4412
4205
  parsedInput = parseEndpointInput(endpoint.input, input, inputGuides);
4413
4206
  } catch (error) {
4414
- return stopBeforeTransport(resolveTransportCancellation(deadline, requestSignal, createPreflightFault("REQ_INPUT_INVALID", error)));
4207
+ return stopBeforeTransport(resolveAbortFault(requestSignal) ?? createPreflightFault("REQ_INPUT_INVALID", error));
4415
4208
  }
4416
- const parsedCancellation = resolveTransportCancellation(deadline, requestSignal);
4209
+ const parsedCancellation = resolveAbortFault(requestSignal);
4417
4210
  if (parsedCancellation) return stopBeforeTransport(parsedCancellation);
4418
4211
  let request;
4419
4212
  try {
@@ -4425,14 +4218,13 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4425
4218
  inputGuides,
4426
4219
  operation: endpoint.operation,
4427
4220
  querySerializer: clientConfig.querySerializer,
4428
- timeout: cancellation.timeout,
4429
4221
  transport: "sse",
4430
4222
  withCredentials: clientConfig.withCredentials
4431
4223
  });
4432
4224
  } catch (error) {
4433
- return stopBeforeTransport(resolveTransportCancellation(deadline, requestSignal, createPreflightFault("REQ_BUILD_FAILED", error)));
4225
+ return stopBeforeTransport(resolveAbortFault(requestSignal) ?? createPreflightFault("REQ_BUILD_FAILED", error));
4434
4226
  }
4435
- const builtCancellation = resolveTransportCancellation(deadline, requestSignal);
4227
+ const builtCancellation = resolveAbortFault(requestSignal);
4436
4228
  if (builtCancellation) return stopBeforeTransport(builtCancellation);
4437
4229
  let chainSettled = false;
4438
4230
  let streamDelivered = false;
@@ -4455,7 +4247,7 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4455
4247
  return rejected;
4456
4248
  }
4457
4249
  const transportAbort = mergeAbortSignals(requestSignal, [req.abort]);
4458
- if (resolveTransportCancellation(deadline, transportAbort)) {
4250
+ if (resolveAbortFault(transportAbort)) {
4459
4251
  const rejected = Promise.reject(transportOrigin.mark(resolveAbortCause(transportAbort)));
4460
4252
  releaseAbortSignal(transportAbort);
4461
4253
  rejected.catch(() => void 0);
@@ -4536,7 +4328,6 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4536
4328
  openInfo
4537
4329
  ];
4538
4330
  } finally {
4539
- deadline.release();
4540
4331
  if (!streamDelivered) releaseAbortSignal(requestSignal);
4541
4332
  }
4542
4333
  function discardOwnedStreams(reason, delivered) {
@@ -5467,70 +5258,97 @@ function shouldReconnect(config, outcome, attempt) {
5467
5258
  * Declare a typed WebSocket endpoint.
5468
5259
  *
5469
5260
  * Pass path, incoming (and optional outgoing) message structs, and queue limits.
5470
- * Call the returned endpoint with input to get a `WebSocketCommand` for `client.execute`.
5261
+ * Call the returned operation with a client and, when declared, the input.
5471
5262
  *
5472
5263
  * - **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`.
5473
5264
  *
5474
5265
  * A WebSocket `builder` may return `path` and `query` only; `headers` and `body` are rejected. `WebSocketDefinitionBase` and `BuilderField` are internal helper types.
5475
5266
  *
5476
5267
  * @param definition - WebSocket contract (path, structs, queue limits, optional input).
5477
- * @returns An endpoint that creates `WebSocketCommand` values from input.
5268
+ * @returns An operation `(client, ...input) => Promise<error-first tuple>`.
5478
5269
  *
5479
5270
  * @group web-socket
5480
5271
  * @exampleFile examples/api/define-web-socket.ts#example
5481
5272
  */
5482
5273
  function defineWebSocket(definition) {
5483
5274
  const endpoint = { ...definition };
5484
- function create(input) {
5485
- return {
5486
- [COMMAND_EXECUTE]: executeWebSocketCommand,
5487
- [COMMAND_TYPE]: WEB_SOCKET_COMMAND,
5488
- endpoint,
5489
- input
5275
+ return (async (client, input) => {
5276
+ const requestOptions = client[CLIENT].options;
5277
+ const controller = new AbortController();
5278
+ const socketState = {
5279
+ listeners: {
5280
+ runtimeError: /* @__PURE__ */ new Set(),
5281
+ stateChange: /* @__PURE__ */ new Set()
5282
+ },
5283
+ status: "idle",
5284
+ transition: 0
5490
5285
  };
5286
+ return runWebSocketCommand(client[CLIENT].config, endpoint, input, requestOptions, controller, socketState);
5287
+ });
5288
+ }
5289
+ function readQueueLimits(maxIncomingQueueSize, maxOutgoingQueueSize) {
5290
+ try {
5291
+ validateWebSocketQueueLimits(maxIncomingQueueSize, maxOutgoingQueueSize);
5292
+ return [void 0, true];
5293
+ } catch (error) {
5294
+ return [createPreflightFault("REQ_OPTIONS_INVALID", error), void 0];
5491
5295
  }
5492
- return ((input) => create(input));
5493
5296
  }
5494
- async function runWebSocketCommand(clientConfig, endpoint, input, config, controller, state) {
5495
- setSocketState(state, "connecting");
5496
- let cancellationConfig;
5297
+ function readCallOptions(config, webSocket) {
5497
5298
  try {
5498
- cancellationConfig = snapshotCancellationConfig(config);
5299
+ const beforeConnect = config.beforeConnect;
5300
+ const configuredProtocols = config.protocols;
5301
+ const protocols = configuredProtocols ? [...configuredProtocols] : void 0;
5302
+ const reconnectOption = config.reconnect ?? webSocket.reconnect;
5303
+ const heartbeatOption = config.heartbeat ?? webSocket.heartbeat;
5304
+ const heartbeat = heartbeatOption ? {
5305
+ intervalMs: heartbeatOption.intervalMs,
5306
+ isAck: heartbeatOption.isAck,
5307
+ message: heartbeatOption.message,
5308
+ timeoutMs: heartbeatOption.timeoutMs
5309
+ } : void 0;
5310
+ validateHeartbeatConfig(heartbeat);
5311
+ return [void 0, {
5312
+ beforeConnect,
5313
+ heartbeat,
5314
+ protocols,
5315
+ reconnect: normalizeReconnectConfig(reconnectOption)
5316
+ }];
5499
5317
  } catch (error) {
5500
- const definitionError = createPreflightFault("REQ_OPTIONS_INVALID", error);
5501
- state.error = definitionError;
5502
- setSocketState(state, "error");
5503
- return [
5504
- definitionError,
5505
- void 0,
5506
- void 0
5507
- ];
5318
+ return [createPreflightFault("REQ_OPTIONS_INVALID", error), void 0];
5508
5319
  }
5509
- if (hasAbortTimeoutConflict(cancellationConfig)) {
5510
- const definitionError = createAbortTimeoutConflictFault();
5511
- state.error = definitionError;
5512
- setSocketState(state, "error");
5513
- return [
5514
- definitionError,
5515
- void 0,
5516
- void 0
5517
- ];
5320
+ }
5321
+ function parseInput(inputStruct, input, guides, signal) {
5322
+ try {
5323
+ return [void 0, parseEndpointInput(inputStruct, input, guides)];
5324
+ } catch (error) {
5325
+ return [resolveAbortFault(signal) ?? createPreflightFault("REQ_INPUT_INVALID", error), void 0];
5518
5326
  }
5327
+ }
5328
+ function buildOutbound(parsedInput, builder, inputDeclared, guides, signal) {
5519
5329
  try {
5520
- validateTransportTimeout(cancellationConfig.timeout);
5330
+ return [void 0, materializeOutboundRequest(runOutboundBuilder(parsedInput, builder, inputDeclared), "webSocket", guides)];
5521
5331
  } catch (error) {
5522
- const definitionError = createPreflightFault("REQ_OPTIONS_INVALID", error);
5523
- state.error = definitionError;
5524
- setSocketState(state, "error");
5525
- return [
5526
- definitionError,
5527
- void 0,
5528
- void 0
5529
- ];
5332
+ return [resolveAbortFault(signal) ?? createPreflightFault("REQ_BUILD_FAILED", error), void 0];
5333
+ }
5334
+ }
5335
+ function createSocketRequest(args) {
5336
+ try {
5337
+ return [void 0, createWebSocketRequest(args)];
5338
+ } catch (error) {
5339
+ return [resolveAbortFault(args.abort) ?? createPreflightFault("REQ_BUILD_FAILED", error), void 0];
5530
5340
  }
5531
- const preAbortedSignal = [cancellationConfig.abort, cancellationConfig.signal].find((signal) => signal?.aborted);
5532
- if (preAbortedSignal) {
5533
- const aborted = resolveAbortedFault(preAbortedSignal);
5341
+ }
5342
+ function resolveSocketConstructor(handle) {
5343
+ const candidate = handle ?? globalThis.WebSocket;
5344
+ if (typeof candidate !== "function") return [createPreflightFault("ENV_UNSUPPORTED", /* @__PURE__ */ new Error("WebSocket is not supported in current runtime")), void 0];
5345
+ return [void 0, candidate];
5346
+ }
5347
+ async function runWebSocketCommand(clientConfig, endpoint, input, config, controller, state) {
5348
+ setSocketState(state, "connecting");
5349
+ const callerAbort = config.abort;
5350
+ if (callerAbort?.aborted) {
5351
+ const aborted = resolveAbortedFault(callerAbort);
5534
5352
  state.error = aborted;
5535
5353
  setSocketState(state, aborted.code === "NET_ABORTED" ? "aborted" : "error");
5536
5354
  return [
@@ -5539,56 +5357,26 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5539
5357
  void 0
5540
5358
  ];
5541
5359
  }
5542
- try {
5543
- validateWebSocketQueueLimits(endpoint.maxIncomingQueueSize, endpoint.maxOutgoingQueueSize);
5544
- } catch (error) {
5545
- const definitionError = createPreflightFault("REQ_OPTIONS_INVALID", error);
5546
- state.error = definitionError;
5547
- setSocketState(state, "error");
5548
- return [
5549
- definitionError,
5550
- void 0,
5551
- void 0
5552
- ];
5553
- }
5554
- let beforeConnectOption;
5555
- let protocolsOption;
5556
- let reconnect;
5557
- let heartbeatConfig;
5558
- try {
5559
- beforeConnectOption = config.beforeConnect;
5560
- const configuredProtocols = config.protocols;
5561
- protocolsOption = configuredProtocols ? [...configuredProtocols] : void 0;
5562
- const reconnectOption = config.reconnect ?? clientConfig.webSocket.reconnect;
5563
- const heartbeatOption = config.heartbeat ?? clientConfig.webSocket.heartbeat;
5564
- heartbeatConfig = heartbeatOption ? {
5565
- intervalMs: heartbeatOption.intervalMs,
5566
- isAck: heartbeatOption.isAck,
5567
- message: heartbeatOption.message,
5568
- timeoutMs: heartbeatOption.timeoutMs
5569
- } : void 0;
5570
- validateHeartbeatConfig(heartbeatConfig);
5571
- reconnect = normalizeReconnectConfig(reconnectOption);
5572
- } catch (error) {
5573
- const definitionError = createPreflightFault("REQ_OPTIONS_INVALID", error);
5574
- state.error = definitionError;
5360
+ const stopInvalidOptions = (fault) => {
5361
+ state.error = fault;
5575
5362
  setSocketState(state, "error");
5576
5363
  return [
5577
- definitionError,
5364
+ fault,
5578
5365
  void 0,
5579
5366
  void 0
5580
5367
  ];
5581
- }
5582
- const deadline = armTransportDeadline(cancellationConfig.timeout);
5583
- const chainSignal = mergeAbortSignals(new AbortController().signal, [
5584
- cancellationConfig.abort,
5585
- cancellationConfig.signal,
5586
- deadline.signal
5587
- ]);
5368
+ };
5369
+ const [queueFault] = readQueueLimits(endpoint.maxIncomingQueueSize, endpoint.maxOutgoingQueueSize);
5370
+ if (queueFault) return stopInvalidOptions(queueFault);
5371
+ const [optionsFault, callOptions] = readCallOptions(config, clientConfig.webSocket);
5372
+ if (optionsFault) return stopInvalidOptions(optionsFault);
5373
+ const beforeConnectOption = callOptions.beforeConnect;
5374
+ const protocolsOption = callOptions.protocols;
5375
+ const reconnect = callOptions.reconnect;
5376
+ const heartbeatConfig = callOptions.heartbeat;
5377
+ const chainSignal = mergeAbortSignals(new AbortController().signal, [callerAbort]);
5588
5378
  const signal = mergeAbortSignals(controller.signal, [chainSignal]);
5589
- deadline.releaseOnAbort(chainSignal);
5590
5379
  const stopBeforeTransport = (fault) => {
5591
- deadline.release();
5592
5380
  releaseAbortSignal(chainSignal);
5593
5381
  releaseAbortSignal(signal);
5594
5382
  state.error = fault;
@@ -5600,39 +5388,28 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5600
5388
  ];
5601
5389
  };
5602
5390
  const inputGuides = /* @__PURE__ */ new WeakMap();
5603
- let parsedInput;
5604
- try {
5605
- parsedInput = parseEndpointInput(endpoint.input, input, inputGuides);
5606
- } catch (error) {
5607
- return stopBeforeTransport(resolveTransportCancellation(deadline, signal, createPreflightFault("REQ_INPUT_INVALID", error)));
5608
- }
5609
- const parsedCancellation = resolveTransportCancellation(deadline, signal);
5391
+ const [inputFault, parsedInput] = parseInput(endpoint.input, input, inputGuides, signal);
5392
+ if (inputFault) return stopBeforeTransport(inputFault);
5393
+ const parsedCancellation = resolveAbortFault(signal);
5610
5394
  if (parsedCancellation) return stopBeforeTransport(parsedCancellation);
5611
- let built;
5612
- try {
5613
- built = materializeOutboundRequest(runOutboundBuilder(parsedInput, endpoint.builder, endpoint.input !== void 0), "webSocket", inputGuides);
5614
- } catch (error) {
5615
- return stopBeforeTransport(resolveTransportCancellation(deadline, signal, createPreflightFault("REQ_BUILD_FAILED", error)));
5616
- }
5617
- const builtCancellation = resolveTransportCancellation(deadline, signal);
5395
+ const [buildFault, built] = buildOutbound(parsedInput, endpoint.builder, endpoint.input !== void 0, inputGuides, signal);
5396
+ if (buildFault) return stopBeforeTransport(buildFault);
5397
+ const builtCancellation = resolveAbortFault(signal);
5618
5398
  if (builtCancellation) return stopBeforeTransport(builtCancellation);
5619
- let wsRequest;
5620
- try {
5621
- wsRequest = createWebSocketRequest({
5622
- abort: signal,
5623
- baseEndpoint: clientConfig.endpoint,
5624
- operation: endpoint.operation,
5625
- params: built.params,
5626
- path: endpoint.path,
5627
- query: built.query,
5628
- querySerializer: clientConfig.querySerializer,
5629
- withCredentials: clientConfig.withCredentials
5630
- });
5631
- } catch (error) {
5632
- return stopBeforeTransport(resolveTransportCancellation(deadline, signal, createPreflightFault("REQ_BUILD_FAILED", error)));
5633
- }
5634
- const WebSocketCtor = clientConfig.webSocket.handle ?? globalThis.WebSocket;
5635
- if (typeof WebSocketCtor !== "function") return stopBeforeTransport(createPreflightFault("ENV_UNSUPPORTED", /* @__PURE__ */ new Error("WebSocket is not supported in current runtime")));
5399
+ const [requestFault, wsRequest] = createSocketRequest({
5400
+ abort: signal,
5401
+ baseEndpoint: clientConfig.endpoint,
5402
+ operation: endpoint.operation,
5403
+ params: built.params,
5404
+ path: endpoint.path,
5405
+ query: built.query,
5406
+ querySerializer: clientConfig.querySerializer,
5407
+ withCredentials: clientConfig.withCredentials
5408
+ });
5409
+ if (requestFault) return stopBeforeTransport(requestFault);
5410
+ const constructorStage = resolveSocketConstructor(clientConfig.webSocket.handle);
5411
+ if (constructorStage[0]) return stopBeforeTransport(constructorStage[0]);
5412
+ const WebSocketCtor = constructorStage[1];
5636
5413
  const WebSocketImpl = WebSocketCtor;
5637
5414
  let startupConnection;
5638
5415
  const undeliveredSessions = /* @__PURE__ */ new Set();
@@ -5800,15 +5577,6 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5800
5577
  };
5801
5578
  }
5802
5579
  async function connectOnce(url, protocols) {
5803
- const due = resolveTransportCancellation(deadline, signal);
5804
- if (due) return {
5805
- cause: due,
5806
- closeInfo: {
5807
- cause: due,
5808
- reason: due.message
5809
- },
5810
- opened: false
5811
- };
5812
5580
  let socket;
5813
5581
  try {
5814
5582
  socket = protocols.length > 0 ? new WebSocketImpl(url, [...protocols]) : new WebSocketImpl(url);
@@ -5900,7 +5668,6 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5900
5668
  if (!startupSettled) {
5901
5669
  startupConnection = latestConnection;
5902
5670
  startupSettled = true;
5903
- deadline.release();
5904
5671
  resolveSession(session);
5905
5672
  }
5906
5673
  if (state.status === "open") startHeartbeat(socket, sessionController, heartbeatConfig, endpoint.outgoing, (cause) => {
@@ -6047,7 +5814,6 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
6047
5814
  /* istanbul ignore if -- @preserve defensive once-only gate: terminal callers and reentrant state transitions check finished before invoking finish */
6048
5815
  if (finished) return;
6049
5816
  finished = true;
6050
- deadline.release();
6051
5817
  stopHeartbeat(sessionController);
6052
5818
  const activeSocket = sessionController.currentSocket;
6053
5819
  if (final.kind !== "closed" && activeSocket && !options.skipNativeClose) requestNativeCloseBestEffort(activeSocket);
@@ -6162,7 +5928,6 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
6162
5928
  const publicConnection = session?.connection;
6163
5929
  if (typeof session?.closed?.then !== "function" || typeof session.close !== "function" || typeof session.send !== "function" || typeof session.receive?.[Symbol.asyncIterator] !== "function" || typeof session[Symbol.asyncDispose] !== "function" || typeof session.onRuntimeError !== "function" || typeof session.onStateChange !== "function" || typeof publicConnection?.generation !== "number" || typeof session.bufferedAmount !== "number" || typeof session.state !== "string") throw new TypeError("WebSocket interceptor must resolve a WebSocket session");
6164
5930
  chainSettled = true;
6165
- deadline.release();
6166
5931
  releaseAbortSignal(chainSignal);
6167
5932
  let deliveredSession = [...undeliveredSessions].some((createdSession) => createdSession === session || createdSession.closed === session.closed);
6168
5933
  const pendingSessionSnapshot = [...pendingSessions];
@@ -6188,7 +5953,6 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
6188
5953
  ];
6189
5954
  } catch (error) {
6190
5955
  chainSettled = true;
6191
- deadline.release();
6192
5956
  releaseAbortSignal(chainSignal);
6193
5957
  const signalError = resolveAbortFault(chainSignal);
6194
5958
  const requestError = signalError ?? (isFault(error) ? error : createPreflightFault("EXT_INTERCEPTOR_FAILED", error));
@@ -6212,32 +5976,6 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
6212
5976
  ];
6213
5977
  }
6214
5978
  }
6215
- /**
6216
- * Open a WebSocket command against the given client config.
6217
- *
6218
- * Prefer `client.execute(command)` in application code; this is the low-level entry used by the client.
6219
- *
6220
- * The result is `[null, session, connection]` or `[fault, undefined, connection | undefined]`, where the fault is `Fault<undefined>`.
6221
- *
6222
- * WebSocket execute options can override the client's `beforeConnect`, `heartbeat`, `protocols`, and `reconnect`.
6223
- *
6224
- * @param clientConfig - Resolved client configuration.
6225
- * @param command - WebSocket command from a `WebSocketEndpoint`.
6226
- * @param options - Optional execute options (heartbeat, reconnect, abort).
6227
- * @returns Await-result tuple of `[error, socket, connection]`.
6228
- * @group web-socket
6229
- */
6230
- async function executeWebSocketCommand(clientConfig, command, options) {
6231
- const { endpoint, input } = command;
6232
- return runWebSocketCommand(clientConfig, endpoint, input, options ?? {}, new AbortController(), {
6233
- listeners: {
6234
- runtimeError: /* @__PURE__ */ new Set(),
6235
- stateChange: /* @__PURE__ */ new Set()
6236
- },
6237
- status: "idle",
6238
- transition: 0
6239
- });
6240
- }
6241
5979
  const SOCKET_CLOSE_GRACE_MS = 1e3;
6242
5980
  const noop = () => void 0;
6243
5981
  function createWebSocketSession(outgoing, queue, closed, state, sessionController, sendQueue, maxOutgoingQueueSize, openState, isUnavailable, requestClose, disposeSession) {
@@ -6413,4 +6151,4 @@ function faultFromCloseInfo(closeInfo) {
6413
6151
  function isFault(value) {
6414
6152
  return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string" && value instanceof Error;
6415
6153
  }
6416
- export { ERR_ABORTED, ERR_TIMEOUT, StructError, basicAuthHttpInterceptor, basicAuthSSEInterceptor, createClient, createDecodeFault, createHttpInterceptor, createHttpStatusFault, createNetworkFault, createPreflightFault, createResolvedRequestUrl, createSSEInterceptor, createUndecodedHttpStatusFault, createWebSocketInterceptor, defineEventStream, defineRequest, defineWebSocket, executeEventStreamCommand, executeHttpCommand, executeWebSocketCommand, fetchHandler, isAbortCause, isTimeoutCause, makeResponse, struct, withCredentials, withEndpoint, withHTTPHandle, withHeaders, withInterceptors, withQuerySerializer, withSSEHandle, withSSEOnInvalidEvent, withSSEReconnect, withTimeout, withWebSocketBeforeConnect, withWebSocketHandle, withWebSocketHeartbeat, withWebSocketProtocols, withWebSocketReconnect, withXSRF };
6154
+ export { ERR_ABORTED, ERR_TIMEOUT, StructError, basicAuthHttpInterceptor, basicAuthSSEInterceptor, createClient, createDecodeFault, createHttpInterceptor, createHttpStatusFault, createNetworkFault, createPreflightFault, createResolvedRequestUrl, createSSEInterceptor, createUndecodedHttpStatusFault, createWebSocketInterceptor, defineEventStream, defineRequest, defineWebSocket, fetchHandler, isAbortCause, isTimeoutCause, makeResponse, struct, withCredentials, withEndpoint, withHTTPHandle, withHeaders, withInterceptors, withQuerySerializer, withSSEHandle, withSSEOnInvalidEvent, withSSEReconnect, withWebSocketBeforeConnect, withWebSocketHandle, withWebSocketHeartbeat, withWebSocketProtocols, withWebSocketReconnect, withXSRF };