@fedify/backfill 2.3.11-dev.2296 → 2.3.12-dev.2341

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,6 +65,14 @@ for await (
65
65
  The seed object itself is not yielded. If it appears in the discovered
66
66
  collection, it is skipped by ID.
67
67
 
68
+ Context and `replies` collections support inline items and pagination through
69
+ `first` and successive `next` links. A collection page used as the starting
70
+ collection also follows `next`. Pages are loaded as needed, and traversal
71
+ stops on repeated page links. Pagination does not increase reply-tree depth.
72
+ If a page cannot be loaded, items already yielded are retained and later
73
+ configured strategies can continue unless cancellation or traversal limits
74
+ stop them.
75
+
68
76
  Configured strategies run in order. They share `maxItems`, `maxRequests`,
69
77
  abort state, and object ID deduplication; if two strategies discover the same
70
78
  object, the first strategy keeps its `BackfillItem` metadata.
@@ -133,7 +141,7 @@ All configured strategies share the same traversal controls:
133
141
  - `maxItems` limits the number of yielded objects. Skipped duplicates do
134
142
  not count.
135
143
  - `maxRequests` limits calls to `documentLoader`. Embedded objects and
136
- collections do not count.
144
+ collections, including embedded pages, do not count.
137
145
  - `maxDepth` limits reply-tree traversal and defaults to 10. It does not
138
146
  limit context collection items.
139
147
  - `interval` adds a delay between loader requests. Its callback receives
@@ -281,12 +281,49 @@ async function* getContextBackfillItems(context, object, strategy, options, budg
281
281
  }
282
282
  }
283
283
  async function* getCollectionItems(context, collection, options, budget, skipIds) {
284
- yield* collection.getItems({
284
+ const itemOptions = {
285
285
  documentLoader: async (url) => {
286
286
  return await loadCollectionItemDocument(context, url, options, budget, skipIds);
287
287
  },
288
288
  crossOrigin: "trust"
289
- });
289
+ };
290
+ const pageOptions = {
291
+ documentLoader: async (url) => {
292
+ const object = await loadObject(context, new URL(url), options, budget, true);
293
+ if (object == null) throw new Error(`Collection page not found: ${url}`);
294
+ return {
295
+ contextUrl: null,
296
+ documentUrl: url,
297
+ document: await object.toJsonLd()
298
+ };
299
+ },
300
+ crossOrigin: "trust"
301
+ };
302
+ const visitedIds = /* @__PURE__ */ new Set();
303
+ const visitedPages = /* @__PURE__ */ new WeakSet();
304
+ let current = collection;
305
+ while (current != null) {
306
+ budget.signal?.throwIfAborted();
307
+ if (visitedPages.has(current)) return;
308
+ if (current.id != null) {
309
+ if (visitedIds.has(current.id.href)) return;
310
+ visitedIds.add(current.id.href);
311
+ }
312
+ visitedPages.add(current);
313
+ yield* current.getItems(itemOptions);
314
+ const page = current instanceof _fedify_vocab.CollectionPage || current instanceof _fedify_vocab.OrderedCollectionPage ? current : null;
315
+ const pageId = page == null ? current.firstId : page.nextId;
316
+ if (pageId != null && visitedIds.has(pageId.href)) return;
317
+ try {
318
+ budget.signal?.throwIfAborted();
319
+ current = page == null ? await current.getFirst(pageOptions) : await page.getNext(pageOptions);
320
+ } catch (error) {
321
+ if (error instanceof MaxRequestsExceeded) throw error;
322
+ budget.signal?.throwIfAborted();
323
+ return;
324
+ }
325
+ if (pageId != null && pageId.href !== current?.id?.href) visitedIds.add(pageId.href);
326
+ }
290
327
  }
291
328
  async function getCreateActivityObject(context, object, options, budget) {
292
329
  if (!(object instanceof _fedify_vocab.Create)) return null;
@@ -1560,4 +1597,164 @@ async function collect(context, note, options = {}) {
1560
1597
  (0, node_assert_strict.deepStrictEqual)(iterations, [0, 1]);
1561
1598
  });
1562
1599
  });
