@fedify/backfill 2.3.12-dev.2341 → 2.3.12-dev.2365
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 +5 -1
- package/dist/backfill.test.cjs +139 -6
- package/dist/backfill.test.js +139 -6
- package/dist/mod.cjs +11 -6
- package/dist/mod.js +11 -6
- package/package.json +2 -2
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
|
package/dist/backfill.test.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/backfill.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2.3.12-dev.2365+89254121",
|
|
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.
|
|
52
|
+
"@fedify/vocab": "2.3.12-dev.2365+89254121"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"tsdown": "^0.22.0",
|