@fedify/vocab-runtime 2.4.0-dev.2056 → 2.4.0-dev.2065

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 (34) hide show
  1. package/deno.json +1 -1
  2. package/dist/internal/portable-dereference.cjs +355 -8
  3. package/dist/internal/portable-dereference.d.cts +238 -6
  4. package/dist/internal/portable-dereference.d.ts +238 -6
  5. package/dist/internal/portable-dereference.js +343 -10
  6. package/dist/mod.cjs +1 -1
  7. package/dist/mod.d.cts +2 -2
  8. package/dist/mod.d.ts +2 -2
  9. package/dist/mod.js +1 -1
  10. package/dist/portable-DH3947GK.d.ts +160 -0
  11. package/dist/portable-comBI05Y.d.cts +160 -0
  12. package/dist/tests/{body-wkL-XGhe.mjs → body-CNO0jMV8.mjs} +1 -1
  13. package/dist/tests/{body-h9zUR79f.cjs → body-DXHOazEl.cjs} +1 -1
  14. package/dist/tests/body.test.cjs +1 -1
  15. package/dist/tests/body.test.mjs +1 -1
  16. package/dist/tests/decimal.test.cjs +1 -1
  17. package/dist/tests/decimal.test.mjs +1 -1
  18. package/dist/tests/{docloader-CBLgaVPd.cjs → docloader-4Txefi7y.cjs} +2 -2
  19. package/dist/tests/{docloader-DMGssPEX.mjs → docloader-C-nWtUWo.mjs} +2 -2
  20. package/dist/tests/docloader.test.cjs +2 -2
  21. package/dist/tests/docloader.test.mjs +2 -2
  22. package/dist/tests/internal/portable-dereference.test.cjs +87 -0
  23. package/dist/tests/internal/portable-dereference.test.mjs +87 -0
  24. package/dist/tests/{request-ClFs0ePp.cjs → request-BOLanqAd.cjs} +1 -1
  25. package/dist/tests/{request-Ytm0xyZH.mjs → request-dpFdQKXr.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/internal/portable-dereference.test.ts +32 -0
  30. package/src/internal/portable-dereference.ts +521 -12
  31. package/src/mod.ts +2 -0
  32. package/src/portable.ts +136 -8
  33. package/dist/portable-CduElToy.d.ts +0 -52
  34. package/dist/portable-DxF2Ut-Z.d.cts +0 -52
@@ -0,0 +1,160 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { n as DocumentLoader } from "./docloader-Dr8wEkif.cjs";
3
+ import { TracerProvider } from "@opentelemetry/api";
4
+
5
+ //#region src/portable.d.ts
6
+ /**
7
+ * Options passed to a {@link PortableObjectVerifier}.
8
+ * @since 2.4.0
9
+ */
10
+ interface PortableObjectVerifierOptions {
11
+ /**
12
+ * The document loader for fetching remote documents, such as verification
13
+ * methods that are not `did:key` URLs.
14
+ */
15
+ readonly documentLoader?: DocumentLoader;
16
+ /**
17
+ * The document loader for fetching remote JSON-LD contexts. During
18
+ * gateway dereferencing, this loader returns the same context documents
19
+ * that the vocabulary parser sees.
20
+ */
21
+ readonly contextLoader?: DocumentLoader;
22
+ /**
23
+ * The OpenTelemetry tracer provider.
24
+ */
25
+ readonly tracerProvider?: TracerProvider;
26
+ /**
27
+ * The URL the document was finally retrieved from, i.e., the `documentUrl`
28
+ * of the document loader's result, which may differ from the requested URL
29
+ * after redirects. Trust decisions such as the [FEP-ef61] gateway trust
30
+ * policy for unsecured collections depend on it, so custom document loaders
31
+ * must report it truthfully.
32
+ *
33
+ * [FEP-ef61]: https://w3id.org/fep/ef61
34
+ * @since 2.4.0
35
+ */
36
+ readonly documentUrl?: URL;
37
+ /**
38
+ * The gateways that the caller explicitly passed to the accessor through
39
+ * its `gateways` option, in order. It is `undefined` if the caller did not
40
+ * pass the option, and can be empty.
41
+ * @since 2.4.0
42
+ */
43
+ readonly gateways?: readonly URL[];
44
+ /**
45
+ * The gateways inferred from the reference, in order, when the caller did
46
+ * not pass the `gateways` option: the `@gateway` location hints of
47
+ * a portable IRI, the gateway named by an FEP-ef61 compatible identifier,
48
+ * or the WebFinger server of a portable actor. They only tell where the
49
+ * document and related objects might be retrieved, and must not be
50
+ * trusted to act for the portable object's DID.
51
+ * @since 2.4.0
52
+ */
53
+ readonly gatewayHints?: readonly URL[];
54
+ /**
55
+ * The object whose property referred to the document, if the document was
56
+ * dereferenced through a property accessor.
57
+ * @since 2.4.0
58
+ */
59
+ readonly referrer?: PortableObjectReferrer;
60
+ }
61
+ /**
62
+ * An object whose property referred to a portable object being verified.
63
+ * Referrers form a chain: {@link PortableObjectReferrer.referrer} is the
64
+ * object through which this referrer itself was obtained, and so on.
65
+ * @since 2.4.0
66
+ */
67
+ interface PortableObjectReferrer {
68
+ /**
69
+ * The referring vocabulary object.
70
+ */
71
+ readonly object: unknown;
72
+ /**
73
+ * The `@id` of the referring object, if any.
74
+ */
75
+ readonly id: URL | null;
76
+ /**
77
+ * The expanded URI of the property through which the referring object
78
+ * referred to the next object in the chain, e.g.,
79
+ * `https://www.w3.org/ns/activitystreams#outbox`.
80
+ */
81
+ readonly property: string;
82
+ /**
83
+ * How the referring object itself was accepted by a
84
+ * {@link PortableObjectVerifier} when it was dereferenced through gateways:
85
+ *
86
+ * - `"verified"`: accepted as verified, e.g., with a valid integrity proof.
87
+ * - `"unsecured"`: accepted without an integrity proof, e.g., as an
88
+ * unsecured collection served by a trusted gateway.
89
+ *
90
+ * It is `undefined` if the referring object was not dereferenced through
91
+ * gateways, e.g., if it was embedded in another object or constructed by
92
+ * the application.
93
+ */
94
+ readonly acceptance?: "verified" | "unsecured";
95
+ /**
96
+ * The opaque collection context that a {@link PortableObjectVerifier}
97
+ * attached to the referring object when it accepted it. See
98
+ * {@link PortableObjectVerification}.
99
+ */
100
+ readonly collectionContext?: unknown;
101
+ /**
102
+ * The referrer of the referring object, if any.
103
+ */
104
+ readonly referrer?: PortableObjectReferrer;
105
+ /**
106
+ * Whether the chain was cut off here because it was too long. If `true`,
107
+ * the referring object may have a referrer which is not included.
108
+ */
109
+ readonly truncated?: boolean;
110
+ }
111
+ /**
112
+ * The result of a {@link PortableObjectVerifier}.
113
+ * @since 2.4.0
114
+ */
115
+ type PortableObjectVerification = {
116
+ /** Whether the document is accepted. */readonly verified: false;
117
+ } | {
118
+ /** Whether the document is accepted. */readonly verified: true;
119
+ /**
120
+ * Whether the document was accepted without an integrity proof, e.g., as
121
+ * an unsecured [FEP-ef61] collection served by a gateway that its owner
122
+ * lists. Objects embedded in such a document are not trusted because of
123
+ * their origin; property accessors dereference and verify them one by
124
+ * one instead.
125
+ *
126
+ * [FEP-ef61]: https://w3id.org/fep/ef61
127
+ */
128
+ readonly unsecured?: boolean;
129
+ /**
130
+ * An opaque value that the verifier wants to receive again as
131
+ * {@link PortableObjectReferrer.collectionContext} when objects referred
132
+ * to by this document, such as collection pages, are verified.
133
+ */
134
+ readonly collectionContext?: unknown;
135
+ };
136
+ /**
137
+ * A function that applies the [FEP-ef61] trust policy to a portable object
138
+ * fetched through a gateway.
139
+ *
140
+ * It receives the fetched JSON-LD document as is, and resolves to an object
141
+ * whose `verified` property tells whether the document is acceptable, e.g.,
142
+ * whether it carries a valid [FEP-8b32] Object Integrity Proof made by the
143
+ * DID in its portable ID. `verifyPortableObject()` and
144
+ * `verifyPortableObjectProof()` from `@fedify/fedify` satisfy this type, so
145
+ * they can be passed as the `verifyPortableObject` option of generated
146
+ * vocabulary accessors such as `Activity.getObject()`.
147
+ *
148
+ * [FEP-ef61]: https://w3id.org/fep/ef61
149
+ * [FEP-8b32]: https://w3id.org/fep/8b32
150
+ *
151
+ * @param document The fetched JSON-LD document.
152
+ * @param options Loaders, tracing options, and where the document came from.
153
+ * @returns An object whose `verified` property is `true` if and only if the
154
+ * document satisfies the policy. Other properties, such as
155
+ * a failure reason, are logged when verification fails.
156
+ * @since 2.4.0
157
+ */
158
+ type PortableObjectVerifier = (document: unknown, options: PortableObjectVerifierOptions) => Promise<PortableObjectVerification>;
159
+ //#endregion
160
+ export { PortableObjectVerifierOptions as i, PortableObjectVerification as n, PortableObjectVerifier as r, PortableObjectReferrer as t };
@@ -1,4 +1,4 @@
1
- import { t as FetchError } from "./request-Ytm0xyZH.mjs";
1
+ import { t as FetchError } from "./request-dpFdQKXr.mjs";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,4 +1,4 @@
1
- const require_request = require("./request-ClFs0ePp.cjs");
1
+ const require_request = require("./request-BOLanqAd.cjs");
2
2
  let _logtape_logtape = require("@logtape/logtape");
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,4 +1,4 @@
1
- const require_body = require("./body-h9zUR79f.cjs");
1
+ const require_body = require("./body-DXHOazEl.cjs");
2
2
  let node_assert_strict = require("node:assert/strict");
3
3
  let node_test = require("node:test");
4
4
  let node_zlib = require("node:zlib");
@@ -1,4 +1,4 @@
1
- import { r as readBoundedText, t as BodyTooLargeError } from "./body-wkL-XGhe.mjs";
1
+ import { r as readBoundedText, t as BodyTooLargeError } from "./body-CNO0jMV8.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
3
  import { test } from "node:test";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
@@ -1,5 +1,5 @@
1
1
  require("./url-BqlldzT8.cjs");
2
- require("./docloader-CBLgaVPd.cjs");
2
+ require("./docloader-4Txefi7y.cjs");
3
3
  require("./key-8Qg2SbZi.cjs");
4
4
  require("./multibase-B5Mea7Ip.cjs");
5
5
  require("./digest-3FeH2Y-Q.cjs");
@@ -1,5 +1,5 @@
1
1
  import "./url-DgJM9WJn.mjs";
2
- import "./docloader-DMGssPEX.mjs";
2
+ import "./docloader-C-nWtUWo.mjs";
3
3
  import "./key-C7MNhlkO.mjs";
4
4
  import "./multibase-BPnF_L4e.mjs";
5
5
  import "./digest-COC7xDiQ.mjs";
@@ -1,5 +1,5 @@
1
- const require_request = require("./request-ClFs0ePp.cjs");
2
- const require_body = require("./body-h9zUR79f.cjs");
1
+ const require_request = require("./request-BOLanqAd.cjs");
2
+ const require_body = require("./body-DXHOazEl.cjs");
3
3
  const require_contexts = require("./contexts-Ch3v2Cgu.cjs");
4
4
  const require_link = require("./link-DlKm8bEr.cjs");
5
5
  const require_url = require("./url-BqlldzT8.cjs");
@@ -1,5 +1,5 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-Ytm0xyZH.mjs";
2
- import { n as MAX_BODY_SIZE, r as readBoundedText, t as BodyTooLargeError } from "./body-wkL-XGhe.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-dpFdQKXr.mjs";
2
+ import { n as MAX_BODY_SIZE, r as readBoundedText, t as BodyTooLargeError } from "./body-CNO0jMV8.mjs";
3
3
  import { t as preloadedContexts } from "./contexts-BBX_qhBL.mjs";
4
4
  import { t as HttpHeaderLink } from "./link-Cevmc87v.mjs";
5
5
  import { t as UrlError, v as validatePublicUrl } from "./url-DgJM9WJn.mjs";
@@ -1,8 +1,8 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-ClFs0ePp.cjs");
2
+ const require_request = require("./request-BOLanqAd.cjs");
3
3
  const require_contexts = require("./contexts-Ch3v2Cgu.cjs");
4
4
  const require_url = require("./url-BqlldzT8.cjs");
5
- const require_docloader = require("./docloader-CBLgaVPd.cjs");
5
+ const require_docloader = require("./docloader-4Txefi7y.cjs");
6
6
  let node_test = require("node:test");
7
7
  let node_zlib = require("node:zlib");
8
8
  let node_assert = require("node:assert");
@@ -1,7 +1,7 @@
1
- import { t as FetchError } from "./request-Ytm0xyZH.mjs";
1
+ import { t as FetchError } from "./request-dpFdQKXr.mjs";
2
2
  import { n as cid_v1_default, t as preloadedContexts } from "./contexts-BBX_qhBL.mjs";
3
3
  import { t as UrlError } from "./url-DgJM9WJn.mjs";
4
- import { n as getRemoteDocument, t as getDocumentLoader } from "./docloader-DMGssPEX.mjs";
4
+ import { n as getRemoteDocument, t as getDocumentLoader } from "./docloader-C-nWtUWo.mjs";
5
5
  import { test } from "node:test";
6
6
  import { gzipSync } from "node:zlib";
7
7
  import { deepStrictEqual, ok, rejects } from "node:assert";
@@ -1,3 +1,4 @@
1
+ const require_contexts = require("../contexts-Ch3v2Cgu.cjs");
1
2
  const require_url = require("../url-BqlldzT8.cjs");
2
3
  require("../jsonld-CmDhHNTx.cjs");
3
4
  let node_assert_strict = require("node:assert/strict");
@@ -17,6 +18,12 @@ const logger = (0, _logtape_logtape.getLogger)([
17
18
  */
18
19
  const MAX_GATEWAY_HINTS = 5;
19
20
  const LOCATION_HINT_PARAMETER = "@gateway";
21
+ const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
22
+ "https://w3id.org/identity/v1",
23
+ "https://www.w3.org/ns/activitystreams",
24
+ "https://w3id.org/security/v1",
25
+ "https://w3id.org/security/data-integrity/v1"
26
+ ]);
20
27
  /**
21
28
  * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
22
29
  *
@@ -76,6 +83,60 @@ function parseGatewayOrigin(gateway) {
76
83
  if (url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) return null;
77
84
  return url;
78
85
  }
86
+ /**
87
+ * Creates a context loader that returns the same context documents for the
88
+ * whole dereference operation, so that the identity check, the proof
89
+ * verifier, and the parser interpret the fetched document identically even
90
+ * if the underlying loader is nondeterministic. Failed loads are not
91
+ * remembered, so a transient failure does not affect the next gateway.
92
+ *
93
+ * The parsed object keeps the loader for its own later dereferences, so
94
+ * `release()` turns it into a plain pass-through to the underlying loader
95
+ * once the operation is over.
96
+ *
97
+ * @internal Technically exported for generated vocabulary classes, but not
98
+ * part of the public API contract. This is not considered public API for
99
+ * Semantic Versioning decisions.
100
+ */
101
+ function createSnapshotContextLoader(contextLoader) {
102
+ for (let wrapped = snapshotLoaders.get(contextLoader); wrapped?.released; wrapped = snapshotLoaders.get(contextLoader)) contextLoader = wrapped.base;
103
+ const cache = /* @__PURE__ */ new Map();
104
+ let released = false;
105
+ const release = () => {
106
+ released = true;
107
+ state.released = true;
108
+ cache.clear();
109
+ };
110
+ const loader = async (url, options) => {
111
+ if (released) return await contextLoader(url, options);
112
+ const key = URL.canParse(url) ? new URL(url).href : url;
113
+ if (BASELINE_CONTEXT_URLS.has(key)) return {
114
+ contextUrl: null,
115
+ document: structuredClone(require_contexts.preloadedContexts[key]),
116
+ documentUrl: key
117
+ };
118
+ let promise = cache.get(key);
119
+ if (promise == null) {
120
+ const loading = contextLoader(url, options).then((document) => structuredClone(document));
121
+ promise = loading;
122
+ cache.set(key, loading);
123
+ loading.catch(() => {
124
+ if (cache.get(key) === loading) cache.delete(key);
125
+ });
126
+ }
127
+ return structuredClone(await promise);
128
+ };
129
+ const state = {
130
+ base: contextLoader,
131
+ released: false
132
+ };
133
+ snapshotLoaders.set(loader, state);
134
+ return {
135
+ loader,
136
+ release
137
+ };
138
+ }
139
+ const snapshotLoaders = /* @__PURE__ */ new WeakMap();
79
140
  //#endregion
80
141
  //#region src/internal/portable-dereference.test.ts
81
142
  const hrefs = (urls) => urls.map((url) => url.href);
@@ -120,4 +181,30 @@ const hrefs = (urls) => urls.map((url) => url.href);
120
181
  4
121
182
  ].map((i) => `https://g${i}.example/`));
