@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
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 +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as MemoryKvStore } from "../kv-
|
|
5
|
-
import { t as assertEquals } from "../assert_equals-
|
|
6
|
-
import "../
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
4
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
5
|
+
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
6
|
+
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
7
|
+
import "../std__assert-DvMDRPes.mjs";
|
|
8
|
+
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
9
|
+
import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-XJ53md4S.mjs";
|
|
10
|
+
import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.mjs";
|
|
9
11
|
import { test } from "@fedify/fixture";
|
|
10
12
|
//#region src/federation/circuit-breaker.test.ts
|
|
11
13
|
var AlwaysConflictingKvStore = class extends MemoryKvStore {
|
|
@@ -18,15 +20,174 @@ var AlwaysConflictingKvStore = class extends MemoryKvStore {
|
|
|
18
20
|
};
|
|
19
21
|
var CountingCasKvStore = class extends MemoryKvStore {
|
|
20
22
|
attempts = 0;
|
|
23
|
+
options = [];
|
|
21
24
|
cas(key, expectedValue, newValue, options) {
|
|
22
25
|
this.attempts++;
|
|
26
|
+
this.options.push(options);
|
|
23
27
|
return super.cas(key, expectedValue, newValue, options);
|
|
24
28
|
}
|
|
25
29
|
};
|
|
30
|
+
var EncodedCasKvStore = class extends MemoryKvStore {
|
|
31
|
+
async cas(key, expectedValue, newValue, options) {
|
|
32
|
+
const current = await this.get(key);
|
|
33
|
+
if (JSON.stringify(current) !== JSON.stringify(expectedValue)) return false;
|
|
34
|
+
if (newValue === void 0) await this.delete(key);
|
|
35
|
+
else await this.set(key, newValue, options);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
test("CircuitBreaker uses raw encoded CAS expectations", async () => {
|
|
40
|
+
const kv = new EncodedCasKvStore();
|
|
41
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
42
|
+
let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
|
|
43
|
+
const circuit = new CircuitBreaker({
|
|
44
|
+
kv,
|
|
45
|
+
prefix: ["_fedify", "circuit"],
|
|
46
|
+
now: () => now,
|
|
47
|
+
options: {
|
|
48
|
+
failureThreshold: 1,
|
|
49
|
+
recoveryDelay: { seconds: 30 }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const key = [
|
|
53
|
+
"_fedify",
|
|
54
|
+
"circuit",
|
|
55
|
+
"remote.example"
|
|
56
|
+
];
|
|
57
|
+
await circuit.recordFailure("remote.example");
|
|
58
|
+
now = now.add({ seconds: 30 });
|
|
59
|
+
assertEquals((await circuit.beforeSend("remote.example", {})).type, "send");
|
|
60
|
+
assertEquals((await circuit.recordSuccess("remote.example"))?.newState, "closed");
|
|
61
|
+
const oldOrder = {
|
|
62
|
+
state: "half-open",
|
|
63
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
64
|
+
opened: "2026-05-25T00:00:00Z",
|
|
65
|
+
__fedifyCircuitBreakerStateVersion: 1,
|
|
66
|
+
halfOpened: "2026-05-25T00:00:00Z"
|
|
67
|
+
};
|
|
68
|
+
await kv.set(key, oldOrder);
|
|
69
|
+
assertEquals((await circuit.recordFailure("remote.example"))?.newState, "open");
|
|
70
|
+
await kv.set(key, oldOrder);
|
|
71
|
+
assertEquals((await circuit.recordSuccess("remote.example"))?.newState, "closed");
|
|
72
|
+
await kv.set(key, oldOrder);
|
|
73
|
+
assertEquals((await circuit.beforeSend("remote.example", {})).type, "send");
|
|
74
|
+
await kv.set(key, { __fedifyDeletingCircuitBreakerLegacyState: true });
|
|
75
|
+
await assertRejects(() => circuit.recordFailure("remote.example"));
|
|
76
|
+
assertEquals(await kv.get(key), { __fedifyDeletingCircuitBreakerLegacyState: true });
|
|
77
|
+
});
|
|
78
|
+
var CountingSetKvStore = class {
|
|
79
|
+
#store = new MemoryKvStore();
|
|
80
|
+
keys = [];
|
|
81
|
+
options = [];
|
|
82
|
+
get(key) {
|
|
83
|
+
return this.#store.get(key);
|
|
84
|
+
}
|
|
85
|
+
set(key, value, options) {
|
|
86
|
+
this.keys.push(key);
|
|
87
|
+
this.options.push(options);
|
|
88
|
+
return this.#store.set(key, value, options);
|
|
89
|
+
}
|
|
90
|
+
delete(key) {
|
|
91
|
+
return this.#store.delete(key);
|
|
92
|
+
}
|
|
93
|
+
list(prefix) {
|
|
94
|
+
return this.#store.list(prefix);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var CountingSweepKvStore = class extends MemoryKvStore {
|
|
98
|
+
listCalls = 0;
|
|
99
|
+
markerGetCalls = 0;
|
|
100
|
+
casCalls = [];
|
|
101
|
+
deletedKeys = [];
|
|
102
|
+
get(key) {
|
|
103
|
+
if (key.length === 4 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "__fedify_meta" && key[3] === "circuit_breaker_state_ttl_sweep_v2") this.markerGetCalls++;
|
|
104
|
+
return super.get(key);
|
|
105
|
+
}
|
|
106
|
+
cas(key, expectedValue, newValue, options) {
|
|
107
|
+
this.casCalls.push({
|
|
108
|
+
key,
|
|
109
|
+
options
|
|
110
|
+
});
|
|
111
|
+
return super.cas(key, expectedValue, newValue, options);
|
|
112
|
+
}
|
|
113
|
+
async *list(prefix) {
|
|
114
|
+
this.listCalls++;
|
|
115
|
+
yield* super.list(prefix);
|
|
116
|
+
}
|
|
117
|
+
delete(key) {
|
|
118
|
+
this.deletedKeys.push(key);
|
|
119
|
+
return super.delete(key);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var ConflictingSweepMarkerKvStore = class extends CountingSweepKvStore {
|
|
123
|
+
attempts = 0;
|
|
124
|
+
cas(key, expectedValue, newValue, options) {
|
|
125
|
+
if (key.length === 4 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "__fedify_meta" && key[3] === "circuit_breaker_state_ttl_sweep_v2") {
|
|
126
|
+
this.attempts++;
|
|
127
|
+
if (this.attempts > 3) throw new Error("legacy sweep did not stop retrying CAS misses");
|
|
128
|
+
return Promise.resolve(false);
|
|
129
|
+
}
|
|
130
|
+
return super.cas(key, expectedValue, newValue, options);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var ClockAdvancingSweepKvStore = class extends CountingSweepKvStore {
|
|
134
|
+
clock = Temporal.Instant.from("2026-05-25T00:00:00Z");
|
|
135
|
+
cas(key, expectedValue, newValue, options) {
|
|
136
|
+
if (key.length === 3) this.clock = this.clock.add({ seconds: 30 });
|
|
137
|
+
return super.cas(key, expectedValue, newValue, options);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var RenewalLostSweepKvStore = class extends ClockAdvancingSweepKvStore {
|
|
141
|
+
markerCasAttempts = 0;
|
|
142
|
+
cas(key, expectedValue, newValue, options) {
|
|
143
|
+
if (key.length === 4 && key[3] === "circuit_breaker_state_ttl_sweep_v2") {
|
|
144
|
+
this.markerCasAttempts++;
|
|
145
|
+
if (this.markerCasAttempts > 1) return Promise.resolve(false);
|
|
146
|
+
}
|
|
147
|
+
return super.cas(key, expectedValue, newValue, options);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var MarkerChangedDuringListKvStore = class extends CountingSweepKvStore {
|
|
151
|
+
async *list(prefix) {
|
|
152
|
+
yield* super.list(prefix);
|
|
153
|
+
await super.set([
|
|
154
|
+
"_fedify",
|
|
155
|
+
"circuit",
|
|
156
|
+
"__fedify_meta",
|
|
157
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
158
|
+
], { state: "final" });
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var UpdatingDuringListKvStore = class extends MemoryKvStore {
|
|
162
|
+
async *list(prefix) {
|
|
163
|
+
for await (const entry of super.list(prefix)) {
|
|
164
|
+
if (entry.key.length === 3 && entry.key[0] === "_fedify" && entry.key[1] === "circuit" && entry.key[2] === "racy.example") await super.set(entry.key, {
|
|
165
|
+
state: "closed",
|
|
166
|
+
failures: ["2026-05-25T00:01:00Z"]
|
|
167
|
+
});
|
|
168
|
+
yield entry;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var FailingOnceSweepKvStore = class extends MemoryKvStore {
|
|
173
|
+
listCalls = 0;
|
|
174
|
+
deletedKeys = [];
|
|
175
|
+
async *list(prefix) {
|
|
176
|
+
this.listCalls++;
|
|
177
|
+
if (this.listCalls === 1) throw new Error("transient list failure");
|
|
178
|
+
yield* super.list(prefix);
|
|
179
|
+
}
|
|
180
|
+
delete(key) {
|
|
181
|
+
this.deletedKeys.push(key);
|
|
182
|
+
return super.delete(key);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
26
185
|
test("normalizeCircuitBreakerOptions() uses numeric failure policy", () => {
|
|
27
186
|
const options = normalizeCircuitBreakerOptions({
|
|
28
187
|
failureThreshold: 3,
|
|
29
|
-
failureWindow: { minutes: 10 }
|
|
188
|
+
failureWindow: { minutes: 10 },
|
|
189
|
+
heldActivityTtl: { hours: 1 },
|
|
190
|
+
recoveryDelay: { hours: 2 }
|
|
30
191
|
});
|
|
31
192
|
const failures = [
|
|
32
193
|
Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
@@ -51,6 +212,7 @@ test("normalizeCircuitBreakerOptions() uses numeric failure policy", () => {
|
|
|
51
212
|
"2026-05-25T00:11:00Z",
|
|
52
213
|
"2026-05-25T00:12:00Z"
|
|
53
214
|
]);
|
|
215
|
+
assertEquals(options.stateTtl, Temporal.Duration.from({ hours: 3 }));
|
|
54
216
|
});
|
|
55
217
|
test("normalizeCircuitBreakerOptions() validates numeric failure policy", () => {
|
|
56
218
|
assertThrows(() => normalizeCircuitBreakerOptions({ failureThreshold: 0 }), TypeError, "failureThreshold");
|
|
@@ -68,6 +230,10 @@ test("normalizeCircuitBreakerOptions() validates positive durations", () => {
|
|
|
68
230
|
assertThrows(() => normalizeCircuitBreakerOptions({ failureWindow: { seconds: 0 } }), RangeError, "failureWindow");
|
|
69
231
|
assertThrows(() => normalizeCircuitBreakerOptions({ releaseInterval: { seconds: 0 } }), RangeError, "releaseInterval");
|
|
70
232
|
assertThrows(() => normalizeCircuitBreakerOptions({ releaseInterval: { nanoseconds: 5e5 } }), RangeError, "releaseInterval");
|
|
233
|
+
assertThrows(() => normalizeCircuitBreakerOptions({
|
|
234
|
+
recoveryDelay: { minutes: 30 },
|
|
235
|
+
stateTtl: { minutes: 29 }
|
|
236
|
+
}), RangeError, "stateTtl");
|
|
71
237
|
});
|
|
72
238
|
test("normalizeCircuitBreakerOptions() accepts callback failure policy", () => {
|
|
73
239
|
const options = normalizeCircuitBreakerOptions({ failure: (timestamps) => timestamps.length >= 2 });
|
|
@@ -76,13 +242,26 @@ test("normalizeCircuitBreakerOptions() accepts callback failure policy", () => {
|
|
|
76
242
|
assertEquals(options.failure([Temporal.Instant.from("2026-05-25T00:00:00Z")]), false);
|
|
77
243
|
assertEquals(options.failure([Temporal.Instant.from("2026-05-25T00:00:00Z"), Temporal.Instant.from("2026-05-25T00:01:00Z")]), true);
|
|
78
244
|
assertEquals(options.pruneFailures(failures, base.add({ minutes: 105 })).map((t) => t.toString()), failures.slice(-100).map((t) => t.toString()));
|
|
245
|
+
assertEquals(options.stateTtl?.total("millisecond"), Temporal.Duration.from({
|
|
246
|
+
days: 7,
|
|
247
|
+
minutes: 30
|
|
248
|
+
}).total("millisecond"));
|
|
249
|
+
assertEquals(normalizeCircuitBreakerOptions({
|
|
250
|
+
failure: (timestamps) => timestamps.length >= 2,
|
|
251
|
+
recoveryDelay: { hours: 1 },
|
|
252
|
+
heldActivityTtl: { days: 2 }
|
|
253
|
+
}).stateTtl?.total("millisecond"), Temporal.Duration.from({
|
|
254
|
+
days: 2,
|
|
255
|
+
hours: 1
|
|
256
|
+
}).total("millisecond"));
|
|
79
257
|
});
|
|
80
258
|
test("parseCircuitBreakerKvState() validates stored shape", () => {
|
|
81
259
|
assertEquals(parseCircuitBreakerKvState({
|
|
82
260
|
state: "open",
|
|
83
261
|
failures: ["2026-05-25T00:00:00Z"],
|
|
84
262
|
opened: "2026-05-25T00:00:00Z",
|
|
85
|
-
halfOpened: "2026-05-25T00:00:00Z"
|
|
263
|
+
halfOpened: "2026-05-25T00:00:00Z",
|
|
264
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
86
265
|
}), {
|
|
87
266
|
state: "open",
|
|
88
267
|
failures: ["2026-05-25T00:00:00Z"],
|
|
@@ -192,6 +371,7 @@ test("CircuitBreaker opens, probes, closes, and drops held activities", async ()
|
|
|
192
371
|
});
|
|
193
372
|
test("CircuitBreaker recovers stale half-open probes", async () => {
|
|
194
373
|
const kv = new MemoryKvStore();
|
|
374
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
195
375
|
let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
|
|
196
376
|
const circuit = new CircuitBreaker({
|
|
197
377
|
kv,
|
|
@@ -240,6 +420,7 @@ test("CircuitBreaker recovers stale half-open probes", async () => {
|
|
|
240
420
|
});
|
|
241
421
|
test("CircuitBreaker caps held delays at activity TTL", async () => {
|
|
242
422
|
const kv = new MemoryKvStore();
|
|
423
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
243
424
|
const now = Temporal.Instant.from("2026-05-25T00:05:00Z");
|
|
244
425
|
const circuit = new CircuitBreaker({
|
|
245
426
|
kv,
|
|
@@ -303,6 +484,7 @@ test("CircuitBreaker caps held delays at activity TTL", async () => {
|
|
|
303
484
|
});
|
|
304
485
|
test("CircuitBreaker ignores malformed held timestamps", async () => {
|
|
305
486
|
const kv = new MemoryKvStore();
|
|
487
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
306
488
|
const now = Temporal.Instant.from("2026-05-25T00:05:00Z");
|
|
307
489
|
const circuit = new CircuitBreaker({
|
|
308
490
|
kv,
|
|
@@ -328,6 +510,7 @@ test("CircuitBreaker ignores malformed held timestamps", async () => {
|
|
|
328
510
|
});
|
|
329
511
|
test("CircuitBreaker bounds beforeSend CAS retries", async () => {
|
|
330
512
|
let kv = new AlwaysConflictingKvStore();
|
|
513
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
331
514
|
const now = Temporal.Instant.from("2026-05-25T00:30:00Z");
|
|
332
515
|
let circuit = new CircuitBreaker({
|
|
333
516
|
kv,
|
|
@@ -356,6 +539,7 @@ test("CircuitBreaker bounds beforeSend CAS retries", async () => {
|
|
|
356
539
|
heldSince: now
|
|
357
540
|
});
|
|
358
541
|
kv = new AlwaysConflictingKvStore();
|
|
542
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
359
543
|
circuit = new CircuitBreaker({
|
|
360
544
|
kv,
|
|
361
545
|
prefix: ["_fedify", "circuit"],
|
|
@@ -386,6 +570,7 @@ test("CircuitBreaker bounds beforeSend CAS retries", async () => {
|
|
|
386
570
|
});
|
|
387
571
|
test("CircuitBreaker skips recording failures for open circuits", async () => {
|
|
388
572
|
const kv = new CountingCasKvStore();
|
|
573
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
389
574
|
const circuit = new CircuitBreaker({
|
|
390
575
|
kv,
|
|
391
576
|
prefix: ["_fedify", "circuit"],
|
|
@@ -412,6 +597,652 @@ test("CircuitBreaker skips recording failures for open circuits", async () => {
|
|
|
412
597
|
opened: "2026-05-25T00:00:00Z"
|
|
413
598
|
});
|
|
414
599
|
});
|
|
600
|
+
test("CircuitBreaker writes stored states with a TTL", async () => {
|
|
601
|
+
const kv = new CountingCasKvStore();
|
|
602
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
603
|
+
await new CircuitBreaker({
|
|
604
|
+
kv,
|
|
605
|
+
prefix: ["_fedify", "circuit"],
|
|
606
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
607
|
+
options: {
|
|
608
|
+
failureThreshold: 2,
|
|
609
|
+
failureWindow: { hours: 3 },
|
|
610
|
+
recoveryDelay: { hours: 1 },
|
|
611
|
+
heldActivityTtl: { days: 2 }
|
|
612
|
+
}
|
|
613
|
+
}).recordFailure("remote.example");
|
|
614
|
+
assertEquals(kv.options.at(-1), { ttl: Temporal.Duration.from({
|
|
615
|
+
days: 2,
|
|
616
|
+
hours: 1
|
|
617
|
+
}) });
|
|
618
|
+
});
|
|
619
|
+
test("CircuitBreaker writes stored states with a TTL without CAS", async () => {
|
|
620
|
+
const kv = new CountingSetKvStore();
|
|
621
|
+
await new CircuitBreaker({
|
|
622
|
+
kv,
|
|
623
|
+
prefix: ["_fedify", "circuit"],
|
|
624
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
625
|
+
options: {
|
|
626
|
+
failureThreshold: 2,
|
|
627
|
+
failureWindow: { hours: 3 },
|
|
628
|
+
recoveryDelay: { hours: 1 },
|
|
629
|
+
heldActivityTtl: { days: 2 }
|
|
630
|
+
}
|
|
631
|
+
}).recordFailure("remote.example");
|
|
632
|
+
assertEquals(kv.keys, [[
|
|
633
|
+
"_fedify",
|
|
634
|
+
"circuit",
|
|
635
|
+
"remote.example"
|
|
636
|
+
]]);
|
|
637
|
+
assertEquals(kv.options.at(-1), { ttl: Temporal.Duration.from({
|
|
638
|
+
days: 2,
|
|
639
|
+
hours: 1
|
|
640
|
+
}) });
|
|
641
|
+
});
|
|
642
|
+
test("CircuitBreaker writes custom-policy states with a derived TTL", async () => {
|
|
643
|
+
const kv = new CountingCasKvStore();
|
|
644
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
645
|
+
await new CircuitBreaker({
|
|
646
|
+
kv,
|
|
647
|
+
prefix: ["_fedify", "circuit"],
|
|
648
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
649
|
+
options: { failure: (timestamps) => timestamps.length >= 2 }
|
|
650
|
+
}).recordFailure("remote.example");
|
|
651
|
+
assertEquals(kv.options.at(-1)?.ttl?.total("millisecond"), Temporal.Duration.from({
|
|
652
|
+
days: 7,
|
|
653
|
+
minutes: 30
|
|
654
|
+
}).total("millisecond"));
|
|
655
|
+
});
|
|
656
|
+
test("CircuitBreaker accepts an explicit TTL with custom failure policies", async () => {
|
|
657
|
+
const kv = new CountingCasKvStore();
|
|
658
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
659
|
+
await new CircuitBreaker({
|
|
660
|
+
kv,
|
|
661
|
+
prefix: ["_fedify", "circuit"],
|
|
662
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
663
|
+
options: {
|
|
664
|
+
failure: (timestamps) => timestamps.length >= 2,
|
|
665
|
+
stateTtl: { days: 14 }
|
|
666
|
+
}
|
|
667
|
+
}).recordFailure("remote.example");
|
|
668
|
+
assertEquals(kv.options.at(-1), { ttl: Temporal.Duration.from({ days: 14 }) });
|
|
669
|
+
});
|
|
670
|
+
test("CircuitBreaker sweeps legacy states for custom failure policies", async () => {
|
|
671
|
+
const kv = new CountingSweepKvStore();
|
|
672
|
+
await kv.set([
|
|
673
|
+
"_fedify",
|
|
674
|
+
"circuit",
|
|
675
|
+
"stale.example"
|
|
676
|
+
], {
|
|
677
|
+
state: "closed",
|
|
678
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
679
|
+
});
|
|
680
|
+
const circuit = new CircuitBreaker({
|
|
681
|
+
kv,
|
|
682
|
+
prefix: ["_fedify", "circuit"],
|
|
683
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
684
|
+
options: { failure: (timestamps) => timestamps.length >= 2 }
|
|
685
|
+
});
|
|
686
|
+
await circuit.recordFailure("remote.example");
|
|
687
|
+
await circuit.pendingSweep;
|
|
688
|
+
assertEquals(kv.listCalls, 1);
|
|
689
|
+
assertEquals(await kv.get([
|
|
690
|
+
"_fedify",
|
|
691
|
+
"circuit",
|
|
692
|
+
"stale.example"
|
|
693
|
+
]), {
|
|
694
|
+
state: "closed",
|
|
695
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
696
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
697
|
+
});
|
|
698
|
+
assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "stale.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
|
|
699
|
+
days: 7,
|
|
700
|
+
minutes: 30
|
|
701
|
+
}).total("millisecond"));
|
|
702
|
+
});
|
|
703
|
+
test("CircuitBreaker stamps a TTL on versioned states during sweeps", async () => {
|
|
704
|
+
const kv = new CountingSweepKvStore();
|
|
705
|
+
await kv.set([
|
|
706
|
+
"_fedify",
|
|
707
|
+
"circuit",
|
|
708
|
+
"lingering.example"
|
|
709
|
+
], {
|
|
710
|
+
state: "closed",
|
|
711
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
712
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
713
|
+
});
|
|
714
|
+
const circuit = new CircuitBreaker({
|
|
715
|
+
kv,
|
|
716
|
+
prefix: ["_fedify", "circuit"],
|
|
717
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
718
|
+
options: { failure: (timestamps) => timestamps.length >= 2 }
|
|
719
|
+
});
|
|
720
|
+
await circuit.recordFailure("remote.example");
|
|
721
|
+
await circuit.pendingSweep;
|
|
722
|
+
assertEquals(await kv.get([
|
|
723
|
+
"_fedify",
|
|
724
|
+
"circuit",
|
|
725
|
+
"lingering.example"
|
|
726
|
+
]), {
|
|
727
|
+
state: "closed",
|
|
728
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
729
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
730
|
+
});
|
|
731
|
+
assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "lingering.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
|
|
732
|
+
days: 7,
|
|
733
|
+
minutes: 30
|
|
734
|
+
}).total("millisecond"));
|
|
735
|
+
});
|
|
736
|
+
test("CircuitBreaker re-sweeps despite a completed v1 sweep marker", async () => {
|
|
737
|
+
const kv = new CountingSweepKvStore();
|
|
738
|
+
await kv.set([
|
|
739
|
+
"_fedify",
|
|
740
|
+
"circuit",
|
|
741
|
+
"__fedify_meta",
|
|
742
|
+
"circuit_breaker_state_ttl_sweep_v1"
|
|
743
|
+
], { state: "final" });
|
|
744
|
+
await kv.set([
|
|
745
|
+
"_fedify",
|
|
746
|
+
"circuit",
|
|
747
|
+
"lingering.example"
|
|
748
|
+
], {
|
|
749
|
+
state: "closed",
|
|
750
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
751
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
752
|
+
});
|
|
753
|
+
const circuit = new CircuitBreaker({
|
|
754
|
+
kv,
|
|
755
|
+
prefix: ["_fedify", "circuit"],
|
|
756
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
757
|
+
options: { failure: (timestamps) => timestamps.length >= 2 }
|
|
758
|
+
});
|
|
759
|
+
await circuit.recordFailure("remote.example");
|
|
760
|
+
await circuit.pendingSweep;
|
|
761
|
+
assertEquals(kv.listCalls, 1);
|
|
762
|
+
assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "lingering.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
|
|
763
|
+
days: 7,
|
|
764
|
+
minutes: 30
|
|
765
|
+
}).total("millisecond"));
|
|
766
|
+
});
|
|
767
|
+
test("CircuitBreaker renews the sweep marker lease during long sweeps", async () => {
|
|
768
|
+
const kv = new ClockAdvancingSweepKvStore();
|
|
769
|
+
for (let i = 0; i < 5; i++) await kv.set([
|
|
770
|
+
"_fedify",
|
|
771
|
+
"circuit",
|
|
772
|
+
`host-${i}.example`
|
|
773
|
+
], {
|
|
774
|
+
state: "closed",
|
|
775
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
776
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
777
|
+
});
|
|
778
|
+
const circuit = new CircuitBreaker({
|
|
779
|
+
kv,
|
|
780
|
+
prefix: ["_fedify", "circuit"],
|
|
781
|
+
now: () => kv.clock,
|
|
782
|
+
options: { failure: (timestamps) => timestamps.length >= 2 }
|
|
783
|
+
});
|
|
784
|
+
await circuit.getState("remote.example");
|
|
785
|
+
await circuit.pendingSweep;
|
|
786
|
+
assertEquals(kv.casCalls.filter(({ key }) => key.at(-1) === "circuit_breaker_state_ttl_sweep_v2").length, 4);
|
|
787
|
+
assertEquals(await kv.get([
|
|
788
|
+
"_fedify",
|
|
789
|
+
"circuit",
|
|
790
|
+
"__fedify_meta",
|
|
791
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
792
|
+
]), {
|
|
793
|
+
state: "done",
|
|
794
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
795
|
+
});
|
|
796
|
+
assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "host-4.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
|
|
797
|
+
days: 7,
|
|
798
|
+
minutes: 30
|
|
799
|
+
}).total("millisecond"));
|
|
800
|
+
});
|
|
801
|
+
test("CircuitBreaker abandons the sweep when the marker lease is lost", async () => {
|
|
802
|
+
const kv = new RenewalLostSweepKvStore();
|
|
803
|
+
for (let i = 0; i < 5; i++) await kv.set([
|
|
804
|
+
"_fedify",
|
|
805
|
+
"circuit",
|
|
806
|
+
`host-${i}.example`
|
|
807
|
+
], {
|
|
808
|
+
state: "closed",
|
|
809
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
810
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
811
|
+
});
|
|
812
|
+
const circuit = new CircuitBreaker({
|
|
813
|
+
kv,
|
|
814
|
+
prefix: ["_fedify", "circuit"],
|
|
815
|
+
now: () => kv.clock,
|
|
816
|
+
options: { failure: (timestamps) => timestamps.length >= 2 }
|
|
817
|
+
});
|
|
818
|
+
await circuit.getState("remote.example");
|
|
819
|
+
await circuit.pendingSweep;
|
|
820
|
+
assertEquals(kv.markerCasAttempts, 2);
|
|
821
|
+
assertEquals(kv.casCalls.filter(({ key }) => key.at(-1) === "host-4.example").length, 0);
|
|
822
|
+
assertEquals((await kv.get([
|
|
823
|
+
"_fedify",
|
|
824
|
+
"circuit",
|
|
825
|
+
"__fedify_meta",
|
|
826
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
827
|
+
]))?.state, "sweeping");
|
|
828
|
+
});
|
|
829
|
+
test("CircuitBreaker migrates legacy states without TTL once", async () => {
|
|
830
|
+
const kv = new CountingSweepKvStore();
|
|
831
|
+
await kv.set([
|
|
832
|
+
"_fedify",
|
|
833
|
+
"circuit",
|
|
834
|
+
"stale-a.example"
|
|
835
|
+
], {
|
|
836
|
+
state: "closed",
|
|
837
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
838
|
+
});
|
|
839
|
+
await kv.set([
|
|
840
|
+
"_fedify",
|
|
841
|
+
"circuit",
|
|
842
|
+
"stale-b.example"
|
|
843
|
+
], {
|
|
844
|
+
state: "open",
|
|
845
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
846
|
+
opened: "2026-05-25T00:00:00Z"
|
|
847
|
+
});
|
|
848
|
+
await kv.set([
|
|
849
|
+
"_fedify",
|
|
850
|
+
"circuit",
|
|
851
|
+
"malformed.example"
|
|
852
|
+
], "stale");
|
|
853
|
+
await kv.set([
|
|
854
|
+
"_fedify",
|
|
855
|
+
"other",
|
|
856
|
+
"untouched.example"
|
|
857
|
+
], "keep");
|
|
858
|
+
await kv.set([
|
|
859
|
+
"_fedify",
|
|
860
|
+
"circuit",
|
|
861
|
+
"__fedify_meta",
|
|
862
|
+
"future"
|
|
863
|
+
], {
|
|
864
|
+
state: "closed",
|
|
865
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
866
|
+
});
|
|
867
|
+
let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
|
|
868
|
+
const circuit = new CircuitBreaker({
|
|
869
|
+
kv,
|
|
870
|
+
prefix: ["_fedify", "circuit"],
|
|
871
|
+
now: () => now,
|
|
872
|
+
options: { failureThreshold: 2 }
|
|
873
|
+
});
|
|
874
|
+
assertEquals(await circuit.beforeSend("stale-b.example", {}), {
|
|
875
|
+
type: "hold",
|
|
876
|
+
state: "open",
|
|
877
|
+
delay: Temporal.Duration.from({ minutes: 30 }),
|
|
878
|
+
heldSince: Temporal.Instant.from("2026-05-25T00:00:00Z")
|
|
879
|
+
});
|
|
880
|
+
await circuit.pendingSweep;
|
|
881
|
+
assertEquals(await kv.get([
|
|
882
|
+
"_fedify",
|
|
883
|
+
"circuit",
|
|
884
|
+
"stale-a.example"
|
|
885
|
+
]), {
|
|
886
|
+
state: "closed",
|
|
887
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
888
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
889
|
+
});
|
|
890
|
+
assertEquals(await kv.get([
|
|
891
|
+
"_fedify",
|
|
892
|
+
"circuit",
|
|
893
|
+
"stale-b.example"
|
|
894
|
+
]), {
|
|
895
|
+
state: "open",
|
|
896
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
897
|
+
opened: "2026-05-25T00:00:00Z",
|
|
898
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
899
|
+
});
|
|
900
|
+
assertEquals(await kv.get([
|
|
901
|
+
"_fedify",
|
|
902
|
+
"circuit",
|
|
903
|
+
"malformed.example"
|
|
904
|
+
]), void 0);
|
|
905
|
+
assertEquals(await kv.get([
|
|
906
|
+
"_fedify",
|
|
907
|
+
"other",
|
|
908
|
+
"untouched.example"
|
|
909
|
+
]), "keep");
|
|
910
|
+
assertEquals(await kv.get([
|
|
911
|
+
"_fedify",
|
|
912
|
+
"circuit",
|
|
913
|
+
"__fedify_meta",
|
|
914
|
+
"future"
|
|
915
|
+
]), {
|
|
916
|
+
state: "closed",
|
|
917
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
918
|
+
});
|
|
919
|
+
await circuit.recordFailure("remote.example");
|
|
920
|
+
assertEquals(await circuit.getState("remote.example"), {
|
|
921
|
+
state: "closed",
|
|
922
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
923
|
+
});
|
|
924
|
+
assertEquals(kv.listCalls, 1);
|
|
925
|
+
assertEquals(kv.casCalls.find(({ key }) => key.length === 3 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "stale-a.example")?.options, { ttl: Temporal.Duration.from({ days: 7 }) });
|
|
926
|
+
assertEquals(kv.deletedKeys, [[
|
|
927
|
+
"_fedify",
|
|
928
|
+
"circuit",
|
|
929
|
+
"malformed.example"
|
|
930
|
+
]]);
|
|
931
|
+
assertEquals(await kv.get([
|
|
932
|
+
"_fedify",
|
|
933
|
+
"circuit",
|
|
934
|
+
"__fedify_meta",
|
|
935
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
936
|
+
]), {
|
|
937
|
+
state: "done",
|
|
938
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
939
|
+
});
|
|
940
|
+
await kv.set([
|
|
941
|
+
"_fedify",
|
|
942
|
+
"circuit",
|
|
943
|
+
"late-stale.example"
|
|
944
|
+
], {
|
|
945
|
+
state: "closed",
|
|
946
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
947
|
+
});
|
|
948
|
+
await circuit.recordFailure("another.example");
|
|
949
|
+
assertEquals(await kv.get([
|
|
950
|
+
"_fedify",
|
|
951
|
+
"circuit",
|
|
952
|
+
"late-stale.example"
|
|
953
|
+
]), {
|
|
954
|
+
state: "closed",
|
|
955
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
956
|
+
});
|
|
957
|
+
assertEquals(kv.listCalls, 1);
|
|
958
|
+
now = Temporal.Instant.from("2026-06-02T00:00:00Z");
|
|
959
|
+
await circuit.recordFailure("final.example");
|
|
960
|
+
await circuit.pendingSweep;
|
|
961
|
+
assertEquals(await kv.get([
|
|
962
|
+
"_fedify",
|
|
963
|
+
"circuit",
|
|
964
|
+
"late-stale.example"
|
|
965
|
+
]), {
|
|
966
|
+
state: "closed",
|
|
967
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
968
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
969
|
+
});
|
|
970
|
+
assertEquals(await circuit.getState("another.example"), {
|
|
971
|
+
state: "closed",
|
|
972
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
973
|
+
});
|
|
974
|
+
assertEquals(kv.listCalls, 2);
|
|
975
|
+
assertEquals(await kv.get([
|
|
976
|
+
"_fedify",
|
|
977
|
+
"circuit",
|
|
978
|
+
"__fedify_meta",
|
|
979
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
980
|
+
]), { state: "final" });
|
|
981
|
+
});
|
|
982
|
+
test("CircuitBreaker does not sweep state changed after listing", async () => {
|
|
983
|
+
const kv = new UpdatingDuringListKvStore();
|
|
984
|
+
await kv.set([
|
|
985
|
+
"_fedify",
|
|
986
|
+
"circuit",
|
|
987
|
+
"racy.example"
|
|
988
|
+
], {
|
|
989
|
+
state: "closed",
|
|
990
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
991
|
+
});
|
|
992
|
+
const circuit = new CircuitBreaker({
|
|
993
|
+
kv,
|
|
994
|
+
prefix: ["_fedify", "circuit"],
|
|
995
|
+
now: () => Temporal.Instant.from("2026-05-25T00:02:00Z"),
|
|
996
|
+
options: { failureThreshold: 2 }
|
|
997
|
+
});
|
|
998
|
+
await circuit.recordFailure("remote.example");
|
|
999
|
+
await circuit.pendingSweep;
|
|
1000
|
+
assertEquals(await kv.get([
|
|
1001
|
+
"_fedify",
|
|
1002
|
+
"circuit",
|
|
1003
|
+
"racy.example"
|
|
1004
|
+
]), {
|
|
1005
|
+
state: "closed",
|
|
1006
|
+
failures: ["2026-05-25T00:01:00Z"]
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
test("CircuitBreaker skips legacy sweep already running elsewhere", async () => {
|
|
1010
|
+
const kv = new CountingSweepKvStore();
|
|
1011
|
+
await kv.set([
|
|
1012
|
+
"_fedify",
|
|
1013
|
+
"circuit",
|
|
1014
|
+
"__fedify_meta",
|
|
1015
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1016
|
+
], {
|
|
1017
|
+
state: "sweeping",
|
|
1018
|
+
started: "2026-05-25T00:00:00Z",
|
|
1019
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
1020
|
+
});
|
|
1021
|
+
const circuit = new CircuitBreaker({
|
|
1022
|
+
kv,
|
|
1023
|
+
prefix: ["_fedify", "circuit"],
|
|
1024
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
1025
|
+
options: { failureThreshold: 2 }
|
|
1026
|
+
});
|
|
1027
|
+
await circuit.recordFailure("remote.example");
|
|
1028
|
+
assertEquals(kv.listCalls, 0);
|
|
1029
|
+
assertEquals(await circuit.getState("remote.example"), {
|
|
1030
|
+
state: "closed",
|
|
1031
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
test("CircuitBreaker does not overwrite a changed legacy sweep marker", async () => {
|
|
1035
|
+
const kv = new MarkerChangedDuringListKvStore();
|
|
1036
|
+
await kv.set([
|
|
1037
|
+
"_fedify",
|
|
1038
|
+
"circuit",
|
|
1039
|
+
"stale.example"
|
|
1040
|
+
], {
|
|
1041
|
+
state: "closed",
|
|
1042
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1043
|
+
});
|
|
1044
|
+
const circuit = new CircuitBreaker({
|
|
1045
|
+
kv,
|
|
1046
|
+
prefix: ["_fedify", "circuit"],
|
|
1047
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
1048
|
+
options: { failureThreshold: 2 }
|
|
1049
|
+
});
|
|
1050
|
+
await circuit.recordFailure("remote.example");
|
|
1051
|
+
await circuit.pendingSweep;
|
|
1052
|
+
assertEquals(await kv.get([
|
|
1053
|
+
"_fedify",
|
|
1054
|
+
"circuit",
|
|
1055
|
+
"__fedify_meta",
|
|
1056
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1057
|
+
]), { state: "final" });
|
|
1058
|
+
});
|
|
1059
|
+
test("CircuitBreaker bounds legacy sweep CAS retries", async () => {
|
|
1060
|
+
const kv = new ConflictingSweepMarkerKvStore();
|
|
1061
|
+
const circuit = new CircuitBreaker({
|
|
1062
|
+
kv,
|
|
1063
|
+
prefix: ["_fedify", "circuit"],
|
|
1064
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
1065
|
+
options: { failureThreshold: 2 }
|
|
1066
|
+
});
|
|
1067
|
+
await circuit.getState("remote.example");
|
|
1068
|
+
await circuit.pendingSweep;
|
|
1069
|
+
assertEquals(kv.attempts, 3);
|
|
1070
|
+
assertEquals(kv.listCalls, 0);
|
|
1071
|
+
});
|
|
1072
|
+
test("CircuitBreaker retries expired legacy sweep markers", async () => {
|
|
1073
|
+
const kv = new CountingSweepKvStore();
|
|
1074
|
+
await kv.set([
|
|
1075
|
+
"_fedify",
|
|
1076
|
+
"circuit",
|
|
1077
|
+
"__fedify_meta",
|
|
1078
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1079
|
+
], {
|
|
1080
|
+
state: "sweeping",
|
|
1081
|
+
started: "2026-05-25T00:00:00Z",
|
|
1082
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
1083
|
+
});
|
|
1084
|
+
await kv.set([
|
|
1085
|
+
"_fedify",
|
|
1086
|
+
"circuit",
|
|
1087
|
+
"stale.example"
|
|
1088
|
+
], {
|
|
1089
|
+
state: "closed",
|
|
1090
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1091
|
+
});
|
|
1092
|
+
const circuit = new CircuitBreaker({
|
|
1093
|
+
kv,
|
|
1094
|
+
prefix: ["_fedify", "circuit"],
|
|
1095
|
+
now: () => Temporal.Instant.from("2026-05-25T00:06:00Z"),
|
|
1096
|
+
options: { failureThreshold: 2 }
|
|
1097
|
+
});
|
|
1098
|
+
await circuit.recordFailure("remote.example");
|
|
1099
|
+
await circuit.pendingSweep;
|
|
1100
|
+
assertEquals(kv.listCalls, 1);
|
|
1101
|
+
assertEquals(await kv.get([
|
|
1102
|
+
"_fedify",
|
|
1103
|
+
"circuit",
|
|
1104
|
+
"stale.example"
|
|
1105
|
+
]), {
|
|
1106
|
+
state: "closed",
|
|
1107
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
1108
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
1109
|
+
});
|
|
1110
|
+
assertEquals(await kv.get([
|
|
1111
|
+
"_fedify",
|
|
1112
|
+
"circuit",
|
|
1113
|
+
"__fedify_meta",
|
|
1114
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1115
|
+
]), {
|
|
1116
|
+
state: "done",
|
|
1117
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
test("CircuitBreaker caches completed legacy sweep markers", async () => {
|
|
1121
|
+
const doneKv = new CountingSweepKvStore();
|
|
1122
|
+
await doneKv.set([
|
|
1123
|
+
"_fedify",
|
|
1124
|
+
"circuit",
|
|
1125
|
+
"__fedify_meta",
|
|
1126
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1127
|
+
], {
|
|
1128
|
+
state: "done",
|
|
1129
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
1130
|
+
});
|
|
1131
|
+
let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
|
|
1132
|
+
const doneCircuit = new CircuitBreaker({
|
|
1133
|
+
kv: doneKv,
|
|
1134
|
+
prefix: ["_fedify", "circuit"],
|
|
1135
|
+
now: () => now,
|
|
1136
|
+
options: { failureThreshold: 2 }
|
|
1137
|
+
});
|
|
1138
|
+
await doneCircuit.recordFailure("remote.example");
|
|
1139
|
+
await doneCircuit.pendingSweep;
|
|
1140
|
+
await doneCircuit.recordFailure("another.example");
|
|
1141
|
+
await doneCircuit.pendingSweep;
|
|
1142
|
+
assertEquals(doneKv.markerGetCalls, 1);
|
|
1143
|
+
now = Temporal.Instant.from("2026-06-02T00:00:00Z");
|
|
1144
|
+
await doneCircuit.recordFailure("final.example");
|
|
1145
|
+
await doneCircuit.pendingSweep;
|
|
1146
|
+
assertEquals(doneKv.markerGetCalls, 2);
|
|
1147
|
+
const finalKv = new CountingSweepKvStore();
|
|
1148
|
+
await markCircuitBreakerLegacySweepDone(finalKv);
|
|
1149
|
+
const finalCircuit = new CircuitBreaker({
|
|
1150
|
+
kv: finalKv,
|
|
1151
|
+
prefix: ["_fedify", "circuit"],
|
|
1152
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
1153
|
+
options: { failureThreshold: 2 }
|
|
1154
|
+
});
|
|
1155
|
+
await finalCircuit.recordFailure("remote.example");
|
|
1156
|
+
await finalCircuit.pendingSweep;
|
|
1157
|
+
await finalCircuit.recordFailure("another.example");
|
|
1158
|
+
await finalCircuit.pendingSweep;
|
|
1159
|
+
assertEquals(finalKv.markerGetCalls, 1);
|
|
1160
|
+
});
|
|
1161
|
+
test("CircuitBreaker retries malformed legacy sweep markers", async () => {
|
|
1162
|
+
const kv = new CountingSweepKvStore();
|
|
1163
|
+
await kv.set([
|
|
1164
|
+
"_fedify",
|
|
1165
|
+
"circuit",
|
|
1166
|
+
"__fedify_meta",
|
|
1167
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1168
|
+
], {
|
|
1169
|
+
state: "done",
|
|
1170
|
+
retryUntil: "not an instant"
|
|
1171
|
+
});
|
|
1172
|
+
await kv.set([
|
|
1173
|
+
"_fedify",
|
|
1174
|
+
"circuit",
|
|
1175
|
+
"stale.example"
|
|
1176
|
+
], {
|
|
1177
|
+
state: "closed",
|
|
1178
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1179
|
+
});
|
|
1180
|
+
const circuit = new CircuitBreaker({
|
|
1181
|
+
kv,
|
|
1182
|
+
prefix: ["_fedify", "circuit"],
|
|
1183
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
1184
|
+
options: { failureThreshold: 2 }
|
|
1185
|
+
});
|
|
1186
|
+
await circuit.recordFailure("remote.example");
|
|
1187
|
+
await circuit.pendingSweep;
|
|
1188
|
+
assertEquals(kv.listCalls, 1);
|
|
1189
|
+
assertEquals(await kv.get([
|
|
1190
|
+
"_fedify",
|
|
1191
|
+
"circuit",
|
|
1192
|
+
"stale.example"
|
|
1193
|
+
]), {
|
|
1194
|
+
state: "closed",
|
|
1195
|
+
failures: ["2026-05-25T00:00:00Z"],
|
|
1196
|
+
__fedifyCircuitBreakerStateVersion: 1
|
|
1197
|
+
});
|
|
1198
|
+
assertEquals(await circuit.getState("remote.example"), {
|
|
1199
|
+
state: "closed",
|
|
1200
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1201
|
+
});
|
|
1202
|
+
assertEquals(await kv.get([
|
|
1203
|
+
"_fedify",
|
|
1204
|
+
"circuit",
|
|
1205
|
+
"__fedify_meta",
|
|
1206
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1207
|
+
]), {
|
|
1208
|
+
state: "done",
|
|
1209
|
+
retryUntil: "2026-06-01T00:00:00Z"
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
test("CircuitBreaker retries legacy sweep after transient failures", async () => {
|
|
1213
|
+
const kv = new FailingOnceSweepKvStore();
|
|
1214
|
+
const circuit = new CircuitBreaker({
|
|
1215
|
+
kv,
|
|
1216
|
+
prefix: ["_fedify", "circuit"],
|
|
1217
|
+
now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
|
|
1218
|
+
options: { failureThreshold: 2 }
|
|
1219
|
+
});
|
|
1220
|
+
await circuit.recordFailure("remote.example");
|
|
1221
|
+
await circuit.pendingSweep;
|
|
1222
|
+
assertEquals(await kv.get([
|
|
1223
|
+
"_fedify",
|
|
1224
|
+
"circuit",
|
|
1225
|
+
"__fedify_meta",
|
|
1226
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1227
|
+
]), void 0);
|
|
1228
|
+
assertEquals(kv.deletedKeys, [[
|
|
1229
|
+
"_fedify",
|
|
1230
|
+
"circuit",
|
|
1231
|
+
"__fedify_meta",
|
|
1232
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
1233
|
+
]]);
|
|
1234
|
+
assertEquals(await circuit.getState("remote.example"), {
|
|
1235
|
+
state: "closed",
|
|
1236
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1237
|
+
});
|
|
1238
|
+
await circuit.recordFailure("another.example");
|
|
1239
|
+
await circuit.pendingSweep;
|
|
1240
|
+
assertEquals(kv.listCalls, 2);
|
|
1241
|
+
assertEquals(await circuit.getState("another.example"), {
|
|
1242
|
+
state: "closed",
|
|
1243
|
+
failures: ["2026-05-25T00:00:00Z"]
|
|
1244
|
+
});
|
|
1245
|
+
});
|
|
415
1246
|
test("CircuitBreaker prunes stale closed failure history", async () => {
|
|
416
1247
|
const kv = new MemoryKvStore();
|
|
417
1248
|
let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
|