@fedify/vocab-runtime 2.4.0-dev.2169 → 2.4.0-dev.2190

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/deno.json +1 -1
  2. package/dist/{docloader-ClQraSWr.d.cts → docloader-C63enuyr.d.cts} +53 -1
  3. package/dist/{docloader-ClQraSWr.d.ts → docloader-C63enuyr.d.ts} +53 -1
  4. package/dist/internal/jsonld-cache.d.cts +1 -1
  5. package/dist/internal/jsonld-cache.d.ts +1 -1
  6. package/dist/internal/portable-dereference.d.cts +2 -2
  7. package/dist/internal/portable-dereference.d.ts +2 -2
  8. package/dist/mod.cjs +205 -16
  9. package/dist/mod.d.cts +3 -3
  10. package/dist/mod.d.ts +3 -3
  11. package/dist/mod.js +204 -17
  12. package/dist/{portable-ilkFPcXa.d.cts → portable-Cux3IA20.d.cts} +1 -1
  13. package/dist/{portable-DRtvOxTb.d.ts → portable-DBA1FS_L.d.ts} +1 -1
  14. package/dist/tests/{body-DHAC8qhR.cjs → body-CmZdqiw3.cjs} +60 -1
  15. package/dist/tests/{body-CNwDKYQs.mjs → body-DMNbCBhz.mjs} +55 -2
  16. package/dist/tests/body.test.cjs +1 -1
  17. package/dist/tests/body.test.mjs +1 -1
  18. package/dist/tests/decimal.test.cjs +1 -1
  19. package/dist/tests/decimal.test.mjs +1 -1
  20. package/dist/tests/{docloader-DWkl3u7X.mjs → docloader-6iodlx7G.mjs} +152 -18
  21. package/dist/tests/{docloader-DLkb9qAS.cjs → docloader-B0omnjRS.cjs} +163 -17
  22. package/dist/tests/docloader.test.cjs +265 -2
  23. package/dist/tests/docloader.test.mjs +266 -3
  24. package/dist/tests/{request-Dm3ye46t.cjs → request-BU0mCgOL.cjs} +1 -1
  25. package/dist/tests/{request-Cg8MQByW.mjs → request-CGmX5n4v.mjs} +1 -1
  26. package/dist/tests/request.test.cjs +1 -1
  27. package/dist/tests/request.test.mjs +1 -1
  28. package/package.json +1 -1
  29. package/src/body.ts +57 -0
  30. package/src/docloader.test.ts +341 -3
  31. package/src/docloader.ts +205 -5
  32. package/src/mod.ts +2 -0
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.4.0-dev.2169+0a45df07",
3
+ "version": "2.4.0-dev.2190+be3a8be0",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
@@ -146,6 +146,28 @@ interface DocumentLoaderFactoryOptions {
146
146
  * @since 2.2.0
147
147
  */
148
148
  maxRedirection?: number;
149
+ /**
150
+ * The timeout in milliseconds for each call of the created document
151
+ * loader. The timeout is shared by all the steps of a call, including
152
+ * URL validation, every HTTP redirect and alternate document link it
153
+ * follows, retries, and reading the response body; it does not restart
154
+ * for each of them. It does not interrupt synchronous work such as
155
+ * parsing a document that has already been received.
156
+ *
157
+ * When a call times out, the loader throws a {@link FetchError} without
158
+ * a {@link FetchError.response}, whose `cause` is a `DOMException` named
159
+ * `"TimeoutError"`. An `AbortSignal` passed through
160
+ * {@link DocumentLoaderOptions.signal} still cancels a call; in that case
161
+ * the loader throws the signal's reason as before.
162
+ *
163
+ * Fractional values are rounded up. Set it to `null` to turn off the
164
+ * timeout.
165
+ * @default `10000` (10 seconds)
166
+ * @throws {RangeError} If the value is not a positive finite number or is
167
+ * greater than 2,147,483,647 (about 24.8 days).
168
+ * @since 2.4.0
169
+ */
170
+ timeout?: number | null;
149
171
  }