122
183
  });
184
+ (0, node_test.test)("createSnapshotContextLoader() does not nest released snapshots", async () => {
185
+ const calls = [];
186
+ const base = (url) => {
187
+ calls.push(url);
188
+ return Promise.resolve({
189
+ contextUrl: null,
190
+ documentUrl: url,
191
+ document: { "@context": {} }
192
+ });
193
+ };
194
+ let loader = base;
195
+ for (let i = 0; i < 1e4; i++) {
196
+ const snapshot = createSnapshotContextLoader(loader);
197
+ snapshot.release();
198
+ loader = snapshot.loader;
199
+ }
200
+ await loader("https://example.com/context");
201
+ (0, node_assert_strict.deepStrictEqual)(calls, ["https://example.com/context"]);
202
+ const outer = createSnapshotContextLoader(base);
203
+ await outer.loader("https://example.com/context");
204
+ const inner = createSnapshotContextLoader(outer.loader);
205
+ await inner.loader("https://example.com/context");
206
+ (0, node_assert_strict.deepStrictEqual)(calls.length, 2);
207
+ inner.release();
208
+ outer.release();
209
+ });
123
210
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { t as preloadedContexts } from "../contexts-BBX_qhBL.mjs";
1
2
  import { a as formatIri, m as parseIri } from "../url-DgJM9WJn.mjs";
