@fedify/backfill 2.3.12-dev.2341 → 2.3.12-dev.2346

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
@@ -153,7 +153,11 @@ An `interval` string requires the global `Temporal` API or a polyfill.
153
153
 
154
154
  If the seed has no context, or its context resolves to a non-collection,
155
155
  context strategies yield nothing. Loader failures are skipped unless
156
- traversal is aborted.
156
+ traversal is aborted. If the context collection cannot be loaded, later
157
+ configured strategies can still run. Failed loads consume a request, and
158
+ later strategies can use embedded data even when the request budget is
159
+ exhausted. Configuration errors, such as an invalid `interval`, still
160
+ propagate.
157
161
 
158
162
  Dereferenced documents are cached in memory for one `backfill()` traversal.
159
163
  Applications that need persistent or shared caching can provide it through
@@ -114,7 +114,7 @@ function currentContextGroupHasAuto(strategies) {
114
114
  async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
115
115
  const contextId = note.contextIds[0];
116
116
  if (contextId == null) return;
117
- const collection = await loadObject(context, contextId, options, budget);
117
+ const collection = await loadObject(context, contextId, options, budget, { skipLoaderErrors: true });
118
118
  if (!isCollection(collection)) return;
119
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
120
  object: item.object,
@@ -289,7 +289,7 @@ async function* getCollectionItems(context, collection, options, budget, skipIds
289
289
  };
290
290
  const pageOptions = {
291
291
  documentLoader: async (url) => {
292
- const object = await loadObject(context, new URL(url), options, budget, true);
292
+ const object = await loadObject(context, new URL(url), options, budget, { throwOnBudgetExceeded: true });
293
293
  if (object == null) throw new Error(`Collection page not found: ${url}`);
294
294
  return {
295
295
  contextUrl: null,
@@ -345,7 +345,7 @@ async function loadCollectionItemDocument(context, url, options, budget, skipIds
345
345
  try {
346
346
  const iri = new URL(url);
347
347
  if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
348
- object = await loadObject(context, iri, options, budget, true);
348
+ object = await loadObject(context, iri, options, budget, { throwOnBudgetExceeded: true });
349
349
  } catch (error) {
350
350
  if (error instanceof MaxRequestsExceeded) throw error;
351
351
  budget.signal?.throwIfAborted();
@@ -368,7 +368,7 @@ function skippedCollectionItemDocument(url) {
368
368
  }
369
369
  };
370
370
  }
371
- async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
371
+ async function loadObject(context, iri, options, budget, { throwOnBudgetExceeded = false, skipLoaderErrors = false } = {}) {
372
372
  budget.signal?.throwIfAborted();
373
373
  const cacheKey = iri.href;
374
374
  const cached = budget.documents.get(cacheKey);
@@ -380,12 +380,17 @@ async function loadObject(context, iri, options, budget, throwOnBudgetExceeded =
380
380
  await waitForInterval(options, budget);
381
381
  budget.signal?.throwIfAborted();
382
382
  budget.requestCount++;
383
- const document = context.documentLoader(iri, { signal: budget.signal });
384
- budget.documents.set(cacheKey, document);
383
+ let document;
385
384
  try {
385
+ document = context.documentLoader(iri, { signal: budget.signal });
386
+ budget.documents.set(cacheKey, document);
386
387
  return await document;
387
388
  } catch (error) {
388
389
  if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
390
+ if (skipLoaderErrors) {
391
+ budget.signal?.throwIfAborted();
392
+ return null;
393
+ }
389
394
  throw error;
390
395
  }
391
396
  }
@@ -1757,4 +1762,132 @@ async function collect(context, note, options = {}) {
1757
1762
  maxDepth: 1
1758
1763
  })).map((item) => item.id?.href), [a.id?.href, b.id?.href]);
1759
1764
  });
