@defjs/core 0.4.0-beta.1 → 0.4.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 +58 -29
  2. package/index.d.ts +1839 -342
  3. package/index.js +1587 -817
  4. package/index.min.js +21 -27
  5. package/package.json +2 -1
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/client/command.ts
1
+ // src/client/command.ts
2
2
  const COMMAND_TYPE = Symbol.for("defjs.command.type");
3
3
  const HTTP_COMMAND = Symbol.for("defjs.command.http");
4
4
  const EVENT_STREAM_COMMAND = Symbol.for("defjs.command.event-stream");
@@ -18,17 +18,20 @@ function isEventStreamCommand(value) {
18
18
  function isWebSocketCommand(value) {
19
19
  return commandTypeOf(value) === WEB_SOCKET_COMMAND;
20
20
  }
21
- //#endregion
22
- //#region src/client/config.ts
21
+ // src/client/config.ts
23
22
  const PLATFORM_FETCH = globalThis.fetch;
23
+ const PLATFORM_FETCH_HANDLE = PLATFORM_FETCH?.bind(globalThis);
24
+ /** @internal Classified as ENV_UNSUPPORTED only when a transport reports it. */
25
+ const ERR_FETCH_UNSUPPORTED = /* @__PURE__ */ new Error("fetch is not supported in current runtime");
24
26
  /**
25
- * The library default handle. The HTTP transport recognizes it by identity: only it is the platform fetch called with the
26
- * request signal, so only it may be called as `(url, init)` and have its body read in one piece.
27
+ * Resolves global `fetch` on each call, including calls made by detached HTTP/SSE default handles.
28
+ * HTTP recognizes it as the platform fast path only while the global fetch is untouched.
27
29
  */
28
- const DEFAULT_FETCH = PLATFORM_FETCH.bind(globalThis);
29
- /** `DEFAULT_FETCH` while the global fetch is untouched; a replaced global is an ordinary custom handle. */
30
+ const DEFAULT_FETCH = (input, init) => resolveGlobalFetch()(input, init);
31
+ /** The bound platform fast path while global fetch is untouched; a replaced global is an ordinary custom handle. */
30
32
  function resolveGlobalFetch() {
31
- return globalThis.fetch === PLATFORM_FETCH ? DEFAULT_FETCH : globalThis.fetch.bind(globalThis);
33
+ if (typeof globalThis.fetch !== "function") throw ERR_FETCH_UNSUPPORTED;
34
+ return globalThis.fetch === PLATFORM_FETCH && PLATFORM_FETCH_HANDLE ? PLATFORM_FETCH_HANDLE : globalThis.fetch.bind(globalThis);
32
35
  }
33
36
  const DEFAULT_HTTP_OPTIONS = { handle: DEFAULT_FETCH };
34
37
  const DEFAULT_SSE_OPTIONS = {
@@ -57,22 +60,20 @@ function createClientConfig() {
57
60
  }
58
61
  };
59
62
  }
60
- //#endregion
61
- //#region src/client/client.ts
63
+ // src/client/client.ts
62
64
  const CLIENT = Symbol("Client");
63
65
  /**
64
- * Create a Defjs client from option helpers.
66
+ * Creates a Defjs client from option helpers.
65
67
  *
66
- * Options are applied in order. Prefer creating the client inside the
67
- * request boundary when interceptors close over auth or tenants.
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.
68
69
  *
69
70
  * @param options - Client option helpers such as `withEndpoint`.
70
71
  * @returns A client that can `execute` HTTP, SSE, and WebSocket commands.
71
72
  *
72
- * @example
73
- * ```ts
74
- * const client = createClient(withEndpoint('https://api.example.com'))
75
- * ```
73
+ * @group client
74
+ * @see {@link Client}
75
+ * @see {@link ClientOption}
76
+ * @exampleFile examples/api/create-client.ts#example
76
77
  */
77
78
  function createClient(...options) {
78
79
  const conf = createClientConfig();
@@ -86,24 +87,26 @@ function createClient(...options) {
86
87
  execute
87
88
  };
88
89
  }
89
- //#endregion
90
- //#region src/struct/symbols.ts
90
+ // src/struct/symbols.ts
91
91
  const DEFINITION = Symbol.for("defjs.struct.definition");
92
- const OMIT = Symbol.for("defjs.struct.omit");
92
+ const OMIT = Symbol("defjs.struct.omit");
93
93
  /**
94
94
  * Stands in for a parsed value when parsing fails; the issue waits in the slot that `failure()` fills.
95
95
  * Not in the symbol registry, so no input value can ever equal it.
96
96
  */
97
97
  const FAILED = Symbol("defjs.struct.failed");
98
98
  /**
99
- * Enumerable tag on objects produced while parsing endpoint input.
100
- * Response decoding and `struct.parse` leave it unset. Internal to the outbound encoder.
99
+ * Type-only export of the internal enumerable tag key on objects produced while parsing endpoint input, used by the outbound encoder.
100
+ *
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.
102
+ *
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.
101
104
  *
102
105
  * @internal
106
+ * @group struct
103
107
  */
104
108
  const STRUCT = Symbol.for("defjs.struct.STRUCT");