150
172
  /**
151
173
  * A factory function that creates an authenticated {@link DocumentLoader} for
@@ -173,6 +195,33 @@ type AuthenticatedDocumentLoaderFactory = (identity: {
173
195
  */
174
196
  declare function getRemoteDocument(url: string, response: Response, fetch: (url: string, options?: DocumentLoaderOptions) => Promise<RemoteDocument>, options?: DocumentLoaderOptions): Promise<RemoteDocument>;
175
197
  /**
198
+ * Resolves {@link DocumentLoaderFactoryOptions.timeout} into milliseconds.
199
+ * @param timeout The timeout option. `undefined` means the default timeout,
200
+ * and `null` means no timeout.
201
+ * @returns The timeout in milliseconds, or `null` if it is turned off.
202
+ * @throws {RangeError} If the timeout is invalid.
203
+ * @internal
204
+ */
205
+ declare function resolveDocumentLoaderTimeout(timeout: number | null | undefined): number | null;
206
+ /**
207
+ * Bounds each call of the given document loader by the given timeout.
208
+ * The timeout is combined with the caller's `signal`, and the combined
209
+ * signal is passed to the loader. The call settles no later than the
210
+ * timeout even if the loader is stuck in a step that cannot be aborted,
211
+ * e.g., a DNS lookup.
212
+ *
213
+ * A timed-out call throws a {@link FetchError} without a response, whose
214
+ * `cause` is a `DOMException` named `"TimeoutError"`. If the caller's
215
+ * signal is aborted, its reason is thrown instead.
216
+ * @param loader The document loader to bound.
217
+ * @param timeout The timeout in milliseconds, or `null` for no timeout.
218
+ * It is assumed to have been resolved by
219
+ * {@link resolveDocumentLoaderTimeout}.
220
+ * @returns The bounded document loader.
221
+ * @internal
222
+ */
223
+ declare function withDocumentLoaderTimeout(loader: DocumentLoader, timeout: number | null): DocumentLoader;
224
+ /**
176
225
  * Options for {@link getDocumentLoader}.
177
226
  * @since 1.3.0
178
227
  */
@@ -186,6 +235,8 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
186
235
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
187
236
  * At most 20 HTTP redirects and alternate document links are followed in total
188
237
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
238
+ * Each call times out after 10 seconds by default; see
239
+ * {@link DocumentLoaderFactoryOptions.timeout}.
189
240
  *
190
241
  * The created loader preloads the below frequently used contexts by default
191
242
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -209,7 +260,8 @@ declare function getDocumentLoader({
209
260
  allowPrivateAddress,
210
261
  maxRedirection,
211
262
  skipPreloadedContexts,
263
+ timeout,
212
264
  userAgent
213
265
  }?: GetDocumentLoaderOptions): DocumentLoader;
214
266
  //#endregion
