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