@fedify/backfill 2.3.0-dev.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.
@@ -0,0 +1,1563 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_assert_strict = require("node:assert/strict");
24
+ let node_test = require("node:test");
25
+ node_test = __toESM(node_test, 1);
26
+ let _fedify_vocab = require("@fedify/vocab");
27
+ //#region src/backfill.ts
28
+ const defaultStrategies = ["context-auto"];
29
+ const DEFAULT_MAX_DEPTH = 10;
30
+ /**
31
+ * Thrown when backfill traversal exceeds the configured request budget.
32
+ *
33
+ * @since 2.3.0
34
+ */
35
+ var MaxRequestsExceeded = class extends Error {};
36
+ /**
37
+ * Backfills post-like objects related to a seed object.
38
+ *
39
+ * The seed object is not yielded by default, but its ID is treated as already
40
+ * seen so it will not be yielded again if the collection contains it.
41
+ *
42
+ * @since 2.3.0
43
+ */
44
+ async function* backfill(context, note, options = {}) {
45
+ if (options.maxItems != null && options.maxItems <= 0) return;
46
+ const strategies = normalizeStrategies(options.strategies);
47
+ if (strategies.length < 1) return;
48
+ const budget = {
49
+ signal: options.signal,
50
+ requestCount: 0,
51
+ documents: /* @__PURE__ */ new Map()
52
+ };
53
+ const seenIds = /* @__PURE__ */ new Set();
54
+ if (note.id != null) seenIds.add(note.id.href);
55
+ let yielded = 0;
56
+ try {
57
+ for (let i = 0; i < strategies.length; i++) {
58
+ const strategy = strategies[i];
59
+ let items;
60
+ if (isContextStrategy(strategy)) {
61
+ const contextStrategies = [strategy];
62
+ while (true) {
63
+ const nextStrategy = strategies[i + 1];
64
+ if (nextStrategy == null || !isContextStrategy(nextStrategy)) break;
65
+ contextStrategies.push(nextStrategy);
66
+ i++;
67
+ }
68
+ items = getContextStrategyItems(context, note, contextStrategies, options, budget, seenIds);
69
+ } else items = getStrategyItems(context, note, strategy, options, budget, seenIds);
70
+ for await (const item of items) {
71
+ const id = item.object.id ?? void 0;
72
+ if (id != null) {
73
+ if (seenIds.has(id.href)) continue;
74
+ seenIds.add(id.href);
75
+ }
76
+ options.signal?.throwIfAborted();
77
+ yield {
78
+ object: item.object,
79
+ id,
80
+ strategy: item.strategy,
81
+ origin: item.origin,
82
+ depth: item.depth
83
+ };
84
+ yielded++;
85
+ if (options.maxItems != null && yielded >= options.maxItems) return;
86
+ }
87
+ }
88
+ } catch (error) {
89
+ if (error instanceof MaxRequestsExceeded) return;
90
+ throw error;
91
+ }
92
+ }
93
+ function normalizeStrategies(strategies = defaultStrategies) {
94
+ const normalized = [];
95
+ for (const strategy of strategies) if (strategy === "context-auto") {
96
+ for (let i = normalized.length - 1; i >= 0 && isContextStrategy(normalized[i]); i--) normalized.splice(i, 1);
97
+ if (!normalized.includes(strategy)) normalized.push(strategy);
98
+ } else if (isContextStrategy(strategy)) {
99
+ if (!currentContextGroupHasAuto(normalized) && !normalized.includes(strategy)) normalized.push(strategy);
100
+ } else if (!normalized.includes(strategy)) normalized.push(strategy);
101
+ return normalized;
102
+ }
103
+ function isContextStrategy(strategy) {
104
+ return strategy === "context-objects" || strategy === "context-activities" || strategy === "context-auto";
105
+ }
106
+ function currentContextGroupHasAuto(strategies) {
107
+ for (let i = strategies.length - 1; i >= 0; i--) {
108
+ const strategy = strategies[i];
109
+ if (!isContextStrategy(strategy)) return false;
110
+ if (strategy === "context-auto") return true;
111
+ }
112
+ return false;
113
+ }
114
+ async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
115
+ const contextId = note.contextIds[0];
116
+ if (contextId == null) return;
117
+ const collection = await loadObject(context, contextId, options, budget);
118
+ if (!isCollection(collection)) return;
119
+ for await (const object of getCollectionItems(context, collection, options, budget, seenIds)) for (const strategy of strategies) for await (const item of getContextBackfillItems(context, object, strategy, options, budget)) yield {
120
+ object: item.object,
121
+ strategy: item.strategy,
122
+ origin: "collection",
123
+ depth: 0
124
+ };
125
+ }
126
+ async function* getStrategyItems(context, note, strategy, options, budget, seenIds) {
127
+ if (isContextStrategy(strategy)) yield* getContextStrategyItems(context, note, [strategy], options, budget, seenIds);
128
+ else if (strategy === "reply-tree") yield* getReplyTreeItems(context, note, options, budget);
129
+ }
130
+ async function* getReplyTreeItems(context, note, options, budget) {
131
+ const visitedObjectIds = /* @__PURE__ */ new Set();
132
+ const visitedObjects = /* @__PURE__ */ new WeakSet();
133
+ const visitedCollectionIds = /* @__PURE__ */ new Set();
134
+ const visitedCollections = /* @__PURE__ */ new WeakSet();
135
+ if (note.id != null) visitedObjectIds.add(note.id.href);
136
+ visitedObjects.add(note);
137
+ const ancestors = [];
138
+ for await (const item of getReplyAncestors(context, note, options, budget, {
139
+ depth: 1,
140
+ visitedObjectIds,
141
+ visitedObjects,
142
+ visitedCollectionIds,
143
+ visitedCollections
144
+ })) {
145
+ ancestors.push({
146
+ object: item.object,
147
+ depth: item.depth
148
+ });
149
+ yield item;
150
+ }
151
+ for (const ancestor of ancestors.toReversed()) yield* getReplyDescendants(context, ancestor.object, options, budget, {
152
+ depth: ancestor.depth + 1,
153
+ visitedObjectIds,
154
+ visitedObjects,
155
+ visitedCollectionIds,
156
+ visitedCollections
157
+ });
158
+ yield* getReplyDescendants(context, note, options, budget, {
159
+ depth: 1,
160
+ visitedObjectIds,
161
+ visitedObjects,
162
+ visitedCollectionIds,
163
+ visitedCollections
164
+ });
165
+ }
166
+ async function* getReplyAncestors(context, object, options, budget, traversal) {
167
+ if (traversal.depth > (options.maxDepth ?? DEFAULT_MAX_DEPTH)) return;
168
+ for await (const target of getReplyTargets(context, object, options, budget)) {
169
+ if (!isContextPostObject(target)) continue;
170
+ if (!visitReplyTreeObject(target, traversal)) continue;
171
+ yield {
172
+ object: target,
173
+ strategy: "reply-tree",
174
+ origin: "in-reply-to",
175
+ depth: traversal.depth
176
+ };
177
+ yield* getReplyAncestors(context, target, options, budget, {
178
+ depth: traversal.depth + 1,
179
+ visitedObjectIds: traversal.visitedObjectIds,
180
+ visitedObjects: traversal.visitedObjects,
181
+ visitedCollectionIds: traversal.visitedCollectionIds,
182
+ visitedCollections: traversal.visitedCollections
183
+ });
184
+ }
185
+ }
186
+ async function* getReplyDescendants(context, object, options, budget, traversal) {
187
+ if (traversal.depth > (options.maxDepth ?? DEFAULT_MAX_DEPTH)) return;
188
+ const repliesId = object.repliesId;
189
+ if (repliesId != null && traversal.visitedCollectionIds.has(repliesId.href)) return;
190
+ const replies = await getRepliesCollection(context, object, options, budget);
191
+ if (replies == null) return;
192
+ const unvisited = visitReplyTreeCollection(replies, traversal);
193
+ if (repliesId != null) traversal.visitedCollectionIds.add(repliesId.href);
194
+ if (!unvisited) return;
195
+ for await (const reply of getCollectionItems(context, replies, options, budget, traversal.visitedObjectIds)) {
196
+ if (!isContextPostObject(reply)) continue;
197
+ if (!visitReplyTreeObject(reply, traversal)) continue;
198
+ yield {
199
+ object: reply,
200
+ strategy: "reply-tree",
201
+ origin: "replies",
202
+ depth: traversal.depth
203
+ };
204
+ yield* getReplyDescendants(context, reply, options, budget, {
205
+ depth: traversal.depth + 1,
206
+ visitedObjectIds: traversal.visitedObjectIds,
207
+ visitedObjects: traversal.visitedObjects,
208
+ visitedCollectionIds: traversal.visitedCollectionIds,
209
+ visitedCollections: traversal.visitedCollections
210
+ });
211
+ }
212
+ }
213
+ async function* getReplyTargets(context, object, options, budget) {
214
+ try {
215
+ yield* object.getReplyTargets({
216
+ documentLoader: async (url) => {
217
+ return await loadCollectionItemDocument(context, url, options, budget);
218
+ },
219
+ crossOrigin: "trust"
220
+ });
221
+ } catch (error) {
222
+ if (error instanceof MaxRequestsExceeded) throw error;
223
+ budget.signal?.throwIfAborted();
224
+ }
225
+ }
226
+ async function getRepliesCollection(context, object, options, budget) {
227
+ try {
228
+ return await object.getReplies({
229
+ documentLoader: async (url) => {
230
+ return await loadCollectionItemDocument(context, url, options, budget);
231
+ },
232
+ crossOrigin: "trust"
233
+ });
234
+ } catch (error) {
235
+ if (error instanceof MaxRequestsExceeded) throw error;
236
+ budget.signal?.throwIfAborted();
237
+ return null;
238
+ }
239
+ }
240
+ function visitReplyTreeObject(object, traversal) {
241
+ if (object.id != null) {
242
+ if (traversal.visitedObjectIds.has(object.id.href)) return false;
243
+ traversal.visitedObjectIds.add(object.id.href);
244
+ } else if (traversal.visitedObjects.has(object)) return false;
245
+ traversal.visitedObjects.add(object);
246
+ return true;
247
+ }
248
+ function visitReplyTreeCollection(collection, traversal) {
249
+ if (collection.id != null) return visitReplyTreeCollectionId(collection.id, traversal);
250
+ else if (traversal.visitedCollections.has(collection)) return false;
251
+ traversal.visitedCollections.add(collection);
252
+ return true;
253
+ }
254
+ function visitReplyTreeCollectionId(id, traversal) {
255
+ if (traversal.visitedCollectionIds.has(id.href)) return false;
256
+ traversal.visitedCollectionIds.add(id.href);
257
+ return true;
258
+ }
259
+ async function* getContextBackfillItems(context, object, strategy, options, budget) {
260
+ if (strategy === "context-objects" && isContextPostObject(object)) yield {
261
+ object,
262
+ strategy
263
+ };
264
+ else if (strategy === "context-activities") {
265
+ const activityObject = await getCreateActivityObject(context, object, options, budget);
266
+ if (activityObject != null && isContextPostObject(activityObject)) yield {
267
+ object: activityObject,
268
+ strategy
269
+ };
270
+ } else if (strategy === "context-auto") {
271
+ if (object instanceof _fedify_vocab.Activity) {
272
+ const activityObject = await getCreateActivityObject(context, object, options, budget);
273
+ if (activityObject != null && isContextPostObject(activityObject)) yield {
274
+ object: activityObject,
275
+ strategy
276
+ };
277
+ } else if (isContextPostObject(object)) yield {
278
+ object,
279
+ strategy
280
+ };
281
+ }
282
+ }
283
+ async function* getCollectionItems(context, collection, options, budget, skipIds) {
284
+ yield* collection.getItems({
285
+ documentLoader: async (url) => {
286
+ return await loadCollectionItemDocument(context, url, options, budget, skipIds);
287
+ },
288
+ crossOrigin: "trust"
289
+ });
290
+ }
291
+ async function getCreateActivityObject(context, object, options, budget) {
292
+ if (!(object instanceof _fedify_vocab.Create)) return null;
293
+ try {
294
+ return await object.getObject({
295
+ documentLoader: async (url) => {
296
+ return await loadCollectionItemDocument(context, url, options, budget);
297
+ },
298
+ crossOrigin: "trust"
299
+ });
300
+ } catch (error) {
301
+ if (error instanceof MaxRequestsExceeded) throw error;
302
+ budget.signal?.throwIfAborted();
303
+ return null;
304
+ }
305
+ }
306
+ async function loadCollectionItemDocument(context, url, options, budget, skipIds) {
307
+ let object;
308
+ try {
309
+ const iri = new URL(url);
310
+ if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
311
+ object = await loadObject(context, iri, options, budget, true);
312
+ } catch (error) {
313
+ if (error instanceof MaxRequestsExceeded) throw error;
314
+ budget.signal?.throwIfAborted();
315
+ return skippedCollectionItemDocument(url);
316
+ }
317
+ if (object == null) return skippedCollectionItemDocument(url);
318
+ return {
319
+ contextUrl: null,
320
+ documentUrl: url,
321
+ document: await object.toJsonLd()
322
+ };
323
+ }
324
+ function skippedCollectionItemDocument(url) {
325
+ return {
326
+ contextUrl: null,
327
+ documentUrl: url,
328
+ document: {
329
+ "@context": "https://www.w3.org/ns/activitystreams",
330
+ type: "Activity"
331
+ }
332
+ };
333
+ }
334
+ async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
335
+ budget.signal?.throwIfAborted();
336
+ const cacheKey = iri.href;
337
+ const cached = budget.documents.get(cacheKey);
338
+ if (cached != null) return await cached;
339
+ if (options.maxRequests != null && budget.requestCount >= options.maxRequests) {
340
+ if (throwOnBudgetExceeded) throw new MaxRequestsExceeded();
341
+ return null;
342
+ }
343
+ await waitForInterval(options, budget);
344
+ budget.signal?.throwIfAborted();
345
+ budget.requestCount++;
346
+ const document = context.documentLoader(iri, { signal: budget.signal });
347
+ budget.documents.set(cacheKey, document);
348
+ try {
349
+ return await document;
350
+ } catch (error) {
351
+ if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
352
+ throw error;
353
+ }
354
+ }
355
+ async function waitForInterval(options, budget) {
356
+ if (options.interval == null) return;
357
+ const milliseconds = durationToMilliseconds(typeof options.interval === "function" ? options.interval(budget.requestCount) : options.interval);
358
+ if (milliseconds <= 0) return;
359
+ await new Promise((resolve, reject) => {
360
+ if (budget.signal?.aborted) {
361
+ reject(budget.signal.reason);
362
+ return;
363
+ }
364
+ const timeout = setTimeout(() => {
365
+ budget.signal?.removeEventListener("abort", onAbort);
366
+ resolve();
367
+ }, milliseconds);
368
+ const onAbort = () => {
369
+ clearTimeout(timeout);
370
+ reject(budget.signal?.reason);
371
+ };
372
+ budget.signal?.addEventListener("abort", onAbort, { once: true });
373
+ });
374
+ }
375
+ function durationToMilliseconds(duration) {
376
+ if (typeof duration === "string") {
377
+ if (typeof Temporal === "undefined") throw new TypeError("Temporal is not globally available; pass interval as a Temporal.DurationLike object instead of a string, or provide a Temporal polyfill.");
378
+ return Temporal.Duration.from(duration).total({ unit: "milliseconds" });
379
+ }
380
+ return (duration.milliseconds ?? 0) + (duration.seconds ?? 0) * 1e3 + (duration.minutes ?? 0) * 60 * 1e3 + (duration.hours ?? 0) * 60 * 60 * 1e3 + (duration.days ?? 0) * 24 * 60 * 60 * 1e3;
381
+ }
382
+ function isCollection(object) {
383
+ return object instanceof _fedify_vocab.Collection || object instanceof _fedify_vocab.OrderedCollection || object instanceof _fedify_vocab.CollectionPage || object instanceof _fedify_vocab.OrderedCollectionPage;
384
+ }
385
+ function isContextPostObject(object) {
386
+ return object instanceof _fedify_vocab.Object && !(object instanceof _fedify_vocab.Activity) && !isCollection(object);
387
+ }
388
+ //#endregion
389
+ //#region src/backfill.test.ts
390
+ async function collect(context, note, options = {}) {
391
+ return await Array.fromAsync(backfill(context, note, options));
392
+ }
393
+ (0, node_test.describe)("backfill", () => {
394
+ (0, node_test.default)("package exports backfill", () => {
395
+ (0, node_assert_strict.strictEqual)(typeof backfill, "function");
396
+ (0, node_assert_strict.strictEqual)(typeof MaxRequestsExceeded, "function");
397
+ });
398
+ (0, node_test.default)("context missing yields nothing", async () => {
399
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => {
400
+ throw new Error("documentLoader should not be called");
401
+ } }, new _fedify_vocab.Note({ id: new URL("https://example.com/notes/1") })), []);
402
+ });
403
+ (0, node_test.default)("context resolves to non-collection yields nothing", async () => {
404
+ const contextId = new URL("https://example.com/contexts/1");
405
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") })) }, new _fedify_vocab.Note({
406
+ id: new URL("https://example.com/notes/1"),
407
+ contexts: [contextId]
408
+ })), []);
409
+ });
410
+ (0, node_test.default)("context collection with embedded objects yields items", async () => {
411
+ const contextId = new URL("https://example.com/contexts/1");
412
+ const item = new _fedify_vocab.Note({
413
+ id: new URL("https://example.com/notes/2"),
414
+ content: "hello"
415
+ });
416
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
417
+ id: contextId,
418
+ items: [item]
419
+ })) }, new _fedify_vocab.Note({
420
+ id: new URL("https://example.com/notes/1"),
421
+ contexts: [contextId]
422
+ }));
423
+ (0, node_assert_strict.strictEqual)(items.length, 1);
424
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
425
+ (0, node_assert_strict.deepStrictEqual)(items[0].id, item.id);
426
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-auto");
427
+ (0, node_assert_strict.strictEqual)(items[0].origin, "collection");
428
+ });
429
+ (0, node_test.default)("context object strategy yields embedded objects", async () => {
430
+ const contextId = new URL("https://example.com/contexts/1");
431
+ const item = new _fedify_vocab.Note({
432
+ id: new URL("https://example.com/notes/2"),
433
+ content: "hello"
434
+ });
435
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
436
+ id: contextId,
437
+ items: [item]
438
+ })) }, new _fedify_vocab.Note({
439
+ id: new URL("https://example.com/notes/1"),
440
+ contexts: [contextId]
441
+ }), { strategies: ["context-objects"] });
442
+ (0, node_assert_strict.strictEqual)(items.length, 1);
443
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
444
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-objects");
445
+ });
446
+ (0, node_test.default)("embedded object without id is yielded without id", async () => {
447
+ const contextId = new URL("https://example.com/contexts/1");
448
+ const item = new _fedify_vocab.Note({ content: "anonymous" });
449
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
450
+ id: contextId,
451
+ items: [item]
452
+ })) }, new _fedify_vocab.Note({
453
+ id: new URL("https://example.com/notes/1"),
454
+ contexts: [contextId]
455
+ }));
456
+ (0, node_assert_strict.strictEqual)(items.length, 1);
457
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
458
+ (0, node_assert_strict.strictEqual)(items[0].id, void 0);
459
+ });
460
+ (0, node_test.default)("context object strategy skips activity objects", async () => {
461
+ const contextId = new URL("https://example.com/contexts/1");
462
+ const activity = new _fedify_vocab.Create({
463
+ id: new URL("https://example.com/activities/1"),
464
+ object: new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") })
465
+ });
466
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
467
+ id: contextId,
468
+ items: [activity]
469
+ })) }, new _fedify_vocab.Note({
470
+ id: new URL("https://example.com/notes/1"),
471
+ contexts: [contextId]
472
+ }), { strategies: ["context-objects"] }), []);
473
+ });
474
+ (0, node_test.default)("context auto strategy yields object from embedded Create", async () => {
475
+ const contextId = new URL("https://example.com/contexts/1");
476
+ const item = new _fedify_vocab.Note({
477
+ id: new URL("https://example.com/notes/2"),
478
+ content: "hello"
479
+ });
480
+ const activity = new _fedify_vocab.Create({
481
+ id: new URL("https://example.com/activities/1"),
482
+ object: item
483
+ });
484
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
485
+ id: contextId,
486
+ items: [activity]
487
+ })) }, new _fedify_vocab.Note({
488
+ id: new URL("https://example.com/notes/1"),
489
+ contexts: [contextId]
490
+ }));
491
+ (0, node_assert_strict.strictEqual)(items.length, 1);
492
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
493
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-auto");
494
+ });
495
+ (0, node_test.default)("empty strategies yield nothing without dereferencing context", async () => {
496
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => {
497
+ throw new Error("documentLoader should not be called");
498
+ } }, new _fedify_vocab.Note({
499
+ id: new URL("https://example.com/notes/1"),
500
+ contexts: [new URL("https://example.com/contexts/1")]
501
+ }), { strategies: [] }), []);
502
+ });
503
+ (0, node_test.default)("reply tree strategy does not require context collection", async () => {
504
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => {
505
+ throw new Error("documentLoader should not be called");
506
+ } }, new _fedify_vocab.Note({
507
+ id: new URL("https://example.com/notes/1"),
508
+ contexts: [new URL("https://example.com/contexts/1")]
509
+ }), { strategies: ["reply-tree"] }), []);
510
+ });
511
+ (0, node_test.default)("reply tree yields embedded ancestor", async () => {
512
+ const parent = new _fedify_vocab.Note({
513
+ id: new URL("https://example.com/notes/1"),
514
+ content: "parent"
515
+ });
516
+ const items = await collect({ documentLoader: () => {
517
+ throw new Error("documentLoader should not be called");
518
+ } }, new _fedify_vocab.Note({
519
+ id: new URL("https://example.com/notes/2"),
520
+ replyTarget: parent
521
+ }), { strategies: ["reply-tree"] });
522
+ (0, node_assert_strict.strictEqual)(items.length, 1);
523
+ (0, node_assert_strict.strictEqual)(items[0].object, parent);
524
+ (0, node_assert_strict.deepStrictEqual)(items[0].id, parent.id);
525
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "reply-tree");
526
+ (0, node_assert_strict.strictEqual)(items[0].origin, "in-reply-to");
527
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
528
+ });
529
+ (0, node_test.default)("reply tree dereferences ancestor URL", async () => {
530
+ const parentId = new URL("https://example.com/notes/1");
531
+ const parent = new _fedify_vocab.Note({
532
+ id: parentId,
533
+ content: "parent"
534
+ });
535
+ const items = await collect({ documentLoader: (iri) => Promise.resolve(iri.href === parentId.href ? parent : null) }, new _fedify_vocab.Note({
536
+ id: new URL("https://example.com/notes/2"),
537
+ replyTarget: parentId
538
+ }), { strategies: ["reply-tree"] });
539
+ (0, node_assert_strict.strictEqual)(items.length, 1);
540
+ (0, node_assert_strict.deepStrictEqual)(items[0].object.id, parent.id);
541
+ (0, node_assert_strict.strictEqual)(items[0].origin, "in-reply-to");
542
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
543
+ });
544
+ (0, node_test.default)("reply tree maxDepth limits ancestors", async () => {
545
+ const rootId = new URL("https://example.com/notes/1");
546
+ const parentId = new URL("https://example.com/notes/2");
547
+ const root = new _fedify_vocab.Note({
548
+ id: rootId,
549
+ content: "root"
550
+ });
551
+ const parent = new _fedify_vocab.Note({
552
+ id: parentId,
553
+ content: "parent",
554
+ replyTarget: rootId
555
+ });
556
+ const items = await collect({ documentLoader: (iri) => {
557
+ if (iri.href === parentId.href) return Promise.resolve(parent);
558
+ if (iri.href === rootId.href) return Promise.resolve(root);
559
+ return Promise.resolve(null);
560
+ } }, new _fedify_vocab.Note({
561
+ id: new URL("https://example.com/notes/3"),
562
+ replyTarget: parentId
563
+ }), {
564
+ strategies: ["reply-tree"],
565
+ maxDepth: 1
566
+ });
567
+ (0, node_assert_strict.strictEqual)(items.length, 1);
568
+ (0, node_assert_strict.deepStrictEqual)(items[0].object.id, parent.id);
569
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
570
+ });
571
+ (0, node_test.default)("reply tree defaults maxDepth to 10 for ancestors", async () => {
572
+ let note = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/0") });
573
+ for (let i = 1; i <= 12; i++) note = new _fedify_vocab.Note({
574
+ id: new URL(`https://example.com/notes/${i}`),
575
+ replyTarget: note
576
+ });
577
+ const items = await collect({ documentLoader: () => {
578
+ throw new Error("documentLoader should not be called");
579
+ } }, note, { strategies: ["reply-tree"] });
580
+ (0, node_assert_strict.strictEqual)(items.length, 10);
581
+ (0, node_assert_strict.strictEqual)(items.at(-1)?.depth, 10);
582
+ });
583
+ (0, node_test.default)("maxRequests limits reply tree ancestor dereferencing", async () => {
584
+ const parentId = new URL("https://example.com/notes/1");
585
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => {
586
+ throw new Error("documentLoader should not be called");
587
+ } }, new _fedify_vocab.Note({
588
+ id: new URL("https://example.com/notes/2"),
589
+ replyTarget: parentId
590
+ }), {
591
+ strategies: ["reply-tree"],
592
+ maxRequests: 0
593
+ }), []);
594
+ });
595
+ (0, node_test.default)("reply tree avoids ancestor cycles", async () => {
596
+ const seedId = new URL("https://example.com/notes/1");
597
+ const parentId = new URL("https://example.com/notes/2");
598
+ const note = new _fedify_vocab.Note({
599
+ id: seedId,
600
+ replyTarget: parentId
601
+ });
602
+ const parent = new _fedify_vocab.Note({
603
+ id: parentId,
604
+ replyTarget: seedId
605
+ });
606
+ const items = await collect({ documentLoader: (iri) => {
607
+ if (iri.href === seedId.href) return Promise.resolve(note);
608
+ if (iri.href === parentId.href) return Promise.resolve(parent);
609
+ return Promise.resolve(null);
610
+ } }, note, { strategies: ["reply-tree"] });
611
+ (0, node_assert_strict.strictEqual)(items.length, 1);
612
+ (0, node_assert_strict.deepStrictEqual)(items[0].object.id, parent.id);
613
+ });
614
+ (0, node_test.default)("reply tree deduplicates ancestors from context collection", async () => {
615
+ const contextId = new URL("https://example.com/contexts/1");
616
+ const parentId = new URL("https://example.com/notes/1");
617
+ const parent = new _fedify_vocab.Note({
618
+ id: parentId,
619
+ content: "parent"
620
+ });
621
+ const items = await collect({ documentLoader: (iri) => {
622
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
623
+ id: contextId,
624
+ items: [parent]
625
+ }));
626
+ if (iri.href === parentId.href) return Promise.resolve(parent);
627
+ return Promise.resolve(null);
628
+ } }, new _fedify_vocab.Note({
629
+ id: new URL("https://example.com/notes/2"),
630
+ contexts: [contextId],
631
+ replyTarget: parentId
632
+ }), { strategies: ["context-auto", "reply-tree"] });
633
+ (0, node_assert_strict.strictEqual)(items.length, 1);
634
+ (0, node_assert_strict.strictEqual)(items[0].object, parent);
635
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-auto");
636
+ });
637
+ (0, node_test.default)("document cache avoids duplicate dereferences across strategies", async () => {
638
+ const contextId = new URL("https://example.com/contexts/1");
639
+ const parentId = new URL("https://example.com/notes/1");
640
+ const parent = new _fedify_vocab.Note({
641
+ id: parentId,
642
+ content: "parent"
643
+ });
644
+ const note = new _fedify_vocab.Note({
645
+ id: new URL("https://example.com/notes/2"),
646
+ contexts: [contextId],
647
+ replyTarget: parentId
648
+ });
649
+ const requests = [];
650
+ const items = await collect({ documentLoader: (iri) => {
651
+ requests.push(iri);
652
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
653
+ id: contextId,
654
+ items: [parentId]
655
+ }));
656
+ if (iri.href === parentId.href) return Promise.resolve(parent);
657
+ return Promise.resolve(null);
658
+ } }, note, { strategies: ["context-auto", "reply-tree"] });
659
+ (0, node_assert_strict.strictEqual)(items.length, 1);
660
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, parentId.href);
661
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [contextId.href, parentId.href]);
662
+ });
663
+ (0, node_test.default)("document cache does not keep failed dereferences", async () => {
664
+ const contextId = new URL("https://example.com/contexts/1");
665
+ const parentId = new URL("https://example.com/notes/1");
666
+ const parent = new _fedify_vocab.Note({
667
+ id: parentId,
668
+ content: "parent"
669
+ });
670
+ const note = new _fedify_vocab.Note({
671
+ id: new URL("https://example.com/notes/2"),
672
+ contexts: [contextId],
673
+ replyTarget: parentId
674
+ });
675
+ const requests = [];
676
+ let parentRequests = 0;
677
+ const items = await collect({ documentLoader: (iri) => {
678
+ requests.push(iri);
679
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
680
+ id: contextId,
681
+ items: [parentId]
682
+ }));
683
+ if (iri.href === parentId.href) {
684
+ parentRequests++;
685
+ if (parentRequests === 1) throw new Error("temporary failure");
686
+ return Promise.resolve(parent);
687
+ }
688
+ return Promise.resolve(null);
689
+ } }, note, { strategies: ["context-auto", "reply-tree"] });
690
+ (0, node_assert_strict.strictEqual)(items.length, 1);
691
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, parentId.href);
692
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [
693
+ contextId.href,
694
+ parentId.href,
695
+ parentId.href
696
+ ]);
697
+ });
698
+ (0, node_test.default)("strategy order controls deduplicated item metadata", async () => {
699
+ const contextId = new URL("https://example.com/contexts/1");
700
+ const parentId = new URL("https://example.com/notes/1");
701
+ const parent = new _fedify_vocab.Note({
702
+ id: parentId,
703
+ content: "parent"
704
+ });
705
+ const items = await collect({ documentLoader: (iri) => {
706
+ if (iri.href === parentId.href) return Promise.resolve(parent);
707
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
708
+ id: contextId,
709
+ items: [parent]
710
+ }));
711
+ return Promise.resolve(null);
712
+ } }, new _fedify_vocab.Note({
713
+ id: new URL("https://example.com/notes/2"),
714
+ contexts: [contextId],
715
+ replyTarget: parentId
716
+ }), { strategies: ["reply-tree", "context-auto"] });
717
+ (0, node_assert_strict.strictEqual)(items.length, 1);
718
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, parentId.href);
719
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "reply-tree");
720
+ (0, node_assert_strict.strictEqual)(items[0].origin, "in-reply-to");
721
+ });
722
+ (0, node_test.default)("context auto preserves strategy order across reply tree", async () => {
723
+ const contextId = new URL("https://example.com/contexts/1");
724
+ const parentId = new URL("https://example.com/notes/1");
725
+ const parent = new _fedify_vocab.Note({
726
+ id: parentId,
727
+ content: "parent"
728
+ });
729
+ const items = await collect({ documentLoader: (iri) => {
730
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
731
+ id: contextId,
732
+ items: [parent]
733
+ }));
734
+ if (iri.href === parentId.href) return Promise.resolve(parent);
735
+ return Promise.resolve(null);
736
+ } }, new _fedify_vocab.Note({
737
+ id: new URL("https://example.com/notes/2"),
738
+ contexts: [contextId],
739
+ replyTarget: parentId
740
+ }), { strategies: [
741
+ "context-objects",
742
+ "reply-tree",
743
+ "context-auto"
744
+ ] });
745
+ (0, node_assert_strict.strictEqual)(items.length, 1);
746
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, parentId.href);
747
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-objects");
748
+ (0, node_assert_strict.strictEqual)(items[0].origin, "collection");
749
+ });
750
+ (0, node_test.default)("reply tree yields embedded descendants", async () => {
751
+ const reply = new _fedify_vocab.Note({
752
+ id: new URL("https://example.com/notes/2"),
753
+ content: "reply"
754
+ });
755
+ const items = await collect({ documentLoader: () => {
756
+ throw new Error("documentLoader should not be called");
757
+ } }, new _fedify_vocab.Note({
758
+ id: new URL("https://example.com/notes/1"),
759
+ replies: new _fedify_vocab.Collection({
760
+ id: new URL("https://example.com/notes/1/replies"),
761
+ items: [reply]
762
+ })
763
+ }), { strategies: ["reply-tree"] });
764
+ (0, node_assert_strict.strictEqual)(items.length, 1);
765
+ (0, node_assert_strict.strictEqual)(items[0].object, reply);
766
+ (0, node_assert_strict.deepStrictEqual)(items[0].id, reply.id);
767
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "reply-tree");
768
+ (0, node_assert_strict.strictEqual)(items[0].origin, "replies");
769
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
770
+ });
771
+ (0, node_test.default)("reply tree walks sibling descendants from discovered ancestor", async () => {
772
+ const seedId = new URL("https://example.com/notes/2");
773
+ const sibling = new _fedify_vocab.Note({
774
+ id: new URL("https://example.com/notes/3"),
775
+ content: "sibling"
776
+ });
777
+ const parent = new _fedify_vocab.Note({
778
+ id: new URL("https://example.com/notes/1"),
779
+ content: "parent",
780
+ replies: new _fedify_vocab.Collection({
781
+ id: new URL("https://example.com/notes/1/replies"),
782
+ items: [seedId, sibling]
783
+ })
784
+ });
785
+ const items = await collect({ documentLoader: () => {
786
+ throw new Error("documentLoader should not be called");
787
+ } }, new _fedify_vocab.Note({
788
+ id: seedId,
789
+ replyTarget: parent
790
+ }), { strategies: ["reply-tree"] });
791
+ (0, node_assert_strict.strictEqual)(items.length, 2);
792
+ (0, node_assert_strict.strictEqual)(items[0].object, parent);
793
+ (0, node_assert_strict.strictEqual)(items[0].origin, "in-reply-to");
794
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
795
+ (0, node_assert_strict.strictEqual)(items[1].object, sibling);
796
+ (0, node_assert_strict.strictEqual)(items[1].origin, "replies");
797
+ (0, node_assert_strict.strictEqual)(items[1].depth, 2);
798
+ });
799
+ (0, node_test.default)("reply tree maxDepth applies from seed through ancestors", async () => {
800
+ const seedId = new URL("https://example.com/notes/2");
801
+ const sibling = new _fedify_vocab.Note({
802
+ id: new URL("https://example.com/notes/3"),
803
+ content: "sibling"
804
+ });
805
+ const parent = new _fedify_vocab.Note({
806
+ id: new URL("https://example.com/notes/1"),
807
+ content: "parent",
808
+ replies: new _fedify_vocab.Collection({
809
+ id: new URL("https://example.com/notes/1/replies"),
810
+ items: [seedId, sibling]
811
+ })
812
+ });
813
+ const items = await collect({ documentLoader: () => {
814
+ throw new Error("documentLoader should not be called");
815
+ } }, new _fedify_vocab.Note({
816
+ id: seedId,
817
+ replyTarget: parent
818
+ }), {
819
+ strategies: ["reply-tree"],
820
+ maxDepth: 1
821
+ });
822
+ (0, node_assert_strict.strictEqual)(items.length, 1);
823
+ (0, node_assert_strict.strictEqual)(items[0].object, parent);
824
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
825
+ });
826
+ (0, node_test.default)("reply tree dereferences replies collection URL", async () => {
827
+ const repliesId = new URL("https://example.com/notes/1/replies");
828
+ const reply = new _fedify_vocab.Note({
829
+ id: new URL("https://example.com/notes/2"),
830
+ content: "reply"
831
+ });
832
+ const items = await collect({ documentLoader: (iri) => Promise.resolve(iri.href === repliesId.href ? new _fedify_vocab.Collection({
833
+ id: repliesId,
834
+ items: [reply]
835
+ }) : null) }, new _fedify_vocab.Note({
836
+ id: new URL("https://example.com/notes/1"),
837
+ replies: repliesId
838
+ }), { strategies: ["reply-tree"] });
839
+ (0, node_assert_strict.strictEqual)(items.length, 1);
840
+ (0, node_assert_strict.deepStrictEqual)(items[0].object.id, reply.id);
841
+ (0, node_assert_strict.strictEqual)(items[0].origin, "replies");
842
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
843
+ });
844
+ (0, node_test.default)("reply tree maxDepth limits descendants", async () => {
845
+ const grandchild = new _fedify_vocab.Note({
846
+ id: new URL("https://example.com/notes/3"),
847
+ content: "grandchild"
848
+ });
849
+ const reply = new _fedify_vocab.Note({
850
+ id: new URL("https://example.com/notes/2"),
851
+ content: "reply",
852
+ replies: new _fedify_vocab.Collection({
853
+ id: new URL("https://example.com/notes/2/replies"),
854
+ items: [grandchild]
855
+ })
856
+ });
857
+ const items = await collect({ documentLoader: () => {
858
+ throw new Error("documentLoader should not be called");
859
+ } }, new _fedify_vocab.Note({
860
+ id: new URL("https://example.com/notes/1"),
861
+ replies: new _fedify_vocab.Collection({
862
+ id: new URL("https://example.com/notes/1/replies"),
863
+ items: [reply]
864
+ })
865
+ }), {
866
+ strategies: ["reply-tree"],
867
+ maxDepth: 1
868
+ });
869
+ (0, node_assert_strict.strictEqual)(items.length, 1);
870
+ (0, node_assert_strict.strictEqual)(items[0].object, reply);
871
+ (0, node_assert_strict.strictEqual)(items[0].depth, 1);
872
+ });
873
+ (0, node_test.default)("reply tree defaults maxDepth to 10 for descendants", async () => {
874
+ let note = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/12") });
875
+ for (let i = 11; i >= 0; i--) note = new _fedify_vocab.Note({
876
+ id: new URL(`https://example.com/notes/${i}`),
877
+ replies: new _fedify_vocab.Collection({
878
+ id: new URL(`https://example.com/notes/${i}/replies`),
879
+ items: [note]
880
+ })
881
+ });
882
+ const items = await collect({ documentLoader: () => {
883
+ throw new Error("documentLoader should not be called");
884
+ } }, note, { strategies: ["reply-tree"] });
885
+ (0, node_assert_strict.strictEqual)(items.length, 10);
886
+ (0, node_assert_strict.strictEqual)(items.at(-1)?.depth, 10);
887
+ });
888
+ (0, node_test.default)("maxRequests limits reply tree replies dereferencing", async () => {
889
+ const repliesId = new URL("https://example.com/notes/1/replies");
890
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => {
891
+ throw new Error("documentLoader should not be called");
892
+ } }, new _fedify_vocab.Note({
893
+ id: new URL("https://example.com/notes/1"),
894
+ replies: repliesId
895
+ }), {
896
+ strategies: ["reply-tree"],
897
+ maxRequests: 0
898
+ }), []);
899
+ });
900
+ (0, node_test.default)("reply tree does not reload visited replies collection URL", async () => {
901
+ const repliesId = new URL("https://example.com/notes/1/replies");
902
+ const reply = new _fedify_vocab.Note({
903
+ id: new URL("https://example.com/notes/2"),
904
+ content: "reply",
905
+ replies: repliesId
906
+ });
907
+ const note = new _fedify_vocab.Note({
908
+ id: new URL("https://example.com/notes/1"),
909
+ replies: repliesId
910
+ });
911
+ let requests = 0;
912
+ const items = await collect({ documentLoader: (iri) => {
913
+ requests++;
914
+ (0, node_assert_strict.strictEqual)(iri.href, repliesId.href);
915
+ return Promise.resolve(new _fedify_vocab.Collection({
916
+ id: repliesId,
917
+ items: [reply]
918
+ }));
919
+ } }, note, { strategies: ["reply-tree"] });
920
+ (0, node_assert_strict.strictEqual)(requests, 1);
921
+ (0, node_assert_strict.strictEqual)(items.length, 1);
922
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, reply.id?.href);
923
+ });
924
+ (0, node_test.default)("reply tree retries a replies collection after load failure", async () => {
925
+ const repliesId = new URL("https://example.com/replies/shared");
926
+ const grandchild = new _fedify_vocab.Note({
927
+ id: new URL("https://example.com/notes/4"),
928
+ content: "grandchild"
929
+ });
930
+ const first = new _fedify_vocab.Note({
931
+ id: new URL("https://example.com/notes/2"),
932
+ replies: repliesId
933
+ });
934
+ const second = new _fedify_vocab.Note({
935
+ id: new URL("https://example.com/notes/3"),
936
+ replies: repliesId
937
+ });
938
+ const note = new _fedify_vocab.Note({
939
+ id: new URL("https://example.com/notes/1"),
940
+ replies: new _fedify_vocab.Collection({
941
+ id: new URL("https://example.com/notes/1/replies"),
942
+ items: [first, second]
943
+ })
944
+ });
945
+ let requests = 0;
946
+ const items = await collect({ documentLoader: (iri) => {
947
+ (0, node_assert_strict.strictEqual)(iri.href, repliesId.href);
948
+ requests++;
949
+ if (requests === 1) throw new Error("temporary failure");
950
+ return Promise.resolve(new _fedify_vocab.Collection({
951
+ id: repliesId,
952
+ items: [grandchild]
953
+ }));
954
+ } }, note, { strategies: ["reply-tree"] });
955
+ (0, node_assert_strict.strictEqual)(requests, 2);
956
+ (0, node_assert_strict.deepStrictEqual)(items.map((item) => item.object.id?.href), [
957
+ first.id?.href,
958
+ second.id?.href,
959
+ grandchild.id?.href
960
+ ]);
961
+ (0, node_assert_strict.strictEqual)(items[2].depth, 2);
962
+ });
963
+ (0, node_test.default)("reply tree skips visited reply IRIs before dereferencing", async () => {
964
+ const seedId = new URL("https://example.com/notes/1");
965
+ const siblingId = new URL("https://example.com/notes/2");
966
+ const sibling = new _fedify_vocab.Note({
967
+ id: siblingId,
968
+ content: "sibling"
969
+ });
970
+ const note = new _fedify_vocab.Note({
971
+ id: seedId,
972
+ replies: new _fedify_vocab.Collection({
973
+ id: new URL("https://example.com/notes/1/replies"),
974
+ items: [seedId, siblingId]
975
+ })
976
+ });
977
+ const requests = [];
978
+ const items = await collect({ documentLoader: (iri) => {
979
+ requests.push(iri.href);
980
+ if (iri.href === siblingId.href) return Promise.resolve(sibling);
981
+ if (iri.href === seedId.href) throw new Error("seed should have been skipped");
982
+ return Promise.resolve(null);
983
+ } }, note, {
984
+ strategies: ["reply-tree"],
985
+ maxRequests: 1
986
+ });
987
+ (0, node_assert_strict.deepStrictEqual)(requests, [siblingId.href]);
988
+ (0, node_assert_strict.strictEqual)(items.length, 1);
989
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, siblingId.href);
990
+ });
991
+ (0, node_test.default)("reply tree avoids descendant cycles", async () => {
992
+ const seedId = new URL("https://example.com/notes/1");
993
+ const replyId = new URL("https://example.com/notes/2");
994
+ const note = new _fedify_vocab.Note({ id: seedId });
995
+ const reply = new _fedify_vocab.Note({
996
+ id: replyId,
997
+ replies: new _fedify_vocab.Collection({
998
+ id: new URL("https://example.com/notes/2/replies"),
999
+ items: [note]
1000
+ })
1001
+ });
1002
+ const items = await collect({ documentLoader: () => {
1003
+ throw new Error("documentLoader should not be called");
1004
+ } }, note.clone({ replies: new _fedify_vocab.Collection({
1005
+ id: new URL("https://example.com/notes/1/replies"),
1006
+ items: [reply]
1007
+ }) }), { strategies: ["reply-tree"] });
1008
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1009
+ (0, node_assert_strict.strictEqual)(items[0].object, reply);
1010
+ });
1011
+ (0, node_test.default)("context auto overrides overlapping context strategies", async () => {
1012
+ const contextId = new URL("https://example.com/contexts/1");
1013
+ const item = new _fedify_vocab.Note({ content: "anonymous" });
1014
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1015
+ id: contextId,
1016
+ items: [item]
1017
+ })) }, new _fedify_vocab.Note({
1018
+ id: new URL("https://example.com/notes/1"),
1019
+ contexts: [contextId]
1020
+ }), { strategies: [
1021
+ "context-objects",
1022
+ "context-auto",
1023
+ "reply-tree"
1024
+ ] });
1025
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1026
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
1027
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-auto");
1028
+ });
1029
+ (0, node_test.default)("duplicate strategies are ignored", async () => {
1030
+ const contextId = new URL("https://example.com/contexts/1");
1031
+ const item = new _fedify_vocab.Note({ content: "anonymous" });
1032
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1033
+ id: contextId,
1034
+ items: [item]
1035
+ })) }, new _fedify_vocab.Note({
1036
+ id: new URL("https://example.com/notes/1"),
1037
+ contexts: [contextId]
1038
+ }), { strategies: ["context-objects", "context-objects"] });
1039
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1040
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
1041
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-objects");
1042
+ });
1043
+ (0, node_test.default)("context activity collection yields object from embedded Create", async () => {
1044
+ const contextId = new URL("https://example.com/contexts/1");
1045
+ const item = new _fedify_vocab.Note({
1046
+ id: new URL("https://example.com/notes/2"),
1047
+ content: "hello"
1048
+ });
1049
+ const activity = new _fedify_vocab.Create({
1050
+ id: new URL("https://example.com/activities/1"),
1051
+ object: item
1052
+ });
1053
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1054
+ id: contextId,
1055
+ items: [activity]
1056
+ })) }, new _fedify_vocab.Note({
1057
+ id: new URL("https://example.com/notes/1"),
1058
+ contexts: [contextId]
1059
+ }), { strategies: ["context-activities"] });
1060
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1061
+ (0, node_assert_strict.strictEqual)(items[0].object, item);
1062
+ (0, node_assert_strict.strictEqual)(items[0].id?.href, item.id?.href);
1063
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-activities");
1064
+ (0, node_assert_strict.strictEqual)(items[0].origin, "collection");
1065
+ });
1066
+ (0, node_test.default)("combined context strategies yield posts and activity objects", async () => {
1067
+ const contextId = new URL("https://example.com/contexts/1");
1068
+ const post = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") });
1069
+ const activityObject = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/3") });
1070
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1071
+ id: contextId,
1072
+ items: [post, new _fedify_vocab.Create({
1073
+ id: new URL("https://example.com/activities/1"),
1074
+ object: activityObject
1075
+ })]
1076
+ })) }, new _fedify_vocab.Note({
1077
+ id: new URL("https://example.com/notes/1"),
1078
+ contexts: [contextId]
1079
+ }), { strategies: ["context-objects", "context-activities"] });
1080
+ (0, node_assert_strict.strictEqual)(items.length, 2);
1081
+ (0, node_assert_strict.strictEqual)(items[0].object, post);
1082
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-objects");
1083
+ (0, node_assert_strict.strictEqual)(items[1].object, activityObject);
1084
+ (0, node_assert_strict.strictEqual)(items[1].strategy, "context-activities");
1085
+ });
1086
+ (0, node_test.default)("combined context strategies share context collection loading", async () => {
1087
+ const contextId = new URL("https://example.com/contexts/1");
1088
+ const post = new _fedify_vocab.Note({
1089
+ id: new URL("https://example.com/notes/2"),
1090
+ content: "hello"
1091
+ });
1092
+ const activityObject = new _fedify_vocab.Note({
1093
+ id: new URL("https://example.com/notes/3"),
1094
+ content: "activity object"
1095
+ });
1096
+ const activity = new _fedify_vocab.Create({
1097
+ id: new URL("https://example.com/activities/1"),
1098
+ object: activityObject
1099
+ });
1100
+ const note = new _fedify_vocab.Note({
1101
+ id: new URL("https://example.com/notes/1"),
1102
+ contexts: [contextId]
1103
+ });
1104
+ let requests = 0;
1105
+ const items = await collect({ documentLoader: (iri) => {
1106
+ requests++;
1107
+ (0, node_assert_strict.strictEqual)(iri.href, contextId.href);
1108
+ return Promise.resolve(new _fedify_vocab.Collection({
1109
+ id: contextId,
1110
+ items: [post, activity]
1111
+ }));
1112
+ } }, note, { strategies: ["context-objects", "context-activities"] });
1113
+ (0, node_assert_strict.strictEqual)(requests, 1);
1114
+ (0, node_assert_strict.strictEqual)(items.length, 2);
1115
+ (0, node_assert_strict.strictEqual)(items[0].object, post);
1116
+ (0, node_assert_strict.strictEqual)(items[1].object, activityObject);
1117
+ });
1118
+ (0, node_test.default)("context activity collection dereferences activity object URL", async () => {
1119
+ const contextId = new URL("https://example.com/contexts/1");
1120
+ const itemId = new URL("https://example.com/notes/2");
1121
+ const item = new _fedify_vocab.Note({
1122
+ id: itemId,
1123
+ content: "hello"
1124
+ });
1125
+ const activity = new _fedify_vocab.Create({
1126
+ id: new URL("https://example.com/activities/1"),
1127
+ object: itemId
1128
+ });
1129
+ const note = new _fedify_vocab.Note({
1130
+ id: new URL("https://example.com/notes/1"),
1131
+ contexts: [contextId]
1132
+ });
1133
+ const requests = [];
1134
+ const items = await collect({ documentLoader: (iri) => {
1135
+ requests.push(iri);
1136
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1137
+ id: contextId,
1138
+ items: [activity]
1139
+ }));
1140
+ if (iri.href === itemId.href) return Promise.resolve(item);
1141
+ return Promise.resolve(null);
1142
+ } }, note, { strategies: ["context-activities"] });
1143
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1144
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, item.id?.href);
1145
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [contextId.href, itemId.href]);
1146
+ });
1147
+ (0, node_test.default)("context activity collection dereferences activity URL", async () => {
1148
+ const contextId = new URL("https://example.com/contexts/1");
1149
+ const activityId = new URL("https://example.com/activities/1");
1150
+ const item = new _fedify_vocab.Note({
1151
+ id: new URL("https://example.com/notes/2"),
1152
+ content: "hello"
1153
+ });
1154
+ const activity = new _fedify_vocab.Create({
1155
+ id: activityId,
1156
+ object: item
1157
+ });
1158
+ const note = new _fedify_vocab.Note({
1159
+ id: new URL("https://example.com/notes/1"),
1160
+ contexts: [contextId]
1161
+ });
1162
+ const requests = [];
1163
+ const items = await collect({ documentLoader: (iri) => {
1164
+ requests.push(iri);
1165
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1166
+ id: contextId,
1167
+ items: [activityId]
1168
+ }));
1169
+ if (iri.href === activityId.href) return Promise.resolve(activity);
1170
+ return Promise.resolve(null);
1171
+ } }, note, { strategies: ["context-activities"] });
1172
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1173
+ (0, node_assert_strict.strictEqual)(items[0].object.id?.href, item.id?.href);
1174
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [contextId.href, activityId.href]);
1175
+ });
1176
+ (0, node_test.default)("context activity collection deduplicates by extracted object ID", async () => {
1177
+ const contextId = new URL("https://example.com/contexts/1");
1178
+ const itemId = new URL("https://example.com/notes/2");
1179
+ const first = new _fedify_vocab.Create({
1180
+ id: new URL("https://example.com/activities/1"),
1181
+ object: new _fedify_vocab.Note({
1182
+ id: itemId,
1183
+ content: "first"
1184
+ })
1185
+ });
1186
+ const second = new _fedify_vocab.Create({
1187
+ id: new URL("https://example.com/activities/2"),
1188
+ object: new _fedify_vocab.Note({
1189
+ id: itemId,
1190
+ content: "second"
1191
+ })
1192
+ });
1193
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1194
+ id: contextId,
1195
+ items: [first, second]
1196
+ })) }, new _fedify_vocab.Note({
1197
+ id: new URL("https://example.com/notes/1"),
1198
+ contexts: [contextId]
1199
+ }), { strategies: ["context-activities"] });
1200
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1201
+ (0, node_assert_strict.strictEqual)(items[0].id?.href, itemId.href);
1202
+ });
1203
+ (0, node_test.default)("context activity collection skips missing object", async () => {
1204
+ const contextId = new URL("https://example.com/contexts/1");
1205
+ const activity = new _fedify_vocab.Create({ id: new URL("https://example.com/activities/1") });
1206
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1207
+ id: contextId,
1208
+ items: [activity]
1209
+ })) }, new _fedify_vocab.Note({
1210
+ id: new URL("https://example.com/notes/1"),
1211
+ contexts: [contextId]
1212
+ }), { strategies: ["context-activities"] }), []);
1213
+ });
1214
+ (0, node_test.default)("context activity collection skips unsupported activity type", async () => {
1215
+ const contextId = new URL("https://example.com/contexts/1");
1216
+ const item = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") });
1217
+ const activity = new _fedify_vocab.Announce({
1218
+ id: new URL("https://example.com/activities/1"),
1219
+ object: item
1220
+ });
1221
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1222
+ id: contextId,
1223
+ items: [activity]
1224
+ })) }, new _fedify_vocab.Note({
1225
+ id: new URL("https://example.com/notes/1"),
1226
+ contexts: [contextId]
1227
+ }), { strategies: ["context-activities"] }), []);
1228
+ });
1229
+ (0, node_test.default)("maxRequests limits activity object dereferencing", async () => {
1230
+ const contextId = new URL("https://example.com/contexts/1");
1231
+ const activityId = new URL("https://example.com/activities/1");
1232
+ const itemId = new URL("https://example.com/notes/2");
1233
+ const activity = new _fedify_vocab.Create({
1234
+ id: activityId,
1235
+ object: itemId
1236
+ });
1237
+ const note = new _fedify_vocab.Note({
1238
+ id: new URL("https://example.com/notes/1"),
1239
+ contexts: [contextId]
1240
+ });
1241
+ const requests = [];
1242
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: (iri) => {
1243
+ requests.push(iri);
1244
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1245
+ id: contextId,
1246
+ items: [activityId]
1247
+ }));
1248
+ if (iri.href === activityId.href) return Promise.resolve(activity);
1249
+ if (iri.href === itemId.href) return Promise.resolve(new _fedify_vocab.Note({ id: itemId }));
1250
+ return Promise.resolve(null);
1251
+ } }, note, {
1252
+ maxRequests: 2,
1253
+ strategies: ["context-activities"]
1254
+ }), []);
1255
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [contextId.href, activityId.href]);
1256
+ });
1257
+ (0, node_test.default)("maxItems limits context activity items", async () => {
1258
+ const contextId = new URL("https://example.com/contexts/1");
1259
+ const first = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") });
1260
+ const second = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/3") });
1261
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1262
+ id: contextId,
1263
+ items: [new _fedify_vocab.Create({
1264
+ id: new URL("https://example.com/activities/1"),
1265
+ object: first
1266
+ }), new _fedify_vocab.Create({
1267
+ id: new URL("https://example.com/activities/2"),
1268
+ object: second
1269
+ })]
1270
+ })) }, new _fedify_vocab.Note({
1271
+ id: new URL("https://example.com/notes/1"),
1272
+ contexts: [contextId]
1273
+ }), {
1274
+ maxItems: 1,
1275
+ strategies: ["context-activities"]
1276
+ });
1277
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1278
+ (0, node_assert_strict.strictEqual)(items[0].id?.href, first.id?.href);
1279
+ });
1280
+ (0, node_test.default)("context collection with URL items loads and yields objects", async () => {
1281
+ const contextId = new URL("https://example.com/contexts/1");
1282
+ const itemId = new URL("https://example.com/notes/2");
1283
+ const item = new _fedify_vocab.Note({
1284
+ id: itemId,
1285
+ content: "hello"
1286
+ });
1287
+ const note = new _fedify_vocab.Note({
1288
+ id: new URL("https://example.com/notes/1"),
1289
+ contexts: [contextId]
1290
+ });
1291
+ const requests = [];
1292
+ const items = await collect({ documentLoader: (iri) => {
1293
+ requests.push(iri);
1294
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1295
+ id: contextId,
1296
+ items: [itemId]
1297
+ }));
1298
+ if (iri.href === itemId.href) return Promise.resolve(item);
1299
+ return Promise.resolve(null);
1300
+ } }, note);
1301
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1302
+ (0, node_assert_strict.ok)(items[0].id instanceof URL);
1303
+ (0, node_assert_strict.strictEqual)(items[0].id.href, itemId.href);
1304
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [contextId.href, itemId.href]);
1305
+ });
1306
+ (0, node_test.default)("seen context collection URL items are not loaded", async () => {
1307
+ const contextId = new URL("https://example.com/contexts/1");
1308
+ const seedId = new URL("https://example.com/notes/1");
1309
+ const itemId = new URL("https://example.com/notes/2");
1310
+ const item = new _fedify_vocab.Note({
1311
+ id: itemId,
1312
+ content: "hello"
1313
+ });
1314
+ const note = new _fedify_vocab.Note({
1315
+ id: seedId,
1316
+ contexts: [contextId]
1317
+ });
1318
+ const requests = [];
1319
+ const items = await collect({ documentLoader: (iri) => {
1320
+ requests.push(iri);
1321
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1322
+ id: contextId,
1323
+ items: [seedId, itemId]
1324
+ }));
1325
+ if (iri.href === itemId.href) return Promise.resolve(item);
1326
+ throw new Error("seen collection item should not be loaded");
1327
+ } }, note);
1328
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1329
+ (0, node_assert_strict.strictEqual)(items[0].id?.href, itemId.href);
1330
+ (0, node_assert_strict.deepStrictEqual)(requests.map((url) => url.href), [contextId.href, itemId.href]);
1331
+ });
1332
+ (0, node_test.default)("failed URL collection items are skipped", async () => {
1333
+ const contextId = new URL("https://example.com/contexts/1");
1334
+ const missingItemId = new URL("https://example.com/notes/missing");
1335
+ const failedItemId = new URL("https://example.com/notes/failed");
1336
+ const itemId = new URL("https://example.com/notes/2");
1337
+ const item = new _fedify_vocab.Note({
1338
+ id: itemId,
1339
+ content: "hello"
1340
+ });
1341
+ const items = await collect({ documentLoader: (iri) => {
1342
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1343
+ id: contextId,
1344
+ items: [
1345
+ missingItemId,
1346
+ failedItemId,
1347
+ itemId
1348
+ ]
1349
+ }));
1350
+ if (iri.href === missingItemId.href) return Promise.resolve(null);
1351
+ if (iri.href === failedItemId.href) return Promise.reject(/* @__PURE__ */ new Error("failed to load"));
1352
+ if (iri.href === itemId.href) return Promise.resolve(item);
1353
+ return Promise.resolve(null);
1354
+ } }, new _fedify_vocab.Note({
1355
+ id: new URL("https://example.com/notes/1"),
1356
+ contexts: [contextId]
1357
+ }));
1358
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1359
+ (0, node_assert_strict.strictEqual)(items[0].id?.href, itemId.href);
1360
+ });
1361
+ (0, node_test.default)("seed is not yielded again when present in collection", async () => {
1362
+ const contextId = new URL("https://example.com/contexts/1");
1363
+ const note = new _fedify_vocab.Note({
1364
+ id: new URL("https://example.com/notes/1"),
1365
+ contexts: [contextId]
1366
+ });
1367
+ const other = new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") });
1368
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1369
+ id: contextId,
1370
+ items: [note, other]
1371
+ })) }, note);
1372
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1373
+ (0, node_assert_strict.strictEqual)(items[0].object, other);
1374
+ });
1375
+ (0, node_test.default)("duplicate object IDs are skipped", async () => {
1376
+ const contextId = new URL("https://example.com/contexts/1");
1377
+ const duplicateId = new URL("https://example.com/notes/2");
1378
+ const first = new _fedify_vocab.Note({
1379
+ id: duplicateId,
1380
+ content: "first"
1381
+ });
1382
+ const second = new _fedify_vocab.Note({
1383
+ id: duplicateId,
1384
+ content: "second"
1385
+ });
1386
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1387
+ id: contextId,
1388
+ items: [first, second]
1389
+ })) }, new _fedify_vocab.Note({
1390
+ id: new URL("https://example.com/notes/1"),
1391
+ contexts: [contextId]
1392
+ }));
1393
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1394
+ (0, node_assert_strict.strictEqual)(items[0].object, first);
1395
+ });
1396
+ (0, node_test.default)("maxItems limits yielded items", async () => {
1397
+ const contextId = new URL("https://example.com/contexts/1");
1398
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1399
+ id: contextId,
1400
+ items: [new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") }), new _fedify_vocab.Note({ id: new URL("https://example.com/notes/3") })]
1401
+ })) }, new _fedify_vocab.Note({
1402
+ id: new URL("https://example.com/notes/1"),
1403
+ contexts: [contextId]
1404
+ }), { maxItems: 1 });
1405
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1406
+ (0, node_assert_strict.strictEqual)(items[0].id?.href, "https://example.com/notes/2");
1407
+ });
1408
+ (0, node_test.default)("maxItems is shared across context and reply tree", async () => {
1409
+ const contextId = new URL("https://example.com/contexts/1");
1410
+ const reply = new _fedify_vocab.Note({
1411
+ id: new URL("https://example.com/notes/3"),
1412
+ content: "reply"
1413
+ });
1414
+ const note = new _fedify_vocab.Note({
1415
+ id: new URL("https://example.com/notes/1"),
1416
+ contexts: [contextId],
1417
+ replies: new _fedify_vocab.Collection({
1418
+ id: new URL("https://example.com/notes/1/replies"),
1419
+ items: [reply]
1420
+ })
1421
+ });
1422
+ const contextItem = new _fedify_vocab.Note({
1423
+ id: new URL("https://example.com/notes/2"),
1424
+ content: "context item"
1425
+ });
1426
+ const items = await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1427
+ id: contextId,
1428
+ items: [contextItem]
1429
+ })) }, note, {
1430
+ strategies: ["context-auto", "reply-tree"],
1431
+ maxItems: 1
1432
+ });
1433
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1434
+ (0, node_assert_strict.strictEqual)(items[0].object, contextItem);
1435
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "context-auto");
1436
+ });
1437
+ (0, node_test.default)("maxRequests limits dereferencing", async () => {
1438
+ const contextId = new URL("https://example.com/contexts/1");
1439
+ const itemId = new URL("https://example.com/notes/2");
1440
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: (iri) => {
1441
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1442
+ id: contextId,
1443
+ items: [itemId]
1444
+ }));
1445
+ return Promise.resolve(new _fedify_vocab.Note({ id: iri }));
1446
+ } }, new _fedify_vocab.Note({
1447
+ id: new URL("https://example.com/notes/1"),
1448
+ contexts: [contextId]
1449
+ }), { maxRequests: 1 }), []);
1450
+ });
1451
+ (0, node_test.default)("maxRequests is shared across context and reply tree", async () => {
1452
+ const contextId = new URL("https://example.com/contexts/1");
1453
+ const parentId = new URL("https://example.com/notes/0");
1454
+ const note = new _fedify_vocab.Note({
1455
+ id: new URL("https://example.com/notes/1"),
1456
+ contexts: [contextId],
1457
+ replyTarget: parentId
1458
+ });
1459
+ const contextItem = new _fedify_vocab.Note({
1460
+ id: new URL("https://example.com/notes/2"),
1461
+ content: "context item"
1462
+ });
1463
+ const items = await collect({ documentLoader: (iri) => {
1464
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1465
+ id: contextId,
1466
+ items: [contextItem]
1467
+ }));
1468
+ throw new Error("reply-tree request should be budgeted out");
1469
+ } }, note, {
1470
+ strategies: ["context-auto", "reply-tree"],
1471
+ maxRequests: 1
1472
+ });
1473
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1474
+ (0, node_assert_strict.strictEqual)(items[0].object, contextItem);
1475
+ });
1476
+ (0, node_test.default)("AbortSignal stops traversal", async () => {
1477
+ const contextId = new URL("https://example.com/contexts/1");
1478
+ const note = new _fedify_vocab.Note({
1479
+ id: new URL("https://example.com/notes/1"),
1480
+ contexts: [contextId]
1481
+ });
1482
+ const controller = new AbortController();
1483
+ controller.abort();
1484
+ await (0, node_assert_strict.rejects)(collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1485
+ id: contextId,
1486
+ items: [new _fedify_vocab.Note({ id: new URL("https://example.com/notes/2") })]
1487
+ })) }, note, { signal: controller.signal }), { name: "AbortError" });
1488
+ });
1489
+ (0, node_test.default)("AbortSignal stops traversal across strategies", async () => {
1490
+ const contextId = new URL("https://example.com/contexts/1");
1491
+ const parentId = new URL("https://example.com/notes/0");
1492
+ const controller = new AbortController();
1493
+ const note = new _fedify_vocab.Note({
1494
+ id: new URL("https://example.com/notes/1"),
1495
+ contexts: [contextId],
1496
+ replyTarget: parentId
1497
+ });
1498
+ const contextItem = new _fedify_vocab.Note({
1499
+ id: new URL("https://example.com/notes/2"),
1500
+ content: "context item"
1501
+ });
1502
+ let requests = 0;
1503
+ const context = { documentLoader: (iri) => {
1504
+ requests++;
1505
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1506
+ id: contextId,
1507
+ items: [contextItem]
1508
+ }));
1509
+ throw new Error("reply-tree request should not be started");
1510
+ } };
1511
+ const items = [];
1512
+ await (0, node_assert_strict.rejects)(async () => {
1513
+ for await (const item of backfill(context, note, {
1514
+ strategies: ["context-auto", "reply-tree"],
1515
+ signal: controller.signal
1516
+ })) {
1517
+ items.push(item);
1518
+ controller.abort();
1519
+ }
1520
+ }, { name: "AbortError" });
1521
+ (0, node_assert_strict.strictEqual)(requests, 1);
1522
+ (0, node_assert_strict.strictEqual)(items.length, 1);
1523
+ (0, node_assert_strict.strictEqual)(items[0].object, contextItem);
1524
+ });
1525
+ (0, node_test.default)("documentLoader receives AbortSignal", async () => {
1526
+ const contextId = new URL("https://example.com/contexts/1");
1527
+ const note = new _fedify_vocab.Note({
1528
+ id: new URL("https://example.com/notes/1"),
1529
+ contexts: [contextId]
1530
+ });
1531
+ const controller = new AbortController();
1532
+ let receivedSignal;
1533
+ await collect({ documentLoader: (_iri, options) => {
1534
+ receivedSignal = options?.signal;
1535
+ return Promise.resolve(new _fedify_vocab.Collection({
1536
+ id: contextId,
1537
+ items: []
1538
+ }));
1539
+ } }, note, { signal: controller.signal });
1540
+ (0, node_assert_strict.strictEqual)(receivedSignal, controller.signal);
1541
+ });
1542
+ (0, node_test.default)("interval callback receives zero-based request index", async () => {
1543
+ const contextId = new URL("https://example.com/contexts/1");
1544
+ const itemId = new URL("https://example.com/notes/2");
1545
+ const note = new _fedify_vocab.Note({
1546
+ id: new URL("https://example.com/notes/1"),
1547
+ contexts: [contextId]
1548
+ });
1549
+ const iterations = [];
1550
+ await collect({ documentLoader: (iri) => {
1551
+ if (iri.href === contextId.href) return Promise.resolve(new _fedify_vocab.Collection({
1552
+ id: contextId,
1553
+ items: [itemId]
1554
+ }));
1555
+ return Promise.resolve(new _fedify_vocab.Note({ id: iri }));
1556
+ } }, note, { interval: (iteration) => {
1557
+ iterations.push(iteration);
1558
+ return { milliseconds: 0 };
1559
+ } });
1560
+ (0, node_assert_strict.deepStrictEqual)(iterations, [0, 1]);
1561
+ });
1562
+ });
1563
+ //#endregion