@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.
package/dist/mod.cjs ADDED
@@ -0,0 +1,366 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _fedify_vocab = require("@fedify/vocab");
3
+ //#region src/backfill.ts
4
+ const defaultStrategies = ["context-auto"];
5
+ const DEFAULT_MAX_DEPTH = 10;
6
+ /**
7
+ * Thrown when backfill traversal exceeds the configured request budget.
8
+ *
9
+ * @since 2.3.0
10
+ */
11
+ var MaxRequestsExceeded = class extends Error {};
12
+ /**
13
+ * Backfills post-like objects related to a seed object.
14
+ *
15
+ * The seed object is not yielded by default, but its ID is treated as already
16
+ * seen so it will not be yielded again if the collection contains it.
17
+ *
18
+ * @since 2.3.0
19
+ */
20
+ async function* backfill(context, note, options = {}) {
21
+ if (options.maxItems != null && options.maxItems <= 0) return;
22
+ const strategies = normalizeStrategies(options.strategies);
23
+ if (strategies.length < 1) return;
24
+ const budget = {
25
+ signal: options.signal,
26
+ requestCount: 0,
27
+ documents: /* @__PURE__ */ new Map()
28
+ };
29
+ const seenIds = /* @__PURE__ */ new Set();
30
+ if (note.id != null) seenIds.add(note.id.href);
31
+ let yielded = 0;
32
+ try {
33
+ for (let i = 0; i < strategies.length; i++) {
34
+ const strategy = strategies[i];
35
+ let items;
36
+ if (isContextStrategy(strategy)) {
37
+ const contextStrategies = [strategy];
38
+ while (true) {
39
+ const nextStrategy = strategies[i + 1];
40
+ if (nextStrategy == null || !isContextStrategy(nextStrategy)) break;
41
+ contextStrategies.push(nextStrategy);
42
+ i++;
43
+ }
44
+ items = getContextStrategyItems(context, note, contextStrategies, options, budget, seenIds);
45
+ } else items = getStrategyItems(context, note, strategy, options, budget, seenIds);
46
+ for await (const item of items) {
47
+ const id = item.object.id ?? void 0;
48
+ if (id != null) {
49
+ if (seenIds.has(id.href)) continue;
50
+ seenIds.add(id.href);
51
+ }
52
+ options.signal?.throwIfAborted();
53
+ yield {
54
+ object: item.object,
55
+ id,
56
+ strategy: item.strategy,
57
+ origin: item.origin,
58
+ depth: item.depth
59
+ };
60
+ yielded++;
61
+ if (options.maxItems != null && yielded >= options.maxItems) return;
62
+ }
63
+ }
64
+ } catch (error) {
65
+ if (error instanceof MaxRequestsExceeded) return;
66
+ throw error;
67
+ }
68
+ }
69
+ function normalizeStrategies(strategies = defaultStrategies) {
70
+ const normalized = [];
71
+ for (const strategy of strategies) if (strategy === "context-auto") {
72
+ for (let i = normalized.length - 1; i >= 0 && isContextStrategy(normalized[i]); i--) normalized.splice(i, 1);
73
+ if (!normalized.includes(strategy)) normalized.push(strategy);
74
+ } else if (isContextStrategy(strategy)) {
75
+ if (!currentContextGroupHasAuto(normalized) && !normalized.includes(strategy)) normalized.push(strategy);
76
+ } else if (!normalized.includes(strategy)) normalized.push(strategy);
77
+ return normalized;
78
+ }
79
+ function isContextStrategy(strategy) {
80
+ return strategy === "context-objects" || strategy === "context-activities" || strategy === "context-auto";
81
+ }
82
+ function currentContextGroupHasAuto(strategies) {
83
+ for (let i = strategies.length - 1; i >= 0; i--) {
84
+ const strategy = strategies[i];
85
+ if (!isContextStrategy(strategy)) return false;
86
+ if (strategy === "context-auto") return true;
87
+ }
88
+ return false;
89
+ }
90
+ async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
91
+ const contextId = note.contextIds[0];
92
+ if (contextId == null) return;
93
+ const collection = await loadObject(context, contextId, options, budget);
94
+ if (!isCollection(collection)) return;
95
+ 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 {
96
+ object: item.object,
97
+ strategy: item.strategy,
98
+ origin: "collection",
99
+ depth: 0
100
+ };
101
+ }
102
+ async function* getStrategyItems(context, note, strategy, options, budget, seenIds) {
103
+ if (isContextStrategy(strategy)) yield* getContextStrategyItems(context, note, [strategy], options, budget, seenIds);
104
+ else if (strategy === "reply-tree") yield* getReplyTreeItems(context, note, options, budget);
105
+ }
106
+ async function* getReplyTreeItems(context, note, options, budget) {
107
+ const visitedObjectIds = /* @__PURE__ */ new Set();
108
+ const visitedObjects = /* @__PURE__ */ new WeakSet();
109
+ const visitedCollectionIds = /* @__PURE__ */ new Set();
110
+ const visitedCollections = /* @__PURE__ */ new WeakSet();
111
+ if (note.id != null) visitedObjectIds.add(note.id.href);
112
+ visitedObjects.add(note);
113
+ const ancestors = [];
114
+ for await (const item of getReplyAncestors(context, note, options, budget, {
115
+ depth: 1,
116
+ visitedObjectIds,
117
+ visitedObjects,
118
+ visitedCollectionIds,
119
+ visitedCollections
120
+ })) {
121
+ ancestors.push({
122
+ object: item.object,
123
+ depth: item.depth
124
+ });
125
+ yield item;
126
+ }
127
+ for (const ancestor of ancestors.toReversed()) yield* getReplyDescendants(context, ancestor.object, options, budget, {
128
+ depth: ancestor.depth + 1,
129
+ visitedObjectIds,
130
+ visitedObjects,
131
+ visitedCollectionIds,
132
+ visitedCollections
133
+ });
134
+ yield* getReplyDescendants(context, note, options, budget, {
135
+ depth: 1,
136
+ visitedObjectIds,
137
+ visitedObjects,
138
+ visitedCollectionIds,
139
+ visitedCollections
140
+ });
141
+ }
142
+ async function* getReplyAncestors(context, object, options, budget, traversal) {
143
+ if (traversal.depth > (options.maxDepth ?? DEFAULT_MAX_DEPTH)) return;
144
+ for await (const target of getReplyTargets(context, object, options, budget)) {
145
+ if (!isContextPostObject(target)) continue;
146
+ if (!visitReplyTreeObject(target, traversal)) continue;
147
+ yield {
148
+ object: target,
149
+ strategy: "reply-tree",
150
+ origin: "in-reply-to",
151
+ depth: traversal.depth
152
+ };
153
+ yield* getReplyAncestors(context, target, options, budget, {
154
+ depth: traversal.depth + 1,
155
+ visitedObjectIds: traversal.visitedObjectIds,
156
+ visitedObjects: traversal.visitedObjects,
157
+ visitedCollectionIds: traversal.visitedCollectionIds,
158
+ visitedCollections: traversal.visitedCollections
159
+ });
160
+ }
161
+ }
162
+ async function* getReplyDescendants(context, object, options, budget, traversal) {
163
+ if (traversal.depth > (options.maxDepth ?? DEFAULT_MAX_DEPTH)) return;
164
+ const repliesId = object.repliesId;
165
+ if (repliesId != null && traversal.visitedCollectionIds.has(repliesId.href)) return;
166
+ const replies = await getRepliesCollection(context, object, options, budget);
167
+ if (replies == null) return;
168
+ const unvisited = visitReplyTreeCollection(replies, traversal);
169
+ if (repliesId != null) traversal.visitedCollectionIds.add(repliesId.href);
170
+ if (!unvisited) return;
171
+ for await (const reply of getCollectionItems(context, replies, options, budget, traversal.visitedObjectIds)) {
172
+ if (!isContextPostObject(reply)) continue;
173
+ if (!visitReplyTreeObject(reply, traversal)) continue;
174
+ yield {
175
+ object: reply,
176
+ strategy: "reply-tree",
177
+ origin: "replies",
178
+ depth: traversal.depth
179
+ };
180
+ yield* getReplyDescendants(context, reply, options, budget, {
181
+ depth: traversal.depth + 1,
182
+ visitedObjectIds: traversal.visitedObjectIds,
183
+ visitedObjects: traversal.visitedObjects,
184
+ visitedCollectionIds: traversal.visitedCollectionIds,
185
+ visitedCollections: traversal.visitedCollections
186
+ });
187
+ }
188
+ }
189
+ async function* getReplyTargets(context, object, options, budget) {
190
+ try {
191
+ yield* object.getReplyTargets({
192
+ documentLoader: async (url) => {
193
+ return await loadCollectionItemDocument(context, url, options, budget);
194
+ },
195
+ crossOrigin: "trust"
196
+ });
197
+ } catch (error) {
198
+ if (error instanceof MaxRequestsExceeded) throw error;
199
+ budget.signal?.throwIfAborted();
200
+ }
201
+ }
202
+ async function getRepliesCollection(context, object, options, budget) {
203
+ try {
204
+ return await object.getReplies({
205
+ documentLoader: async (url) => {
206
+ return await loadCollectionItemDocument(context, url, options, budget);
207
+ },
208
+ crossOrigin: "trust"
209
+ });
210
+ } catch (error) {
211
+ if (error instanceof MaxRequestsExceeded) throw error;
212
+ budget.signal?.throwIfAborted();
213
+ return null;
214
+ }
215
+ }
216
+ function visitReplyTreeObject(object, traversal) {
217
+ if (object.id != null) {
218
+ if (traversal.visitedObjectIds.has(object.id.href)) return false;
219
+ traversal.visitedObjectIds.add(object.id.href);
220
+ } else if (traversal.visitedObjects.has(object)) return false;
221
+ traversal.visitedObjects.add(object);
222
+ return true;
223
+ }
224
+ function visitReplyTreeCollection(collection, traversal) {
225
+ if (collection.id != null) return visitReplyTreeCollectionId(collection.id, traversal);
226
+ else if (traversal.visitedCollections.has(collection)) return false;
227
+ traversal.visitedCollections.add(collection);
228
+ return true;
229
+ }
230
+ function visitReplyTreeCollectionId(id, traversal) {
231
+ if (traversal.visitedCollectionIds.has(id.href)) return false;
232
+ traversal.visitedCollectionIds.add(id.href);
233
+ return true;
234
+ }
235
+ async function* getContextBackfillItems(context, object, strategy, options, budget) {
236
+ if (strategy === "context-objects" && isContextPostObject(object)) yield {
237
+ object,
238
+ strategy
239
+ };
240
+ else if (strategy === "context-activities") {
241
+ const activityObject = await getCreateActivityObject(context, object, options, budget);
242
+ if (activityObject != null && isContextPostObject(activityObject)) yield {
243
+ object: activityObject,
244
+ strategy
245
+ };
246
+ } else if (strategy === "context-auto") {
247
+ if (object instanceof _fedify_vocab.Activity) {
248
+ const activityObject = await getCreateActivityObject(context, object, options, budget);
249
+ if (activityObject != null && isContextPostObject(activityObject)) yield {
250
+ object: activityObject,
251
+ strategy
252
+ };
253
+ } else if (isContextPostObject(object)) yield {
254
+ object,
255
+ strategy
256
+ };
257
+ }
258
+ }
259
+ async function* getCollectionItems(context, collection, options, budget, skipIds) {
260
+ yield* collection.getItems({
261
+ documentLoader: async (url) => {
262
+ return await loadCollectionItemDocument(context, url, options, budget, skipIds);
263
+ },
264
+ crossOrigin: "trust"
265
+ });
266
+ }
267
+ async function getCreateActivityObject(context, object, options, budget) {
268
+ if (!(object instanceof _fedify_vocab.Create)) return null;
269
+ try {
270
+ return await object.getObject({
271
+ documentLoader: async (url) => {
272
+ return await loadCollectionItemDocument(context, url, options, budget);
273
+ },
274
+ crossOrigin: "trust"
275
+ });
276
+ } catch (error) {
277
+ if (error instanceof MaxRequestsExceeded) throw error;
278
+ budget.signal?.throwIfAborted();
279
+ return null;
280
+ }
281
+ }
282
+ async function loadCollectionItemDocument(context, url, options, budget, skipIds) {
283
+ let object;
284
+ try {
285
+ const iri = new URL(url);
286
+ if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
287
+ object = await loadObject(context, iri, options, budget, true);
288
+ } catch (error) {
289
+ if (error instanceof MaxRequestsExceeded) throw error;
290
+ budget.signal?.throwIfAborted();
291
+ return skippedCollectionItemDocument(url);
292
+ }
293
+ if (object == null) return skippedCollectionItemDocument(url);
294
+ return {
295
+ contextUrl: null,
296
+ documentUrl: url,
297
+ document: await object.toJsonLd()
298
+ };
299
+ }
300
+ function skippedCollectionItemDocument(url) {
301
+ return {
302
+ contextUrl: null,
303
+ documentUrl: url,
304
+ document: {
305
+ "@context": "https://www.w3.org/ns/activitystreams",
306
+ type: "Activity"
307
+ }
308
+ };
309
+ }
310
+ async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
311
+ budget.signal?.throwIfAborted();
312
+ const cacheKey = iri.href;
313
+ const cached = budget.documents.get(cacheKey);
314
+ if (cached != null) return await cached;
315
+ if (options.maxRequests != null && budget.requestCount >= options.maxRequests) {
316
+ if (throwOnBudgetExceeded) throw new MaxRequestsExceeded();
317
+ return null;
318
+ }
319
+ await waitForInterval(options, budget);
320
+ budget.signal?.throwIfAborted();
321
+ budget.requestCount++;
322
+ const document = context.documentLoader(iri, { signal: budget.signal });
323
+ budget.documents.set(cacheKey, document);
324
+ try {
325
+ return await document;
326
+ } catch (error) {
327
+ if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
328
+ throw error;
329
+ }
330
+ }
331
+ async function waitForInterval(options, budget) {
332
+ if (options.interval == null) return;
333
+ const milliseconds = durationToMilliseconds(typeof options.interval === "function" ? options.interval(budget.requestCount) : options.interval);
334
+ if (milliseconds <= 0) return;
335
+ await new Promise((resolve, reject) => {
336
+ if (budget.signal?.aborted) {
337
+ reject(budget.signal.reason);
338
+ return;
339
+ }
340
+ const timeout = setTimeout(() => {
341
+ budget.signal?.removeEventListener("abort", onAbort);
342
+ resolve();
343
+ }, milliseconds);
344
+ const onAbort = () => {
345
+ clearTimeout(timeout);
346
+ reject(budget.signal?.reason);
347
+ };
348
+ budget.signal?.addEventListener("abort", onAbort, { once: true });
349
+ });
350
+ }
351
+ function durationToMilliseconds(duration) {
352
+ if (typeof duration === "string") {
353
+ 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.");
354
+ return Temporal.Duration.from(duration).total({ unit: "milliseconds" });
355
+ }
356
+ 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;
357
+ }
358
+ function isCollection(object) {
359
+ return object instanceof _fedify_vocab.Collection || object instanceof _fedify_vocab.OrderedCollection || object instanceof _fedify_vocab.CollectionPage || object instanceof _fedify_vocab.OrderedCollectionPage;
360
+ }
361
+ function isContextPostObject(object) {
362
+ return object instanceof _fedify_vocab.Object && !(object instanceof _fedify_vocab.Activity) && !isCollection(object);
363
+ }
364
+ //#endregion
365
+ exports.MaxRequestsExceeded = MaxRequestsExceeded;
366
+ exports.backfill = backfill;
package/dist/mod.d.cts ADDED
@@ -0,0 +1,157 @@
1
+ import { Object as Object$1 } from "@fedify/vocab";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Backfill traversal strategy used to discover the returned object.
6
+ *
7
+ * - `"context-objects"` yields post-like objects directly from the context
8
+ * collection.
9
+ * - `"context-activities"` yields objects extracted from supported `Create`
10
+ * activities in the context collection.
11
+ * - `"context-auto"` classifies context collection items automatically,
12
+ * handling direct post-like objects and supported `Create` activities.
13
+ * If included, it absorbs other context collection strategies.
14
+ * - `"reply-tree"` walks the reply graph through `inReplyTo` ancestors and
15
+ * `replies` descendants, yielding discovered post-like objects.
16
+ *
17
+ * @since 2.3.0
18
+ */
19
+ type BackfillStrategy = "context-objects" | "context-activities" | "context-auto" | "reply-tree";
20
+ /**
21
+ * Source relation that produced a backfilled object.
22
+ *
23
+ * @since 2.3.0
24
+ */
25
+ type BackfillOrigin = "collection" | "in-reply-to" | "replies";
26
+ /**
27
+ * Options passed to {@link BackfillDocumentLoader}.
28
+ *
29
+ * @since 2.3.0
30
+ */
31
+ interface BackfillDocumentLoaderOptions {
32
+ /**
33
+ * Cancellation signal for the current dereference operation.
34
+ */
35
+ readonly signal?: AbortSignal;
36
+ }
37
+ /**
38
+ * Dereferences an ActivityPub object or collection IRI.
39
+ *
40
+ * @since 2.3.0
41
+ */
42
+ type BackfillDocumentLoader = (iri: URL, options?: BackfillDocumentLoaderOptions) => Promise<Object$1 | null>;
43
+ /**
44
+ * Dependencies used by backfill traversal.
45
+ *
46
+ * @since 2.3.0
47
+ */
48
+ interface BackfillContext {
49
+ /**
50
+ * Dereferences context collections, collection item IRIs, reply targets,
51
+ * and replies collections.
52
+ */
53
+ readonly documentLoader: BackfillDocumentLoader;
54
+ }
55
+ /**
56
+ * Controls backfill traversal.
57
+ *
58
+ * @since 2.3.0
59
+ */
60
+ interface BackfillOptions<TObject extends Object$1 = Object$1> {
61
+ /**
62
+ * Backfill strategies to run.
63
+ *
64
+ * Strategies run in order and share request, item, abort, and deduplication
65
+ * state. If multiple strategies discover the same object ID, the first
66
+ * strategy keeps its {@link BackfillItem} metadata.
67
+ *
68
+ * Defaults to `["context-auto"]`.
69
+ * If `"context-auto"` is included, it absorbs other context collection
70
+ * strategies.
71
+ *
72
+ * @since 2.3.0
73
+ */
74
+ readonly strategies?: readonly BackfillStrategy[];
75
+ /**
76
+ * Maximum number of items to yield. Skipped duplicates do not count.
77
+ */
78
+ readonly maxItems?: number;
79
+ /**
80
+ * Maximum reply-tree traversal depth.
81
+ *
82
+ * Immediate `inReplyTo` targets and direct `replies` collection items have
83
+ * depth 1. Their parents or replies have depth 2, and so on. Context
84
+ * collection items are depth 0 and are not limited by this option.
85
+ *
86
+ * Defaults to 10.
87
+ */
88
+ readonly maxDepth?: number;
89
+ /**
90
+ * Maximum number of calls to {@link BackfillContext.documentLoader}.
91
+ *
92
+ * Dereferencing the note context, collection item IRIs, reply target IRIs,
93
+ * replies collection IRIs, and future page IRIs all count as requests across
94
+ * all strategies. Embedded objects and collections do not count.
95
+ */
96
+ readonly maxRequests?: number;
97
+ /**
98
+ * Delay between `documentLoader` requests.
99
+ *
100
+ * When a callback is provided, `iteration` is the zero-based request index.
101
+ */
102
+ readonly interval?: Temporal.DurationLike | string | ((iteration: number) => Temporal.DurationLike | string);
103
+ /**
104
+ * Cancels traversal before requests and before yields.
105
+ */
106
+ readonly signal?: AbortSignal;
107
+ }
108
+ /**
109
+ * A single object discovered by backfill traversal.
110
+ *
111
+ * @since 2.3.0
112
+ */
113
+ interface BackfillItem<TObject extends Object$1 = Object$1> {
114
+ /**
115
+ * The discovered ActivityPub object.
116
+ */
117
+ readonly object: TObject;
118
+ /**
119
+ * The object's ActivityPub ID, when present.
120
+ */
121
+ readonly id?: URL;
122
+ /**
123
+ * The traversal strategy that produced this item.
124
+ */
125
+ readonly strategy: BackfillStrategy;
126
+ /**
127
+ * The source relation that produced this item.
128
+ */
129
+ readonly origin: BackfillOrigin;
130
+ /**
131
+ * Traversal depth.
132
+ *
133
+ * Direct context collection items are depth 0. Reply-tree items use depth
134
+ * 1 for immediate `inReplyTo` targets and direct `replies` collection items,
135
+ * depth 2 for the next level, and so on.
136
+ */
137
+ readonly depth?: number;
138
+ }
139
+ //#endregion
140
+ //#region src/backfill.d.ts
141
+ /**
142
+ * Thrown when backfill traversal exceeds the configured request budget.
143
+ *
144
+ * @since 2.3.0
145
+ */
146
+ declare class MaxRequestsExceeded extends Error {}
147
+ /**
148
+ * Backfills post-like objects related to a seed object.
149
+ *
150
+ * The seed object is not yielded by default, but its ID is treated as already
151
+ * seen so it will not be yielded again if the collection contains it.
152
+ *
153
+ * @since 2.3.0
154
+ */
155
+ declare function backfill<TObject extends Object$1 = Object$1>(context: BackfillContext, note: TObject, options?: BackfillOptions<TObject>): AsyncGenerator<BackfillItem<TObject>, void, void>;
156
+ //#endregion
157
+ export { type BackfillContext, type BackfillDocumentLoader, type BackfillDocumentLoaderOptions, type BackfillItem, type BackfillOptions, type BackfillOrigin, type BackfillStrategy, MaxRequestsExceeded, backfill };
package/dist/mod.d.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Object as Object$1 } from "@fedify/vocab";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Backfill traversal strategy used to discover the returned object.
6
+ *
7
+ * - `"context-objects"` yields post-like objects directly from the context
8
+ * collection.
9
+ * - `"context-activities"` yields objects extracted from supported `Create`
10
+ * activities in the context collection.
11
+ * - `"context-auto"` classifies context collection items automatically,
12
+ * handling direct post-like objects and supported `Create` activities.
13
+ * If included, it absorbs other context collection strategies.
14
+ * - `"reply-tree"` walks the reply graph through `inReplyTo` ancestors and
15
+ * `replies` descendants, yielding discovered post-like objects.
16
+ *
17
+ * @since 2.3.0
18
+ */
19
+ type BackfillStrategy = "context-objects" | "context-activities" | "context-auto" | "reply-tree";
20
+ /**
21
+ * Source relation that produced a backfilled object.
22
+ *
23
+ * @since 2.3.0
24
+ */
25
+ type BackfillOrigin = "collection" | "in-reply-to" | "replies";
26
+ /**
27
+ * Options passed to {@link BackfillDocumentLoader}.
28
+ *
29
+ * @since 2.3.0
30
+ */
31
+ interface BackfillDocumentLoaderOptions {
32
+ /**
33
+ * Cancellation signal for the current dereference operation.
34
+ */
35
+ readonly signal?: AbortSignal;
36
+ }
37
+ /**
38
+ * Dereferences an ActivityPub object or collection IRI.
39
+ *
40
+ * @since 2.3.0
41
+ */
42
+ type BackfillDocumentLoader = (iri: URL, options?: BackfillDocumentLoaderOptions) => Promise<Object$1 | null>;
43
+ /**
44
+ * Dependencies used by backfill traversal.
45
+ *
46
+ * @since 2.3.0
47
+ */
48
+ interface BackfillContext {
49
+ /**
50
+ * Dereferences context collections, collection item IRIs, reply targets,
51
+ * and replies collections.
52
+ */
53
+ readonly documentLoader: BackfillDocumentLoader;
54
+ }
55
+ /**
56
+ * Controls backfill traversal.
57
+ *
58
+ * @since 2.3.0
59
+ */
60
+ interface BackfillOptions<TObject extends Object$1 = Object$1> {
61
+ /**
62
+ * Backfill strategies to run.
63
+ *
64
+ * Strategies run in order and share request, item, abort, and deduplication
65
+ * state. If multiple strategies discover the same object ID, the first
66
+ * strategy keeps its {@link BackfillItem} metadata.
67
+ *
68
+ * Defaults to `["context-auto"]`.
69
+ * If `"context-auto"` is included, it absorbs other context collection
70
+ * strategies.
71
+ *
72
+ * @since 2.3.0
73
+ */
74
+ readonly strategies?: readonly BackfillStrategy[];
75
+ /**
76
+ * Maximum number of items to yield. Skipped duplicates do not count.
77
+ */
78
+ readonly maxItems?: number;
79
+ /**
80
+ * Maximum reply-tree traversal depth.
81
+ *
82
+ * Immediate `inReplyTo` targets and direct `replies` collection items have
83
+ * depth 1. Their parents or replies have depth 2, and so on. Context
84
+ * collection items are depth 0 and are not limited by this option.
85
+ *
86
+ * Defaults to 10.
87
+ */
88
+ readonly maxDepth?: number;
89
+ /**
90
+ * Maximum number of calls to {@link BackfillContext.documentLoader}.
91
+ *
92
+ * Dereferencing the note context, collection item IRIs, reply target IRIs,
93
+ * replies collection IRIs, and future page IRIs all count as requests across
94
+ * all strategies. Embedded objects and collections do not count.
95
+ */
96
+ readonly maxRequests?: number;
97
+ /**
98
+ * Delay between `documentLoader` requests.
99
+ *
100
+ * When a callback is provided, `iteration` is the zero-based request index.
101
+ */
102
+ readonly interval?: Temporal.DurationLike | string | ((iteration: number) => Temporal.DurationLike | string);
103
+ /**
104
+ * Cancels traversal before requests and before yields.
105
+ */
106
+ readonly signal?: AbortSignal;
107
+ }
108
+ /**
109
+ * A single object discovered by backfill traversal.
110
+ *
111
+ * @since 2.3.0
112
+ */
113
+ interface BackfillItem<TObject extends Object$1 = Object$1> {
114
+ /**
115
+ * The discovered ActivityPub object.
116
+ */
117
+ readonly object: TObject;
118
+ /**
119
+ * The object's ActivityPub ID, when present.
120
+ */
121
+ readonly id?: URL;
122
+ /**
123
+ * The traversal strategy that produced this item.
124
+ */
125
+ readonly strategy: BackfillStrategy;
126
+ /**
127
+ * The source relation that produced this item.
128
+ */
129
+ readonly origin: BackfillOrigin;
130
+ /**
131
+ * Traversal depth.
132
+ *
133
+ * Direct context collection items are depth 0. Reply-tree items use depth
134
+ * 1 for immediate `inReplyTo` targets and direct `replies` collection items,
135
+ * depth 2 for the next level, and so on.
136
+ */
137
+ readonly depth?: number;
138
+ }
139
+ //#endregion
140
+ //#region src/backfill.d.ts
141
+ /**
142
+ * Thrown when backfill traversal exceeds the configured request budget.
143
+ *
144
+ * @since 2.3.0
145
+ */
146
+ declare class MaxRequestsExceeded extends Error {}
147
+ /**
148
+ * Backfills post-like objects related to a seed object.
149
+ *
150
+ * The seed object is not yielded by default, but its ID is treated as already
151
+ * seen so it will not be yielded again if the collection contains it.
152
+ *
153
+ * @since 2.3.0
154
+ */
155
+ declare function backfill<TObject extends Object$1 = Object$1>(context: BackfillContext, note: TObject, options?: BackfillOptions<TObject>): AsyncGenerator<BackfillItem<TObject>, void, void>;
156
+ //#endregion
157
+ export { type BackfillContext, type BackfillDocumentLoader, type BackfillDocumentLoaderOptions, type BackfillItem, type BackfillOptions, type BackfillOrigin, type BackfillStrategy, MaxRequestsExceeded, backfill };