@fedify/fedify 2.3.0-dev.1189 → 2.3.0-dev.1190

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 (76) hide show
  1. package/dist/{builder-Dc6s3gPe.mjs → builder-BzgNpXoY.mjs} +2 -2
  2. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{context-CRXCkTM6.d.cts → context-DMHK7jqX.d.cts} +224 -3
  7. package/dist/{context-MgCh7YGu.d.ts → context-K9cg8oGx.d.ts} +224 -3
  8. package/dist/{deno-BomxIkHS.mjs → deno-CoAwVm1I.mjs} +1 -1
  9. package/dist/{docloader-CzS6F5sZ.mjs → docloader-hPqZT20O.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  12. package/dist/federation/circuit-breaker.test.mjs +446 -0
  13. package/dist/federation/collection.test.mjs +1 -1
  14. package/dist/federation/handler.test.mjs +3 -3
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +1 -1
  17. package/dist/federation/metrics.test.mjs +16 -1
  18. package/dist/federation/middleware.test.mjs +817 -6
  19. package/dist/federation/mod.cjs +4 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +2 -2
  23. package/dist/federation/negotiation.test.mjs +1 -1
  24. package/dist/federation/retry.test.mjs +1 -1
  25. package/dist/federation/send.test.mjs +43 -10
  26. package/dist/federation/temporal.test.mjs +1 -1
  27. package/dist/federation/webfinger.test.mjs +1 -1
  28. package/dist/{getMachineId-bsd-BY01PL1n.mjs → getMachineId-bsd-Bn0le7-J.mjs} +1 -1
  29. package/dist/{getMachineId-darwin-Dr1gkBkp.mjs → getMachineId-darwin-CVjKuDgj.mjs} +1 -1
  30. package/dist/{getMachineId-win-QEYwcJiy.mjs → getMachineId-win-c5zxTSS1.mjs} +1 -1
  31. package/dist/{http-DnJyL_6c.cjs → http-BAarxBe5.cjs} +30 -5
  32. package/dist/{http-DtWN_XvX.mjs → http-CSwCAQ-H.mjs} +3 -3
  33. package/dist/{http-B-psRIq6.js → http-Dq_qElWc.js} +25 -6
  34. package/dist/{key-CT2NnJuR.mjs → key-DYK_T_PD.mjs} +2 -2
  35. package/dist/{kv-cache-DKhLDCH8.js → kv-cache-BhPocHdd.js} +1 -1
  36. package/dist/{kv-cache-Bf8AoV6C.mjs → kv-cache-CFzIDCMJ.mjs} +1 -1
  37. package/dist/{kv-cache-CVre456Y.cjs → kv-cache-Ds1kjvnu.cjs} +1 -1
  38. package/dist/{ld-DCyQasTE.mjs → ld-BdcT_irA.mjs} +3 -3
  39. package/dist/{metrics-xgr0P4hO.mjs → metrics-Ci97wkob.mjs} +25 -6
  40. package/dist/{middleware-DK0thDHX.mjs → middleware-BUGT2LmO.mjs} +279 -40
  41. package/dist/{middleware-BgbdoV61.js → middleware-C-C_I_wJ.js} +615 -32
  42. package/dist/{middleware-DIJ_6KFI.cjs → middleware-ddMAHsyF.cjs} +632 -31
  43. package/dist/{middleware-sgx08IEk.mjs → middleware-hWs3qtrr.mjs} +1 -1
  44. package/dist/{mod-CpQHB3Ys.d.ts → mod-CfOFqS0w.d.ts} +1 -1
  45. package/dist/{mod-C7HOzGqH.d.cts → mod-YLnSsEHY.d.cts} +1 -1
  46. package/dist/mod.cjs +7 -4
  47. package/dist/mod.d.cts +4 -4
  48. package/dist/mod.d.ts +4 -4
  49. package/dist/mod.js +5 -5
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/{owner-BIU_Sl7y.mjs → owner-B8ePZh4q.mjs} +2 -2
  52. package/dist/{proof-B9xbksrX.cjs → proof-CXdtqYKw.cjs} +1 -1
  53. package/dist/{proof-DDs7BRl7.mjs → proof-CzqluPMh.mjs} +3 -3
  54. package/dist/{proof-B5defvTr.js → proof-Dq_RyTjd.js} +1 -1
  55. package/dist/{send-BuxDCpxz.mjs → send-NzJqiStx.mjs} +21 -7
  56. package/dist/sig/http.test.mjs +2 -2
  57. package/dist/sig/key.test.mjs +1 -1
  58. package/dist/sig/ld.test.mjs +2 -2
  59. package/dist/sig/mod.cjs +2 -2
  60. package/dist/sig/mod.js +2 -2
  61. package/dist/sig/owner.test.mjs +1 -1
  62. package/dist/sig/proof.test.mjs +1 -1
  63. package/dist/{temporal-DHgeMWiP.mjs → temporal-CnhE0LLn.mjs} +1 -1
  64. package/dist/testing/mod.d.mts +36 -2
  65. package/dist/utils/docloader.test.mjs +2 -2
  66. package/dist/utils/kv-cache.test.mjs +1 -1
  67. package/dist/utils/mod.cjs +1 -1
  68. package/dist/utils/mod.js +1 -1
  69. package/package.json +7 -7
  70. /package/dist/{collection-CA3V5zyK.mjs → collection-Cc3DVAhE.mjs} +0 -0
  71. /package/dist/{execAsync-Dxb7rNf3.mjs → execAsync-Dmet7-28.mjs} +0 -0
  72. /package/dist/{getMachineId-linux-Bbhofx-s.mjs → getMachineId-linux-DbG4BXa-.mjs} +0 -0
  73. /package/dist/{getMachineId-unsupported-dIOte2Ct.mjs → getMachineId-unsupported-lC8T9hPE.mjs} +0 -0
  74. /package/dist/{keycache-BYMd8q7F.mjs → keycache-BeU0LCII.mjs} +0 -0
  75. /package/dist/{negotiation-CDW-_gUU.mjs → negotiation-DDstyBvc.mjs} +0 -0
  76. /package/dist/{retry-_VvV0h9f.mjs → retry-CXg_MBI-.mjs} +0 -0
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { C as recordFanoutRecipients, D as recordWebFingerHandle, E as recordOutboxEnqueue, M as name, N as version, O as formatAcceptSignature, T as recordOutboxActivity, a as verifyRequestDetailed, b as recordCollectionRequest, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCollectionDispatchDuration, w as recordInboxActivity, x as recordCollectionTotalItems, y as recordCollectionPageItems } from "./http-B-psRIq6.js";
6
- import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-B5defvTr.js";
5
+ import { D as recordOutboxEnqueue, E as recordOutboxActivity, N as name, O as recordWebFingerHandle, P as version, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as formatAcceptSignature, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-Dq_qElWc.js";
6
+ import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-Dq_RyTjd.js";
7
7
  import { n as getNodeInfo, t as nodeInfoToJson } from "./types-CAY3OdLq.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DKhLDCH8.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-BhPocHdd.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -652,6 +652,338 @@ function createFederationBuilder() {
652
652
  return new FederationBuilderImpl();
653
653
  }
654
654
  //#endregion
655
+ //#region src/federation/circuit-breaker.ts
656
+ const MAX_CUSTOM_FAILURE_HISTORY = 100;
657
+ /**
658
+ * Tracks reachability state for remote outbox delivery hosts.
659
+ * @since 2.3.0
660
+ */
661
+ var CircuitBreaker = class {
662
+ #kv;
663
+ #prefix;
664
+ #options;
665
+ #now;
666
+ #stateChangeObserver;
667
+ constructor(options) {
668
+ this.#kv = options.kv;
669
+ this.#prefix = options.prefix;
670
+ this.#options = normalizeCircuitBreakerOptions(options.options ?? {});
671
+ this.#now = options.now ?? (() => Temporal.Now.instant());
672
+ this.#stateChangeObserver = options.stateChangeObserver;
673
+ }
674
+ get options() {
675
+ return this.#options;
676
+ }
677
+ capHeldDelay(heldSince, delay) {
678
+ const now = this.#now();
679
+ return now.until(this.#capHeldRetryAt(now, heldSince, now.add(delay)));
680
+ }
681
+ async beforeSend(remoteHost, message) {
682
+ const heldSince = parseHeldSince(message.circuitHeldSince);
683
+ const now = this.#now();
684
+ if (heldSince != null && Temporal.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
685
+ type: "drop",
686
+ heldSince
687
+ };
688
+ let lastConflictingState;
689
+ for (let attempt = 0; attempt < 10; attempt++) {
690
+ const oldState = await this.#get(remoteHost);
691
+ if (oldState == null || oldState.state === "closed") return {
692
+ type: "send",
693
+ probe: false
694
+ };
695
+ if (oldState.state === "half-open") {
696
+ const halfOpened = oldState.halfOpened == null ? void 0 : Temporal.Instant.from(oldState.halfOpened);
697
+ if (halfOpened != null) {
698
+ const staleAt = halfOpened.add(this.#options.recoveryDelay);
699
+ if (Temporal.Instant.compare(now, staleAt) < 0) {
700
+ const releaseAt = now.add(this.#options.releaseInterval);
701
+ const retryAt = Temporal.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
702
+ const cappedRetryAt = this.#capHeldRetryAt(now, heldSince, retryAt);
703
+ return {
704
+ type: "hold",
705
+ state: "half-open",
706
+ delay: now.until(cappedRetryAt),
707
+ heldSince: heldSince ?? now
708
+ };
709
+ }
710
+ }
711
+ const newState = {
712
+ ...oldState,
713
+ state: "half-open",
714
+ halfOpened: now.toString()
715
+ };
716
+ if (await this.#replace(remoteHost, oldState, newState)) return {
717
+ type: "send",
718
+ probe: true
719
+ };
720
+ lastConflictingState = "half-open";
721
+ continue;
722
+ }
723
+ const probeAt = (oldState.opened == null ? now : Temporal.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
724
+ if (Temporal.Instant.compare(now, probeAt) < 0) {
725
+ const retryAt = this.#capHeldRetryAt(now, heldSince, probeAt);
726
+ return {
727
+ type: "hold",
728
+ state: "open",
729
+ delay: now.until(retryAt),
730
+ heldSince: heldSince ?? now
731
+ };
732
+ }
733
+ const newState = {
734
+ ...oldState,
735
+ state: "half-open",
736
+ halfOpened: now.toString()
737
+ };
738
+ if (await this.#replace(remoteHost, oldState, newState)) {
739
+ await this.#notifyStateChange(remoteHost, "open", "half-open");
740
+ return {
741
+ type: "send",
742
+ probe: true,
743
+ stateChange: {
744
+ previousState: "open",
745
+ newState: "half-open"
746
+ }
747
+ };
748
+ }
749
+ lastConflictingState = "open";
750
+ }
751
+ if (lastConflictingState != null) {
752
+ const retryAt = this.#capHeldRetryAt(now, heldSince, now.add(this.#options.releaseInterval));
753
+ return {
754
+ type: "hold",
755
+ state: lastConflictingState,
756
+ delay: now.until(retryAt),
757
+ heldSince: heldSince ?? now
758
+ };
759
+ }
760
+ throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
761
+ }
762
+ async recordSuccess(remoteHost) {
763
+ for (let attempt = 0; attempt < 10; attempt++) {
764
+ const oldState = await this.#get(remoteHost);
765
+ if (oldState == null) return void 0;
766
+ if (await this.#replace(remoteHost, oldState, void 0)) {
767
+ if (oldState.state !== "closed") {
768
+ await this.#notifyStateChange(remoteHost, oldState.state, "closed");
769
+ return {
770
+ previousState: oldState.state,
771
+ newState: "closed"
772
+ };
773
+ }
774
+ return;
775
+ }
776
+ }
777
+ throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
778
+ }
779
+ async recordReachableFailure(remoteHost) {
780
+ return await this.recordSuccess(remoteHost);
781
+ }
782
+ async recordFailure(remoteHost) {
783
+ const now = this.#now();
784
+ for (let attempt = 0; attempt < 10; attempt++) {
785
+ const oldState = await this.#get(remoteHost);
786
+ if (oldState?.state === "open") return void 0;
787
+ const oldFailures = oldState?.failures.map(Temporal.Instant.from) ?? [];
788
+ const failures = this.#options.pruneFailures([...oldFailures, now], now);
789
+ let newState;
790
+ let transition;
791
+ if (oldState?.state === "half-open" || this.#options.failure(failures)) {
792
+ newState = {
793
+ state: "open",
794
+ failures: failures.map((t) => t.toString()),
795
+ opened: now.toString()
796
+ };
797
+ transition = [oldState?.state ?? "closed", "open"];
798
+ } else newState = {
799
+ state: "closed",
800
+ failures: failures.map((t) => t.toString())
801
+ };
802
+ if (await this.#replace(remoteHost, oldState, newState)) {
803
+ if (transition != null) {
804
+ await this.#notifyStateChange(remoteHost, transition[0], transition[1]);
805
+ return {
806
+ previousState: transition[0],
807
+ newState: transition[1]
808
+ };
809
+ }
810
+ return;
811
+ }
812
+ }
813
+ throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
814
+ }
815
+ async dropActivity(remoteHost, details) {
816
+ try {
817
+ await this.#options.onActivityDrop?.(remoteHost, details);
818
+ } catch (error) {
819
+ getLogger([
820
+ "fedify",
821
+ "federation",
822
+ "circuit"
823
+ ]).error("An unexpected error occurred in circuit breaker activity drop handler:\n{error}", {
824
+ remoteHost,
825
+ error
826
+ });
827
+ }
828
+ }
829
+ async getState(remoteHost) {
830
+ return await this.#get(remoteHost);
831
+ }
832
+ #key(remoteHost) {
833
+ return [...this.#prefix, remoteHost];
834
+ }
835
+ #capHeldRetryAt(now, heldSince, retryAt) {
836
+ const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
837
+ return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
838
+ }
839
+ async #get(remoteHost) {
840
+ return parseCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
841
+ }
842
+ async #replace(remoteHost, oldState, newState) {
843
+ const key = this.#key(remoteHost);
844
+ if (this.#kv.cas == null) {
845
+ if (newState == null) await this.#kv.delete(key);
846
+ else await this.#kv.set(key, newState);
847
+ return true;
848
+ }
849
+ return await this.#kv.cas(key, oldState, newState);
850
+ }
851
+ async #notifyStateChange(remoteHost, previousState, newState) {
852
+ try {
853
+ await this.#options.onStateChange?.(remoteHost, previousState, newState);
854
+ } catch (error) {
855
+ getLogger([
856
+ "fedify",
857
+ "federation",
858
+ "circuit"
859
+ ]).error("An unexpected error occurred in circuit breaker state change handler:\n{error}", {
860
+ remoteHost,
861
+ previousState,
862
+ newState,
863
+ error
864
+ });
865
+ }
866
+ try {
867
+ await this.#stateChangeObserver?.(remoteHost, previousState, newState);
868
+ } catch (error) {
869
+ getLogger([
870
+ "fedify",
871
+ "federation",
872
+ "circuit"
873
+ ]).error("An unexpected error occurred in circuit breaker state change observer:\n{error}", {
874
+ remoteHost,
875
+ previousState,
876
+ newState,
877
+ error
878
+ });
879
+ }
880
+ }
881
+ };
882
+ /**
883
+ * Normalizes user-provided circuit breaker options into the internal policy
884
+ * shape used while processing queued outbox deliveries.
885
+ *
886
+ * @param options The public circuit breaker options supplied to Fedify.
887
+ * @returns The normalized failure predicate, failure pruning function,
888
+ * duration values, and optional callbacks with defaults applied.
889
+ * @throws {RangeError} If any configured duration is not positive.
890
+ * @throws {TypeError} If `failureThreshold` is not a positive integer.
891
+ */
892
+ function normalizeCircuitBreakerOptions(options) {
893
+ const recoveryDelay = toInstantDuration(options.recoveryDelay ?? { minutes: 30 });
894
+ const heldActivityTtl = toInstantDuration(options.heldActivityTtl ?? { hours: 168 });
895
+ const releaseInterval = toInstantDuration(options.releaseInterval ?? { seconds: 1 });
896
+ assertPositiveDuration(recoveryDelay, "recoveryDelay");
897
+ assertPositiveDuration(heldActivityTtl, "heldActivityTtl");
898
+ assertPositiveDuration(releaseInterval, "releaseInterval");
899
+ let failure;
900
+ let pruneFailures;
901
+ if (options.failure == null) {
902
+ const failureThreshold = options.failureThreshold ?? 5;
903
+ if (!Number.isInteger(failureThreshold) || failureThreshold <= 0) throw new TypeError("failureThreshold must be a positive integer.");
904
+ const failureWindow = toInstantDuration(options.failureWindow ?? { minutes: 10 });
905
+ assertPositiveDuration(failureWindow, "failureWindow");
906
+ pruneFailures = (timestamps, now) => {
907
+ const earliest = now.subtract(failureWindow);
908
+ return timestamps.filter((timestamp) => Temporal.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
909
+ };
910
+ failure = (timestamps) => {
911
+ if (timestamps.length < failureThreshold) return false;
912
+ const first = timestamps[timestamps.length - failureThreshold];
913
+ const last = timestamps[timestamps.length - 1];
914
+ return Temporal.Duration.compare(first.until(last), failureWindow) <= 0;
915
+ };
916
+ } else {
917
+ failure = options.failure;
918
+ pruneFailures = (timestamps) => timestamps.slice(-MAX_CUSTOM_FAILURE_HISTORY);
919
+ }
920
+ return {
921
+ failure,
922
+ pruneFailures,
923
+ recoveryDelay,
924
+ heldActivityTtl,
925
+ releaseInterval,
926
+ onStateChange: options.onStateChange,
927
+ onActivityDrop: options.onActivityDrop
928
+ };
929
+ }
930
+ function toInstantDuration(duration) {
931
+ const parsed = Temporal.Duration.from(duration);
932
+ return Temporal.Duration.from({ milliseconds: Math.trunc(parsed.total({
933
+ unit: "millisecond",
934
+ relativeTo: Temporal.PlainDateTime.from("2026-01-01T00:00:00")
935
+ })) });
936
+ }
937
+ function assertPositiveDuration(duration, name) {
938
+ if (Temporal.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
939
+ }
940
+ function parseHeldSince(value) {
941
+ if (value == null) return void 0;
942
+ try {
943
+ return Temporal.Instant.from(value);
944
+ } catch (error) {
945
+ getLogger([
946
+ "fedify",
947
+ "federation",
948
+ "circuit"
949
+ ]).warn("Invalid circuitHeldSince value in queued outbox message: {value}", {
950
+ value,
951
+ error
952
+ });
953
+ return;
954
+ }
955
+ }
956
+ /**
957
+ * Parses a value loaded from the circuit breaker KV store.
958
+ *
959
+ * @param value The raw KV value to validate.
960
+ * @returns A circuit breaker state when `value` has a recognized state and
961
+ * valid instant strings, or `undefined` when the stored value is malformed.
962
+ */
963
+ function parseCircuitBreakerKvState(value) {
964
+ const isInstantString = (v) => {
965
+ if (typeof v !== "string") return false;
966
+ try {
967
+ Temporal.Instant.from(v);
968
+ return true;
969
+ } catch {
970
+ return false;
971
+ }
972
+ };
973
+ if (typeof value !== "object" || value == null) return void 0;
974
+ const record = value;
975
+ if (record.state !== "closed" && record.state !== "open" && record.state !== "half-open") return;
976
+ if (!Array.isArray(record.failures) || !record.failures.every((failure) => isInstantString(failure))) return;
977
+ if (record.opened != null && !isInstantString(record.opened)) return;
978
+ if (record.halfOpened != null && !isInstantString(record.halfOpened)) return;
979
+ return {
980
+ state: record.state,
981
+ failures: record.failures,
982
+ ...record.opened == null ? {} : { opened: record.opened },
983
+ ...record.halfOpened == null ? {} : { halfOpened: record.halfOpened }
984
+ };
985
+ }
986
+ //#endregion
655
987
  //#region src/federation/collection.ts
656
988
  /**
657
989
  * Calculates the [partial follower collection digest][1].
@@ -2910,13 +3242,14 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
2910
3242
  specDeterminer
2911
3243
  });
2912
3244
  } catch (error) {
3245
+ const transportError = error instanceof FetchError ? error : createFetchError(inbox.href, error);
2913
3246
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2914
3247
  activityId,
2915
3248
  inbox: inbox.href,
2916
- error
3249
+ error: transportError
2917
3250
  });
2918
3251
  federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
2919
- throw error;
3252
+ throw transportError;
2920
3253
  }
2921
3254
  try {
2922
3255
  if (!response.ok) {
@@ -2933,7 +3266,7 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
2933
3266
  statusText: response.statusText,
2934
3267
  error
2935
3268
  });
2936
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
3269
+ throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
2937
3270
  }
2938
3271
  deliverySuccess = true;
2939
3272
  const eventAttributes = {
@@ -2948,6 +3281,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
2948
3281
  federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
2949
3282
  }
2950
3283
  }
3284
+ function createFetchError(url, cause) {
3285
+ const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
3286
+ error.cause = cause;
3287
+ return error;
3288
+ }
2951
3289
  /**
2952
3290
  * An error that is thrown when an activity fails to send to a remote inbox.
2953
3291
  * It contains structured information about the failure, including the HTTP
@@ -2971,18 +3309,25 @@ var SendActivityError = class extends Error {
2971
3309
  */
2972
3310
  responseBody;
2973
3311
  /**
3312
+ * The response headers from the inbox.
3313
+ * @since 2.3.0
3314
+ */
3315
+ responseHeaders;
3316
+ /**
2974
3317
  * Creates a new {@link SendActivityError}.
2975
3318
  * @param inbox The inbox URL.
2976
3319
  * @param statusCode The HTTP status code.
2977
3320
  * @param message The error message.
2978
3321
  * @param responseBody The response body.
3322
+ * @param responseHeaders The response headers.
2979
3323
  */
2980
- constructor(inbox, statusCode, message, responseBody) {
3324
+ constructor(inbox, statusCode, message, responseBody, responseHeaders) {
2981
3325
  super(message);
2982
3326
  this.name = "SendActivityError";
2983
3327
  this.inbox = inbox;
2984
3328
  this.statusCode = statusCode;
2985
3329
  this.responseBody = responseBody;
3330
+ this.responseHeaders = new Headers(responseHeaders);
2986
3331
  }
2987
3332
  };
2988
3333
  //#endregion
@@ -3181,6 +3526,57 @@ var middleware_exports = /* @__PURE__ */ __exportAll({
3181
3526
  OutboxContextImpl: () => OutboxContextImpl,
3182
3527
  createFederation: () => createFederation
3183
3528
  });
3529
+ const circuitBreakerCasWarningKvStores = /* @__PURE__ */ new WeakSet();
3530
+ const retryAfterHttpDate = /* @__PURE__ */ new RegExp("^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} \\d{2}:\\d{2}:\\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{2} \\d{2}:\\d{2}:\\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: \\d|\\d{2}) \\d{2}:\\d{2}:\\d{2} \\d{4})$");
3531
+ function parseRetryAfter(headers, now = Temporal.Now.instant()) {
3532
+ const value = headers.get("Retry-After");
3533
+ if (value == null) return void 0;
3534
+ const trimmed = value.trim();
3535
+ if (/^\d+$/.test(trimmed)) {
3536
+ const seconds = Number(trimmed);
3537
+ if (!Number.isFinite(seconds)) return void 0;
3538
+ return parseRetryAfterDuration({ seconds });
3539
+ }
3540
+ if (!retryAfterHttpDate.test(trimmed)) return void 0;
3541
+ const httpDate = trimmed.endsWith("GMT") ? trimmed : `${trimmed} GMT`;
3542
+ const retryAtMs = Date.parse(httpDate);
3543
+ if (Number.isNaN(retryAtMs)) return void 0;
3544
+ const nowMs = Number(now.epochMilliseconds);
3545
+ return parseRetryAfterDuration({ milliseconds: Math.max(0, retryAtMs - nowMs) });
3546
+ }
3547
+ function parseRetryAfterDuration(durationLike) {
3548
+ try {
3549
+ return Temporal.Duration.from(durationLike);
3550
+ } catch (error) {
3551
+ if (error instanceof RangeError) return void 0;
3552
+ throw error;
3553
+ }
3554
+ }
3555
+ function clampNegativeDelay(delay) {
3556
+ return delay.sign < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay;
3557
+ }
3558
+ function maxDelay(first, second) {
3559
+ return Temporal.Duration.compare(first, second) >= 0 ? first : second;
3560
+ }
3561
+ function isTransportDeliveryError(error) {
3562
+ return error instanceof FetchError || isAbortError(error);
3563
+ }
3564
+ function toCircuitBreakerMetricState(state) {
3565
+ return state === "half-open" ? "half_open" : state;
3566
+ }
3567
+ function recordCircuitBreakerSpanEvent(span, remoteHost, change) {
3568
+ span.addEvent("activitypub.circuit_breaker.state_change", {
3569
+ "activitypub.remote.host": remoteHost,
3570
+ "activitypub.circuit_breaker.previous_state": toCircuitBreakerMetricState(change.previousState),
3571
+ "activitypub.circuit_breaker.state": toCircuitBreakerMetricState(change.newState)
3572
+ });
3573
+ }
3574
+ function recordCircuitBreakerHeldSpanEvent(span, remoteHost, state) {
3575
+ span.addEvent("activitypub.circuit_breaker.held", {
3576
+ "activitypub.remote.host": remoteHost,
3577
+ "activitypub.circuit_breaker.state": toCircuitBreakerMetricState(state)
3578
+ });
3579
+ }
3184
3580
  function isRemoteContextLoadingFailure(error) {
3185
3581
  return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
3186
3582
  }
@@ -3224,6 +3620,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3224
3620
  skipSignatureVerification;
3225
3621
  outboxRetryPolicy;
3226
3622
  inboxRetryPolicy;
3623
+ circuitBreaker;
3227
3624
  activityTransformers;
3228
3625
  _tracerProvider;
3229
3626
  _meterProvider;
@@ -3238,6 +3635,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3238
3635
  publicKey: ["_fedify", "publicKey"],
3239
3636
  httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
3240
3637
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
3638
+ circuitBreaker: ["_fedify", "circuit"],
3241
3639
  ...options.kvPrefixes ?? {}
3242
3640
  };
3243
3641
  if (options.queue == null) {
@@ -3253,6 +3651,25 @@ var FederationImpl = class extends FederationBuilderImpl {
3253
3651
  this.outboxQueue = options.queue.outbox;
3254
3652
  this.fanoutQueue = options.queue.fanout;
3255
3653
  }
3654
+ if (options.circuitBreaker !== false && this.outboxQueue != null) {
3655
+ this.circuitBreaker = new CircuitBreaker({
3656
+ kv: options.kv,
3657
+ prefix: this.kvPrefixes.circuitBreaker,
3658
+ options: options.circuitBreaker,
3659
+ stateChangeObserver: (remoteHost, _previousState, newState) => {
3660
+ const metricState = toCircuitBreakerMetricState(newState);
3661
+ recordCircuitBreakerStateChange(this.meterProvider, remoteHost, metricState);
3662
+ }
3663
+ });
3664
+ if (options.kv.cas == null && !circuitBreakerCasWarningKvStores.has(options.kv)) {
3665
+ circuitBreakerCasWarningKvStores.add(options.kv);
3666
+ getLogger([
3667
+ "fedify",
3668
+ "federation",
3669
+ "circuit"
3670
+ ]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
3671
+ }
3672
+ }
3256
3673
  this.inboxQueueStarted = false;
3257
3674
  this.outboxQueueStarted = false;
3258
3675
  this.fanoutQueueStarted = false;
@@ -3561,19 +3978,129 @@ var FederationImpl = class extends FederationBuilderImpl {
3561
3978
  if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
3562
3979
  keys.push(pair);
3563
3980
  }
3981
+ const loaderOptions = this.#getLoaderOptions(message.baseUrl);
3982
+ let parsedActorIds;
3983
+ const getActorIds = () => {
3984
+ parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
3985
+ try {
3986
+ return [new URL(id)];
3987
+ } catch {
3988
+ logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
3989
+ return [];
3990
+ }
3991
+ });
3992
+ return parsedActorIds;
3993
+ };
3994
+ const parseActivity = () => Activity.fromJsonLd(message.activity, {
3995
+ contextLoader: this.contextLoaderFactory(loaderOptions),
3996
+ documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
3997
+ tracerProvider: this.tracerProvider
3998
+ });
3999
+ const enqueueHeldOutboxMessage = async (delay, heldSince) => {
4000
+ const { outboxQueue } = this;
4001
+ if (outboxQueue == null) return;
4002
+ const heldMessage = {
4003
+ ...message,
4004
+ circuitHeld: true,
4005
+ circuitHeldSince: heldSince.toString()
4006
+ };
4007
+ await outboxQueue.enqueue(heldMessage, {
4008
+ delay: clampNegativeDelay(delay),
4009
+ orderingKey: message.orderingKey
4010
+ });
4011
+ getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
4012
+ role: "outbox",
4013
+ queue: outboxQueue,
4014
+ activityType: heldMessage.activityType
4015
+ }, heldMessage.attempt);
4016
+ };
4017
+ const dropHeldOutboxMessage = async (circuit, remoteHost, inbox, heldSince, activity) => {
4018
+ await circuit.dropActivity(remoteHost, {
4019
+ inbox,
4020
+ activity,
4021
+ activityId: message.activityId,
4022
+ activityType: message.activityType,
4023
+ actorIds: getActorIds(),
4024
+ heldSince
4025
+ });
4026
+ if (this.outboxPermanentFailureHandler != null) {
4027
+ const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
4028
+ try {
4029
+ await this.outboxPermanentFailureHandler(ctx, {
4030
+ reason: "circuit-breaker-ttl",
4031
+ inbox,
4032
+ activity,
4033
+ error: new SendActivityError(inbox, 0, "Circuit breaker held activity expired.", ""),
4034
+ statusCode: 0,
4035
+ circuitHeldSince: heldSince,
4036
+ actorIds: getActorIds()
4037
+ });
4038
+ } catch (handlerError) {
4039
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
4040
+ ...logData,
4041
+ error: handlerError
4042
+ });
4043
+ }
4044
+ }
4045
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
4046
+ };
3564
4047
  try {
4048
+ const inbox = new URL(message.inbox);
4049
+ const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
4050
+ const remoteHost = getRemoteHost(inbox);
4051
+ let decision;
4052
+ if (circuit != null) try {
4053
+ decision = await circuit.beforeSend(remoteHost, message);
4054
+ } catch (circuitError) {
4055
+ getLogger([
4056
+ "fedify",
4057
+ "federation",
4058
+ "circuit"
4059
+ ]).error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
4060
+ ...logData,
4061
+ remoteHost,
4062
+ error: circuitError
4063
+ });
4064
+ }
4065
+ if (decision != null && circuit != null) {
4066
+ if (decision.type === "hold") {
4067
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
4068
+ await enqueueHeldOutboxMessage(decision.delay, decision.heldSince);
4069
+ return;
4070
+ }
4071
+ if (decision.type === "drop") {
4072
+ const activity = await parseActivity();
4073
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
4074
+ return;
4075
+ }
4076
+ if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
4077
+ }
3565
4078
  await sendActivity({
3566
4079
  keys,
3567
4080
  activity: message.activity,
3568
4081
  activityId: message.activityId,
3569
4082
  activityType: message.activityType,
3570
- inbox: new URL(message.inbox),
4083
+ inbox,
3571
4084
  sharedInbox: message.sharedInbox,
3572
4085
  headers: new Headers(message.headers),
3573
4086
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
3574
4087
  meterProvider: this.meterProvider,
3575
4088
  tracerProvider: this.tracerProvider
3576
4089
  });