215
- export { DocumentLoaderOptions as a, getDocumentLoader as c, FetchError as d, GetUserAgentOptions as f, logRequest as h, DocumentLoaderFactoryOptions as i, getRemoteDocument as l, getUserAgent as m, DocumentLoader as n, GetDocumentLoaderOptions as o, createActivityPubRequest as p, DocumentLoaderFactory as r, RemoteDocument as s, AuthenticatedDocumentLoaderFactory as t, CreateRequestOptions as u };
267
+ export { logRequest as _, DocumentLoaderOptions as a, getDocumentLoader as c, withDocumentLoaderTimeout as d, CreateRequestOptions as f, getUserAgent as g, createActivityPubRequest as h, DocumentLoaderFactoryOptions as i, getRemoteDocument as l, GetUserAgentOptions as m, DocumentLoader as n, GetDocumentLoaderOptions as o, FetchError as p, DocumentLoaderFactory as r, RemoteDocument as s, AuthenticatedDocumentLoaderFactory as t, resolveDocumentLoaderTimeout as u };
@@ -146,6 +146,28 @@ interface DocumentLoaderFactoryOptions {
146
146
  * @since 2.2.0
147
147
  */
148
148
  maxRedirection?: number;
149
+ /**
150
+ * The timeout in milliseconds for each call of the created document
151
+ * loader. The timeout is shared by all the steps of a call, including
152
+ * URL validation, every HTTP redirect and alternate document link it
153
+ * follows, retries, and reading the response body; it does not restart
154
+ * for each of them. It does not interrupt synchronous work such as
155
+ * parsing a document that has already been received.
156
+ *
157
+ * When a call times out, the loader throws a {@link FetchError} without
158
+ * a {@link FetchError.response}, whose `cause` is a `DOMException` named
159
+ * `"TimeoutError"`. An `AbortSignal` passed through
160
+ * {@link DocumentLoaderOptions.signal} still cancels a call; in that case
161
+ * the loader throws the signal's reason as before.
162
+ *
163
+ * Fractional values are rounded up. Set it to `null` to turn off the
164
+ * timeout.
165
+ * @default `10000` (10 seconds)
166
+ * @throws {RangeError} If the value is not a positive finite number or is
167
+ * greater than 2,147,483,647 (about 24.8 days).
168
+ * @since 2.4.0
169
+ */
170
+ timeout?: number | null;
149
171
  }
150
172
  /**
151
173
  * A factory function that creates an authenticated {@link DocumentLoader} for
@@ -173,6 +195,33 @@ type AuthenticatedDocumentLoaderFactory = (identity: {
173
195
  */
174
196
  declare function getRemoteDocument(url: string, response: Response, fetch: (url: string, options?: DocumentLoaderOptions) => Promise<RemoteDocument>, options?: DocumentLoaderOptions): Promise<RemoteDocument>;
175
197
  /**
198
+ * Resolves {@link DocumentLoaderFactoryOptions.timeout} into milliseconds.
199
+ * @param timeout The timeout option. `undefined` means the default timeout,
200
+ * and `null` means no timeout.
201
+ * @returns The timeout in milliseconds, or `null` if it is turned off.
202
+ * @throws {RangeError} If the timeout is invalid.
203
+ * @internal
204
+ */
205
+ declare function resolveDocumentLoaderTimeout(timeout: number | null | undefined): number | null;
206
+ /**
207
+ * Bounds each call of the given document loader by the given timeout.
208
+ * The timeout is combined with the caller's `signal`, and the combined
209
+ * signal is passed to the loader. The call settles no later than the
210
+ * timeout even if the loader is stuck in a step that cannot be aborted,
211
+ * e.g., a DNS lookup.
212
+ *
213
+ * A timed-out call throws a {@link FetchError} without a response, whose
214
+ * `cause` is a `DOMException` named `"TimeoutError"`. If the caller's
215
+ * signal is aborted, its reason is thrown instead.
216
+ * @param loader The document loader to bound.
217
+ * @param timeout The timeout in milliseconds, or `null` for no timeout.
218
+ * It is assumed to have been resolved by
219
+ * {@link resolveDocumentLoaderTimeout}.
220
+ * @returns The bounded document loader.
221
+ * @internal
222
+ */
223
+ declare function withDocumentLoaderTimeout(loader: DocumentLoader, timeout: number | null): DocumentLoader;
224
+ /**
176
225
  * Options for {@link getDocumentLoader}.
177
226
  * @since 1.3.0
178
227
  */
@@ -186,6 +235,8 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
186
235
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
187
236
  * At most 20 HTTP redirects and alternate document links are followed in total
188
237
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
238
+ * Each call times out after 10 seconds by default; see
239
+ * {@link DocumentLoaderFactoryOptions.timeout}.
189
240
  *
190
241
  * The created loader preloads the below frequently used contexts by default
191
242
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -209,7 +260,8 @@ declare function getDocumentLoader({
209
260
  allowPrivateAddress,
210
261
  maxRedirection,
211
262
  skipPreloadedContexts,
263
+ timeout,
212
264
  userAgent
213
265
  }?: GetDocumentLoaderOptions): DocumentLoader;
