arthron 2.0.1 → 2.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 +45 -506
- package/dist/index.cjs +26 -16
- package/dist/index.mjs +27 -17
- package/dist/plugins.cjs +45 -25
- package/dist/plugins.d.cts +10 -1
- package/dist/plugins.d.mts +10 -1
- package/dist/plugins.mjs +46 -26
- package/dist/shared.cjs +138 -22
- package/dist/shared.mjs +127 -23
- package/llms.txt +8 -8
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -183,6 +183,7 @@ async function validate(schema, body) {
|
|
|
183
183
|
//#endregion
|
|
184
184
|
//#region src/response/stream-parser.ts
|
|
185
185
|
const DEFAULT_LIMIT = 8388608;
|
|
186
|
+
const DECODE_CHUNK_BYTES = 16384;
|
|
186
187
|
/**
|
|
187
188
|
* Yield the body line by line, reassembling lines split across chunks. Only
|
|
188
189
|
* the newly arrived text is scanned, so a long line costs linear time; a line
|
|
@@ -199,28 +200,37 @@ async function* lines(response, maxLineLength) {
|
|
|
199
200
|
const breaks = /\r\n|\r|\n/g;
|
|
200
201
|
let pending = "";
|
|
201
202
|
let afterCR = false;
|
|
203
|
+
const scan = function* (decoded) {
|
|
204
|
+
let text = decoded;
|
|
205
|
+
if (afterCR && text.startsWith("\n")) {
|
|
206
|
+
text = text.slice(1);
|
|
207
|
+
afterCR = false;
|
|
208
|
+
}
|
|
209
|
+
if (text) afterCR = text.endsWith("\r");
|
|
210
|
+
let start = 0;
|
|
211
|
+
breaks.lastIndex = 0;
|
|
212
|
+
for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
|
|
213
|
+
yield checked(pending + text.slice(start, m.index));
|
|
214
|
+
pending = "";
|
|
215
|
+
start = breaks.lastIndex;
|
|
216
|
+
}
|
|
217
|
+
pending = checked(pending + text.slice(start));
|
|
218
|
+
};
|
|
202
219
|
try {
|
|
203
220
|
while (true) {
|
|
204
221
|
const { value, done } = await reader.read();
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
afterCR = false;
|
|
222
|
+
if (value) for (let offset = 0; offset < value.byteLength; offset += DECODE_CHUNK_BYTES) {
|
|
223
|
+
const part = value.subarray(offset, offset + DECODE_CHUNK_BYTES);
|
|
224
|
+
yield* scan(decoder.decode(part, { stream: true }));
|
|
209
225
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
|
|
214
|
-
yield checked(pending + text.slice(start, m.index));
|
|
215
|
-
pending = "";
|
|
216
|
-
start = breaks.lastIndex;
|
|
226
|
+
if (done) {
|
|
227
|
+
yield* scan(decoder.decode());
|
|
228
|
+
break;
|
|
217
229
|
}
|
|
218
|
-
pending = checked(pending + text.slice(start));
|
|
219
|
-
if (done) break;
|
|
220
230
|
}
|
|
221
231
|
if (pending) yield pending;
|
|
222
232
|
} finally {
|
|
223
|
-
|
|
233
|
+
reader.cancel().catch(() => {});
|
|
224
234
|
reader.releaseLock();
|
|
225
235
|
}
|
|
226
236
|
}
|
|
@@ -481,9 +491,9 @@ function assertSendable(url, method) {
|
|
|
481
491
|
try {
|
|
482
492
|
new Request(url, { method });
|
|
483
493
|
} catch (cause) {
|
|
484
|
-
require_create_error.redactErrorUrl(cause, url);
|
|
494
|
+
const safeCause = require_create_error.redactErrorUrl(cause, url);
|
|
485
495
|
const reason = require_create_error.redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
|
|
486
|
-
throw new TypeError(`arthron: cannot send ${method} ${require_create_error.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
|
|
496
|
+
throw new TypeError(`arthron: cannot send ${method} ${require_create_error.redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause: safeCause });
|
|
487
497
|
}
|
|
488
498
|
}
|
|
489
499
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as redactUrlIn, S as redactUrl, T as withoutSecrets, _ as httpOrigin, a as redactErrorUrl, d as runtimeStreamsRequestBody, f as serializeBody, g as fillPath, h as isArthronError, i as networkError, m as ArthronError, n as isCancellation, o as responseError, p as tagOf, t as cancellationError, u as isReadableStream, w as resolveUrl, x as originSet, y as leavesBase } from "./shared.mjs";
|
|
2
2
|
//#region src/request/headers.ts
|
|
3
3
|
const isPlainRecord = (headers) => !Array.isArray(headers) && Object.prototype.toString.call(headers) !== "[object Headers]";
|
|
4
4
|
/**
|
|
@@ -182,6 +182,7 @@ async function validate(schema, body) {
|
|
|
182
182
|
//#endregion
|
|
183
183
|
//#region src/response/stream-parser.ts
|
|
184
184
|
const DEFAULT_LIMIT = 8388608;
|
|
185
|
+
const DECODE_CHUNK_BYTES = 16384;
|
|
185
186
|
/**
|
|
186
187
|
* Yield the body line by line, reassembling lines split across chunks. Only
|
|
187
188
|
* the newly arrived text is scanned, so a long line costs linear time; a line
|
|
@@ -198,28 +199,37 @@ async function* lines(response, maxLineLength) {
|
|
|
198
199
|
const breaks = /\r\n|\r|\n/g;
|
|
199
200
|
let pending = "";
|
|
200
201
|
let afterCR = false;
|
|
202
|
+
const scan = function* (decoded) {
|
|
203
|
+
let text = decoded;
|
|
204
|
+
if (afterCR && text.startsWith("\n")) {
|
|
205
|
+
text = text.slice(1);
|
|
206
|
+
afterCR = false;
|
|
207
|
+
}
|
|
208
|
+
if (text) afterCR = text.endsWith("\r");
|
|
209
|
+
let start = 0;
|
|
210
|
+
breaks.lastIndex = 0;
|
|
211
|
+
for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
|
|
212
|
+
yield checked(pending + text.slice(start, m.index));
|
|
213
|
+
pending = "";
|
|
214
|
+
start = breaks.lastIndex;
|
|
215
|
+
}
|
|
216
|
+
pending = checked(pending + text.slice(start));
|
|
217
|
+
};
|
|
201
218
|
try {
|
|
202
219
|
while (true) {
|
|
203
220
|
const { value, done } = await reader.read();
|
|
204
|
-
let
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
afterCR = false;
|
|
221
|
+
if (value) for (let offset = 0; offset < value.byteLength; offset += DECODE_CHUNK_BYTES) {
|
|
222
|
+
const part = value.subarray(offset, offset + DECODE_CHUNK_BYTES);
|
|
223
|
+
yield* scan(decoder.decode(part, { stream: true }));
|
|
208
224
|
}
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (let m = breaks.exec(text); m; m = breaks.exec(text)) {
|
|
213
|
-
yield checked(pending + text.slice(start, m.index));
|
|
214
|
-
pending = "";
|
|
215
|
-
start = breaks.lastIndex;
|
|
225
|
+
if (done) {
|
|
226
|
+
yield* scan(decoder.decode());
|
|
227
|
+
break;
|
|
216
228
|
}
|
|
217
|
-
pending = checked(pending + text.slice(start));
|
|
218
|
-
if (done) break;
|
|
219
229
|
}
|
|
220
230
|
if (pending) yield pending;
|
|
221
231
|
} finally {
|
|
222
|
-
|
|
232
|
+
reader.cancel().catch(() => {});
|
|
223
233
|
reader.releaseLock();
|
|
224
234
|
}
|
|
225
235
|
}
|
|
@@ -480,9 +490,9 @@ function assertSendable(url, method) {
|
|
|
480
490
|
try {
|
|
481
491
|
new Request(url, { method });
|
|
482
492
|
} catch (cause) {
|
|
483
|
-
redactErrorUrl(cause, url);
|
|
493
|
+
const safeCause = redactErrorUrl(cause, url);
|
|
484
494
|
const reason = redactUrlIn(cause instanceof Error ? cause.message : String(cause), url);
|
|
485
|
-
throw new TypeError(`arthron: cannot send ${method} ${redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause });
|
|
495
|
+
throw new TypeError(`arthron: cannot send ${method} ${redactUrl(url)} (${reason}) — use an absolute URL or a baseURL with a scheme (https://…), and a valid HTTP method`, { cause: safeCause });
|
|
486
496
|
}
|
|
487
497
|
}
|
|
488
498
|
/**
|
package/dist/plugins.cjs
CHANGED
|
@@ -100,18 +100,24 @@ function bearerAuth(options) {
|
|
|
100
100
|
}
|
|
101
101
|
//#endregion
|
|
102
102
|
//#region src/plugins/logging/logging.ts
|
|
103
|
+
/** What an error line shows without a status: the error's name (`TimeoutError`, `TypeError`), else `-`. */
|
|
104
|
+
function nameOf(error) {
|
|
105
|
+
const name = error?.name;
|
|
106
|
+
return typeof name === "string" && name !== "" ? name : "-";
|
|
107
|
+
}
|
|
103
108
|
function consoleSink(logger) {
|
|
104
109
|
return (e) => {
|
|
105
110
|
if (e.phase === "start") logger.log(`[arthron] → ${e.method} ${e.url}`);
|
|
106
111
|
else if (e.phase === "success") logger.log(`[arthron] ← ${e.method} ${e.url} ${e.status} ${e.duration}ms`);
|
|
107
|
-
else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ??
|
|
112
|
+
else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ?? nameOf(e.error)} ${e.duration}ms`);
|
|
108
113
|
};
|
|
109
114
|
}
|
|
110
115
|
/**
|
|
111
116
|
* One event when a request starts, one when it settles, with status and
|
|
112
117
|
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
113
118
|
* Default output is a console line per event; pass `onEvent` to get the
|
|
114
|
-
* structured event instead. Cancellations are not reported
|
|
119
|
+
* structured event instead. Cancellations are not reported, timeouts only
|
|
120
|
+
* with `timeouts: true`.
|
|
115
121
|
*/
|
|
116
122
|
function logging(options = {}) {
|
|
117
123
|
if (options.now !== void 0) throw new TypeError("arthron: logging's now option left the public options in 2.0 — mock performance.now in tests (vi.spyOn(performance, \"now\"))");
|
|
@@ -119,7 +125,8 @@ function logging(options = {}) {
|
|
|
119
125
|
}
|
|
120
126
|
/** `logging()` with the clock injected — for this package's tests; `arthron/plugins` does not export it. */
|
|
121
127
|
function loggingWithClock(options, now) {
|
|
122
|
-
const { enabled = true, logger = console, onEvent } = options;
|
|
128
|
+
const { enabled = true, logger = console, onEvent, timeouts = false } = options;
|
|
129
|
+
const reported = (error, signal) => !require_create_error.isCancellation(error, signal) || timeouts && require_create_error.isTimeout(error);
|
|
123
130
|
const emit = onEvent ?? consoleSink(logger);
|
|
124
131
|
return {
|
|
125
132
|
pluginName: "logging",
|
|
@@ -138,7 +145,7 @@ function loggingWithClock(options, now) {
|
|
|
138
145
|
try {
|
|
139
146
|
res = await next(ctx);
|
|
140
147
|
} catch (error) {
|
|
141
|
-
if (
|
|
148
|
+
if (reported(error, ctx.init.signal)) {
|
|
142
149
|
const status = require_create_error.isArthronError(error) ? error.status : void 0;
|
|
143
150
|
emit({
|
|
144
151
|
phase: "error",
|
|
@@ -293,8 +300,10 @@ const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
|
293
300
|
/**
|
|
294
301
|
* Wrap `body` so the deadline keeps running until it is fully read, and so an
|
|
295
302
|
* abort while reading surfaces as a stream error instead of a silent stall.
|
|
303
|
+
* The idle deadline runs only while a read waits on the source: the time the
|
|
304
|
+
* caller takes between reads is not the server going quiet.
|
|
296
305
|
*/
|
|
297
|
-
function guardBody(body, signal, finish,
|
|
306
|
+
function guardBody(body, signal, finish, idle) {
|
|
298
307
|
const reader = body.getReader();
|
|
299
308
|
let onAbort = () => {};
|
|
300
309
|
const release = () => {
|
|
@@ -305,12 +314,15 @@ function guardBody(body, signal, finish, onChunk) {
|
|
|
305
314
|
start(controller) {
|
|
306
315
|
onAbort = () => {
|
|
307
316
|
controller.error(signal.reason);
|
|
317
|
+
release();
|
|
308
318
|
reader.cancel(signal.reason).catch(() => {});
|
|
309
319
|
};
|
|
310
|
-
signal.
|
|
320
|
+
if (signal.aborted) onAbort();
|
|
321
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
311
322
|
},
|
|
312
323
|
async pull(controller) {
|
|
313
324
|
let chunk;
|
|
325
|
+
idle.start();
|
|
314
326
|
try {
|
|
315
327
|
chunk = await reader.read();
|
|
316
328
|
} catch (error) {
|
|
@@ -322,7 +334,7 @@ function guardBody(body, signal, finish, onChunk) {
|
|
|
322
334
|
controller.close();
|
|
323
335
|
return;
|
|
324
336
|
}
|
|
325
|
-
|
|
337
|
+
idle.stop();
|
|
326
338
|
controller.enqueue(chunk.value);
|
|
327
339
|
},
|
|
328
340
|
cancel(reason) {
|
|
@@ -396,16 +408,19 @@ function timeout(first, removed) {
|
|
|
396
408
|
const signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
|
|
397
409
|
const total = deadline === Number.POSITIVE_INFINITY ? void 0 : deadlineTimer(controller, deadline, `arthron: request timed out after ${deadline}ms`);
|
|
398
410
|
let quiet;
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
411
|
+
const idleTimer = {
|
|
412
|
+
start: () => {
|
|
413
|
+
if (idleMs === void 0) return;
|
|
414
|
+
clearTimeout(quiet);
|
|
415
|
+
quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
|
|
416
|
+
},
|
|
417
|
+
stop: () => clearTimeout(quiet)
|
|
403
418
|
};
|
|
404
419
|
const finish = () => {
|
|
405
420
|
clearTimeout(total);
|
|
406
|
-
|
|
421
|
+
idleTimer.stop();
|
|
407
422
|
};
|
|
408
|
-
|
|
423
|
+
idleTimer.start();
|
|
409
424
|
let res;
|
|
410
425
|
try {
|
|
411
426
|
res = await next({
|
|
@@ -423,8 +438,8 @@ function timeout(first, removed) {
|
|
|
423
438
|
finish();
|
|
424
439
|
return res;
|
|
425
440
|
}
|
|
426
|
-
|
|
427
|
-
return require_create_error.rebuildResponse(res, guardBody(res.body, signal, finish,
|
|
441
|
+
idleTimer.stop();
|
|
442
|
+
return require_create_error.rebuildResponse(res, guardBody(res.body, signal, finish, idleTimer));
|
|
428
443
|
}
|
|
429
444
|
};
|
|
430
445
|
}
|
|
@@ -449,7 +464,7 @@ function encodeFormData(form) {
|
|
|
449
464
|
const b = boundary();
|
|
450
465
|
const parts = [];
|
|
451
466
|
for (const [name, value] of form) {
|
|
452
|
-
let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(name)}"`;
|
|
467
|
+
let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(normalizeBreaks(name))}"`;
|
|
453
468
|
if (typeof value === "string") {
|
|
454
469
|
parts.push(encoder.encode(`${head}\r\n\r\n`));
|
|
455
470
|
parts.push(encoder.encode(normalizeBreaks(value)));
|
|
@@ -711,15 +726,20 @@ function xhrFetch(onProgress) {
|
|
|
711
726
|
xhr.onload = () => {
|
|
712
727
|
if (!reported) report(0, 0);
|
|
713
728
|
if (xhr.status === 0) return settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
status: xhr.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
729
|
+
let res;
|
|
730
|
+
try {
|
|
731
|
+
const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
|
|
732
|
+
const body = NULL_BODY_STATUSES.has(xhr.status) ? null : xhr.response;
|
|
733
|
+
const page = globalThis.location?.href;
|
|
734
|
+
res = require_create_error.withFields(require_create_error.createResponse(body, {
|
|
735
|
+
status: xhr.status,
|
|
736
|
+
statusText: xhr.statusText,
|
|
737
|
+
headers
|
|
738
|
+
}), responseFields(url, xhr.responseURL, page));
|
|
739
|
+
} catch (error) {
|
|
740
|
+
return settle(() => reject(error));
|
|
741
|
+
}
|
|
742
|
+
settle(() => resolve(res));
|
|
723
743
|
};
|
|
724
744
|
xhr.onerror = xhr.ontimeout = () => settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
|
|
725
745
|
xhr.open(init.method ?? "GET", url);
|
package/dist/plugins.d.cts
CHANGED
|
@@ -71,6 +71,14 @@ type LoggingOptions = {
|
|
|
71
71
|
logger?: Pick<Console, "log" | "error">;
|
|
72
72
|
/** Receive structured events instead of the default console lines. */
|
|
73
73
|
onEvent?: (event: LogEvent) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Default `false`. `true` reports a call that timed out before its response
|
|
76
|
+
* arrived — a `TimeoutError`, from `timeout()` or `AbortSignal.timeout()` —
|
|
77
|
+
* as an error event, `status` undefined. A timeout while the body is read
|
|
78
|
+
* rejects that read instead: the call has already logged its success.
|
|
79
|
+
* Other cancellations are never reported.
|
|
80
|
+
*/
|
|
81
|
+
timeouts?: boolean;
|
|
74
82
|
};
|
|
75
83
|
//#endregion
|
|
76
84
|
//#region src/plugins/logging/logging.d.ts
|
|
@@ -78,7 +86,8 @@ type LoggingOptions = {
|
|
|
78
86
|
* One event when a request starts, one when it settles, with status and
|
|
79
87
|
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
80
88
|
* Default output is a console line per event; pass `onEvent` to get the
|
|
81
|
-
* structured event instead. Cancellations are not reported
|
|
89
|
+
* structured event instead. Cancellations are not reported, timeouts only
|
|
90
|
+
* with `timeouts: true`.
|
|
82
91
|
*/
|
|
83
92
|
export declare function logging(options?: LoggingOptions): Plugin;
|
|
84
93
|
//#endregion
|
package/dist/plugins.d.mts
CHANGED
|
@@ -71,6 +71,14 @@ type LoggingOptions = {
|
|
|
71
71
|
logger?: Pick<Console, "log" | "error">;
|
|
72
72
|
/** Receive structured events instead of the default console lines. */
|
|
73
73
|
onEvent?: (event: LogEvent) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Default `false`. `true` reports a call that timed out before its response
|
|
76
|
+
* arrived — a `TimeoutError`, from `timeout()` or `AbortSignal.timeout()` —
|
|
77
|
+
* as an error event, `status` undefined. A timeout while the body is read
|
|
78
|
+
* rejects that read instead: the call has already logged its success.
|
|
79
|
+
* Other cancellations are never reported.
|
|
80
|
+
*/
|
|
81
|
+
timeouts?: boolean;
|
|
74
82
|
};
|
|
75
83
|
//#endregion
|
|
76
84
|
//#region src/plugins/logging/logging.d.ts
|
|
@@ -78,7 +86,8 @@ type LoggingOptions = {
|
|
|
78
86
|
* One event when a request starts, one when it settles, with status and
|
|
79
87
|
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
80
88
|
* Default output is a console line per event; pass `onEvent` to get the
|
|
81
|
-
* structured event instead. Cancellations are not reported
|
|
89
|
+
* structured event instead. Cancellations are not reported, timeouts only
|
|
90
|
+
* with `timeouts: true`.
|
|
82
91
|
*/
|
|
83
92
|
export declare function logging(options?: LoggingOptions): Plugin;
|
|
84
93
|
//#endregion
|
package/dist/plugins.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as redactUrl, b as originOf, c as rebuildResponse, h as isArthronError, l as withFields, n as isCancellation, p as tagOf, r as isTimeout, s as createResponse, u as isReadableStream, v as httpOrigins } from "./shared.mjs";
|
|
2
2
|
//#region src/plugins/bearer-auth/bearer-auth.ts
|
|
3
3
|
/** 1.x option, its replacement, and how to pass it. */
|
|
4
4
|
const REPLACED$1 = [
|
|
@@ -99,18 +99,24 @@ function bearerAuth(options) {
|
|
|
99
99
|
}
|
|
100
100
|
//#endregion
|
|
101
101
|
//#region src/plugins/logging/logging.ts
|
|
102
|
+
/** What an error line shows without a status: the error's name (`TimeoutError`, `TypeError`), else `-`. */
|
|
103
|
+
function nameOf(error) {
|
|
104
|
+
const name = error?.name;
|
|
105
|
+
return typeof name === "string" && name !== "" ? name : "-";
|
|
106
|
+
}
|
|
102
107
|
function consoleSink(logger) {
|
|
103
108
|
return (e) => {
|
|
104
109
|
if (e.phase === "start") logger.log(`[arthron] → ${e.method} ${e.url}`);
|
|
105
110
|
else if (e.phase === "success") logger.log(`[arthron] ← ${e.method} ${e.url} ${e.status} ${e.duration}ms`);
|
|
106
|
-
else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ??
|
|
111
|
+
else logger.error(`[arthron] ✕ ${e.method} ${e.url} ${e.status ?? nameOf(e.error)} ${e.duration}ms`);
|
|
107
112
|
};
|
|
108
113
|
}
|
|
109
114
|
/**
|
|
110
115
|
* One event when a request starts, one when it settles, with status and
|
|
111
116
|
* elapsed time. Wraps the whole call, so retries are included in the time.
|
|
112
117
|
* Default output is a console line per event; pass `onEvent` to get the
|
|
113
|
-
* structured event instead. Cancellations are not reported
|
|
118
|
+
* structured event instead. Cancellations are not reported, timeouts only
|
|
119
|
+
* with `timeouts: true`.
|
|
114
120
|
*/
|
|
115
121
|
function logging(options = {}) {
|
|
116
122
|
if (options.now !== void 0) throw new TypeError("arthron: logging's now option left the public options in 2.0 — mock performance.now in tests (vi.spyOn(performance, \"now\"))");
|
|
@@ -118,7 +124,8 @@ function logging(options = {}) {
|
|
|
118
124
|
}
|
|
119
125
|
/** `logging()` with the clock injected — for this package's tests; `arthron/plugins` does not export it. */
|
|
120
126
|
function loggingWithClock(options, now) {
|
|
121
|
-
const { enabled = true, logger = console, onEvent } = options;
|
|
127
|
+
const { enabled = true, logger = console, onEvent, timeouts = false } = options;
|
|
128
|
+
const reported = (error, signal) => !isCancellation(error, signal) || timeouts && isTimeout(error);
|
|
122
129
|
const emit = onEvent ?? consoleSink(logger);
|
|
123
130
|
return {
|
|
124
131
|
pluginName: "logging",
|
|
@@ -137,7 +144,7 @@ function loggingWithClock(options, now) {
|
|
|
137
144
|
try {
|
|
138
145
|
res = await next(ctx);
|
|
139
146
|
} catch (error) {
|
|
140
|
-
if (
|
|
147
|
+
if (reported(error, ctx.init.signal)) {
|
|
141
148
|
const status = isArthronError(error) ? error.status : void 0;
|
|
142
149
|
emit({
|
|
143
150
|
phase: "error",
|
|
@@ -292,8 +299,10 @@ const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
|
292
299
|
/**
|
|
293
300
|
* Wrap `body` so the deadline keeps running until it is fully read, and so an
|
|
294
301
|
* abort while reading surfaces as a stream error instead of a silent stall.
|
|
302
|
+
* The idle deadline runs only while a read waits on the source: the time the
|
|
303
|
+
* caller takes between reads is not the server going quiet.
|
|
295
304
|
*/
|
|
296
|
-
function guardBody(body, signal, finish,
|
|
305
|
+
function guardBody(body, signal, finish, idle) {
|
|
297
306
|
const reader = body.getReader();
|
|
298
307
|
let onAbort = () => {};
|
|
299
308
|
const release = () => {
|
|
@@ -304,12 +313,15 @@ function guardBody(body, signal, finish, onChunk) {
|
|
|
304
313
|
start(controller) {
|
|
305
314
|
onAbort = () => {
|
|
306
315
|
controller.error(signal.reason);
|
|
316
|
+
release();
|
|
307
317
|
reader.cancel(signal.reason).catch(() => {});
|
|
308
318
|
};
|
|
309
|
-
signal.
|
|
319
|
+
if (signal.aborted) onAbort();
|
|
320
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
310
321
|
},
|
|
311
322
|
async pull(controller) {
|
|
312
323
|
let chunk;
|
|
324
|
+
idle.start();
|
|
313
325
|
try {
|
|
314
326
|
chunk = await reader.read();
|
|
315
327
|
} catch (error) {
|
|
@@ -321,7 +333,7 @@ function guardBody(body, signal, finish, onChunk) {
|
|
|
321
333
|
controller.close();
|
|
322
334
|
return;
|
|
323
335
|
}
|
|
324
|
-
|
|
336
|
+
idle.stop();
|
|
325
337
|
controller.enqueue(chunk.value);
|
|
326
338
|
},
|
|
327
339
|
cancel(reason) {
|
|
@@ -395,16 +407,19 @@ function timeout(first, removed) {
|
|
|
395
407
|
const signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
|
|
396
408
|
const total = deadline === Number.POSITIVE_INFINITY ? void 0 : deadlineTimer(controller, deadline, `arthron: request timed out after ${deadline}ms`);
|
|
397
409
|
let quiet;
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
410
|
+
const idleTimer = {
|
|
411
|
+
start: () => {
|
|
412
|
+
if (idleMs === void 0) return;
|
|
413
|
+
clearTimeout(quiet);
|
|
414
|
+
quiet = deadlineTimer(controller, idleMs, `arthron: no data for ${idleMs}ms`);
|
|
415
|
+
},
|
|
416
|
+
stop: () => clearTimeout(quiet)
|
|
402
417
|
};
|
|
403
418
|
const finish = () => {
|
|
404
419
|
clearTimeout(total);
|
|
405
|
-
|
|
420
|
+
idleTimer.stop();
|
|
406
421
|
};
|
|
407
|
-
|
|
422
|
+
idleTimer.start();
|
|
408
423
|
let res;
|
|
409
424
|
try {
|
|
410
425
|
res = await next({
|
|
@@ -422,8 +437,8 @@ function timeout(first, removed) {
|
|
|
422
437
|
finish();
|
|
423
438
|
return res;
|
|
424
439
|
}
|
|
425
|
-
|
|
426
|
-
return rebuildResponse(res, guardBody(res.body, signal, finish,
|
|
440
|
+
idleTimer.stop();
|
|
441
|
+
return rebuildResponse(res, guardBody(res.body, signal, finish, idleTimer));
|
|
427
442
|
}
|
|
428
443
|
};
|
|
429
444
|
}
|
|
@@ -448,7 +463,7 @@ function encodeFormData(form) {
|
|
|
448
463
|
const b = boundary();
|
|
449
464
|
const parts = [];
|
|
450
465
|
for (const [name, value] of form) {
|
|
451
|
-
let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(name)}"`;
|
|
466
|
+
let head = `--${b}\r\nContent-Disposition: form-data; name="${escapeName(normalizeBreaks(name))}"`;
|
|
452
467
|
if (typeof value === "string") {
|
|
453
468
|
parts.push(encoder.encode(`${head}\r\n\r\n`));
|
|
454
469
|
parts.push(encoder.encode(normalizeBreaks(value)));
|
|
@@ -710,15 +725,20 @@ function xhrFetch(onProgress) {
|
|
|
710
725
|
xhr.onload = () => {
|
|
711
726
|
if (!reported) report(0, 0);
|
|
712
727
|
if (xhr.status === 0) return settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
status: xhr.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
728
|
+
let res;
|
|
729
|
+
try {
|
|
730
|
+
const headers = parseResponseHeaders(xhr.getAllResponseHeaders());
|
|
731
|
+
const body = NULL_BODY_STATUSES.has(xhr.status) ? null : xhr.response;
|
|
732
|
+
const page = globalThis.location?.href;
|
|
733
|
+
res = withFields(createResponse(body, {
|
|
734
|
+
status: xhr.status,
|
|
735
|
+
statusText: xhr.statusText,
|
|
736
|
+
headers
|
|
737
|
+
}), responseFields(url, xhr.responseURL, page));
|
|
738
|
+
} catch (error) {
|
|
739
|
+
return settle(() => reject(error));
|
|
740
|
+
}
|
|
741
|
+
settle(() => resolve(res));
|
|
722
742
|
};
|
|
723
743
|
xhr.onerror = xhr.ontimeout = () => settle(() => reject(/* @__PURE__ */ new TypeError("Network request failed")));
|
|
724
744
|
xhr.open(init.method ?? "GET", url);
|