4090
+ if (circuit != null) try {
4091
+ const stateChange = await circuit.recordSuccess(remoteHost);
4092
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
4093
+ } catch (error) {
4094
+ getLogger([
4095
+ "fedify",
4096
+ "federation",
4097
+ "circuit"
4098
+ ]).error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
4099
+ ...logData,
4100
+ remoteHost,
4101
+ error
4102
+ });
4103
+ }
3577
4104
  } catch (error) {
3578
4105
  span.setStatus({
3579
4106
  code: SpanStatusCode.ERROR,
@@ -3588,18 +4115,65 @@ var FederationImpl = class extends FederationBuilderImpl {
3588
4115
  return;
3589
4116
  }
3590
4117
  })();
4118
+ let retryAfterDelay;
4119
+ let circuitHold;
4120
+ let circuitDrop;
4121
+ let retryPolicyDelay;
4122
+ let policyDelayCalculated = false;
4123
+ const getPolicyDelay = () => {
4124
+ if (!policyDelayCalculated) {
4125
+ retryPolicyDelay = this.outboxRetryPolicy({
4126
+ elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
4127
+ attempts: message.attempt
4128
+ });
4129
+ policyDelayCalculated = true;
4130
+ }
4131
+ return retryPolicyDelay;
4132
+ };
4133
+ const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
4134
+ if (!isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503)) retryAfterDelay = parseRetryAfter(error.responseHeaders);
4135
+ if (remoteHost != null && this.outboxQueue != null && this.circuitBreaker != null) try {
4136
+ if (error instanceof SendActivityError) {
4137
+ const { statusCode } = error;
4138
+ const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await this.circuitBreaker.recordReachableFailure(remoteHost) : statusCode >= 500 ? await this.circuitBreaker.recordFailure(remoteHost) : void 0;
4139
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
4140
+ } else if (isTransportDeliveryError(error)) {
4141
+ const stateChange = await this.circuitBreaker.recordFailure(remoteHost);
4142
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
4143
+ }
4144
+ if (!isPermanentFailure) {
4145
+ const circuitDecision = await this.circuitBreaker.beforeSend(remoteHost, message);
4146
+ if (circuitDecision.type === "hold") circuitHold = {
4147
+ delay: circuitDecision.delay,
4148
+ heldSince: circuitDecision.heldSince,
4149
+ remoteHost,
4150
+ state: circuitDecision.state
4151
+ };
4152
+ else if (circuitDecision.type === "drop") circuitDrop = {
4153
+ circuit: this.circuitBreaker,
4154
+ remoteHost,
4155
+ inbox: new URL(message.inbox),
4156
+ heldSince: circuitDecision.heldSince
4157
+ };
4158
+ }
4159
+ } catch (circuitError) {
4160
+ getLogger([
4161
+ "fedify",
4162
+ "federation",
4163
+ "circuit"
4164
+ ]).error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
4165
+ ...logData,
4166
+ remoteHost,
4167
+ error: circuitError
4168
+ });
4169
+ }
3591
4170
  span.addEvent("activitypub.delivery.failed", {
3592
4171
  ...remoteHost == null ? {} : { "activitypub.remote.host": remoteHost },
3593
4172
  "activitypub.delivery.attempt": message.attempt,
3594
- "activitypub.delivery.permanent_failure": error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode),
4173
+ "activitypub.delivery.permanent_failure": isPermanentFailure,
3595
4174
  ...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
3596
4175
  });
