arthron 1.0.0 → 1.1.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 +101 -51
- package/dist/index-Bz1P80JN.d.cts +293 -0
- package/dist/index-Bz1P80JN.d.mts +293 -0
- package/dist/index.cjs +78 -653
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -238
- package/dist/index.d.mts +16 -238
- package/dist/index.mjs +70 -645
- package/dist/index.mjs.map +1 -1
- package/dist/plugins-CVrzb8x6.cjs +864 -0
- package/dist/plugins-CVrzb8x6.cjs.map +1 -0
- package/dist/plugins-ChNZ2Q8t.mjs +757 -0
- package/dist/plugins-ChNZ2Q8t.mjs.map +1 -0
- package/dist/plugins.cjs +6 -0
- package/dist/plugins.d.cts +2 -0
- package/dist/plugins.d.mts +2 -0
- package/dist/plugins.mjs +1 -0
- package/llms.txt +13 -14
- package/package.json +14 -1
- package/plugins/package.json +6 -0
package/dist/index.cjs
CHANGED
|
@@ -1,164 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
const ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
4
|
-
const HTTP_URL = /^https?:\/\//i;
|
|
5
|
-
function isAbsoluteUrl(path) {
|
|
6
|
-
return ABSOLUTE_URL.test(path);
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Origin + pathname only — the query and userinfo tend to carry secrets.
|
|
10
|
-
* For anything printed: log lines, error messages, `ArthronError.toJSON`.
|
|
11
|
-
*/
|
|
12
|
-
function redactUrl(url) {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = new URL(url);
|
|
15
|
-
return `${parsed.origin}${parsed.pathname}`;
|
|
16
|
-
} catch {
|
|
17
|
-
return url.split(/[?#]/)[0] ?? url;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* `undefined` when `url` has no origin of its own. A relative baseURL takes
|
|
22
|
-
* the page's origin in a browser and has none elsewhere.
|
|
23
|
-
*/
|
|
24
|
-
function originOf(url) {
|
|
25
|
-
const page = globalThis.location?.href;
|
|
26
|
-
try {
|
|
27
|
-
return new URL(url, page).origin;
|
|
28
|
-
} catch {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/** `path` is absolute and points away from `baseURL`'s origin. */
|
|
33
|
-
function leavesBase(baseURL, path) {
|
|
34
|
-
if (baseURL === void 0 || !isAbsoluteUrl(path)) return false;
|
|
35
|
-
const base = originOf(baseURL);
|
|
36
|
-
return base === void 0 || originOf(path) !== base;
|
|
37
|
-
}
|
|
38
|
-
function toQueryString(params) {
|
|
39
|
-
if (!params) return "";
|
|
40
|
-
if (Object.prototype.toString.call(params) === "[object URLSearchParams]") return params.toString();
|
|
41
|
-
const search = new URLSearchParams();
|
|
42
|
-
for (const [key, value] of Object.entries(params)) for (const item of Array.isArray(value) ? value : [value]) {
|
|
43
|
-
if (item === null || item === void 0) continue;
|
|
44
|
-
if (typeof item === "object" || typeof item === "function") throw new TypeError(`arthron: params.${key} is not a string, number or boolean — pass one of those or an array of them (dates: date.toISOString())`);
|
|
45
|
-
search.append(key, String(item));
|
|
46
|
-
}
|
|
47
|
-
return search.toString();
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Fill `{name}` placeholders with encoded values, so each one stays inside
|
|
51
|
-
* its segment: "/", "?" and "#" are encoded, and the values WHATWG URL would
|
|
52
|
-
* turn into a different path — "", "." and ".." — are refused. A placeholder
|
|
53
|
-
* without a value, or a value without a placeholder, is a mistake too.
|
|
54
|
-
*/
|
|
55
|
-
function fillPath(template, values) {
|
|
56
|
-
const used = /* @__PURE__ */ new Set();
|
|
57
|
-
const filled = template.replace(/\{([^{}/]+)\}/g, (_, name) => {
|
|
58
|
-
if (!Object.hasOwn(values, name)) throw new TypeError(`arthron: path "${template}" has {${name}} but no value for it — pass path: { ${name}: … }`);
|
|
59
|
-
used.add(name);
|
|
60
|
-
const value = String(values[name]);
|
|
61
|
-
if (value === "" || value === "." || value === "..") throw new TypeError(`arthron: path value ${name}=${JSON.stringify(value)} would change the path, not fill a segment — pass a real id`);
|
|
62
|
-
return encodeURIComponent(value);
|
|
63
|
-
});
|
|
64
|
-
const unused = Object.keys(values).find((name) => !used.has(name));
|
|
65
|
-
if (unused !== void 0) throw new TypeError(`arthron: path "${template}" has no {${unused}} — add the placeholder or drop the value`);
|
|
66
|
-
return filled;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Join `baseURL` and `path` by plain string concatenation so a path prefix in
|
|
70
|
-
* the base (`/v1`) survives a leading slash in the path. WHATWG `new URL()`
|
|
71
|
-
* would drop it. An absolute path replaces the base only when it stays on the
|
|
72
|
-
* base's origin, or when `allowAbsoluteUrls` says other origins are fine — a
|
|
73
|
-
* path built from user input or an API response must not carry the client's
|
|
74
|
-
* credentials somewhere else.
|
|
75
|
-
*/
|
|
76
|
-
function resolveUrl(baseURL, path, params, allowAbsoluteUrls = false) {
|
|
77
|
-
let url;
|
|
78
|
-
if (isAbsoluteUrl(path)) {
|
|
79
|
-
if (!HTTP_URL.test(path)) {
|
|
80
|
-
const scheme = path.slice(0, path.indexOf(":") + 1);
|
|
81
|
-
throw new TypeError(`arthron: only http(s) URLs can be requested, got a "${scheme}" URL — use an http:// or https:// URL`);
|
|
82
|
-
}
|
|
83
|
-
if (!allowAbsoluteUrls && leavesBase(baseURL, path)) throw new TypeError(`arthron: ${originOf(path)} is outside baseURL ${(baseURL && originOf(baseURL)) ?? baseURL} — pass a path relative to baseURL, or set arthron({ allowAbsoluteUrls: true }) to allow other origins (config headers are not sent to them)`);
|
|
84
|
-
url = path;
|
|
85
|
-
} else {
|
|
86
|
-
if (baseURL === void 0) throw new TypeError(`arthron: relative path "${redactUrl(path)}" needs a baseURL — pass baseURL in arthron({ baseURL }) or use an absolute URL`);
|
|
87
|
-
url = `${baseURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
88
|
-
}
|
|
89
|
-
const query = toQueryString(params);
|
|
90
|
-
if (!query) return url;
|
|
91
|
-
const hash = url.indexOf("#");
|
|
92
|
-
const [base, fragment] = hash === -1 ? [url, ""] : [url.slice(0, hash), url.slice(hash)];
|
|
93
|
-
return `${base}${base.includes("?") ? "&" : "?"}${query}${fragment}`;
|
|
94
|
-
}
|
|
95
|
-
//#endregion
|
|
96
|
-
//#region src/error/arthron-error.ts
|
|
97
|
-
/**
|
|
98
|
-
* Marks an ArthronError across copies of this package. The ESM and CJS
|
|
99
|
-
* builds can both end up loaded (a CJS dependency creates the client, ESM
|
|
100
|
-
* app code catches), each with its own class — `Symbol.for` is shared.
|
|
101
|
-
*/
|
|
102
|
-
const BRAND = Symbol.for("arthron.ArthronError");
|
|
103
|
-
/**
|
|
104
|
-
* The only error arthron itself produces. Two sources: a response with
|
|
105
|
-
* `ok === false`, or fetch rejecting outright (network failure, `status` 0).
|
|
106
|
-
* Everything else — cancellation, interceptor bugs — is rethrown untouched.
|
|
107
|
-
*/
|
|
108
|
-
var ArthronError = class ArthronError extends Error {
|
|
109
|
-
name = "ArthronError";
|
|
110
|
-
/** HTTP status of the failed response; `0` when fetch itself failed (network). */
|
|
111
|
-
status;
|
|
112
|
-
statusText;
|
|
113
|
-
/** Parsed body: JSON if it parses, else the text, else `undefined`. */
|
|
114
|
-
data;
|
|
115
|
-
/**
|
|
116
|
-
* The server's own explanation, picked out of `data` (by default
|
|
117
|
-
* `message` → `error` → `error.message` → `detail`; see the `errorDetail`
|
|
118
|
-
* config). `undefined` when the body had none. Untrusted server text: show
|
|
119
|
-
* it to users as text, never as HTML. `message`, by contrast, is written by
|
|
120
|
-
* arthron — method, URL without query, status — and is meant for logs.
|
|
121
|
-
*/
|
|
122
|
-
detail;
|
|
123
|
-
/** The underlying error for network failures. */
|
|
124
|
-
cause;
|
|
125
|
-
constructor(init) {
|
|
126
|
-
super(init.message);
|
|
127
|
-
this.status = init.status;
|
|
128
|
-
this.statusText = init.statusText;
|
|
129
|
-
this.data = init.data;
|
|
130
|
-
this.detail = init.detail;
|
|
131
|
-
this.cause = init.cause;
|
|
132
|
-
Object.defineProperties(this, {
|
|
133
|
-
headers: { value: init.headers },
|
|
134
|
-
request: { value: init.request },
|
|
135
|
-
response: { value: init.response },
|
|
136
|
-
[BRAND]: { value: true }
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
/** `instanceof ArthronError` holds for an error from any copy of the package. */
|
|
140
|
-
static [Symbol.hasInstance](value) {
|
|
141
|
-
return this === ArthronError ? isArthronError(value) : Function.prototype[Symbol.hasInstance].call(this, value);
|
|
142
|
-
}
|
|
143
|
-
/** What serializers see: no headers, no body, no query or userinfo. */
|
|
144
|
-
toJSON() {
|
|
145
|
-
return {
|
|
146
|
-
name: this.name,
|
|
147
|
-
message: this.message,
|
|
148
|
-
detail: this.detail,
|
|
149
|
-
status: this.status,
|
|
150
|
-
statusText: this.statusText,
|
|
151
|
-
method: this.request.init.method ?? "GET",
|
|
152
|
-
url: redactUrl(this.request.url),
|
|
153
|
-
data: this.data
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
/** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
|
|
158
|
-
function isArthronError(error) {
|
|
159
|
-
return typeof error === "object" && error !== null && error[BRAND] === true;
|
|
160
|
-
}
|
|
161
|
-
//#endregion
|
|
2
|
+
const require_plugins = require("./plugins-CVrzb8x6.cjs");
|
|
162
3
|
//#region src/request/headers.ts
|
|
163
4
|
const isPlainRecord = (headers) => !Array.isArray(headers) && Object.prototype.toString.call(headers) !== "[object Headers]";
|
|
164
5
|
/**
|
|
@@ -226,6 +67,15 @@ function normalize(ctx, previous) {
|
|
|
226
67
|
};
|
|
227
68
|
}
|
|
228
69
|
/**
|
|
70
|
+
* A plugin written in JS (or with a cast) that forgets to return would hand
|
|
71
|
+
* `undefined` on — and an error interceptor's would be what the caller
|
|
72
|
+
* catches. Name the plugin slot instead.
|
|
73
|
+
*/
|
|
74
|
+
function returned(value, who, what, cause, verb = "returned") {
|
|
75
|
+
if (value != null) return value;
|
|
76
|
+
throw new TypeError(`arthron: ${who} ${verb} ${value} — return ${what}`, cause === void 0 ? void 0 : { cause });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
229
79
|
* Owns the order of one request.
|
|
230
80
|
*
|
|
231
81
|
* Middleware wraps the whole call, outermost first in registration order.
|
|
@@ -247,98 +97,30 @@ async function runPipeline(plugins, ctx, send) {
|
|
|
247
97
|
}
|
|
248
98
|
const attempt = async (input) => {
|
|
249
99
|
let current = cloneContext(input);
|
|
100
|
+
for (const interceptor of request) current = normalize(returned(await interceptor(current), "a request interceptor", "the context (ctx or a changed copy)"), current);
|
|
101
|
+
let res;
|
|
250
102
|
try {
|
|
251
|
-
|
|
252
|
-
let res = await send(current);
|
|
253
|
-
for (const interceptor of response) res = await interceptor(res, current);
|
|
254
|
-
return res;
|
|
103
|
+
res = await send(current);
|
|
255
104
|
} catch (thrown) {
|
|
256
|
-
if (!isArthronError(thrown)) throw thrown;
|
|
105
|
+
if (!require_plugins.isArthronError(thrown)) throw thrown;
|
|
257
106
|
let err = thrown;
|
|
258
|
-
for (const interceptor of error) err = await interceptor(err, current);
|
|
107
|
+
for (const interceptor of error) err = returned(await interceptor(err, current), "an error interceptor", "the error (err or a changed one)", thrown);
|
|
259
108
|
throw err;
|
|
260
109
|
}
|
|
110
|
+
for (const interceptor of response) res = returned(await interceptor(res, current), "a response interceptor", "the Response (res or a new one)");
|
|
111
|
+
return res;
|
|
261
112
|
};
|
|
262
|
-
const dispatch = (index, input) => {
|
|
113
|
+
const dispatch = async (index, input) => {
|
|
263
114
|
const middleware = middlewares[index];
|
|
264
115
|
if (!middleware) return attempt(input);
|
|
265
|
-
return middleware(input, (next) => dispatch(index + 1, {
|
|
116
|
+
return returned(await middleware(input, (next) => dispatch(index + 1, {
|
|
266
117
|
...next,
|
|
267
118
|
meta: next.meta ?? input.meta
|
|
268
|
-
}));
|
|
119
|
+
})), "a middleware", "the Response, usually `return next(ctx)`", void 0, "resolved with");
|
|
269
120
|
};
|
|
270
121
|
return dispatch(0, ctx);
|
|
271
122
|
}
|
|
272
123
|
//#endregion
|
|
273
|
-
//#region src/request/body.ts
|
|
274
|
-
const RAW_TAGS = /* @__PURE__ */ new Set([
|
|
275
|
-
"FormData",
|
|
276
|
-
"Blob",
|
|
277
|
-
"File",
|
|
278
|
-
"ArrayBuffer",
|
|
279
|
-
"URLSearchParams",
|
|
280
|
-
"ReadableStream"
|
|
281
|
-
]);
|
|
282
|
-
/**
|
|
283
|
-
* `Symbol.toStringTag`, not `instanceof`: a body from another realm or a
|
|
284
|
-
* polyfill (formdata-node, fetch-blob) must not be JSON-encoded into `{}`.
|
|
285
|
-
*/
|
|
286
|
-
const tagOf = (value) => Object.prototype.toString.call(value).slice(8, -1);
|
|
287
|
-
function isReadableStream(value) {
|
|
288
|
-
return tagOf(value) === "ReadableStream";
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* What JSON.stringify would turn into `{}`, `undefined` or internal state
|
|
292
|
-
* instead of the data — with the fix, so it reads as an error message.
|
|
293
|
-
*/
|
|
294
|
-
function mangledByJson(body) {
|
|
295
|
-
if (typeof body === "function" || typeof body === "symbol") return `a ${typeof body}, which JSON cannot carry — send its data instead`;
|
|
296
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return;
|
|
297
|
-
if (Symbol.asyncIterator in body) return "an async iterable (a Node stream?) — pass a ReadableStream (Readable.toWeb(stream)) or collect it first";
|
|
298
|
-
if (Symbol.iterator in body) return "an iterable such as a Map, Set or generator — convert it to an array or plain object first";
|
|
299
|
-
}
|
|
300
|
-
/** Bodies fetch already knows how to send. Content-Type is left to the caller / runtime. */
|
|
301
|
-
function isRawBody(body) {
|
|
302
|
-
if (typeof body === "string") return true;
|
|
303
|
-
if (ArrayBuffer.isView(body)) return true;
|
|
304
|
-
return RAW_TAGS.has(tagOf(body));
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Turn a caller-supplied body into something fetch accepts. Raw bodies pass
|
|
308
|
-
* through untouched; anything else is JSON with `content-type` set — unless
|
|
309
|
-
* the caller already set one (headers arrive lowercased, so a plain lookup is
|
|
310
|
-
* enough).
|
|
311
|
-
*/
|
|
312
|
-
function serializeBody(body, headers) {
|
|
313
|
-
if (body === void 0) return {
|
|
314
|
-
body: void 0,
|
|
315
|
-
headers
|
|
316
|
-
};
|
|
317
|
-
if (tagOf(body) === "FormData") {
|
|
318
|
-
const { "content-type": _dropped, ...rest } = headers;
|
|
319
|
-
return {
|
|
320
|
-
body,
|
|
321
|
-
headers: rest
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
if (isRawBody(body)) return {
|
|
325
|
-
body,
|
|
326
|
-
headers
|
|
327
|
-
};
|
|
328
|
-
const mangled = mangledByJson(body);
|
|
329
|
-
if (mangled) throw new TypeError(`arthron: the body is ${mangled}`);
|
|
330
|
-
const type = headers["content-type"];
|
|
331
|
-
if (type !== void 0 && !/\bjson\b/i.test(type)) throw new TypeError(`arthron: a ${typeof body} body is sent as JSON, but content-type is "${type}" — pass a string, URLSearchParams or FormData for that content type, or drop the header`);
|
|
332
|
-
const withType = "content-type" in headers ? headers : {
|
|
333
|
-
...headers,
|
|
334
|
-
"content-type": "application/json"
|
|
335
|
-
};
|
|
336
|
-
return {
|
|
337
|
-
body: JSON.stringify(body),
|
|
338
|
-
headers: withType
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
//#endregion
|
|
342
124
|
//#region src/response/schema-validation.ts
|
|
343
125
|
const pathOf = (issue) => (issue.path ?? []).map((part) => String(typeof part === "object" && part !== null ? part.key : part)).join(".");
|
|
344
126
|
/**
|
|
@@ -373,6 +155,10 @@ const DEFAULT_LIMIT = 8388608;
|
|
|
373
155
|
*/
|
|
374
156
|
async function* lines(response, maxLineLength) {
|
|
375
157
|
if (!response.body) return;
|
|
158
|
+
const checked = (line) => {
|
|
159
|
+
if (line.length <= maxLineLength) return line;
|
|
160
|
+
throw new RangeError(`arthron: a stream line is longer than ${maxLineLength} characters — pass a larger maxLineLength to .sse() / .ndjson() if that is expected`);
|
|
161
|
+
};
|
|
376
162
|
const reader = response.body.getReader();
|
|
377
163
|
const decoder = new TextDecoder();
|
|
378
164
|
const breaks = /\r\n|\r|\n/g;
|
|
@@ -390,12 +176,11 @@ async function* lines(response, maxLineLength) {
|
|
|
390
176
|
let start = 0;
|
|
391
177
|
breaks.lastIndex = 0;
|
|
392
178
|
for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
|
|
393
|
-
yield pending + text.slice(start, m.index);
|
|
179
|
+
yield checked(pending + text.slice(start, m.index));
|
|
394
180
|
pending = "";
|
|
395
181
|
start = breaks.lastIndex;
|
|
396
182
|
}
|
|
397
|
-
pending
|
|
398
|
-
if (pending.length > maxLineLength) throw new RangeError(`arthron: a stream line is longer than ${maxLineLength} characters — pass a larger maxLineLength to .sse() / .ndjson() if that is expected`);
|
|
183
|
+
pending = checked(pending + text.slice(start));
|
|
399
184
|
if (done) break;
|
|
400
185
|
}
|
|
401
186
|
if (pending) yield pending;
|
|
@@ -472,28 +257,35 @@ async function* parseNdjson(response, { maxLineLength = DEFAULT_LIMIT } = {}) {
|
|
|
472
257
|
//#endregion
|
|
473
258
|
//#region src/response/arthron-promise.ts
|
|
474
259
|
/** Defer the Response until iteration starts, so the helper itself never rejects. */
|
|
475
|
-
function iterate(promise, parse) {
|
|
260
|
+
function iterate(promise, parse, signal) {
|
|
476
261
|
promise.catch(() => {});
|
|
477
262
|
return { async *[Symbol.asyncIterator]() {
|
|
478
|
-
|
|
263
|
+
try {
|
|
264
|
+
yield* parse(await promise);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw require_plugins.cancellationError(error, signal);
|
|
267
|
+
}
|
|
479
268
|
} };
|
|
480
269
|
}
|
|
481
270
|
/**
|
|
482
271
|
* Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting
|
|
483
272
|
* without a helper still yields the untouched `Response`.
|
|
484
273
|
*/
|
|
485
|
-
function toArthronPromise(promise) {
|
|
274
|
+
function toArthronPromise(promise, signal) {
|
|
275
|
+
const read = (body) => promise.then(body).catch((error) => {
|
|
276
|
+
throw require_plugins.cancellationError(error, signal);
|
|
277
|
+
});
|
|
486
278
|
return Object.assign(promise, {
|
|
487
|
-
json: ((schema) =>
|
|
279
|
+
json: ((schema) => read(async (res) => {
|
|
488
280
|
const data = await res.json();
|
|
489
281
|
return schema ? validate(schema, data) : data;
|
|
490
282
|
})),
|
|
491
|
-
text: () =>
|
|
492
|
-
blob: () =>
|
|
493
|
-
arrayBuffer: () =>
|
|
494
|
-
formData: () =>
|
|
495
|
-
sse: (options) => iterate(promise, (res) => parseSse(res, options)),
|
|
496
|
-
ndjson: (options) => iterate(promise, (res) => parseNdjson(res, options))
|
|
283
|
+
text: () => read((res) => res.text()),
|
|
284
|
+
blob: () => read((res) => res.blob()),
|
|
285
|
+
arrayBuffer: () => read((res) => res.arrayBuffer()),
|
|
286
|
+
formData: () => read((res) => res.formData()),
|
|
287
|
+
sse: (options) => iterate(promise, (res) => parseSse(res, options), signal),
|
|
288
|
+
ndjson: (options) => iterate(promise, (res) => parseNdjson(res, options), signal)
|
|
497
289
|
});
|
|
498
290
|
}
|
|
499
291
|
//#endregion
|
|
@@ -548,130 +340,6 @@ function resolveConfig(config) {
|
|
|
548
340
|
};
|
|
549
341
|
}
|
|
550
342
|
//#endregion
|
|
551
|
-
//#region src/response/rebuild-response.ts
|
|
552
|
-
/**
|
|
553
|
-
* A new Response with `body` but everything else taken from `original`. The
|
|
554
|
-
* constructor resets `url`, `redirected` and `type`, so those are copied over
|
|
555
|
-
* as own properties — callers that resolve relative links or detect
|
|
556
|
-
* redirects keep working.
|
|
557
|
-
*/
|
|
558
|
-
function rebuildResponse(original, body) {
|
|
559
|
-
return withFetchedFields(new Response(body, {
|
|
560
|
-
status: original.status,
|
|
561
|
-
statusText: original.statusText,
|
|
562
|
-
headers: original.headers
|
|
563
|
-
}), original);
|
|
564
|
-
}
|
|
565
|
-
/** Copy the fields the constructor resets — onto `clone()`s as well. */
|
|
566
|
-
function withFetchedFields(target, from) {
|
|
567
|
-
const clone = target.clone.bind(target);
|
|
568
|
-
return Object.defineProperties(target, {
|
|
569
|
-
url: { value: from.url },
|
|
570
|
-
redirected: { value: from.redirected },
|
|
571
|
-
type: { value: from.type },
|
|
572
|
-
clone: { value: () => withFetchedFields(clone(), from) }
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
//#endregion
|
|
576
|
-
//#region src/error/create-error.ts
|
|
577
|
-
/**
|
|
578
|
-
* Cancellation is not an HTTP failure. Matched by name so it works across
|
|
579
|
-
* realms — or by the request's own `signal`, since `abort(reason)` rejects
|
|
580
|
-
* with whatever reason the caller chose.
|
|
581
|
-
*/
|
|
582
|
-
function isCancellation(error, signal) {
|
|
583
|
-
if (signal?.aborted) return true;
|
|
584
|
-
const name = error?.name;
|
|
585
|
-
return name === "AbortError" || name === "TimeoutError";
|
|
586
|
-
}
|
|
587
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
588
|
-
/** `message` → `error` (string) → `error.message` → `detail`. */
|
|
589
|
-
function defaultErrorDetail(data) {
|
|
590
|
-
if (!isRecord(data)) return void 0;
|
|
591
|
-
if (typeof data.message === "string") return data.message;
|
|
592
|
-
if (typeof data.error === "string") return data.error;
|
|
593
|
-
if (isRecord(data.error) && typeof data.error.message === "string") return data.error.message;
|
|
594
|
-
if (typeof data.detail === "string") return data.detail;
|
|
595
|
-
}
|
|
596
|
-
/** How much of an error body arthron reads. Error bodies are for diagnosis; a hostile or broken server must not be able to fill memory with one. */
|
|
597
|
-
const MAX_ERROR_BODY_BYTES = 65536;
|
|
598
|
-
/** Up to `limit` bytes of the body; the rest is cancelled unread. `undefined` when there is no body or reading it failed. */
|
|
599
|
-
async function readCapped(response, limit) {
|
|
600
|
-
if (!response.body) return void 0;
|
|
601
|
-
let reader;
|
|
602
|
-
const chunks = [];
|
|
603
|
-
let size = 0;
|
|
604
|
-
try {
|
|
605
|
-
reader = response.body.getReader();
|
|
606
|
-
while (size <= limit) {
|
|
607
|
-
const { value, done } = await reader.read();
|
|
608
|
-
if (done) break;
|
|
609
|
-
chunks.push(value);
|
|
610
|
-
size += value.byteLength;
|
|
611
|
-
}
|
|
612
|
-
} catch {
|
|
613
|
-
return;
|
|
614
|
-
} finally {
|
|
615
|
-
await reader?.cancel().catch(() => {});
|
|
616
|
-
reader?.releaseLock();
|
|
617
|
-
}
|
|
618
|
-
const bytes = new Uint8Array(size);
|
|
619
|
-
let offset = 0;
|
|
620
|
-
for (const chunk of chunks) {
|
|
621
|
-
bytes.set(chunk, offset);
|
|
622
|
-
offset += chunk.byteLength;
|
|
623
|
-
}
|
|
624
|
-
return size > limit ? {
|
|
625
|
-
bytes: bytes.subarray(0, limit),
|
|
626
|
-
truncated: true
|
|
627
|
-
} : {
|
|
628
|
-
bytes,
|
|
629
|
-
truncated: false
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
/** JSON if the whole body parses, else the text (cut at the limit), else `undefined` when empty. */
|
|
633
|
-
function parseBody(read) {
|
|
634
|
-
const text = new TextDecoder().decode(read.bytes);
|
|
635
|
-
if (text === "") return void 0;
|
|
636
|
-
if (read.truncated) return text;
|
|
637
|
-
try {
|
|
638
|
-
return JSON.parse(text);
|
|
639
|
-
} catch {
|
|
640
|
-
return text;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
/** The request as error messages name it: method and URL, no query or userinfo. */
|
|
644
|
-
function describeRequest(request) {
|
|
645
|
-
return `${request.init.method ?? "GET"} ${redactUrl(request.url)}`;
|
|
646
|
-
}
|
|
647
|
-
async function responseError(response, request, resolve = defaultErrorDetail) {
|
|
648
|
-
const read = await readCapped(response, MAX_ERROR_BODY_BYTES);
|
|
649
|
-
const data = read && parseBody(read);
|
|
650
|
-
if (read) response = rebuildResponse(response, read.bytes);
|
|
651
|
-
const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
|
|
652
|
-
return new ArthronError({
|
|
653
|
-
message: `${describeRequest(request)} failed with ${status}`,
|
|
654
|
-
detail: resolve(data, response) || void 0,
|
|
655
|
-
status: response.status,
|
|
656
|
-
statusText: response.statusText,
|
|
657
|
-
headers: response.headers,
|
|
658
|
-
data,
|
|
659
|
-
request,
|
|
660
|
-
response
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
function networkError(cause, request) {
|
|
664
|
-
return new ArthronError({
|
|
665
|
-
message: `${describeRequest(request)} failed: ${cause instanceof Error && cause.message ? cause.message : "network error"}`,
|
|
666
|
-
status: 0,
|
|
667
|
-
statusText: "",
|
|
668
|
-
headers: new Headers(),
|
|
669
|
-
data: void 0,
|
|
670
|
-
request,
|
|
671
|
-
cause
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
//#endregion
|
|
675
343
|
//#region src/client/send.ts
|
|
676
344
|
/**
|
|
677
345
|
* The global fetch rejects a malformed URL or method with a TypeError that
|
|
@@ -685,12 +353,21 @@ function assertSendable(url, method) {
|
|
|
685
353
|
try {
|
|
686
354
|
new Request(url, { method });
|
|
687
355
|
} catch (cause) {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
throw new TypeError(`arthron: cannot send ${method} ${shown} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
|
|
356
|
+
const reason = require_plugins.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
|
|
357
|
+
throw new TypeError(`arthron: cannot send ${method} ${require_plugins.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
|
|
691
358
|
}
|
|
692
359
|
}
|
|
693
360
|
/**
|
|
361
|
+
* A ReadableStream body the browser cannot stream is rejected before it is
|
|
362
|
+
* sent: Firefox would send the text "[object ReadableStream]", older Safari
|
|
363
|
+
* fails with a bare network error. Only for the global fetch, like
|
|
364
|
+
* assertSendable.
|
|
365
|
+
*/
|
|
366
|
+
function assertStreamable(url, method, body) {
|
|
367
|
+
if (!require_plugins.isReadableStream(body) || require_plugins.runtimeStreamsRequestBody()) return;
|
|
368
|
+
throw new TypeError(`arthron: cannot send ${method} ${require_plugins.redactUrl(url)} with a ReadableStream body — this browser cannot stream a request body and would send it garbled or not at all; pass a Blob or ArrayBuffer instead`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
694
371
|
* Non-ok responses the caller asked for are not failures: an opaque one
|
|
695
372
|
* (`mode: "no-cors"`) and the 3xx of `redirect: "manual"`.
|
|
696
373
|
*/
|
|
@@ -701,18 +378,22 @@ function failedByDefault(res, init) {
|
|
|
701
378
|
/** The one place a failure becomes an ArthronError. Cancellation passes through. */
|
|
702
379
|
async function send(resolved, ctx, throwOn) {
|
|
703
380
|
const fetchImpl = resolved.fetch ?? globalThis.fetch;
|
|
704
|
-
if (resolved.fetch === void 0)
|
|
381
|
+
if (resolved.fetch === void 0) {
|
|
382
|
+
const method = ctx.init.method ?? "GET";
|
|
383
|
+
assertSendable(ctx.url, method);
|
|
384
|
+
assertStreamable(ctx.url, method, ctx.init.body);
|
|
385
|
+
}
|
|
705
386
|
const { signal } = ctx.init;
|
|
706
387
|
let res;
|
|
707
388
|
try {
|
|
708
389
|
res = await fetchImpl(ctx.url, ctx.init);
|
|
709
390
|
} catch (thrown) {
|
|
710
|
-
if (isCancellation(thrown, signal)) throw thrown;
|
|
711
|
-
throw networkError(thrown, ctx);
|
|
391
|
+
if (require_plugins.isCancellation(thrown, signal)) throw require_plugins.cancellationError(thrown, signal);
|
|
392
|
+
throw require_plugins.networkError(thrown, ctx);
|
|
712
393
|
}
|
|
713
394
|
if (res.ok) return res;
|
|
714
395
|
if (!(throwOn ? throwOn(res) : failedByDefault(res, ctx.init))) return res;
|
|
715
|
-
const error = await responseError(res, ctx, resolved.errorDetail);
|
|
396
|
+
const error = await require_plugins.responseError(res, ctx, resolved.errorDetail);
|
|
716
397
|
if (signal?.aborted) throw signal.reason;
|
|
717
398
|
throw error;
|
|
718
399
|
}
|
|
@@ -742,9 +423,10 @@ function arthron(config = {}) {
|
|
|
742
423
|
const upper = method.toUpperCase();
|
|
743
424
|
const verb = STANDARD_METHODS.has(upper) ? upper : method;
|
|
744
425
|
if (rawBody !== void 0 && BODYLESS_METHODS.has(upper)) throw new TypeError(`arthron: ${upper} requests cannot carry a body — send it as params (query) or switch to POST`);
|
|
745
|
-
if (pathValues) path = fillPath(path, pathValues);
|
|
746
|
-
const url = resolveUrl(resolved.baseURL, path, params, resolved.allowAbsoluteUrls);
|
|
747
|
-
const
|
|
426
|
+
if (pathValues) path = require_plugins.fillPath(path, pathValues);
|
|
427
|
+
const url = require_plugins.resolveUrl(resolved.baseURL, path, params, resolved.allowAbsoluteUrls);
|
|
428
|
+
const defaults = require_plugins.leavesBase(resolved.baseURL, path) ? void 0 : resolved.headers;
|
|
429
|
+
const { body, headers } = require_plugins.serializeBody(rawBody, mergeHeaders(defaults, callHeaders));
|
|
748
430
|
const requestInit = {
|
|
749
431
|
...initDefaults,
|
|
750
432
|
...init,
|
|
@@ -752,12 +434,12 @@ function arthron(config = {}) {
|
|
|
752
434
|
headers
|
|
753
435
|
};
|
|
754
436
|
if (body !== void 0) requestInit.body = body;
|
|
755
|
-
if (isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
|
|
437
|
+
if (require_plugins.isReadableStream(body)) Object.assign(requestInit, { duplex: "half" });
|
|
756
438
|
return toArthronPromise(runPipeline(resolved.use, {
|
|
757
439
|
url,
|
|
758
440
|
init: requestInit,
|
|
759
441
|
meta: { ...meta }
|
|
760
|
-
}, (ctx) => send(resolved, ctx, throwOn)));
|
|
442
|
+
}, (ctx) => send(resolved, ctx, throwOn)), init.signal);
|
|
761
443
|
} catch (thrown) {
|
|
762
444
|
return toArthronPromise(Promise.reject(thrown));
|
|
763
445
|
}
|
|
@@ -804,278 +486,21 @@ const defineInterceptor = {
|
|
|
804
486
|
/** Identity helper: type-checks a plugin object literal without an annotation. */
|
|
805
487
|
const definePlugin = (plugin) => plugin;
|
|
806
488
|
//#endregion
|
|
807
|
-
//#region src/
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* One event when a request starts, one when it settles, with status and
|
|
817
|
-
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
818
|
-
* Default output is a console line per event; pass `onEvent` to get the
|
|
819
|
-
* structured event instead. Cancellations are not reported.
|
|
820
|
-
*/
|
|
821
|
-
function logging(options = {}) {
|
|
822
|
-
const { enabled = true, logger = console, onEvent, now = () => performance.now() } = options;
|
|
823
|
-
const emit = onEvent ?? consoleSink(logger);
|
|
824
|
-
return {
|
|
825
|
-
name: "logging",
|
|
826
|
-
middleware: async (ctx, next) => {
|
|
827
|
-
if (!enabled) return next(ctx);
|
|
828
|
-
const method = ctx.init.method ?? "GET";
|
|
829
|
-
const path = redactUrl(ctx.url);
|
|
830
|
-
const started = now();
|
|
831
|
-
const ms = () => Math.round(now() - started);
|
|
832
|
-
emit({
|
|
833
|
-
phase: "start",
|
|
834
|
-
method,
|
|
835
|
-
path
|
|
836
|
-
});
|
|
837
|
-
let res;
|
|
838
|
-
try {
|
|
839
|
-
res = await next(ctx);
|
|
840
|
-
} catch (error) {
|
|
841
|
-
if (!isCancellation(error, ctx.init.signal)) {
|
|
842
|
-
const status = isArthronError(error) ? error.status : void 0;
|
|
843
|
-
emit({
|
|
844
|
-
phase: "error",
|
|
845
|
-
method,
|
|
846
|
-
path,
|
|
847
|
-
status,
|
|
848
|
-
ms: ms(),
|
|
849
|
-
error
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
emit({
|
|
855
|
-
phase: "success",
|
|
856
|
-
method,
|
|
857
|
-
path,
|
|
858
|
-
status: res.status,
|
|
859
|
-
ms: ms()
|
|
860
|
-
});
|
|
861
|
-
return res;
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
//#endregion
|
|
866
|
-
//#region src/plugins/retry/retry.ts
|
|
867
|
-
const DEFAULT_RETRY_ON = [
|
|
868
|
-
408,
|
|
869
|
-
429,
|
|
870
|
-
500,
|
|
871
|
-
502,
|
|
872
|
-
503,
|
|
873
|
-
504
|
|
874
|
-
];
|
|
875
|
-
const DEFAULT_METHODS = [
|
|
876
|
-
"GET",
|
|
877
|
-
"HEAD",
|
|
878
|
-
"OPTIONS",
|
|
879
|
-
"QUERY",
|
|
880
|
-
"PUT",
|
|
881
|
-
"DELETE"
|
|
882
|
-
];
|
|
883
|
-
const DEFAULT_MAX_RETRY_AFTER = 6e4;
|
|
884
|
-
/** setTimeout fires at once past this, so every wait is capped to it. */
|
|
885
|
-
const MAX_TIMER_MS$1 = 2 ** 31 - 1;
|
|
886
|
-
function defaultSleep(ms, signal) {
|
|
887
|
-
return new Promise((resolve, reject) => {
|
|
888
|
-
if (signal?.aborted) return reject(signal.reason);
|
|
889
|
-
const onAbort = () => {
|
|
890
|
-
clearTimeout(timer);
|
|
891
|
-
reject(signal?.reason);
|
|
892
|
-
};
|
|
893
|
-
const timer = setTimeout(() => {
|
|
894
|
-
signal?.removeEventListener("abort", onAbort);
|
|
895
|
-
resolve();
|
|
896
|
-
}, ms);
|
|
897
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
/** `Retry-After` as ms — seconds or an HTTP-date. `undefined` when absent or unparsable. */
|
|
901
|
-
function retryAfterMs(error) {
|
|
902
|
-
const value = error.headers.get("retry-after")?.trim();
|
|
903
|
-
if (!value) return void 0;
|
|
904
|
-
const seconds = Number(value);
|
|
905
|
-
if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
|
|
906
|
-
const at = Date.parse(value);
|
|
907
|
-
if (Number.isNaN(at)) return void 0;
|
|
908
|
-
return Math.max(0, at - Date.now());
|
|
909
|
-
}
|
|
910
|
-
/** The rule without `shouldRetry`: a network error or a `retryOn` status, on a method in `methods`. */
|
|
911
|
-
function byDefault(thrown, method, settings) {
|
|
912
|
-
if (!settings.allowed.has(method) || !isArthronError(thrown)) return false;
|
|
913
|
-
return thrown.status === 0 || settings.retryOn.includes(thrown.status);
|
|
914
|
-
}
|
|
915
|
-
/** Defaults filled in and `retries` checked — `attempt >= NaN` is never true, so an unset env var would retry forever. */
|
|
916
|
-
function settle(options, where) {
|
|
917
|
-
const { retries = 2, retryOn = DEFAULT_RETRY_ON, methods = DEFAULT_METHODS, delay = (retry) => Math.random() * 300 * 2 ** retry, maxRetryAfter = DEFAULT_MAX_RETRY_AFTER, shouldRetry, onRetry } = options;
|
|
918
|
-
if (!Number.isInteger(retries) || retries < 0) throw new TypeError(`arthron: ${where} retries: ${retries} needs a whole number ≥ 0 — e.g. retries: 2`);
|
|
919
|
-
return {
|
|
920
|
-
retries,
|
|
921
|
-
retryOn,
|
|
922
|
-
allowed: new Set(methods.map((m) => m.toUpperCase())),
|
|
923
|
-
delay,
|
|
924
|
-
maxRetryAfter,
|
|
925
|
-
shouldRetry,
|
|
926
|
-
onRetry
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Retry on network errors and the configured statuses by calling `next`
|
|
931
|
-
* again — the whole attempt (request interceptors included) is re-run.
|
|
932
|
-
* Only `ArthronError` is considered; cancellation and plugin bugs pass through.
|
|
933
|
-
* A call can turn it off or override its options with `meta.retry`.
|
|
934
|
-
*/
|
|
935
|
-
function retry(options = {}) {
|
|
936
|
-
const { sleep = defaultSleep, ...rest } = options;
|
|
937
|
-
const base = settle(rest, "retry()");
|
|
938
|
-
return {
|
|
939
|
-
name: "retry",
|
|
940
|
-
middleware: async (ctx, next) => {
|
|
941
|
-
const override = ctx.meta?.retry;
|
|
942
|
-
if (override === false) return next(ctx);
|
|
943
|
-
const settings = override ? settle({
|
|
944
|
-
...rest,
|
|
945
|
-
...override
|
|
946
|
-
}, "meta.retry") : base;
|
|
947
|
-
const method = (ctx.init.method ?? "GET").toUpperCase();
|
|
948
|
-
const resendable = !isReadableStream(ctx.init.body);
|
|
949
|
-
for (let attempt = 0;; attempt += 1) try {
|
|
950
|
-
return await next(ctx);
|
|
951
|
-
} catch (thrown) {
|
|
952
|
-
if (!resendable || attempt >= settings.retries) throw thrown;
|
|
953
|
-
if (ctx.init.signal?.aborted) throw thrown;
|
|
954
|
-
const verdict = settings.shouldRetry?.(thrown, attempt, ctx);
|
|
955
|
-
if (verdict === false) throw thrown;
|
|
956
|
-
if (verdict === void 0 && !byDefault(thrown, method, settings)) throw thrown;
|
|
957
|
-
const retryAfter = isArthronError(thrown) ? retryAfterMs(thrown) : void 0;
|
|
958
|
-
if (retryAfter !== void 0 && retryAfter > settings.maxRetryAfter) throw thrown;
|
|
959
|
-
const wait = Math.min(retryAfter ?? settings.delay(attempt, thrown), MAX_TIMER_MS$1);
|
|
960
|
-
settings.onRetry?.(thrown, attempt, wait);
|
|
961
|
-
await sleep(wait, ctx.init.signal);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
//#endregion
|
|
967
|
-
//#region src/plugins/timeout/timeout.ts
|
|
968
|
-
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
969
|
-
/**
|
|
970
|
-
* Wrap `body` so the deadline keeps running until it is fully read, and so an
|
|
971
|
-
* abort while reading surfaces as a stream error instead of a silent stall.
|
|
972
|
-
*/
|
|
973
|
-
function guardBody(body, signal, finish, onChunk) {
|
|
974
|
-
const reader = body.getReader();
|
|
975
|
-
return new ReadableStream({
|
|
976
|
-
start(controller) {
|
|
977
|
-
signal.addEventListener("abort", () => {
|
|
978
|
-
controller.error(signal.reason);
|
|
979
|
-
reader.cancel(signal.reason).catch(() => {});
|
|
980
|
-
}, { once: true });
|
|
981
|
-
},
|
|
982
|
-
async pull(controller) {
|
|
983
|
-
const { value, done } = await reader.read();
|
|
984
|
-
if (done) {
|
|
985
|
-
finish();
|
|
986
|
-
controller.close();
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
onChunk();
|
|
990
|
-
controller.enqueue(value);
|
|
991
|
-
},
|
|
992
|
-
cancel(reason) {
|
|
993
|
-
finish();
|
|
994
|
-
return reader.cancel(reason);
|
|
995
|
-
}
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
/** setTimeout fires at once for NaN, negatives and anything past 2^31-1. */
|
|
999
|
-
function checkDeadline(ms, where) {
|
|
1000
|
-
if (ms === Number.POSITIVE_INFINITY || ms >= 0 && ms <= MAX_TIMER_MS) return;
|
|
1001
|
-
throw new TypeError(`arthron: ${where} needs 0–${MAX_TIMER_MS} ms — pass Infinity for no deadline`);
|
|
1002
|
-
}
|
|
1003
|
-
/** A timer that aborts `controller` with a TimeoutError and never keeps Node alive on its own. */
|
|
1004
|
-
function deadlineTimer(controller, ms, message) {
|
|
1005
|
-
const timer = setTimeout(() => controller.abort(new DOMException(message, "TimeoutError")), ms);
|
|
1006
|
-
timer.unref?.();
|
|
1007
|
-
return timer;
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* Abort the request when it takes longer than `ms` — counted from the start
|
|
1011
|
-
* of the attempt until the body has been fully read, so a server that sends
|
|
1012
|
-
* headers and then stalls still trips it. With `idle`, also abort once no
|
|
1013
|
-
* data has arrived for that long. Rejects with a `TimeoutError` (a
|
|
1014
|
-
* `DOMException`, like `AbortSignal.timeout()` would give); it is not an
|
|
1015
|
-
* `ArthronError`, so error interceptors and `retry` leave it alone.
|
|
1016
|
-
*
|
|
1017
|
-
* A call can change the total deadline with `meta.timeout`.
|
|
1018
|
-
*
|
|
1019
|
-
* Deadlines are timers on an AbortController — not `AbortSignal.timeout`,
|
|
1020
|
-
* since the idle one restarts — joined to the caller's signal with
|
|
1021
|
-
* `AbortSignal.any`.
|
|
1022
|
-
*/
|
|
1023
|
-
function timeout(ms, options = {}) {
|
|
1024
|
-
checkDeadline(ms, `timeout(${ms})`);
|
|
1025
|
-
const { idle } = options;
|
|
1026
|
-
if (idle !== void 0) checkDeadline(idle, `timeout(…, { idle: ${idle} })`);
|
|
1027
|
-
return {
|
|
1028
|
-
name: "timeout",
|
|
1029
|
-
middleware: async (ctx, next) => {
|
|
1030
|
-
const deadline = ctx.meta?.timeout ?? ms;
|
|
1031
|
-
if (deadline !== ms) checkDeadline(deadline, `meta.timeout: ${deadline}`);
|
|
1032
|
-
const idleMs = idle === Number.POSITIVE_INFINITY ? void 0 : idle;
|
|
1033
|
-
if (deadline === Number.POSITIVE_INFINITY && idleMs === void 0) return next(ctx);
|
|
1034
|
-
const controller = new AbortController();
|
|
1035
|
-
const outer = ctx.init.signal;
|
|
1036
|
-
const signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
|
|
1037
|
-
const total = deadline === Number.POSITIVE_INFINITY ? void 0 : deadlineTimer(controller, deadline, `arthron: request timed out after ${deadline}ms`);
|
|
1038
|
-
let quiet;
|
|
1039
|
-
const stillAlive = () => {
|
|
1040
|
-
if (idleMs === void 0) return;
|
|
1041
|
-
clearTimeout(quiet);
|
|
1042
|
-
quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
|
|
1043
|
-
};
|
|
1044
|
-
const finish = () => {
|
|
1045
|
-
clearTimeout(total);
|
|
1046
|
-
clearTimeout(quiet);
|
|
1047
|
-
};
|
|
1048
|
-
stillAlive();
|
|
1049
|
-
let res;
|
|
1050
|
-
try {
|
|
1051
|
-
res = await next({
|
|
1052
|
-
...ctx,
|
|
1053
|
-
init: {
|
|
1054
|
-
...ctx.init,
|
|
1055
|
-
signal
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
} catch (thrown) {
|
|
1059
|
-
finish();
|
|
1060
|
-
throw thrown;
|
|
1061
|
-
}
|
|
1062
|
-
if (!res.body) {
|
|
1063
|
-
finish();
|
|
1064
|
-
return res;
|
|
1065
|
-
}
|
|
1066
|
-
stillAlive();
|
|
1067
|
-
return rebuildResponse(res, guardBody(res.body, signal, finish, stillAlive));
|
|
1068
|
-
}
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
489
|
+
//#region src/index.ts
|
|
490
|
+
/** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
|
|
491
|
+
const logging = require_plugins.logging;
|
|
492
|
+
/** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
|
|
493
|
+
const retry = require_plugins.retry;
|
|
494
|
+
/** @deprecated Import from "arthron/plugins"; this export goes in the next major. */
|
|
495
|
+
const timeout = require_plugins.timeout;
|
|
1071
496
|
//#endregion
|
|
1072
|
-
exports.ArthronError = ArthronError;
|
|
497
|
+
exports.ArthronError = require_plugins.ArthronError;
|
|
1073
498
|
exports.SchemaError = SchemaError;
|
|
1074
499
|
exports.arthron = arthron;
|
|
1075
500
|
exports.defineInterceptor = defineInterceptor;
|
|
1076
501
|
exports.defineMiddleware = defineMiddleware;
|
|
1077
502
|
exports.definePlugin = definePlugin;
|
|
1078
|
-
exports.isArthronError = isArthronError;
|
|
503
|
+
exports.isArthronError = require_plugins.isArthronError;
|
|
1079
504
|
exports.logging = logging;
|
|
1080
505
|
exports.retry = retry;
|
|
1081
506
|
exports.timeout = timeout;
|