1600
+ (0, node_test.describe)("collection pagination", () => {
1601
+ const iri = (name) => new URL(`https://example.com/${name}`);
1602
+ for (const ordered of [false, true]) for (const replies of [false, true]) for (const embedded of [false, true]) (0, node_test.default)(`${ordered ? "ordered" : "unordered"} ${replies ? "replies" : "context"} with ${embedded ? "embedded" : "linked"} pages`, async () => {
1603
+ const CollectionType = ordered ? _fedify_vocab.OrderedCollection : _fedify_vocab.Collection;
1604
+ const PageType = ordered ? _fedify_vocab.OrderedCollectionPage : _fedify_vocab.CollectionPage;
1605
+ const a = new _fedify_vocab.Note({ id: iri("a") });
1606
+ const b = new _fedify_vocab.Note({ id: iri("b") });
1607
+ const second = new PageType({
1608
+ id: iri("page-2"),
1609
+ items: [b]
1610
+ });
1611
+ const first = new PageType({
1612
+ id: iri("page-1"),
1613
+ items: [a],
1614
+ next: embedded ? second : second.id
1615
+ });
1616
+ const collection = new CollectionType({
1617
+ id: iri("thread"),
1618
+ first: embedded ? first : first.id
1619
+ });
1620
+ const seed = new _fedify_vocab.Note({
1621
+ id: iri("seed"),
1622
+ contexts: replies ? [] : [iri("thread")],
1623
+ replies: replies ? collection : null
1624
+ });
1625
+ const documents = new Map([
1626
+ [iri("thread").href, collection],
1627
+ [iri("page-1").href, first],
1628
+ [iri("page-2").href, second]
1629
+ ]);
1630
+ const calls = [];
1631
+ const items = await collect({ documentLoader: (url) => {
1632
+ calls.push(url.href);
1633
+ return Promise.resolve(documents.get(url.href) ?? null);
1634
+ } }, seed, {
1635
+ strategies: replies ? ["reply-tree"] : ["context-auto"],
1636
+ maxDepth: 1,
1637
+ maxRequests: embedded ? replies ? 0 : 1 : 3
1638
+ });
1639
+ (0, node_assert_strict.deepStrictEqual)(items.map((item) => item.id?.href), [a.id?.href, b.id?.href]);
1640
+ (0, node_assert_strict.deepStrictEqual)(items.map((item) => item.depth), replies ? [1, 1] : [0, 0]);
1641
+ (0, node_assert_strict.strictEqual)(calls.length, embedded ? replies ? 0 : 1 : replies ? 2 : 3);
1642
+ });
1643
+ (0, node_test.default)("starting page follows next and stops before loading a repeated page", async () => {
1644
+ const first = new _fedify_vocab.OrderedCollectionPage({
1645
+ id: iri("page-1"),
1646
+ items: [new _fedify_vocab.Note({ id: iri("a") })],
1647
+ next: iri("page-2")
1648
+ });
1649
+ const second = new _fedify_vocab.OrderedCollectionPage({
1650
+ id: iri("page-2"),
1651
+ items: [new _fedify_vocab.Note({ id: iri("b") })],
1652
+ next: first.id
1653
+ });
1654
+ const calls = [];
1655
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: (url) => {
1656
+ calls.push(url.href);
1657
+ return Promise.resolve(url.href === first.id?.href ? first : second);
1658
+ } }, new _fedify_vocab.Note({ contexts: [first.id] }))).map((item) => item.id?.href), [iri("a").href, iri("b").href]);
1659
+ (0, node_assert_strict.deepStrictEqual)(calls, [first.id?.href, second.id?.href]);
1660
+ });
1661
+ for (const limit of ["maxItems", "maxRequests"]) (0, node_test.default)(`${limit} stops between pages`, async () => {
1662
+ const first = new _fedify_vocab.CollectionPage({
1663
+ id: iri("page-1"),
1664
+ items: [new _fedify_vocab.Note({ id: iri("a") })],
1665
+ next: iri("page-2")
1666
+ });
1667
+ const calls = [];
1668
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: (url) => {
1669
+ calls.push(url.href);
1670
+ return Promise.resolve(first);
1671
+ } }, new _fedify_vocab.Note({
1672
+ contexts: [first.id],
1673
+ replies: new _fedify_vocab.Collection({ items: [new _fedify_vocab.Note({ id: iri("fallback") })] })
1674
+ }), {
1675
+ [limit]: 1,
1676
+ strategies: ["context-auto", "reply-tree"]
1677
+ })).map((item) => item.id?.href), [iri("a").href]);
1678
+ (0, node_assert_strict.deepStrictEqual)(calls, [first.id?.href]);
1679
+ });
1680
+ for (const failure of [
1681
+ "missing",
1682
+ "throw",
1683
+ "invalid"
1684
+ ]) (0, node_test.default)(`${failure} page retains items and allows the next strategy`, async () => {
1685
+ const seed = new _fedify_vocab.Note({
1686
+ contexts: [iri("thread")],
1687
+ replies: new _fedify_vocab.Collection({ items: [new _fedify_vocab.Note({ id: iri("reply") })] })
1688
+ });
1689
+ const first = new _fedify_vocab.CollectionPage({
1690
+ items: [new _fedify_vocab.Note({ id: iri("a") })],
1691
+ next: iri("broken")
1692
+ });
1693
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: (url) => {
1694
+ if (url.href === iri("thread").href) return Promise.resolve(new _fedify_vocab.Collection({ first }));
1695
+ if (failure === "throw") throw new Error("page failed");
1696
+ return Promise.resolve(failure === "missing" ? null : new _fedify_vocab.Note({}));
1697
+ } }, seed, { strategies: ["context-auto", "reply-tree"] })).map((item) => item.id?.href), [iri("a").href, iri("reply").href]);
1698
+ });
1699
+ (0, node_test.default)("inline items precede page items", async () => {
1700
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: () => Promise.resolve(new _fedify_vocab.Collection({
1701
+ items: [new _fedify_vocab.Note({ id: iri("inline") })],
1702
+ first: new _fedify_vocab.CollectionPage({ items: [new _fedify_vocab.Note({ id: iri("paged") })] })
1703
+ })) }, new _fedify_vocab.Note({ contexts: [iri("thread")] }))).map((item) => item.id?.href), [iri("inline").href, iri("paged").href]);
1704
+ });
1705
+ (0, node_test.default)("cancellation during a page load prevents later strategies", async () => {
1706
+ const controller = new AbortController();
1707
+ const reason = /* @__PURE__ */ new Error("cancelled");
1708
+ await (0, node_assert_strict.rejects)(collect({ documentLoader: (url, options) => {
1709
+ (0, node_assert_strict.strictEqual)(options?.signal, controller.signal);
1710
+ if (url.href === iri("thread").href) return Promise.resolve(new _fedify_vocab.Collection({ first: iri("page") }));
1711
+ controller.abort(reason);
1712
+ throw reason;
1713
+ } }, new _fedify_vocab.Note({
1714
+ contexts: [iri("thread")],
1715
+ replies: new _fedify_vocab.Collection({ items: [new _fedify_vocab.Note({ id: iri("fallback") })] })
1716
+ }), {
1717
+ signal: controller.signal,
1718
+ strategies: ["context-auto", "reply-tree"]
1719
+ }), (error) => error === reason);
1720
+ });
1721
+ });
1722
+ (0, node_test.default)("page loads share the cache and interval across strategies", async () => {
1723
+ const iri = (name) => new URL(`https://example.com/${name}`);
1724
+ const collection = new _fedify_vocab.Collection({ first: iri("page") });
1725
+ const page = new _fedify_vocab.CollectionPage({ items: [new _fedify_vocab.Note({ id: iri("a") })] });
1726
+ const seed = new _fedify_vocab.Note({
1727
+ contexts: [iri("thread")],
1728
+ replies: new _fedify_vocab.Collection({ first: iri("page") })
1729
+ });
1730
+ const calls = [];
1731
+ const intervals = [];
1732
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: (url) => {
1733
+ calls.push(url.href);
1734
+ return Promise.resolve(url.href === iri("thread").href ? collection : page);
1735
+ } }, seed, {
1736
+ strategies: ["context-auto", "reply-tree"],
1737
+ maxRequests: 2,
1738
+ interval: (count) => {
1739
+ intervals.push(count);
1740
+ return { milliseconds: 0 };
1741
+ }
1742
+ })).map((item) => item.id?.href), [iri("a").href]);
1743
+ (0, node_assert_strict.deepStrictEqual)(calls, [iri("thread").href, iri("page").href]);
1744
+ (0, node_assert_strict.deepStrictEqual)(intervals, [0, 1]);
1745
+ });
1746
+ (0, node_test.default)("embedded pages without IDs follow next without loader calls", async () => {
1747
+ const a = new _fedify_vocab.Note({ id: new URL("https://example.com/a") });
1748
+ const b = new _fedify_vocab.Note({ id: new URL("https://example.com/b") });
1749
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: () => {
1750
+ throw new Error("embedded pages must not be fetched");
1751
+ } }, new _fedify_vocab.Note({ replies: new _fedify_vocab.Collection({ first: new _fedify_vocab.CollectionPage({
1752
+ items: [a],
1753
+ next: new _fedify_vocab.CollectionPage({ items: [b] })
1754
+ }) }) }), {
1755
+ strategies: ["reply-tree"],
1756
+ maxRequests: 0,
1757
+ maxDepth: 1
1758
+ })).map((item) => item.id?.href), [a.id?.href, b.id?.href]);
1759
+ });
1563
1760
  //#endregion