3597
- const loaderOptions = this.#getLoaderOptions(message.baseUrl);
3598
- const activity = await Activity.fromJsonLd(message.activity, {
3599
- contextLoader: this.contextLoaderFactory(loaderOptions),
3600
- documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
3601
- tracerProvider: this.tracerProvider
3602
- });
4176
+ const activity = await parseActivity();
3603
4177
  try {
3604
4178
  await this.onOutboxError?.(error, activity);
3605
4179
  } catch (error) {
@@ -3608,7 +4182,11 @@ var FederationImpl = class extends FederationBuilderImpl {
3608
4182
  error
3609
4183
  });
3610
4184
  }
3611
- if (error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode)) {
4185
+ if (circuitDrop != null) {
4186
+ await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
4187
+ return;
4188
+ }
4189
+ if (isPermanentFailure) {
3612
4190
  getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
3613
4191
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
3614
4192
  ...logData,
@@ -3618,18 +4196,12 @@ var FederationImpl = class extends FederationBuilderImpl {
3618
4196
  const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
3619
4197
  try {
3620
4198
  await this.outboxPermanentFailureHandler(ctx, {
4199
+ reason: "http",
3621
4200
  inbox: new URL(message.inbox),
3622
4201
  activity,
3623
4202
  error,
3624
4203
  statusCode: error.statusCode,
3625
- actorIds: (message.actorIds ?? []).flatMap((id) => {
3626
- try {
3627
- return [new URL(id)];
3628
- } catch {
3629
- logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
3630
- return [];
3631
- }
3632
- })
4204
+ actorIds: getActorIds()
3633
4205
  });
3634
4206
  } catch (handlerError) {
3635
4207
  logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
@@ -3641,17 +4213,25 @@ var FederationImpl = class extends FederationBuilderImpl {
3641
4213
  recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
3642
4214
  return;
3643
4215
  }
3644
- if (this.outboxQueue?.nativeRetrial) {
4216
+ if (circuitHold != null && getPolicyDelay() != null) {
4217
+ logger.error("Failed to send activity {activityId} to {inbox}; holding because the remote host circuit is open:\n{error}", {
4218
+ ...logData,
4219
+ error
4220
+ });
4221
+ recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
4222
+ const circuit = this.circuitBreaker;
4223
+ await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
4224
+ return;
4225
+ }
4226
+ if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
3645
4227
  logger.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
3646
4228
  ...logData,
3647
4229
  error
3648
4230
  });
3649
4231
  throw error;
3650
4232
  }
3651
- const delay = this.outboxRetryPolicy({
3652
- elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
3653
- attempts: message.attempt
3654
- });
4233
+ const policyDelay = getPolicyDelay();
4234
+ const delay = policyDelay == null ? null : retryAfterDelay ?? policyDelay;
3655
4235
  if (delay != null) {
3656
4236
  logger.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
3657
4237
  ...logData,
@@ -3663,7 +4243,10 @@ var FederationImpl = class extends FederationBuilderImpl {
3663
4243
  };
3664
4244
  const { outboxQueue } = this;
3665
4245
  if (outboxQueue != null) {
3666
- await outboxQueue.enqueue(retryMessage, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
4246
+ await outboxQueue.enqueue(retryMessage, {
4247
+ delay: clampNegativeDelay(delay),
4248
+ orderingKey: message.orderingKey
4249
+ });
3667
4250
  getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
3668
4251
  role: "outbox",
3669
4252
  queue: outboxQueue,
@@ -3752,7 +4335,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3752
4335
  ...message,
3753
4336
  attempt: message.attempt + 1
3754
4337
  };
3755
- await this.inboxQueue.enqueue(retryMessage, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
4338
+ await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
3756
4339
  if (activityType != null) {
3757
4340
  getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
3758
4341
  role: "inbox",
@@ -5505,4 +6088,4 @@ function getRequestId(request) {
5505
6088
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
5506
6089
  }
5507
6090
  //#endregion
5508
- export { createExponentialBackoffPolicy as a, buildCollectionSynchronizationHeader as c, SendActivityError as i, digest as l, middleware_exports as n, respondWithObject as o, handleWebFinger as r, respondWithObjectIfAcceptable as s, createFederation as t, createFederationBuilder as u };
6091
+ export { createExponentialBackoffPolicy as a, buildCollectionSynchronizationHeader as c, normalizeCircuitBreakerOptions as d, parseCircuitBreakerKvState as f, SendActivityError as i, digest as l, middleware_exports as n, respondWithObject as o, createFederationBuilder as p, handleWebFinger as r, respondWithObjectIfAcceptable as s, createFederation as t, CircuitBreaker as u };