@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.36

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.
Files changed (138) hide show
  1. package/README.md +16 -7
  2. package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
  3. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
  4. package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
  5. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
  6. package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
  7. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
  8. package/dist/{builder-yAlpiIqP.mjs → builder-DSZ7FZmg.mjs} +96 -42
  9. package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
  10. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  11. package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
  12. package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
  13. package/dist/compat/mod.d.cts +2 -1
  14. package/dist/compat/mod.d.ts +2 -3
  15. package/dist/compat/mod.js +3 -3
  16. package/dist/compat/outgoing-jsonld.test.mjs +4 -4
  17. package/dist/compat/public-audience.test.mjs +4 -4
  18. package/dist/compat/transformers.test.mjs +5 -5
  19. package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
  20. package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
  21. package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
  22. package/dist/{deno-XBVlKnOX.mjs → deno-DCdM7d93.mjs} +1 -1
  23. package/dist/{docloader-DPp-X6gk.mjs → docloader-B7mVwA5_.mjs} +4 -4
  24. package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
  25. package/dist/federation/builder.test.mjs +163 -11
  26. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  27. package/dist/federation/circuit-breaker.test.mjs +446 -0
  28. package/dist/federation/collection.test.mjs +3 -3
  29. package/dist/federation/handler.test.mjs +1770 -29
  30. package/dist/federation/idempotency.test.mjs +5 -5
  31. package/dist/federation/inbox.test.mjs +4 -4
  32. package/dist/federation/keycache.test.mjs +5 -5
  33. package/dist/federation/kv.test.mjs +2 -2
  34. package/dist/federation/metrics.test.d.mts +2 -0
  35. package/dist/federation/metrics.test.mjs +634 -0
  36. package/dist/federation/middleware.test.mjs +4036 -146
  37. package/dist/federation/mod.cjs +195 -14
  38. package/dist/federation/mod.d.cts +6 -4
  39. package/dist/federation/mod.d.ts +6 -6
  40. package/dist/federation/mod.js +192 -14
  41. package/dist/federation/mq.test.mjs +176 -15
  42. package/dist/federation/negotiation.test.mjs +4 -4
  43. package/dist/federation/retry.test.mjs +3 -3
  44. package/dist/federation/router.test.mjs +190 -9
  45. package/dist/federation/send.test.mjs +153 -15
  46. package/dist/federation/temporal.test.d.mts +2 -0
  47. package/dist/federation/temporal.test.mjs +71 -0
  48. package/dist/federation/webfinger.test.mjs +150 -5
  49. package/dist/{http-CSlLA54v.mjs → http-BufbkLuW.mjs} +146 -47
  50. package/dist/{http-B4rrzxtg.js → http-Dh18nwJg.js} +994 -64
  51. package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
  52. package/dist/{http-BrGqJDXL.cjs → http-gOiudB3g.cjs} +1099 -61
  53. package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
  54. package/dist/{key-Bzx--sHD.mjs → key-DZdKLJXT.mjs} +43 -18
  55. package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
  56. package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BuK_nlpY.cjs} +20 -3
  57. package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-C7SQnkGI.mjs} +21 -3
  58. package/dist/{kv-cache-EMIqoIuB.js → kv-cache-DNDSb6Po.js} +20 -3
  59. package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
  60. package/dist/ld-0Lsznacf.mjs +626 -0
  61. package/dist/metrics-BHeWd24f.mjs +815 -0
  62. package/dist/{middleware-zjZ6AFCW.mjs → middleware-BNANvDh2.mjs} +1 -1
  63. package/dist/{middleware-nidH7VZH.js → middleware-BgzRkDcm.js} +2260 -556
  64. package/dist/{middleware-An4DjES4.cjs → middleware-D-iES1nQ.cjs} +2318 -623
  65. package/dist/{middleware-K1g6bEkV.mjs → middleware-EV6J_eAc.mjs} +1639 -373
  66. package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
  67. package/dist/mod-C0F6kvgS.d.cts +182 -0
  68. package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
  69. package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
  70. package/dist/mod-vPYVoa5n.d.ts +182 -0
  71. package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
  72. package/dist/mod.cjs +9 -6
  73. package/dist/mod.d.cts +12 -10
  74. package/dist/mod.d.ts +12 -12
  75. package/dist/mod.js +11 -11
  76. package/dist/mq-D-nlpY04.d.ts +208 -0
  77. package/dist/mq-D8uSFzxe.d.cts +208 -0
  78. package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
  79. package/dist/nodeinfo/client.test.mjs +4 -4
  80. package/dist/nodeinfo/handler.test.mjs +4 -4
  81. package/dist/nodeinfo/mod.d.cts +2 -1
  82. package/dist/nodeinfo/mod.d.ts +2 -3
  83. package/dist/nodeinfo/mod.js +3 -3
  84. package/dist/nodeinfo/types.test.mjs +3 -3
  85. package/dist/otel/exporter.test.mjs +28 -25
  86. package/dist/otel/mod.cjs +6 -5
  87. package/dist/otel/mod.d.cts +5 -3
  88. package/dist/otel/mod.d.ts +5 -5
  89. package/dist/otel/mod.js +8 -7
  90. package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
  91. package/dist/{owner-C-tqfvxn.mjs → owner-Cl-1cAR8.mjs} +2 -2
  92. package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
  93. package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
  94. package/dist/{proof-Bbbwf8_x.js → proof-CyXndf6-.js} +378 -15
  95. package/dist/{proof-BlmRPzrK.mjs → proof-Dnz8jtiQ.mjs} +26 -8
  96. package/dist/{proof-BW89QMVB.cjs → proof-tz35unOj.cjs} +432 -15
  97. package/dist/runtime/mod.d.cts +1 -0
  98. package/dist/runtime/mod.d.ts +1 -2
  99. package/dist/runtime/mod.js +3 -3
  100. package/dist/{send-05pJLcb6.mjs → send-DIbBYN5P.mjs} +72 -26
  101. package/dist/sig/http.test.mjs +356 -33
  102. package/dist/sig/key.test.mjs +103 -6
  103. package/dist/sig/ld.test.mjs +696 -7
  104. package/dist/sig/mod.cjs +2 -2
  105. package/dist/sig/mod.d.cts +4 -3
  106. package/dist/sig/mod.d.ts +4 -5
  107. package/dist/sig/mod.js +4 -4
  108. package/dist/sig/owner.test.mjs +6 -6
  109. package/dist/sig/proof.test.mjs +169 -8
  110. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
  111. package/dist/temporal-BEwob1Vg.mjs +95 -0
  112. package/dist/testing/mod.d.mts +110 -10
  113. package/dist/testing/mod.mjs +1 -2
  114. package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
  115. package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
  116. package/dist/utils/docloader.test.mjs +6 -6
  117. package/dist/utils/kv-cache.test.mjs +67 -2
  118. package/dist/utils/mod.cjs +1 -1
  119. package/dist/utils/mod.d.cts +2 -1
  120. package/dist/utils/mod.d.ts +2 -3
  121. package/dist/utils/mod.js +3 -3
  122. package/dist/vocab/cjs.test.mjs +1 -1
  123. package/dist/vocab/mod.d.cts +1 -0
  124. package/dist/vocab/mod.d.ts +1 -2
  125. package/dist/vocab/mod.js +2 -2
  126. package/package.json +16 -16
  127. package/dist/ld-DR78beiv.mjs +0 -279
  128. package/dist/middleware-BFBaR0mF.cjs +0 -4
  129. package/dist/mod-2d12ffz3.d.ts +0 -64
  130. package/dist/mod-D35TRn09.d.cts +0 -62
  131. package/dist/router-CrMLXoOr.mjs +0 -114
  132. package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  133. package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  134. package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
  135. package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
  136. package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
  137. package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
  138. /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