214
266
  //#endregion
215
- export { DocumentLoaderOptions as a, getDocumentLoader as c, FetchError as d, GetUserAgentOptions as f, logRequest as h, DocumentLoaderFactoryOptions as i, getRemoteDocument as l, getUserAgent as m, DocumentLoader as n, GetDocumentLoaderOptions as o, createActivityPubRequest as p, DocumentLoaderFactory as r, RemoteDocument as s, AuthenticatedDocumentLoaderFactory as t, CreateRequestOptions as u };
267
+ export { logRequest as _, DocumentLoaderOptions as a, getDocumentLoader as c, withDocumentLoaderTimeout as d, CreateRequestOptions as f, getUserAgent as g, createActivityPubRequest as h, DocumentLoaderFactoryOptions as i, getRemoteDocument as l, GetUserAgentOptions as m, DocumentLoader as n, GetDocumentLoaderOptions as o, FetchError as p, DocumentLoaderFactory as r, RemoteDocument as s, AuthenticatedDocumentLoaderFactory as t, resolveDocumentLoaderTimeout as u };
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader } from "../docloader-ClQraSWr.cjs";
2
+ import { n as DocumentLoader } from "../docloader-C63enuyr.cjs";
3
3
 
4
4
  //#region src/internal/jsonld-cache.d.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader } from "../docloader-ClQraSWr.js";
2
+ import { n as DocumentLoader } from "../docloader-C63enuyr.js";
3
3
 