1765
+ (0, node_test.describe)("context loader failures", () => {
1766
+ const contextId = new URL("https://example.com/thread");
1767
+ const parent = new _fedify_vocab.Note({ id: new URL("https://example.com/parent") });
1768
+ for (const synchronous of [false, true]) {
1769
+ const failure = synchronous ? "synchronous throw" : "promise rejection";
1770
+ const fail = () => {
1771
+ const error = /* @__PURE__ */ new Error("Context collection unavailable");
1772
+ if (synchronous) throw error;
1773
+ return Promise.reject(error);
1774
+ };
1775
+ (0, node_test.default)(`${failure} allows reply-tree to yield an embedded parent`, async () => {
1776
+ const seed = new _fedify_vocab.Note({
1777
+ contexts: [contextId],
1778
+ replyTarget: parent
1779
+ });
1780
+ let requests = 0;
1781
+ const items = await collect({ documentLoader: () => {
1782
+ requests++;
1783
+ return fail();
1784
+ } }, seed, {
1785
+ strategies: ["context-auto", "reply-tree"],
1786
+ maxRequests: 1
1787
+ });
1788
+ (0, node_assert_strict.deepStrictEqual)(items.map((item) => item.object), [parent]);
1789
+ (0, node_assert_strict.strictEqual)(items[0].strategy, "reply-tree");
1790
+ (0, node_assert_strict.strictEqual)(requests, 1);
1791
+ });
1792
+ (0, node_test.default)(`${failure} finishes the default strategy without items`, async () => {
1793
+ const seed = new _fedify_vocab.Note({ contexts: [contextId] });
1794
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: fail }, seed), []);
1795
+ });
1796
+ (0, node_test.default)(`${failure} consumes the request budget`, async () => {
1797
+ const seed = new _fedify_vocab.Note({
1798
+ contexts: [contextId],
1799
+ replyTarget: parent.id
1800
+ });
1801
+ const requests = [];
1802
+ (0, node_assert_strict.deepStrictEqual)(await collect({ documentLoader: (url) => {
1803
+ requests.push(url.href);
1804
+ if (url.href === contextId.href) return fail();
1805
+ return Promise.resolve(parent);
1806
+ } }, seed, {
1807
+ strategies: ["context-auto", "reply-tree"],
1808
+ maxRequests: 1
1809
+ }), []);
1810
+ (0, node_assert_strict.deepStrictEqual)(requests, [contextId.href]);
1811
+ });
1812
+ (0, node_test.default)(`${failure} propagates the cancellation reason`, async () => {
1813
+ const controller = new AbortController();
1814
+ const reason = /* @__PURE__ */ new Error("Stop backfill");
1815
+ const seed = new _fedify_vocab.Note({
1816
+ contexts: [contextId],
1817
+ replyTarget: parent
1818
+ });
1819
+ const yielded = [];
1820
+ await (0, node_assert_strict.rejects)(async () => {
1821
+ for await (const item of backfill({ documentLoader: (_url, options) => {
1822
+ (0, node_assert_strict.strictEqual)(options?.signal, controller.signal);
1823
+ controller.abort(reason);
1824
+ return fail();
1825
+ } }, seed, {
1826
+ strategies: ["context-auto", "reply-tree"],
1827
+ signal: controller.signal
1828
+ })) yielded.push(item);
1829
+ }, (error) => error === reason);
1830
+ (0, node_assert_strict.deepStrictEqual)(yielded, []);
1831
+ });
1832
+ }
1833
+ (0, node_test.default)("a failed context load is not cached for later reply-tree loads", async () => {
1834
+ const seed = new _fedify_vocab.Note({
1835
+ contexts: [contextId],
1836
+ replyTarget: contextId
1837
+ });
1838
+ let requests = 0;
1839
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: (url) => {
1840
+ (0, node_assert_strict.strictEqual)(url.href, contextId.href);
1841
+ requests++;
1842
+ if (requests === 1) return Promise.reject(/* @__PURE__ */ new Error("Temporary failure"));
1843
+ return Promise.resolve(parent);
1844
+ } }, seed, {
1845
+ strategies: ["context-auto", "reply-tree"],
1846
+ maxRequests: 2
1847
+ })).map((item) => item.object.id?.href), [parent.id?.href]);
1848
+ (0, node_assert_strict.strictEqual)(requests, 2);
1849
+ });
1850
+ (0, node_test.default)("an exhausted budget still allows embedded reply-tree data", async () => {
1851
+ (0, node_assert_strict.deepStrictEqual)((await collect({ documentLoader: () => {
1852
+ throw new Error("No requests allowed");
1853
+ } }, new _fedify_vocab.Note({
1854
+ contexts: [contextId],
1855
+ replyTarget: parent
1856
+ }), {
1857
+ strategies: ["context-auto", "reply-tree"],
1858
+ maxRequests: 0
1859
+ })).map((item) => item.object), [parent]);
1860
+ });
1861
+ (0, node_test.default)("interval callback errors propagate before the loader is called", async () => {
1862
+ const reason = /* @__PURE__ */ new Error("Invalid interval configuration");
1863
+ let requests = 0;
1864
+ await (0, node_assert_strict.rejects)(collect({ documentLoader: () => {
1865
+ requests++;
1866
+ return Promise.resolve(null);
1867
+ } }, new _fedify_vocab.Note({
1868
+ contexts: [contextId],
1869
+ replyTarget: parent
1870
+ }), {
1871
+ strategies: ["context-auto", "reply-tree"],
1872
+ interval: () => {
1873
+ throw reason;
1874
+ }
1875
+ }), (error) => error === reason);
1876
+ (0, node_assert_strict.strictEqual)(requests, 0);
1877
+ });
1878
+ (0, node_test.default)("invalid interval strings still propagate", async () => {
1879
+ let requests = 0;
1880
+ await (0, node_assert_strict.rejects)(collect({ documentLoader: () => {
1881
+ requests++;
1882
+ return Promise.resolve(null);
1883
+ } }, new _fedify_vocab.Note({
1884
+ contexts: [contextId],
1885
+ replyTarget: parent
1886
+ }), {
1887
+ strategies: ["context-auto", "reply-tree"],
1888
+ interval: "not a duration"
1889
+ }));
1890
+ (0, node_assert_strict.strictEqual)(requests, 0);
1891
+ });
1892
+ });
1760
1893
  //#endregion