2
3
  import "../jsonld-DweIJ07b.mjs";
3
4
  import { deepStrictEqual, ok, throws } from "node:assert/strict";
@@ -17,6 +18,12 @@ const logger = getLogger([
17
18
  */
18
19
  const MAX_GATEWAY_HINTS = 5;
19
20
  const LOCATION_HINT_PARAMETER = "@gateway";
21
+ const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
22
+ "https://w3id.org/identity/v1",
23
+ "https://www.w3.org/ns/activitystreams",
24
+ "https://w3id.org/security/v1",
25
+ "https://w3id.org/security/data-integrity/v1"
26
+ ]);
20
27
  /**
21
28
  * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
22
29
  *
@@ -76,6 +83,60 @@ function parseGatewayOrigin(gateway) {
76
83
  if (url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) return null;
77
84
  return url;
78
85
  }
86
+ /**
87
+ * Creates a context loader that returns the same context documents for the
88
+ * whole dereference operation, so that the identity check, the proof
89
+ * verifier, and the parser interpret the fetched document identically even
90
+ * if the underlying loader is nondeterministic. Failed loads are not
91
+ * remembered, so a transient failure does not affect the next gateway.
92
+ *
93
+ * The parsed object keeps the loader for its own later dereferences, so
94
+ * `release()` turns it into a plain pass-through to the underlying loader
95
+ * once the operation is over.
96
+ *
97
+ * @internal Technically exported for generated vocabulary classes, but not
98
+ * part of the public API contract. This is not considered public API for
99
+ * Semantic Versioning decisions.
100
+ */
101
+ function createSnapshotContextLoader(contextLoader) {
102
+ for (let wrapped = snapshotLoaders.get(contextLoader); wrapped?.released; wrapped = snapshotLoaders.get(contextLoader)) contextLoader = wrapped.base;
103
+ const cache = /* @__PURE__ */ new Map();
104
+ let released = false;
105
+ const release = () => {
106
+ released = true;
107
+ state.released = true;
108
+ cache.clear();
109
+ };
110
+ const loader = async (url, options) => {
111
+ if (released) return await contextLoader(url, options);
112
+ const key = URL.canParse(url) ? new URL(url).href : url;
113
+ if (BASELINE_CONTEXT_URLS.has(key)) return {
114
+ contextUrl: null,
115
+ document: structuredClone(preloadedContexts[key]),
116
+ documentUrl: key
117
+ };
118
+ let promise = cache.get(key);
119
+ if (promise == null) {
120
+ const loading = contextLoader(url, options).then((document) => structuredClone(document));
121
+ promise = loading;
122
+ cache.set(key, loading);
123
+ loading.catch(() => {
124
+ if (cache.get(key) === loading) cache.delete(key);
125
+ });
126
+ }
127
+ return structuredClone(await promise);
128
+ };
129
+ const state = {
130
+ base: contextLoader,
131
+ released: false
132
+ };
133
+ snapshotLoaders.set(loader, state);
134
+ return {
135
+ loader,
136
+ release
137
+ };
138
+ }
139
+ const snapshotLoaders = /* @__PURE__ */ new WeakMap();
79
140
  //#endregion