@@ -0,0 +1,446 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
+ import "../std__assert-BBjXFNOb.mjs";
6
+ import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
7
+ import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
8
+ import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-CSWsyoef.mjs";
9
+ import { test } from "@fedify/fixture";
10
+ //#region src/federation/circuit-breaker.test.ts
11
+ var AlwaysConflictingKvStore = class extends MemoryKvStore {
12
+ attempts = 0;
13
+ cas(_key, _expectedValue, _newValue, _options) {
14
+ this.attempts++;
15
+ if (this.attempts > 10) throw new Error("beforeSend did not stop retrying CAS misses");
16
+ return Promise.resolve(false);
17
+ }
18
+ };
19
+ var CountingCasKvStore = class extends MemoryKvStore {
20
+ attempts = 0;
21
+ cas(key, expectedValue, newValue, options) {
22
+ this.attempts++;
23
+ return super.cas(key, expectedValue, newValue, options);
24
+ }
25
+ };
26
+ test("normalizeCircuitBreakerOptions() uses numeric failure policy", () => {
27
+ const options = normalizeCircuitBreakerOptions({
28
+ failureThreshold: 3,
29
+ failureWindow: { minutes: 10 }
30
+ });
31
+ const failures = [
32
+ Temporal.Instant.from("2026-05-25T00:00:00Z"),
33
+ Temporal.Instant.from("2026-05-25T00:05:00Z"),
34
+ Temporal.Instant.from("2026-05-25T00:10:00Z")
35
+ ];
36
+ assertEquals(options.failure(failures.slice(0, 2)), false);
37
+ assertEquals(options.failure(failures), true);
38
+ assertEquals(options.failure([
39
+ Temporal.Instant.from("2026-05-25T00:00:00Z"),
40
+ Temporal.Instant.from("2026-05-25T00:11:00Z"),
41
+ Temporal.Instant.from("2026-05-25T00:12:00Z")
42
+ ]), false);
43
+ assertEquals(options.pruneFailures([
44
+ Temporal.Instant.from("2026-05-25T00:00:00Z"),
45
+ Temporal.Instant.from("2026-05-25T00:09:00Z"),
46
+ Temporal.Instant.from("2026-05-25T00:10:00Z"),
47
+ Temporal.Instant.from("2026-05-25T00:11:00Z"),
48
+ Temporal.Instant.from("2026-05-25T00:12:00Z")
49
+ ], Temporal.Instant.from("2026-05-25T00:12:00Z")).map((t) => t.toString()), [
50
+ "2026-05-25T00:10:00Z",
51
+ "2026-05-25T00:11:00Z",
52
+ "2026-05-25T00:12:00Z"
53
+ ]);
54
+ });
55
+ test("normalizeCircuitBreakerOptions() validates numeric failure policy", () => {
56
+ assertThrows(() => normalizeCircuitBreakerOptions({ failureThreshold: 0 }), TypeError, "failureThreshold");
57
+ assertThrows(() => normalizeCircuitBreakerOptions({ failureThreshold: 1.5 }), TypeError, "failureThreshold");
58
+ });
59
+ test("normalizeCircuitBreakerOptions() truncates sub-millisecond durations", () => {
60
+ assertEquals(normalizeCircuitBreakerOptions({ recoveryDelay: {
61
+ milliseconds: 1,
62
+ nanoseconds: 5e5
63
+ } }).recoveryDelay, Temporal.Duration.from({ milliseconds: 1 }));
64
+ });
65
+ test("normalizeCircuitBreakerOptions() validates positive durations", () => {
66
+ assertThrows(() => normalizeCircuitBreakerOptions({ recoveryDelay: { seconds: 0 } }), RangeError, "recoveryDelay");
67
+ assertThrows(() => normalizeCircuitBreakerOptions({ heldActivityTtl: { seconds: 0 } }), RangeError, "heldActivityTtl");
68
+ assertThrows(() => normalizeCircuitBreakerOptions({ failureWindow: { seconds: 0 } }), RangeError, "failureWindow");
69
+ assertThrows(() => normalizeCircuitBreakerOptions({ releaseInterval: { seconds: 0 } }), RangeError, "releaseInterval");
70
+ assertThrows(() => normalizeCircuitBreakerOptions({ releaseInterval: { nanoseconds: 5e5 } }), RangeError, "releaseInterval");
71
+ });
72
+ test("normalizeCircuitBreakerOptions() accepts callback failure policy", () => {
73
+ const options = normalizeCircuitBreakerOptions({ failure: (timestamps) => timestamps.length >= 2 });
74
+ const base = Temporal.Instant.from("2026-05-25T00:00:00Z");
75
+ const failures = Array.from({ length: 105 }, (_, i) => base.add({ minutes: i }));
76
+ assertEquals(options.failure([Temporal.Instant.from("2026-05-25T00:00:00Z")]), false);
77
+ assertEquals(options.failure([Temporal.Instant.from("2026-05-25T00:00:00Z"), Temporal.Instant.from("2026-05-25T00:01:00Z")]), true);
78
+ assertEquals(options.pruneFailures(failures, base.add({ minutes: 105 })).map((t) => t.toString()), failures.slice(-100).map((t) => t.toString()));
79
+ });
80
+ test("parseCircuitBreakerKvState() validates stored shape", () => {
81
+ assertEquals(parseCircuitBreakerKvState({
82
+ state: "open",
83
+ failures: ["2026-05-25T00:00:00Z"],
84
+ opened: "2026-05-25T00:00:00Z",
85
+ halfOpened: "2026-05-25T00:00:00Z"
86
+ }), {
87
+ state: "open",
88
+ failures: ["2026-05-25T00:00:00Z"],
89
+ opened: "2026-05-25T00:00:00Z",
90
+ halfOpened: "2026-05-25T00:00:00Z"
91
+ });
92
+ assertEquals(parseCircuitBreakerKvState({ state: "open" }), void 0);
93
+ assertEquals(parseCircuitBreakerKvState({
94
+ state: "other",
95
+ failures: []
96
+ }), void 0);
97
+ assertEquals(parseCircuitBreakerKvState({
98
+ state: "open",
99
+ failures: [],
100
+ opened: 1
101
+ }), void 0);
102
+ assertEquals(parseCircuitBreakerKvState({
103
+ state: "open",
104
+ failures: ["not an instant"]
105
+ }), void 0);
106
+ assertEquals(parseCircuitBreakerKvState({
107
+ state: "open",
108
+ failures: [],
109
+ halfOpened: "not an instant"
110
+ }), void 0);
111
+ });
112
+ test("CircuitBreaker opens, probes, closes, and drops held activities", async () => {
113
+ const kv = new MemoryKvStore();
114
+ let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
115
+ const transitions = [];
116
+ const circuit = new CircuitBreaker({
117
+ kv,
118
+ prefix: ["_fedify", "circuit"],
119
+ now: () => now,
120
+ options: {
121
+ failureThreshold: 2,
122
+ failureWindow: { minutes: 10 },
123
+ recoveryDelay: { minutes: 30 },
124
+ heldActivityTtl: { days: 7 },
125
+ onStateChange(host, previousState, newState) {
126
+ transitions.push(`${host}:${previousState}->${newState}`);
127
+ }
128
+ }
129
+ });
130
+ await circuit.recordFailure("remote.example");
131
+ assertEquals(await circuit.getState("remote.example"), {
132
+ state: "closed",
133
+ failures: ["2026-05-25T00:00:00Z"]
134
+ });
135
+ now = Temporal.Instant.from("2026-05-25T00:05:00Z");
136
+ await circuit.recordFailure("remote.example");
137
+ assertEquals(await circuit.getState("remote.example"), {
138
+ state: "open",
139
+ failures: ["2026-05-25T00:00:00Z", "2026-05-25T00:05:00Z"],
140
+ opened: "2026-05-25T00:05:00Z"
141
+ });
142
+ assertEquals(transitions, ["remote.example:closed->open"]);
143
+ let decision = await circuit.beforeSend("remote.example", {});
144
+ assertEquals(decision, {
145
+ type: "hold",
146
+ delay: Temporal.Duration.from({ minutes: 30 }),
147
+ heldSince: now,
148
+ state: "open"
149
+ });
150
+ now = Temporal.Instant.from("2026-05-25T00:35:00Z");
151
+ decision = await circuit.beforeSend("remote.example", {});
152
+ assertEquals(decision, {
153
+ type: "send",
154
+ probe: true,
155
+ stateChange: {
156
+ previousState: "open",
157
+ newState: "half-open"
158
+ }
159
+ });
160
+ assertEquals(await circuit.getState("remote.example"), {
161
+ state: "half-open",
162
+ failures: ["2026-05-25T00:00:00Z", "2026-05-25T00:05:00Z"],
163
+ opened: "2026-05-25T00:05:00Z",
164
+ halfOpened: "2026-05-25T00:35:00Z"
165
+ });
166
+ await circuit.recordSuccess("remote.example");
167
+ assertEquals(await circuit.getState("remote.example"), void 0);
168
+ assertEquals(transitions, [
169
+ "remote.example:closed->open",
170
+ "remote.example:open->half-open",
171
+ "remote.example:half-open->closed"
172
+ ]);
173
+ decision = await circuit.beforeSend("remote.example", { circuitHeldSince: "2026-05-17T00:00:00Z" });
174
+ assertEquals(decision, {
175
+ type: "drop",
176
+ heldSince: Temporal.Instant.from("2026-05-17T00:00:00Z")
177
+ });
178
+ await kv.set([
179
+ "_fedify",
180
+ "circuit",
181
+ "remote.example"
182
+ ], {
183
+ state: "open",
184
+ failures: ["2026-05-25T00:00:00Z", "2026-05-25T00:05:00Z"],
185
+ opened: "2026-05-25T00:05:00Z"
186
+ });
187
+ decision = await circuit.beforeSend("remote.example", { circuitHeldSince: "2026-05-17T00:00:00Z" });
188
+ assertEquals(decision, {
189
+ type: "drop",
190
+ heldSince: Temporal.Instant.from("2026-05-17T00:00:00Z")
191
+ });
192
+ });
193
+ test("CircuitBreaker recovers stale half-open probes", async () => {
194
+ const kv = new MemoryKvStore();
195
+ let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
196
+ const circuit = new CircuitBreaker({
197
+ kv,
198
+ prefix: ["_fedify", "circuit"],
199
+ now: () => now,
200
+ options: {
201
+ recoveryDelay: { seconds: 30 },
202
+ releaseInterval: { seconds: 5 }
203
+ }
204
+ });
205
+ await kv.set([
206
+ "_fedify",
207
+ "circuit",
208
+ "remote.example"
209
+ ], {
210
+ state: "half-open",
211
+ failures: ["2026-05-24T23:00:00Z"],
212
+ opened: "2026-05-24T23:00:00Z",
213
+ halfOpened: "2026-05-24T23:59:54Z"
214
+ });
215
+ let decision = await circuit.beforeSend("remote.example", {});
216
+ assertEquals(decision, {
217
+ type: "hold",
218
+ state: "half-open",
219
+ delay: Temporal.Duration.from({ seconds: 5 }),
220
+ heldSince: now
221
+ });
222
+ assertEquals(await circuit.getState("remote.example"), {
223
+ state: "half-open",
224
+ failures: ["2026-05-24T23:00:00Z"],
225
+ opened: "2026-05-24T23:00:00Z",
226
+ halfOpened: "2026-05-24T23:59:54Z"
227
+ });
228
+ now = Temporal.Instant.from("2026-05-25T00:00:30Z");
229
+ decision = await circuit.beforeSend("remote.example", {});
230
+ assertEquals(decision, {
231
+ type: "send",
232
+ probe: true
233
+ });
234
+ assertEquals(await circuit.getState("remote.example"), {
235
+ state: "half-open",
236
+ failures: ["2026-05-24T23:00:00Z"],
237
+ opened: "2026-05-24T23:00:00Z",
238
+ halfOpened: "2026-05-25T00:00:30Z"
239
+ });
240
+ });
241
+ test("CircuitBreaker caps held delays at activity TTL", async () => {
242
+ const kv = new MemoryKvStore();
243
+ const now = Temporal.Instant.from("2026-05-25T00:05:00Z");
244
+ const circuit = new CircuitBreaker({
245
+ kv,
246
+ prefix: ["_fedify", "circuit"],
247
+ now: () => now,
248
+ options: {
249
+ recoveryDelay: { minutes: 30 },
250
+ heldActivityTtl: { minutes: 10 },
251
+ releaseInterval: { minutes: 10 }
252
+ }
253
+ });
254
+ await kv.set([
255
+ "_fedify",
256
+ "circuit",
257
+ "new-open.example"
258
+ ], {
259
+ state: "open",
260
+ failures: ["2026-05-25T00:00:00Z"],
261
+ opened: "2026-05-25T00:00:00Z"
262
+ });
263
+ let decision = await circuit.beforeSend("new-open.example", {});
264
+ assertEquals(decision.type, "hold");
265
+ if (decision.type === "hold") {
266
+ assertEquals(decision.state, "open");
267
+ assertEquals(decision.delay.total({ unit: "minute" }), 10);
268
+ assertEquals(decision.heldSince.toString(), "2026-05-25T00:05:00Z");
269
+ }
270
+ await kv.set([
271
+ "_fedify",
272
+ "circuit",
273
+ "open.example"
274
+ ], {
275
+ state: "open",
276
+ failures: ["2026-05-25T00:00:00Z"],
277
+ opened: "2026-05-25T00:00:00Z"
278
+ });
279
+ decision = await circuit.beforeSend("open.example", { circuitHeldSince: "2026-05-25T00:00:00Z" });
280
+ assertEquals(decision.type, "hold");
281
+ if (decision.type === "hold") {
282
+ assertEquals(decision.state, "open");
283
+ assertEquals(decision.delay.total({ unit: "minute" }), 5);
284
+ assertEquals(decision.heldSince.toString(), "2026-05-25T00:00:00Z");
285
+ }
286
+ await kv.set([
287
+ "_fedify",
288
+ "circuit",
289
+ "half-open.example"
290
+ ], {
291
+ state: "half-open",
292
+ failures: ["2026-05-25T00:00:00Z"],
293
+ opened: "2026-05-25T00:00:00Z",
294
+ halfOpened: "2026-05-25T00:00:00Z"
295
+ });
296
+ decision = await circuit.beforeSend("half-open.example", { circuitHeldSince: "2026-05-25T00:00:00Z" });
297
+ assertEquals(decision.type, "hold");
298
+ if (decision.type === "hold") {
299
+ assertEquals(decision.state, "half-open");
300
+ assertEquals(decision.delay.total({ unit: "minute" }), 5);
301
+ assertEquals(decision.heldSince.toString(), "2026-05-25T00:00:00Z");
302
+ }
303
+ });
304
+ test("CircuitBreaker ignores malformed held timestamps", async () => {
305
+ const kv = new MemoryKvStore();
306
+ const now = Temporal.Instant.from("2026-05-25T00:05:00Z");
307
+ const circuit = new CircuitBreaker({
308
+ kv,
309
+ prefix: ["_fedify", "circuit"],
310
+ now: () => now,
311
+ options: { recoveryDelay: { minutes: 30 } }
312
+ });
313
+ await kv.set([
314
+ "_fedify",
315
+ "circuit",
316
+ "malformed-held.example"
317
+ ], {
318
+ state: "open",
319
+ failures: ["2026-05-25T00:00:00Z"],
320
+ opened: "2026-05-25T00:00:00Z"
321
+ });
322
+ assertEquals(await circuit.beforeSend("malformed-held.example", { circuitHeldSince: "not an instant" }), {
323
+ type: "hold",
324
+ state: "open",
325
+ delay: Temporal.Duration.from({ minutes: 25 }),
326
+ heldSince: now
327
+ });
328
+ });
329
+ test("CircuitBreaker bounds beforeSend CAS retries", async () => {
330
+ let kv = new AlwaysConflictingKvStore();
331
+ const now = Temporal.Instant.from("2026-05-25T00:30:00Z");
332
+ let circuit = new CircuitBreaker({
333
+ kv,
334
+ prefix: ["_fedify", "circuit"],
335
+ now: () => now,
336
+ options: {
337
+ recoveryDelay: { minutes: 30 },
338
+ releaseInterval: { seconds: 5 }
339
+ }
340
+ });
341
+ await kv.set([
342
+ "_fedify",
343
+ "circuit",
344
+ "open.example"
345
+ ], {
346
+ state: "open",
347
+ failures: ["2026-05-25T00:00:00Z"],
348
+ opened: "2026-05-25T00:00:00Z"
349
+ });
350
+ let decision = await circuit.beforeSend("open.example", {});
351
+ assertEquals(kv.attempts, 10);
352
+ assertEquals(decision, {
353
+ type: "hold",
354
+ state: "open",
355
+ delay: Temporal.Duration.from({ seconds: 5 }),
356
+ heldSince: now
357
+ });
358
+ kv = new AlwaysConflictingKvStore();
359
+ circuit = new CircuitBreaker({
360
+ kv,
361
+ prefix: ["_fedify", "circuit"],
362
+ now: () => now,
363
+ options: {
364
+ recoveryDelay: { minutes: 30 },
365
+ releaseInterval: { seconds: 5 }
366
+ }
367
+ });
368
+ await kv.set([
369
+ "_fedify",
370
+ "circuit",
371
+ "half-open.example"
372
+ ], {
373
+ state: "half-open",
374
+ failures: ["2026-05-25T00:00:00Z"],
375
+ opened: "2026-05-25T00:00:00Z",
376
+ halfOpened: "2026-05-25T00:00:00Z"
377
+ });
378
+ decision = await circuit.beforeSend("half-open.example", {});
379
+ assertEquals(kv.attempts, 10);
380
+ assertEquals(decision, {
381
+ type: "hold",
382
+ state: "half-open",
383
+ delay: Temporal.Duration.from({ seconds: 5 }),
384
+ heldSince: now
385
+ });
386
+ });
387
+ test("CircuitBreaker skips recording failures for open circuits", async () => {
388
+ const kv = new CountingCasKvStore();
389
+ const circuit = new CircuitBreaker({
390
+ kv,
391
+ prefix: ["_fedify", "circuit"],
392
+ now: () => Temporal.Instant.from("2026-05-25T00:01:00Z")
393
+ });
394
+ await kv.set([
395
+ "_fedify",
396
+ "circuit",
397
+ "open.example"
398
+ ], {
399
+ state: "open",
400
+ failures: ["2026-05-25T00:00:00Z"],
401
+ opened: "2026-05-25T00:00:00Z"
402
+ });
403
+ assertEquals(await circuit.recordFailure("open.example"), void 0);
404
+ assertEquals(kv.attempts, 0);
405
+ assertEquals(await kv.get([
406
+ "_fedify",
407
+ "circuit",
408
+ "open.example"
409
+ ]), {
410
+ state: "open",
411
+ failures: ["2026-05-25T00:00:00Z"],
412
+ opened: "2026-05-25T00:00:00Z"
413
+ });
414
+ });
415
+ test("CircuitBreaker prunes stale closed failure history", async () => {
416
+ const kv = new MemoryKvStore();
417
+ let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
418
+ const circuit = new CircuitBreaker({
419
+ kv,
420
+ prefix: ["_fedify", "circuit"],
421
+ now: () => now,
422
+ options: {
423
+ failureThreshold: 2,
424
+ failureWindow: { minutes: 10 }
425
+ }
426
+ });
427
+ await circuit.recordFailure("sporadic.example");
428
+ assertEquals(await circuit.getState("sporadic.example"), {
429
+ state: "closed",
430
+ failures: ["2026-05-25T00:00:00Z"]
431
+ });
432
+ now = Temporal.Instant.from("2026-05-25T00:20:00Z");
433
+ await circuit.recordFailure("sporadic.example");
434
+ assertEquals(await circuit.getState("sporadic.example"), {
435
+ state: "closed",
436
+ failures: ["2026-05-25T00:20:00Z"]
437
+ });
438
+ now = Temporal.Instant.from("2026-05-25T00:40:00Z");
439
+ await circuit.recordFailure("sporadic.example");
440
+ assertEquals(await circuit.getState("sporadic.example"), {
441
+ state: "closed",
442
+ failures: ["2026-05-25T00:40:00Z"]
443
+ });
444
+ });
445
+ //#endregion
446
+ export {};
@@ -1,9 +1,9 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-CRDpx_HF.mjs";
6
- import { n as digest, t as buildCollectionSynchronizationHeader } from "../collection-D-HqUuA2.mjs";
4
+ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
+ import "../std__assert-BBjXFNOb.mjs";
6
+ import { n as digest, t as buildCollectionSynchronizationHeader } from "../collection-Cc3DVAhE.mjs";
7
7
  import { test } from "@fedify/fixture";
8
8
  import { decodeHex } from "byte-encodings/hex";
9
9
  //#region src/federation/collection.test.ts