@@ -258,12 +258,49 @@ async function* getContextBackfillItems(context, object, strategy, options, budg
258
258
  }
259
259
  }
260
260
  async function* getCollectionItems(context, collection, options, budget, skipIds) {
261
- yield* collection.getItems({
261
+ const itemOptions = {
262
262
  documentLoader: async (url) => {
263
263
  return await loadCollectionItemDocument(context, url, options, budget, skipIds);
264
264
  },
265
265
  crossOrigin: "trust"
266
- });
266
+ };
267
+ const pageOptions = {
268
+ documentLoader: async (url) => {
269
+ const object = await loadObject(context, new URL(url), options, budget, true);
270
+ if (object == null) throw new Error(`Collection page not found: ${url}`);
271
+ return {
272
+ contextUrl: null,
273
+ documentUrl: url,
274
+ document: await object.toJsonLd()
275
+ };
276
+ },
277
+ crossOrigin: "trust"
278
+ };
279
+ const visitedIds = /* @__PURE__ */ new Set();
280
+ const visitedPages = /* @__PURE__ */ new WeakSet();
281
+ let current = collection;
282
+ while (current != null) {
283
+ budget.signal?.throwIfAborted();
284
+ if (visitedPages.has(current)) return;
285
+ if (current.id != null) {
286
+ if (visitedIds.has(current.id.href)) return;
287
+ visitedIds.add(current.id.href);
288
+ }
289
+ visitedPages.add(current);
290
+ yield* current.getItems(itemOptions);
291
+ const page = current instanceof CollectionPage || current instanceof OrderedCollectionPage ? current : null;
292
+ const pageId = page == null ? current.firstId : page.nextId;
293
+ if (pageId != null && visitedIds.has(pageId.href)) return;
294
+ try {
295
+ budget.signal?.throwIfAborted();
296
+ current = page == null ? await current.getFirst(pageOptions) : await page.getNext(pageOptions);
297
+ } catch (error) {
298
+ if (error instanceof MaxRequestsExceeded) throw error;
299
+ budget.signal?.throwIfAborted();
300
+ return;
301
+ }
302
+ if (pageId != null && pageId.href !== current?.id?.href) visitedIds.add(pageId.href);
303
+ }
267
304
  }
