@fedify/fedify 2.3.5-dev.1755 → 2.3.5

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 (61) hide show
  1. package/dist/{builder-De3ym5P9.mjs → builder-DXekGevh.mjs} +2 -2
  2. package/dist/{circuit-breaker-DLS_n1x5.mjs → circuit-breaker-D1fxthDI.mjs} +25 -5
  3. package/dist/{circuit-breaker-test-utils-BVnv3T76.mjs → circuit-breaker-test-utils-CAdwjgMi.mjs} +1 -1
  4. package/dist/compat/mod.d.cts +1 -1
  5. package/dist/compat/mod.d.ts +1 -1
  6. package/dist/compat/transformers.test.mjs +1 -1
  7. package/dist/{context-Bjswu3eC.d.cts → context-BpmLyiPx.d.cts} +3 -2
  8. package/dist/{context-COxc_O-c.d.ts → context-BxVVO5Ro.d.ts} +3 -2
  9. package/dist/{deno-Bb3DoPyg.mjs → deno-BaUlOnHe.mjs} +1 -1
  10. package/dist/{docloader-B9iBabXZ.mjs → docloader-BYH2ymPh.mjs} +13 -9
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/circuit-breaker.test.mjs +184 -21
  13. package/dist/federation/handler.test.mjs +3 -3
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/metrics.test.mjs +1 -1
  16. package/dist/federation/middleware.test.mjs +10 -9
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.d.cts +2 -2
  19. package/dist/federation/mod.d.ts +2 -2
  20. package/dist/federation/mod.js +1 -1
  21. package/dist/federation/send.test.mjs +3 -3
  22. package/dist/federation/temporal.test.mjs +1 -1
  23. package/dist/federation/webfinger.test.mjs +1 -1
  24. package/dist/{http-CNA76HH5.js → http-CD5pDhPT.js} +8 -6
  25. package/dist/{http-DjON0zte.mjs → http-Cm7Y71lD.mjs} +9 -7
  26. package/dist/{http-DbMJX--j.cjs → http-ifHta2Pj.cjs} +8 -6
  27. package/dist/{key-C2zsXBK1.mjs → key-BSbJc-6T.mjs} +3 -3
  28. package/dist/{kv-cache-CZ9RI_NZ.cjs → kv-cache-CQHLWIXt.cjs} +12 -8
  29. package/dist/{kv-cache-M9GujpTX.js → kv-cache-DMI3h8vn.js} +12 -8
  30. package/dist/{kv-cache-CTOxE4cl.mjs → kv-cache-NmAX6AF2.mjs} +1 -1
  31. package/dist/{ld-CB6S9oHd.mjs → ld-DMY2-bQC.mjs} +3 -3
  32. package/dist/{metrics-DbryxSc6.mjs → metrics-B9nzbvNe.mjs} +1 -1
  33. package/dist/{middleware-HRbD2E7D.mjs → middleware-BogX5PzT.mjs} +13 -13
  34. package/dist/{middleware-BZdewTD9.mjs → middleware-C3LRXUcT.mjs} +1 -1
  35. package/dist/{middleware-iPQJA9In.js → middleware-bK-NpoPV.js} +28 -8
  36. package/dist/{middleware-aw8NYL2-.cjs → middleware-rf7LIZPi.cjs} +28 -8
  37. package/dist/{mod-0bIqGrKQ.d.cts → mod-CjERT09F.d.cts} +1 -1
  38. package/dist/{mod-3H9Gym5G.d.ts → mod-Rpy4Uwkk.d.ts} +1 -1
  39. package/dist/mod.cjs +4 -4
  40. package/dist/mod.d.cts +2 -2
  41. package/dist/mod.d.ts +2 -2
  42. package/dist/mod.js +4 -4
  43. package/dist/nodeinfo/handler.test.mjs +1 -1
  44. package/dist/{owner-C5QCaiH6.mjs → owner-Ci8O4Q03.mjs} +2 -2
  45. package/dist/{proof-CCC9itsd.mjs → proof-BSlz1EIk.mjs} +3 -3
  46. package/dist/{proof-yq1G2Z6x.cjs → proof-Ba3iHVt9.cjs} +1 -1
  47. package/dist/{proof-Bzsr8i8m.js → proof-CIOa9FCc.js} +1 -1
  48. package/dist/{send-Zq0hXa8X.mjs → send--ocjLzu4.mjs} +3 -3
  49. package/dist/sig/http.test.mjs +65 -3
  50. package/dist/sig/key.test.mjs +46 -1
  51. package/dist/sig/ld.test.mjs +2 -2
  52. package/dist/sig/mod.cjs +2 -2
  53. package/dist/sig/mod.js +2 -2
  54. package/dist/sig/owner.test.mjs +1 -1
  55. package/dist/sig/proof.test.mjs +1 -1
  56. package/dist/{temporal-Rytw3yy2.mjs → temporal-C9rNBS3M.mjs} +1 -1
  57. package/dist/utils/docloader.test.mjs +35 -2
  58. package/dist/utils/kv-cache.test.mjs +1 -1
  59. package/dist/utils/mod.cjs +1 -1
  60. package/dist/utils/mod.js +1 -1
  61. package/package.json +6 -6
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-Bb3DoPyg.mjs";
4
+ import { n as version, t as name } from "./deno-BaUlOnHe.mjs";
5
5
  import { t as ActivityListenerSet } from "./activity-listener-tztVvlNb.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
