@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.
- package/README.md +58 -29
- package/index.d.ts +1839 -342
- package/index.js +1587 -817
- package/index.min.js +21 -27
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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 =
|
|
29
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
//#region src/client/client.ts
|
|
63
|
+
// src/client/client.ts
|
|
62
64
|
const CLIENT = Symbol("Client");
|
|
63
65
|
/**
|
|
64
|
-
*
|
|
66
|
+
* Creates a Defjs client from option helpers.
|
|
65
67
|
*
|
|
66
|
-
* Options
|
|
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
|
-
* @
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
-
|
|
90
|
-
//#region src/struct/symbols.ts
|
|
90
|
+
// src/struct/symbols.ts
|
|
91
91
|
const DEFINITION = Symbol.for("defjs.struct.definition");
|
|
92
|
-
const OMIT = Symbol
|
|
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
|
-
*
|
|
100
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
|
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
|
|
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
|
|
519
|
-
if (
|
|
520
|
-
if (path.includes("?") || path.includes("#")) throw new
|
|
521
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
const HTTP_RESPONSE = Symbol("HttpResponse");
|
|
531
|
+
// src/internal/http_response.ts
|
|
532
|
+
const HTTP_RESPONSE = Symbol.for("defjs.http.response");
|
|
526
533
|
/**
|
|
527
|
-
*
|
|
528
|
-
*
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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`
|
|
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
|
-
*
|
|
639
|
+
* @group errors
|
|
640
|
+
* @see {@link ERR_ABORTED}
|
|
641
|
+
* @see {@link isTimeoutCause}
|
|
570
642
|
*/
|
|
571
643
|
function isAbortCause(cause) {
|
|
572
|
-
return cause
|
|
644
|
+
return hasCauseBrand(cause, ABORT_BRAND) || cause instanceof DOMException && cause.name === "AbortError";
|
|
573
645
|
}
|
|
574
646
|
/**
|
|
575
|
-
* Whether `cause`
|
|
647
|
+
* Whether `cause` is a timeout, across Core copies and platform timeout errors.
|
|
576
648
|
*
|
|
577
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
624
|
-
*
|
|
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
|
-
*
|
|
728
|
+
* Builds a decode fault for a response that arrived but could not be read as declared.
|
|
634
729
|
*
|
|
635
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
775
|
+
* Builds a `NET_*` `PreflightFault`, choosing the code from the shape of `cause`.
|
|
665
776
|
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
866
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1118
|
+
* Sets the WebSocket subprotocols offered during the handshake.
|
|
968
1119
|
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
1055
|
-
//#region src/interceptor/interceptor.ts
|
|
1299
|
+
// src/interceptor/interceptor.ts
|
|
1056
1300
|
/**
|
|
1057
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
1184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1432
|
-
//#region src/struct/errors.ts
|
|
1686
|
+
// src/struct/errors.ts
|
|
1433
1687
|
/**
|
|
1434
|
-
*
|
|
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
|
-
|
|
1813
|
+
return {
|
|
1528
1814
|
code,
|
|
1529
1815
|
expected,
|
|
1530
|
-
message: message ?? `Expected ${expected} at ${formatPath(path)}, 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
|
-
|
|
1541
|
-
|
|
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,
|
|
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
|
|
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":
|
|
1573
|
-
|
|
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,
|
|
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,
|
|
1587
|
-
case "or": return parseUnionValue(definition, input, path, useAliases, reportIssues,
|
|
1588
|
-
case "discriminatedUnion": return parseDiscriminatedUnionValue(definition, input, path, useAliases, reportIssues,
|
|
1589
|
-
case "record": return parseRecordValue(struct, definition, input, path, useAliases, reportIssues,
|
|
1590
|
-
case "tuple": return parseTupleValue(definition, input, path, useAliases, reportIssues,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 (
|
|
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,
|
|
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,
|
|
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 (
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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))
|
|
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],
|
|
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 (
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1784
|
-
|
|
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, {
|
|
2130
|
+
return parseStructValue(struct, input, {
|
|
2131
|
+
guides,
|
|
2132
|
+
tagObjects: true
|
|
2133
|
+
});
|
|
1788
2134
|
}
|
|
1789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1859
|
-
const
|
|
1860
|
-
if (
|
|
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
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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(
|
|
2378
|
+
body: JSON.stringify(encodeOutbound(body, guides), rejectNonJson),
|
|
2047
2379
|
defaultContentType: "application/json"
|
|
2048
2380
|
};
|
|
2049
2381
|
}
|
|
2050
|
-
function
|
|
2051
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2393
|
-
//#region src/http/transport/body_failure.ts
|
|
2739
|
+
// src/http/transport/body_failure.ts
|
|
2394
2740
|
/**
|
|
2395
|
-
* A failure raised while reading or
|
|
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
|
-
*
|
|
2399
|
-
*
|
|
2400
|
-
* indistinguishable once they collapse into a bare cause, so
|
|
2401
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2811
|
+
const request = new Request("https://example.com", {
|
|
2460
2812
|
body: stream,
|
|
2461
2813
|
duplex: "half",
|
|
2462
2814
|
method: "POST"
|
|
2463
|
-
})
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
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,
|
|
2796
|
-
*
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3231
|
+
lengthComputable: total > 0,
|
|
2840
3232
|
loaded: 0,
|
|
2841
|
-
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:
|
|
3245
|
+
bodyContentType: headers.get("Content-Type"),
|
|
2854
3246
|
bodyContentTypeSource: converted.stream,
|
|
2855
3247
|
headers
|
|
2856
3248
|
};
|
|
2857
3249
|
}
|
|
2858
|
-
|
|
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:
|
|
2875
|
-
total:
|
|
3265
|
+
stream: serialized.body,
|
|
3266
|
+
total: 0
|
|
2876
3267
|
};
|
|
2877
3268
|
}
|
|
2878
3269
|
}
|
|
2879
|
-
|
|
2880
|
-
//#region src/http/http.ts
|
|
3270
|
+
// src/http/http.ts
|
|
2881
3271
|
/**
|
|
2882
|
-
*
|
|
3272
|
+
* Declares a typed HTTP request endpoint.
|
|
2883
3273
|
*
|
|
2884
|
-
* Pass method, path, an optional input struct, and the single `output` and
|
|
2885
|
-
* Call the returned endpoint with input to get an `HttpCommand`
|
|
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
|
-
*
|
|
2916
|
-
*
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3037
|
-
const
|
|
3038
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
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
|
-
|
|
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
|
|
3334
|
-
let
|
|
3335
|
-
let
|
|
3336
|
-
let
|
|
3337
|
-
|
|
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
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
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
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
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
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
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 (
|
|
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
|
|
3381
|
-
|
|
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
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
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 +=
|
|
3412
|
-
if (
|
|
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
|
-
|
|
3870
|
+
id = value;
|
|
3422
3871
|
onId(value);
|
|
3423
3872
|
}
|
|
3424
3873
|
break;
|
|
3425
|
-
case "retry": if (
|
|
3874
|
+
case "retry": if (/^[0-9]+$/.test(value)) onRetry(Math.min(Number(value), 2147483647));
|
|
3426
3875
|
}
|
|
3427
3876
|
};
|
|
3428
3877
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
3715
|
-
if (!await awaitWithSignal(() => reconnect
|
|
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
|
-
|
|
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
|
-
|
|
3730
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
3896
|
-
*
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4071
|
-
stream.closed.then(
|
|
4072
|
-
|
|
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 =
|
|
4530
|
+
const fromTransport = transportOrigin.has(error);
|
|
4083
4531
|
const cause = error;
|
|
4084
|
-
const openInfo =
|
|
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
|
-
|
|
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
|
-
|
|
4228
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4670
|
-
//#region src/web_socket/reconnect.ts
|
|
5435
|
+
// src/web_socket/reconnect.ts
|
|
4671
5436
|
function normalizeReconnectConfig(config) {
|
|
4672
|
-
|
|
4673
|
-
const
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
4727
|
-
*
|
|
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 =
|
|
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
|
-
|
|
5071
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|