268
305
  async function getCreateActivityObject(context, object, options, budget) {
269
306
  if (!(object instanceof Create)) return null;
@@ -1537,5 +1574,165 @@ describe("backfill", () => {
1537
1574
  deepStrictEqual(iterations, [0, 1]);
1538
1575
  });
1539
1576
  });
1577
+ describe("collection pagination", () => {
1578
+ const iri = (name) => new URL(`https://example.com/${name}`);
1579
+ for (const ordered of [false, true]) for (const replies of [false, true]) for (const embedded of [false, true]) test(`${ordered ? "ordered" : "unordered"} ${replies ? "replies" : "context"} with ${embedded ? "embedded" : "linked"} pages`, async () => {
1580
+ const CollectionType = ordered ? OrderedCollection : Collection;
1581
+ const PageType = ordered ? OrderedCollectionPage : CollectionPage;
1582
+ const a = new Note({ id: iri("a") });
1583
+ const b = new Note({ id: iri("b") });
1584
+ const second = new PageType({
1585
+ id: iri("page-2"),
1586
+ items: [b]
1587
+ });
1588
+ const first = new PageType({
1589
+ id: iri("page-1"),
1590
+ items: [a],
1591
+ next: embedded ? second : second.id
1592
+ });
1593
+ const collection = new CollectionType({
1594
+ id: iri("thread"),
1595
+ first: embedded ? first : first.id
1596
+ });
1597
+ const seed = new Note({
1598
+ id: iri("seed"),
1599
+ contexts: replies ? [] : [iri("thread")],
1600
+ replies: replies ? collection : null
1601
+ });
1602
+ const documents = new Map([
1603
+ [iri("thread").href, collection],
1604
+ [iri("page-1").href, first],
1605
+ [iri("page-2").href, second]
1606
+ ]);
1607
+ const calls = [];
1608
+ const items = await collect({ documentLoader: (url) => {
1609
+ calls.push(url.href);
1610
+ return Promise.resolve(documents.get(url.href) ?? null);
1611
+ } }, seed, {
1612
+ strategies: replies ? ["reply-tree"] : ["context-auto"],
1613
+ maxDepth: 1,
1614
+ maxRequests: embedded ? replies ? 0 : 1 : 3
1615
+ });
1616
+ deepStrictEqual(items.map((item) => item.id?.href), [a.id?.href, b.id?.href]);
1617
+ deepStrictEqual(items.map((item) => item.depth), replies ? [1, 1] : [0, 0]);
1618
+ strictEqual(calls.length, embedded ? replies ? 0 : 1 : replies ? 2 : 3);
1619
+ });
1620
+ test("starting page follows next and stops before loading a repeated page", async () => {
1621
+ const first = new OrderedCollectionPage({
1622
+ id: iri("page-1"),
1623
+ items: [new Note({ id: iri("a") })],
1624
+ next: iri("page-2")
1625
+ });
1626
+ const second = new OrderedCollectionPage({
1627
+ id: iri("page-2"),
1628
+ items: [new Note({ id: iri("b") })],
1629
+ next: first.id
1630
+ });
1631
+ const calls = [];
1632
+ deepStrictEqual((await collect({ documentLoader: (url) => {
1633
+ calls.push(url.href);
1634
+ return Promise.resolve(url.href === first.id?.href ? first : second);
1635
+ } }, new Note({ contexts: [first.id] }))).map((item) => item.id?.href), [iri("a").href, iri("b").href]);
1636
+ deepStrictEqual(calls, [first.id?.href, second.id?.href]);
1637
+ });
1638
+ for (const limit of ["maxItems", "maxRequests"]) test(`${limit} stops between pages`, async () => {
1639
+ const first = new CollectionPage({
1640
+ id: iri("page-1"),
1641
+ items: [new Note({ id: iri("a") })],
1642
+ next: iri("page-2")
1643
+ });
1644
+ const calls = [];
1645
+ deepStrictEqual((await collect({ documentLoader: (url) => {
1646
+ calls.push(url.href);
1647
+ return Promise.resolve(first);
1648
+ } }, new Note({
1649
+ contexts: [first.id],
1650
+ replies: new Collection({ items: [new Note({ id: iri("fallback") })] })
1651
+ }), {
1652
+ [limit]: 1,
1653
+ strategies: ["context-auto", "reply-tree"]
1654
+ })).map((item) => item.id?.href), [iri("a").href]);
1655
+ deepStrictEqual(calls, [first.id?.href]);
1656
+ });
1657
+ for (const failure of [
1658
+ "missing",
1659
+ "throw",
1660
+ "invalid"
1661
+ ]) test(`${failure} page retains items and allows the next strategy`, async () => {
1662
+ const seed = new Note({
1663
+ contexts: [iri("thread")],
1664
+ replies: new Collection({ items: [new Note({ id: iri("reply") })] })
1665
+ });
1666
+ const first = new CollectionPage({
1667
+ items: [new Note({ id: iri("a") })],
1668
+ next: iri("broken")
1669
+ });
1670
+ deepStrictEqual((await collect({ documentLoader: (url) => {
1671
+ if (url.href === iri("thread").href) return Promise.resolve(new Collection({ first }));
1672
+ if (failure === "throw") throw new Error("page failed");
1673
+ return Promise.resolve(failure === "missing" ? null : new Note({}));
1674
+ } }, seed, { strategies: ["context-auto", "reply-tree"] })).map((item) => item.id?.href), [iri("a").href, iri("reply").href]);
1675
+ });
1676
+ test("inline items precede page items", async () => {
1677
+ deepStrictEqual((await collect({ documentLoader: () => Promise.resolve(new Collection({
1678
+ items: [new Note({ id: iri("inline") })],
1679
+ first: new CollectionPage({ items: [new Note({ id: iri("paged") })] })
1680
+ })) }, new Note({ contexts: [iri("thread")] }))).map((item) => item.id?.href), [iri("inline").href, iri("paged").href]);
1681
+ });
1682
+ test("cancellation during a page load prevents later strategies", async () => {
1683
+ const controller = new AbortController();
1684
+ const reason = /* @__PURE__ */ new Error("cancelled");
1685
+ await rejects(collect({ documentLoader: (url, options) => {
1686
+ strictEqual(options?.signal, controller.signal);
1687
+ if (url.href === iri("thread").href) return Promise.resolve(new Collection({ first: iri("page") }));
1688
+ controller.abort(reason);
1689
+ throw reason;
1690
+ } }, new Note({
1691
+ contexts: [iri("thread")],
1692
+ replies: new Collection({ items: [new Note({ id: iri("fallback") })] })
1693
+ }), {
1694
+ signal: controller.signal,
1695
+ strategies: ["context-auto", "reply-tree"]
1696
+ }), (error) => error === reason);
1697
+ });
1698
+ });
1699
+ test("page loads share the cache and interval across strategies", async () => {
1700
+ const iri = (name) => new URL(`https://example.com/${name}`);
1701
+ const collection = new Collection({ first: iri("page") });
1702
+ const page = new CollectionPage({ items: [new Note({ id: iri("a") })] });
1703
+ const seed = new Note({
1704
+ contexts: [iri("thread")],
1705
+ replies: new Collection({ first: iri("page") })
1706
+ });
1707
+ const calls = [];
1708
+ const intervals = [];
1709
+ deepStrictEqual((await collect({ documentLoader: (url) => {
1710
+ calls.push(url.href);
1711
+ return Promise.resolve(url.href === iri("thread").href ? collection : page);
1712
+ } }, seed, {
1713
+ strategies: ["context-auto", "reply-tree"],
1714
+ maxRequests: 2,
1715
+ interval: (count) => {
1716
+ intervals.push(count);
1717
+ return { milliseconds: 0 };
1718
+ }
1719
+ })).map((item) => item.id?.href), [iri("a").href]);
1720
+ deepStrictEqual(calls, [iri("thread").href, iri("page").href]);
1721
+ deepStrictEqual(intervals, [0, 1]);
1722
+ });
1723
+ test("embedded pages without IDs follow next without loader calls", async () => {
1724
+ const a = new Note({ id: new URL("https://example.com/a") });
1725
+ const b = new Note({ id: new URL("https://example.com/b") });
1726
+ deepStrictEqual((await collect({ documentLoader: () => {
1727
+ throw new Error("embedded pages must not be fetched");
1728
+ } }, new Note({ replies: new Collection({ first: new CollectionPage({
1729
+ items: [a],
1730
+ next: new CollectionPage({ items: [b] })
1731
+ }) }) }), {
1732
+ strategies: ["reply-tree"],
1733
+ maxRequests: 0,
1734
+ maxDepth: 1
1735
+ })).map((item) => item.id?.href), [a.id?.href, b.id?.href]);
1736
+ });
1540
1737
  //#endregion