@@ -80,7 +80,7 @@ var FederationBuilderImpl = class {
80
80
  * specified.
81
81
  */
82
82
  async build(options) {
83
- const { FederationImpl } = await import("./middleware-BZdewTD9.mjs");
83
+ const { FederationImpl } = await import("./middleware-C3LRXUcT.mjs");
84
84
  const f = new FederationImpl(options);
85
85
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
86
86
  f.router = this.router.clone();
@@ -4,10 +4,11 @@ globalThis.addEventListener = () => {};
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  //#region src/federation/circuit-breaker.ts
6
6
  const MAX_CUSTOM_FAILURE_HISTORY = 100;
7
- const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v1"];
7
+ const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
8
8
  const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
9
9
  const CIRCUIT_BREAKER_STATE_VERSION = 1;
10
10
  const LEGACY_SWEEP_LOCK_TTL = Temporal.Duration.from({ minutes: 5 });
11
+ const LEGACY_SWEEP_RENEW_INTERVAL = Temporal.Duration.from({ minutes: 1 });
11
12
  const LEGACY_SWEEP_RETRY_WINDOW = Temporal.Duration.from({ hours: 168 });
12
13
  const LEGACY_SWEEP_WAIT_INTERVAL = 10;
13
14
  const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
@@ -222,12 +223,20 @@ var CircuitBreaker = class {
222
223
  }
223
224
  async #sweepLegacyStatesImpl() {
224
225
  const markerKey = this.#legacySweepMarkerKey();
225
- const marker = await this.#acquireLegacySweep(markerKey);
226
- if (marker === "done") return;
226
+ const acquired = await this.#acquireLegacySweep(markerKey);
227
+ if (acquired === "done") return;
228
+ let marker = acquired;
229
+ let leaseRenewedAt = this.#now();
227
230
  try {
228
231
  for await (const { key, value } of this.#kv.list(this.#prefix)) {
229
232
  if (key.length !== this.#prefix.length + 1) continue;
230
233
  await this.#migrateLegacyState(key, value);
234
+ if (Temporal.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
235
+ const renewed = await this.#renewLegacySweep(markerKey, marker);
236
+ if (renewed == null) return;
237
+ marker = renewed;
238
+ leaseRenewedAt = this.#now();
239
+ }
231
240
  }
232
241
  } catch (error) {
233
242
  await this.#deleteIfUnchanged(markerKey, marker);
@@ -236,6 +245,14 @@ var CircuitBreaker = class {
236
245
  const finishedMarker = this.#finishLegacySweepMarker(marker);
237
246
  if (await this.#kv.cas(markerKey, marker, finishedMarker)) this.#rememberLegacySweepMarker(finishedMarker);
238
247
  }
248
+ async #renewLegacySweep(markerKey, marker) {
249
+ const renewed = {
250
+ state: "sweeping",
251
+ started: this.#now().toString(),
252
+ retryUntil: getLegacySweepRetryUntil(marker) ?? this.#now().add(LEGACY_SWEEP_RETRY_WINDOW).toString()
253
+ };
254
+ if (await this.#kv.cas(markerKey, marker, renewed, { ttl: LEGACY_SWEEP_LOCK_TTL })) return renewed;
255
+ }
239
256
  #finishLegacySweepMarker(marker) {
240
257
  const retryUntil = getLegacySweepRetryUntil(marker);
241
258
  if (retryUntil != null && Temporal.Instant.compare(this.#now(), Temporal.Instant.from(retryUntil)) < 0) return {
@@ -245,7 +262,10 @@ var CircuitBreaker = class {
245
262
  return { state: "final" };
246
263
  }
247
264
  async #migrateLegacyState(key, value) {
248
- if (isCurrentCircuitBreakerState(value)) return;
265
+ if (isCurrentCircuitBreakerState(value)) {
266
+ await this.#kv.cas(key, value, value, this.#setOptions());
267
+ return;
268
+ }
249
269
  const state = parseCircuitBreakerKvState(value);
250
270
  if (state != null) {
251
271
  await this.#kv.cas(key, value, markCircuitBreakerState(state), this.#setOptions());
@@ -382,7 +402,7 @@ function normalizeCircuitBreakerOptions(options) {
382
402
  } else {
383
403
  failure = options.failure;
384
404
  pruneFailures = (timestamps) => timestamps.slice(-MAX_CUSTOM_FAILURE_HISTORY);
385
- stateTtl = configuredStateTtl;
405
+ stateTtl = configuredStateTtl ?? recoveryDelay.add(heldActivityTtl);
386
406
  }
387
407
  return {
388
408
  failure,
@@ -7,7 +7,7 @@ function markCircuitBreakerLegacySweepDone(kv) {
7
7
  "_fedify",
8
8
  "circuit",
9
9
  "__fedify_meta",
10
- "circuit_breaker_state_ttl_sweep_v1"
10
+ "circuit_breaker_state_ttl_sweep_v2"
11
11
  ], { state: "final" });
12
12
  }
13
13
  //#endregion
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { jt as ActivityTransformer, n as Context } from "../context-Bjswu3eC.cjs";
2
+ import { jt as ActivityTransformer, n as Context } from "../context-BpmLyiPx.cjs";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { jt as ActivityTransformer, n as Context } from "../context-COxc_O-c.js";
2
+ import { jt as ActivityTransformer, n as Context } from "../context-BxVVO5Ro.js";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -5,7 +5,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
6
6
  import { t as assert } from "../assert-OguE97r2.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
8
- import { b as autoIdAssigner, n as FederationImpl, y as actorDehydrator } from "../middleware-HRbD2E7D.mjs";
8
+ import { b as autoIdAssigner, n as FederationImpl, y as actorDehydrator } from "../middleware-BogX5PzT.mjs";
9
9
  import { Follow, Person } from "@fedify/vocab";
10
10
  import { test } from "@fedify/fixture";
11
11
  //#region src/compat/transformers.test.ts
@@ -92,8 +92,9 @@ type CircuitBreakerOptions = CircuitBreakerFailurePolicy & {
92
92
  *
93
93
  * When omitted, Fedify derives this from `failureWindow`, `recoveryDelay`, and
94
94
  * `heldActivityTtl` for the default numeric failure policy. Custom `failure`
95
- * callbacks do not have an inspectable time window, so custom policies do not
96
- * expire their stored state unless this option is provided.
95
+ * callbacks do not have an inspectable time window, so their state expires
96
+ * after `recoveryDelay` plus `heldActivityTtl` unless this option overrides
97
+ * it.
97
98
  */
98
99
  readonly stateTtl?: Temporal.Duration | Temporal.DurationLike;
99
100
  /**
@@ -95,8 +95,9 @@ type CircuitBreakerOptions = CircuitBreakerFailurePolicy & {
95
95
  *
96
96
  * When omitted, Fedify derives this from `failureWindow`, `recoveryDelay`, and
97
97
  * `heldActivityTtl` for the default numeric failure policy. Custom `failure`
98
- * callbacks do not have an inspectable time window, so custom policies do not
99
- * expire their stored state unless this option is provided.
98
+ * callbacks do not have an inspectable time window, so their state expires
99
+ * after `recoveryDelay` plus `heldActivityTtl` unless this option overrides
100
+ * it.
100
101
  */
101
102
  readonly stateTtl?: Temporal.Duration | Temporal.DurationLike;
102
103
  /**
@@ -3,6 +3,6 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  //#region deno.json
5
5
  var name = "@fedify/fedify";
6
- var version = "2.3.5-dev.1755+9b03b9cc";
6
+ var version = "2.3.5";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { o as validateCryptoKey } from "./key-C2zsXBK1.mjs";
5
- import { n as doubleKnock } from "./http-DjON0zte.mjs";
4
+ import { o as validateCryptoKey } from "./key-BSbJc-6T.mjs";
5
+ import { n as doubleKnock } from "./http-Cm7Y71lD.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
8
8
  import { curry } from "es-toolkit";
@@ -26,6 +26,17 @@ const logger = getLogger([
26
26
  function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
27
27
  validateCryptoKey(identity.privateKey);
28
28
  async function load(url, options) {
29
+ await validateUrl(url);
30
+ return getRemoteDocument(url, await doubleKnock(createActivityPubRequest(url, { userAgent }), identity, {
31
+ maxRedirection,
32
+ specDeterminer,
33
+ log: curry(logRequest)(logger),
34
+ tracerProvider,
35
+ signal: options?.signal,
36
+ validateRedirect: validateUrl
37
+ }), load);
38
+ }
39
+ async function validateUrl(url) {
29
40
  if (!allowPrivateAddress) try {
30
41
  await validatePublicUrl(url);
31
42
  } catch (error) {
@@ -35,13 +46,6 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
35
46
  });
36
47
  throw error;
37
48
  }
38
- return getRemoteDocument(url, await doubleKnock(createActivityPubRequest(url, { userAgent }), identity, {
39
- maxRedirection,
40
- specDeterminer,
41
- log: curry(logRequest)(logger),
42
- tracerProvider,
43
- signal: options?.signal
44
- }), load);
45
49
  }
46
50
  return load;
47
51
  }
@@ -5,7 +5,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import { r as assertExists } from "../std__assert-BBjXFNOb.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
8
- import { r as createFederationBuilder } from "../builder-De3ym5P9.mjs";
8
+ import { r as createFederationBuilder } from "../builder-DXekGevh.mjs";
9
9
  import { DisallowedOperatorError, DisallowedVarSpecModifierError, DuplicateRouteVariableError, RouteTemplateOptionsNotMatchedError, RouterError } from "@fedify/uri-template";
10
10
  import { Activity, Note, Person } from "@fedify/vocab";
11
11
  import { test } from "@fedify/fixture";
@@ -5,8 +5,8 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import "../std__assert-BBjXFNOb.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
8
- import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-DLS_n1x5.mjs";
9
- import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-BVnv3T76.mjs";
8
+ import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-D1fxthDI.mjs";
9
+ import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-CAdwjgMi.mjs";
10
10
  import { test } from "@fedify/fixture";
11
11
  //#region src/federation/circuit-breaker.test.ts
12
12
  var AlwaysConflictingKvStore = class extends MemoryKvStore {
@@ -51,7 +51,7 @@ var CountingSweepKvStore = class extends MemoryKvStore {
51
51
  casCalls = [];
52
52
  deletedKeys = [];
53
53
  get(key) {
54
- if (key.length === 4 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "__fedify_meta" && key[3] === "circuit_breaker_state_ttl_sweep_v1") this.markerGetCalls++;
54
+ if (key.length === 4 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "__fedify_meta" && key[3] === "circuit_breaker_state_ttl_sweep_v2") this.markerGetCalls++;
55
55
  return super.get(key);
56
56
  }
57
57
  cas(key, expectedValue, newValue, options) {
@@ -73,7 +73,7 @@ var CountingSweepKvStore = class extends MemoryKvStore {
73
73
  var ConflictingSweepMarkerKvStore = class extends CountingSweepKvStore {
74
74
  attempts = 0;
75
75
  cas(key, expectedValue, newValue, options) {
76
- if (key.length === 4 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "__fedify_meta" && key[3] === "circuit_breaker_state_ttl_sweep_v1") {
76
+ if (key.length === 4 && key[0] === "_fedify" && key[1] === "circuit" && key[2] === "__fedify_meta" && key[3] === "circuit_breaker_state_ttl_sweep_v2") {
77
77
  this.attempts++;
78
78
  if (this.attempts > 3) throw new Error("legacy sweep did not stop retrying CAS misses");
79
79
  return Promise.resolve(false);
@@ -81,6 +81,23 @@ var ConflictingSweepMarkerKvStore = class extends CountingSweepKvStore {
81
81
  return super.cas(key, expectedValue, newValue, options);
82
82
  }
83
83
  };
84
+ var ClockAdvancingSweepKvStore = class extends CountingSweepKvStore {
85
+ clock = Temporal.Instant.from("2026-05-25T00:00:00Z");
86
+ cas(key, expectedValue, newValue, options) {
87
+ if (key.length === 3) this.clock = this.clock.add({ seconds: 30 });
88
+ return super.cas(key, expectedValue, newValue, options);
89
+ }
90
+ };
91
+ var RenewalLostSweepKvStore = class extends ClockAdvancingSweepKvStore {
92
+ markerCasAttempts = 0;
93
+ cas(key, expectedValue, newValue, options) {
94
+ if (key.length === 4 && key[3] === "circuit_breaker_state_ttl_sweep_v2") {
95
+ this.markerCasAttempts++;
96
+ if (this.markerCasAttempts > 1) return Promise.resolve(false);
97
+ }
98
+ return super.cas(key, expectedValue, newValue, options);
99
+ }
100
+ };
84
101
  var MarkerChangedDuringListKvStore = class extends CountingSweepKvStore {
85
102
  async *list(prefix) {
86
103
  yield* super.list(prefix);
@@ -88,7 +105,7 @@ var MarkerChangedDuringListKvStore = class extends CountingSweepKvStore {
88
105
  "_fedify",
89
106
  "circuit",
90
107
  "__fedify_meta",
91
- "circuit_breaker_state_ttl_sweep_v1"
108
+ "circuit_breaker_state_ttl_sweep_v2"
92
109
  ], { state: "final" });
93
110
  }
94
111
  };
@@ -176,6 +193,18 @@ test("normalizeCircuitBreakerOptions() accepts callback failure policy", () => {
176
193
  assertEquals(options.failure([Temporal.Instant.from("2026-05-25T00:00:00Z")]), false);
177
194
  assertEquals(options.failure([Temporal.Instant.from("2026-05-25T00:00:00Z"), Temporal.Instant.from("2026-05-25T00:01:00Z")]), true);
178
195
  assertEquals(options.pruneFailures(failures, base.add({ minutes: 105 })).map((t) => t.toString()), failures.slice(-100).map((t) => t.toString()));
196
+ assertEquals(options.stateTtl?.total("millisecond"), Temporal.Duration.from({
197
+ days: 7,
198
+ minutes: 30
199
+ }).total("millisecond"));
200
+ assertEquals(normalizeCircuitBreakerOptions({
201
+ failure: (timestamps) => timestamps.length >= 2,
202
+ recoveryDelay: { hours: 1 },
203
+ heldActivityTtl: { days: 2 }
204
+ }).stateTtl?.total("millisecond"), Temporal.Duration.from({
205
+ days: 2,
206
+ hours: 1
207
+ }).total("millisecond"));
179
208
  });
180
209
  test("parseCircuitBreakerKvState() validates stored shape", () => {
181
210
  assertEquals(parseCircuitBreakerKvState({
@@ -561,7 +590,7 @@ test("CircuitBreaker writes stored states with a TTL without CAS", async () => {
561
590
  hours: 1
562
591
  }) });
563
592
  });
564
- test("CircuitBreaker preserves custom failure history by default", async () => {
593
+ test("CircuitBreaker writes custom-policy states with a derived TTL", async () => {
565
594
  const kv = new CountingCasKvStore();
566
595
  await markCircuitBreakerLegacySweepDone(kv);
567
596
  await new CircuitBreaker({
@@ -570,7 +599,10 @@ test("CircuitBreaker preserves custom failure history by default", async () => {
570
599
  now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
571
600
  options: { failure: (timestamps) => timestamps.length >= 2 }
572
601
  }).recordFailure("remote.example");
573
- assertEquals(kv.options.at(-1), void 0);
602
+ assertEquals(kv.options.at(-1)?.ttl?.total("millisecond"), Temporal.Duration.from({
603
+ days: 7,
604
+ minutes: 30
605
+ }).total("millisecond"));
574
606
  });
575
607
  test("CircuitBreaker accepts an explicit TTL with custom failure policies", async () => {
576
608
  const kv = new CountingCasKvStore();
@@ -586,7 +618,7 @@ test("CircuitBreaker accepts an explicit TTL with custom failure policies", asyn
586
618
  }).recordFailure("remote.example");
587
619
  assertEquals(kv.options.at(-1), { ttl: Temporal.Duration.from({ days: 14 }) });
588
620
  });
589
- test("CircuitBreaker skips legacy sweep when custom policies have no TTL", async () => {
621
+ test("CircuitBreaker sweeps legacy states for custom failure policies", async () => {
590
622
  const kv = new CountingSweepKvStore();
591
623
  await kv.set([
592
624
  "_fedify",
@@ -604,15 +636,146 @@ test("CircuitBreaker skips legacy sweep when custom policies have no TTL", async
604
636
  });
605
637
  await circuit.recordFailure("remote.example");
606
638
  await circuit.pendingSweep;
607
- assertEquals(kv.listCalls, 0);
639
+ assertEquals(kv.listCalls, 1);
608
640
  assertEquals(await kv.get([
609
641
  "_fedify",
610
642
  "circuit",
611
643
  "stale.example"
612
644
  ]), {
613
645
  state: "closed",
614
- failures: ["2026-05-25T00:00:00Z"]
646
+ failures: ["2026-05-25T00:00:00Z"],
647
+ __fedifyCircuitBreakerStateVersion: 1
648
+ });
649
+ assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "stale.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
650
+ days: 7,
651
+ minutes: 30
652
+ }).total("millisecond"));
653
+ });
654
+ test("CircuitBreaker stamps a TTL on versioned states during sweeps", async () => {
655
+ const kv = new CountingSweepKvStore();
656
+ await kv.set([
657
+ "_fedify",
658
+ "circuit",
659
+ "lingering.example"
660
+ ], {
661
+ state: "closed",
662
+ failures: ["2026-05-25T00:00:00Z"],
663
+ __fedifyCircuitBreakerStateVersion: 1
664
+ });
665
+ const circuit = new CircuitBreaker({
666
+ kv,
667
+ prefix: ["_fedify", "circuit"],
668
+ now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
669
+ options: { failure: (timestamps) => timestamps.length >= 2 }
670
+ });
671
+ await circuit.recordFailure("remote.example");
672
+ await circuit.pendingSweep;
673
+ assertEquals(await kv.get([
674
+ "_fedify",
675
+ "circuit",
676
+ "lingering.example"
677
+ ]), {
678
+ state: "closed",
679
+ failures: ["2026-05-25T00:00:00Z"],
680
+ __fedifyCircuitBreakerStateVersion: 1
681
+ });
682
+ assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "lingering.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
683
+ days: 7,
684
+ minutes: 30
685
+ }).total("millisecond"));
686
+ });
687
+ test("CircuitBreaker re-sweeps despite a completed v1 sweep marker", async () => {
688
+ const kv = new CountingSweepKvStore();
689
+ await kv.set([
690
+ "_fedify",
691
+ "circuit",
692
+ "__fedify_meta",
693
+ "circuit_breaker_state_ttl_sweep_v1"
694
+ ], { state: "final" });
695
+ await kv.set([
696
+ "_fedify",
697
+ "circuit",
698
+ "lingering.example"
699
+ ], {
700
+ state: "closed",
701
+ failures: ["2026-05-25T00:00:00Z"],
702
+ __fedifyCircuitBreakerStateVersion: 1
703
+ });
704
+ const circuit = new CircuitBreaker({
705
+ kv,
706
+ prefix: ["_fedify", "circuit"],
707
+ now: () => Temporal.Instant.from("2026-05-25T00:00:00Z"),
708
+ options: { failure: (timestamps) => timestamps.length >= 2 }
709
+ });
710
+ await circuit.recordFailure("remote.example");
711
+ await circuit.pendingSweep;
712
+ assertEquals(kv.listCalls, 1);
713
+ assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "lingering.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
714
+ days: 7,
715
+ minutes: 30
716
+ }).total("millisecond"));
717
+ });
718
+ test("CircuitBreaker renews the sweep marker lease during long sweeps", async () => {
719
+ const kv = new ClockAdvancingSweepKvStore();
720
+ for (let i = 0; i < 5; i++) await kv.set([
721
+ "_fedify",
722
+ "circuit",
723
+ `host-${i}.example`
724
+ ], {
725
+ state: "closed",
726
+ failures: ["2026-05-25T00:00:00Z"],
727
+ __fedifyCircuitBreakerStateVersion: 1
728
+ });
729
+ const circuit = new CircuitBreaker({
730
+ kv,
731
+ prefix: ["_fedify", "circuit"],
732
+ now: () => kv.clock,
733
+ options: { failure: (timestamps) => timestamps.length >= 2 }
734
+ });
735
+ await circuit.getState("remote.example");
736
+ await circuit.pendingSweep;
737
+ assertEquals(kv.casCalls.filter(({ key }) => key.at(-1) === "circuit_breaker_state_ttl_sweep_v2").length, 4);
738
+ assertEquals(await kv.get([
739
+ "_fedify",
740
+ "circuit",
741
+ "__fedify_meta",
742
+ "circuit_breaker_state_ttl_sweep_v2"
743
+ ]), {
744
+ state: "done",
745
+ retryUntil: "2026-06-01T00:00:00Z"
615
746
  });
747
+ assertEquals(kv.casCalls.find(({ key }) => key.at(-1) === "host-4.example")?.options?.ttl?.total("millisecond"), Temporal.Duration.from({
748
+ days: 7,
749
+ minutes: 30
750
+ }).total("millisecond"));
751
+ });
752
+ test("CircuitBreaker abandons the sweep when the marker lease is lost", async () => {
753
+ const kv = new RenewalLostSweepKvStore();
754
+ for (let i = 0; i < 5; i++) await kv.set([
755
+ "_fedify",
756
+ "circuit",
757
+ `host-${i}.example`
758
+ ], {
759
+ state: "closed",
760
+ failures: ["2026-05-25T00:00:00Z"],
761
+ __fedifyCircuitBreakerStateVersion: 1
762
+ });
763
+ const circuit = new CircuitBreaker({
764
+ kv,
765
+ prefix: ["_fedify", "circuit"],
766
+ now: () => kv.clock,
767
+ options: { failure: (timestamps) => timestamps.length >= 2 }
768
+ });
769
+ await circuit.getState("remote.example");
770
+ await circuit.pendingSweep;
771
+ assertEquals(kv.markerCasAttempts, 2);
772
+ assertEquals(kv.casCalls.filter(({ key }) => key.at(-1) === "host-4.example").length, 0);
773
+ assertEquals((await kv.get([
774
+ "_fedify",
775
+ "circuit",
776
+ "__fedify_meta",
777
+ "circuit_breaker_state_ttl_sweep_v2"
778
+ ]))?.state, "sweeping");
616
779
  });
617
780
  test("CircuitBreaker migrates legacy states without TTL once", async () => {
618
781
  const kv = new CountingSweepKvStore();
@@ -720,7 +883,7 @@ test("CircuitBreaker migrates legacy states without TTL once", async () => {
720
883
  "_fedify",
721
884
  "circuit",
722
885
  "__fedify_meta",
723
- "circuit_breaker_state_ttl_sweep_v1"
886
+ "circuit_breaker_state_ttl_sweep_v2"
724
887
  ]), {
725
888
  state: "done",
726
889
  retryUntil: "2026-06-01T00:00:00Z"
@@ -764,7 +927,7 @@ test("CircuitBreaker migrates legacy states without TTL once", async () => {
764
927
  "_fedify",
765
928
  "circuit",
766
929
  "__fedify_meta",
767
- "circuit_breaker_state_ttl_sweep_v1"
930
+ "circuit_breaker_state_ttl_sweep_v2"
768
931
  ]), { state: "final" });
769
932
  });
770
933
  test("CircuitBreaker does not sweep state changed after listing", async () => {
@@ -800,7 +963,7 @@ test("CircuitBreaker skips legacy sweep already running elsewhere", async () =>
800
963
  "_fedify",
801
964
  "circuit",
802
965
  "__fedify_meta",
803
- "circuit_breaker_state_ttl_sweep_v1"
966
+ "circuit_breaker_state_ttl_sweep_v2"
804
967
  ], {
805
968
  state: "sweeping",
806
969
  started: "2026-05-25T00:00:00Z",
@@ -841,7 +1004,7 @@ test("CircuitBreaker does not overwrite a changed legacy sweep marker", async ()
841
1004
  "_fedify",
842
1005
  "circuit",
843
1006
  "__fedify_meta",
844
- "circuit_breaker_state_ttl_sweep_v1"
1007
+ "circuit_breaker_state_ttl_sweep_v2"
845
1008
  ]), { state: "final" });
846
1009
  });
847
1010
  test("CircuitBreaker bounds legacy sweep CAS retries", async () => {
@@ -863,7 +1026,7 @@ test("CircuitBreaker retries expired legacy sweep markers", async () => {
863
1026
  "_fedify",
864
1027
  "circuit",
865
1028
  "__fedify_meta",
866
- "circuit_breaker_state_ttl_sweep_v1"
1029
+ "circuit_breaker_state_ttl_sweep_v2"
867
1030
  ], {
868
1031
  state: "sweeping",
869
1032
  started: "2026-05-25T00:00:00Z",
@@ -899,7 +1062,7 @@ test("CircuitBreaker retries expired legacy sweep markers", async () => {
899
1062
  "_fedify",
900
1063
  "circuit",
901
1064
  "__fedify_meta",
902
- "circuit_breaker_state_ttl_sweep_v1"
1065
+ "circuit_breaker_state_ttl_sweep_v2"
903
1066
  ]), {
904
1067
  state: "done",
905
1068
  retryUntil: "2026-06-01T00:00:00Z"
@@ -911,7 +1074,7 @@ test("CircuitBreaker caches completed legacy sweep markers", async () => {
911
1074
  "_fedify",
912
1075
  "circuit",
913
1076
  "__fedify_meta",
914
- "circuit_breaker_state_ttl_sweep_v1"
1077
+ "circuit_breaker_state_ttl_sweep_v2"
915
1078
  ], {
916
1079
  state: "done",
917
1080
  retryUntil: "2026-06-01T00:00:00Z"
@@ -952,7 +1115,7 @@ test("CircuitBreaker retries malformed legacy sweep markers", async () => {
952
1115
  "_fedify",
953
1116
  "circuit",
954
1117
  "__fedify_meta",
955
- "circuit_breaker_state_ttl_sweep_v1"
1118
+ "circuit_breaker_state_ttl_sweep_v2"
956
1119
  ], {
957
1120
  state: "done",
958
1121
  retryUntil: "not an instant"
@@ -991,7 +1154,7 @@ test("CircuitBreaker retries malformed legacy sweep markers", async () => {
991
1154
  "_fedify",
992
1155
  "circuit",
993
1156
  "__fedify_meta",
994
- "circuit_breaker_state_ttl_sweep_v1"
1157
+ "circuit_breaker_state_ttl_sweep_v2"
995
1158
  ]), {
996
1159
  state: "done",
997
1160
  retryUntil: "2026-06-01T00:00:00Z"
@@ -1011,13 +1174,13 @@ test("CircuitBreaker retries legacy sweep after transient failures", async () =>
1011
1174
  "_fedify",
1012
1175
  "circuit",
1013
1176
  "__fedify_meta",
1014
- "circuit_breaker_state_ttl_sweep_v1"
1177
+ "circuit_breaker_state_ttl_sweep_v2"
1015
1178
  ]), void 0);
1016
1179
  assertEquals(kv.deletedKeys, [[
1017
1180
  "_fedify",
1018
1181
  "circuit",
1019
1182
  "__fedify_meta",
1020
- "circuit_breaker_state_ttl_sweep_v1"
1183
+ "circuit_breaker_state_ttl_sweep_v2"
1021
1184
  ]]);
1022
1185
  assertEquals(await circuit.getState("remote.example"), {
1023
1186
  state: "closed",
@@ -8,11 +8,11 @@ import { n as assertGreaterOrEqual, t as assertRejects } from "../assert_rejects
8
8
  import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
9
9
  import { t as assert } from "../assert-OguE97r2.mjs";
10
10
  import { r as parseAcceptSignature } from "../accept-CPkZzmGN.mjs";
11
- import { s as signRequest } from "../http-DjON0zte.mjs";
11
+ import { s as signRequest } from "../http-Cm7Y71lD.mjs";
12
12
  import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs";
13
- import { a as compactJsonLd, p as signJsonLd } from "../ld-CB6S9oHd.mjs";
13
+ import { a as compactJsonLd, p as signJsonLd } from "../ld-DMY2-bQC.mjs";
14
14
  import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
15
- import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-HRbD2E7D.mjs";
15
+ import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-BogX5PzT.mjs";
16
16
  import { t as ActivityListenerSet } from "../activity-listener-tztVvlNb.mjs";
17
17
  import { Activity, Create, Note, Person, Tombstone } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -4,9 +4,9 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import "../std__assert-BBjXFNOb.mjs";
6
6
  import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
7
- import { r as signObject } from "../proof-CCC9itsd.mjs";
7
+ import { r as signObject } from "../proof-BSlz1EIk.mjs";
8
8
  import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
9
- import { o as createFederation } from "../middleware-HRbD2E7D.mjs";
9
+ import { o as createFederation } from "../middleware-BogX5PzT.mjs";
10
10
  import { Create, Follow, Person } from "@fedify/vocab";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  //#region src/federation/idempotency.test.ts
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import "../std__assert-BBjXFNOb.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
7
- import { _ as recordKeyLookup, a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, m as recordDocumentFetch, p as recordDocumentCache, r as getFederationMetrics, t as classifyFetchError, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "../metrics-DbryxSc6.mjs";
7
+ import { _ as recordKeyLookup, a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, m as recordDocumentFetch, p as recordDocumentCache, r as getFederationMetrics, t as classifyFetchError, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "../metrics-B9nzbvNe.mjs";
8
8
  import { createTestMeterProvider, test } from "@fedify/fixture";
9
9
  import { FetchError } from "@fedify/vocab-runtime";
10
10
  import { DataPointType, MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";
@@ -10,17 +10,17 @@ import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
10
10
  import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
11
11
  import { t as assertStrictEquals } from "../assert_strict_equals-XEgZAlrj.mjs";
12
12
  import { t as assert } from "../assert-OguE97r2.mjs";
13
- import { g as recordInboxActivity } from "../metrics-DbryxSc6.mjs";
14
- import { l as verifyRequest, s as signRequest } from "../http-DjON0zte.mjs";
13
+ import { g as recordInboxActivity } from "../metrics-B9nzbvNe.mjs";
14
+ import { l as verifyRequest, s as signRequest } from "../http-Cm7Y71lD.mjs";
15
15
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
16
- import { t as getAuthenticatedDocumentLoader } from "../docloader-B9iBabXZ.mjs";
17
- import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-CB6S9oHd.mjs";
18
- import { t as doesActorOwnKey } from "../owner-C5QCaiH6.mjs";
19
- import { i as verifyObject, r as signObject } from "../proof-CCC9itsd.mjs";
16
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-BYH2ymPh.mjs";
17
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-DMY2-bQC.mjs";
18
+ import { t as doesActorOwnKey } from "../owner-Ci8O4Q03.mjs";
19
+ import { i as verifyObject, r as signObject } from "../proof-BSlz1EIk.mjs";
20
20
  import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
21
- import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-HRbD2E7D.mjs";
22
- import { t as CircuitBreaker } from "../circuit-breaker-DLS_n1x5.mjs";
23
- import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-BVnv3T76.mjs";
21
+ import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-BogX5PzT.mjs";
22
+ import { t as CircuitBreaker } from "../circuit-breaker-D1fxthDI.mjs";
23
+ import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-CAdwjgMi.mjs";
24
24
  import { configure, reset } from "@logtape/logtape";
25
25
  import { RouterError } from "@fedify/uri-template";
26
26
  import * as vocab from "@fedify/vocab";
@@ -4906,6 +4906,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
4906
4906
  const { federation, queued, kv } = setup({ failureThreshold: 1 }, { outboxRetryPolicy: () => Temporal.Duration.from({ seconds: 3 }) });
4907
4907
  kv.cas = () => Promise.resolve(false);
4908
4908
  await federation.processQueuedTask(void 0, createOutboxMessage("https://failure-bookkeeping.example/inbox"));
4909
+ await federation.circuitBreaker?.pendingSweep;
4909
4910
  assertEquals(queued.length, 1);
4910
4911
  const retry = queued[0].message;
4911
4912
  assertEquals(retry.attempt, 1);
@@ -2,7 +2,7 @@ const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  require("../chunk-DDcVe30Y.cjs");
5
- const require_middleware = require("../middleware-aw8NYL2-.cjs");
5
+ const require_middleware = require("../middleware-rf7LIZPi.cjs");
6
6
  let _logtape_logtape = require("@logtape/logtape");
7
7
  let _fedify_uri_template = require("@fedify/uri-template");
8
8
  let es_toolkit = require("es-toolkit");