@@ -91,7 +91,7 @@ function currentContextGroupHasAuto(strategies) {
91
91
  async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
92
92
  const contextId = note.contextIds[0];
93
93
  if (contextId == null) return;
94
- const collection = await loadObject(context, contextId, options, budget);
94
+ const collection = await loadObject(context, contextId, options, budget, { skipLoaderErrors: true });
95
95
  if (!isCollection(collection)) return;
96
96
  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 {
97
97
  object: item.object,
@@ -266,7 +266,7 @@ async function* getCollectionItems(context, collection, options, budget, skipIds
266
266
  };
267
267
  const pageOptions = {
268
268
  documentLoader: async (url) => {
269
- const object = await loadObject(context, new URL(url), options, budget, true);
269
+ const object = await loadObject(context, new URL(url), options, budget, { throwOnBudgetExceeded: true });
270
270
  if (object == null) throw new Error(`Collection page not found: ${url}`);
271
271
  return {
272
272
  contextUrl: null,
@@ -322,7 +322,7 @@ async function loadCollectionItemDocument(context, url, options, budget, skipIds
322
322
  try {
323
323
  const iri = new URL(url);
324
324
  if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
325
- object = await loadObject(context, iri, options, budget, true);
325
+ object = await loadObject(context, iri, options, budget, { throwOnBudgetExceeded: true });
326
326
  } catch (error) {
327
327
  if (error instanceof MaxRequestsExceeded) throw error;
328
328
  budget.signal?.throwIfAborted();
@@ -345,7 +345,7 @@ function skippedCollectionItemDocument(url) {
345
345
  }
346
346
  };
347
347
  }
348
- async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
348
+ async function loadObject(context, iri, options, budget, { throwOnBudgetExceeded = false, skipLoaderErrors = false } = {}) {
349
349
  budget.signal?.throwIfAborted();
350
350
  const cacheKey = iri.href;
351
351
  const cached = budget.documents.get(cacheKey);
@@ -357,12 +357,17 @@ async function loadObject(context, iri, options, budget, throwOnBudgetExceeded =
357
357
  await waitForInterval(options, budget);
358
358
  budget.signal?.throwIfAborted();
359
359
  budget.requestCount++;
360
- const document = context.documentLoader(iri, { signal: budget.signal });
361
- budget.documents.set(cacheKey, document);
360
+ let document;
362
361
  try {
362
+ document = context.documentLoader(iri, { signal: budget.signal });
363
+ budget.documents.set(cacheKey, document);
363
364
  return await document;
364
365
  } catch (error) {
365
366
  if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
367
+ if (skipLoaderErrors) {
368
+ budget.signal?.throwIfAborted();
369
+ return null;
370
+ }
366
371
  throw error;
367
372
  }
368
373
  }
@@ -1734,5 +1739,133 @@ test("embedded pages without IDs follow next without loader calls", async () =>
1734
1739
  maxDepth: 1
1735
1740
  })).map((item) => item.id?.href), [a.id?.href, b.id?.href]);
1736
1741
  });
1742
+ describe("context loader failures", () => {
1743
+ const contextId = new URL("https://example.com/thread");
1744
+ const parent = new Note({ id: new URL("https://example.com/parent") });
1745
+ for (const synchronous of [false, true]) {
1746
+ const failure = synchronous ? "synchronous throw" : "promise rejection";
1747
+ const fail = () => {
1748
+ const error = /* @__PURE__ */ new Error("Context collection unavailable");
1749
+ if (synchronous) throw error;
1750
+ return Promise.reject(error);
1751
+ };
1752
+ test(`${failure} allows reply-tree to yield an embedded parent`, async () => {
1753
+ const seed = new Note({
1754
+ contexts: [contextId],
1755
+ replyTarget: parent
1756
+ });
1757
+ let requests = 0;
1758
+ const items = await collect({ documentLoader: () => {
1759
+ requests++;
1760
+ return fail();
1761
+ } }, seed, {
1762
+ strategies: ["context-auto", "reply-tree"],
1763
+ maxRequests: 1
1764
+ });
1765
+ deepStrictEqual(items.map((item) => item.object), [parent]);
1766
+ strictEqual(items[0].strategy, "reply-tree");
1767
+ strictEqual(requests, 1);
1768
+ });
1769
+ test(`${failure} finishes the default strategy without items`, async () => {
1770
+ const seed = new Note({ contexts: [contextId] });
1771
+ deepStrictEqual(await collect({ documentLoader: fail }, seed), []);
1772
+ });
1773
+ test(`${failure} consumes the request budget`, async () => {
1774
+ const seed = new Note({
1775
+ contexts: [contextId],
1776
+ replyTarget: parent.id
1777
+ });
1778
+ const requests = [];
1779
+ deepStrictEqual(await collect({ documentLoader: (url) => {
1780
+ requests.push(url.href);
1781
+ if (url.href === contextId.href) return fail();
1782
+ return Promise.resolve(parent);
1783
+ } }, seed, {
1784
+ strategies: ["context-auto", "reply-tree"],
1785
+ maxRequests: 1
1786
+ }), []);
1787
+ deepStrictEqual(requests, [contextId.href]);
1788
+ });
1789
+ test(`${failure} propagates the cancellation reason`, async () => {
1790
+ const controller = new AbortController();
1791
+ const reason = /* @__PURE__ */ new Error("Stop backfill");
1792
+ const seed = new Note({
1793
+ contexts: [contextId],
1794
+ replyTarget: parent
1795
+ });
1796
+ const yielded = [];
1797
+ await rejects(async () => {
1798
+ for await (const item of backfill({ documentLoader: (_url, options) => {
1799
+ strictEqual(options?.signal, controller.signal);
1800
+ controller.abort(reason);
1801
+ return fail();
1802
+ } }, seed, {
1803
+ strategies: ["context-auto", "reply-tree"],
1804
+ signal: controller.signal
1805
+ })) yielded.push(item);
1806
+ }, (error) => error === reason);
1807
+ deepStrictEqual(yielded, []);
1808
+ });
1809
+ }
1810
+ test("a failed context load is not cached for later reply-tree loads", async () => {
1811
+ const seed = new Note({
1812
+ contexts: [contextId],
1813
+ replyTarget: contextId
1814
+ });
1815
+ let requests = 0;
1816
+ deepStrictEqual((await collect({ documentLoader: (url) => {
1817
+ strictEqual(url.href, contextId.href);
1818
+ requests++;
1819
+ if (requests === 1) return Promise.reject(/* @__PURE__ */ new Error("Temporary failure"));
1820
+ return Promise.resolve(parent);
1821
+ } }, seed, {
1822
+ strategies: ["context-auto", "reply-tree"],
1823
+ maxRequests: 2
1824
+ })).map((item) => item.object.id?.href), [parent.id?.href]);
1825
+ strictEqual(requests, 2);
1826
+ });
1827
+ test("an exhausted budget still allows embedded reply-tree data", async () => {
1828
+ deepStrictEqual((await collect({ documentLoader: () => {
1829
+ throw new Error("No requests allowed");
1830
+ } }, new Note({
1831
+ contexts: [contextId],
1832
+ replyTarget: parent
1833
+ }), {
1834
+ strategies: ["context-auto", "reply-tree"],
1835
+ maxRequests: 0
1836
+ })).map((item) => item.object), [parent]);
1837
+ });
1838
+ test("interval callback errors propagate before the loader is called", async () => {
1839
+ const reason = /* @__PURE__ */ new Error("Invalid interval configuration");
1840
+ let requests = 0;
1841
+ await rejects(collect({ documentLoader: () => {
1842
+ requests++;
1843
+ return Promise.resolve(null);
1844
+ } }, new Note({
1845
+ contexts: [contextId],
1846
+ replyTarget: parent
1847
+ }), {
1848
+ strategies: ["context-auto", "reply-tree"],
1849
+ interval: () => {
1850
+ throw reason;
1851
+ }
1852
+ }), (error) => error === reason);
1853
+ strictEqual(requests, 0);
1854
+ });
1855
+ test("invalid interval strings still propagate", async () => {
1856
+ let requests = 0;
1857
+ await rejects(collect({ documentLoader: () => {
1858
+ requests++;
1859
+ return Promise.resolve(null);
1860
+ } }, new Note({
1861
+ contexts: [contextId],
1862
+ replyTarget: parent
1863
+ }), {
1864
+ strategies: ["context-auto", "reply-tree"],
1865
+ interval: "not a duration"
1866
+ }));
1867
+ strictEqual(requests, 0);
1868
+ });
1869
+ });
1737
1870
  //#endregion
1738
1871
  export {};
package/dist/mod.cjs CHANGED
@@ -90,7 +90,7 @@ function currentContextGroupHasAuto(strategies) {
90
90
  async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
91
91
  const contextId = note.contextIds[0];
92
92
  if (contextId == null) return;
93
- const collection = await loadObject(context, contextId, options, budget);
93
+ const collection = await loadObject(context, contextId, options, budget, { skipLoaderErrors: true });
94
94
  if (!isCollection(collection)) return;
95
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
96
  object: item.object,
@@ -265,7 +265,7 @@ async function* getCollectionItems(context, collection, options, budget, skipIds
265
265
  };
266
266
  const pageOptions = {
267
267
  documentLoader: async (url) => {
268
- const object = await loadObject(context, new URL(url), options, budget, true);
268
+ const object = await loadObject(context, new URL(url), options, budget, { throwOnBudgetExceeded: true });
269
269
  if (object == null) throw new Error(`Collection page not found: ${url}`);
270
270
  return {
271
271
  contextUrl: null,
@@ -321,7 +321,7 @@ async function loadCollectionItemDocument(context, url, options, budget, skipIds
321
321
  try {
322
322
  const iri = new URL(url);
323
323
  if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
324
- object = await loadObject(context, iri, options, budget, true);
324
+ object = await loadObject(context, iri, options, budget, { throwOnBudgetExceeded: true });
325
325
  } catch (error) {
326
326
  if (error instanceof MaxRequestsExceeded) throw error;
327
327
  budget.signal?.throwIfAborted();
@@ -344,7 +344,7 @@ function skippedCollectionItemDocument(url) {
344
344
  }
345
345
  };
346
346
  }
347
- async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
347
+ async function loadObject(context, iri, options, budget, { throwOnBudgetExceeded = false, skipLoaderErrors = false } = {}) {
348
348
  budget.signal?.throwIfAborted();
349
349
  const cacheKey = iri.href;
350
350
  const cached = budget.documents.get(cacheKey);
@@ -356,12 +356,17 @@ async function loadObject(context, iri, options, budget, throwOnBudgetExceeded =
356
356
  await waitForInterval(options, budget);
357
357
  budget.signal?.throwIfAborted();
358
358
  budget.requestCount++;
359
- const document = context.documentLoader(iri, { signal: budget.signal });
360
- budget.documents.set(cacheKey, document);
359
+ let document;
361
360
  try {
361
+ document = context.documentLoader(iri, { signal: budget.signal });
362
+ budget.documents.set(cacheKey, document);
362
363
  return await document;
363
364
  } catch (error) {
364
365
  if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
366
+ if (skipLoaderErrors) {
367
+ budget.signal?.throwIfAborted();
368
+ return null;
369
+ }
365
370
  throw error;
366
371
  }
367
372
  }
package/dist/mod.js CHANGED
@@ -89,7 +89,7 @@ function currentContextGroupHasAuto(strategies) {
89
89
  async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
90
90
  const contextId = note.contextIds[0];
91
91
  if (contextId == null) return;
92
- const collection = await loadObject(context, contextId, options, budget);
92
+ const collection = await loadObject(context, contextId, options, budget, { skipLoaderErrors: true });
93
93
  if (!isCollection(collection)) return;
94
94
  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 {
95
95
  object: item.object,
@@ -264,7 +264,7 @@ async function* getCollectionItems(context, collection, options, budget, skipIds
264
264
  };
265
265
  const pageOptions = {
266
266
  documentLoader: async (url) => {
267
- const object = await loadObject(context, new URL(url), options, budget, true);
267
+ const object = await loadObject(context, new URL(url), options, budget, { throwOnBudgetExceeded: true });
268
268
  if (object == null) throw new Error(`Collection page not found: ${url}`);
269
269
  return {
270
270
  contextUrl: null,
@@ -320,7 +320,7 @@ async function loadCollectionItemDocument(context, url, options, budget, skipIds
320
320
  try {
321
321
  const iri = new URL(url);
322
322
  if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
323
- object = await loadObject(context, iri, options, budget, true);
323
+ object = await loadObject(context, iri, options, budget, { throwOnBudgetExceeded: true });
324
324
  } catch (error) {
325
325
  if (error instanceof MaxRequestsExceeded) throw error;
326
326
  budget.signal?.throwIfAborted();
@@ -343,7 +343,7 @@ function skippedCollectionItemDocument(url) {
343
343
  }
344
344
  };
345
345
  }
346
- async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
346
+ async function loadObject(context, iri, options, budget, { throwOnBudgetExceeded = false, skipLoaderErrors = false } = {}) {
347
347
  budget.signal?.throwIfAborted();
348
348
  const cacheKey = iri.href;
349
349
  const cached = budget.documents.get(cacheKey);
@@ -355,12 +355,17 @@ async function loadObject(context, iri, options, budget, throwOnBudgetExceeded =
355
355
  await waitForInterval(options, budget);
356
356
  budget.signal?.throwIfAborted();
357
357
  budget.requestCount++;
358
- const document = context.documentLoader(iri, { signal: budget.signal });
359
- budget.documents.set(cacheKey, document);
358
+ let document;
360
359
  try {
360
+ document = context.documentLoader(iri, { signal: budget.signal });
361
+ budget.documents.set(cacheKey, document);
361
362
  return await document;
362
363
  } catch (error) {
363
364
  if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
365
+ if (skipLoaderErrors) {
366
+ budget.signal?.throwIfAborted();
367
+ return null;
368
+ }
364
369
  throw error;
365
370
  }
366
371
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/backfill",
3
- "version": "2.3.12-dev.2341+eb343ebf",
3
+ "version": "2.3.12-dev.2346+e087854c",
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.12-dev.2341+eb343ebf"
52
+ "@fedify/vocab": "2.3.12-dev.2346+e087854c"
53
53
  },
54
54
  "devDependencies": {
55
55
  "tsdown": "^0.22.0",