80
141
  //#region src/internal/portable-dereference.test.ts
81
142
  const hrefs = (urls) => urls.map((url) => url.href);
@@ -120,5 +181,31 @@ test("getPortableGatewayCandidates() reads @gateway hints", () => {
120
181
  4
121
182
  ].map((i) => `https://g${i}.example/`));
122
183
  });
184
+ test("createSnapshotContextLoader() does not nest released snapshots", async () => {
185
+ const calls = [];
186
+ const base = (url) => {
187
+ calls.push(url);
188
+ return Promise.resolve({
189
+ contextUrl: null,
190
+ documentUrl: url,
191
+ document: { "@context": {} }
192
+ });
193
+ };
194
+ let loader = base;
195
+ for (let i = 0; i < 1e4; i++) {
196
+ const snapshot = createSnapshotContextLoader(loader);
197
+ snapshot.release();
198
+ loader = snapshot.loader;
199
+ }
200
+ await loader("https://example.com/context");
201
+ deepStrictEqual(calls, ["https://example.com/context"]);
202
+ const outer = createSnapshotContextLoader(base);
203
+ await outer.loader("https://example.com/context");
204
+ const inner = createSnapshotContextLoader(outer.loader);
205
+ await inner.loader("https://example.com/context");
206
+ deepStrictEqual(calls.length, 2);
207
+ inner.release();
208
+ outer.release();
209
+ });
123
210
  //#endregion
