@peerbit/shared-log 16.0.25 → 16.0.27

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.
@@ -25,6 +25,11 @@ const REQUIRED_SENDER_CAPABILITIES =
25
25
  const DEFAULT_REQUEST_RETRY_MS = 1_000;
26
26
  const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
27
27
  const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
28
+ const DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS = 2_000;
29
+ const DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS = 5_000;
30
+ const MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION = 2;
31
+ const MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL = 64;
32
+ const MAX_TIMER_MS = 2_147_483_647;
28
33
  const MAX_U64 = (1n << 64n) - 1n;
29
34
  const MAX_BACKOFF_EXPONENT = 20;
30
35
 
@@ -113,10 +118,50 @@ export type ReplicationInfoV2LocalCapabilityAdvertisement = {
113
118
  };
114
119
 
115
120
  type ReplicationInfoV2RemoteFullRearm = {
116
- lastAttemptAt: number;
121
+ peerHash: string;
122
+ receiveEpoch: object | null;
123
+ receiverTransportSession: bigint;
124
+ nextAttemptAt: number;
125
+ attemptsStarted: number;
126
+ outstandingAttempts: number;
117
127
  inFlight?: Promise<void>;
128
+ controller?: AbortController;
118
129
  };
119
130
 
131
+ export type ReplicationInfoV2ReceiveDiagnostic = Readonly<{
132
+ state: "absent" | "current" | "stale";
133
+ /** Current-state classification; no historical packet/drop log is retained. */
134
+ reason:
135
+ | "state-absent"
136
+ | "peer-session-mismatch"
137
+ | "receive-epoch-mismatch"
138
+ | "transport-session-mismatch"
139
+ | "state-not-current"
140
+ | "admission-pending"
141
+ | "active"
142
+ | "request-parked"
143
+ | "request-in-flight"
144
+ | "request-scheduled"
145
+ | "awaiting-full"
146
+ | "resync";
147
+ phase?: ReplicationInfoV2ReceivePhase;
148
+ requestState?:
149
+ | "idle"
150
+ | "scheduled"
151
+ | "in-flight"
152
+ | "parked"
153
+ | "admission-pending";
154
+ lastAppliedSequence?: string;
155
+ requestAttempts?: number;
156
+ capabilityAdvertisementAttempts: number;
157
+ capabilityAdvertisement: "absent" | "advertising" | "ready" | "stale";
158
+ remoteFullRearm: "idle" | "in-flight" | "cooldown" | "limited";
159
+ remoteFullRearmAttempts: number;
160
+ remoteFullRearmOutstanding: number;
161
+ remoteFullRearmGlobalOutstanding: number;
162
+ remoteFullRearmCooldownRemainingMs?: number;
163
+ }>;
164
+
120
165
  export type ReplicationInfoV2LocalCapabilityRefresh = {
121
166
  receiverTransportSession: bigint;
122
167
  requestNotBeforeMs: number;
@@ -196,6 +241,9 @@ export type ReplicationInfoV2ReceiveDeps = {
196
241
  requestRetryMs?: number;
197
242
  maxRequestRetryMs?: number;
198
243
  requestMaxAttempts?: number;
244
+ remoteFullRearmAttemptTimeoutMs?: number;
245
+ remoteFullRearmCooldownMs?: number;
246
+ maxRemoteFullRearmOutstandingGlobal?: number;
199
247
  };
200
248
 
201
249
  /**
@@ -224,12 +272,17 @@ export class ReplicationInfoV2ReceiveCoordinator {
224
272
  ReplicationInfoV2LocalCapabilityAdvertisement
225
273
  >;
226
274
  _remoteFullRearmBySession!: WeakMap<object, ReplicationInfoV2RemoteFullRearm>;
275
+ _remoteFullRearmsInFlight!: Set<ReplicationInfoV2RemoteFullRearm>;
276
+ _remoteFullRearmOutstanding!: Set<object>;
227
277
  _reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
228
278
 
229
279
  private readonly now: () => number;
230
280
  private readonly requestRetryMs: number;
231
281
  private readonly maxRequestRetryMs: number;
232
282
  private readonly requestMaxAttempts: number;
283
+ private readonly remoteFullRearmAttemptTimeoutMs: number;
284
+ private readonly remoteFullRearmCooldownMs: number;
285
+ private readonly maxRemoteFullRearmOutstandingGlobal: number;
233
286
 
234
287
  constructor(private readonly deps: ReplicationInfoV2ReceiveDeps) {
235
288
  this.now = deps.now ?? Date.now;
@@ -245,12 +298,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
245
298
  1,
246
299
  Math.floor(deps.requestMaxAttempts ?? DEFAULT_REQUEST_MAX_ATTEMPTS),
247
300
  );
301
+ this.remoteFullRearmAttemptTimeoutMs = Math.max(
302
+ 1,
303
+ Math.min(
304
+ MAX_TIMER_MS,
305
+ Math.floor(
306
+ deps.remoteFullRearmAttemptTimeoutMs ??
307
+ DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS,
308
+ ),
309
+ ),
310
+ );
311
+ this.remoteFullRearmCooldownMs = Math.max(
312
+ this.requestRetryMs,
313
+ Math.min(
314
+ MAX_TIMER_MS,
315
+ Math.floor(
316
+ deps.remoteFullRearmCooldownMs ??
317
+ DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS,
318
+ ),
319
+ ),
320
+ );
321
+ // The injected value is a test seam that may only tighten this lifetime
322
+ // resource bound, never widen it.
323
+ this.maxRemoteFullRearmOutstandingGlobal = Math.min(
324
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
325
+ Math.max(
326
+ 1,
327
+ Math.floor(
328
+ deps.maxRemoteFullRearmOutstandingGlobal ??
329
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
330
+ ),
331
+ ),
332
+ );
248
333
  this._receiveStates = new Map();
249
334
  this._cutoverPeerSessions = new WeakSet();
250
335
  this._localCapabilityReadyBySession = new WeakMap();
251
336
  this._localCapabilityContextBySession = new WeakMap();
252
337
  this._localCapabilityAdvertisementsByPeer = new Map();
253
338
  this._remoteFullRearmBySession = new WeakMap();
339
+ this._remoteFullRearmsInFlight = new Set();
340
+ this._remoteFullRearmOutstanding = new Set();
254
341
  this._reservedAdmissionsByPeer = new Map();
255
342
  }
256
343
 
@@ -262,10 +349,19 @@ export class ReplicationInfoV2ReceiveCoordinator {
262
349
  this._localCapabilityContextBySession = new WeakMap();
263
350
  this._localCapabilityAdvertisementsByPeer = new Map();
264
351
  this._remoteFullRearmBySession = new WeakMap();
352
+ this._remoteFullRearmsInFlight = new Set();
265
353
  this._reservedAdmissionsByPeer = new Map();
266
354
  }
267
355
 
268
356
  clearForClose(): void {
357
+ for (const rearm of this._remoteFullRearmsInFlight ?? []) {
358
+ rearm.controller?.abort(
359
+ new Error(
360
+ "Replication-info V2 receiver closed during remote Full rearm",
361
+ ),
362
+ );
363
+ }
364
+ this._remoteFullRearmsInFlight?.clear();
269
365
  for (const advertisement of [
270
366
  ...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
271
367
  ]) {
@@ -298,10 +394,28 @@ export class ReplicationInfoV2ReceiveCoordinator {
298
394
  this._localCapabilityContextBySession.delete(state.peerSession);
299
395
  this._cutoverPeerSessions.delete(state.peerSession);
300
396
  }
397
+ if (!expectedSession) {
398
+ for (const rearm of this._remoteFullRearmsInFlight) {
399
+ if (rearm.peerHash === peerHash) {
400
+ rearm.controller?.abort(
401
+ new Error("Replication-info V2 peer cleared during rearm"),
402
+ );
403
+ }
404
+ }
405
+ }
406
+ const rearmSession =
407
+ expectedSession ?? state?.peerSession ?? advertisement?.peerSession;
408
+ if (rearmSession) {
409
+ this._remoteFullRearmBySession
410
+ .get(rearmSession)
411
+ ?.controller?.abort(
412
+ new Error("Replication-info V2 peer session cleared during rearm"),
413
+ );
414
+ this._remoteFullRearmBySession.delete(rearmSession);
415
+ }
301
416
  if (expectedSession) {
302
417
  this._localCapabilityReadyBySession.delete(expectedSession);
303
418
  this._localCapabilityContextBySession.delete(expectedSession);
304
- this._remoteFullRearmBySession.delete(expectedSession);
305
419
  this._cutoverPeerSessions.delete(expectedSession);
306
420
  }
307
421
  }
@@ -643,9 +757,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
643
757
  * stream that was lost while both directions otherwise remained current.
644
758
  *
645
759
  * This deliberately does not mutate the steady advertisement worker. One
646
- * attempt is coalesced per PeerSession and bound to the caller's signal; the
647
- * readiness wait's bounded recovery tick decides whether another attempt is
648
- * needed.
760
+ * attempt is coalesced per exact PeerSession/receive generation and bound to
761
+ * its own deadline as well as the caller's signal. A cooldown avoids rotating
762
+ * a healthy in-flight challenge on every recovery tick. The remote rotates
763
+ * only when its receive state is active; later hints during resync only nudge
764
+ * that same bounded request generation. At most two transports that disregard
765
+ * abort remain outstanding for one exact session (64 across this coordinator's
766
+ * lifetime); reaching either cap stops new hints while the persisted-readiness
767
+ * gate remains fail closed.
649
768
  */
650
769
  reAdvertiseLocalCapabilityForRemoteFull(properties: {
651
770
  peerHash: string;
@@ -679,47 +798,135 @@ export class ReplicationInfoV2ReceiveCoordinator {
679
798
  }
680
799
 
681
800
  const now = this.now();
801
+ const receiverTransportSession = ready.receiverTransportSession;
682
802
  let rearm = this._remoteFullRearmBySession.get(properties.peerSession);
803
+ if (
804
+ rearm &&
805
+ (rearm.peerHash !== properties.peerHash ||
806
+ rearm.receiveEpoch !== properties.receiveEpoch ||
807
+ rearm.receiverTransportSession !== receiverTransportSession)
808
+ ) {
809
+ rearm.controller?.abort(
810
+ new Error("Replication-info V2 remote Full rearm generation changed"),
811
+ );
812
+ rearm = undefined;
813
+ }
683
814
  if (rearm?.inFlight) {
684
815
  return true;
685
816
  }
817
+ if (rearm !== undefined && now < rearm.nextAttemptAt) {
818
+ return true;
819
+ }
686
820
  if (
687
- rearm !== undefined &&
688
- now - rearm.lastAttemptAt < this.requestRetryMs
821
+ (rearm?.outstandingAttempts ?? 0) >=
822
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION ||
823
+ this._remoteFullRearmOutstanding.size >=
824
+ this.maxRemoteFullRearmOutstandingGlobal
689
825
  ) {
690
826
  return true;
691
827
  }
692
828
  if (!rearm) {
693
- rearm = { lastAttemptAt: now };
829
+ rearm = {
830
+ peerHash: properties.peerHash,
831
+ receiveEpoch: properties.receiveEpoch,
832
+ receiverTransportSession,
833
+ nextAttemptAt: now + this.remoteFullRearmCooldownMs,
834
+ attemptsStarted: 0,
835
+ outstandingAttempts: 0,
836
+ };
694
837
  this._remoteFullRearmBySession.set(properties.peerSession, rearm);
695
838
  } else {
696
- rearm.lastAttemptAt = now;
839
+ rearm.nextAttemptAt = now + this.remoteFullRearmCooldownMs;
697
840
  }
841
+ const attemptState = rearm;
842
+ const outstandingReservation = {};
843
+ attemptState.attemptsStarted = Math.min(
844
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
845
+ attemptState.attemptsStarted + 1,
846
+ );
847
+ attemptState.outstandingAttempts++;
848
+ this._remoteFullRearmOutstanding.add(outstandingReservation);
849
+
850
+ const attemptController = new AbortController();
851
+ const operationSignal = attemptController.signal;
852
+ attemptState.controller = attemptController;
853
+ this._remoteFullRearmsInFlight.add(attemptState);
854
+ const sourceSignals = Array.from(
855
+ new Set([context.lifecycleSignal, properties.signal]),
856
+ );
857
+ const onSourceAbort = (event: Event) => {
858
+ const source = event.currentTarget as AbortSignal;
859
+ attemptController.abort(source.reason);
860
+ };
861
+ for (const source of sourceSignals) {
862
+ source.addEventListener("abort", onSourceAbort, { once: true });
863
+ }
864
+ const attemptTimer = setTimeout(
865
+ () =>
866
+ attemptController.abort(
867
+ new Error("Replication-info V2 remote Full rearm attempt timed out"),
868
+ ),
869
+ this.remoteFullRearmAttemptTimeoutMs,
870
+ );
871
+ attemptTimer.unref?.();
872
+ let releasedOutstanding = false;
873
+ const releaseOutstanding = () => {
874
+ if (releasedOutstanding) return;
875
+ releasedOutstanding = true;
876
+ this._remoteFullRearmOutstanding.delete(outstandingReservation);
877
+ attemptState.outstandingAttempts--;
878
+ };
879
+ const refresh = Promise.resolve().then(() => {
880
+ if (operationSignal.aborted) {
881
+ throw operationSignal.reason;
882
+ }
883
+ return this.deps.refreshLocalCapability({
884
+ peerHash: properties.peerHash,
885
+ target: context.target,
886
+ peerSession: properties.peerSession,
887
+ receiveEpoch: properties.receiveEpoch,
888
+ signal: operationSignal,
889
+ requestRemoteFullRearm: true,
890
+ });
891
+ });
892
+ void refresh.then(releaseOutstanding, releaseOutstanding);
698
893
 
699
- const operationSignal = AbortSignal.any([
700
- context.lifecycleSignal,
701
- properties.signal,
702
- ]);
703
894
  let operation: Promise<void>;
704
895
  const detach = () => {
705
- if (rearm?.inFlight === operation) {
706
- rearm.inFlight = undefined;
896
+ clearTimeout(attemptTimer);
897
+ operationSignal.removeEventListener("abort", detach);
898
+ for (const source of sourceSignals) {
899
+ source.removeEventListener("abort", onSourceAbort);
900
+ }
901
+ if (attemptState.inFlight === operation) {
902
+ this._remoteFullRearmsInFlight.delete(attemptState);
903
+ attemptState.inFlight = undefined;
904
+ attemptState.controller = undefined;
707
905
  }
708
906
  };
709
- operation = Promise.resolve()
710
- .then(() => {
907
+ const raceWithAttemptSignal = <T>(promise: Promise<T>): Promise<T> =>
908
+ new Promise<T>((resolve, reject) => {
909
+ const onAbort = () => {
910
+ operationSignal.removeEventListener("abort", onAbort);
911
+ reject(operationSignal.reason);
912
+ };
913
+ operationSignal.addEventListener("abort", onAbort, { once: true });
711
914
  if (operationSignal.aborted) {
712
- throw operationSignal.reason;
915
+ onAbort();
916
+ return;
713
917
  }
714
- return this.deps.refreshLocalCapability({
715
- peerHash: properties.peerHash,
716
- target: context.target,
717
- peerSession: properties.peerSession,
718
- receiveEpoch: properties.receiveEpoch,
719
- signal: operationSignal,
720
- requestRemoteFullRearm: true,
721
- });
722
- })
918
+ promise.then(
919
+ (value) => {
920
+ operationSignal.removeEventListener("abort", onAbort);
921
+ resolve(value);
922
+ },
923
+ (error) => {
924
+ operationSignal.removeEventListener("abort", onAbort);
925
+ reject(error);
926
+ },
927
+ );
928
+ });
929
+ operation = raceWithAttemptSignal(refresh)
723
930
  .then(() => undefined)
724
931
  .catch((error) => {
725
932
  if (
@@ -735,18 +942,189 @@ export class ReplicationInfoV2ReceiveCoordinator {
735
942
  }
736
943
  })
737
944
  .finally(() => {
738
- operationSignal.removeEventListener("abort", detach);
739
945
  detach();
740
946
  });
741
- rearm.inFlight = operation;
947
+ attemptState.inFlight = operation;
742
948
  operationSignal.addEventListener("abort", detach, { once: true });
743
- if (operationSignal.aborted) {
744
- detach();
949
+ for (const source of sourceSignals) {
950
+ if (source.aborted) {
951
+ attemptController.abort(source.reason);
952
+ break;
953
+ }
745
954
  }
746
955
  void operation;
747
956
  return true;
748
957
  }
749
958
 
959
+ /**
960
+ * Bounded, read-only state for persisted-readiness troubleshooting. This
961
+ * intentionally omits transport sessions, challenges, payloads, and maps.
962
+ */
963
+ diagnosePeer(properties: {
964
+ peerHash: string;
965
+ peerSession?: object;
966
+ receiveEpoch?: object | null;
967
+ senderTransportSession?: bigint;
968
+ }): ReplicationInfoV2ReceiveDiagnostic {
969
+ const state = this._receiveStates.get(properties.peerHash);
970
+ const advertisement = this._localCapabilityAdvertisementsByPeer.get(
971
+ properties.peerHash,
972
+ );
973
+ const session =
974
+ properties.peerSession ??
975
+ state?.peerSession ??
976
+ advertisement?.peerSession;
977
+ const ready = session
978
+ ? this._localCapabilityReadyBySession.get(session)
979
+ : undefined;
980
+ let capabilityAdvertisement: ReplicationInfoV2ReceiveDiagnostic["capabilityAdvertisement"] =
981
+ "absent";
982
+ if (ready && ready.peerHash === properties.peerHash) {
983
+ capabilityAdvertisement =
984
+ session !== undefined &&
985
+ this.deps.isPeerSessionCurrent(properties.peerHash, session) &&
986
+ this.deps.isReceiveEpochCurrent(
987
+ properties.peerHash,
988
+ ready.receiveEpoch,
989
+ ) &&
990
+ ready.receiverTransportSession ===
991
+ this.deps.getReceiverTransportSession() &&
992
+ (properties.receiveEpoch === undefined ||
993
+ ready.receiveEpoch === properties.receiveEpoch)
994
+ ? "ready"
995
+ : "stale";
996
+ } else if (advertisement) {
997
+ capabilityAdvertisement =
998
+ advertisement.peerHash === properties.peerHash &&
999
+ advertisement.peerSession === session &&
1000
+ (properties.receiveEpoch === undefined ||
1001
+ advertisement.receiveEpoch === properties.receiveEpoch) &&
1002
+ this.isLocalCapabilityAdvertisementGenerationCurrent(advertisement)
1003
+ ? "advertising"
1004
+ : "stale";
1005
+ }
1006
+
1007
+ const rearm = session
1008
+ ? this._remoteFullRearmBySession.get(session)
1009
+ : undefined;
1010
+ const now = this.now();
1011
+ const rearmLimited =
1012
+ (rearm?.outstandingAttempts ?? 0) >=
1013
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION ||
1014
+ this._remoteFullRearmOutstanding.size >=
1015
+ this.maxRemoteFullRearmOutstandingGlobal;
1016
+ const rearmStatus: ReplicationInfoV2ReceiveDiagnostic["remoteFullRearm"] =
1017
+ rearm?.inFlight
1018
+ ? "in-flight"
1019
+ : rearmLimited
1020
+ ? "limited"
1021
+ : rearm && now < rearm.nextAttemptAt
1022
+ ? "cooldown"
1023
+ : "idle";
1024
+ const common = {
1025
+ capabilityAdvertisementAttempts: Math.min(
1026
+ MAX_BACKOFF_EXPONENT + 1,
1027
+ Math.max(0, advertisement?.attempts ?? 0),
1028
+ ),
1029
+ capabilityAdvertisement,
1030
+ remoteFullRearm: rearmStatus,
1031
+ remoteFullRearmAttempts: Math.min(
1032
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
1033
+ Math.max(0, rearm?.attemptsStarted ?? 0),
1034
+ ),
1035
+ remoteFullRearmOutstanding: Math.min(
1036
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION,
1037
+ Math.max(0, rearm?.outstandingAttempts ?? 0),
1038
+ ),
1039
+ remoteFullRearmGlobalOutstanding: Math.min(
1040
+ this.maxRemoteFullRearmOutstandingGlobal,
1041
+ this._remoteFullRearmOutstanding.size,
1042
+ ),
1043
+ ...(rearm && now < rearm.nextAttemptAt
1044
+ ? {
1045
+ remoteFullRearmCooldownRemainingMs: Math.min(
1046
+ MAX_TIMER_MS,
1047
+ Math.max(0, rearm.nextAttemptAt - now),
1048
+ ),
1049
+ }
1050
+ : {}),
1051
+ };
1052
+ if (!state) {
1053
+ return Object.freeze({
1054
+ state: "absent" as const,
1055
+ reason: "state-absent" as const,
1056
+ ...common,
1057
+ });
1058
+ }
1059
+
1060
+ let status: "current" | "stale" = "current";
1061
+ let reason: ReplicationInfoV2ReceiveDiagnostic["reason"];
1062
+ if (
1063
+ properties.peerSession !== undefined &&
1064
+ state.peerSession !== properties.peerSession
1065
+ ) {
1066
+ status = "stale";
1067
+ reason = "peer-session-mismatch";
1068
+ } else if (
1069
+ properties.receiveEpoch !== undefined &&
1070
+ state.receiveEpoch !== properties.receiveEpoch
1071
+ ) {
1072
+ status = "stale";
1073
+ reason = "receive-epoch-mismatch";
1074
+ } else if (
1075
+ properties.senderTransportSession !== undefined &&
1076
+ state.senderTransportSession !== properties.senderTransportSession
1077
+ ) {
1078
+ status = "stale";
1079
+ reason = "transport-session-mismatch";
1080
+ } else if (!this.isStateCurrent(state)) {
1081
+ status = "stale";
1082
+ reason = "state-not-current";
1083
+ } else if (
1084
+ state.reservedAdmission !== undefined ||
1085
+ this._reservedAdmissionsByPeer.has(properties.peerHash)
1086
+ ) {
1087
+ reason = "admission-pending";
1088
+ } else if (state.phase === "active") {
1089
+ reason = "active";
1090
+ } else if (state.requestParked) {
1091
+ reason = "request-parked";
1092
+ } else if (state.requestInFlight !== undefined) {
1093
+ reason = "request-in-flight";
1094
+ } else if (state.requestTimer !== undefined) {
1095
+ reason = "request-scheduled";
1096
+ } else {
1097
+ reason = state.phase;
1098
+ }
1099
+ const requestState: NonNullable<
1100
+ ReplicationInfoV2ReceiveDiagnostic["requestState"]
1101
+ > =
1102
+ state.reservedAdmission !== undefined ||
1103
+ this._reservedAdmissionsByPeer.has(properties.peerHash)
1104
+ ? "admission-pending"
1105
+ : state.requestParked
1106
+ ? "parked"
1107
+ : state.requestInFlight !== undefined
1108
+ ? "in-flight"
1109
+ : state.requestTimer !== undefined
1110
+ ? "scheduled"
1111
+ : "idle";
1112
+ return Object.freeze({
1113
+ state: status,
1114
+ reason,
1115
+ phase: state.phase,
1116
+ requestState,
1117
+ ...(state.lastSequence === undefined
1118
+ ? {}
1119
+ : { lastAppliedSequence: state.lastSequence.toString() }),
1120
+ requestAttempts: Math.min(
1121
+ this.requestMaxAttempts,
1122
+ Math.max(0, state.requestAttempts),
1123
+ ),
1124
+ ...common,
1125
+ });
1126
+ }
1127
+
750
1128
  private promoteLocalCapabilityAdvertisement(
751
1129
  state: ReplicationInfoV2LocalCapabilityAdvertisement,
752
1130
  ): boolean {