4
4
  //#region src/internal/jsonld-cache.d.ts
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader, s as RemoteDocument } from "../docloader-ClQraSWr.cjs";
3
- import { r as PortableObjectVerifier } from "../portable-ilkFPcXa.cjs";
2
+ import { n as DocumentLoader, s as RemoteDocument } from "../docloader-C63enuyr.cjs";
3
+ import { r as PortableObjectVerifier } from "../portable-Cux3IA20.cjs";
4
4
  import { Span, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/internal/portable-dereference.d.ts
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader, s as RemoteDocument } from "../docloader-ClQraSWr.js";
3
- import { r as PortableObjectVerifier } from "../portable-DRtvOxTb.js";
2
+ import { n as DocumentLoader, s as RemoteDocument } from "../docloader-C63enuyr.js";
3
+ import { r as PortableObjectVerifier } from "../portable-DBA1FS_L.js";
4
4
  import { Span, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/internal/portable-dereference.d.ts
package/dist/mod.cjs CHANGED
@@ -16,7 +16,7 @@ let _multiformats_base_x = require("@multiformats/base-x");
16
16
  _multiformats_base_x = require_rolldown_runtime.__toESM(_multiformats_base_x, 1);
17
17
  //#region deno.json
18
18
  var name = "@fedify/vocab-runtime";
19
- var version = "2.4.0-dev.2169+0a45df07";
19
+ var version = "2.4.0-dev.2190+be3a8be0";
20
20
  //#endregion
21
21
  //#region src/request.ts
22
22
  /**
@@ -167,6 +167,59 @@ async function readBoundedText(message, maxBytes, url) {
167
167
  reader?.releaseLock();
168
168
  }
169
169
  }
170
+ /**
171
+ * Reads raw bytes while limiting bytes received from the body stream.
172
+ * Unlike {@link readBoundedText}, the bytes are returned as they were
173
+ * received, without decoding.
174
+ * @param message The response or request to read.
175
+ * @param maxBytes The maximum number of decoded bytes to read.
176
+ * @param url The URL to include in errors and logs.
177
+ * @returns The body bytes.
178
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
179
+ * @internal
180
+ */
181
+ async function readBoundedBytes(message, maxBytes, url) {
182
+ validateBodySizeLimit(maxBytes);
183
+ const reader = message.body?.getReader();
184
+ const tooLarge = () => {
185
+ (0, _logtape_logtape.getLogger)([
186
+ "fedify",
187
+ "runtime",
188
+ "body"
189
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
190
+ url: url.toString(),
191
+ maxBytes
192
+ });
193
+ throw new BodyTooLargeError(url, maxBytes);
194
+ };
195
+ try {
196
+ const length = message.headers.get("Content-Length");
197
+ const encoding = message.headers.get("Content-Encoding");
198
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
199
+ if (reader == null) return /* @__PURE__ */ new Uint8Array(0);
200
+ const chunks = [];
201
+ let size = 0;
202
+ while (true) {
203
+ const { done, value } = await reader.read();
204
+ if (done) break;
205
+ size += value.byteLength;
206
+ if (size > maxBytes) tooLarge();
207
+ chunks.push(value);
208
+ }
209
+ const bytes = new Uint8Array(size);
210
+ let offset = 0;
211
+ for (const chunk of chunks) {
212
+ bytes.set(chunk, offset);
213
+ offset += chunk.byteLength;
214
+ }
215
+ return bytes;
216
+ } catch (error) {
217
+ if (reader != null) reader.cancel(error).catch(() => {});
218
+ throw error;
219
+ } finally {
220
+ reader?.releaseLock();
221
+ }
222
+ }
170
223
  //#endregion
171
224
  //#region src/link.ts
172
225
  const parametersNeedLowerCase = ["rel", "type"];
@@ -357,6 +410,9 @@ const logger = (0, _logtape_logtape.getLogger)([
357
410
  ]);
358
411
  const DEFAULT_MAX_REDIRECTION = 20;
359
412
  const MAX_HTML_SIZE = 1024 * 1024;
413
+ const MAX_ERROR_BODY_SIZE = 1024 * 1024;
414
+ const DEFAULT_TIMEOUT = 1e4;
415
+ const MAX_TIMEOUT = 2147483647;
360
416
  function createResponseMetadata(response) {
361
417
  return new Response(null, {
362
418
  headers: response.headers,
@@ -364,6 +420,55 @@ function createResponseMetadata(response) {
364
420
  statusText: response.statusText
365
421
  });
366
422
  }
423
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
424
+ 204,
425
+ 205,
426
+ 304
427
+ ]);
428
+ /**
429
+ * Reads the body of an error response while the document loader is still
430
+ * running, so that its timeout and `AbortSignal` also bound the read, and
431
+ * nothing reading {@link FetchError.response} later waits on the network.
432
+ * The body is kept byte for byte, unless it is too large or cannot be read;
433
+ * then only the status and headers are kept. A response whose status
434
+ * the `Response` constructor does not accept (e.g., 999) is kept as a clone
435
+ * whose body has been read in full; if its body is too large or cannot be
436
+ * read, no response is kept at all.
437
+ */
438
+ async function bufferErrorResponse(response, url, signal) {
439
+ if (response.status < 200 || response.status > 599) {
440
+ const clone = response.clone();
441
+ try {
442
+ await readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
443
+ } catch (error) {
444
+ await clone.body?.cancel().catch(() => {});
445
+ if (signal?.aborted) throw error;
446
+ logger.debug("Failed to read the error response body from {url}: {error}", {
447
+ url,
448
+ error
449
+ });
450
+ return;
451
+ }
452
+ return clone;
453
+ }
454
+ if (response.body == null || NULL_BODY_STATUSES.has(response.status)) return createResponseMetadata(response);
455
+ let body;
456
+ try {
457
+ body = await readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
458
+ } catch (error) {
459
+ if (signal?.aborted) throw error;
460
+ logger.debug("Failed to read the error response body from {url}: {error}", {
461
+ url,
462
+ error
463
+ });
464
+ return createResponseMetadata(response);
465
+ }
466
+ return new Response(body, {
467
+ headers: response.headers,
468
+ status: response.status,
469
+ statusText: response.statusText
470
+ });
471
+ }
367
472
  /**
368
473
  * Gets a {@link RemoteDocument} from the given response.
369
474
  * @param url The URL of the document to load.
@@ -388,7 +493,7 @@ async function getRemoteDocument(url, response, fetch, options) {
388
493
  url: documentUrl,
389
494
  headers: Object.fromEntries(response.headers.entries())
390
495
  });
391
- throw new FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, response.clone());
496
+ throw new FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, await bufferErrorResponse(response, documentUrl, options?.signal));
392
497
  }
393
498
  const contentType = response.headers.get("Content-Type");
394
499
  const jsonLd = contentType == null || contentType === "application/activity+json" || contentType.startsWith("application/activity+json;") || contentType === "application/ld+json" || contentType.startsWith("application/ld+json;");
@@ -475,9 +580,87 @@ async function getRemoteDocument(url, response, fetch, options) {
475
580
  };
476
581
  }
477
582
  /**
583
+ * Resolves {@link DocumentLoaderFactoryOptions.timeout} into milliseconds.
584
+ * @param timeout The timeout option. `undefined` means the default timeout,
585
+ * and `null` means no timeout.
586
+ * @returns The timeout in milliseconds, or `null` if it is turned off.
587
+ * @throws {RangeError} If the timeout is invalid.
588
+ * @internal
589
+ */
590
+ function resolveDocumentLoaderTimeout(timeout) {
591
+ if (timeout === void 0) return DEFAULT_TIMEOUT;
592
+ if (timeout === null) return null;
593
+ if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new RangeError(`The document loader timeout must be a positive finite number of milliseconds, but got ${String(timeout)}.`);
594
+ const ms = Math.ceil(timeout);
595
+ if (ms > MAX_TIMEOUT) throw new RangeError(`The document loader timeout must not be greater than ${MAX_TIMEOUT} milliseconds, but got ${timeout}.`);
596
+ return ms;
597
+ }
598
+ /**
599
+ * Bounds each call of the given document loader by the given timeout.
600
+ * The timeout is combined with the caller's `signal`, and the combined
601
+ * signal is passed to the loader. The call settles no later than the
602
+ * timeout even if the loader is stuck in a step that cannot be aborted,
603
+ * e.g., a DNS lookup.
604
+ *
605
+ * A timed-out call throws a {@link FetchError} without a response, whose
606
+ * `cause` is a `DOMException` named `"TimeoutError"`. If the caller's
607
+ * signal is aborted, its reason is thrown instead.
608
+ * @param loader The document loader to bound.
609
+ * @param timeout The timeout in milliseconds, or `null` for no timeout.
610
+ * It is assumed to have been resolved by
611
+ * {@link resolveDocumentLoaderTimeout}.
612
+ * @returns The bounded document loader.
613
+ * @internal
614
+ */
615
+ function withDocumentLoaderTimeout(loader, timeout) {
616
+ if (timeout == null) return loader;
617
+ return async (url, options) => {
618
+ const callerSignal = options?.signal;
619
+ callerSignal?.throwIfAborted();
620
+ const controller = new AbortController();
621
+ const timeoutReason = new DOMException(`The document loader timed out after ${timeout} ms.`, "TimeoutError");
622
+ let timedOut = false;
623
+ const timer = setTimeout(() => {
624
+ timedOut = true;
625
+ controller.abort(timeoutReason);
626
+ }, timeout);
627
+ const onCallerAbort = () => controller.abort(callerSignal?.reason);
628
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
629
+ let onAbort;
630
+ const aborted = new Promise((_, reject) => {
631
+ onAbort = () => reject(controller.signal.reason);
632
+ controller.signal.addEventListener("abort", onAbort, { once: true });
633
+ });
634
+ const loading = loader(url, {
635
+ ...options,
636
+ signal: controller.signal
637
+ });
638
+ loading.catch(() => {});
639
+ try {
640
+ return await Promise.race([loading, aborted]);
641
+ } catch (error) {
642
+ if (callerSignal?.aborted) throw callerSignal.reason;
643
+ if (!timedOut) throw error;
644
+ logger[options?.suppressError ? "warn" : "error"]("Timed out after {timeout} ms while fetching document: {url}", {
645
+ timeout,
646
+ url
647
+ });
648
+ const fetchError = new FetchError(url, `Timed out after ${timeout} ms`);
649
+ fetchError.cause = timeoutReason;
650
+ throw fetchError;
651
+ } finally {
652
+ clearTimeout(timer);
653
+ callerSignal?.removeEventListener("abort", onCallerAbort);
654
+ if (onAbort != null) controller.signal.removeEventListener("abort", onAbort);
655
+ }
656
+ };
657
+ }
658
+ /**
478
659
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
479
660
  * At most 20 HTTP redirects and alternate document links are followed in total
480
661
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
662
+ * Each call times out after 10 seconds by default; see
663
+ * {@link DocumentLoaderFactoryOptions.timeout}.
481
664
  *
482
665
  * The created loader preloads the below frequently used contexts by default
483
666
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -497,7 +680,8 @@ async function getRemoteDocument(url, response, fetch, options) {
497
680
  * @returns The document loader.
498
681
  * @since 1.3.0
499
682
  */