124
211
  export {};
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.4.0-dev.2056+e6394f3b";
6
+ var version = "2.4.0-dev.2065+b09fdd33";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.4.0-dev.2056+e6394f3b";
4
+ var version = "2.4.0-dev.2065+b09fdd33";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-ClFs0ePp.cjs");
2
+ const require_request = require("./request-BOLanqAd.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_process = require("node:process");
5
5
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
@@ -1,4 +1,4 @@
1
- import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-Ytm0xyZH.mjs";
1
+ import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-dpFdQKXr.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.4.0-dev.2056+e6394f3b",
3
+ "version": "2.4.0-dev.2065+b09fdd33",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,7 +1,9 @@
1
1
  import { deepStrictEqual, ok, throws } from "node:assert/strict";
2
2
  import { test } from "node:test";
3
+ import type { DocumentLoader } from "../docloader.ts";
3
4
  import { parseIri } from "../url.ts";
4
5
  import {
6
+ createSnapshotContextLoader,
5
7
  getPortableGatewayCandidates,
6
8
  isPortableIri,
7
9
  } from "./portable-dereference.ts";
@@ -72,3 +74,33 @@ test("getPortableGatewayCandidates() reads @gateway hints", () => {
72
74
  [0, 1, 2, 3, 4].map((i) => `https://g${i}.example/`),
73
75
  );
74
76
  });
77
+
78
+ test("createSnapshotContextLoader() does not nest released snapshots", async () => {
79
+ const calls: string[] = [];
80
+ const base: DocumentLoader = (url) => {
81
+ calls.push(url);
82
+ return Promise.resolve({
83
+ contextUrl: null,
84
+ documentUrl: url,
85
+ document: { "@context": {} },
86
+ });
87
+ };
88
+ let loader = base;
89
+ for (let i = 0; i < 10000; i++) {
90
+ const snapshot = createSnapshotContextLoader(loader);
91
+ snapshot.release();
92
+ loader = snapshot.loader;
93
+ }
94
+ // A deep chain of wrappers would overflow the stack here:
95
+ await loader("https://example.com/context");
96
+ deepStrictEqual(calls, ["https://example.com/context"]);
97
+
98
+ // Snapshots still in use are kept, so a nested snapshot sees their cache:
99
+ const outer = createSnapshotContextLoader(base);
100
+ await outer.loader("https://example.com/context");
101
+ const inner = createSnapshotContextLoader(outer.loader);
102
+ await inner.loader("https://example.com/context");
103
+ deepStrictEqual(calls.length, 2);
104
+ inner.release();
105
+ outer.release();
106
+ });