1541
1738
  export {};
package/dist/mod.cjs CHANGED
@@ -257,12 +257,49 @@ async function* getContextBackfillItems(context, object, strategy, options, budg
257
257
  }
258
258
  }
259
259
  async function* getCollectionItems(context, collection, options, budget, skipIds) {
260
- yield* collection.getItems({
260
+ const itemOptions = {
261
261
  documentLoader: async (url) => {
262
262
  return await loadCollectionItemDocument(context, url, options, budget, skipIds);
263
263
  },
264
264
  crossOrigin: "trust"
265
- });
265
+ };
266
+ const pageOptions = {
267
+ documentLoader: async (url) => {
268
+ const object = await loadObject(context, new URL(url), options, budget, true);
269
+ if (object == null) throw new Error(`Collection page not found: ${url}`);
270
+ return {
271
+ contextUrl: null,
272
+ documentUrl: url,
273
+ document: await object.toJsonLd()
274
+ };
275
+ },
276
+ crossOrigin: "trust"
277
+ };
278
+ const visitedIds = /* @__PURE__ */ new Set();
279
+ const visitedPages = /* @__PURE__ */ new WeakSet();
280
+ let current = collection;
281
+ while (current != null) {
282
+ budget.signal?.throwIfAborted();
283
+ if (visitedPages.has(current)) return;
284
+ if (current.id != null) {
285
+ if (visitedIds.has(current.id.href)) return;
286
+ visitedIds.add(current.id.href);
287
+ }
288
+ visitedPages.add(current);
289
+ yield* current.getItems(itemOptions);
290
+ const page = current instanceof _fedify_vocab.CollectionPage || current instanceof _fedify_vocab.OrderedCollectionPage ? current : null;
291
+ const pageId = page == null ? current.firstId : page.nextId;
292
+ if (pageId != null && visitedIds.has(pageId.href)) return;
293
+ try {
294
+ budget.signal?.throwIfAborted();
295
+ current = page == null ? await current.getFirst(pageOptions) : await page.getNext(pageOptions);
296
+ } catch (error) {
297
+ if (error instanceof MaxRequestsExceeded) throw error;
298
+ budget.signal?.throwIfAborted();
299
+ return;
300
+ }
301
+ if (pageId != null && pageId.href !== current?.id?.href) visitedIds.add(pageId.href);
302
+ }
266
303
  }