500
- function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
683
+ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, timeout, userAgent } = {}) {
684
+ const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
501
685
  const tracer = _opentelemetry_api.trace.getTracerProvider().getTracer(name, version);
502
686
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
503
687
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
@@ -511,18 +695,21 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
511
695
  documentUrl: currentUrl
512
696
  };
513
697
  }
514
- if (!allowPrivateAddress) try {
515
- await require_url.validatePublicUrl(currentUrl);
516
- } catch (error) {
517
- if (error instanceof require_url.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
518
- url: currentUrl,
519
- error
520
- });
521
- else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
522
- url: currentUrl,
523
- error
524
- });
525
- throw error;
698
+ if (!allowPrivateAddress) {
699
+ try {
700
+ await require_url.validatePublicUrl(currentUrl);
701
+ } catch (error) {
702
+ if (error instanceof require_url.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
703
+ url: currentUrl,
704
+ error
705
+ });
706
+ else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
707
+ url: currentUrl,
708
+ error
709
+ });
710
+ throw error;
711
+ }
712
+ options?.signal?.throwIfAborted();
526
713
  }
527
714
  visited.add(currentUrl);
528
715
  return await tracer.startActiveSpan("activitypub.fetch_document", {
@@ -577,7 +764,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
577
764
  }
578
765
  });