105
- //#endregion
106
- //#region src/struct/guards.ts
109
+ // src/struct/guards.ts
107
110
  const KNOWN_KINDS = /* @__PURE__ */ new Set([
108
111
  "any",
109
112
  "array",
@@ -134,8 +137,7 @@ function isStruct(value) {
134
137
  const flags = definition.flags;
135
138
  return typeof flags === "object" && flags !== null && typeof flags.nullable === "boolean" && typeof flags.optional === "boolean";
136
139
  }
137
- //#endregion
138
- //#region src/struct/guide.ts
140
+ // src/struct/guide.ts
139
141
  const GUIDE_SET = Symbol.for("defjs.struct.guideSet");
140
142
  function isGuideSet(value) {
141
143
  return typeof value === "object" && value !== null && value[GUIDE_SET] === true;
@@ -158,31 +160,17 @@ function combineGuides(left, right) {
158
160
  if (!right) return left;
159
161
  return guideTag([...guideMembers(left), ...guideMembers(right)]);
160
162
  }
161
- function readOwnGuide(value) {
162
- const tag = value[STRUCT];
163
+ function readOwnGuide(value, guides) {
164
+ const tag = guides?.get(value) ?? value[STRUCT];
163
165
  if (isGuideSet(tag)) return tag;
164
166
  if (!isStruct(tag)) return;
165
167
  const kind = tag[DEFINITION].kind;
166
168
  if (kind === "object" || kind === "record") return tag;
167
169
  }
168
- /**
169
- * Tag a null-prototype node this parse just created. A plain store makes the same enumerable,
170
- * writable, configurable data property as `defineStructTag`, without the define path's cost.
171
- */
170
+ /** Tag a null-prototype node this parse just created. Enumerable so object spread keeps its guide. */
172
171
  function tagOwnNode(value, tag) {
173
172
  value[STRUCT] = tag;
174
173
  }
175
- /** Enumerable so object spread keeps the struct that parsed this object. `JSON.stringify` still skips it. */
176
- function defineStructTag(value, tag) {
177
- Object.defineProperty(value, STRUCT, {
178
- configurable: true,
179
- enumerable: true,
180
- value: tag,
181
- writable: true
182
- });
183
- }
184
- //#endregion
185
- //#region src/struct/utils.ts
186
174
  /**
187
175
  * Deep-merge two intersection outputs.
188
176
  *
@@ -192,9 +180,10 @@ function defineStructTag(value, tag) {
192
180
  *
193
181
  * `tagNodes` folds each side's parse tag into the new object at this level. One-sided children are
194
182
  * copied with the tag their own parser already wrote, so a later walk is not required. Null and
195
- * undefined never enter this branch.
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.
196
185
  */
197
- function mergeIntersectionValues(left, right, tagNodes = false) {
186
+ function mergeIntersectionValues(left, right, tagNodes, depth, context) {
198
187
  if (Object.is(left, right)) return {
199
188
  ok: true,
200
189
  value: left
@@ -204,6 +193,11 @@ function mergeIntersectionValues(left, right, tagNodes = false) {
204
193
  value: left
205
194
  };
206
195
  if (isPlainObject(left) && isPlainObject(right)) {
196
+ if (depth >= 256) return {
197
+ ok: false,
198
+ path: [],
199
+ received: right
200
+ };
207
201
  const output = nullObject();
208
202
  for (const key of Object.keys(left)) output[key] = left[key];
209
203
  for (const key of Object.keys(right)) {
@@ -211,7 +205,7 @@ function mergeIntersectionValues(left, right, tagNodes = false) {
211
205
  output[key] = right[key];
212
206
  continue;
213
207
  }
214
- const merged = mergeIntersectionValues(output[key], right[key], tagNodes);
208
+ const merged = mergeIntersectionValues(output[key], right[key], tagNodes, depth + 1, context);
215
209
  if (!merged.ok) return {
216
210
  ok: false,
217
211
  path: [key, ...merged.path],
@@ -220,7 +214,8 @@ function mergeIntersectionValues(left, right, tagNodes = false) {
220
214
  output[key] = merged.value;
221
215
  }
222
216
  if (tagNodes) {
223
- const tag = combineGuides(readOwnGuide(left), readOwnGuide(right));
217
+ context?.owned.add(output);
218
+ const tag = combineGuides(readOwnGuide(left, context?.guides), readOwnGuide(right, context?.guides));
224
219
  if (tag) tagOwnNode(output, tag);
225
220
  }
226
221
  return {
@@ -229,14 +224,14 @@ function mergeIntersectionValues(left, right, tagNodes = false) {
229
224
  };
230
225
  }
231
226
  if (Array.isArray(left) && Array.isArray(right)) {
232
- if (left.length !== right.length) return {
227
+ if (left.length !== right.length || depth >= 256) return {
233
228
  ok: false,
234
229
  path: [],
235
230
  received: right
236
231
  };
237
232
  const output = new Array(left.length);
238
233
  for (let index = 0; index < left.length; index += 1) {
239
- const merged = mergeIntersectionValues(left[index], right[index], tagNodes);
234
+ const merged = mergeIntersectionValues(left[index], right[index], tagNodes, depth + 1, context);
240
235
  if (!merged.ok) return {
241
236
  ok: false,
242
237
  path: [index, ...merged.path],
@@ -273,22 +268,10 @@ function matchesEnum(definition, value) {
273
268
  }
274
269
  return (definition.valueSet ??= new Set(definition.values)).has(value);
275
270
  }
276
- let parkedIssue;
277
271
  /** What a decode hook returns for a value it accepts: the decoded value itself. */
278
272
  function success(value) {
279
273
  return value;
280
274
  }
281
- /** Parks the issue for the parse entry point and returns FAILED. A decode hook receives it as `fail`. */
282
- function failure(issue) {
283
- parkedIssue = issue;
284
- return FAILED;
285
- }
286
- function parkedFailure() {
287
- return parkedIssue;
288
- }
289
- function restoreFailure(issue) {
290
- parkedIssue = issue;
291
- }
292
275
  function expectedType(definition) {
293
276
  switch (definition.kind) {
294
277
  case "any": return "any";
@@ -360,43 +343,36 @@ function sameWireValue(left, right) {
360
343
  const rightKeys = Object.keys(right);
361
344
  return leftKeys.length === rightKeys.length && leftKeys.every((key) => hasOwnKey(right, key) && sameWireValue(left[key], right[key]));
362
345
  }
363
- function describeValue(value) {
346
+ /**
347
+ * Kind of a received value, safe to put in a log line.
348
+ *
349
+ * Names the kind and never the value. `File` is checked before `Blob` because a file is a blob.
350
+ */
351
+ function describeKind(value) {
364
352
  if (value === null) return "null";
365
- if (value === void 0) return "undefined";
366
- if (typeof value === "string") return JSON.stringify(value);
367
- if (typeof value === "number" || typeof value === "boolean") return String(value);
368
- if (value instanceof File) return `File(${value.name})`;
369
- if (value instanceof Blob) return `Blob(${value.type || "application/octet-stream"})`;
370
- if (value instanceof ArrayBuffer) return `ArrayBuffer(${value.byteLength})`;
371
353
  if (Array.isArray(value)) return "array";
372
- if (isPlainObject(value)) return "object";
373
- return Object.prototype.toString.call(value);
354
+ if (typeof value !== "object") return typeof value;
355
+ if (value instanceof File) return "File";
356
+ if (value instanceof Blob) return "Blob";
357
+ if (value instanceof ArrayBuffer) return "ArrayBuffer";
358
+ if (value instanceof Date) return "Date";
359
+ return "object";
360
+ }
361
+ function describeValue(value) {
362
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
374
363
  }
375
- //#endregion
376
- //#region src/internal/request_values.ts
364
+ // src/internal/request_values.ts
377
365
  function isRequestScalarValue(value) {
378
366
  return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || value === null;
379
367
  }
380
368
  function serializeRequestScalarValue(value) {
381
369
  return String(value);
382
370
  }
383
- //#endregion
384
- //#region src/internal/url.ts
371
+ // src/internal/url.ts
372
+ var RequestUrlError = class extends TypeError {};
385
373
  function fillUrl(path, params) {
386
374
  const paramMap = /* @__PURE__ */ new Map();
387
- if (params) for (const [key, value] of Object.entries(params)) {
388
- if (typeof value === "undefined") continue;
389
- if (Array.isArray(value)) {
390
- const first = value[0];
391
- if (typeof first !== "undefined") {
392
- if (!isUrlScalarValue(first)) throw new TypeError(`path value for "${key}" requires a scalar value`);
393
- paramMap.set(key, encodePathSegment(key, serializeRequestScalarValue(first)));
394
- }
395
- continue;
396
- }
397
- if (!isUrlScalarValue(value)) throw new TypeError(`path value for "${key}" requires a scalar value`);
398
- paramMap.set(key, encodePathSegment(key, serializeRequestScalarValue(value)));
399
- }
375
+ if (params) for (const [key, value] of Object.entries(params)) paramMap.set(key, encodePathSegment(key, value));
400
376
  return path.replace(/:([^/]+)/g, (_, part) => {
401
377
  const value = paramMap.get(part);
402
378
  if (typeof value === "undefined") throw new TypeError(`Missing path param: ${part}`);
@@ -447,8 +423,17 @@ function appendRecordToHeaders(headers, value) {
447
423
  });
448
424
  return;
449
425
  }
426
+ const applied = /* @__PURE__ */ new Set();
427
+ const appendValue = (key, headerValue) => {
428
+ const name = key.toLowerCase();
429
+ if (applied.has(name)) headers.append(key, headerValue);
430
+ else {
431
+ headers.set(key, headerValue);
432
+ applied.add(name);
433
+ }
434
+ };
450
435
  if (Array.isArray(value)) {
451
- for (const [key, headerValue] of value) headers.append(key, headerValue);
436
+ for (const [key, headerValue] of value) appendValue(key, headerValue);
452
437
  return;
453
438
  }
454
439
  for (const [key, headerValue] of Object.entries(value)) {
@@ -456,23 +441,46 @@ function appendRecordToHeaders(headers, value) {
456
441
  if (Array.isArray(headerValue)) {
457
442
  for (const item of headerValue) {
458
443
  if (!isUrlScalarValue(item)) throw new TypeError(`header value for "${key}" requires a scalar value`);
459
- headers.append(key, serializeRequestScalarValue(item));
444
+ appendValue(key, serializeRequestScalarValue(item));
460
445
  }
461
446
  continue;
462
447
  }
463
448
  if (!isUrlScalarValue(headerValue)) throw new TypeError(`header value for "${key}" requires a scalar value`);
464
- headers.set(key, serializeRequestScalarValue(headerValue));
449
+ appendValue(key, serializeRequestScalarValue(headerValue));
465
450
  }
466
451
  }
452
+ /**
453
+ * Resolves a relative definition path under the base endpoint's origin and directory into a URL.
454
+ *
455
+ * During `client.execute`, a missing, relative, or invalid base or an illegal
456
+ * path becomes `REQ_BUILD_FAILED` before any interceptor or transport runs,
457
+ * including a short-circuiting interceptor, for HTTP, SSE, and WebSocket alike.
458
+ * An interceptor's final `req.endpoint` is also validated against its final
459
+ * `req.baseEndpoint`; deliberately replacing the base with another valid
460
+ * absolute URL is allowed. Deterministic URL errors are never retried.
461
+ *
462
+ * @throws TypeError when the endpoint or path is invalid.
463
+ * @param baseEndpoint - A valid absolute client endpoint. Its query and hash are dropped, and a trailing slash is added to its pathname so that the base path is kept: `https://api.example.com/v1` with `/users` gives `https://api.example.com/v1/users`.
464
+ * @param path - A relative endpoint path. A single leading slash is removed. After resolution, the URL must keep the base origin and stay under its directory. Absolute or protocol-relative paths, C0 control characters (including tab, CR, and LF), backslashes, `?`, `#`, and directory escapes are rejected. Scheme-like and protocol-relative forms are rejected even with leading spaces; a plain inner space is allowed and sent as `%20`.
465
+ * @param queryString - Query without a leading `?`. When non-empty it is set as the URL's search verbatim.
466
+ * @returns the resolved `URL`.
467
+ * @group http
468
+ */
467
469
  function createResolvedRequestUrl(baseEndpoint, path, queryString = "") {
468
470
  const base = createEndpointDirectoryBase(baseEndpoint);
469
- const normalizedPath = normalizeEndpointPath(path);
470
- const url = new URL(normalizedPath, base);
471
+ let url;
472
+ try {
473
+ url = new URL(path.replace(/^\/(?!\/)/, ""), base);
474
+ } catch {
475
+ throw new RequestUrlError("Endpoint path must resolve to a valid URL");
476
+ }
477
+ if (url.origin !== base.origin || !url.pathname.startsWith(base.pathname)) throw new RequestUrlError("Endpoint path must not be an absolute URL or leave the client endpoint directory");
478
+ validateEndpointPath(path);
471
479
  if (queryString) url.search = queryString;
472
480
  return url;
473
481
  }
474
482
  function resolveRequestUrl(request) {
475
- if (!request.baseEndpoint) throw new TypeError("Client endpoint is required");
483
+ if (!request.baseEndpoint) throw new RequestUrlError("Client endpoint is required");
476
484
  const queryString = typeof request.queryString === "string" ? request.queryString : request.queryParams ? request.queryParams.toString() : "";
477
485
  return createResolvedRequestUrl(request.baseEndpoint, request.endpoint, queryString);
478
486
  }
@@ -506,7 +514,7 @@ function createEndpointDirectoryBase(baseEndpoint) {
506
514
  try {
507
515
  base = new URL(baseEndpoint);
508
516
  } catch {
509
- throw new TypeError("Client endpoint must be a valid URL");
517
+ throw new RequestUrlError("Client endpoint must be a valid URL");
510
518
  }
511
519
  base.search = "";
512
520
  base.hash = "";
@@ -515,20 +523,24 @@ function createEndpointDirectoryBase(baseEndpoint) {
515
523
  lastBase = base;
516
524
  return base;
517
525
  }
518
- function normalizeEndpointPath(path) {
519
- if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(path) || path.startsWith("//")) throw new Error("Endpoint path must not be an absolute URL");
520
- if (path.includes("?") || path.includes("#")) throw new Error("Endpoint path must not include query or hash");
521
- return path.replace(/^\/+/, "");
526
+ function validateEndpointPath(path) {
527
+ if (URL.canParse(path) || /^ *\/\//.test(path)) throw new RequestUrlError("Endpoint path must not be an absolute URL");
528
+ if (path.includes("?") || path.includes("#")) throw new RequestUrlError("Endpoint path must not include query or hash");
529
+ if (/[\u0000-\u001f\\]/.test(path)) throw new RequestUrlError("Endpoint path must not include C0 controls or backslashes");
522
530
  }
523
- //#endregion
524
- //#region src/internal/http_response.ts
525
- const HTTP_RESPONSE = Symbol("HttpResponse");
531
+ // src/internal/http_response.ts
532
+ const HTTP_RESPONSE = Symbol.for("defjs.http.response");
526
533
  /**
527
- * Build an `HttpResponse` value without performing a network call.
528
- * Useful in interceptors that short-circuit `next`, and in tests.
534
+ * Builds an `HttpResponse` without a network call, for interceptors that short-circuit `next` and for tests.
535
+ *
536
+ * Default status is `0`, so pass a 2xx `status` for a success. `ok` is true for
537
+ * 2xx. The value it returns goes through the same media-type check and the same
538
+ * declared struct as one off the wire; there is no trusted short-circuit. For a
539
+ * JSON body, set a JSON `content-type` header.
529
540
  *
530
541
  * @param options - Status, headers, body, and an optional request to copy headers/url from; defaults yield status `0`.
531
542
  * @returns An `HttpResponse` with `ok` derived from the status code.
543
+ * @group http
532
544
  */
533
545
  function makeResponse(options) {
534
546
  const status = options?.status ?? 0;
@@ -547,6 +559,13 @@ function makeResponse(options) {
547
559
  ok
548
560
  };
549
561
  }
562
+ /**
563
+ * Whether `value` is an `HttpResponse`: built by `makeResponse` or the transport, or derived from one by spreading.
564
+ * The brand is the runtime half of the type's nominal guard, so a native `Response` or a lookalike object fails.
565
+ */
566
+ function isHttpResponse(value) {
567
+ return value?.[HTTP_RESPONSE] === true;
568
+ }
550
569
  function getHttpErrorMessage(response) {
551
570
  let message = `Http failure response: ${response.status}`;
552
571
  if (response.statusText) message += ` - ${response.statusText}`;
@@ -557,34 +576,97 @@ function requestUrl(request) {
557
576
  if (!request.baseEndpoint) return request.endpoint;
558
577
  return resolveRequestUrl(request).href;
559
578
  }
560
- //#endregion
561
- //#region src/error/cause.ts
562
- /** Shared cause/message sentinel for aborted requests. */
563
- const ERR_ABORTED = /* @__PURE__ */ new Error("Request was aborted");
564
- /** Shared cause/message sentinel for timed-out requests. */
565
- const ERR_TIMEOUT = /* @__PURE__ */ new Error("Request timed out");
579
+ // src/error/cause.ts
580
+ const ABORT_BRAND = Symbol.for("defjs.cause.aborted");
581
+ const TIMEOUT_BRAND = Symbol.for("defjs.cause.timeout");
582
+ function brandSentinel(error, brand) {
583
+ Object.defineProperty(error, brand, {
584
+ configurable: false,
585
+ enumerable: false,
586
+ value: true,
587
+ writable: false
588
+ });
589
+ return error;
590
+ }
591
+ function hasCauseBrand(cause, brand) {
592
+ return typeof cause === "object" && cause !== null && brand in cause;
593
+ }
594
+ /**
595
+ * Shared Error sentinel that marks a request as aborted.
596
+ *
597
+ * Its `message` is `Request was aborted`. Shared `cause` and message value for caller cancellation. `createNetworkFault`
598
+ * maps it, and a `DOMException` named `AbortError`, to `NET_ABORTED`. An HTTP
599
+ * interceptor that throws it makes the call resolve with a `NET_ABORTED` fault.
600
+ * SSE and WebSocket interceptors that throw it are reported as
601
+ * `EXT_INTERCEPTOR_FAILED`.
602
+ *
603
+ * The sentinel carries a non-enumerable `Symbol.for('defjs.cause.aborted')`
604
+ * brand. {@link isAbortCause} recognizes this sentinel from any Core copy
605
+ * without comparing object identity. Classification is independent of unrelated
606
+ * earlier executions.
607
+ *
608
+ * @group errors
609
+ */
610
+ const ERR_ABORTED = brandSentinel(/* @__PURE__ */ new Error("Request was aborted"), ABORT_BRAND);
611
+ /**
612
+ * Shared Error sentinel that marks a request as timed out.
613
+ *
614
+ * Its `message` is `Request timed out`. Shared `cause` and message value for timeouts. `createNetworkFault` maps it,
615
+ * and any error named `TimeoutError`, to `NET_TIMEOUT`. An HTTP interceptor
616
+ * that throws it makes the call resolve with a `NET_TIMEOUT` fault. SSE and
617
+ * WebSocket interceptors that throw it are reported as `EXT_INTERCEPTOR_FAILED`.
618
+ *
619
+ * The sentinel carries a non-enumerable `Symbol.for('defjs.cause.timeout')`
620
+ * brand. {@link isTimeoutCause} recognizes this sentinel from any Core copy
621
+ * without comparing object identity. Classification is independent of unrelated
622
+ * earlier executions.
623
+ *
624
+ * @group errors
625
+ */
626
+ const ERR_TIMEOUT = brandSentinel(/* @__PURE__ */ new Error("Request timed out"), TIMEOUT_BRAND);
566
627
  /**
567
- * Whether `cause` represents caller cancellation.
628
+ * Whether `cause` is caller cancellation, across Core copies and platform abort errors.
629
+ *
630
+ * Returns `true` for an `Error` carrying the non-enumerable
631
+ * `Symbol.for('defjs.cause.aborted')` brand, including `ERR_ABORTED` from
632
+ * another Core copy, or a `DOMException` named `AbortError`. An ordinary
633
+ * unbranded `Error` with that name does not match. Other values return `false`.
634
+ *
635
+ * Pass the underlying `fault.cause`; this function does not unwrap faults.
636
+ * `createNetworkFault` uses this check to select `NET_ABORTED`. OpenTelemetry
637
+ * uses it to suppress caller-cancellation logs while preserving timeout logs.
568
638
  *
569
- * Recognizes the shared `ERR_ABORTED` sentinel and the platform's `AbortError`.
639
+ * @group errors
640
+ * @see {@link ERR_ABORTED}
641
+ * @see {@link isTimeoutCause}
570
642
  */
571
643
  function isAbortCause(cause) {
572
- return cause === ERR_ABORTED || cause instanceof DOMException && cause.name === "AbortError";
644
+ return hasCauseBrand(cause, ABORT_BRAND) || cause instanceof DOMException && cause.name === "AbortError";
573
645
  }
574
646
  /**
575
- * Whether `cause` represents a timeout.
647
+ * Whether `cause` is a timeout, across Core copies and platform timeout errors.
576
648
  *
577
- * Recognizes the shared `ERR_TIMEOUT` sentinel and any error named `TimeoutError`.
649
+ * Returns `true` for an `Error` carrying the non-enumerable
650
+ * `Symbol.for('defjs.cause.timeout')` brand, including `ERR_TIMEOUT` from
651
+ * another Core copy, or a `DOMException` or `Error` named `TimeoutError`.
652
+ * Other values return `false`.
653
+ *
654
+ * Pass the underlying `fault.cause`; this function does not unwrap faults.
655
+ * `createNetworkFault` uses this check to select `NET_TIMEOUT`. OpenTelemetry
656
+ * keeps logging timeouts even when the abort signal is also set.
657
+ *
658
+ * @group errors
659
+ * @see {@link ERR_TIMEOUT}
660
+ * @see {@link isAbortCause}
578
661
  */
579
662
  function isTimeoutCause(cause) {
580
- return cause === ERR_TIMEOUT || cause instanceof DOMException && cause.name === "TimeoutError" || cause instanceof Error && cause.name === "TimeoutError";
663
+ return hasCauseBrand(cause, TIMEOUT_BRAND) || cause instanceof DOMException && cause.name === "TimeoutError" || cause instanceof Error && cause.name === "TimeoutError";
581
664
  }
582
665
  /** Message carried by `cause`, or its string form when it is not an error. */
583
666
  function describeCause(cause) {
584
667
  return cause instanceof Error ? cause.message : String(cause);
585
668
  }
586
- //#endregion
587
- //#region src/error/factory.ts
669
+ // src/error/factory.ts
588
670
  const FAULT_NAME = "DefjsFault";
589
671
  function createFault(message, cause) {
590
672
  const fault = cause === void 0 ? new Error(message) : new Error(message, { cause });
@@ -611,48 +693,77 @@ function buildHttpStatusFault(response, data) {
611
693
  return fault;
612
694
  }
613
695
  /**
614
- * Build the fault for a non-2xx response whose body decoded against the declared `error` struct.
696
+ * Builds an `HTTP_STATUS` fault for a non-2xx response whose body decoded against the declared `error` struct.
697
+ *
698
+ * The fault is a native `Error` named `DefjsFault` with `code: 'HTTP_STATUS'`,
699
+ * `data` set to `response.body`, `status` copied from the response, and
700
+ * `response` as passed. The message is `Http failure response: <status>`,
701
+ * followed by ` - <statusText>` when there is one.
702
+ *
703
+ * Use {@link createUndecodedHttpStatusFault} when no body was decoded.
615
704
  *
616
705
  * @param response - The response, carrying the decoded body.
617
706
  * @returns An `HTTP_STATUS` fault whose `data` is that body.
707
+ * @group errors
618
708
  */
619
709
  function createHttpStatusFault(response) {
620
710
  return buildHttpStatusFault(response, response.body);
621
711
  }
622
712
  /**
623
- * Build the fault for a non-2xx response that was never decoded, because the endpoint
624
- * declared no `error` struct.
713
+ * Builds an `HTTP_STATUS` fault for a non-2xx response that was never decoded, because the endpoint declared no `error` struct.
714
+ *
715
+ * The fault is a native `Error` named `DefjsFault` with `code: 'HTTP_STATUS'`,
716
+ * `data: undefined`, `status` copied from the response, and `response` as passed.
717
+ *
718
+ * Use {@link createHttpStatusFault} when the body was decoded.
625
719
  *
626
720
  * @param response - Metadata of the response; no body was read.
627
721
  * @returns An `HTTP_STATUS` fault whose `data` is `undefined`.
722
+ * @group errors
628
723
  */
629
724
  function createUndecodedHttpStatusFault(response) {
630
725
  return buildHttpStatusFault(response, void 0);
631
726
  }
632
727
  /**
633
- * Build the fault for a response that arrived but could not be read as declared.
728
+ * Builds a decode fault for a response that arrived but could not be read as declared.
634
729
  *
635
- * Carries metadata only: a body is a decoded value, and decoding is what failed.
730
+ * `code` is `RES_DECODE_FAILED`, `RES_MEDIA_TYPE_INVALID`, or
731
+ * `RES_STRUCT_MISMATCH`. The fault carries metadata only: a body is a decoded
732
+ * value, and decoding is what failed, so there is no `body`. `status` is copied
733
+ * from `response`. The result is a native `Error` named `DefjsFault`.
734
+ *
735
+ * `cause` is the underlying parser or struct failure and is kept as `cause`.
736
+ * Its message normally becomes the fault message (`String(cause)` when it is
737
+ * not an `Error`). For `RES_DECODE_FAILED` with an `Error` named `SyntaxError`,
738
+ * the message is `Response body could not be decoded`, so `String(fault)` does
739
+ * not include a body fragment. Default Struct messages contain received kinds,
740
+ * not values; raw values and parser text remain on the cause.
636
741
  *
637
742
  * @param code - Which read step failed.
638
743
  * @param cause - Underlying parser or struct failure.
639
744
  * @param response - Metadata of the response that could not be read.
640
745
  * @returns A decode fault with no body.
746
+ * @group errors
641
747
  */
642
748
  function createDecodeFault(code, cause, response) {
643
- const fault = createFault(describeCause(cause), cause);
749
+ const fault = createFault(code === "RES_DECODE_FAILED" && cause instanceof Error && cause.name === "SyntaxError" ? "Response body could not be decoded" : describeCause(cause), cause);
644
750
  fault.code = code;
645
751
  fault.response = response;
646
752
  fault.status = response.status;
647
753
  return fault;
648
754
  }
649
755
  /**
650
- * Build a fault for a failure that may have happened before any response existed.
756
+ * Builds a fault for a failure that may have happened before any response existed.
757
+ *
758
+ * When `cause` is given, its message becomes the fault message; otherwise the
759
+ * code is the message. The result is a native `Error` named `DefjsFault` with
760
+ * `code`, plus `response` when one was given.
651
761
  *
652
762
  * @param code - The specific failure.
653
763
  * @param cause - Underlying value, when there is one; the code becomes the message otherwise.
654
764
  * @param response - Metadata, only where the transport already had some to report.
655
765
  * @returns A preflight fault.
766
+ * @group errors
656
767
  */
657
768
  function createPreflightFault(code, cause, response) {
658
769
  const fault = createFault(cause === void 0 ? code : describeCause(cause), cause);
@@ -661,14 +772,24 @@ function createPreflightFault(code, cause, response) {
661
772
  return fault;
662
773
  }
663
774
  /**
664
- * Build a `NET_*` fault, choosing the code from the shape of `cause`.
775
+ * Builds a `NET_*` `PreflightFault`, choosing the code from the shape of `cause`.
665
776
  *
666
- * Recognizes the shared abort/timeout sentinels and the platform's `AbortError`/`TimeoutError`;
667
- * anything else is reported as unreachable.
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.
668
788
  *
669
789
  * @param cause - Underlying cancellation, timeout, or transport failure.
670
790
  * @param response - Metadata, only where the transport already had some to report.
671
791
  * @returns A `NET_ABORTED`, `NET_TIMEOUT`, or `NET_UNREACHABLE` fault.
792
+ * @group errors
672
793
  */
673
794
  function createNetworkFault(cause, response) {
674
795
  if (isAbortCause(cause)) return withResponse(createFault(ERR_ABORTED.message, cause), "NET_ABORTED", response);
@@ -680,8 +801,7 @@ function withResponse(fault, code, response) {
680
801
  if (response !== void 0) fault.response = response;
681
802
  return fault;
682
803
  }
683
- //#endregion
684
- //#region src/internal/abort.ts
804
+ // src/internal/abort.ts
685
805
  const ABORT_TIMEOUT_CONFLICT_MESSAGE = "abort and timeout cannot be used together";
686
806
  const MAX_TIMER_DELAY_MS$2 = 2147483647;
687
807
  function snapshotCancellationConfig(config) {
@@ -699,10 +819,9 @@ function resolveAbortCause(signal) {
699
819
  if (!signal.aborted) return;
700
820
  return resolveAbortedCause(signal);
701
821
  }
702
- /** `ERR_TIMEOUT` or `ERR_ABORTED`, chosen from the signal's abort reason. */
822
+ /** `ERR_TIMEOUT` or `ERR_ABORTED`, chosen from the signal's abort reason, including another copy's timeout sentinel. */
703
823
  function resolveAbortedCause(signal) {
704
- const reason = signal.reason;
705
- return reason === ERR_TIMEOUT || reason instanceof Error && reason.name === "TimeoutError" ? ERR_TIMEOUT : ERR_ABORTED;
824
+ return isTimeoutCause(signal.reason) ? ERR_TIMEOUT : ERR_ABORTED;
706
825
  }
707
826
  /** `REQ_OPTIONS_INVALID` for execute options that pass both `abort` and `timeout`. */
708
827
  function createAbortTimeoutConflictFault() {
@@ -846,13 +965,17 @@ function mergeAbortSignals(controller, signals, timeout) {
846
965
  }
847
966
  return combinedController.signal;
848
967
  }
849
- //#endregion
850
- //#region src/client/option.ts
968
+ // src/client/option.ts
851
969
  /**
852
- * Sets the base URL prepended to relative request paths.
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:`.
853
973
  *
854
- * @param endpoint - Absolute or relative base endpoint.
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.
855
977
  * @returns An option that writes `config.endpoint`.
978
+ * @group client
856
979
  */
857
980
  function withEndpoint(endpoint) {
858
981
  return (config) => {
@@ -860,14 +983,17 @@ function withEndpoint(endpoint) {
860
983
  };
861
984
  }
862
985
  /**
863
- * Sets static default headers for HTTP/SSE requests built by this client.
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.
864
989
  *
865
- * Builder `headers` override the same names. HMAC and
866
- * dynamic Bearer tokens still belong in interceptors (they need the serialized body
867
- * or a refreshed credential).
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.
868
993
  *
869
994
  * @param headers - `HeadersInit` bag cloned onto the client config.
870
995
  * @returns An option that writes `config.headers`.
996
+ * @group client
871
997
  */
872
998
  function withHeaders(headers) {
873
999
  return (config) => {
@@ -875,15 +1001,15 @@ function withHeaders(headers) {
875
1001
  };
876
1002
  }
877
1003
  /**
878
- * Sets the default timeout for HTTP `execute` only.
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.
879
1007
  *
880
- * Timeout is applied before the interceptor chain (`mergeAbortSignals`). Execute
881
- * `{ timeout }` overrides this default. Execute `{ abort }` combines with the
882
- * default (abort → `ABORTED`, timer → `TIMEOUT`). SSE and WebSocket ignore this
883
- * option. XOR still rejects execute options that pass both `abort` and `timeout`.
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.
884
1009
  *
885
1010
  * @param ms - Positive safe integer timeout in milliseconds.
886
1011
  * @returns An option that writes `config.timeout`.
1012
+ * @group client
887
1013
  */
888
1014
  function withTimeout(ms) {
889
1015
  validateTransportTimeout(ms);
@@ -894,8 +1020,11 @@ function withTimeout(ms) {
894
1020
  /**
895
1021
  * Appends interceptors to the client interceptor chain.
896
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
+ *
897
1025
  * @param interceptors - Interceptors applied in registration order.
898
1026
  * @returns An option that pushes onto `config.interceptors`.
1027
+ * @group client
899
1028
  */
900
1029
  function withInterceptors(...interceptors) {
901
1030
  return (config) => {
@@ -903,14 +1032,15 @@ function withInterceptors(...interceptors) {
903
1032
  };
904
1033
  }
905
1034
  /**
906
- * Sets the query serializer for HTTP, SSE, and WebSocket requests.
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.
907
1038
  *
908
- * The function receives the encoded wire object and its string is used verbatim.
909
- * Without this option, queries use `URLSearchParams`: arrays repeat keys and a nested
910
- * object fails. A builder that returns `URLSearchParams` skips this serializer.
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.
911
1040
  *
912
1041
  * @param serializer - Function that turns an encoded query object into a string.
913
1042
  * @returns An option that sets `config.querySerializer`.
1043
+ * @group client
914
1044
  */
915
1045
  function withQuerySerializer(serializer) {
916
1046
  return (config) => {
@@ -918,11 +1048,14 @@ function withQuerySerializer(serializer) {
918
1048
  };
919
1049
  }
920
1050
  /**
921
- * Overrides the Fetch-compatible function used for HTTP requests.
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}.
922
1054
  *
923
1055
  * @param fetchImpl - `(input, init?) => Promise<Response>`. Not `typeof fetch`, so
924
1056
  * a mock does not need host extras such as `preconnect`.
925
1057
  * @returns An option that sets `config.http.handle`.
1058
+ * @group client
926
1059
  */
927
1060
  function withHTTPHandle(fetchImpl) {
928
1061
  return (config) => {
@@ -930,11 +1063,14 @@ function withHTTPHandle(fetchImpl) {
930
1063
  };
931
1064
  }
932
1065
  /**
933
- * Overrides the Fetch-compatible function used for SSE connections.
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}.
934
1069
  *
935
1070
  * @param fetchImpl - `(input, init?) => Promise<Response>`. Not `typeof fetch`, so
936
1071
  * a mock does not need host extras such as `preconnect`.
937
1072
  * @returns An option that sets `config.sse.handle`.
1073
+ * @group client
938
1074
  */
939
1075
  function withSSEHandle(fetchImpl) {
940
1076
  return (config) => {
@@ -942,10 +1078,13 @@ function withSSEHandle(fetchImpl) {
942
1078
  };
943
1079
  }
944
1080
  /**
945
- * Overrides the WebSocket constructor used for socket connections.
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.
946
1084
  *
947
1085
  * @param WebSocketImpl - Constructor that returns a `WebSocketHandle`.
948
1086
  * @returns An option that sets `config.webSocket.handle`.
1087
+ * @group client
949
1088
  */
950
1089
  function withWebSocketHandle(WebSocketImpl) {
951
1090
  return (config) => {
@@ -953,10 +1092,22 @@ function withWebSocketHandle(WebSocketImpl) {
953
1092
  };
954
1093
  }
955
1094
  /**
956
- * Runs logic before each WebSocket connect or reconnect attempt.
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.
957
1107
  *
958
1108
  * @param beforeConnect - Hook receiving attempt index and abort signal.
959
1109
  * @returns An option that sets `config.webSocket.beforeConnect`.
1110
+ * @group client
960
1111
  */
961
1112
  function withWebSocketBeforeConnect(beforeConnect) {
962
1113
  return (config) => {
@@ -964,15 +1115,15 @@ function withWebSocketBeforeConnect(beforeConnect) {
964
1115
  };
965
1116
  }
966
1117
  /**
967
- * Configure WebSocket subprotocols for the connection handshake.
1118
+ * Sets the WebSocket subprotocols offered during the handshake.
968
1119
  *
969
- * `protocols` are passed as the second argument to the `WebSocket` constructor
970
- * and participate in the RFC 6455 subprotocol negotiation. They are only
971
- * meaningful when the server requires a specific protocol (for example,
972
- * `['json']` or `['v1']` to select a message encoding version).
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.
973
1123
  *
974
1124
  * @param protocols - Subprotocol list for the WebSocket constructor.
975
1125
  * @returns An option that sets `config.webSocket.protocols`.
1126
+ * @group client
976
1127
  */
977
1128
  function withWebSocketProtocols(protocols) {
978
1129
  return (config) => {
@@ -980,10 +1131,29 @@ function withWebSocketProtocols(protocols) {
980
1131
  };
981
1132
  }
982
1133
  /**
983
- * Enables WebSocket heartbeat pings and optional ACK detection.
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.
984
1153
  *
985
1154
  * @param options - Heartbeat interval, payload, timeout, and ACK predicate.
986
1155
  * @returns An option that sets `config.webSocket.heartbeat`.
1156
+ * @group client
987
1157
  */
988
1158
  function withWebSocketHeartbeat(options) {
989
1159
  return (config) => {
@@ -991,10 +1161,30 @@ function withWebSocketHeartbeat(options) {
991
1161
  };
992
1162
  }
993
1163
  /**
994
- * Configures automatic WebSocket reconnection after unexpected closes.
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.
995
1184
  *
996
1185
  * @param options - Retry budget, backoff, and `shouldReconnect` gate.
997
1186
  * @returns An option that sets `config.webSocket.reconnect`.
1187
+ * @group client
998
1188
  */
999
1189
  function withWebSocketReconnect(options) {
1000
1190
  return (config) => {
@@ -1002,10 +1192,26 @@ function withWebSocketReconnect(options) {
1002
1192
  };
1003
1193
  }
1004
1194
  /**
1005
- * Handles SSE events that lack a matching struct or fail validation.
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:
1006
1198
  *
1007
- * @param handler - Callback for invalid events; may throw or return a value.
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.
1008
1213
  * @returns An option that sets `config.sse.onInvalidEvent`.
1214
+ * @group client
1009
1215
  */
1010
1216
  function withSSEOnInvalidEvent(handler) {
1011
1217
  return (config) => {
@@ -1013,10 +1219,41 @@ function withSSEOnInvalidEvent(handler) {
1013
1219
  };
1014
1220
  }
1015
1221
  /**
1016
- * Configures automatic SSE reconnection after stream failures.
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.
1017
1253
  *
1018
1254
  * @param options - Retry budget, backoff, and `shouldReconnect` gate.
1019
1255
  * @returns An option that sets `config.sse.reconnect`.
1256
+ * @group client
1020
1257
  */
1021
1258
  function withSSEReconnect(options) {
1022
1259
  return (config) => {
@@ -1026,10 +1263,15 @@ function withSSEReconnect(options) {
1026
1263
  /**
1027
1264
  * Enables XSRF cookie-to-header token forwarding for HTTP requests.
1028
1265
  *
1029
- * Defaults to cookie `XSRF-TOKEN` and header `X-XSRF-TOKEN` when names are omitted.
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.
1030
1271
  *
1031
1272
  * @param options - Cookie/header names and optional token provider.
1032
1273
  * @returns An option that sets `config.xsrf`.
1274
+ * @group client
1033
1275
  */
1034
1276
  function withXSRF(options = {}) {
1035
1277
  return (config) => {
@@ -1041,23 +1283,35 @@ function withXSRF(options = {}) {
1041
1283
  };
1042
1284
  }
1043
1285
  /**
1044
- * Sets whether HTTP/SSE requests should include credentials (cookies).
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.
1045
1289
  *
1046
1290
  * @param value - When `true`, credentials are sent with cross-origin requests.
1047
1291
  * @returns An option that sets `config.withCredentials`.
1292
+ * @group client
1048
1293
  */
1049
1294
  function withCredentials(value) {
1050
1295
  return (config) => {
1051
1296
  config.withCredentials = value;
1052
1297
  };
1053
1298
  }
1054
- //#endregion
1055
- //#region src/interceptor/interceptor.ts
1299
+ // src/interceptor/interceptor.ts
1056
1300
  /**
1057
- * Wrap an `InterceptorFn` as a tagged `HttpInterceptor`.
1301
+ * Wraps an `InterceptorFn` as a tagged `HttpInterceptor`.
1302
+ *
1303
+ * Returns `{ kind: 'http', fn }`. Call `next(req)` (optionally with a modified
1304
+ * request) or short-circuit with `makeResponse`. `next` runs only the
1305
+ * interceptors registered after this one and then the fetch handler. HTTP
1306
+ * interceptors do not run for SSE or WebSocket commands.
1307
+ *
1308
+ * 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`
1310
+ * still reports `REQ_BUILD_FAILED`.
1058
1311
  *
1059
1312
  * @param fn - HTTP interceptor function.
1060
1313
  * @returns An `HttpInterceptor` with `kind: 'http'`.
1314
+ * @group interceptors
1061
1315
  */
1062
1316
  function createHttpInterceptor(fn) {
1063
1317
  return {
@@ -1066,10 +1320,23 @@ function createHttpInterceptor(fn) {
1066
1320
  };
1067
1321
  }
1068
1322
  /**
1069
- * Wrap an `SSEInterceptorFn` as a tagged `SSEInterceptor`.
1323
+ * Wraps an `SSEInterceptorFn` as a tagged `SSEInterceptor`.
1324
+ *
1325
+ * Returns `{ kind: 'sse', fn }`. SSE interceptors run only for SSE commands, in
1326
+ * registration order; `next(req)` opens the stream through the remaining
1327
+ * interceptors.
1328
+ *
1329
+ * 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`
1331
+ * still reports `REQ_BUILD_FAILED`.
1332
+ *
1333
+ * `EventStreamHandle` extends `AsyncDisposable`. A structural short-circuit
1334
+ * implementation must provide `[Symbol.asyncDispose](): PromiseLike<void>` and
1335
+ * connect it to the same close lifecycle.
1070
1336
  *
1071
1337
  * @param fn - SSE interceptor function.
1072
1338
  * @returns An `SSEInterceptor` with `kind: 'sse'`.
1339
+ * @group interceptors
1073
1340
  */
1074
1341
  function createSSEInterceptor(fn) {
1075
1342
  return {
@@ -1078,10 +1345,16 @@ function createSSEInterceptor(fn) {
1078
1345
  };
1079
1346
  }
1080
1347
  /**
1081
- * Wrap a `WebSocketInterceptorFn` as a tagged `WebSocketInterceptor`.
1348
+ * Wraps a `WebSocketInterceptorFn` as a tagged `WebSocketInterceptor`.
1349
+ *
1350
+ * Returns `{ kind: 'web-socket', fn }`. WebSocket interceptors run only for
1351
+ * WebSocket commands. `next` and the interceptor return a
1352
+ * {@link WebSocketSessionLike}, the interceptor-facing session. A wrapper or
1353
+ * short-circuit session must forward its `[Symbol.asyncDispose]()` teardown.
1082
1354
  *
1083
1355
  * @param fn - WebSocket interceptor function.
1084
1356
  * @returns A `WebSocketInterceptor` with `kind: 'web-socket'`.
1357
+ * @group interceptors
1085
1358
  */
1086
1359
  function createWebSocketInterceptor(fn) {
1087
1360
  return {
@@ -1104,8 +1377,7 @@ function resolveSSEInterceptors(interceptors) {
1104
1377
  function resolveWebSocketInterceptors(interceptors) {
1105
1378
  return interceptors.filter((i) => i.kind === "web-socket").map((i) => i.fn);
1106
1379
  }
1107
- //#endregion
1108
- //#region src/internal/bytes.ts
1380
+ // src/internal/bytes.ts
1109
1381
  const textDecoder = new TextDecoder();
1110
1382
  function decodeBytes(value) {
1111
1383
  return textDecoder.decode(value);
@@ -1137,8 +1409,7 @@ function concatChunks(chunks, totalLength) {
1137
1409
  }
1138
1410
  return chunksAll;
1139
1411
  }
1140
- //#endregion
1141
- //#region src/internal/inbound_representation.ts
1412
+ // src/internal/inbound_representation.ts
1142
1413
  /**
1143
1414
  * Representation chosen from the struct kind.
1144
1415
  *
@@ -1154,37 +1425,24 @@ function resolveInboundRepresentation(struct) {
1154
1425
  if (kind === "arrayBuffer") return "arraybuffer";
1155
1426
  return "json";
1156
1427
  }
1157
- //#endregion
1158
- //#region src/internal/transport_origin.ts
1159
- const transportOrigin = /* @__PURE__ */ new WeakSet();
1160
- /**
1161
- * Record that a rejection came from the transport rather than from an interceptor.
1162
- *
1163
- * A dead server and an interceptor that throws the same value are indistinguishable once both
1164
- * reach the chain's catch, so the one boundary that knows records it. The mark is kept beside the
1165
- * value rather than wrapping it, so interceptors and telemetry still see the real cause.
1166
- *
1167
- * A non-object cause cannot be marked and is therefore attributed to the extension. Transports
1168
- * reject with errors, so this only affects a custom handle that throws a primitive.
1169
- *
1170
- * @param cause - The value the transport rejected with.
1171
- * @returns The same value, unchanged.
1172
- */
1173
- function markTransportOrigin(cause) {
1174
- if (typeof cause === "object" && cause !== null) transportOrigin.add(cause);
1175
- return cause;
1176
- }
1428
+ // src/internal/transport_origin.ts
1177
1429
  /**
1178
- * Whether `cause` was recorded as coming from the transport.
1179
- *
1180
- * @param cause - A caught value.
1181
- * @returns True when the transport produced it.
1430
+ * Track transport rejections for one execution, without changing the cause seen by interceptors.
1431
+ * Primitive causes retain extension attribution. Never share this tracker between executions.
1182
1432
  */
1183
- function isTransportOrigin(cause) {
1184
- return typeof cause === "object" && cause !== null && transportOrigin.has(cause);
1433
+ function createTransportOrigin() {
1434
+ const causes = /* @__PURE__ */ new WeakSet();
1435
+ return {
1436
+ mark(cause) {
1437
+ if (typeof cause === "object" && cause !== null) causes.add(cause);
1438
+ return cause;
1439
+ },
1440
+ has(cause) {
1441
+ return typeof cause === "object" && cause !== null && causes.has(cause);
1442
+ }
1443
+ };
1185
1444
  }
1186
- //#endregion
1187
- //#region src/struct/shape.ts
1445
+ // src/struct/shape.ts
1188
1446
  function resolveObjectShape(_struct, definition) {
1189
1447
  const cached = definition.cache.resolvedShape;
1190
1448
  if (cached) return cached;
@@ -1201,20 +1459,19 @@ function readObjectShape(shape, descriptors) {
1201
1459
  function assertStruct(value, label) {
1202
1460
  if (!isStruct(value)) throw new TypeError(`${label} must be a struct`);
1203
1461
  }
1204
- //#endregion
1205
- //#region src/struct/match.ts
1206
- function matchesRuntimeValue(struct, value) {
1207
- return matchesDefinition(struct[DEFINITION], value, struct);
1462
+ // src/struct/match.ts
1463
+ function matchesRuntimeValue(struct, value, depth = 0) {
1464
+ return matchesDefinition(struct[DEFINITION], value, struct, depth);
1208
1465
  }
1209
- function selectUnionOptions(options, value) {
1466
+ function selectUnionOptions(options, value, depth = 0) {
1210
1467
  const matches = [];
1211
1468
  for (const option of options) {
1212
1469
  const runtime = option;
1213
- if (matchesRuntimeValue(runtime, value)) matches.push(runtime);
1470
+ if (matchesRuntimeValue(runtime, value, depth)) matches.push(runtime);
1214
1471
  }
1215
1472
  return matches;
1216
1473
  }
1217
- function matchesDefinition(definition, value, struct) {
1474
+ function matchesDefinition(definition, value, struct, depth = 0) {
1218
1475
  if (value === null) {
1219
1476
  if (definition.kind === "null" || definition.flags.nullable) return true;
1220
1477
  if ((definition.kind !== "literal" || definition.value !== null) && definition.kind !== "intersection" && definition.kind !== "or") return false;
@@ -1234,20 +1491,20 @@ function matchesDefinition(definition, value, struct) {
1234
1491
  case "string": return (definition.runtimeIs ?? definition.is)(value);
1235
1492
  case "literal": return Object.is(value, definition.value);
1236
1493
  case "enum": return matchesEnum(definition, value);
1237
- case "array": return Array.isArray(value) && value.every((item) => matchesRuntimeValue(definition.item, item));
1238
- case "tuple": return Array.isArray(value) && value.length === definition.items.length && definition.items.every((item, index) => matchesRuntimeValue(item, value[index]));
1239
- case "object": return isPlainObject(value) && matchesObjectValue(struct, value);
1240
- case "record": return isPlainObject(value) && Object.keys(value).every((key) => matchesRuntimeValue(definition.value, value[key]));
1241
- case "or": return definition.options.some((option) => matchesRuntimeValue(option, value));
1494
+ case "array": return depth < 256 && Array.isArray(value) && value.every((item) => matchesRuntimeValue(definition.item, item, depth + 1));
1495
+ case "tuple": return depth < 256 && Array.isArray(value) && value.length === definition.items.length && definition.items.every((item, index) => matchesRuntimeValue(item, value[index], depth + 1));
1496
+ case "object": return depth < 256 && isPlainObject(value) && matchesObjectValue(struct, value, depth);
1497
+ case "record": return depth < 256 && isPlainObject(value) && Object.keys(value).every((key) => matchesRuntimeValue(definition.value, value[key], depth + 1));
1498
+ case "or": return definition.options.some((option) => matchesRuntimeValue(option, value, depth));
1242
1499
  case "discriminatedUnion": {
1243
1500
  if (!isPlainObject(value) || !hasOwnKey(value, definition.discriminator)) return false;
1244
1501
  const target = definition.map.get(value[definition.discriminator]);
1245
- return target ? matchesRuntimeValue(target, value) : false;
1502
+ return target ? matchesRuntimeValue(target, value, depth) : false;
1246
1503
  }
1247
- case "intersection": return definition.options.every((option) => matchesRuntimeValue(option, value));
1504
+ case "intersection": return definition.options.every((option) => matchesRuntimeValue(option, value, depth));
1248
1505
  }
1249
1506
  }
1250
- function matchesObjectValue(struct, value) {
1507
+ function matchesObjectValue(struct, value, depth) {
1251
1508
  const definition = struct[DEFINITION];
1252
1509
  if (definition.kind !== "object") return true;
1253
1510
  const shape = resolveObjectShape(struct, definition);
@@ -1258,15 +1515,14 @@ function matchesObjectValue(struct, value) {
1258
1515
  if (isRequiredField(fieldDefinition)) return false;
1259
1516
  continue;
1260
1517
  }
1261
- if (!matchesRuntimeValue(field, value[key])) return false;
1518
+ if (!matchesRuntimeValue(field, value[key], depth + 1)) return false;
1262
1519
  }
1263
1520
  return true;
1264
1521
  }
1265
1522
  function isRequiredField(definition) {
1266
1523
  return !definition.flags.optional;
1267
1524
  }
1268
- //#endregion
1269
- //#region src/struct/fields.ts
1525
+ // src/struct/fields.ts
1270
1526
  function resolveStructFields(struct, definition) {
1271
1527
  const cached = definition.cache.fields;
1272
1528
  if (cached) return cached;
@@ -1306,8 +1562,7 @@ function mapAliasedObjectFields(struct, value, encodeChild, output = nullObject(
1306
1562
  }
1307
1563
  return output;
1308
1564
  }
1309
- //#endregion
1310
- //#region src/struct/encode.ts
1565
+ // src/struct/encode.ts
1311
1566
  function sameEncodedShape(leftStruct, leftValue, rightStruct, rightValue, options) {
1312
1567
  return sameWireValue(getComparableEncodedValue(leftStruct, leftValue, options), getComparableEncodedValue(rightStruct, rightValue, options));
1313
1568
  }
@@ -1319,35 +1574,35 @@ function getComparableEncodedValue(struct, value, options) {
1319
1574
  };
1320
1575
  return value;
1321
1576
  }
1322
- function writeMergedWire(output, key, encoded) {
1577
+ function writeMergedWire(output, key, encoded, depth) {
1323
1578
  if (!hasOwnKey(output, key)) {
1324
1579
  output[key] = encoded;
1325
1580
  return;
1326
1581
  }
1327
- const merged = mergeIntersectionValues(output[key], encoded);
1582
+ const merged = mergeIntersectionValues(output[key], encoded, false, depth);
1328
1583
  if (!merged.ok) throw intersectionEncodeError([key, ...merged.path]);
1329
1584
  output[key] = merged.value;
1330
1585
  }
1331
- function writeObjectFieldsMerged(struct, value, options, output) {
1586
+ function writeObjectFieldsMerged(struct, value, options, output, depth) {
1332
1587
  const definition = struct[DEFINITION];
1333
1588
  if (options.encodeObject === mapAliasedObjectFields) {
1334
1589
  for (const field of resolveStructFields(struct, definition)) {
1335
1590
  if (!hasOwnKey(value, field.key)) continue;
1336
1591
  const fieldValue = value[field.key];
1337
1592
  if (typeof fieldValue === "undefined") continue;
1338
- writeMergedWire(output, field.wireKey, encodeValue(field.struct, fieldValue, options));
1593
+ writeMergedWire(output, field.wireKey, encodeValue(field.struct, fieldValue, options, depth + 1), depth + 1);
1339
1594
  }
1340
1595
  return;
1341
1596
  }
1342
1597
  const shape = resolveObjectShape(struct, definition);
1343
- for (const key in shape) if (hasOwnKey(value, key)) writeMergedWire(output, key, encodeValue(shape[key], value[key], options));
1598
+ for (const key in shape) if (hasOwnKey(value, key)) writeMergedWire(output, key, encodeValue(shape[key], value[key], options, depth + 1), depth + 1);
1344
1599
  }
1345
- function encodeObjectFields(struct, definition, value, options, output = nullObject()) {
1600
+ function encodeObjectFields(struct, definition, value, options, depth, output = nullObject()) {
1346
1601
  const shape = resolveObjectShape(struct, definition);
1347
- for (const key in shape) if (hasOwnKey(value, key)) output[key] = encodeValue(shape[key], value[key], options);
1602
+ for (const key in shape) if (hasOwnKey(value, key)) output[key] = encodeValue(shape[key], value[key], options, depth + 1);
1348
1603
  return output;
1349
1604
  }
1350
- function encodeValue(struct, value, options = {}) {
1605
+ function encodeValue(struct, value, options = {}, depth = 0) {
1351
1606
  const definition = struct[DEFINITION];
1352
1607
  if (value === null && (definition.kind === "null" || definition.flags.nullable)) return null;
1353
1608
  if (typeof value === "undefined" && definition.flags.optional) return;
@@ -1365,40 +1620,40 @@ function encodeValue(struct, value, options = {}) {
1365
1620
  case "file":
1366
1621
  case "number":
1367
1622
  case "string": return definition.encode ? definition.encode(value) : value;
1368
- case "array": return Array.isArray(value) ? value.map((item) => encodeValue(definition.item, item, options)) : value;
1369
- case "tuple": return Array.isArray(value) ? value.map((item, index) => index < definition.items.length ? encodeValue(definition.items[index], item, options) : item) : value;
1623
+ case "array": return Array.isArray(value) ? value.map((item) => encodeValue(definition.item, item, options, depth + 1)) : value;
1624
+ case "tuple": return Array.isArray(value) ? value.map((item, index) => index < definition.items.length ? encodeValue(definition.items[index], item, options, depth + 1) : item) : value;
1370
1625
  case "record": {
1371
1626
  if (!isPlainObject(value)) return value;
1372
1627
  const output = Object.create(null);
1373
- for (const [key, entry] of Object.entries(value)) output[key] = encodeValue(definition.value, entry, options);
1628
+ for (const [key, entry] of Object.entries(value)) output[key] = encodeValue(definition.value, entry, options, depth + 1);
1374
1629
  return output;
1375
1630
  }
1376
1631
  case "object":
1377
1632
  if (!isPlainObject(value)) return value;
1378
- if (options.encodeObject) return options.encodeObject(struct, value, (fieldStruct, fieldValue) => encodeValue(fieldStruct, fieldValue, options));
1379
- return encodeObjectFields(struct, definition, value, options);
1633
+ if (options.encodeObject) return options.encodeObject(struct, value, (fieldStruct, fieldValue) => encodeValue(fieldStruct, fieldValue, options, depth + 1));
1634
+ return encodeObjectFields(struct, definition, value, options, depth);
1380
1635
  case "or": {
1381
1636
  if (!options.encodeObject && !options.selectUnionOptions && definition.uniformIdentityEncode) {
1382
1637
  for (const option of definition.options) {
1383
1638
  const runtime = option;
1384
- if (matchesRuntimeValue(runtime, value)) return encodeValue(runtime, value, options);
1639
+ if (matchesRuntimeValue(runtime, value)) return encodeValue(runtime, value, options, depth);
1385
1640
  }
1386
1641
  return value;
1387
1642
  }
1388
1643
  const matches = (options.selectUnionOptions ?? selectUnionOptions)(definition.options, value);
1389
1644
  const firstOption = matches[0];
1390
1645
  if (!firstOption) return value;
1391
- const first = encodeValue(firstOption, value, options);
1646
+ const first = encodeValue(firstOption, value, options, depth);
1392
1647
  for (let index = 1; index < matches.length; index += 1) {
1393
1648
  const option = matches[index];
1394
- if (!sameEncodedShape(firstOption, first, option, encodeValue(option, value, options), options)) throw new TypeError("ambiguous union encode: multiple union branches match with different wire output");
1649
+ if (!sameEncodedShape(firstOption, first, option, encodeValue(option, value, options, depth), options)) throw new TypeError("ambiguous union encode: multiple union branches match with different wire output");
1395
1650
  }
1396
1651
  return first;
1397
1652
  }
1398
1653
  case "discriminatedUnion":
1399
1654
  if (isPlainObject(value)) {
1400
1655
  const matched = definition.map.get(value[definition.discriminator]);
1401
- if (matched) return encodeValue(matched, value, options);
1656
+ if (matched) return encodeValue(matched, value, options, depth);
1402
1657
  }
1403
1658
  return value;
1404
1659
  case "intersection": {
@@ -1410,17 +1665,17 @@ function encodeValue(struct, value, options = {}) {
1410
1665
  if (parts.length === 0) return value;
1411
1666
  if (parts.length === 1) {
1412
1667
  const only = parts[0];
1413
- return encodeValue(only, value, options);
1668
+ return encodeValue(only, value, options, depth);
1414
1669
  }
1415
1670
  if (definition.objectSides && (!options.encodeObject || options.encodeObject === mapAliasedObjectFields) && isPlainObject(value)) {
1416
1671
  const output = nullObject();
1417
- for (const runtime of parts) writeObjectFieldsMerged(runtime, value, options, output);
1672
+ for (const runtime of parts) writeObjectFieldsMerged(runtime, value, options, output, depth);
1418
1673
  return output;
1419
1674
  }
1420
- let merged = encodeValue(parts[0], value, options);
1675
+ let merged = encodeValue(parts[0], value, options, depth);
1421
1676
  for (let index = 1; index < parts.length; index += 1) {
1422
- const encoded = encodeValue(parts[index], value, options);
1423
- const result = mergeIntersectionValues(merged, encoded);
1677
+ const encoded = encodeValue(parts[index], value, options, depth);
1678
+ const result = mergeIntersectionValues(merged, encoded, false, depth);
1424
1679
  if (!result.ok) throw intersectionEncodeError(result.path);
1425
1680
  merged = result.value;
1426
1681
  }
@@ -1428,12 +1683,25 @@ function encodeValue(struct, value, options = {}) {
1428
1683
  }
1429
1684
  }
1430
1685
  }
1431
- //#endregion
1432
- //#region src/struct/errors.ts
1686
+ // src/struct/errors.ts
1433
1687
  /**
1434
- * Aggregate of one or more `StructIssue`s from a failed parse.
1688
+ * Error returned by a failed struct parse, aggregating one or more {@link StructIssue}s and their formatters.
1689
+ *
1690
+ * `name` is `'StructError'`. Construct it with `constructor(issues: StructIssue[])`. `issues` is the `readonly` list of those issues. `message` is the issue's message for a single issue, `N struct issues: <first message>` for several, and `Struct parse failed` for none.
1691
+ *
1692
+ * Default issue messages name the received kind and omit the value, so `message` and `prettify()` can be logged. For example, `Expected number at id, received string`. `message` and the formatters use that text. The raw value stays on `StructIssue.received`; custom `errorMap` text is used verbatim and can reintroduce sensitive values.
1435
1693
  *
1436
1694
  * Use `format()`, `flatten()`, or `prettify()` to present issues to callers.
1695
+ *
1696
+ * - `struct.parse` stops at the first failure, so `issues` holds one {@link StructIssue} there.
1697
+ *
1698
+ * - `format()` returns a {@link FormattedStructError} tree. Each node has an `_errors` array, and nested keys follow `issue.path`.
1699
+ *
1700
+ * - `flatten()` returns a {@link FlattenedStructError}: messages with an empty path go to `formErrors`, the others are grouped by the first path segment in `fieldErrors`.
1701
+ *
1702
+ * - `prettify()` returns one `× path: message` line per issue. An empty path is shown as `<root>`.
1703
+ *
1704
+ * @group struct
1437
1705
  */
1438
1706
  var StructError = class extends Error {
1439
1707
  /** Issues collected for this failure, in encounter order. */
@@ -1494,6 +1762,8 @@ var StructError = class extends Error {
1494
1762
  /**
1495
1763
  * Render a multi-line, human-readable summary of all issues.
1496
1764
  *
1765
+ * Each line uses the issue message, which names the received kind and omits the value.
1766
+ *
1497
1767
  * @returns One `× path: message` line per issue.
1498
1768
  */
1499
1769
  prettify() {
@@ -1511,6 +1781,7 @@ function formatErrorTreeKey(segment) {
1511
1781
  return key === "_errors" ? "\\_errors" : key;
1512
1782
  }
1513
1783
  let activeErrorMap;
1784
+ let parkedIssue;
1514
1785
  function hasErrorMap() {
1515
1786
  return activeErrorMap !== void 0;
1516
1787
  }
@@ -1523,29 +1794,46 @@ function runWithErrorMap(map, run) {
1523
1794
  activeErrorMap = previous;
1524
1795
  }
1525
1796
  }
1797
+ /** Parks the issue for the parse entry point and returns FAILED. A decode hook receives it as `fail`. */
1798
+ function failure(candidate) {
1799
+ if (activeErrorMap) {
1800
+ const override = activeErrorMap(candidate);
1801
+ if (override !== void 0) candidate.message = override;
1802
+ }
1803
+ parkedIssue = candidate;
1804
+ return FAILED;
1805
+ }
1806
+ function parkedFailure() {
1807
+ return parkedIssue;
1808
+ }
1809
+ function restoreFailure(issue) {
1810
+ parkedIssue = issue;
1811
+ }
1526
1812
  function issue(path, code, expected, received, message) {
1527
- const candidate = {
1813
+ return {
1528
1814
  code,
1529
1815
  expected,
1530
- message: message ?? `Expected ${expected} at ${formatPath(path)}, received ${describeValue(received)}`,
1816
+ message: message ?? `Expected ${expected} at ${formatPath(path)}, received ${describeKind(received)}`,
1531
1817
  path,
1532
1818
  received
1533
1819
  };
1534
- if (activeErrorMap) {
1535
- const override = activeErrorMap(candidate);
1536
- if (override) candidate.message = override;
1537
- }
1538
- return candidate;
1539
1820
  }
1540
- //#endregion
1541
- //#region src/struct/parse.ts
1542
- function parseRootValue(struct, input, mode, useAliases = false, tagObjects = false) {
1543
- return parseEntry(struct, input, [], mode, useAliases, tagObjects);
1821
+ // src/struct/parse.ts
1822
+ const MAX_DEPTH_EXPECTED = `value nested at most 256 levels deep`;
1823
+ function parseRootValue(struct, input, mode, useAliases = false, tagObjects = false, guides) {
1824
+ return parseEntry(struct, input, [], mode, useAliases, tagObjects, guides);
1544
1825
  }
1545
- function parseEntry(struct, input, path, mode, useAliases, tagObjects) {
1826
+ function parseEntry(struct, input, path, mode, useAliases, tagObjects, guides) {
1546
1827
  const parked = parkedFailure();
1828
+ const context = tagObjects ? {
1829
+ owned: /* @__PURE__ */ new WeakSet(),
1830
+ guides: guides ?? /* @__PURE__ */ new WeakMap(),
1831
+ changes: []
1832
+ } : void 0;
1833
+ let succeeded = false;
1547
1834
  try {
1548
- const value = parseNode(struct, input, path, mode, useAliases, true, tagObjects);
1835
+ const value = parseNode(struct, input, path, mode, useAliases, true, context);
1836
+ succeeded = value !== FAILED;
1549
1837
  return value === FAILED ? {
1550
1838
  issue: parkedFailure(),
1551
1839
  ok: false
@@ -1554,10 +1842,18 @@ function parseEntry(struct, input, path, mode, useAliases, tagObjects) {
1554
1842
  value
1555
1843
  };
1556
1844
  } finally {
1845
+ if (!succeeded && context) restoreGuides(context, 0);
1557
1846
  restoreFailure(parked);
1558
1847
  }
1559
1848
  }
1560
- function parseNode(struct, input, path, mode, useAliases, reportIssues, tagObjects) {
1849
+ function restoreGuides(context, checkpoint) {
1850
+ while (context.changes.length > checkpoint) {
1851
+ const [value, previous] = context.changes.pop();
1852
+ if (previous) context.guides.set(value, previous);
1853
+ else context.guides.delete(value);
1854
+ }
1855
+ }
1856
+ function parseNode(struct, input, path, mode, useAliases, reportIssues, context, provenance) {
1561
1857
  const definition = struct[DEFINITION];
1562
1858
  if (input === void 0) {
1563
1859
  if (definition.flags.optional) return mode === "field" ? OMIT : void 0;
@@ -1569,8 +1865,18 @@ function parseNode(struct, input, path, mode, useAliases, reportIssues, tagObjec
1569
1865
  }
1570
1866
  switch (definition.kind) {
1571
1867
  case "any":
1572
- case "unknown": return input;
1573
- case "array": return parseArrayValue(definition, input, path, useAliases, reportIssues, tagObjects);
1868
+ case "unknown":
1869
+ if (provenance) provenance.passthrough = struct;
1870
+ return input;
1871
+ case "array": {
1872
+ const value = parseArrayValue(definition, input, path, useAliases, reportIssues, context);
1873
+ if (provenance && value !== FAILED) {
1874
+ const item = definition.item;
1875
+ const kind = item[DEFINITION].kind;
1876
+ if (kind === "any" || kind === "unknown") provenance.passthrough = item;
1877
+ }
1878
+ return value;
1879
+ }
1574
1880
  case "arrayBuffer":
1575
1881
  case "bigint":
1576
1882
  case "blob":
@@ -1581,15 +1887,18 @@ function parseNode(struct, input, path, mode, useAliases, reportIssues, tagObjec
1581
1887
  case "number":
1582
1888
  case "string": return parsePrimitiveValue(definition, input, path, reportIssues);
1583
1889
  case "enum": return parseEnumValue(definition, input, path, reportIssues);
1584
- case "intersection": return parseIntersectionValue(definition, input, path, useAliases, reportIssues, tagObjects);
1890
+ case "intersection": return parseIntersectionValue(definition, input, path, useAliases, reportIssues, context);
1585
1891
  case "literal": return parseLiteralValue(definition, input, path, reportIssues);
1586
- case "object": return parseObjectValue(struct, definition, input, path, useAliases, reportIssues, tagObjects);
1587
- case "or": return parseUnionValue(definition, input, path, useAliases, reportIssues, tagObjects);
1588
- case "discriminatedUnion": return parseDiscriminatedUnionValue(definition, input, path, useAliases, reportIssues, tagObjects);
1589
- case "record": return parseRecordValue(struct, definition, input, path, useAliases, reportIssues, tagObjects);
1590
- case "tuple": return parseTupleValue(definition, input, path, useAliases, reportIssues, tagObjects);
1892
+ case "object": return parseObjectValue(struct, definition, input, path, useAliases, reportIssues, context);
1893
+ case "or": return parseUnionValue(definition, input, path, useAliases, reportIssues, context, provenance);
1894
+ case "discriminatedUnion": return parseDiscriminatedUnionValue(definition, input, path, useAliases, reportIssues, context);
1895
+ case "record": return parseRecordValue(struct, definition, input, path, useAliases, reportIssues, context);
1896
+ case "tuple": return parseTupleValue(definition, input, path, useAliases, reportIssues, context);
1591
1897
  }
1592
1898
  }
1899
+ function nestingExceeded(input, path, reportIssues) {
1900
+ return reportIssues ? failure(issue([...path], "invalid_type", MAX_DEPTH_EXPECTED, input)) : FAILED;
1901
+ }
1593
1902
  function parsePrimitiveValue(definition, input, path, reportIssues) {
1594
1903
  if (!definition.is(input)) return reportIssues ? failure(issue([...path], "invalid_type", definition.expected, input)) : FAILED;
1595
1904
  return definition.decode ? definition.decode(input, [...path], failure) : input;
@@ -1600,21 +1909,23 @@ function parseEnumValue(definition, input, path, reportIssues) {
1600
1909
  function parseLiteralValue(definition, input, path, reportIssues) {
1601
1910
  return Object.is(input, definition.value) ? input : reportIssues ? failure(issue([...path], "invalid_literal", definition.expected, input)) : FAILED;
1602
1911
  }
1603
- function parseArrayValue(definition, input, path, useAliases, reportIssues, tagObjects) {
1912
+ function parseArrayValue(definition, input, path, useAliases, reportIssues, context) {
1604
1913
  if (!Array.isArray(input)) return reportIssues ? failure(issue([...path], "invalid_type", "array", input)) : FAILED;
1914
+ if (path.length >= 256) return nestingExceeded(input, path, reportIssues);
1605
1915
  const item = definition.item;
1606
1916
  const output = [];
1607
1917
  for (let index = 0; index < input.length; index += 1) {
1608
1918
  path.push(index);
1609
- const value = parseNode(item, input[index], path, "value", useAliases, reportIssues, tagObjects);
1919
+ const value = parseNode(item, input[index], path, "value", useAliases, reportIssues, context);
1610
1920
  if (value === FAILED) return FAILED;
1611
1921
  path.pop();
1612
1922
  output.push(value);
1613
1923
  }
1614
1924
  return output;
1615
1925
  }
1616
- function parseObjectValue(struct, definition, input, path, useAliases, reportIssues, tagObjects, cachedKey, cachedValue, target) {
1926
+ function parseObjectValue(struct, definition, input, path, useAliases, reportIssues, context, cachedKey, cachedValue, target) {
1617
1927
  if (!isPlainObject(input)) return reportIssues ? failure(issue([...path], "invalid_type", "object", input)) : FAILED;
1928
+ if (path.length >= 256) return nestingExceeded(input, path, reportIssues);
1618
1929
  const output = target ?? nullObject();
1619
1930
  const fields = resolveStructFields(struct, definition);
1620
1931
  for (let index = 0; index < fields.length; index += 1) {
@@ -1622,12 +1933,12 @@ function parseObjectValue(struct, definition, input, path, useAliases, reportIss
1622
1933
  const inputKey = useAliases ? field.wireKey : field.key;
1623
1934
  const inputValue = cachedKey === inputKey ? cachedValue : hasOwnKey(input, inputKey) ? input[inputKey] : void 0;
1624
1935
  path.push(field.key);
1625
- const value = parseNode(field.struct, inputValue, path, "field", useAliases, reportIssues, tagObjects);
1936
+ const value = parseNode(field.struct, inputValue, path, "field", useAliases, reportIssues, context);
1626
1937
  if (value === FAILED) return FAILED;
1627
1938
  path.pop();
1628
1939
  if (value !== OMIT) {
1629
1940
  if (target !== void 0 && hasOwnKey(output, field.key)) {
1630
- const merged = mergeIntersectionValues(output[field.key], value, tagObjects);
1941
+ const merged = mergeIntersectionValues(output[field.key], value, context !== void 0, path.length + 1, context);
1631
1942
  if (!merged.ok) return reportIssues ? failure(issue([
1632
1943
  ...path,
1633
1944
  field.key,
@@ -1637,46 +1948,56 @@ function parseObjectValue(struct, definition, input, path, useAliases, reportIss
1637
1948
  } else output[field.key] = value;
1638
1949
  }
1639
1950
  }
1640
- if (tagObjects && !target) tagOwnNode(output, struct);
1951
+ if (context && !target) {
1952
+ context.owned.add(output);
1953
+ tagOwnNode(output, struct);
1954
+ }
1641
1955
  return output;
1642
1956
  }
1643
- function parseRecordValue(struct, definition, input, path, useAliases, reportIssues, tagObjects) {
1957
+ function parseRecordValue(struct, definition, input, path, useAliases, reportIssues, context) {
1644
1958
  if (!isPlainObject(input)) return reportIssues ? failure(issue([...path], "invalid_type", "record", input)) : FAILED;
1959
+ if (path.length >= 256) return nestingExceeded(input, path, reportIssues);
1645
1960
  const item = definition.value;
1646
1961
  const output = Object.create(null);
1647
1962
  for (const key of Object.keys(input)) {
1648
1963
  path.push(key);
1649
- const value = parseNode(item, input[key], path, "field", useAliases, reportIssues, tagObjects);
1964
+ const value = parseNode(item, input[key], path, "field", useAliases, reportIssues, context);
1650
1965
  if (value === FAILED) return FAILED;
1651
1966
  path.pop();
1652
1967
  if (value !== OMIT) output[key] = value;
1653
1968
  }
1654
- if (tagObjects) tagOwnNode(output, struct);
1969
+ if (context) {
1970
+ context.owned.add(output);
1971
+ tagOwnNode(output, struct);
1972
+ }
1655
1973
  return output;
1656
1974
  }
1657
- function parseTupleValue(definition, input, path, useAliases, reportIssues, tagObjects) {
1975
+ function parseTupleValue(definition, input, path, useAliases, reportIssues, context) {
1658
1976
  if (!Array.isArray(input) || input.length !== definition.items.length) return reportIssues ? failure(issue([...path], "invalid_type", `tuple of length ${definition.items.length}`, input)) : FAILED;
1977
+ if (path.length >= 256) return nestingExceeded(input, path, reportIssues);
1659
1978
  const output = [];
1660
1979
  for (let index = 0; index < definition.items.length; index += 1) {
1661
1980
  path.push(index);
1662
- const value = parseNode(definition.items[index], input[index], path, "value", useAliases, reportIssues, tagObjects);
1981
+ const value = parseNode(definition.items[index], input[index], path, "value", useAliases, reportIssues, context);
1663
1982
  if (value === FAILED) return FAILED;
1664
1983
  path.pop();
1665
1984
  output.push(value);
1666
1985
  }
1667
1986
  return output;
1668
1987
  }
1669
- function parseUnionValue(definition, input, path, useAliases, reportIssues, tagObjects) {
1988
+ function parseUnionValue(definition, input, path, useAliases, reportIssues, context, provenance) {
1670
1989
  const reportCandidateIssues = reportIssues && hasErrorMap();
1671
1990
  const depth = path.length;
1672
1991
  for (const option of definition.options) {
1673
- const value = parseNode(option, input, path, "value", useAliases, reportCandidateIssues, tagObjects);
1992
+ const checkpoint = context?.changes.length ?? 0;
1993
+ const value = parseNode(option, input, path, "value", useAliases, reportCandidateIssues, context, provenance);
1674
1994
  if (value !== FAILED) return value;
1995
+ if (context) restoreGuides(context, checkpoint);
1675
1996
  path.length = depth;
1676
1997
  }
1677
1998
  return reportIssues ? failure(issue([...path], "invalid_union", definition.expected, input)) : FAILED;
1678
1999
  }
1679
- function parseDiscriminatedUnionValue(definition, input, path, useAliases, reportIssues, tagObjects) {
2000
+ function parseDiscriminatedUnionValue(definition, input, path, useAliases, reportIssues, context) {
1680
2001
  if (!isPlainObject(input)) return reportIssues ? failure(issue([...path], "invalid_type", "object", input)) : FAILED;
1681
2002
  if (!useAliases) {
1682
2003
  const value = hasOwnKey(input, definition.discriminator) ? input[definition.discriminator] : void 0;
@@ -1684,7 +2005,7 @@ function parseDiscriminatedUnionValue(definition, input, path, useAliases, repor
1684
2005
  const target = definition.map.get(value);
1685
2006
  if (!target) return reportIssues ? failure(issue([...path, definition.discriminator], "invalid_union", definition.expected, value)) : FAILED;
1686
2007
  const runtime = target;
1687
- return parseObjectValue(runtime, runtime[DEFINITION], input, path, false, reportIssues, tagObjects, definition.discriminator, value);
2008
+ return parseObjectValue(runtime, runtime[DEFINITION], input, path, false, reportIssues, context, definition.discriminator, value);
1688
2009
  }
1689
2010
  for (const wireKey of definition.discriminatorWireKeys ?? []) {
1690
2011
  if (!hasOwnKey(input, wireKey)) continue;
@@ -1692,79 +2013,102 @@ function parseDiscriminatedUnionValue(definition, input, path, useAliases, repor
1692
2013
  if (value === void 0) return reportIssues ? failure(issue([...path, definition.discriminator], "missing_key", definition.expected, void 0)) : FAILED;
1693
2014
  const target = definition.map.get(value);
1694
2015
  if (!target || definition.wireKeyByValue?.get(value) !== wireKey) return reportIssues ? failure(issue([...path, definition.discriminator], "invalid_union", definition.expected, value)) : FAILED;
1695
- return parseObjectValue(target, target[DEFINITION], input, path, true, reportIssues, tagObjects, wireKey, value);
2016
+ return parseObjectValue(target, target[DEFINITION], input, path, true, reportIssues, context, wireKey, value);
1696
2017
  }
1697
2018
  return reportIssues ? failure(issue([...path, definition.discriminator], "missing_key", definition.expected, void 0)) : FAILED;
1698
2019
  }
1699
- function parseIntersectionValue(definition, input, path, useAliases, reportIssues, tagObjects) {
2020
+ function parseIntersectionValue(definition, input, path, useAliases, reportIssues, context) {
1700
2021
  if (definition.objectSides && isPlainObject(input)) {
1701
2022
  const merged = nullObject();
1702
2023
  for (const option of definition.options) {
1703
2024
  const runtime = option;
1704
- if (parseObjectValue(runtime, runtime[DEFINITION], input, path, useAliases, reportIssues, tagObjects, void 0, void 0, merged) === FAILED) return FAILED;
2025
+ if (parseObjectValue(runtime, runtime[DEFINITION], input, path, useAliases, reportIssues, context, void 0, void 0, merged) === FAILED) return FAILED;
2026
+ }
2027
+ if (context) {
2028
+ context.owned.add(merged);
2029
+ tagOwnNode(merged, guideTag(definition.options));
1705
2030
  }
1706
- if (tagObjects) tagOwnNode(merged, guideTag(definition.options));
1707
2031
  return merged;
1708
2032
  }
1709
2033
  const contributed = [];
2034
+ const passthroughs = [];
1710
2035
  for (const option of definition.options) {
1711
2036
  const runtime = option;
1712
- const value = parseNode(runtime, input, path, "value", useAliases, reportIssues, tagObjects);
2037
+ const provenance = context ? {} : void 0;
2038
+ const value = parseNode(runtime, input, path, "value", useAliases, reportIssues, context, provenance);
1713
2039
  if (value === FAILED) return FAILED;
1714
- if (!isIntersectionIdentity(runtime)) contributed.push(value);
2040
+ if (!isIntersectionIdentity(runtime)) {
2041
+ contributed.push(value);
2042
+ if (provenance?.passthrough) passthroughs.push(provenance.passthrough);
2043
+ }
1715
2044
  }
1716
2045
  if (contributed.length === 0) return input;
1717
2046
  let merged = contributed[0];
1718
2047
  for (let index = 1; index < contributed.length; index += 1) {
1719
- const mergedValue = mergeIntersectionValues(merged, contributed[index], tagObjects);
2048
+ const mergedValue = mergeIntersectionValues(merged, contributed[index], context !== void 0, path.length, context);
1720
2049
  if (!mergedValue.ok) return reportIssues ? failure(issue([...path, ...mergedValue.path], "invalid_intersection", "compatible intersection value", mergedValue.received)) : FAILED;
1721
2050
  merged = mergedValue.value;
1722
2051
  }
1723
- if (tagObjects) rememberPassthroughBranches(definition, input, merged);
1724
- return merged;
1725
- }
1726
- function rememberPassthroughBranches(definition, input, merged) {
1727
- const guides = [];
1728
- for (const option of definition.options) {
1729
- const runtime = option;
1730
- if (isIntersectionIdentity(runtime)) continue;
1731
- const branch = passthroughBranch(runtime, input);
1732
- if (branch) guides.push(branch);
1733
- }
1734
- if (guides.length === 0) return;
1735
- if (isPlainObject(merged)) {
1736
- attachPassthroughGuides(merged, guides);
1737
- return;
2052
+ if (context && passthroughs.length > 0) {
2053
+ if (isPlainObject(merged)) attachPassthroughGuides(merged, passthroughs, context);
2054
+ else if (Array.isArray(merged)) {
2055
+ for (const item of merged) if (isPlainObject(item)) attachPassthroughGuides(item, passthroughs, context);
2056
+ }
1738
2057
  }
1739
- if (!Array.isArray(merged)) return;
1740
- for (const item of merged) if (isPlainObject(item)) attachPassthroughGuides(item, guides);
2058
+ return merged;
1741
2059
  }
1742
- function attachPassthroughGuides(value, guides) {
1743
- const current = readOwnGuide(value);
2060
+ function attachPassthroughGuides(value, guides, context) {
2061
+ const current = readOwnGuide(value, context.guides);
1744
2062
  const next = guideTag([...current ? guideMembers(current) : [], ...guides]);
1745
- /* istanbul ignore if -- @preserve guideTag returns a tag whenever at least one passthrough guide is present */
1746
- if (!next) return;
1747
- defineStructTag(value, next);
1748
- }
1749
- function passthroughBranch(struct, value) {
1750
- const definition = struct[DEFINITION];
1751
- if (definition.kind === "or") {
1752
- const selected = selectUnionOptions(definition.options, value)[0];
1753
- if (!selected) return;
1754
- const kind = selected[DEFINITION].kind;
1755
- return kind === "any" || kind === "unknown" ? selected : void 0;
1756
- }
1757
- if (definition.kind === "array" && Array.isArray(value)) {
1758
- const item = definition.item;
1759
- const kind = item[DEFINITION].kind;
1760
- return kind === "any" || kind === "unknown" ? item : void 0;
2063
+ if (context.owned.has(value)) tagOwnNode(value, next);
2064
+ else {
2065
+ context.changes.push([value, context.guides.get(value)]);
2066
+ context.guides.set(value, next);
1761
2067
  }
1762
2068
  }
1763
- //#endregion
1764
- //#region src/struct/introspection.ts
2069
+ // src/struct/introspection.ts
1765
2070
  function isObjectStruct(value) {
1766
2071
  return isStruct(value) && value[DEFINITION].kind === "object";
1767
2072
  }
2073
+ /**
2074
+ * Parses an unknown value with a struct and returns an error-first tuple.
2075
+ *
2076
+ * - **struct** is the struct to parse with. A value that is not a struct throws `TypeError`.
2077
+ *
2078
+ * - **value** is the unknown input.
2079
+ *
2080
+ * - **options.aliases** defaults to `false`: object keys are read by their **logical** names (`displayName`). With `true` they are read by their wire names (`display_name`), the way HTTP/JSON codecs read them. The output always uses logical names.
2081
+ *
2082
+ * - **options.errorMap** is an {@link ErrorMap} that rewrites issue messages for this call only, including bigint/date conversion failures from a struct created by another Core copy. Any string, including `''`, replaces the message; only `undefined` keeps the default. Custom text is used verbatim.
2083
+ *
2084
+ * - Parsing is fail-fast: it stops at the first failure, so the error holds one issue. Unknown object keys are stripped from the output, and the input is not modified.
2085
+ *
2086
+ * - At most **256 nested containers** (`object`, `array`, `record`, or `tuple`) are visited along a path, counting the root container as the first. The next container fails with `invalid_type` at its logical path, with `expected: 'value nested at most 256 levels deep'`. In a union, later candidates may still succeed; if none match, the union reports `invalid_union`. Cycles through declared container fields also reach this limit; shared non-cyclic children are allowed. Union and intersection branches do not add a container level, and `any`/`unknown` values remain opaque rather than being traversed.
2087
+ *
2088
+ * ```ts
2089
+ * import { struct } from "@defjs/core";
2090
+ *
2091
+ * const User = struct.object({ id: struct.number(), displayName: struct.string() });
2092
+ *
2093
+ * function readUser() {
2094
+ * const [error, user] = struct.parse(User, { id: 1, displayName: "Ada" });
2095
+ * if (error) {
2096
+ * console.error(error.prettify());
2097
+ * return;
2098
+ * }
2099
+ * console.log(user.displayName);
2100
+ * }
2101
+ *
2102
+ * readUser();
2103
+ * ```
2104
+ *
2105
+ * `any` and `unknown` do not traverse their values, but an intersection may recursively merge two distinct object or array values returned by those leaves. This merge shares the same 256-container budget with its enclosing logical path. Exceeding it produces `invalid_intersection` at the rejected container's logical path. Identical references are retained without traversal, including a shared cyclic value.
2106
+ *
2107
+ * @returns `[null, output]` or `[StructError, undefined]`. Validation failures do not throw. Exceptions from caller-provided getters and `errorMap` callbacks propagate unchanged.
2108
+ * @group struct
2109
+ * @see {@link Struct}
2110
+ * @see {@link ParseResult}
2111
+ */
1768
2112
  function parseStructTuple(struct, value, options) {
1769
2113
  assertStruct(struct, "struct");
1770
2114
  const runtime = struct;
@@ -1776,26 +2120,27 @@ function parseStructTuple(struct, value, options) {
1776
2120
  }
1777
2121
  function parseStructValue(struct, value, options) {
1778
2122
  assertStruct(struct, "struct");
1779
- const result = parseRootValue(struct, value, "value", options?.useAliases, options?.tagObjects === true);
2123
+ const result = parseRootValue(struct, value, "value", options?.useAliases, options?.tagObjects === true, options?.guides);
1780
2124
  if (!result.ok) throw new StructError([result.issue]);
1781
2125
  return result.value;
1782
2126
  }
1783
- //#endregion
1784
- //#region src/internal/endpoint_input.ts
1785
- async function parseEndpointInput(struct, input) {
2127
+ // src/internal/endpoint_input.ts
2128
+ function parseEndpointInput(struct, input, guides) {
1786
2129
  if (!struct) return input;
1787
- return parseStructValue(struct, input, { tagObjects: true });
2130
+ return parseStructValue(struct, input, {
2131
+ guides,
2132
+ tagObjects: true
2133
+ });
1788
2134
  }
1789
- //#endregion
1790
- //#region src/struct/codec/common.ts
2135
+ // src/struct/codec/common.ts
1791
2136
  const ALIAS_ENCODE_OPTIONS = { encodeObject: mapAliasedObjectFields };
1792
- function encodeObjectByAlias(struct, value, label = "json") {
2137
+ function encodeObjectByAlias(struct, value, label = "json", depth = 0) {
1793
2138
  const definition = struct[DEFINITION];
1794
2139
  if (value === null && definition.flags.nullable) return null;
1795
2140
  if (typeof value === "undefined" && definition.flags.optional) return;
1796
- if (!isObjectStruct(struct)) return encodeAliasedField(struct, value, label);
2141
+ if (!isObjectStruct(struct)) return encodeAliasedField(struct, value, label, depth);
1797
2142
  assertPlainObject(value, `${label} encode expects object value`);
1798
- return mapAliasedObjectFields(struct, value, (fieldStruct, fieldValue) => encodeAliasedField(fieldStruct, fieldValue, label));
2143
+ return mapAliasedObjectFields(struct, value, (fieldStruct, fieldValue) => encodeAliasedField(fieldStruct, fieldValue, label, depth + 1));
1799
2144
  }
1800
2145
  function decodeObjectByAlias(struct, value) {
1801
2146
  return parseStructValue(struct, value, { useAliases: true });
@@ -1803,20 +2148,18 @@ function decodeObjectByAlias(struct, value) {
1803
2148
  function assertPlainObject(value, message) {
1804
2149
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(message);
1805
2150
  }
1806
- function encodeAliasedField(struct, value, label) {
1807
- if (isObjectStruct(struct)) return encodeObjectByAlias(struct, value, label);
1808
- return encodeValue(struct, value, ALIAS_ENCODE_OPTIONS);
2151
+ function encodeAliasedField(struct, value, label, depth) {
2152
+ if (isObjectStruct(struct)) return encodeObjectByAlias(struct, value, label, depth);
2153
+ return encodeValue(struct, value, ALIAS_ENCODE_OPTIONS, depth);
1809
2154
  }
1810
- //#endregion
1811
- //#region src/struct/codec/json.ts
2155
+ // src/struct/codec/json.ts
1812
2156
  function encodeJson(struct, value) {
1813
2157
  return encodeObjectByAlias(struct, value, "json");
1814
2158
  }
1815
2159
  function decodeJson(struct, value) {
1816
2160
  return decodeObjectByAlias(struct, value);
1817
2161
  }
1818
- //#endregion
1819
- //#region src/internal/type_guards.ts
2162
+ // src/internal/type_guards.ts
1820
2163
  function isBlob(value) {
1821
2164
  return typeof Blob !== "undefined" && value instanceof Blob;
1822
2165
  }
@@ -1829,8 +2172,7 @@ function isURLSearchParams(value) {
1829
2172
  function isReadableStream(value) {
1830
2173
  return typeof ReadableStream !== "undefined" && value instanceof ReadableStream;
1831
2174
  }
1832
- //#endregion
1833
- //#region src/http/transport/body.ts
2175
+ // src/http/transport/body.ts
1834
2176
  const BODY_CONTENT_TYPE_SOURCE = "bodyContentTypeSource";
1835
2177
  function serializeHttpBody(body) {
1836
2178
  switch (true) {
@@ -1850,24 +2192,13 @@ function serializeHttpBody(body) {
1850
2192
  }
1851
2193
  function applyRequestContentType(request, headers) {
1852
2194
  const body = request.body;
1853
- if (typeof body === "undefined") return;
1854
2195
  if (isFormData(body)) {
1855
2196
  headers.delete("Content-Type");
1856
2197
  return;
1857
2198
  }
1858
- const hasBodyContentTypeSource = Object.hasOwn(request, BODY_CONTENT_TYPE_SOURCE);
1859
- const bodyContentType = !hasBodyContentTypeSource || request.bodyContentTypeSource === body ? request.bodyContentType : void 0;
1860
- if (bodyContentType === null) {
1861
- headers.delete("Content-Type");
1862
- return;
1863
- }
1864
- if (typeof bodyContentType === "string") {
1865
- headers.set("Content-Type", bodyContentType);
1866
- return;
1867
- }
1868
- if (hasBodyContentTypeSource && request.bodyContentTypeSource === body && headers.has("content-type")) return;
1869
- const detectedType = resolveHttpBodyContentType(body);
1870
- if (detectedType) headers.set("Content-Type", detectedType);
2199
+ if (body === void 0 || headers.has("Content-Type")) return;
2200
+ const contentType = (!Object.hasOwn(request, BODY_CONTENT_TYPE_SOURCE) || request.bodyContentTypeSource === body) && request.bodyContentType !== void 0 ? request.bodyContentType : resolveHttpBodyContentType(body);
2201
+ if (contentType) headers.set("Content-Type", contentType);
1871
2202
  }
1872
2203
  function resolveHttpBodyContentType(body) {
1873
2204
  switch (true) {
@@ -1885,25 +2216,27 @@ function resolveHttpBodyContentType(body) {
1885
2216
  default: return null;
1886
2217
  }
1887
2218
  }
1888
- //#endregion
1889
- //#region src/internal/outbound_request.ts
1890
- function runOutboundBuilder(input, builder) {
1891
- if (!builder) return input === void 0 ? {} : input;
2219
+ // src/internal/outbound_request.ts
2220
+ function runOutboundBuilder(input, builder, inputDeclared = input !== void 0) {
2221
+ if (builder === void 0) {
2222
+ if (inputDeclared) throw new TypeError("An endpoint with input requires an explicit builder");
2223
+ return {};
2224
+ }
1892
2225
  const result = builder(input);
1893
2226
  return result === void 0 ? {} : result;
1894
2227
  }
1895
- function materializeOutboundRequest(shape, transport) {
2228
+ function materializeOutboundRequest(shape, transport, guides) {
1896
2229
  if (!isPlainObject(shape)) throw new TypeError("builder must return an object");
1897
2230
  if (transport === "webSocket") {
1898
2231
  if (shape["headers"] !== void 0) throw new TypeError("WebSocket request does not support headers");
1899
2232
  if (shape["body"] !== void 0) throw new TypeError("WebSocket request does not support body");
1900
2233
  }
1901
2234
  const request = {};
1902
- if (shape["path"] !== void 0) request.params = materializePath(shape["path"]);
1903
- if (shape["query"] !== void 0) request.query = materializeQuery(shape["query"]);
1904
- if (shape["headers"] !== void 0) request.headers = materializeHeaders(shape["headers"]);
2235
+ if (shape["path"] !== void 0) request.params = materializePath(shape["path"], guides);
2236
+ if (shape["query"] !== void 0) request.query = materializeQuery(shape["query"], guides);
2237
+ if (shape["headers"] !== void 0) request.headers = materializeHeaders(shape["headers"], guides);
1905
2238
  if (shape["body"] !== void 0) {
1906
- const body = materializeBody(shape["body"]);
2239
+ const body = materializeBody(shape["body"], guides);
1907
2240
  request.body = body.body;
1908
2241
  if (body.defaultContentType) request.defaultContentType = body.defaultContentType;
1909
2242
  }
@@ -1924,51 +2257,51 @@ const LEAF_KINDS = /* @__PURE__ */ new Set([
1924
2257
  "string",
1925
2258
  "unknown"
1926
2259
  ]);
1927
- function encodeOutbound(value) {
1928
- return encodeGuided(value, void 0);
2260
+ function encodeOutbound(value, guides) {
2261
+ return encodeGuided(value, void 0, void 0, guides);
1929
2262
  }
1930
- function encodeGuided(value, guide, path = []) {
1931
- if (guide !== void 0 && !isGuideSet(guide) && isPassthroughStruct(guide) && (!isPlainObject(value) || readOwnGuide(value))) return value;
2263
+ function encodeGuided(value, guide, path = [], guides) {
2264
+ if (guide !== void 0 && !isGuideSet(guide) && isPassthroughStruct(guide) && (!isPlainObject(value) || readOwnGuide(value, guides))) return value;
1932
2265
  if (isPlainObject(value)) {
1933
- const tagged = readOwnGuide(value);
1934
- if (tagged) return encodeOwnGuide(value, guide === void 0 ? tagged : narrowToParent(tagged, guide, value, path), path);
1935
- }
1936
- if (isGuideSet(guide)) return encodeMembers(value, guide.members, path);
1937
- if (guide?.[DEFINITION].kind === "intersection") return encodeIntersectionGuide(value, guide, path);
1938
- if (guide?.[DEFINITION].kind === "or") return encodeMatchingUnion(value, guide, path);
1939
- if (Array.isArray(value)) return encodeGuidedArray(value, guide, path);
1940
- if (isPlainObject(value)) return encodeGuidedObject(value, guide, path);
2266
+ const tagged = readOwnGuide(value, guides);
2267
+ if (tagged) return encodeOwnGuide(value, guide === void 0 ? tagged : narrowToParent(tagged, guide, value, path, guides), path, guides);
2268
+ }
2269
+ if (isGuideSet(guide)) return encodeMembers(value, guide.members, path, guides);
2270
+ if (guide?.[DEFINITION].kind === "intersection") return encodeIntersectionGuide(value, guide, path, guides);
2271
+ if (guide?.[DEFINITION].kind === "or") return encodeMatchingUnion(value, guide, path, guides);
2272
+ if (Array.isArray(value)) return encodeGuidedArray(value, guide, path, guides);
2273
+ if (isPlainObject(value)) return encodeGuidedObject(value, guide, path, guides);
1941
2274
  return encodeGuidedLeaf(value, guide);
1942
2275
  }
1943
- function narrowToParent(own, parent, value, path) {
1944
- const derived = resolveGuideMembers(guideMembers(parent), value, path, true);
2276
+ function narrowToParent(own, parent, value, path, guides) {
2277
+ const derived = resolveGuideMembers(guideMembers(parent), value, path, true, guides);
1945
2278
  const shared = [];
1946
2279
  for (const member of guideMembers(own)) if (derived.includes(member)) shared.push(member);
1947
2280
  if (shared.length === 0) return own;
1948
2281
  return guideTag(shared);
1949
2282
  }
1950
- function encodeOwnGuide(value, guide, path) {
1951
- if (isGuideSet(guide)) return encodeMembers(value, guide.members, path);
1952
- return encodeTaggedObject(value, guide, path);
2283
+ function encodeOwnGuide(value, guide, path, guides) {
2284
+ if (isGuideSet(guide)) return encodeMembers(value, guide.members, path, guides);
2285
+ return encodeTaggedObject(value, guide, path, guides);
1953
2286
  }
1954
- function encodeIntersectionGuide(value, guide, path) {
2287
+ function encodeIntersectionGuide(value, guide, path, guides) {
1955
2288
  const definition = guide[DEFINITION];
1956
- if (definition.objectSides && isPlainObject(value)) return encodeMembers(value, definition.options, path);
1957
- if (Array.isArray(value)) return encodeMembers(value, [guide], path);
1958
- return encodeGuided(value, void 0, path);
1959
- }
1960
- function encodeMembers(value, members, path) {
1961
- const resolved = resolveGuideMembers(members, value, path, Array.isArray(value));
1962
- if (Array.isArray(value)) return encodeArrayMembers(value, resolved, path);
1963
- if (isPlainObject(value)) return encodeObjectNode(value, resolved, path);
2289
+ if (definition.objectSides && isPlainObject(value)) return encodeMembers(value, definition.options, path, guides);
2290
+ if (Array.isArray(value)) return encodeMembers(value, [guide], path, guides);
2291
+ return encodeGuided(value, void 0, path, guides);
2292
+ }
2293
+ function encodeMembers(value, members, path, guides) {
2294
+ const resolved = resolveGuideMembers(members, value, path, Array.isArray(value), guides);
2295
+ if (Array.isArray(value)) return encodeArrayMembers(value, resolved, path, guides);
2296
+ if (isPlainObject(value)) return encodeObjectNode(value, resolved, path, guides);
1964
2297
  return encodeLeafMembers(value, resolved);
1965
2298
  }
1966
- function resolveGuideMembers(members, value, path, unwrapIntersections) {
2299
+ function resolveGuideMembers(members, value, path, unwrapIntersections, guides) {
1967
2300
  const resolved = [];
1968
- for (const member of members) appendResolved(member, value, path, resolved, unwrapIntersections);
2301
+ for (const member of members) appendResolved(member, value, path, resolved, unwrapIntersections, guides);
1969
2302
  return resolved;
1970
2303
  }
1971
- function appendResolved(struct, value, path, out, unwrapIntersections) {
2304
+ function appendResolved(struct, value, path, out, unwrapIntersections, guides) {
1972
2305
  const definition = struct[DEFINITION];
1973
2306
  switch (definition.kind) {
1974
2307
  case "or": {
@@ -1976,23 +2309,23 @@ function appendResolved(struct, value, path, out, unwrapIntersections) {
1976
2309
  const first = matches[0];
1977
2310
  if (!first) return;
1978
2311
  if (matches.length > 1) {
1979
- const encoded = encodeGuided(value, first, path);
1980
- for (let index = 1; index < matches.length; index += 1) if (!sameWireValue(encoded, encodeGuided(value, matches[index], path))) throw new TypeError("ambiguous union encode: multiple union branches match with different wire output");
2312
+ const encoded = encodeGuided(value, first, path, guides);
2313
+ for (let index = 1; index < matches.length; index += 1) if (!sameWireValue(encoded, encodeGuided(value, matches[index], path, guides))) throw new TypeError("ambiguous union encode: multiple union branches match with different wire output");
1981
2314
  }
1982
- appendResolved(first, value, path, out, unwrapIntersections);
2315
+ appendResolved(first, value, path, out, unwrapIntersections, guides);
1983
2316
  return;
1984
2317
  }
1985
2318
  case "discriminatedUnion": {
1986
2319
  if (!isPlainObject(value)) return;
1987
2320
  const matched = definition.map.get(value[definition.discriminator]);
1988
- if (matched) appendResolved(matched, value, path, out, unwrapIntersections);
2321
+ if (matched) appendResolved(matched, value, path, out, unwrapIntersections, guides);
1989
2322
  return;
1990
2323
  }
1991
2324
  case "intersection":
1992
2325
  if (definition.objectSides || unwrapIntersections) for (const option of definition.options) {
1993
2326
  const runtime = option;
1994
2327
  if (unwrapIntersections && isIntersectionIdentity(runtime)) continue;
1995
- appendResolved(runtime, value, path, out, unwrapIntersections);
2328
+ appendResolved(runtime, value, path, out, unwrapIntersections, guides);
1996
2329
  }
1997
2330
  return;
1998
2331
  default: out.push(struct);
@@ -2010,8 +2343,8 @@ function encodeLeafMembers(value, members) {
2010
2343
  for (const member of members) if (LEAF_KINDS.has(member[DEFINITION].kind)) return encodeGuidedLeaf(value, member);
2011
2344
  return coerceJsonLeaf(value);
2012
2345
  }
2013
- function materializePath(value) {
2014
- const encoded = encodeOutbound(value);
2346
+ function materializePath(value, guides) {
2347
+ const encoded = encodeOutbound(value, guides);
2015
2348
  if (!isPlainObject(encoded)) throw new TypeError("path value must be an object");
2016
2349
  const params = Object.create(null);
2017
2350
  for (const [key, item] of Object.entries(encoded)) {
@@ -2021,34 +2354,41 @@ function materializePath(value) {
2021
2354
  }
2022
2355
  return params;
2023
2356
  }
2024
- function materializeQuery(value) {
2357
+ function materializeQuery(value, guides) {
2025
2358
  if (isURLSearchParams(value)) return value;
2026
- const encoded = encodeOutbound(value);
2359
+ const encoded = encodeOutbound(value, guides);
2027
2360
  if (!isPlainObject(encoded)) throw new TypeError("query value must be an object or URLSearchParams");
2028
2361
  rejectNullValues(encoded, "query");
2029
2362
  return encoded;
2030
2363
  }
2031
- function materializeHeaders(value) {
2364
+ function materializeHeaders(value, guides) {
2032
2365
  if (typeof Headers !== "undefined" && value instanceof Headers) return value;
2033
2366
  if (Array.isArray(value)) return value;
2034
- const encoded = encodeOutbound(value);
2367
+ const encoded = encodeOutbound(value, guides);
2035
2368
  if (!isPlainObject(encoded)) throw new TypeError("headers value must be a header record or HeadersInit");
2036
2369
  for (const [key, item] of Object.entries(encoded)) assertHeaderValue(item, key);
2037
2370
  return encoded;
2038
2371
  }
2039
- function materializeBody(body) {
2040
- const codec = codecOf(body);
2041
- if (codec && isPlainObject(body)) return { body: encodeCodecBody(body, codec) };
2372
+ function materializeBody(body, guides) {
2373
+ const codec = codecOf(body, guides);
2374
+ if (codec && isPlainObject(body)) return { body: encodeCodecBody(body, codec, guides) };
2042
2375
  if (isFormData(body) || isURLSearchParams(body) || isBlob(body) || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || isReadableStream(body)) return { body };
2043
2376
  if (typeof body === "string") return { body };
2044
- const encoded = encodeOutbound(body);
2045
2377
  return {
2046
- body: JSON.stringify(encoded),
2378
+ body: JSON.stringify(encodeOutbound(body, guides), rejectNonJson),
2047
2379
  defaultContentType: "application/json"
2048
2380
  };
2049
2381
  }
2050
- function encodeCodecBody(body, codec) {
2051
- const encoded = encodeOutbound(body);
2382
+ function rejectNonJson(key, value, source = "body") {
2383
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "number" && !Number.isFinite(value) || typeof value === "object" && value !== null && !Array.isArray(value) && !isPlainObject(value)) {
2384
+ const kind = typeof value === "number" ? String(value) : typeof value === "object" ? value.constructor?.name || Object.prototype.toString.call(value).slice(8, -1) : typeof value;
2385
+ const advice = source === "body" ? "Send JSON data, a string, bytes, Blob, FormData, URLSearchParams or a Web ReadableStream (ReadableStream.from(iterable), Readable.toWeb(readable))." : "Send JSON data.";
2386
+ throw new TypeError(`${source}${key ? ` field "${key}"` : ""} is not JSON data: ${kind}. ${advice}`);
2387
+ }
2388
+ return value;
2389
+ }
2390
+ function encodeCodecBody(body, codec, guides) {
2391
+ const encoded = encodeOutbound(body, guides);
2052
2392
  if (codec === "formData") {
2053
2393
  const form = new FormData();
2054
2394
  for (const [key, value] of Object.entries(encoded)) appendFormValue(form, key, value, codec);
@@ -2074,8 +2414,8 @@ function appendFormValue(target, key, value, codec) {
2074
2414
  }
2075
2415
  throw new TypeError(`${codec} field "${key}" does not support nested values`);
2076
2416
  }
2077
- function encodeTaggedObject(value, struct, path) {
2078
- return encodeObjectNode(value, [struct], path);
2417
+ function encodeTaggedObject(value, struct, path, guides) {
2418
+ return encodeObjectNode(value, [struct], path, guides);
2079
2419
  }
2080
2420
  function isPassthroughStruct(struct) {
2081
2421
  const kind = struct[DEFINITION].kind;
@@ -2093,14 +2433,14 @@ function keySetsOf(fields) {
2093
2433
  }
2094
2434
  return sets;
2095
2435
  }
2096
- function encodeSingleObject(value, struct, path) {
2436
+ function encodeSingleObject(value, struct, path, guides) {
2097
2437
  const fields = resolveStructFields(struct, struct[DEFINITION]);
2098
2438
  const output = nullObject();
2099
2439
  for (const field of fields) {
2100
2440
  if (!Object.hasOwn(value, field.key)) continue;
2101
2441
  const fieldValue = value[field.key];
2102
2442
  if (fieldValue === void 0) continue;
2103
- output[field.wireKey] = encodeGuided(fieldValue, field.struct, [...path, field.key]);
2443
+ output[field.wireKey] = encodeGuided(fieldValue, field.struct, [...path, field.key], guides);
2104
2444
  }
2105
2445
  const { fieldKeys, wireKeys } = keySetsOf(fields);
2106
2446
  for (const key of Object.keys(value)) {
@@ -2108,12 +2448,12 @@ function encodeSingleObject(value, struct, path) {
2108
2448
  if (wireKeys.has(key)) throw new TypeError(`wire key collision for "${key}"`);
2109
2449
  const extra = value[key];
2110
2450
  if (extra === void 0) continue;
2111
- output[key] = encodeGuided(extra, void 0, [...path, key]);
2451
+ output[key] = encodeGuided(extra, void 0, [...path, key], guides);
2112
2452
  }
2113
2453
  return output;
2114
2454
  }
2115
- function encodeObjectNode(value, members, path) {
2116
- if (members.length === 1 && members[0][DEFINITION].kind === "object") return encodeSingleObject(value, members[0], path);
2455
+ function encodeObjectNode(value, members, path, guides) {
2456
+ if (members.length === 1 && members[0][DEFINITION].kind === "object") return encodeSingleObject(value, members[0], path, guides);
2117
2457
  const objects = [];
2118
2458
  const records = [];
2119
2459
  const passthroughs = [];
@@ -2140,24 +2480,24 @@ function encodeObjectNode(value, members, path) {
2140
2480
  if (!Object.hasOwn(value, key)) continue;
2141
2481
  const fieldValue = value[key];
2142
2482
  if (fieldValue === void 0) continue;
2143
- writeWireGroups(output, fieldValue, plans.get(key), [...path, key]);
2483
+ writeWireGroups(output, fieldValue, plans.get(key), [...path, key], guides);
2144
2484
  }
2145
- if (records.length > 0) return applyPassthrough(output, value, passthroughs, path);
2146
- if (passthroughs.length > 0) return applyPassthrough(output, value, passthroughs, path);
2485
+ if (records.length > 0) return applyPassthrough(output, value, passthroughs, path, guides);
2486
+ if (passthroughs.length > 0) return applyPassthrough(output, value, passthroughs, path, guides);
2147
2487
  for (const key of Object.keys(value)) {
2148
2488
  if (fieldKeys.has(key)) continue;
2149
2489
  if (wireKeys.has(key)) throw new TypeError(`wire key collision for "${key}"`);
2150
2490
  const extra = value[key];
2151
2491
  if (extra === void 0) continue;
2152
- output[key] = encodeGuided(extra, void 0, [...path, key]);
2492
+ output[key] = encodeGuided(extra, void 0, [...path, key], guides);
2153
2493
  }
2154
2494
  return output;
2155
2495
  }
2156
- function applyPassthrough(output, value, passthroughs, path) {
2496
+ function applyPassthrough(output, value, passthroughs, path, guides) {
2157
2497
  let merged = output;
2158
2498
  for (const guide of passthroughs) {
2159
- const passed = encodeGuided(value, guide, path);
2160
- const result = mergeIntersectionValues(merged, passed);
2499
+ const passed = encodeGuided(value, guide, path, guides);
2500
+ const result = mergeIntersectionValues(merged, passed, false, path.length);
2161
2501
  if (!result.ok) throw intersectionEncodeError([...path, ...result.path]);
2162
2502
  merged = result.value;
2163
2503
  }
@@ -2182,10 +2522,10 @@ function addWireGroup(plans, order, logical, wire, struct) {
2182
2522
  }
2183
2523
  group.structs.push(struct);
2184
2524
  }
2185
- function writeWireGroups(output, fieldValue, groups, path) {
2525
+ function writeWireGroups(output, fieldValue, groups, path, guides) {
2186
2526
  for (const group of groups) {
2187
2527
  const guide = group.passthrough ? group.structs[0] : guideTag(group.structs);
2188
- writeWire(output, group.wire, encodeGuided(fieldValue, guide, path));
2528
+ writeWire(output, group.wire, encodeGuided(fieldValue, guide, path, guides), path.length);
2189
2529
  }
2190
2530
  }
2191
2531
  function declaredRecordKeys(value, order) {
@@ -2198,28 +2538,28 @@ function declaredRecordKeys(value, order) {
2198
2538
  for (const key of Object.keys(value)) if (!seen.has(key)) keys.push(key);
2199
2539
  return keys;
2200
2540
  }
2201
- function writeWire(output, wireKey, encoded) {
2541
+ function writeWire(output, wireKey, encoded, depth) {
2202
2542
  if (!Object.hasOwn(output, wireKey)) {
2203
2543
  output[wireKey] = encoded;
2204
2544
  return;
2205
2545
  }
2206
- const merged = mergeIntersectionValues(output[wireKey], encoded);
2546
+ const merged = mergeIntersectionValues(output[wireKey], encoded, false, depth);
2207
2547
  if (!merged.ok) throw intersectionEncodeError([wireKey, ...merged.path]);
2208
2548
  output[wireKey] = merged.value;
2209
2549
  }
2210
- function encodeGuidedArray(value, guide, path) {
2550
+ function encodeGuidedArray(value, guide, path, guides) {
2211
2551
  const container = guide ? containerOf(guide, value) : void 0;
2212
2552
  if (container?.kind === "array") {
2213
2553
  const item = container.item;
2214
- return value.map((entry, index) => encodeGuided(entry, item, [...path, index]));
2554
+ return value.map((entry, index) => encodeGuided(entry, item, [...path, index], guides));
2215
2555
  }
2216
2556
  if (container?.kind === "tuple") {
2217
2557
  const items = container.items;
2218
- return value.map((entry, index) => encodeGuided(entry, index < items.length ? items[index] : void 0, [...path, index]));
2558
+ return value.map((entry, index) => encodeGuided(entry, index < items.length ? items[index] : void 0, [...path, index], guides));
2219
2559
  }
2220
- return value.map((entry, index) => encodeGuided(entry, void 0, [...path, index]));
2560
+ return value.map((entry, index) => encodeGuided(entry, void 0, [...path, index], guides));
2221
2561
  }
2222
- function encodeArrayMembers(value, members, path) {
2562
+ function encodeArrayMembers(value, members, path, guides) {
2223
2563
  return value.map((entry, index) => {
2224
2564
  const children = [];
2225
2565
  const passthrough = [];
@@ -2236,21 +2576,21 @@ function encodeArrayMembers(value, members, path) {
2236
2576
  children.push(child);
2237
2577
  }
2238
2578
  const itemPath = [...path, index];
2239
- let encoded = encodeGuided(entry, guideTag(children), itemPath);
2579
+ let encoded = encodeGuided(entry, guideTag(children), itemPath, guides);
2240
2580
  for (const guide of passthrough) {
2241
- const passed = encodeGuided(entry, guide, itemPath);
2242
- const merged = mergeIntersectionValues(encoded, passed);
2581
+ const passed = encodeGuided(entry, guide, itemPath, guides);
2582
+ const merged = mergeIntersectionValues(encoded, passed, false, itemPath.length);
2243
2583
  if (!merged.ok) throw intersectionEncodeError([...itemPath, ...merged.path]);
2244
2584
  encoded = merged.value;
2245
2585
  }
2246
2586
  return encoded;
2247
2587
  });
2248
2588
  }
2249
- function encodeGuidedObject(value, guide, path) {
2589
+ function encodeGuidedObject(value, guide, path, guides) {
2250
2590
  const container = guide ? containerOf(guide, value) : void 0;
2251
- if (container?.kind === "object") return encodeTaggedObject(value, container.struct, path);
2252
- if (container?.kind === "record") return encodeRecord(container.valueStruct, value, path);
2253
- return encodeAnonymousObject(value, path);
2591
+ if (container?.kind === "object") return encodeTaggedObject(value, container.struct, path, guides);
2592
+ if (container?.kind === "record") return encodeRecord(container.valueStruct, value, path, guides);
2593
+ return encodeAnonymousObject(value, path, guides);
2254
2594
  }
2255
2595
  function encodeGuidedLeaf(value, guide) {
2256
2596
  return coerceJsonLeaf(guide && LEAF_KINDS.has(guide[DEFINITION].kind) ? encodeValue(guide, value) : value);
@@ -2290,25 +2630,29 @@ function containerOf(guide, value) {
2290
2630
  default: return;
2291
2631
  }
2292
2632
  }
2293
- function encodeMatchingUnion(value, guide, path) {
2633
+ function encodeMatchingUnion(value, guide, path, guides) {
2294
2634
  const matches = selectUnionOptions(guide[DEFINITION].options, value);
2295
2635
  const first = matches[0];
2296
- if (!first) return encodeGuided(value, void 0, path);
2297
- const encoded = encodeGuided(value, first, path);
2298
- for (let index = 1; index < matches.length; index += 1) if (!sameWireValue(encoded, encodeGuided(value, matches[index], path))) throw new TypeError("ambiguous union encode: multiple union branches match with different wire output");
2636
+ if (!first) return encodeGuided(value, void 0, path, guides);
2637
+ const encoded = encodeGuided(value, first, path, guides);
2638
+ for (let index = 1; index < matches.length; index += 1) if (!sameWireValue(encoded, encodeGuided(value, matches[index], path, guides))) throw new TypeError("ambiguous union encode: multiple union branches match with different wire output");
2299
2639
  return encoded;
2300
2640
  }
2301
- function encodeRecord(valueStruct, value, path) {
2641
+ function encodeRecord(valueStruct, value, path, guides) {
2302
2642
  const output = Object.create(null);
2303
- for (const [key, entry] of Object.entries(value)) output[key] = encodeGuided(entry, valueStruct, [...path, key]);
2643
+ for (const [key, entry] of Object.entries(value)) output[key] = encodeGuided(entry, valueStruct, [...path, key], guides);
2304
2644
  return output;
2305
2645
  }
2306
- function encodeAnonymousObject(value, path) {
2646
+ function encodeAnonymousObject(value, path, guides) {
2647
+ if (Symbol.iterator in value || Symbol.asyncIterator in value) {
2648
+ const key = path.at(-1);
2649
+ throw new TypeError(`${key === void 0 ? "value" : `field "${key}"`} is an iterable object, not a record. Convert it first: Array.from(iterable) for a list or header pairs, ReadableStream.from(iterable) for a streamed body.`);
2650
+ }
2307
2651
  const output = nullObject();
2308
2652
  for (const key of Object.keys(value)) {
2309
2653
  const item = value[key];
2310
2654
  if (item === void 0) continue;
2311
- output[key] = encodeGuided(item, void 0, [...path, key]);
2655
+ output[key] = encodeGuided(item, void 0, [...path, key], guides);
2312
2656
  }
2313
2657
  return output;
2314
2658
  }
@@ -2331,9 +2675,9 @@ function assertHeaderValue(value, key) {
2331
2675
  }
2332
2676
  if (typeof value === "object" && value !== null) throw new TypeError(`header value for "${key}" must be a scalar`);
2333
2677
  }
2334
- function codecOf(value) {
2678
+ function codecOf(value, guides) {
2335
2679
  if (!isPlainObject(value)) return;
2336
- const tag = structTag(value);
2680
+ const tag = guides?.get(value) ?? structTag(value);
2337
2681
  if (!isStruct(tag)) return;
2338
2682
  const definition = tag[DEFINITION];
2339
2683
  if (definition.kind !== "object") return;
@@ -2342,15 +2686,15 @@ function codecOf(value) {
2342
2686
  function structTag(value) {
2343
2687
  return value[STRUCT];
2344
2688
  }
2345
- //#endregion
2346
- //#region src/internal/transport_request.ts
2689
+ // src/internal/transport_request.ts
2347
2690
  function createBaseTransportRequest(method, path, input, builder, options) {
2348
- const built = materializeOutboundRequest(runOutboundBuilder(input, builder), options.transport);
2691
+ const built = materializeOutboundRequest(runOutboundBuilder(input, builder, options.inputDeclared), options.transport, options.inputGuides);
2349
2692
  if (built.body !== void 0 && (method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD")) throw new TypeError("GET/HEAD requests cannot have a body");
2350
2693
  const { queryParams, queryString } = resolveTransportQuery(built.query, options.querySerializer);
2351
2694
  const headers = options.defaultHeaders ? new Headers(options.defaultHeaders) : new Headers();
2352
2695
  appendRecordToHeaders(headers, built.headers);
2353
- if (built.defaultContentType && !headers.has("content-type")) headers.set("content-type", built.defaultContentType);
2696
+ const declaredContentType = headers.has("content-type");
2697
+ if (options.transport === "sse" && built.defaultContentType && !declaredContentType) headers.set("content-type", built.defaultContentType);
2354
2698
  const request = {
2355
2699
  abort: options.abort,
2356
2700
  baseEndpoint: options.baseEndpoint,
@@ -2365,17 +2709,20 @@ function createBaseTransportRequest(method, path, input, builder, options) {
2365
2709
  timeout: options.timeout,
2366
2710
  withCredentials: options.withCredentials ?? false
2367
2711
  };
2368
- applyRequestContentType(request, headers);
2712
+ resolveRequestUrl(request);
2713
+ if (options.transport === "sse" || declaredContentType) applyRequestContentType(request, headers);
2714
+ if (options.transport === "http") request.bodyContentType = built.defaultContentType ?? resolveHttpBodyContentType(request.body);
2369
2715
  return request;
2370
2716
  }
2371
- //#endregion
2372
- //#region src/http/request.ts
2717
+ // src/http/request.ts
2373
2718
  function createHttpRequest(method, path, input, builder, options) {
2374
2719
  return {
2375
2720
  ...createBaseTransportRequest(method, path, input, builder, {
2376
2721
  abort: options.abort,
2377
2722
  baseEndpoint: options.baseEndpoint,
2378
2723
  defaultHeaders: options.defaultHeaders,
2724
+ inputDeclared: options.inputDeclared,
2725
+ inputGuides: options.inputGuides,
2379
2726
  operation: options.operation,
2380
2727
  querySerializer: options.querySerializer,
2381
2728
  timeout: options.timeout,
@@ -2389,16 +2736,15 @@ function createHttpRequest(method, path, input, builder, options) {
2389
2736
  xsrf: options.xsrf
2390
2737
  };
2391
2738
  }
2392
- //#endregion
2393
- //#region src/http/transport/body_failure.ts
2739
+ // src/http/transport/body_failure.ts
2394
2740
  /**
2395
- * A failure raised while reading or interpreting a response body, tagged at the point
2741
+ * A failure raised while reading a body or running a progress observer, tagged at the point
2396
2742
  * where the transport still knows which step failed.
2397
2743
  *
2398
- * Reading the body, running a download observer, and interpreting bytes as the declared
2399
- * representation are three different faults with three different owners. They are
2400
- * indistinguishable once they collapse into a bare cause, so the transport tags them here
2401
- * and command execution maps `code` straight through.
2744
+ * An upload observer can fail before any response exists, so `meta` is absent then. Download
2745
+ * 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
2747
+ * `code` straight through.
2402
2748
  *
2403
2749
  * Caller cancellation is deliberately never tagged: it stays the raw abort reason so the
2404
2750
  * cancellation path keeps priority over body classification.
@@ -2427,8 +2773,7 @@ var BodyFailure = class extends Error {
2427
2773
  function isBodyFailure(value) {
2428
2774
  return value instanceof BodyFailure;
2429
2775
  }
2430
- //#endregion
2431
- //#region src/internal/fetch.ts
2776
+ // src/internal/fetch.ts
2432
2777
  async function fetchWithSignal(send, signal, onLateResponseBody) {
2433
2778
  if (!signal) return send();
2434
2779
  return awaitWithSignal(() => {
@@ -2439,13 +2784,20 @@ async function fetchWithSignal(send, signal, onLateResponseBody) {
2439
2784
  return pending;
2440
2785
  }, signal);
2441
2786
  }
2442
- //#endregion
2443
- //#region src/http/transport/fetch_init.ts
2787
+ // src/http/transport/fetch_init.ts
2444
2788
  const ERR_STREAMING_REQUEST_UNSUPPORTED = /* @__PURE__ */ new Error("ERR_STREAMING_REQUEST_UNSUPPORTED");
2445
2789
  function isReadableStreamBody(body) {
2446
2790
  return isReadableStream(body);
2447
2791
  }
2448
2792
  let streamingRequestBodySupport;
2793
+ /**
2794
+ * Whether a ReadableStream can be a Request body: a non-null body and no Content-Type.
2795
+ * A runtime without stream bodies stringifies the stream and adds a text/plain Content-Type (Firefox).
2796
+ * The `duplex` getter is not used: Bun streams request bodies without reading it.
2797
+ * This does not predict whether the connection takes the stream (Chromium streams only over HTTP/2+).
2798
+ * Upload-progress conversion is also skipped in browsers (`prepareUploadProgressRequest`): the body is sent
2799
+ * unchanged and the callback is not called. Download progress is unaffected.
2800
+ */
2449
2801
  function supportsStreamingRequestBody() {
2450
2802
  if (streamingRequestBodySupport !== void 0) return streamingRequestBodySupport;
2451
2803
  if (typeof Request !== "function" || typeof ReadableStream === "undefined") {
@@ -2456,11 +2808,12 @@ function supportsStreamingRequestBody() {
2456
2808
  const stream = new ReadableStream({ start(controller) {
2457
2809
  controller.close();
2458
2810
  } });
2459
- streamingRequestBodySupport = new Request("https://example.com", {
2811
+ const request = new Request("https://example.com", {
2460
2812
  body: stream,
2461
2813
  duplex: "half",
2462
2814
  method: "POST"
2463
- }).body !== null;
2815
+ });
2816
+ streamingRequestBodySupport = request.body !== null && !request.headers.has("Content-Type");
2464
2817
  return streamingRequestBodySupport;
2465
2818
  } catch {
2466
2819
  streamingRequestBodySupport = false;
@@ -2480,15 +2833,14 @@ function createFetchInitBase(request, options) {
2480
2833
  signal: options.signal
2481
2834
  };
2482
2835
  if (isReadableStreamBody(body)) {
2483
- if (!supportsStreamingRequestBody()) throw options.streamingRequestUnsupportedError;
2836
+ if (!supportsStreamingRequestBody()) throw ERR_STREAMING_REQUEST_UNSUPPORTED;
2484
2837
  body = options.wrapReadableStreamBody?.(body) ?? body;
2485
2838
  init.body = body;
2486
2839
  init.duplex = "half";
2487
2840
  }
2488
2841
  return init;
2489
2842
  }
2490
- //#endregion
2491
- //#region src/internal/media_type.ts
2843
+ // src/internal/media_type.ts
2492
2844
  const MEDIA_TYPE_ESSENCE = /^\s*([\w!#$%&'*+.^`|~-]+)\/([\w!#$%&'*+.^`|~-]+)\s*(?:;|$)/i;
2493
2845
  function parseMediaTypeEssence(value) {
2494
2846
  const match = value.match(MEDIA_TYPE_ESSENCE);
@@ -2524,8 +2876,7 @@ function readToken(value, position) {
2524
2876
  position
2525
2877
  } : void 0;
2526
2878
  }
2527
- //#endregion
2528
- //#region src/http/transport/utils.ts
2879
+ // src/http/transport/utils.ts
2529
2880
  function getContentLength(headers) {
2530
2881
  const value = headers.get("Content-Length");
2531
2882
  if (!value) return 0;
@@ -2554,8 +2905,7 @@ function isJsonMediaType(contentType) {
2554
2905
  const suffixAt = subtype.lastIndexOf("+");
2555
2906
  return suffixAt > 0 && subtype.slice(suffixAt + 1) === "json";
2556
2907
  }
2557
- //#endregion
2558
- //#region src/http/transport/fetch.ts
2908
+ // src/http/transport/fetch.ts
2559
2909
  const XSRF_SAFE_METHODS = /* @__PURE__ */ new Set([
2560
2910
  "GET",
2561
2911
  "HEAD",
@@ -2565,9 +2915,6 @@ const XSRF_SAFE_METHODS = /* @__PURE__ */ new Set([
2565
2915
  function isSafeMethod(method) {
2566
2916
  return XSRF_SAFE_METHODS.has(method.toUpperCase());
2567
2917
  }
2568
- function isBrowserRuntime() {
2569
- return typeof document !== "undefined" && typeof location !== "undefined";
2570
- }
2571
2918
  function readBrowserXSRFCookie(name) {
2572
2919
  try {
2573
2920
  const cookie = document.cookie;
@@ -2582,7 +2929,7 @@ function readBrowserXSRFCookie(name) {
2582
2929
  }
2583
2930
  function isSameOriginRequest(request) {
2584
2931
  if (!request.baseEndpoint) return false;
2585
- if (!isBrowserRuntime()) return true;
2932
+ if (typeof location === "undefined") return true;
2586
2933
  return resolveRequestUrl(request).origin === location.origin;
2587
2934
  }
2588
2935
  function normalizeXSRFToken(token) {
@@ -2591,7 +2938,7 @@ function normalizeXSRFToken(token) {
2591
2938
  }
2592
2939
  function resolveXSRFToken(request, xsrf) {
2593
2940
  if (xsrf.tokenProvider) return normalizeXSRFToken(xsrf.tokenProvider({ request }));
2594
- if (!isBrowserRuntime()) return;
2941
+ if (typeof document === "undefined") return;
2595
2942
  return normalizeXSRFToken(readBrowserXSRFCookie(xsrf.cookieName));
2596
2943
  }
2597
2944
  function applyXSRFHeaderIfNeeded(request, headers) {
@@ -2668,7 +3015,6 @@ function createFetchRequestInit(request) {
2668
3015
  defaultAccept: "application/json, text/plain, */*",
2669
3016
  headers,
2670
3017
  signal: request.abort,
2671
- streamingRequestUnsupportedError: ERR_STREAMING_REQUEST_UNSUPPORTED,
2672
3018
  wrapReadableStreamBody: uploadProgress ? (body) => wrapUploadProgressStream(body, uploadProgress, getContentLength(headers), request.abort) : void 0
2673
3019
  });
2674
3020
  }
@@ -2713,7 +3059,8 @@ async function parseFetchResponse(httpRequest, response, fallbackUrl, platform)
2713
3059
  url
2714
3060
  });
2715
3061
  }
2716
- const contentLength = getContentLength(headers);
3062
+ const contentEncoding = headers.get("Content-Encoding")?.trim().toLowerCase();
3063
+ let contentLength = contentEncoding !== void 0 && contentEncoding !== "identity" ? 0 : getContentLength(headers);
2717
3064
  const contentType = getContentType(headers);
2718
3065
  const meta = {
2719
3066
  headers,
@@ -2749,6 +3096,7 @@ async function parseFetchResponse(httpRequest, response, fallbackUrl, platform)
2749
3096
  if (result.done) break;
2750
3097
  chunks.push(result.value);
2751
3098
  receivedLength += result.value.byteLength;
3099
+ if (receivedLength > contentLength) contentLength = 0;
2752
3100
  if (downloadProgress) {
2753
3101
  const event = {
2754
3102
  lengthComputable: contentLength > 0,
@@ -2786,18 +3134,48 @@ async function parseFetchResponse(httpRequest, response, fallbackUrl, platform)
2786
3134
  }
2787
3135
  /**
2788
3136
  * Default HTTP transport: perform `httpRequest` with `fetch` and return an `HttpResponse`.
2789
- * Used by the client unless `withHTTPHandle` replaces it; also usable in tests and custom handlers.
3137
+ *
3138
+ * The client always uses it; `withHTTPHandle` only supplies the `fetchImpl` it
3139
+ * calls. Also usable in tests and custom handlers. `fetchImpl` defaults to the
3140
+ * global `fetch` resolved at send time. It rejects, rather than resolving with
3141
+ * a synthetic response, when no response reached the client.
2790
3142
  *
2791
3143
  * @param httpRequest - Normalized request (URL, headers, body, abort, progress hooks).
2792
3144
  * @param fetchImpl - Fetch implementation; defaults to global `fetch`. The library default handle is called as
2793
- * `(url, init)` and its body is read in one piece unless download progress is observed. Any other handle receives a
3145
+ * `(url, init)` while the global is untouched, and its body is read in one piece unless download progress is observed. Any other handle receives a
2794
3146
  * `Request` and its body is read chunk by chunk.
2795
- * @returns Parsed `HttpResponse`. Rejects when no response reached the client, or when reading the
2796
- * body failed — a transport failure is a fault, never a response.
3147
+ * @returns Parsed `HttpResponse`. Rejects when no response reached the client, when reading the body
3148
+ * failed, or when an upload observer threw (`EXT_OBSERVER_FAILED`). Cancellation still wins.
3149
+ * @group http
2797
3150
  */
2798
3151
  async function fetchHandler(httpRequest, fetchImpl = resolveGlobalFetch()) {
3152
+ if (fetchImpl === DEFAULT_FETCH) fetchImpl = resolveGlobalFetch();
3153
+ const uploadProgress = httpRequest.uploadProgress;
3154
+ if (!uploadProgress) return sendFetchRequest(httpRequest, fetchImpl);
3155
+ let observerFailure;
3156
+ const observed = {
3157
+ ...httpRequest,
3158
+ uploadProgress: async (event) => {
3159
+ try {
3160
+ await uploadProgress(event);
3161
+ } catch (error) {
3162
+ observerFailure ??= new BodyFailure("EXT_OBSERVER_FAILED", error, void 0);
3163
+ throw error;
3164
+ }
3165
+ }
3166
+ };
3167
+ let response;
3168
+ try {
3169
+ response = await sendFetchRequest(observed, fetchImpl);
3170
+ } catch (error) {
3171
+ throw observerFailure !== void 0 && !httpRequest.abort?.aborted ? observerFailure : error;
3172
+ }
3173
+ if (observerFailure !== void 0) throw observerFailure;
3174
+ return response;
3175
+ }
3176
+ async function sendFetchRequest(httpRequest, fetchImpl) {
2799
3177
  const abortSignal = httpRequest.abort;
2800
- const platform = fetchImpl === DEFAULT_FETCH;
3178
+ const platform = fetchImpl === PLATFORM_FETCH_HANDLE;
2801
3179
  let prepared = httpRequest;
2802
3180
  let request;
2803
3181
  let platformBody;
@@ -2827,35 +3205,49 @@ async function fetchHandler(httpRequest, fetchImpl = resolveGlobalFetch()) {
2827
3205
  throw (abortSignal && resolveAbortCause(abortSignal)) ?? error;
2828
3206
  }
2829
3207
  }
3208
+ function isBrowserWindowOrWorker() {
3209
+ return typeof location !== "undefined" || typeof document !== "undefined" || "importScripts" in globalThis;
3210
+ }
3211
+ /**
3212
+ * Turns Blob/File, ArrayBuffer, and FormData into a stream to count bytes, only where the runtime streams
3213
+ * request bodies over any HTTP version (Bun, Node). Browsers are excluded: Chromium rejects a streamed
3214
+ * body on HTTP/1.1 and the protocol is unknown beforehand, and Firefox cannot stream request bodies.
3215
+ * `location`, `document`, or `importScripts` mark browser windows/workers: there the body is sent unchanged
3216
+ * and `onUploadProgress` is not called. Download progress is unaffected.
3217
+ * FormData uses the serialized stream and boundary directly; its total length is unknown unless declared in a header.
3218
+ */
2830
3219
  async function prepareUploadProgressRequest(request) {
2831
3220
  const { uploadProgress, body } = request;
2832
- if (!uploadProgress || body == null || isReadableStreamBody(body) || !supportsStreamingRequestBody()) return request;
2833
- const converted = await convertBodyToUploadStream(body);
3221
+ if (!uploadProgress || body == null || isReadableStreamBody(body) || !supportsStreamingRequestBody() || isBrowserWindowOrWorker()) return request;
3222
+ request.abort?.throwIfAborted();
3223
+ const converted = convertBodyToUploadStream(body);
2834
3224
  if (!converted) return request;
2835
3225
  const headers = new Headers(request.headers);
2836
- if (converted.contentType) headers.set("Content-Type", converted.contentType);
3226
+ applyRequestContentType(request, headers);
3227
+ if (converted.contentType && (body instanceof FormData || !headers.has("Content-Type") && request.bodyContentType !== null)) headers.set("Content-Type", converted.contentType);
2837
3228
  if (converted.total > 0) headers.set("Content-Length", String(converted.total));
3229
+ const total = getContentLength(headers);
2838
3230
  const startEvent = {
2839
- lengthComputable: converted.total > 0,
3231
+ lengthComputable: total > 0,
2840
3232
  loaded: 0,
2841
- total: converted.total
3233
+ total
2842
3234
  };
2843
3235
  try {
2844
3236
  if (request.abort) await awaitWithSignal(() => uploadProgress(startEvent), request.abort);
2845
3237
  else await uploadProgress(startEvent);
2846
3238
  } catch (error) {
2847
- converted.stream.cancel(error);
3239
+ converted.stream.cancel(error).catch(() => void 0);
2848
3240
  throw error;
2849
3241
  }
2850
3242
  return {
2851
3243
  ...request,
2852
3244
  body: converted.stream,
2853
- bodyContentType: converted.contentType ?? request.bodyContentType,
3245
+ bodyContentType: headers.get("Content-Type"),
2854
3246
  bodyContentTypeSource: converted.stream,
2855
3247
  headers
2856
3248
  };
2857
3249
  }
2858
- async function convertBodyToUploadStream(body) {
3250
+ function convertBodyToUploadStream(body) {
2859
3251
  if (typeof Blob !== "undefined" && body instanceof Blob) return {
2860
3252
  contentType: body.type || void 0,
2861
3253
  stream: body.stream(),
@@ -2868,36 +3260,51 @@ async function convertBodyToUploadStream(body) {
2868
3260
  };
2869
3261
  if (typeof FormData !== "undefined" && body instanceof FormData) {
2870
3262
  const serialized = new Response(body);
2871
- const blob = await serialized.blob();
2872
3263
  return {
2873
3264
  contentType: serialized.headers.get("Content-Type"),
2874
- stream: blob.stream(),
2875
- total: blob.size
3265
+ stream: serialized.body,
3266
+ total: 0
2876
3267
  };
2877
3268
  }
2878
3269
  }
2879
- //#endregion
2880
- //#region src/http/http.ts
3270
+ // src/http/http.ts
2881
3271
  /**
2882
- * Declare a typed HTTP request endpoint.
3272
+ * Declares a typed HTTP request endpoint.
2883
3273
  *
2884
- * Pass method, path, an optional input struct, and the single `output` and `error` structs.
2885
- * Call the returned endpoint with input to get an `HttpCommand` for `client.execute`.
3274
+ * 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`.
3277
+ *
3278
+ * The contract is `method`, `path`, an optional `input` struct, optional
3279
+ * `output` and `error` structs, and an optional `operation`. A declared `input`
3280
+ * always requires an explicit `builder`. `input` only defines the parameters;
3281
+ * the builder alone decides where each value goes. Only definitions without
3282
+ * `input` may omit `builder`. `ResponseDeclaration` and `BuilderField` are
3283
+ * internal helper types: `output` and `error` are required only when the
3284
+ * matching type parameter is a struct, and `builder` is
3285
+ * `(input: BuilderInput<TInput>) => HttpRequestShape` when `input` is declared.
3286
+ *
3287
+ * `output` is one struct for the 2xx body; `error` is one struct for every
3288
+ * non-2xx body. Neither is keyed by status. A declaration is an assertion:
3289
+ * omitting one means that body is never read.
2886
3290
  *
2887
3291
  * @param definition - Request contract (method, path, input, builder, output).
2888
3292
  * @returns An endpoint that creates `HttpCommand` values from input.
2889
3293
  *
3294
+ * The checked example file passes the whole input object as `path`. This snippet maps individual fields into `path` and `query` instead:
3295
+ *
2890
3296
  * @example
2891
3297
  * ```ts
2892
3298
  * const getUser = defineRequest({
2893
3299
  * method: 'GET',
2894
3300
  * path: '/users/:id',
2895
- * input: struct.object({ id: struct.number() }),
2896
- * builder: (input) => ({ path: { id: input.id } }),
3301
+ * input: struct.object({ id: struct.number(), verbose: struct.boolean() }),
3302
+ * builder: (input) => ({ path: { id: input.id }, query: { verbose: input.verbose } }),
2897
3303
  * output: struct.object({ id: struct.number(), name: struct.string() }),
2898
- * error: struct.object({ message: struct.string() }),
2899
3304
  * })
2900
3305
  * ```
3306
+ * @group http
3307
+ * @exampleFile examples/api/define-request.ts#example
2901
3308
  */
2902
3309
  function defineRequest(definition) {
2903
3310
  const endpoint = { ...definition };
@@ -2912,13 +3319,28 @@ function defineRequest(definition) {
2912
3319
  return ((input) => create(input));
2913
3320
  }
2914
3321
  /**
2915
- * Run an `HttpCommand` against client config: validate input, send the request, parse the response.
2916
- * Prefer `client.execute(command)` in application code; this is the underlying implementation.
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.
2917
3338
  *
2918
3339
  * @param clientConfig - Resolved HTTP client configuration (endpoint, interceptors, handler).
2919
3340
  * @param command - Command from a `defineRequest` endpoint.
2920
3341
  * @param options - Per-request progress and cancellation options.
2921
3342
  * @returns An await-result tuple of success body or typed request error.
3343
+ * @group http
2922
3344
  */
2923
3345
  async function executeHttpCommand(clientConfig, command, options) {
2924
3346
  const { definition, input } = command;
@@ -2963,9 +3385,10 @@ async function executeHttpCommand(clientConfig, command, options) {
2963
3385
  };
2964
3386
  const responseType = definition.output ? resolveInboundRepresentation(definition.output) : void 0;
2965
3387
  const errorResponseType = definition.error ? resolveInboundRepresentation(definition.error) : void 0;
3388
+ const inputGuides = /* @__PURE__ */ new WeakMap();
2966
3389
  let parsedInput;
2967
3390
  try {
2968
- parsedInput = await parseEndpointInput(definition.input, input);
3391
+ parsedInput = parseEndpointInput(definition.input, input, inputGuides);
2969
3392
  } catch (error) {
2970
3393
  return stopBeforeTransport(resolveTransportCancellation(deadline, requestSignal, createPreflightFault("REQ_INPUT_INVALID", error)));
2971
3394
  }
@@ -2978,6 +3401,8 @@ async function executeHttpCommand(clientConfig, command, options) {
2978
3401
  baseEndpoint: clientConfig.endpoint,
2979
3402
  defaultHeaders: clientConfig.headers,
2980
3403
  downloadProgress: config.onDownloadProgress,
3404
+ inputDeclared: definition.input !== void 0,
3405
+ inputGuides,
2981
3406
  errorResponseType,
2982
3407
  operation: definition.operation,
2983
3408
  querySerializer: clientConfig.querySerializer,
@@ -2993,6 +3418,7 @@ async function executeHttpCommand(clientConfig, command, options) {
2993
3418
  const builtCancellation = resolveTransportCancellation(deadline, requestSignal);
2994
3419
  if (builtCancellation) return stopBeforeTransport(builtCancellation);
2995
3420
  const releaseRequestTimeout = deadline.release;
3421
+ const transportOrigin = createTransportOrigin();
2996
3422
  const transportController = new AbortController();
2997
3423
  let chainSettled = false;
2998
3424
  let inflightTransport;
@@ -3003,9 +3429,16 @@ async function executeHttpCommand(clientConfig, command, options) {
3003
3429
  rejected.catch(() => void 0);
3004
3430
  return rejected;
3005
3431
  }
3432
+ try {
3433
+ resolveRequestUrl(nextRequest);
3434
+ } catch (error) {
3435
+ const rejected = Promise.reject(error);
3436
+ rejected.catch(() => void 0);
3437
+ return rejected;
3438
+ }
3006
3439
  const transportAbort = nextRequest.abort === void 0 || nextRequest.abort === requestSignal ? transportController.signal : mergeAbortSignals(transportController.signal, [nextRequest.abort]);
3007
3440
  if (resolveTransportCancellation(deadline, transportAbort)) {
3008
- const rejected = Promise.reject(markTransportOrigin(resolveAbortCause(transportAbort)));
3441
+ const rejected = Promise.reject(transportOrigin.mark(resolveAbortCause(transportAbort)));
3009
3442
  releaseAbortSignal(transportAbort);
3010
3443
  rejected.catch(() => void 0);
3011
3444
  return rejected;
@@ -3014,7 +3447,7 @@ async function executeHttpCommand(clientConfig, command, options) {
3014
3447
  ...nextRequest,
3015
3448
  abort: transportAbort
3016
3449
  }, clientConfig.http.handle).catch((error) => {
3017
- throw markTransportOrigin(resolveAbortCause(transportAbort) ?? error);
3450
+ throw transportOrigin.mark(resolveAbortCause(transportAbort) ?? error);
3018
3451
  }).finally(() => {
3019
3452
  releaseAbortSignal(transportAbort);
3020
3453
  });
@@ -3031,16 +3464,20 @@ async function executeHttpCommand(clientConfig, command, options) {
3031
3464
  else requestSignal.addEventListener("abort", abortTransport, { once: true });
3032
3465
  const chainPromise = chain(request, handler);
3033
3466
  chainPromise.catch(() => void 0);
3034
- response = await awaitWithSignal(() => chainPromise, requestSignal);
3467
+ const settled = await awaitWithSignal(() => chainPromise, requestSignal);
3468
+ if (!isHttpResponse(settled)) throw new TypeError("HTTP interceptor must resolve an HttpResponse from next() or makeResponse()");
3469
+ response = settled;
3035
3470
  } catch (error) {
3036
- const fromTransport = isTransportOrigin(error);
3037
- const aborted = resolveAbortFault(requestSignal);
3038
- if (aborted) {
3471
+ const fromTransport = transportOrigin.has(error);
3472
+ const cancelled = resolveTransportCancellation(deadline, requestSignal);
3473
+ if (cancelled) {
3039
3474
  abortTransport();
3040
3475
  if (inflightTransport) await Promise.allSettled([inflightTransport]);
3041
- return fail(aborted);
3476
+ return fail(cancelled);
3042
3477
  }
3478
+ if (error instanceof RequestUrlError) return fail(createPreflightFault("REQ_BUILD_FAILED", error));
3043
3479
  if (isBodyFailure(error)) return fail(fromBodyFailure(error));
3480
+ if (fromTransport && (error === ERR_STREAMING_REQUEST_UNSUPPORTED || error === ERR_FETCH_UNSUPPORTED)) return fail(createPreflightFault("ENV_UNSUPPORTED", error));
3044
3481
  const networkFault = createNetworkFault(error);
3045
3482
  if (networkFault.code !== "NET_UNREACHABLE") return fail(networkFault);
3046
3483
  return fail(fromTransport ? networkFault : createPreflightFault("EXT_INTERCEPTOR_FAILED", error));
@@ -3145,8 +3582,7 @@ async function parseStructResponse(struct, body) {
3145
3582
  function readBodyText(value) {
3146
3583
  return readBytesText(value) ?? (typeof Blob !== "undefined" && value instanceof Blob ? value.text() : void 0);
3147
3584
  }
3148
- //#endregion
3149
- //#region src/interceptor/basic_auth.ts
3585
+ // src/interceptor/basic_auth.ts
3150
3586
  function createBasicAuthModifier(fn, options) {
3151
3587
  let encode = options?.encode;
3152
3588
  if (!encode) {
@@ -3166,9 +3602,18 @@ function createBasicAuthModifier(fn, options) {
3166
3602
  /**
3167
3603
  * HTTP interceptor that sets an `Authorization: Basic …` header from credentials.
3168
3604
  *
3605
+ * It sets `Authorization: Basic <encoded>` on a copy of the request headers,
3606
+ * replacing any existing `Authorization` header. The default encoder is
3607
+ * ``btoa(`${username}:${password}`)``, so credentials must be Latin-1. Without
3608
+ * `encode`, calling `basicAuthHttpInterceptor` throws
3609
+ * `Error: BasicAuthInterceptor is not supported in this environment` when `btoa`
3610
+ * is not available in the runtime. Use {@link basicAuthSSEInterceptor} for SSE
3611
+ * commands.
3612
+ *
3169
3613
  * @param fn - Factory returning username/password for each request.
3170
3614
  * @param options - Optional custom encoder (defaults to `btoa`).
3171
3615
  * @returns An `HttpInterceptor` that injects basic auth.
3616
+ * @group interceptors
3172
3617
  */
3173
3618
  function basicAuthHttpInterceptor(fn, options) {
3174
3619
  const modify = createBasicAuthModifier(fn, options);
@@ -3177,23 +3622,51 @@ function basicAuthHttpInterceptor(fn, options) {
3177
3622
  /**
3178
3623
  * SSE interceptor that sets an `Authorization: Basic …` header from credentials.
3179
3624
  *
3625
+ * It sets `Authorization: Basic <encoded>` on a copy of the request headers,
3626
+ * replacing any existing `Authorization` header. Encoding and the `btoa`
3627
+ * requirement are the same as for {@link basicAuthHttpInterceptor}, including
3628
+ * the synchronous throw when `btoa` is missing.
3629
+ *
3180
3630
  * @param fn - Factory returning username/password for each request.
3181
3631
  * @param options - Optional custom encoder (defaults to `btoa`).
3182
3632
  * @returns An `SSEInterceptor` that injects basic auth.
3633
+ * @group interceptors
3183
3634
  */
3184
3635
  function basicAuthSSEInterceptor(fn, options) {
3185
3636
  const modify = createBasicAuthModifier(fn, options);
3186
3637
  return createSSEInterceptor((req, next) => next(modify(req)));
3187
3638
  }
3188
- //#endregion
3189
- //#region src/internal/media_type_parameters.ts
3639
+ // src/internal/reconnect_options.ts
3640
+ function normalizeReconnectOptions(config, defaults, transport) {
3641
+ if (!config) return void 0;
3642
+ const attempts = config.attempts ?? 3;
3643
+ if (!Number.isSafeInteger(attempts) || attempts < 0) throw new RangeError(`${transport} reconnect attempts must be a non-negative safe integer`);
3644
+ const delayMs = config.delayMs ?? defaults.delayMs;
3645
+ const factor = config.factor ?? defaults.factor;
3646
+ const jitter = config.jitter ?? defaults.jitter;
3647
+ const maxDelayMs = config.maxDelayMs ?? defaults.maxDelayMs;
3648
+ assertFiniteRange("delayMs", delayMs, 0);
3649
+ assertFiniteRange("factor", factor, Number.MIN_VALUE);
3650
+ assertFiniteRange("jitter", jitter, 0, 1);
3651
+ if (maxDelayMs !== Number.POSITIVE_INFINITY) assertFiniteRange("maxDelayMs", maxDelayMs, 0);
3652
+ return {
3653
+ attempts,
3654
+ delayMs,
3655
+ factor,
3656
+ jitter,
3657
+ maxDelayMs
3658
+ };
3659
+ function assertFiniteRange(field, value, minimum, maximum = Number.POSITIVE_INFINITY) {
3660
+ if (!Number.isFinite(value) || value < minimum || value > maximum) throw new RangeError(`${transport} reconnect ${field} is out of range`);
3661
+ }
3662
+ }
3663
+ // src/internal/media_type_parameters.ts
3190
3664
  const MEDIA_TYPE_PARAMETERS = /^(?:[ \t]*;[ \t]*[!#$%&'*+\-.^_`|~0-9A-Za-z]+[ \t]*=[ \t]*(?:"(?:[\t\x20\x21\x23-\x5B\x5D-\x7E\x80-\uFFFF]|\\[\t\x20\x21-\x7E\x80-\uFFFF])*"|[!#$%&'*+\-.^_`|~0-9A-Za-z]+))*[ \t]*$/;
3191
3665
  function isMediaTypeParametersWellFormed(value) {
3192
3666
  const prefix = parseMediaTypePrefix(value);
3193
3667
  return !!prefix.essence && value.trimStart() === value && MEDIA_TYPE_PARAMETERS.test(value.slice(prefix.position));
3194
3668
  }
3195
- //#endregion
3196
- //#region src/internal/async_queue.ts
3669
+ // src/internal/async_queue.ts
3197
3670
  const NO_ERROR = Symbol("NO_ERROR");
3198
3671
  var AsyncQueue = class {
3199
3672
  values = [];
@@ -3256,14 +3729,13 @@ var AsyncQueue = class {
3256
3729
  } };
3257
3730
  }
3258
3731
  };
3259
- //#endregion
3260
- //#region src/internal/backoff.ts
3732
+ // src/internal/backoff.ts
3261
3733
  const MAX_TIMER_DELAY_MS$1 = 2147483647;
3262
3734
  function computeReconnectDelay(config, attempt) {
3263
- const exponential = Math.min(config.delayMs * config.factor ** Math.max(0, attempt - 1), config.maxDelayMs);
3735
+ if (!Number.isFinite(config.delayMs)) throw new RangeError("Reconnect delay must be finite");
3736
+ const exponential = config.delayMs === 0 ? 0 : Math.min(config.delayMs * config.factor ** Math.max(0, attempt - 1), config.maxDelayMs, Number.MAX_VALUE);
3264
3737
  const delay = config.jitter <= 0 ? exponential : Math.max(0, Math.round(exponential * (1 + (Math.random() * 2 - 1) * config.jitter)));
3265
- if (!Number.isFinite(delay)) throw new RangeError("Reconnect delay must be finite");
3266
- return Math.min(delay, MAX_TIMER_DELAY_MS$1);
3738
+ return Math.min(delay, config.maxDelayMs, MAX_TIMER_DELAY_MS$1);
3267
3739
  }
3268
3740
  async function wait(ms, signal) {
3269
3741
  if (!Number.isFinite(ms)) throw new RangeError("Reconnect delay must be finite");
@@ -3286,8 +3758,7 @@ async function wait(ms, signal) {
3286
3758
  signal.addEventListener("abort", onAbort, { once: true });
3287
3759
  });
3288
3760
  }
3289
- //#endregion
3290
- //#region src/sse/transport/parser.ts
3761
+ // src/sse/transport/parser.ts
3291
3762
  var SSEParserLimitError = class extends Error {
3292
3763
  constructor() {
3293
3764
  super("SSE parser buffer exceeded maxBufferSize");
@@ -3296,121 +3767,99 @@ var SSEParserLimitError = class extends Error {
3296
3767
  };
3297
3768
  async function readStreamBytes(stream, onChunk, signal) {
3298
3769
  const reader = stream.getReader();
3299
- let cancelPromise;
3300
- const cancelOnce = (reason) => {
3301
- cancelPromise ??= reader.cancel(reason).catch(() => void 0);
3302
- };
3303
- let rejectAbort;
3304
- const abortPromise = signal ? new Promise((_, reject) => {
3305
- rejectAbort = reject;
3306
- }) : void 0;
3307
- abortPromise?.catch(() => {});
3308
- const onAbort = () => {
3309
- /* istanbul ignore next -- AbortController.abort() always provides a reason in standard runtimes */
3310
- const reason = signal?.reason ?? /* @__PURE__ */ new Error("The operation was aborted");
3311
- rejectAbort?.(reason);
3312
- cancelOnce(reason);
3313
- };
3314
3770
  try {
3315
- if (signal?.aborted) signal.throwIfAborted();
3316
- else signal?.addEventListener("abort", onAbort, { once: true });
3317
- while (true) {
3318
- const readPromise = reader.read();
3319
- readPromise.catch(() => {});
3320
- const { done, value } = abortPromise ? await Promise.race([readPromise, abortPromise]) : await readPromise;
3321
- if (done) return;
3322
- await onChunk(value);
3771
+ for (;;) {
3772
+ const read = () => reader.read();
3773
+ const result = signal ? await awaitWithSignal(read, signal) : await read();
3774
+ if (result.done) return;
3775
+ await onChunk(result.value);
3323
3776
  }
3324
3777
  } catch (error) {
3325
- cancelOnce(error);
3778
+ reader.cancel(error).catch(() => void 0);
3326
3779
  throw error;
3327
3780
  } finally {
3328
- signal?.removeEventListener("abort", onAbort);
3329
3781
  reader.releaseLock();
3330
3782
  }
3331
3783
  }
3784
+ /** Split byte chunks at the WHATWG SSE CR, LF and CRLF line boundaries. */
3332
3785
  function createLineParser(onLine, options) {
3333
- const maxBufferSize = validateMaxBufferSize(options?.maxBufferSize);
3334
- let buffer;
3335
- let position = 0;
3336
- let fieldLength = -1;
3337
- let discardTrailingNewline = false;
3786
+ const limit = validateMaxBufferSize(options?.maxBufferSize);
3787
+ let parts = [];
3788
+ let byteLength = 0;
3789
+ let afterCR = false;
3790
+ function append(part) {
3791
+ byteLength += part.length;
3792
+ if (limit !== void 0 && byteLength > limit) throw new SSEParserLimitError();
3793
+ parts.push(part);
3794
+ }
3338
3795
  return async (chunk) => {
3339
- if (buffer) buffer = concatUint8Array(buffer, chunk);
3340
- else {
3341
- buffer = chunk;
3342
- position = 0;
3343
- fieldLength = -1;
3344
- }
3345
- const bufferLength = buffer.length;
3346
- let lineStart = 0;
3347
- while (position < bufferLength) {
3348
- if (discardTrailingNewline) {
3349
- if (buffer[position] === 10) lineStart = ++position;
3350
- discardTrailingNewline = false;
3796
+ let start = 0;
3797
+ for (let cursor = 0; cursor < chunk.length; cursor += 1) {
3798
+ const byte = chunk[cursor];
3799
+ if (afterCR) {
3800
+ afterCR = false;
3801
+ if (byte === 10) {
3802
+ start = cursor + 1;
3803
+ continue;
3804
+ }
3351
3805
  }
3352
- let lineEnd = -1;
3353
- for (; position < bufferLength && lineEnd === -1; ++position) switch (buffer[position]) {
3354
- case 58:
3355
- if (fieldLength === -1) fieldLength = position - lineStart;
3356
- break;
3357
- case 13:
3358
- discardTrailingNewline = true;
3359
- lineEnd = position;
3360
- break;
3361
- case 10: lineEnd = position;
3806
+ if (byte !== 10 && byte !== 13) continue;
3807
+ const segment = chunk.subarray(start, cursor);
3808
+ append(segment);
3809
+ let line = segment;
3810
+ if (parts.length > 1) {
3811
+ line = new Uint8Array(byteLength);
3812
+ let offset = 0;
3813
+ for (const part of parts) {
3814
+ line.set(part, offset);
3815
+ offset += part.length;
3816
+ }
3362
3817
  }
3363
- if (lineEnd === -1) break;
3364
- if (maxBufferSize !== void 0 && lineEnd - lineStart > maxBufferSize) throw new SSEParserLimitError();
3365
- const currentFieldLength = fieldLength === -1 && lineEnd > lineStart ? lineEnd - lineStart : fieldLength;
3366
- const pending = onLine(buffer.subarray(lineStart, lineEnd), currentFieldLength);
3818
+ parts = [];
3819
+ byteLength = 0;
3820
+ afterCR = byte === 13;
3821
+ start = cursor + 1;
3822
+ const pending = onLine(line);
3367
3823
  if (pending) await pending;
3368
- lineStart = position;
3369
- fieldLength = -1;
3370
- }
3371
- if (lineStart === bufferLength) buffer = void 0;
3372
- else if (lineStart !== 0) {
3373
- buffer = buffer.subarray(lineStart);
3374
- position -= lineStart;
3375
3824
  }
3376
- if (maxBufferSize !== void 0 && buffer && buffer.length > maxBufferSize) throw new SSEParserLimitError();
3825
+ if (start < chunk.length) append(chunk.subarray(start));
3377
3826
  };
3378
3827
  }
3828
+ /** Interpret SSE fields and dispatch only when a blank line ends a data-bearing event. */
3379
3829
  function createMessageParser(onId, onRetry, onMessage, options) {
3380
- const maxBufferSize = validateMaxBufferSize(options?.maxBufferSize);
3381
- let lastEventId = "";
3830
+ const limit = validateMaxBufferSize(options?.maxBufferSize);
3831
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
3832
+ let firstLine = true;
3833
+ let id = "";
3382
3834
  let event = "";
3383
3835
  let data = "";
3384
3836
  let dataBytes = 0;
3385
- let hasData = false;
3386
- const decoder = new TextDecoder();
3387
- return (line, fieldLength) => {
3388
- if (line.length === 0) {
3389
- const message = hasData ? {
3390
- id: lastEventId,
3837
+ return (line) => {
3838
+ let content = line;
3839
+ if (firstLine) {
3840
+ firstLine = false;
3841
+ if (line[0] === 239 && line[1] === 187 && line[2] === 191) content = line.subarray(3);
3842
+ }
3843
+ if (content.length === 0) {
3844
+ const message = data === "" ? void 0 : {
3845
+ id,
3391
3846
  event,
3392
3847
  data: data.slice(0, -1)
3393
- } : void 0;
3848
+ };
3394
3849
  event = "";
3395
3850
  data = "";
3396
3851
  dataBytes = 0;
3397
- hasData = false;
3398
3852
  return message ? onMessage?.(message) : void 0;
3399
3853
  }
3400
- /* istanbul ignore if -- @preserve */
3401
- if (fieldLength < 0) return;
3402
- const hasColon = fieldLength < line.length && line[fieldLength] === 58;
3403
- const field = fieldName(line, fieldLength);
3404
- if (field === void 0) return;
3405
- let valueOffset = hasColon ? fieldLength + 1 : line.length;
3406
- if (hasColon && line[valueOffset] === 32) valueOffset += 1;
3407
- const valueBytes = line.subarray(valueOffset);
3408
- const value = decoder.decode(valueBytes);
3854
+ const separator = content.indexOf(58);
3855
+ const field = decoder.decode(separator < 0 ? content : content.subarray(0, separator));
3856
+ let valueStart = separator < 0 ? content.length : separator + 1;
3857
+ if (content[valueStart] === 32) valueStart += 1;
3858
+ const value = decoder.decode(content.subarray(valueStart));
3409
3859
  switch (field) {
3410
3860
  case "data":
3411
- dataBytes += valueBytes.byteLength + 1;
3412
- if (maxBufferSize !== void 0 && dataBytes > maxBufferSize) throw new SSEParserLimitError();
3413
- hasData = true;
3861
+ dataBytes += content.length - valueStart + 1;
3862
+ if (limit !== void 0 && dataBytes > limit) throw new SSEParserLimitError();
3414
3863
  data += `${value}\n`;
3415
3864
  break;
3416
3865
  case "event":
@@ -3418,67 +3867,35 @@ function createMessageParser(onId, onRetry, onMessage, options) {
3418
3867
  break;
3419
3868
  case "id":
3420
3869
  if (!value.includes("\0")) {
3421
- lastEventId = value;
3870
+ id = value;
3422
3871
  onId(value);
3423
3872
  }
3424
3873
  break;
3425
- case "retry": if (/^\d+$/.test(value)) onRetry(Math.min(Number(value), 2147483647));
3874
+ case "retry": if (/^[0-9]+$/.test(value)) onRetry(Math.min(Number(value), 2147483647));
3426
3875
  }
3427
3876
  };
3428
3877
  }
3429
- const DATA = [
3430
- 100,
3431
- 97,
3432
- 116,
3433
- 97
3434
- ];
3435
- const EVENT = [
3436
- 101,
3437
- 118,
3438
- 101,
3439
- 110,
3440
- 116
3441
- ];
3442
- const RETRY = [
3443
- 114,
3444
- 101,
3445
- 116,
3446
- 114,
3447
- 121
3448
- ];
3449
- function sameBytes(line, start, expected) {
3450
- for (let index = 0; index < expected.length; index += 1) if (line[start + index] !== expected[index]) return false;
3451
- return true;
3452
- }
3453
- /**
3454
- * The four field names SSE acts on, matched as bytes so a line does not decode its name.
3455
- * Decoding dropped one leading BOM, so one is skipped here too. Any other name is ignored.
3456
- */
3457
- function fieldName(line, length) {
3458
- const start = length >= 3 && line[0] === 239 && line[1] === 187 && line[2] === 191 ? 3 : 0;
3459
- switch (length - start) {
3460
- case 2: return line[start] === 105 && line[start + 1] === 100 ? "id" : void 0;
3461
- case 4: return sameBytes(line, start, DATA) ? "data" : void 0;
3462
- case 5: return sameBytes(line, start, EVENT) ? "event" : sameBytes(line, start, RETRY) ? "retry" : void 0;
3463
- default: return;
3464
- }
3465
- }
3466
- function validateMaxBufferSize(maxBufferSize) {
3467
- if (maxBufferSize === void 0) return;
3468
- if (!Number.isSafeInteger(maxBufferSize) || maxBufferSize < 1) throw new TypeError("SSE maxBufferSize must be a positive safe integer");
3469
- return maxBufferSize;
3470
- }
3471
- function concatUint8Array(a, b) {
3472
- const buffer = new Uint8Array(a.length + b.length);
3473
- buffer.set(a);
3474
- buffer.set(b, a.length);
3475
- return buffer;
3878
+ function validateMaxBufferSize(value) {
3879
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new TypeError("SSE maxBufferSize must be a positive safe integer");
3880
+ return value;
3476
3881
  }
3477
- //#endregion
3478
- //#region src/sse/transport/event_stream.ts
3882
+ // src/sse/transport/event_stream.ts
3479
3883
  const EVENT_STREAM_CONTENT_TYPE = "text/event-stream";
3480
3884
  const LAST_EVENT_ID_HEADER = "last-event-id";
3481
3885
  const DEFAULT_RETRY_INTERVAL = 1e3;
3886
+ const SSE_RECONNECT_DEFAULTS = {
3887
+ delayMs: DEFAULT_RETRY_INTERVAL,
3888
+ factor: 1,
3889
+ jitter: 0,
3890
+ maxDelayMs: Number.POSITIVE_INFINITY
3891
+ };
3892
+ const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
3893
+ "DELETE",
3894
+ "GET",
3895
+ "HEAD",
3896
+ "OPTIONS",
3897
+ "PUT"
3898
+ ]);
3482
3899
  var EventStreamFatalError = class extends Error {
3483
3900
  code;
3484
3901
  statusSignal;
@@ -3506,6 +3923,9 @@ function toEventStreamFatalError(error) {
3506
3923
  if (error instanceof SSEParserLimitError) return new EventStreamFatalError("CAP_BUFFER_EXCEEDED", error.message, { cause: error });
3507
3924
  }
3508
3925
  async function fetchEventStream(request, options) {
3926
+ const reconnect = normalizeReconnectOptions(options.reconnect, SSE_RECONNECT_DEFAULTS, "SSE");
3927
+ const url = resolveRequestUrl(request);
3928
+ const failure = options.failure ?? {};
3509
3929
  const queue = new AsyncQueue({ maxSize: options.maxQueueSize });
3510
3930
  const closedDeferred = Promise.withResolvers();
3511
3931
  const openDeferred = Promise.withResolvers();
@@ -3515,7 +3935,7 @@ async function fetchEventStream(request, options) {
3515
3935
  if (!headers.has("Accept")) headers.set("Accept", EVENT_STREAM_CONTENT_TYPE);
3516
3936
  let settledClosed = false;
3517
3937
  let settledOpen = false;
3518
- let retryInterval = options.retryInterval ?? options.reconnect?.delayMs ?? DEFAULT_RETRY_INTERVAL;
3938
+ let retryInterval = options.retryInterval ?? reconnect?.delayMs ?? DEFAULT_RETRY_INTERVAL;
3519
3939
  let retryCount = 0;
3520
3940
  let lastEventId = "";
3521
3941
  let lastEventIdDirty = false;
@@ -3590,16 +4010,20 @@ async function fetchEventStream(request, options) {
3590
4010
  }
3591
4011
  async function start() {
3592
4012
  while (!closeController.signal.aborted) {
3593
- if (lastEventIdDirty) {
3594
- lastEventIdDirty = false;
3595
- if (lastEventId) headers.set(LAST_EVENT_ID_HEADER, lastEventId);
3596
- else headers.delete(LAST_EVENT_ID_HEADER);
3597
- }
3598
4013
  const attemptAbort = mergeAbortSignals(closeController.signal, [request.abort]);
3599
4014
  let response;
3600
4015
  let readerStarted = false;
3601
4016
  try {
3602
- const eventRequest = new Request(resolveRequestUrl(request), createEventStreamRequestInit(request, headers, attemptAbort));
4017
+ if (lastEventIdDirty) {
4018
+ if (lastEventId) {
4019
+ const bytes = new TextEncoder().encode(lastEventId);
4020
+ let value = "";
4021
+ for (const byte of bytes) value += String.fromCharCode(byte);
4022
+ headers.set(LAST_EVENT_ID_HEADER, value);
4023
+ } else headers.delete(LAST_EVENT_ID_HEADER);
4024
+ lastEventIdDirty = false;
4025
+ }
4026
+ const eventRequest = new Request(url, createEventStreamRequestInit(request, headers, attemptAbort));
3603
4027
  response = await fetchWithSignal(() => fetchImpl(eventRequest), attemptAbort, cancelLateResponseBody);
3604
4028
  const handshakeResponse = response;
3605
4029
  const open = await createOpenInfo(handshakeResponse, options.handshakeError, attemptAbort).catch((error) => {
@@ -3610,6 +4034,7 @@ async function fetchEventStream(request, options) {
3610
4034
  validateOpenResponse(open);
3611
4035
  if (!response.body) throw new EventStreamFatalError("RES_DECODE_FAILED", "Missing response body for event stream");
3612
4036
  await runFatalHook(() => options.onopen?.(open), attemptAbort, "Event stream onopen callback failed");
4037
+ retryCount = 0;
3613
4038
  if (!settledOpen) {
3614
4039
  settledOpen = true;
3615
4040
  openDeferred.resolve(handle);
@@ -3635,6 +4060,10 @@ async function fetchEventStream(request, options) {
3635
4060
  else finishError(fatalError);
3636
4061
  return;
3637
4062
  }
4063
+ if (isReadableStreamBody(request.body)) {
4064
+ finishError(retryError);
4065
+ return;
4066
+ }
3638
4067
  let retryDelay;
3639
4068
  try {
3640
4069
  retryDelay = await resolveRetryDelay(retryError, attemptAbort);
@@ -3709,27 +4138,21 @@ async function fetchEventStream(request, options) {
3709
4138
  open: latestOpen
3710
4139
  }), signal) : void 0;
3711
4140
  if (next === null) return null;
3712
- const reconnect = options.reconnect;
3713
4141
  if (!reconnect) return null;
3714
- if (reconnect.shouldReconnect) {
3715
- if (!await awaitWithSignal(() => reconnect.shouldReconnect?.({
4142
+ if (options.reconnect?.shouldReconnect) {
4143
+ if (!await awaitWithSignal(() => options.reconnect?.shouldReconnect?.({
3716
4144
  attempt: retryCount,
3717
4145
  cause: error,
3718
4146
  lastEventId,
4147
+ method: request.method,
3719
4148
  open: latestOpen
3720
4149
  }), signal)) return null;
3721
- }
3722
- const attempts = reconnect.attempts ?? 3;
3723
- if (retryCount > attempts) return null;
4150
+ } else if (!IDEMPOTENT_METHODS.has(request.method.toUpperCase())) return null;
4151
+ if (retryCount > reconnect.attempts) return null;
3724
4152
  const delayMs = typeof next === "number" ? next : retryInterval;
3725
- const factor = reconnect.factor ?? 1;
3726
- const jitter = reconnect.jitter ?? 0;
3727
- if (!Number.isFinite(jitter) || jitter < 0 || jitter > 1) throw new RangeError("SSE reconnect jitter is out of range");
3728
4153
  return computeReconnectDelay({
3729
- delayMs,
3730
- factor,
3731
- jitter,
3732
- maxDelayMs: reconnect.maxDelayMs ?? Number.POSITIVE_INFINITY
4154
+ ...reconnect,
4155
+ delayMs
3733
4156
  }, retryCount);
3734
4157
  }
3735
4158
  async function observeFatalError(error, signal) {
@@ -3758,7 +4181,7 @@ async function fetchEventStream(request, options) {
3758
4181
  return;
3759
4182
  }
3760
4183
  const closeCause = signal.reason;
3761
- attachOpenInfo(abortedError, latestOpen);
4184
+ failure.open = latestOpen;
3762
4185
  if (settledOpen) queue.close();
3763
4186
  else {
3764
4187
  settledOpen = true;
@@ -3771,7 +4194,7 @@ async function fetchEventStream(request, options) {
3771
4194
  });
3772
4195
  }
3773
4196
  function finishError(error) {
3774
- attachOpenInfo(error, latestOpen);
4197
+ failure.open = latestOpen;
3775
4198
  if (settledOpen) queue.fail(error);
3776
4199
  else {
3777
4200
  settledOpen = true;
@@ -3850,8 +4273,7 @@ function createEventStreamRequestInit(request, headers, abort) {
3850
4273
  return createFetchInitBase(request, {
3851
4274
  defaultAccept: EVENT_STREAM_CONTENT_TYPE,
3852
4275
  headers,
3853
- signal: abort,
3854
- streamingRequestUnsupportedError: /* @__PURE__ */ new Error("ERR_STREAMING_REQUEST_UNSUPPORTED")
4276
+ signal: abort
3855
4277
  });
3856
4278
  }
3857
4279
  function validateOpenResponse(open) {
@@ -3872,35 +4294,22 @@ function toCloseReason(reason) {
3872
4294
  if (reason instanceof Error) return reason.message;
3873
4295
  if (typeof reason === "string") return reason;
3874
4296
  }
3875
- const errorOpenInfoMap = /* @__PURE__ */ new WeakMap();
3876
- function attachOpenInfo(error, open) {
3877
- if (!open || typeof error !== "object" || error === null) return;
3878
- errorOpenInfoMap.set(error, open);
3879
- }
3880
- function getErrorOpenInfo(error) {
3881
- if (typeof error !== "object" || error === null) return;
3882
- return errorOpenInfoMap.get(error);
3883
- }
3884
- //#endregion
3885
- //#region src/sse/sse.ts
4297
+ // src/sse/sse.ts
3886
4298
  /**
3887
4299
  * Declare a typed Server-Sent Events endpoint.
3888
4300
  *
3889
4301
  * Pass path, event structs, and buffer/queue limits. Call the returned endpoint
3890
4302
  * with input to get an `EventStreamCommand` for `client.execute`.
3891
4303
  *
4304
+ * - **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
+ *
4306
+ * `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
+ *
3892
4308
  * @param definition - SSE contract (path, events, buffer limits, optional input).
3893
4309
  * @returns An endpoint that creates `EventStreamCommand` values from input.
3894
4310
  *
3895
- * @example
3896
- * ```ts
3897
- * const useEvents = defineEventStream({
3898
- * maxBufferSize: 1024,
3899
- * maxQueueSize: 16,
3900
- * path: '/events',
3901
- * events: { message: struct.object({ text: struct.string() }) },
3902
- * })
3903
- * ```
4311
+ * @group sse
4312
+ * @exampleFile examples/api/define-event-stream.ts#example
3904
4313
  */
3905
4314
  function defineEventStream(definition) {
3906
4315
  const endpoint = {
@@ -3917,18 +4326,18 @@ function defineEventStream(definition) {
3917
4326
  }
3918
4327
  return ((input) => create(input));
3919
4328
  }
3920
- function castParsedEventStreamInput(value) {
3921
- return value;
3922
- }
3923
4329
  /**
3924
4330
  * Open an SSE command against the given client config.
3925
4331
  *
3926
4332
  * Prefer `client.execute(command)` in application code; this is the low-level entry used by the client.
3927
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
+ *
3928
4336
  * @param clientConfig - Resolved client configuration.
3929
4337
  * @param command - SSE command from an `EventStreamEndpoint`.
3930
4338
  * @param options - Optional execute options (abort, timeout).
3931
4339
  * @returns Await-result tuple of `[error, stream, open]`.
4340
+ * @group sse
3932
4341
  */
3933
4342
  async function executeEventStreamCommand(clientConfig, command, options) {
3934
4343
  const { endpoint, input } = command;
@@ -3965,8 +4374,15 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
3965
4374
  void 0,
3966
4375
  void 0
3967
4376
  ];
4377
+ let reconnect;
3968
4378
  try {
3969
4379
  validateEventStreamLimits(endpoint);
4380
+ const configured = clientConfig.sse.reconnect;
4381
+ const normalized = normalizeReconnectOptions(configured, SSE_RECONNECT_DEFAULTS, "SSE");
4382
+ reconnect = normalized && {
4383
+ ...normalized,
4384
+ shouldReconnect: configured?.shouldReconnect
4385
+ };
3970
4386
  } catch (error) {
3971
4387
  return [
3972
4388
  createPreflightFault("REQ_OPTIONS_INVALID", error),
@@ -3990,9 +4406,10 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
3990
4406
  void 0
3991
4407
  ];
3992
4408
  };
4409
+ const inputGuides = /* @__PURE__ */ new WeakMap();
3993
4410
  let parsedInput;
3994
4411
  try {
3995
- parsedInput = castParsedEventStreamInput(await parseEndpointInput(endpoint.input, input));
4412
+ parsedInput = parseEndpointInput(endpoint.input, input, inputGuides);
3996
4413
  } catch (error) {
3997
4414
  return stopBeforeTransport(resolveTransportCancellation(deadline, requestSignal, createPreflightFault("REQ_INPUT_INVALID", error)));
3998
4415
  }
@@ -4004,6 +4421,8 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4004
4421
  abort: requestSignal,
4005
4422
  baseEndpoint: clientConfig.endpoint,
4006
4423
  defaultHeaders: clientConfig.headers,
4424
+ inputDeclared: endpoint.input !== void 0,
4425
+ inputGuides,
4007
4426
  operation: endpoint.operation,
4008
4427
  querySerializer: clientConfig.querySerializer,
4009
4428
  timeout: cancellation.timeout,
@@ -4018,6 +4437,8 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4018
4437
  let chainSettled = false;
4019
4438
  let streamDelivered = false;
4020
4439
  const ownedStreams = /* @__PURE__ */ new Set();
4440
+ const transportOrigin = createTransportOrigin();
4441
+ const errorOpenInfo = /* @__PURE__ */ new Map();
4021
4442
  try {
4022
4443
  const sseInterceptors = resolveSSEInterceptors(clientConfig.interceptors);
4023
4444
  const sseHandler = (req) => {
@@ -4026,9 +4447,22 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4026
4447
  rejected.catch(() => void 0);
4027
4448
  return rejected;
4028
4449
  }
4450
+ try {
4451
+ resolveRequestUrl(req);
4452
+ } catch (error) {
4453
+ const rejected = Promise.reject(error);
4454
+ rejected.catch(() => void 0);
4455
+ return rejected;
4456
+ }
4029
4457
  const transportAbort = mergeAbortSignals(requestSignal, [req.abort]);
4030
4458
  if (resolveTransportCancellation(deadline, transportAbort)) {
4031
- const rejected = Promise.reject(markTransportOrigin(resolveAbortCause(transportAbort)));
4459
+ const rejected = Promise.reject(transportOrigin.mark(resolveAbortCause(transportAbort)));
4460
+ releaseAbortSignal(transportAbort);
4461
+ rejected.catch(() => void 0);
4462
+ return rejected;
4463
+ }
4464
+ if (clientConfig.sse.handle === DEFAULT_FETCH && typeof globalThis.fetch !== "function") {
4465
+ const rejected = Promise.reject(transportOrigin.mark(ERR_FETCH_UNSUPPORTED));
4032
4466
  releaseAbortSignal(transportAbort);
4033
4467
  rejected.catch(() => void 0);
4034
4468
  return rejected;
@@ -4041,16 +4475,21 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4041
4475
  releaseAbortSignal(streamAbort);
4042
4476
  releaseAbortSignal(transportAbort);
4043
4477
  };
4478
+ const failure = {};
4044
4479
  const promise = fetchEventStream({
4045
4480
  ...req,
4046
4481
  abort: streamAbort
4047
4482
  }, {
4048
4483
  fetch: clientConfig.sse.handle,
4484
+ failure,
4049
4485
  handshakeError: endpoint.error,
4486
+ onopen(open) {
4487
+ owned.initialOpen ??= open;
4488
+ },
4050
4489
  transformMessage(message, signal) {
4051
4490
  return transformStreamMessage(endpoint.events, message, clientConfig.sse.onInvalidEvent, signal);
4052
4491
  },
4053
- reconnect: clientConfig.sse.reconnect,
4492
+ reconnect,
4054
4493
  maxBufferSize: endpoint.maxBufferSize,
4055
4494
  maxQueueSize: endpoint.maxQueueSize
4056
4495
  }).then((stream) => {
@@ -4059,18 +4498,27 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4059
4498
  return stream;
4060
4499
  }, (cause) => {
4061
4500
  owned.release?.();
4062
- throw markTransportOrigin(cause);
4501
+ errorOpenInfo.set(cause, failure.open);
4502
+ throw transportOrigin.mark(cause);
4063
4503
  });
4064
4504
  promise.catch(() => void 0);
4065
4505
  return promise;
4066
4506
  };
4067
4507
  const sseChain = makeChain(sseInterceptors);
4068
4508
  const stream = await awaitWithSignal(() => sseChain(request, sseHandler), requestSignal);
4509
+ const publicOpen = stream?.open;
4510
+ if (typeof stream?.closed?.then !== "function" || typeof stream.close !== "function" || typeof stream[Symbol.asyncIterator] !== "function" || typeof stream[Symbol.asyncDispose] !== "function" || typeof publicOpen?.url !== "string") throw new TypeError("SSE interceptor must resolve an event stream handle");
4069
4511
  chainSettled = true;
4070
- streamDelivered = true;
4071
- stream.closed.then(() => releaseAbortSignal(requestSignal));
4072
- const open = stream.open;
4512
+ const release = () => releaseAbortSignal(requestSignal);
4513
+ stream.closed.then(release, release);
4514
+ let open;
4515
+ for (const owned of ownedStreams) if (owned.stream && (owned.stream === stream || owned.stream.closed === stream.closed && publicOpen === owned.stream.open)) {
4516
+ open = owned.initialOpen;
4517
+ break;
4518
+ }
4519
+ open ??= publicOpen;
4073
4520
  discardOwnedStreams(/* @__PURE__ */ new Error("SSE interceptor discarded an opened stream"), stream);
4521
+ streamDelivered = true;
4074
4522
  return [
4075
4523
  null,
4076
4524
  stream,
@@ -4079,11 +4527,11 @@ async function runEventStreamCommand(clientConfig, endpoint, input, config, cont
4079
4527
  } catch (error) {
4080
4528
  chainSettled = true;
4081
4529
  discardOwnedStreams(error);
4082
- const fromTransport = isTransportOrigin(error);
4530
+ const fromTransport = transportOrigin.has(error);
4083
4531
  const cause = error;
4084
- const openInfo = getErrorOpenInfo(cause);
4532
+ const openInfo = errorOpenInfo.get(cause);
4085
4533
  return [
4086
- requestSignal.aborted ? resolveAbortedFault(requestSignal) : createEventStreamFault(cause, fromTransport, openInfo?.response),
4534
+ requestSignal.aborted ? resolveAbortedFault(requestSignal) : error instanceof RequestUrlError ? createPreflightFault("REQ_BUILD_FAILED", error) : createEventStreamFault(cause, fromTransport, openInfo?.response),
4087
4535
  void 0,
4088
4536
  openInfo
4089
4537
  ];
@@ -4173,10 +4621,10 @@ function createEventStreamFault(cause, fromTransport, response) {
4173
4621
  return createUndecodedHttpStatusFault(meta);
4174
4622
  }
4175
4623
  if (response && fatal === "RES_DECODE_FAILED") return createDecodeFault(fatal, handshakeDecodeCause(cause), toEventStreamMeta(response));
4624
+ if (fromTransport && (cause === ERR_STREAMING_REQUEST_UNSUPPORTED || cause === ERR_FETCH_UNSUPPORTED)) return createPreflightFault("ENV_UNSUPPORTED", cause);
4176
4625
  return fromTransport ? createNetworkFault(cause) : createPreflightFault("EXT_INTERCEPTOR_FAILED", cause);
4177
4626
  }
4178
- //#endregion
4179
- //#region src/struct/runtime.ts
4627
+ // src/struct/runtime.ts
4180
4628
  function createPrimitiveStruct(definition) {
4181
4629
  return castStruct(makeStruct({
4182
4630
  ...definition,
@@ -4224,8 +4672,16 @@ function makeStruct(definition) {
4224
4672
  }
4225
4673
  };
4226
4674
  }
4227
- //#endregion
4228
- //#region src/struct/constructors.ts
4675
+ // src/struct/constructors.ts
4676
+ /**
4677
+ * Struct that accepts string values.
4678
+ *
4679
+ * - Accepts `string` values only; anything else fails with `invalid_type`.
4680
+ *
4681
+ * - As a body root, or as an inbound `output` / `error` struct, the bytes stay text instead of being parsed as JSON.
4682
+ *
4683
+ * @group struct
4684
+ */
4229
4685
  function createStringStruct() {
4230
4686
  return castStruct(createPrimitiveStruct({
4231
4687
  expected: "string",
@@ -4233,6 +4689,15 @@ function createStringStruct() {
4233
4689
  kind: "string"
4234
4690
  }));
4235
4691
  }
4692
+ /**
4693
+ * Struct that accepts number values except NaN.
4694
+ *
4695
+ * - Accepts `number` values, including `Infinity`. `NaN` and non-numbers fail with `invalid_type`.
4696
+ *
4697
+ * - Strings are not coerced.
4698
+ *
4699
+ * @group struct
4700
+ */
4236
4701
  function createNumberStruct() {
4237
4702
  return castStruct(createPrimitiveStruct({
4238
4703
  expected: "number",
@@ -4240,6 +4705,13 @@ function createNumberStruct() {
4240
4705
  kind: "number"
4241
4706
  }));
4242
4707
  }
4708
+ /**
4709
+ * Struct that accepts boolean values.
4710
+ *
4711
+ * Accepts `true` and `false` only. Other values, including `'true'` and `1`, fail with `invalid_type`.
4712
+ *
4713
+ * @group struct
4714
+ */
4243
4715
  function createBooleanStruct() {
4244
4716
  return createPrimitiveStruct({
4245
4717
  expected: "boolean",
@@ -4247,6 +4719,15 @@ function createBooleanStruct() {
4247
4719
  kind: "boolean"
4248
4720
  });
4249
4721
  }
4722
+ /**
4723
+ * Struct that accepts only null.
4724
+ *
4725
+ * Accepts `null` only. Every other value fails with `invalid_type`. That includes `undefined` at the root or in an array or tuple; a missing object or record field reports `missing_key` instead.
4726
+ *
4727
+ * This is not the `.null()` modifier. The modifier adds `null` to the values another struct accepts.
4728
+ *
4729
+ * @group struct
4730
+ */
4250
4731
  function createNullStruct() {
4251
4732
  return castStruct(createPrimitiveStruct({
4252
4733
  expected: "null",
@@ -4254,18 +4735,51 @@ function createNullStruct() {
4254
4735
  kind: "null"
4255
4736
  }));
4256
4737
  }
4738
+ /**
4739
+ * Struct that accepts any value except null and undefined, without validation.
4740
+ *
4741
+ * The value is returned unchanged, and nothing inside it is validated or stripped.
4742
+ *
4743
+ * - Chain `.null()`, `.optional()`, or `.nullish()` to accept `null` or `undefined`.
4744
+ *
4745
+ * - The output type is `any`. Prefer {@link struct.unknown}() when the caller should narrow the value.
4746
+ *
4747
+ * @group struct
4748
+ */
4257
4749
  function createAnyStruct() {
4258
4750
  return castStruct(makeStruct({
4259
4751
  flags: DEFAULT_FLAGS,
4260
4752
  kind: "any"
4261
4753
  }));
4262
4754
  }
4755
+ /**
4756
+ * Struct that accepts any value except null and undefined, typed as unknown.
4757
+ *
4758
+ * The value is returned unchanged, and nothing inside it is validated or stripped.
4759
+ *
4760
+ * - Chain `.null()`, `.optional()`, or `.nullish()` to accept `null` or `undefined`.
4761
+ *
4762
+ * - The output type is `unknown`, so the caller must narrow it before use.
4763
+ *
4764
+ * @group struct
4765
+ */
4263
4766
  function createUnknownStruct() {
4264
4767
  return castStruct(makeStruct({
4265
4768
  flags: DEFAULT_FLAGS,
4266
4769
  kind: "unknown"
4267
4770
  }));
4268
4771
  }
4772
+ /**
4773
+ * Struct that accepts exactly one boolean, null, number, or string value.
4774
+ *
4775
+ * - Accepts only `value`. The comparison uses `Object.is`, so any other value fails with `invalid_literal`.
4776
+ *
4777
+ * - `value` must be a `boolean`, `null`, `number`, or `string`.
4778
+ *
4779
+ * - Use it for the tag field of {@link struct.discriminatedUnion}().
4780
+ *
4781
+ * @group struct
4782
+ */
4269
4783
  function createLiteralStruct(value) {
4270
4784
  return castStruct(makeStruct({
4271
4785
  expected: describeValue(value),
@@ -4293,6 +4807,19 @@ function createObjectEnumStruct(value) {
4293
4807
  values
4294
4808
  }));
4295
4809
  }
4810
+ /**
4811
+ * Struct that parses an array with one item struct.
4812
+ *
4813
+ * - Parses every element with `item` and outputs a new array. A non-array fails with `invalid_type` (expected `array`).
4814
+ *
4815
+ * - Parsing stops at the first failing element, and `issue.path` holds its index.
4816
+ *
4817
+ * - Throws `TypeError` when `item` is not a struct.
4818
+ *
4819
+ * `ArrayStruct<S>` is not exported. It is a `Struct<StructInput<S>[], Infer<S>[]>`.
4820
+ *
4821
+ * @group struct
4822
+ */
4296
4823
  function createArrayStruct(item) {
4297
4824
  assertStruct(item, "array item");
4298
4825
  return castStruct(makeStruct({
@@ -4301,6 +4828,31 @@ function createArrayStruct(item) {
4301
4828
  kind: "array"
4302
4829
  }));
4303
4830
  }
4831
+ /**
4832
+ * Struct that parses a plain object with a fixed set of fields.
4833
+ *
4834
+ * `shape` maps field names to structs.
4835
+ *
4836
+ * - Every field is **required** by default. A missing key fails with `missing_key`.
4837
+ *
4838
+ * - `.optional()` and `.nullish()` allow a missing key. An absent optional field stays absent in the output.
4839
+ *
4840
+ * - `.null()` / `.nullable()` allow `null`.
4841
+ *
4842
+ * - `.alias(name)` sets the field's wire key. `struct.parse` reads logical keys by default; `{ aliases: true }` and HTTP JSON decoding read wire keys, and HTTP JSON encoding writes them. Parsed output uses logical keys.
4843
+ *
4844
+ * - Keys that are not in `shape` are removed from the output. The output is a new object with a `null` prototype.
4845
+ *
4846
+ * - The input must be a plain object. Arrays, `Map`, and class instances fail with `invalid_type` (expected `object`).
4847
+ *
4848
+ * - Parsing stops at the first failing field, and `issue.path` holds its key.
4849
+ *
4850
+ * - Throws `TypeError` when `shape` is not a plain object.
4851
+ *
4852
+ * `ObjectShape` is `{ [key: string]: any }`; each value must be a struct.
4853
+ *
4854
+ * @group struct
4855
+ */
4304
4856
  function createObjectStruct(shape) {
4305
4857
  if (!isPlainObject(shape)) throw new TypeError("object struct requires a plain object");
4306
4858
  return castStruct(makeStruct({
@@ -4310,9 +4862,35 @@ function createObjectStruct(shape) {
4310
4862
  shape
4311
4863
  }));
4312
4864
  }
4865
+ /**
4866
+ * Object struct that is encoded as an URL-encoded form when it is the request body.
4867
+ *
4868
+ * Parses like {@link struct.object}(), and carries a `urlencoded` codec tag.
4869
+ *
4870
+ * - The tag applies only when this struct's value is the request body root. The body is then sent as `URLSearchParams` with `application/x-www-form-urlencoded;charset=UTF-8`.
4871
+ *
4872
+ * - Arrays repeat the field name and `undefined` fields are skipped. A nested object or a `Blob` fails the request with `REQ_BUILD_FAILED`.
4873
+ *
4874
+ * - `struct.parse` ignores the tag. It does not accept a `URLSearchParams` instance, only a plain object.
4875
+ *
4876
+ * @group struct
4877
+ */
4313
4878
  function createUrlencodedBodyStruct(shape) {
4314
4879
  return createCodecObjectStruct(shape, "urlencoded");
4315
4880
  }
4881
+ /**
4882
+ * Object struct that is encoded as multipart FormData when it is the request body.
4883
+ *
4884
+ * Parses like {@link struct.object}(), and carries a `formData` codec tag.
4885
+ *
4886
+ * - The tag applies only when this struct's value is the request body root. The body is then sent as `FormData` with a platform multipart boundary.
4887
+ *
4888
+ * - Arrays repeat the field name, `undefined` fields are skipped, and `Blob` / `File` values are appended as files. A nested object fails the request with `REQ_BUILD_FAILED`.
4889
+ *
4890
+ * - `struct.parse` ignores the tag. It does not accept a `FormData` instance, only a plain object.
4891
+ *
4892
+ * @group struct
4893
+ */
4316
4894
  function createFormDataBodyStruct(shape) {
4317
4895
  return createCodecObjectStruct(shape, "formData");
4318
4896
  }
@@ -4321,6 +4899,21 @@ function createCodecObjectStruct(shape, codec) {
4321
4899
  created[DEFINITION].codec = codec;
4322
4900
  return created;
4323
4901
  }
4902
+ /**
4903
+ * Struct that parses an object with string keys and one value struct.
4904
+ *
4905
+ * - Parses every own key of a plain object with `value`. Anything else fails with `invalid_type` (expected `record`).
4906
+ *
4907
+ * - Parsing stops at the first failing value, and `issue.path` holds its key.
4908
+ *
4909
+ * - The output is a new object with a `null` prototype.
4910
+ *
4911
+ * - Throws `TypeError` when `value` is not a struct.
4912
+ *
4913
+ * `RecordStruct<S>` is not exported. It is a `Struct<{ [key: string]: StructInput<S> }, { [key: string]: Output }>`, where `Output` is `Infer<S>` without `undefined`. When `S` is an optional struct, a value that is `undefined` omits its key from the output.
4914
+ *
4915
+ * @group struct
4916
+ */
4324
4917
  function createRecordStruct(value) {
4325
4918
  assertStruct(value, "record value");
4326
4919
  return castStruct(makeStruct({
@@ -4329,6 +4922,25 @@ function createRecordStruct(value) {
4329
4922
  value
4330
4923
  }));
4331
4924
  }
4925
+ /**
4926
+ * Struct that parses a fixed-length array with one struct per position.
4927
+ *
4928
+ * `struct.tuple` takes **one array** of structs, not separate arguments:
4929
+ *
4930
+ * ```ts
4931
+ * const Pair = struct.tuple([struct.string(), struct.number()]);
4932
+ * ```
4933
+ *
4934
+ * - Each position is parsed with the struct at the same index. The input must be an array of exactly the same length, otherwise it fails with `invalid_type` (expected `tuple of length N`).
4935
+ *
4936
+ * - Passing the structs as separate arguments, `struct.tuple(a, b)`, throws a `TypeError`.
4937
+ *
4938
+ * - Throws `TypeError` when an item is not a struct.
4939
+ *
4940
+ * `TupleStruct<T>` is not exported. It is a `Struct` whose input and output are tuples of the item types.
4941
+ *
4942
+ * @group struct
4943
+ */
4332
4944
  function createTupleStruct(items) {
4333
4945
  const tupleItems = [...items];
4334
4946
  for (const item of tupleItems) assertStruct(item, "tuple item");
@@ -4378,6 +4990,28 @@ function identityEncodeShape(struct) {
4378
4990
  function isIdentityEncodeKind(kind) {
4379
4991
  return kind === "any" || kind === "boolean" || kind === "enum" || kind === "literal" || kind === "null" || kind === "number" || kind === "string" || kind === "unknown";
4380
4992
  }
4993
+ /**
4994
+ * Struct that picks one object struct by the value of a tag field.
4995
+ *
4996
+ * ```ts
4997
+ * const Event = struct.discriminatedUnion("kind", [
4998
+ * struct.object({ kind: struct.literal("created"), id: struct.number() }),
4999
+ * struct.object({ kind: struct.literal("deleted"), id: struct.number() }),
5000
+ * ]);
5001
+ * ```
5002
+ *
5003
+ * - Every option must be an object struct whose `discriminator` field is a required {@link struct.literal}(). A missing field, a non-literal field, an optional field, or a duplicate tag value throws `TypeError` when the struct is created.
5004
+ *
5005
+ * - The input must be a plain object. The tag value selects the option, and that option parses the whole object.
5006
+ *
5007
+ * - A missing tag fails with `missing_key`, and an unknown tag fails with `invalid_union`. Both report the tag field's path. Other failures come from the selected option.
5008
+ *
5009
+ * - With `struct.parse(..., { aliases: true })` the tag is read from the field's alias.
5010
+ *
5011
+ * `RequiredDiscriminatorOptions` and `DiscriminatedUnionStruct` are not exported. The type check rejects an option whose tag field is missing or may be `undefined`.
5012
+ *
5013
+ * @group struct
5014
+ */
4381
5015
  function createDiscriminatedUnionStruct(discriminator, options) {
4382
5016
  const unionOptions = [...options];
4383
5017
  const map = /* @__PURE__ */ new Map();
@@ -4388,7 +5022,6 @@ function createDiscriminatedUnionStruct(discriminator, options) {
4388
5022
  for (const option of unionOptions) {
4389
5023
  assertStruct(option, "discriminatedUnion option");
4390
5024
  const optionDef = option[DEFINITION];
4391
- /* istanbul ignore next -- type-safe: createDiscriminatedUnionStruct only accepts ObjectStruct */
4392
5025
  if (optionDef.kind !== "object") throw new TypeError("discriminatedUnion options must be object structs");
4393
5026
  const descriptor = optionDef.cache.declaredDescriptors[discriminator];
4394
5027
  const fieldStruct = descriptor && typeof descriptor.get === "function" ? descriptor.get.call(optionDef.shape) : descriptor?.value;
@@ -4418,6 +5051,17 @@ function createDiscriminatedUnionStruct(discriminator, options) {
4418
5051
  wireKeyByValue
4419
5052
  }));
4420
5053
  }
5054
+ /**
5055
+ * Struct that accepts a Blob.
5056
+ *
5057
+ * - Accepts a `Blob` (a `File` is a `Blob`). Anything else fails with `invalid_type`.
5058
+ *
5059
+ * - As a body root it is sent as is, with the blob's own type or `application/octet-stream`.
5060
+ *
5061
+ * - As an `output` or `error` struct it makes the response body be read as a `Blob`.
5062
+ *
5063
+ * @group struct
5064
+ */
4421
5065
  function createBlobStruct() {
4422
5066
  return createPrimitiveStruct({
4423
5067
  expected: "Blob",
@@ -4426,6 +5070,17 @@ function createBlobStruct() {
4426
5070
  runtimeIs: (value) => typeof Blob !== "undefined" && value instanceof Blob
4427
5071
  });
4428
5072
  }
5073
+ /**
5074
+ * Struct that accepts a bigint or a bigint string and outputs a bigint.
5075
+ *
5076
+ * - Accepts a `bigint`, or a string that `BigInt()` can parse. A `number` is rejected.
5077
+ *
5078
+ * - Output is always a `bigint`; an unparsable string fails with `invalid_type`.
5079
+ *
5080
+ * - When encoded for the wire, the value becomes a decimal string.
5081
+ *
5082
+ * @group struct
5083
+ */
4429
5084
  function createBigIntStruct() {
4430
5085
  return createPrimitiveStruct({
4431
5086
  decode: (input, path, fail) => {
@@ -4443,6 +5098,17 @@ function createBigIntStruct() {
4443
5098
  runtimeIs: (value) => typeof value === "bigint"
4444
5099
  });
4445
5100
  }
5101
+ /**
5102
+ * Struct that accepts a Date, timestamp, or date string and outputs a Date.
5103
+ *
5104
+ * - Accepts a `Date`, a millisecond timestamp, or a string. A valid `Date` is returned as the same object. A number or string is converted with `new Date(value)`.
5105
+ *
5106
+ * - Output is a `Date`. A value that produces an invalid date fails with `invalid_type`.
5107
+ *
5108
+ * - When encoded for the wire, the value becomes an ISO 8601 string (`toISOString()`).
5109
+ *
5110
+ * @group struct
5111
+ */
4446
5112
  function createDateStruct() {
4447
5113
  return createPrimitiveStruct({
4448
5114
  decode: (input, path, fail) => {
@@ -4460,20 +5126,20 @@ function createDateStruct() {
4460
5126
  function createIntersectionStruct(...structs) {
4461
5127
  if (structs.length === 0) throw new TypeError("intersection requires at least one struct");
4462
5128
  for (const struct of structs) assertStruct(struct, "intersection item");
4463
- if (structs.length === 1) return castStruct(structs[0]);
5129
+ if (structs.length === 1) return structs[0];
4464
5130
  const objectSides = structs.every(isObjectIntersectionSide);
4465
5131
  const options = objectSides ? flattenObjectIntersectionSides(structs) : structs;
4466
- return castStruct(makeStruct({
5132
+ return makeStruct({
4467
5133
  expected: structs.map((item) => expectedType(item[DEFINITION])).join(" & "),
4468
5134
  flags: DEFAULT_FLAGS,
4469
5135
  kind: "intersection",
4470
5136
  objectSides,
4471
5137
  options
4472
- }));
5138
+ });
4473
5139
  }
4474
5140
  function isObjectIntersectionSide(struct) {
4475
5141
  const definition = struct[DEFINITION];
4476
- return definition.kind === "object" || definition.kind === "intersection" && definition.objectSides;
5142
+ return definition.kind === "object" || definition.kind === "intersection" && definition.objectSides && !definition.flags.nullable;
4477
5143
  }
4478
5144
  function flattenObjectIntersectionSides(structs) {
4479
5145
  const options = [];
@@ -4484,6 +5150,17 @@ function flattenObjectIntersectionSides(structs) {
4484
5150
  }
4485
5151
  return options;
4486
5152
  }
5153
+ /**
5154
+ * Struct that accepts a File.
5155
+ *
5156
+ * - Accepts a `File` only; a plain `Blob` fails with `invalid_type`.
5157
+ *
5158
+ * - Use it for a field of {@link struct.formData}(), or for a raw file body.
5159
+ *
5160
+ * - As an `output` or `error` struct it makes the response body be read as a blob.
5161
+ *
5162
+ * @group struct
5163
+ */
4487
5164
  function createFileStruct() {
4488
5165
  return createPrimitiveStruct({
4489
5166
  expected: "File",
@@ -4492,6 +5169,17 @@ function createFileStruct() {
4492
5169
  runtimeIs: (value) => typeof File !== "undefined" && value instanceof File
4493
5170
  });
4494
5171
  }
5172
+ /**
5173
+ * Struct that accepts an ArrayBuffer.
5174
+ *
5175
+ * - Accepts an `ArrayBuffer` only. Typed arrays and other views fail with `invalid_type`.
5176
+ *
5177
+ * - As a body root it is sent as bytes with `application/octet-stream`.
5178
+ *
5179
+ * - As an `output` or `error` struct it makes the response body be read as an `ArrayBuffer`.
5180
+ *
5181
+ * @group struct
5182
+ */
4495
5183
  function createArrayBufferStruct() {
4496
5184
  return createPrimitiveStruct({
4497
5185
  expected: "ArrayBuffer",
@@ -4500,21 +5188,90 @@ function createArrayBufferStruct() {
4500
5188
  runtimeIs: (value) => typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer
4501
5189
  });
4502
5190
  }
4503
- //#endregion
4504
- //#region src/struct/facade.ts
5191
+ // src/struct/facade.ts
4505
5192
  function structEnum(value) {
4506
5193
  if (Array.isArray(value)) return createEnumStruct(value);
4507
5194
  return createObjectEnumStruct(value);
4508
5195
  }
5196
+ /**
5197
+ * Struct that accepts a value matching any one of several structs.
5198
+ *
5199
+ * `struct.or` is variadic: pass each option as its own argument.
5200
+ *
5201
+ * - Tries the options in order and returns the result of the first one that parses.
5202
+ *
5203
+ * - If none matches, it fails with one `invalid_union` issue at the union's own path. The issues of the individual options are not reported.
5204
+ *
5205
+ * - Throws `TypeError` when an option is not a struct.
5206
+ *
5207
+ * `UnionStruct<T>` is not exported. It is a `Struct` whose input and output are the unions of the option types, without `undefined`. For tagged objects prefer {@link struct.discriminatedUnion}(), which reports the issue of the selected option.
5208
+ *
5209
+ * @group struct
5210
+ */
4509
5211
  function structOr(...options) {
4510
5212
  return createUnionStruct(options);
4511
5213
  }
4512
5214
  /**
4513
- * Facade for building protocol-boundary structs and parsing values.
5215
+ * Facade object for building protocol-boundary structs and parsing values with its constructors and `struct.parse`.
4514
5216
  *
4515
- * Prefer this entry point over internal constructors. Compose shapes with
5217
+ * Prefer this object over importing the constructor functions one by one. Compose shapes with
4516
5218
  * `struct.object`, then parse with `struct.parse` or let HTTP/SSE/WebSocket
4517
- * endpoints apply the same contracts at the wire boundary.
5219
+ * endpoints apply the same contracts at the wire boundary. `struct` is a plain object of helpers.
5220
+ *
5221
+ * ```ts
5222
+ * import { struct } from "@defjs/core";
5223
+ *
5224
+ * const User = struct.object({
5225
+ * id: struct.number(),
5226
+ * displayName: struct.string().alias("display_name"),
5227
+ * });
5228
+ *
5229
+ * function readUser() {
5230
+ * const [error, user] = struct.parse(User, { id: 1, displayName: "Ada" });
5231
+ * if (error) {
5232
+ * console.error(error.message);
5233
+ * return;
5234
+ * }
5235
+ * console.log(user.displayName);
5236
+ * }
5237
+ *
5238
+ * readUser();
5239
+ * ```
5240
+ *
5241
+ * ### Primitives and containers
5242
+ *
5243
+ * | Helper | Role |
5244
+ * | -------------------------------------------------------------------------------- | --------------------------------------------------------- |
5245
+ * | {@link struct.string}() | Text |
5246
+ * | {@link struct.number}() | Number, `NaN` rejected |
5247
+ * | {@link struct.boolean}() | Boolean |
5248
+ * | {@link struct.bigint}() | `bigint`, or a string that parses as one |
5249
+ * | {@link struct.date}() | `Date`, from a `Date`, timestamp, or string |
5250
+ * | {@link struct.literal}(value) | Exact literal |
5251
+ * | {@link struct.null}() | Only `null` |
5252
+ * | {@link struct.any}() / unknown() ({@link struct.unknown}) | Any value except `null` and `undefined` |
5253
+ * | {@link struct.array}(item) | Array of one item struct |
5254
+ * | {@link struct.tuple}(items) | Fixed-length array; `items` is one array of structs |
5255
+ * | {@link struct.record}(value) | Plain object with string keys and one value struct |
5256
+ * | {@link struct.object}(shape) | Plain object with fixed fields; unknown keys are stripped |
5257
+ * | {@link struct.or}(...options) | First matching option |
5258
+ * | {@link struct.intersection}(...structs) | Every struct must match; object results are merged |
5259
+ * | {@link struct.discriminatedUnion}(tag, options) | Object option picked by the value of the `tag` field |
5260
+ * | {@link struct.enum}(values) | String array, or the values of a `{ Name: value }` object |
5261
+ *
5262
+ * ### Body codecs and binary values
5263
+ *
5264
+ * | Helper | Role |
5265
+ * | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
5266
+ * | {@link struct.formData}(shape) / urlencoded(shape) ({@link struct.urlencoded}) | Object struct plus a codec tag. The tag applies only when that value is the request body root |
5267
+ * | {@link struct.arrayBuffer}() / blob() ({@link struct.blob}) / file() ({@link struct.file}) | Binary values. As a body root they are sent as bytes; as `output` / `error` they select that representation |
5268
+ * | {@link struct.string}() | Text. As a body root or an inbound `string` struct, the bytes stay text |
5269
+ *
5270
+ * ### Parsing and modifiers
5271
+ *
5272
+ * {@link struct.parse}(struct, value, options?) parses an unknown value and returns `[null, value]` or `[StructError, undefined]`.
5273
+ *
5274
+ * Every struct has the chainable modifiers `.optional()`, `.null()` / `.nullable()`, `.nullish()`, and `.alias(name)`. See {@link StructMethods}.
4518
5275
  *
4519
5276
  * @example
4520
5277
  * ```ts
@@ -4531,8 +5288,18 @@ function structOr(...options) {
4531
5288
  * output: User,
4532
5289
  * })
4533
5290
  *
4534
- * const [error, user] = struct.parse(User, { id: 1, displayName: 'Ada' })
5291
+ * function readUser() {
5292
+ * const [error, user] = struct.parse(User, { id: 1, displayName: 'Ada' })
5293
+ * if (error) {
5294
+ * console.error(error.message)
5295
+ * return
5296
+ * }
5297
+ * console.log(user.displayName)
5298
+ * }
5299
+ *
5300
+ * readUser()
4535
5301
  * ```
5302
+ * @group struct
4536
5303
  */
4537
5304
  const struct = {
4538
5305
  any: createAnyStruct,
@@ -4559,11 +5326,10 @@ const struct = {
4559
5326
  unknown: createUnknownStruct,
4560
5327
  urlencoded: createUrlencodedBodyStruct
4561
5328
  };
4562
- //#endregion
4563
- //#region src/web_socket/build.ts
5329
+ // src/web_socket/build.ts
4564
5330
  function createWebSocketRequest(params) {
4565
5331
  const { queryParams, queryString } = resolveTransportQuery(params.query, params.querySerializer);
4566
- return {
5332
+ const request = {
4567
5333
  abort: params.abort,
4568
5334
  baseEndpoint: params.baseEndpoint,
4569
5335
  endpoint: fillUrl(params.path, params.params),
@@ -4574,6 +5340,8 @@ function createWebSocketRequest(params) {
4574
5340
  queryString,
4575
5341
  withCredentials: params.withCredentials ?? false
4576
5342
  };
5343
+ resolveRequestUrl(request);
5344
+ return request;
4577
5345
  }
4578
5346
  function createWebSocketUrlFromRequest(request) {
4579
5347
  const url = resolveRequestUrl(request);
@@ -4581,11 +5349,10 @@ function createWebSocketUrlFromRequest(request) {
4581
5349
  else if (url.protocol === "http:") url.protocol = "ws:";
4582
5350
  return url.toString();
4583
5351
  }
4584
- //#endregion
4585
- //#region src/web_socket/codec.ts
5352
+ // src/web_socket/codec.ts
4586
5353
  function serializeOutgoingWebSocketMessage(outgoing, message) {
4587
5354
  if (!outgoing) throw new Error("No outgoing WebSocket messages are declared for this endpoint");
4588
- const wire = JSON.stringify(encodeJson(outgoing, parseStructValue(outgoing, message)));
5355
+ const wire = JSON.stringify(encodeJson(outgoing, parseStructValue(outgoing, message)), (key, value) => rejectNonJson(key, value, "Outgoing WebSocket message"));
4589
5356
  if (typeof wire !== "string") throw new TypeError("Outgoing WebSocket message has no JSON representation");
4590
5357
  return wire;
4591
5358
  }
@@ -4605,8 +5372,7 @@ function extractCloseInfo(event, cause) {
4605
5372
  wasClean: event?.wasClean
4606
5373
  };
4607
5374
  }
4608
- //#endregion
4609
- //#region src/web_socket/heartbeat.ts
5375
+ // src/web_socket/heartbeat.ts
4610
5376
  const MAX_TIMER_DELAY_MS = 2147483647;
4611
5377
  function startHeartbeat(socket, sessionController, config, outgoing, onFatal, openState) {
4612
5378
  stopHeartbeat(sessionController);
@@ -4623,6 +5389,7 @@ function startHeartbeat(socket, sessionController, config, outgoing, onFatal, op
4623
5389
  stop() {
4624
5390
  clearInterval(interval);
4625
5391
  clearTimeout(ackTimer);
5392
+ interval = void 0;
4626
5393
  ackTimer = void 0;
4627
5394
  }
4628
5395
  };
@@ -4637,20 +5404,19 @@ function startHeartbeat(socket, sessionController, config, outgoing, onFatal, op
4637
5404
  if (!isActive() || typeof nextMessage === "undefined") return;
4638
5405
  const serialized = serializeOutgoingWebSocketMessage(outgoing, nextMessage);
4639
5406
  if (!isActive()) return;
5407
+ if (typeof config.timeoutMs === "number" && config.timeoutMs > 0) ackTimer = setTimeout(() => {
5408
+ fail(/* @__PURE__ */ new Error("WebSocket heartbeat timeout"));
5409
+ }, config.timeoutMs);
4640
5410
  socket.send(serialized);
4641
- if (!isActive()) return;
4642
5411
  } catch (error) {
4643
5412
  fail(error);
4644
- return;
4645
5413
  }
4646
- if (typeof config.timeoutMs === "number" && config.timeoutMs > 0) ackTimer = setTimeout(() => {
4647
- fail(/* @__PURE__ */ new Error("WebSocket heartbeat timeout"));
4648
- }, config.timeoutMs);
4649
5414
  }, config.intervalMs);
4650
5415
  function fail(error) {
5416
+ runtime.stop();
4651
5417
  /* istanbul ignore if -- @preserve defensive: a cleared platform timeout callback may already be queued when the runtime stops */
4652
5418
  if (!isActive()) return;
4653
- stopHeartbeat(sessionController);
5419
+ sessionController.heartbeat = void 0;
4654
5420
  onFatal(error);
4655
5421
  }
4656
5422
  }
@@ -4666,29 +5432,19 @@ function stopHeartbeat(sessionController) {
4666
5432
  function assertTimerDelay(field, value) {
4667
5433
  if (!Number.isFinite(value) || value <= 0 || value > MAX_TIMER_DELAY_MS) throw new RangeError(`WebSocket heartbeat ${field} must be between 0 and ${MAX_TIMER_DELAY_MS}`);
4668
5434
  }
4669
- //#endregion
4670
- //#region src/web_socket/reconnect.ts
5435
+ // src/web_socket/reconnect.ts
4671
5436
  function normalizeReconnectConfig(config) {
4672
- if (!config) return;
4673
- const reconnectPredicate = config.shouldReconnect;
4674
- const attempts = config.attempts ?? 3;
4675
- if (!Number.isSafeInteger(attempts) || attempts < 0) throw new RangeError("WebSocket reconnect attempts must be a non-negative safe integer");
4676
- if (attempts === 0) return;
4677
- const delayMs = config.delayMs ?? 1e3;
4678
- assertFiniteRange("delayMs", delayMs, 0);
4679
- const factor = config.factor ?? 2;
4680
- assertFiniteRange("factor", factor, Number.MIN_VALUE);
4681
- const jitter = config.jitter ?? 0;
4682
- assertFiniteRange("jitter", jitter, 0, 1);
4683
- const maxDelayMs = config.maxDelayMs ?? 3e4;
4684
- assertFiniteRange("maxDelayMs", maxDelayMs, 0);
5437
+ const reconnectPredicate = config?.shouldReconnect;
5438
+ const normalized = normalizeReconnectOptions(config, {
5439
+ delayMs: 1e3,
5440
+ factor: 2,
5441
+ jitter: 0,
5442
+ maxDelayMs: 3e4
5443
+ }, "WebSocket");
5444
+ if (!normalized) return;
4685
5445
  return {
4686
- attempts,
4687
- delayMs,
4688
- factor,
5446
+ ...normalized,
4689
5447
  hasShouldReconnect: typeof reconnectPredicate === "function",
4690
- jitter,
4691
- maxDelayMs,
4692
5448
  shouldReconnect: (outcome, attempt) => {
4693
5449
  if (typeof reconnectPredicate === "function") return Boolean(reconnectPredicate({
4694
5450
  attempt,
@@ -4706,32 +5462,22 @@ function shouldReconnect(config, outcome, attempt) {
4706
5462
  if (outcome.opened === false && config.attempts <= 0) return false;
4707
5463
  return config.shouldReconnect(outcome, attempt);
4708
5464
  }
4709
- function assertFiniteRange(field, value, minimum, maximum = Number.POSITIVE_INFINITY) {
4710
- if (!Number.isFinite(value) || value < minimum || value > maximum) throw new RangeError(`WebSocket reconnect ${field} is out of range`);
4711
- }
4712
- //#endregion
4713
- //#region src/web_socket/web_socket.ts
4714
- function castParsedWebSocketInput(value) {
4715
- return value;
4716
- }
5465
+ // src/web_socket/web_socket.ts
4717
5466
  /**
4718
5467
  * Declare a typed WebSocket endpoint.
4719
5468
  *
4720
5469
  * Pass path, incoming (and optional outgoing) message structs, and queue limits.
4721
5470
  * Call the returned endpoint with input to get a `WebSocketCommand` for `client.execute`.
4722
5471
  *
5472
+ * - **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
+ *
5474
+ * A WebSocket `builder` may return `path` and `query` only; `headers` and `body` are rejected. `WebSocketDefinitionBase` and `BuilderField` are internal helper types.
5475
+ *
4723
5476
  * @param definition - WebSocket contract (path, structs, queue limits, optional input).
4724
5477
  * @returns An endpoint that creates `WebSocketCommand` values from input.
4725
5478
  *
4726
- * @example
4727
- * ```ts
4728
- * const useChat = defineWebSocket({
4729
- * maxIncomingQueueSize: 16,
4730
- * path: '/ws/chat',
4731
- * incoming: struct.object({ text: struct.string(), type: struct.literal('message') }),
4732
- * outgoing: struct.object({ text: struct.string(), type: struct.literal('message') }),
4733
- * })
4734
- * ```
5479
+ * @group web-socket
5480
+ * @exampleFile examples/api/define-web-socket.ts#example
4735
5481
  */
4736
5482
  function defineWebSocket(definition) {
4737
5483
  const endpoint = { ...definition };
@@ -4853,9 +5599,10 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
4853
5599
  void 0
4854
5600
  ];
4855
5601
  };
5602
+ const inputGuides = /* @__PURE__ */ new WeakMap();
4856
5603
  let parsedInput;
4857
5604
  try {
4858
- parsedInput = castParsedWebSocketInput(await parseEndpointInput(endpoint.input, input));
5605
+ parsedInput = parseEndpointInput(endpoint.input, input, inputGuides);
4859
5606
  } catch (error) {
4860
5607
  return stopBeforeTransport(resolveTransportCancellation(deadline, signal, createPreflightFault("REQ_INPUT_INVALID", error)));
4861
5608
  }
@@ -4863,7 +5610,7 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
4863
5610
  if (parsedCancellation) return stopBeforeTransport(parsedCancellation);
4864
5611
  let built;
4865
5612
  try {
4866
- built = materializeOutboundRequest(runOutboundBuilder(parsedInput, endpoint.builder), "webSocket");
5613
+ built = materializeOutboundRequest(runOutboundBuilder(parsedInput, endpoint.builder, endpoint.input !== void 0), "webSocket", inputGuides);
4867
5614
  } catch (error) {
4868
5615
  return stopBeforeTransport(resolveTransportCancellation(deadline, signal, createPreflightFault("REQ_BUILD_FAILED", error)));
4869
5616
  }
@@ -4927,6 +5674,7 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
4927
5674
  const beforeConnect = beforeConnectOption ?? clientConfig.webSocket.beforeConnect;
4928
5675
  const baseProtocols = [...protocolsOption ?? clientConfig.webSocket.protocols ?? endpoint.protocols ?? []];
4929
5676
  const session = createWebSocketSession(endpoint.outgoing, incomingQueue, closedDeferred.promise, state, sessionController, sendQueue, endpoint.maxOutgoingQueueSize ?? 0, WebSocketCtor.OPEN, () => finished || signal.aborted, requestClose, disposeSession);
5677
+ /* istanbul ignore next -- @preserve defensive: run() settles every attempt, hook and delay inside its own failure boundaries */
4930
5678
  const lifecycleTask = run().catch((cause) => {
4931
5679
  finish({
4932
5680
  cause,
@@ -5064,18 +5812,15 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5064
5812
  let socket;
5065
5813
  try {
5066
5814
  socket = protocols.length > 0 ? new WebSocketImpl(url, [...protocols]) : new WebSocketImpl(url);
5815
+ } catch (error) {
5816
+ return connectFailure(error);
5817
+ }
5818
+ try {
5067
5819
  /* istanbul ignore else -- @preserve defensive: binaryType may not exist on injected nonstandard WebSocket handles */
5068
5820
  if ("binaryType" in socket) socket.binaryType = "arraybuffer";
5069
5821
  } catch (error) {
5070
- return {
5071
- closeInfo: {
5072
- cause: error,
5073
- reason: error instanceof Error ? error.message : "WebSocket connection error"
5074
- },
5075
- connection: void 0,
5076
- cause: error,
5077
- opened: false
5078
- };
5822
+ requestNativeCloseBestEffort(socket);
5823
+ return connectFailure(error);
5079
5824
  }
5080
5825
  sessionController.currentSocket = socket;
5081
5826
  let opened = false;
@@ -5089,6 +5834,7 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5089
5834
  const messageSignal = mergeAbortSignals(signal, [messageController.signal]);
5090
5835
  const rawMessages = [];
5091
5836
  let settled = false;
5837
+ let cleanedUp = false;
5092
5838
  const clearErrorCloseTimer = () => {
5093
5839
  if (typeof errorCloseTimer === "undefined") return;
5094
5840
  clearTimeout(errorCloseTimer);
@@ -5096,15 +5842,17 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5096
5842
  };
5097
5843
  const cleanup = (keepCurrentSocket) => {
5098
5844
  clearErrorCloseTimer();
5099
- stopHeartbeat(sessionController);
5100
- messageController.abort();
5101
- releaseAbortSignal(messageSignal);
5102
- rawMessages.length = 0;
5103
5845
  socket.removeEventListener("open", handleOpen);
5104
5846
  socket.removeEventListener("message", onMessage);
5105
5847
  socket.removeEventListener("close", handleClose);
5106
5848
  socket.removeEventListener("error", handleError);
5107
5849
  signal.removeEventListener("abort", handleAbort);
5850
+ if (cleanedUp) return;
5851
+ cleanedUp = true;
5852
+ stopHeartbeat(sessionController);
5853
+ messageController.abort();
5854
+ releaseAbortSignal(messageSignal);
5855
+ rawMessages.length = 0;
5108
5856
  if (!keepCurrentSocket && sessionController.currentSocket === socket) sessionController.currentSocket = void 0;
5109
5857
  };
5110
5858
  const connectionInfo = () => ({
@@ -5133,6 +5881,7 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5133
5881
  const handleOpen = () => {
5134
5882
  if (finished || opened) return;
5135
5883
  opened = true;
5884
+ attempt = 0;
5136
5885
  generation += 1;
5137
5886
  latestConnection = connectionInfo();
5138
5887
  state.connection = latestConnection;
@@ -5291,6 +6040,7 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5291
6040
  socket.addEventListener("close", handleClose);
5292
6041
  socket.addEventListener("error", handleError);
5293
6042
  if (signal.aborted) handleAbort();
6043
+ if (settled) cleanup(false);
5294
6044
  });
5295
6045
  }
5296
6046
  function finish(final, options = {}) {
@@ -5409,6 +6159,8 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5409
6159
  const wsChain = makeChain(resolveWebSocketInterceptors(clientConfig.interceptors));
5410
6160
  try {
5411
6161
  const session = await awaitWithSignal(() => wsChain(wsRequest, wsHandler), chainSignal);
6162
+ const publicConnection = session?.connection;
6163
+ 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");
5412
6164
  chainSettled = true;
5413
6165
  deadline.release();
5414
6166
  releaseAbortSignal(chainSignal);
@@ -5416,7 +6168,8 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5416
6168
  const pendingSessionSnapshot = [...pendingSessions];
5417
6169
  if (!deliveredSession && (undeliveredSessions.size > 0 || pendingSessionSnapshot.length > 0) && !controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("WebSocket session was replaced by interceptor"));
5418
6170
  await Promise.allSettled(pendingSessionSnapshot);
5419
- deliveredSession = [...undeliveredSessions].some((createdSession) => createdSession === session || createdSession.closed === session.closed);
6171
+ const deliveredOwnedSession = [...undeliveredSessions].find((createdSession) => createdSession === session || createdSession.closed === session.closed);
6172
+ deliveredSession = deliveredOwnedSession !== void 0;
5420
6173
  const discardedSessions = [...undeliveredSessions].filter((createdSession) => createdSession !== session && createdSession.closed !== session.closed);
5421
6174
  await Promise.allSettled(discardedSessions.map((discardedSession) => discardedSession.closed));
5422
6175
  pendingSessions.clear();
@@ -5426,10 +6179,12 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5426
6179
  releaseAbortSignal(signal);
5427
6180
  }
5428
6181
  const typedSession = session;
6182
+ let connection = publicConnection;
6183
+ if (startupConnection && deliveredOwnedSession && (deliveredOwnedSession === session || publicConnection === deliveredOwnedSession.connection)) connection = startupConnection;
5429
6184
  return [
5430
6185
  null,
5431
6186
  typedSession,
5432
- startupConnection ?? typedSession.connection
6187
+ connection
5433
6188
  ];
5434
6189
  } catch (error) {
5435
6190
  chainSettled = true;
@@ -5462,10 +6217,15 @@ async function runWebSocketCommand(clientConfig, endpoint, input, config, contro
5462
6217
  *
5463
6218
  * Prefer `client.execute(command)` in application code; this is the low-level entry used by the client.
5464
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
+ *
5465
6224
  * @param clientConfig - Resolved client configuration.
5466
6225
  * @param command - WebSocket command from a `WebSocketEndpoint`.
5467
6226
  * @param options - Optional execute options (heartbeat, reconnect, abort).
5468
6227
  * @returns Await-result tuple of `[error, socket, connection]`.
6228
+ * @group web-socket
5469
6229
  */
5470
6230
  async function executeWebSocketCommand(clientConfig, command, options) {
5471
6231
  const { endpoint, input } = command;
@@ -5587,6 +6347,17 @@ function validateWebSocketClose(code, reason) {
5587
6347
  if (typeof code !== "undefined" && (!Number.isInteger(code) || code !== 1e3 && (code < 3e3 || code > 4999))) throw new DOMException("The close code must be 1000 or between 3000 and 4999", "InvalidAccessError");
5588
6348
  if (typeof reason !== "undefined" && new TextEncoder().encode(reason).byteLength > 123) throw new DOMException("The close reason must not exceed 123 UTF-8 bytes", "SyntaxError");
5589
6349
  }
6350
+ function connectFailure(error) {
6351
+ return {
6352
+ closeInfo: {
6353
+ cause: error,
6354
+ reason: error instanceof Error ? error.message : "WebSocket connection error"
6355
+ },
6356
+ connection: void 0,
6357
+ cause: error,
6358
+ opened: false
6359
+ };
6360
+ }
5590
6361
  function requestNativeCloseBestEffort(socket) {
5591
6362
  try {
5592
6363
  socket.close();
@@ -5642,5 +6413,4 @@ function faultFromCloseInfo(closeInfo) {
5642
6413
  function isFault(value) {
5643
6414
  return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string" && value instanceof Error;
5644
6415
  }
5645
- //#endregion
5646
- 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, makeResponse, struct, withCredentials, withEndpoint, withHTTPHandle, withHeaders, withInterceptors, withQuerySerializer, withSSEHandle, withSSEOnInvalidEvent, withSSEReconnect, withTimeout, withWebSocketBeforeConnect, withWebSocketHandle, withWebSocketHeartbeat, withWebSocketProtocols, withWebSocketReconnect, withXSRF };
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 };