267
304
  async function getCreateActivityObject(context, object, options, budget) {
268
305
  if (!(object instanceof _fedify_vocab.Create)) return null;
package/dist/mod.js CHANGED
@@ -256,12 +256,49 @@ async function* getContextBackfillItems(context, object, strategy, options, budg
256
256
  }
257
257
  }
258
258
  async function* getCollectionItems(context, collection, options, budget, skipIds) {
259
- yield* collection.getItems({
259
+ const itemOptions = {
260
260
  documentLoader: async (url) => {
261
261
  return await loadCollectionItemDocument(context, url, options, budget, skipIds);
262
262
  },
263
263
  crossOrigin: "trust"
264
- });
264
+ };
265
+ const pageOptions = {
266
+ documentLoader: async (url) => {
267
+ const object = await loadObject(context, new URL(url), options, budget, true);
268
+ if (object == null) throw new Error(`Collection page not found: ${url}`);
269
+ return {
270
+ contextUrl: null,
271
+ documentUrl: url,
272
+ document: await object.toJsonLd()
273
+ };
274
+ },
275
+ crossOrigin: "trust"
276
+ };
277
+ const visitedIds = /* @__PURE__ */ new Set();
278
+ const visitedPages = /* @__PURE__ */ new WeakSet();
279
+ let current = collection;
280
+ while (current != null) {
281
+ budget.signal?.throwIfAborted();
282
+ if (visitedPages.has(current)) return;
283
+ if (current.id != null) {
284
+ if (visitedIds.has(current.id.href)) return;
285
+ visitedIds.add(current.id.href);
286
+ }
287
+ visitedPages.add(current);
288
+ yield* current.getItems(itemOptions);
289
+ const page = current instanceof CollectionPage || current instanceof OrderedCollectionPage ? current : null;
290
+ const pageId = page == null ? current.firstId : page.nextId;
291
+ if (pageId != null && visitedIds.has(pageId.href)) return;
292
+ try {
293
+ budget.signal?.throwIfAborted();
294
+ current = page == null ? await current.getFirst(pageOptions) : await page.getNext(pageOptions);
295
+ } catch (error) {
296
+ if (error instanceof MaxRequestsExceeded) throw error;
297
+ budget.signal?.throwIfAborted();
298
+ return;
299
+ }
300
+ if (pageId != null && pageId.href !== current?.id?.href) visitedIds.add(pageId.href);
301
+ }
265
302
  }
266
303
  async function getCreateActivityObject(context, object, options, budget) {
267
304
  if (!(object instanceof Create)) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/backfill",
3
- "version": "2.3.11-dev.2296+29f4a6de",
3
+ "version": "2.3.12-dev.2341+eb343ebf",
4
4
  "description": "ActivityPub backfill support for Fedify",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -49,7 +49,7 @@
49
49
  "README.md"
50
50
  ],
51
51
  "dependencies": {
52
- "@fedify/vocab": "2.3.11-dev.2296+29f4a6de"
52
+ "@fedify/vocab": "2.3.12-dev.2341+eb343ebf"
53
53
  },
54
54
  "devDependencies": {
55
55
  "tsdown": "^0.22.0",