579
766
  }
580
- return (url, options) => load(url, options);
767
+ return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
581
768
  }
582
769
  //#endregion
583
770
  //#region src/internal/multicodec.ts
@@ -1436,7 +1623,9 @@ exports.parseHashlink = parseHashlink;
1436
1623
  exports.parseIri = require_url.parseIri;
1437
1624
  exports.parseJsonLdId = require_url.parseJsonLdId;
1438
1625
  exports.preloadedContexts = require_contexts.preloadedContexts;
1626
+ exports.resolveDocumentLoaderTimeout = resolveDocumentLoaderTimeout;
1439
1627
  exports.toCompatibleEf61Id = require_url.toCompatibleEf61Id;
1440
1628
  exports.validatePublicUrl = require_url.validatePublicUrl;
1441
1629
  exports.verifyDigestMultibase = verifyDigestMultibase;
1442
1630
  exports.verifyHashlink = verifyHashlink;
1631
+ exports.withDocumentLoaderTimeout = withDocumentLoaderTimeout;
package/dist/mod.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { a as DocumentLoaderOptions, c as getDocumentLoader, d as FetchError, f as GetUserAgentOptions, h as logRequest, i as DocumentLoaderFactoryOptions, l as getRemoteDocument, m as getUserAgent, n as DocumentLoader, o as GetDocumentLoaderOptions, p as createActivityPubRequest, r as DocumentLoaderFactory, s as RemoteDocument, t as AuthenticatedDocumentLoaderFactory, u as CreateRequestOptions } from "./docloader-ClQraSWr.cjs";
3
- import { i as PortableObjectVerifierOptions, n as PortableObjectVerification, r as PortableObjectVerifier, t as PortableObjectReferrer } from "./portable-ilkFPcXa.cjs";
2
+ import { _ as logRequest, a as DocumentLoaderOptions, c as getDocumentLoader, d as withDocumentLoaderTimeout, f as CreateRequestOptions, g as getUserAgent, h as createActivityPubRequest, i as DocumentLoaderFactoryOptions, l as getRemoteDocument, m as GetUserAgentOptions, n as DocumentLoader, o as GetDocumentLoaderOptions, p as FetchError, r as DocumentLoaderFactory, s as RemoteDocument, t as AuthenticatedDocumentLoaderFactory, u as resolveDocumentLoaderTimeout } from "./docloader-C63enuyr.cjs";
3
+ import { i as PortableObjectVerifierOptions, n as PortableObjectVerification, r as PortableObjectVerifier, t as PortableObjectReferrer } from "./portable-Cux3IA20.cjs";
4
4
  import { TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/contexts.d.ts
@@ -567,4 +567,4 @@ declare function isValidPublicIPv4Address(address: string): boolean;
567
567
  declare function isValidPublicIPv6Address(address: string): boolean;
568
568
  declare function expandIPv6Address(address: string): string;
569
569
  //#endregion
570
- export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PortableObjectReferrer, type PortableObjectVerification, type PortableObjectVerifier, type PortableObjectVerifierOptions, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
570
+ export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PortableObjectReferrer, type PortableObjectVerification, type PortableObjectVerifier, type PortableObjectVerifierOptions, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink, withDocumentLoaderTimeout };
package/dist/mod.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { a as DocumentLoaderOptions, c as getDocumentLoader, d as FetchError, f as GetUserAgentOptions, h as logRequest, i as DocumentLoaderFactoryOptions, l as getRemoteDocument, m as getUserAgent, n as DocumentLoader, o as GetDocumentLoaderOptions, p as createActivityPubRequest, r as DocumentLoaderFactory, s as RemoteDocument, t as AuthenticatedDocumentLoaderFactory, u as CreateRequestOptions } from "./docloader-ClQraSWr.js";
3
- import { i as PortableObjectVerifierOptions, n as PortableObjectVerification, r as PortableObjectVerifier, t as PortableObjectReferrer } from "./portable-DRtvOxTb.js";
2
+ import { _ as logRequest, a as DocumentLoaderOptions, c as getDocumentLoader, d as withDocumentLoaderTimeout, f as CreateRequestOptions, g as getUserAgent, h as createActivityPubRequest, i as DocumentLoaderFactoryOptions, l as getRemoteDocument, m as GetUserAgentOptions, n as DocumentLoader, o as GetDocumentLoaderOptions, p as FetchError, r as DocumentLoaderFactory, s as RemoteDocument, t as AuthenticatedDocumentLoaderFactory, u as resolveDocumentLoaderTimeout } from "./docloader-C63enuyr.js";
3
+ import { i as PortableObjectVerifierOptions, n as PortableObjectVerification, r as PortableObjectVerifier, t as PortableObjectReferrer } from "./portable-DBA1FS_L.js";
4
4
  import { TracerProvider } from "@opentelemetry/api";
5
5
  //#region src/contexts.d.ts
6
6
  declare const preloadedContexts: Record<string, unknown>;
@@ -566,4 +566,4 @@ declare function isValidPublicIPv4Address(address: string): boolean;
566
566
  declare function isValidPublicIPv6Address(address: string): boolean;
567
567
  declare function expandIPv6Address(address: string): string;
568
568
  //#endregion
569
- export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PortableObjectReferrer, type PortableObjectVerification, type PortableObjectVerifier, type PortableObjectVerifierOptions, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
569
+ export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PortableObjectReferrer, type PortableObjectVerification, type PortableObjectVerifier, type PortableObjectVerifierOptions, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink, withDocumentLoaderTimeout };