@fedify/fedify 2.4.0-dev.1832 → 2.4.0-dev.1867

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 (64) hide show
  1. package/dist/{builder-DPsTwE3s.mjs → builder-jhGmiXB5.mjs} +2 -2
  2. package/dist/{circuit-breaker-BT7CiPbz.mjs → circuit-breaker-C5NqoLBJ.mjs} +25 -5
  3. package/dist/{circuit-breaker-test-utils-BYIHoliq.mjs → circuit-breaker-test-utils-DW6YC_Pe.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-B-dsBjZ9.d.cts → context-BOnn653X.d.cts} +3 -2
  8. package/dist/{context-DrLXudbA.d.ts → context-CmjP-cgJ.d.ts} +3 -2
  9. package/dist/{deno-HRycs54o.mjs → deno-B5Jy0IH5.mjs} +1 -1
  10. package/dist/{docloader-CqYFM6-s.mjs → docloader-RO5dmEG1.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/tasks/enqueue.test.mjs +1 -1
  23. package/dist/federation/tasks/tasks.test.mjs +2 -2
  24. package/dist/federation/temporal.test.mjs +1 -1
  25. package/dist/federation/webfinger.test.mjs +1 -1
  26. package/dist/{federation-DL4R5aHV.cjs → federation-D4wc67AQ.cjs} +28 -8
  27. package/dist/{http-Cc4z7keO.js → http-ByQYBoMk.js} +8 -6
  28. package/dist/{http-CFWb7us8.mjs → http-CI_W79n9.mjs} +9 -7
  29. package/dist/{http-CE1k_azr.cjs → http-DaCIe27S.cjs} +8 -6
  30. package/dist/{key-DBAd5Alw.mjs → key-Dx1gDplp.mjs} +3 -3
  31. package/dist/{kv-cache-PvEfOS71.js → kv-cache-C3ffIesH.js} +12 -8
  32. package/dist/{kv-cache-CWpU9ryt.mjs → kv-cache-D6L7jCkB.mjs} +1 -1
  33. package/dist/{kv-cache-eSsOzO14.cjs → kv-cache-LBXR3jN5.cjs} +12 -8
  34. package/dist/{ld-D6QZCkDJ.mjs → ld-jRq6FLRU.mjs} +3 -3
  35. package/dist/{metrics-CZKhCz2S.mjs → metrics-DB_aJ3d7.mjs} +1 -1
  36. package/dist/{middleware-D8aOs8i6.mjs → middleware-BZGAqnv7.mjs} +13 -13
  37. package/dist/{middleware-DxfRRHhZ.js → middleware-DXabWqen.js} +28 -8
  38. package/dist/{middleware-Bxq5z3Hv.mjs → middleware-FSACqVKD.mjs} +1 -1
  39. package/dist/{mod-D6O8C7eN.d.ts → mod-BYuZ0WXt.d.ts} +1 -1
  40. package/dist/{mod-D-abvu2C.d.cts → mod-kUSY5n3f.d.cts} +1 -1
  41. package/dist/mod.cjs +4 -4
  42. package/dist/mod.d.cts +2 -2
  43. package/dist/mod.d.ts +2 -2
  44. package/dist/mod.js +4 -4
  45. package/dist/nodeinfo/handler.test.mjs +1 -1
  46. package/dist/{owner-32hqLs84.mjs → owner-B8wSl5D1.mjs} +2 -2
  47. package/dist/{proof-DTdKMoMy.js → proof-CCnQdjNX.js} +1 -1
  48. package/dist/{proof-BH5eExL_.mjs → proof-Dwx8Yb1A.mjs} +4 -4
  49. package/dist/{proof-CsjmqnGU.cjs → proof-FLTH3GQo.cjs} +1 -1
  50. package/dist/{send-B0bVhwKq.mjs → send-DoVP-yHD.mjs} +3 -3
  51. package/dist/sig/http.test.mjs +65 -3
  52. package/dist/sig/key.test.mjs +46 -1
  53. package/dist/sig/ld.test.mjs +2 -2
  54. package/dist/sig/mod.cjs +2 -2
  55. package/dist/sig/mod.js +2 -2
  56. package/dist/sig/owner.test.mjs +1 -1
  57. package/dist/sig/proof.test.mjs +1 -1
  58. package/dist/{temporal-BFt5VzLm.mjs → temporal-BRO-1oR_.mjs} +1 -1
  59. package/dist/testing/mod.d.mts +3 -2
  60. package/dist/utils/docloader.test.mjs +35 -2
  61. package/dist/utils/kv-cache.test.mjs +1 -1
  62. package/dist/utils/mod.cjs +1 -1
  63. package/dist/utils/mod.js +1 -1
  64. package/package.json +6 -6
@@ -1,7 +1,7 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-HRycs54o.mjs";
4
+ import { n as version, t as name } from "./deno-B5Jy0IH5.mjs";
5
5
  import { t as ActivityListenerSet } from "./activity-listener-C-BSbKI7.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
@@ -85,7 +85,7 @@ var FederationBuilderImpl = class {
85
85
  * specified.
86
86
  */
87
87
  async build(options) {
88
- const { FederationImpl } = await import("./middleware-Bxq5z3Hv.mjs");
88
+ const { FederationImpl } = await import("./middleware-FSACqVKD.mjs");
89
89
  const f = new FederationImpl(options);
90
90
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
91
91
  f.router = this.router.clone();
@@ -3,10 +3,11 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  //#region src/federation/circuit-breaker.ts
6
- const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v1"];
6
+ const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
7
7
  const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
8
8
  const CIRCUIT_BREAKER_STATE_VERSION = 1;
9
9
  const LEGACY_SWEEP_LOCK_TTL = Temporal.Duration.from({ minutes: 5 });
10
+ const LEGACY_SWEEP_RENEW_INTERVAL = Temporal.Duration.from({ minutes: 1 });
10
11
  const LEGACY_SWEEP_RETRY_WINDOW = Temporal.Duration.from({ hours: 168 });
11
12
  const LEGACY_SWEEP_WAIT_INTERVAL = 10;
12
13
  const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
@@ -221,12 +222,20 @@ var CircuitBreaker = class {
221
222
  }
222
223
  async #sweepLegacyStatesImpl() {
223
224
  const markerKey = this.#legacySweepMarkerKey();
224
- const marker = await this.#acquireLegacySweep(markerKey);
225
- if (marker === "done") return;
225
+ const acquired = await this.#acquireLegacySweep(markerKey);
226
+ if (acquired === "done") return;
227
+ let marker = acquired;
228
+ let leaseRenewedAt = this.#now();
226
229
  try {
227
230
  for await (const { key, value } of this.#kv.list(this.#prefix)) {
228
231
  if (key.length !== this.#prefix.length + 1) continue;
229
232
  await this.#migrateLegacyState(key, value);
233
+ if (Temporal.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
234
+ const renewed = await this.#renewLegacySweep(markerKey, marker);
235
+ if (renewed == null) return;
236
+ marker = renewed;
237
+ leaseRenewedAt = this.#now();
238
+ }
230
239
  }
231
240
  } catch (error) {
232
241
  await this.#deleteIfUnchanged(markerKey, marker);
@@ -235,6 +244,14 @@ var CircuitBreaker = class {
235
244
  const finishedMarker = this.#finishLegacySweepMarker(marker);
236
245
  if (await this.#kv.cas(markerKey, marker, finishedMarker)) this.#rememberLegacySweepMarker(finishedMarker);
237
246
  }
247
+ async #renewLegacySweep(markerKey, marker) {
248
+ const renewed = {
249
+ state: "sweeping",
250
+ started: this.#now().toString(),
251
+ retryUntil: getLegacySweepRetryUntil(marker) ?? this.#now().add(LEGACY_SWEEP_RETRY_WINDOW).toString()
252
+ };
253
+ if (await this.#kv.cas(markerKey, marker, renewed, { ttl: LEGACY_SWEEP_LOCK_TTL })) return renewed;
254
+ }
238
255
  #finishLegacySweepMarker(marker) {
239
256
  const retryUntil = getLegacySweepRetryUntil(marker);
240
257
  if (retryUntil != null && Temporal.Instant.compare(this.#now(), Temporal.Instant.from(retryUntil)) < 0) return {
@@ -244,7 +261,10 @@ var CircuitBreaker = class {
244
261
  return { state: "final" };
245
262
  }
246
263
  async #migrateLegacyState(key, value) {
247
- if (isCurrentCircuitBreakerState(value)) return;
264
+ if (isCurrentCircuitBreakerState(value)) {
265
+ await this.#kv.cas(key, value, value, this.#setOptions());
266
+ return;
267
+ }
248
268
  const state = parseCircuitBreakerKvState(value);
249
269
  if (state != null) {
250
270
  await this.#kv.cas(key, value, markCircuitBreakerState(state), this.#setOptions());
@@ -381,7 +401,7 @@ function normalizeCircuitBreakerOptions(options) {
381
401
  } else {
382
402
  failure = options.failure;
383
403
  pruneFailures = (timestamps) => timestamps.slice(-100);
384
- stateTtl = configuredStateTtl;
404
+ stateTtl = configuredStateTtl ?? recoveryDelay.add(heldActivityTtl);
385
405
  }
386
406
  return {
387
407
  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 { Gt as ActivityTransformer, n as Context } from "../context-B-dsBjZ9.cjs";
2
+ import { Gt as ActivityTransformer, n as Context } from "../context-BOnn653X.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 { Gt as ActivityTransformer, n as Context } from "../context-DrLXudbA.js";
2
+ import { Gt as ActivityTransformer, n as Context } from "../context-CmjP-cgJ.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 MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
6
6
  import { t as assertInstanceOf } from "../assert_instance_of-g3WcyYNR.mjs";
7
7
  import { t as assert } from "../assert-CebeUf0C.mjs";
8
- import { b as autoIdAssigner, n as FederationImpl, y as actorDehydrator } from "../middleware-D8aOs8i6.mjs";
8
+ import { b as autoIdAssigner, n as FederationImpl, y as actorDehydrator } from "../middleware-BZGAqnv7.mjs";
9
9
  import { Follow, Person } from "@fedify/vocab";
10
10
  import { test } from "@fedify/fixture";
11
11
  //#region src/compat/transformers.test.ts
@@ -511,8 +511,9 @@ type CircuitBreakerOptions = CircuitBreakerFailurePolicy & {
511
511
  *
512
512
  * When omitted, Fedify derives this from `failureWindow`, `recoveryDelay`, and
513
513
  * `heldActivityTtl` for the default numeric failure policy. Custom `failure`
514
- * callbacks do not have an inspectable time window, so custom policies do not
515
- * expire their stored state unless this option is provided.
514
+ * callbacks do not have an inspectable time window, so their state expires
515
+ * after `recoveryDelay` plus `heldActivityTtl` unless this option overrides
516
+ * it.
516
517
  */
517
518
  readonly stateTtl?: Temporal.Duration | Temporal.DurationLike;
518
519
  /**
@@ -513,8 +513,9 @@ type CircuitBreakerOptions = CircuitBreakerFailurePolicy & {
513
513
  *
514
514
  * When omitted, Fedify derives this from `failureWindow`, `recoveryDelay`, and
515
515
  * `heldActivityTtl` for the default numeric failure policy. Custom `failure`
516
- * callbacks do not have an inspectable time window, so custom policies do not
517
- * expire their stored state unless this option is provided.
516
+ * callbacks do not have an inspectable time window, so their state expires
517
+ * after `recoveryDelay` plus `heldActivityTtl` unless this option overrides
518
+ * it.
518
519
  */
519
520
  readonly stateTtl?: Temporal.Duration | Temporal.DurationLike;
520
521
  /**
@@ -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.4.0-dev.1832+1af9355c";
6
+ var version = "2.4.0-dev.1867+ac08358f";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -1,8 +1,8 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { o as validateCryptoKey } from "./key-DBAd5Alw.mjs";
5
- import { n as doubleKnock } from "./http-CFWb7us8.mjs";
4
+ import { o as validateCryptoKey } from "./key-Dx1gDplp.mjs";
5
+ import { n as doubleKnock } from "./http-CI_W79n9.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { curry } from "es-toolkit";
8
8
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
@@ -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
  }
@@ -6,7 +6,7 @@ import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
6
6
  import { t as assertExists } from "../assert_exists-DtzkXC47.mjs";
7
7
  import { t as assertThrows } from "../assert_throws-BJ5wuvR2.mjs";
8
8
  import "../std__assert-CxFT0MNt.mjs";
9
- import { r as createFederationBuilder } from "../builder-DPsTwE3s.mjs";
9
+ import { r as createFederationBuilder } from "../builder-jhGmiXB5.mjs";
10
10
  import { DisallowedOperatorError, DisallowedVarSpecModifierError, DuplicateRouteVariableError, RouteTemplateOptionsNotMatchedError, RouterError } from "@fedify/uri-template";
11
11
  import { Activity, Note, Person } from "@fedify/vocab";
12
12
  import { test } from "@fedify/fixture";
@@ -5,8 +5,8 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-BJ5wuvR2.mjs";
7
7
  import "../std__assert-CxFT0MNt.mjs";
8
- import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-BT7CiPbz.mjs";
9
- import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-BYIHoliq.mjs";
8
+ import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-C5NqoLBJ.mjs";
9
+ import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.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",
@@ -10,10 +10,10 @@ import "../std__assert-CxFT0MNt.mjs";
10
10
  import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
11
11
  import { t as assert } from "../assert-CebeUf0C.mjs";
12
12
  import { r as parseAcceptSignature } from "../accept-CPVqki78.mjs";
13
- import { s as signRequest } from "../http-CFWb7us8.mjs";
13
+ import { s as signRequest } from "../http-CI_W79n9.mjs";
14
14
  import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
15
- import { a as compactJsonLd, p as signJsonLd } from "../ld-D6QZCkDJ.mjs";
16
- 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-D8aOs8i6.mjs";
15
+ import { a as compactJsonLd, p as signJsonLd } from "../ld-jRq6FLRU.mjs";
16
+ 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-BZGAqnv7.mjs";
17
17
  import { t as ActivityListenerSet } from "../activity-listener-C-BSbKI7.mjs";
18
18
  import { Activity, Create, Note, Person, Tombstone } from "@fedify/vocab";
19
19
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -5,8 +5,8 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
6
6
  import "../std__assert-CxFT0MNt.mjs";
7
7
  import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
8
- import { r as signObject } from "../proof-BH5eExL_.mjs";
9
- import { o as createFederation } from "../middleware-D8aOs8i6.mjs";
8
+ import { r as signObject } from "../proof-Dwx8Yb1A.mjs";
9
+ import { o as createFederation } from "../middleware-BZGAqnv7.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-CBlRfA-_.mjs";
5
5
  import "../std__assert-CxFT0MNt.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-wFbSPn1O.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-CZKhCz2S.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-DB_aJ3d7.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";
@@ -14,17 +14,17 @@ import "../std__assert-CxFT0MNt.mjs";
14
14
  import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
15
15
  import { t as assertStrictEquals } from "../assert_strict_equals-Bpjl4Psw.mjs";
16
16
  import { t as assert } from "../assert-CebeUf0C.mjs";
17
- import { g as recordInboxActivity } from "../metrics-CZKhCz2S.mjs";
18
- import { l as verifyRequest, s as signRequest } from "../http-CFWb7us8.mjs";
17
+ import { g as recordInboxActivity } from "../metrics-DB_aJ3d7.mjs";
18
+ import { l as verifyRequest, s as signRequest } from "../http-CI_W79n9.mjs";
19
19
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
20
- import { t as getAuthenticatedDocumentLoader } from "../docloader-CqYFM6-s.mjs";
21
- import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-D6QZCkDJ.mjs";
22
- import { t as doesActorOwnKey } from "../owner-32hqLs84.mjs";
23
- import { i as verifyObject, r as signObject } from "../proof-BH5eExL_.mjs";
24
- import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-D8aOs8i6.mjs";
25
- import { t as CircuitBreaker } from "../circuit-breaker-BT7CiPbz.mjs";
20
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-RO5dmEG1.mjs";
21
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-jRq6FLRU.mjs";
22
+ import { t as doesActorOwnKey } from "../owner-B8wSl5D1.mjs";
23
+ import { i as verifyObject, r as signObject } from "../proof-Dwx8Yb1A.mjs";
24
+ import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-BZGAqnv7.mjs";
25
+ import { t as CircuitBreaker } from "../circuit-breaker-C5NqoLBJ.mjs";
26
26
  import { t as TaskCodec } from "../codec-peqOdBaE.mjs";
27
- import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-BYIHoliq.mjs";
27
+ import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.mjs";
28
28
  import { configure, reset } from "@logtape/logtape";
29
29
  import { RouterError } from "@fedify/uri-template";
30
30
  import * as vocab from "@fedify/vocab";
@@ -5358,6 +5358,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5358
5358
  const { federation, queued, kv } = setup({ failureThreshold: 1 }, { outboxRetryPolicy: () => Temporal.Duration.from({ seconds: 3 }) });
5359
5359
  kv.cas = () => Promise.resolve(false);
5360
5360
  await federation.processQueuedTask(void 0, createOutboxMessage("https://failure-bookkeeping.example/inbox"));
5361
+ await federation.circuitBreaker?.pendingSweep;
5361
5362
  assertEquals(queued.length, 1);
5362
5363
  const retry = queued[0].message;
5363
5364
  assertEquals(retry.attempt, 1);
@@ -1,6 +1,6 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_federation = require("../federation-DL4R5aHV.cjs");
3
+ const require_federation = require("../federation-D4wc67AQ.cjs");
4
4
  exports.CircuitBreaker = require_federation.CircuitBreaker;
5
5
  exports.InProcessMessageQueue = require_federation.InProcessMessageQueue;
6
6
  exports.MemoryKvStore = require_federation.MemoryKvStore;