@peerbit/shared-log 16.0.25 → 16.0.26

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.
package/src/index.ts CHANGED
@@ -4087,10 +4087,15 @@ export class SharedLog<
4087
4087
  : 0),
4088
4088
  }),
4089
4089
  {
4090
- mode: new AcknowledgeDelivery({
4091
- redundancy: 1,
4092
- to: [properties.target],
4093
- }),
4090
+ // The transient rearm is only a bounded recovery hint. The ensuing
4091
+ // authenticated Full/Applied exchange is the readiness proof, so do not
4092
+ // let an ACK-delivery promise park the recovery scheduler itself.
4093
+ mode: properties.requestRemoteFullRearm
4094
+ ? new SilentDelivery({ redundancy: 1, to: [properties.target] })
4095
+ : new AcknowledgeDelivery({
4096
+ redundancy: 1,
4097
+ to: [properties.target],
4098
+ }),
4094
4099
  priority: CONVERGENCE_MESSAGE_PRIORITY,
4095
4100
  signal: properties.signal,
4096
4101
  },
@@ -24381,6 +24386,11 @@ export class SharedLog<
24381
24386
  private nudgePersistedReceiptPeerReadiness(
24382
24387
  publicKey: PublicSignKey,
24383
24388
  signal: AbortSignal,
24389
+ rearmCurrentUnconfirmed?: {
24390
+ peerSession: PeerSession;
24391
+ receiveEpoch: object | null;
24392
+ capabilitySession: bigint;
24393
+ },
24384
24394
  ): void {
24385
24395
  if (this.closed || signal.aborted) return;
24386
24396
  const peerHash = publicKey.hashcode();
@@ -24416,11 +24426,20 @@ export class SharedLog<
24416
24426
  const receiptTarget = this.persistedReceiptPeerSession(peerHash);
24417
24427
  if (
24418
24428
  receiptTarget &&
24419
- !this._v2Send.hasCurrentStateForPeer({
24429
+ (!this._v2Send.hasCurrentStateForPeer({
24420
24430
  peerHash,
24421
24431
  peerSession: receiptTarget.peerSession,
24422
24432
  receiverTransportSession: receiptTarget.capabilitySession,
24423
- })
24433
+ }) ||
24434
+ (rearmCurrentUnconfirmed?.peerSession === receiptTarget.peerSession &&
24435
+ rearmCurrentUnconfirmed.receiveEpoch === receiptTarget.receiveEpoch &&
24436
+ rearmCurrentUnconfirmed.capabilitySession ===
24437
+ receiptTarget.capabilitySession &&
24438
+ !this._v2Send.isLatestConfirmedForPeer({
24439
+ peerHash,
24440
+ peerSession: receiptTarget.peerSession,
24441
+ receiverTransportSession: receiptTarget.capabilitySession,
24442
+ })))
24424
24443
  ) {
24425
24444
  this._v2Receive.reAdvertiseLocalCapabilityForRemoteFull({
24426
24445
  peerHash,
@@ -24563,7 +24582,9 @@ export class SharedLog<
24563
24582
  * Wait for a public key's current (or replacement) connection generation to
24564
24583
  * become persisted-receipt ready. Transition listeners are installed before
24565
24584
  * the first asynchronous inspection, and a bounded recovery tick repairs
24566
- * missed subscriber/capability wakes without retaining stale PeerSessions.
24585
+ * missed subscriber/capability wakes without retaining stale PeerSessions. A
24586
+ * current sender gets one normal confirmation interval before an exact-
24587
+ * generation watchdog requests a bounded Full rearm.
24567
24588
  * This waiter is advisory only; the following persisted delivery remains the
24568
24589
  * operation that proves the requested remote durability quorum.
24569
24590
  */
@@ -24621,6 +24642,14 @@ export class SharedLog<
24621
24642
  let rerun = false;
24622
24643
  let recoveryTimer: ReturnType<typeof setTimeout> | undefined;
24623
24644
  let confirmationController: AbortController | undefined;
24645
+ let confirmationRecoveryTarget:
24646
+ | {
24647
+ peerSession: PeerSession;
24648
+ receiveEpoch: object | null;
24649
+ capabilitySession: bigint;
24650
+ notBefore: number;
24651
+ }
24652
+ | undefined;
24624
24653
  let lastSnapshot: PersistedReceiptPeerReadiness | undefined;
24625
24654
  const createTimeoutError = () => {
24626
24655
  const suffix = lastSnapshot
@@ -24651,6 +24680,7 @@ export class SharedLog<
24651
24680
  new AbortError("Persisted-receipt readiness generation changed"),
24652
24681
  );
24653
24682
  confirmationController = undefined;
24683
+ confirmationRecoveryTarget = undefined;
24654
24684
  operationController.abort(
24655
24685
  new AbortError("Persisted-receipt readiness wait settled"),
24656
24686
  );
@@ -24706,7 +24736,14 @@ export class SharedLog<
24706
24736
  recoveryTimer = setTimeout(() => {
24707
24737
  recoveryTimer = undefined;
24708
24738
  if (!continueWait()) return;
24709
- this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24739
+ const recoveryTarget = confirmationRecoveryTarget;
24740
+ this.nudgePersistedReceiptPeerReadiness(
24741
+ key,
24742
+ operationSignal,
24743
+ recoveryTarget && Date.now() >= recoveryTarget.notBefore
24744
+ ? recoveryTarget
24745
+ : undefined,
24746
+ );
24710
24747
  if (checkInFlight) {
24711
24748
  // Confirmation can legitimately span several recovery intervals.
24712
24749
  // Keep repairing the exact current generation without aborting its
@@ -24719,6 +24756,34 @@ export class SharedLog<
24719
24756
  }, recoveryDelayMs);
24720
24757
  recoveryTimer.unref?.();
24721
24758
  };
24759
+ const prepareConfirmationRecovery = () => {
24760
+ const target = this.persistedReceiptPeerSession(peerHash);
24761
+ if (!target) {
24762
+ confirmationRecoveryTarget = undefined;
24763
+ return undefined;
24764
+ }
24765
+ const retained = confirmationRecoveryTarget;
24766
+ if (
24767
+ retained?.peerSession !== target.peerSession ||
24768
+ retained.receiveEpoch !== target.receiveEpoch ||
24769
+ retained.capabilitySession !== target.capabilitySession
24770
+ ) {
24771
+ confirmationRecoveryTarget = {
24772
+ peerSession: target.peerSession,
24773
+ receiveEpoch: target.receiveEpoch,
24774
+ capabilitySession: target.capabilitySession,
24775
+ notBefore: Date.now() + recoveryDelayMs,
24776
+ };
24777
+ }
24778
+ const recoveryTarget = confirmationRecoveryTarget!;
24779
+ this.nudgePersistedReceiptPeerReadiness(
24780
+ key,
24781
+ operationSignal,
24782
+ Date.now() >= recoveryTarget.notBefore ? recoveryTarget : undefined,
24783
+ );
24784
+ armRecoveryTick();
24785
+ return target;
24786
+ };
24722
24787
  const runCheck = async () => {
24723
24788
  checkScheduled = false;
24724
24789
  if (!continueWait()) return;
@@ -24735,8 +24800,9 @@ export class SharedLog<
24735
24800
  );
24736
24801
  lastSnapshot = snapshot;
24737
24802
  if (!continueWait()) return;
24738
- if (rerun) return;
24739
24803
  if (snapshot.status === "ready") {
24804
+ confirmationRecoveryTarget = undefined;
24805
+ if (rerun) return;
24740
24806
  // A wake observed while the asynchronous inspection was running may
24741
24807
  // already have invalidated this snapshot. Drain that coalesced wake
24742
24808
  // before publishing readiness.
@@ -24747,15 +24813,17 @@ export class SharedLog<
24747
24813
  snapshot.status === "pending" &&
24748
24814
  snapshot.reason === "replication-confirmation-pending"
24749
24815
  ) {
24750
- const target = this.persistedReceiptPeerSession(peerHash);
24816
+ // Retain one watchdog deadline for the exact generation across
24817
+ // coalesced inspection wakes. A sub-interval event stream must drive,
24818
+ // rather than indefinitely postpone, the mature recovery nudge.
24819
+ const target = prepareConfirmationRecovery();
24820
+ if (rerun) return;
24751
24821
  if (target) {
24752
- // A confirmation waiter cannot create an outbound V2 stream when
24753
- // that exact session state is missing. Nudge first, then keep the
24754
- // recovery tick alive while the single confirmation wait remains.
24755
- this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24756
- armRecoveryTick();
24757
24822
  const currentConfirmationController = new AbortController();
24758
24823
  confirmationController = currentConfirmationController;
24824
+ const currentConfirmationRecoveryTarget =
24825
+ confirmationRecoveryTarget;
24826
+ let confirmationSucceeded = false;
24759
24827
  try {
24760
24828
  await this._v2Send.confirmLatestForPeer(
24761
24829
  {
@@ -24771,6 +24839,7 @@ export class SharedLog<
24771
24839
  ]),
24772
24840
  },
24773
24841
  );
24842
+ confirmationSucceeded = true;
24774
24843
  } catch (error) {
24775
24844
  if (!continueWait()) return;
24776
24845
  if (!(error instanceof AbortError)) {
@@ -24781,6 +24850,12 @@ export class SharedLog<
24781
24850
  if (confirmationController === currentConfirmationController) {
24782
24851
  confirmationController = undefined;
24783
24852
  }
24853
+ if (
24854
+ confirmationSucceeded &&
24855
+ confirmationRecoveryTarget === currentConfirmationRecoveryTarget
24856
+ ) {
24857
+ confirmationRecoveryTarget = undefined;
24858
+ }
24784
24859
  }
24785
24860
  if (!continueWait()) return;
24786
24861
  snapshot = await this.inspectPersistedReceiptPeerReadiness(
@@ -24790,13 +24865,26 @@ export class SharedLog<
24790
24865
  );
24791
24866
  lastSnapshot = snapshot;
24792
24867
  if (!continueWait()) return;
24793
- if (rerun) return;
24794
24868
  if (snapshot.status === "ready") {
24869
+ confirmationRecoveryTarget = undefined;
24870
+ if (rerun) return;
24795
24871
  resolve(snapshot);
24796
24872
  return;
24797
24873
  }
24874
+ if (
24875
+ snapshot.status === "pending" &&
24876
+ snapshot.reason === "replication-confirmation-pending"
24877
+ ) {
24878
+ prepareConfirmationRecovery();
24879
+ } else {
24880
+ confirmationRecoveryTarget = undefined;
24881
+ }
24882
+ if (rerun) return;
24798
24883
  }
24884
+ } else {
24885
+ confirmationRecoveryTarget = undefined;
24799
24886
  }
24887
+ if (rerun) return;
24800
24888
  if (!continueWait()) return;
24801
24889
  this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24802
24890
  } catch (error) {
@@ -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,8 +118,13 @@ 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
+ outstandingAttempts: number;
117
126
  inFlight?: Promise<void>;
127
+ controller?: AbortController;
118
128
  };
119
129
 
120
130
  export type ReplicationInfoV2LocalCapabilityRefresh = {
@@ -196,6 +206,9 @@ export type ReplicationInfoV2ReceiveDeps = {
196
206
  requestRetryMs?: number;
197
207
  maxRequestRetryMs?: number;
198
208
  requestMaxAttempts?: number;
209
+ remoteFullRearmAttemptTimeoutMs?: number;
210
+ remoteFullRearmCooldownMs?: number;
211
+ maxRemoteFullRearmOutstandingGlobal?: number;
199
212
  };
200
213
 
201
214
  /**
@@ -224,12 +237,17 @@ export class ReplicationInfoV2ReceiveCoordinator {
224
237
  ReplicationInfoV2LocalCapabilityAdvertisement
225
238
  >;
226
239
  _remoteFullRearmBySession!: WeakMap<object, ReplicationInfoV2RemoteFullRearm>;
240
+ _remoteFullRearmsInFlight!: Set<ReplicationInfoV2RemoteFullRearm>;
241
+ _remoteFullRearmOutstanding!: Set<object>;
227
242
  _reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
228
243
 
229
244
  private readonly now: () => number;
230
245
  private readonly requestRetryMs: number;
231
246
  private readonly maxRequestRetryMs: number;
232
247
  private readonly requestMaxAttempts: number;
248
+ private readonly remoteFullRearmAttemptTimeoutMs: number;
249
+ private readonly remoteFullRearmCooldownMs: number;
250
+ private readonly maxRemoteFullRearmOutstandingGlobal: number;
233
251
 
234
252
  constructor(private readonly deps: ReplicationInfoV2ReceiveDeps) {
235
253
  this.now = deps.now ?? Date.now;
@@ -245,12 +263,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
245
263
  1,
246
264
  Math.floor(deps.requestMaxAttempts ?? DEFAULT_REQUEST_MAX_ATTEMPTS),
247
265
  );
266
+ this.remoteFullRearmAttemptTimeoutMs = Math.max(
267
+ 1,
268
+ Math.min(
269
+ MAX_TIMER_MS,
270
+ Math.floor(
271
+ deps.remoteFullRearmAttemptTimeoutMs ??
272
+ DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS,
273
+ ),
274
+ ),
275
+ );
276
+ this.remoteFullRearmCooldownMs = Math.max(
277
+ this.requestRetryMs,
278
+ Math.min(
279
+ MAX_TIMER_MS,
280
+ Math.floor(
281
+ deps.remoteFullRearmCooldownMs ??
282
+ DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS,
283
+ ),
284
+ ),
285
+ );
286
+ // The injected value is a test seam that may only tighten this lifetime
287
+ // resource bound, never widen it.
288
+ this.maxRemoteFullRearmOutstandingGlobal = Math.min(
289
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
290
+ Math.max(
291
+ 1,
292
+ Math.floor(
293
+ deps.maxRemoteFullRearmOutstandingGlobal ??
294
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
295
+ ),
296
+ ),
297
+ );
248
298
  this._receiveStates = new Map();
249
299
  this._cutoverPeerSessions = new WeakSet();
250
300
  this._localCapabilityReadyBySession = new WeakMap();
251
301
  this._localCapabilityContextBySession = new WeakMap();
252
302
  this._localCapabilityAdvertisementsByPeer = new Map();
253
303
  this._remoteFullRearmBySession = new WeakMap();
304
+ this._remoteFullRearmsInFlight = new Set();
305
+ this._remoteFullRearmOutstanding = new Set();
254
306
  this._reservedAdmissionsByPeer = new Map();
255
307
  }
256
308
 
@@ -262,10 +314,19 @@ export class ReplicationInfoV2ReceiveCoordinator {
262
314
  this._localCapabilityContextBySession = new WeakMap();
263
315
  this._localCapabilityAdvertisementsByPeer = new Map();
264
316
  this._remoteFullRearmBySession = new WeakMap();
317
+ this._remoteFullRearmsInFlight = new Set();
265
318
  this._reservedAdmissionsByPeer = new Map();
266
319
  }
267
320
 
268
321
  clearForClose(): void {
322
+ for (const rearm of this._remoteFullRearmsInFlight ?? []) {
323
+ rearm.controller?.abort(
324
+ new Error(
325
+ "Replication-info V2 receiver closed during remote Full rearm",
326
+ ),
327
+ );
328
+ }
329
+ this._remoteFullRearmsInFlight?.clear();
269
330
  for (const advertisement of [
270
331
  ...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
271
332
  ]) {
@@ -298,10 +359,28 @@ export class ReplicationInfoV2ReceiveCoordinator {
298
359
  this._localCapabilityContextBySession.delete(state.peerSession);
299
360
  this._cutoverPeerSessions.delete(state.peerSession);
300
361
  }
362
+ if (!expectedSession) {
363
+ for (const rearm of this._remoteFullRearmsInFlight) {
364
+ if (rearm.peerHash === peerHash) {
365
+ rearm.controller?.abort(
366
+ new Error("Replication-info V2 peer cleared during rearm"),
367
+ );
368
+ }
369
+ }
370
+ }
371
+ const rearmSession =
372
+ expectedSession ?? state?.peerSession ?? advertisement?.peerSession;
373
+ if (rearmSession) {
374
+ this._remoteFullRearmBySession
375
+ .get(rearmSession)
376
+ ?.controller?.abort(
377
+ new Error("Replication-info V2 peer session cleared during rearm"),
378
+ );
379
+ this._remoteFullRearmBySession.delete(rearmSession);
380
+ }
301
381
  if (expectedSession) {
302
382
  this._localCapabilityReadyBySession.delete(expectedSession);
303
383
  this._localCapabilityContextBySession.delete(expectedSession);
304
- this._remoteFullRearmBySession.delete(expectedSession);
305
384
  this._cutoverPeerSessions.delete(expectedSession);
306
385
  }
307
386
  }
@@ -643,9 +722,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
643
722
  * stream that was lost while both directions otherwise remained current.
644
723
  *
645
724
  * 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.
725
+ * attempt is coalesced per exact PeerSession/receive generation and bound to
726
+ * its own deadline as well as the caller's signal. A cooldown avoids rotating
727
+ * a healthy in-flight challenge on every recovery tick. The remote rotates
728
+ * only when its receive state is active; later hints during resync only nudge
729
+ * that same bounded request generation. At most two transports that disregard
730
+ * abort remain outstanding for one exact session (64 across this coordinator's
731
+ * lifetime); reaching either cap stops new hints while the persisted-readiness
732
+ * gate remains fail closed.
649
733
  */
650
734
  reAdvertiseLocalCapabilityForRemoteFull(properties: {
651
735
  peerHash: string;
@@ -679,47 +763,130 @@ export class ReplicationInfoV2ReceiveCoordinator {
679
763
  }
680
764
 
681
765
  const now = this.now();
766
+ const receiverTransportSession = ready.receiverTransportSession;
682
767
  let rearm = this._remoteFullRearmBySession.get(properties.peerSession);
768
+ if (
769
+ rearm &&
770
+ (rearm.peerHash !== properties.peerHash ||
771
+ rearm.receiveEpoch !== properties.receiveEpoch ||
772
+ rearm.receiverTransportSession !== receiverTransportSession)
773
+ ) {
774
+ rearm.controller?.abort(
775
+ new Error("Replication-info V2 remote Full rearm generation changed"),
776
+ );
777
+ rearm = undefined;
778
+ }
683
779
  if (rearm?.inFlight) {
684
780
  return true;
685
781
  }
782
+ if (rearm !== undefined && now < rearm.nextAttemptAt) {
783
+ return true;
784
+ }
686
785
  if (
687
- rearm !== undefined &&
688
- now - rearm.lastAttemptAt < this.requestRetryMs
786
+ (rearm?.outstandingAttempts ?? 0) >=
787
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION ||
788
+ this._remoteFullRearmOutstanding.size >=
789
+ this.maxRemoteFullRearmOutstandingGlobal
689
790
  ) {
690
791
  return true;
691
792
  }
692
793
  if (!rearm) {
693
- rearm = { lastAttemptAt: now };
794
+ rearm = {
795
+ peerHash: properties.peerHash,
796
+ receiveEpoch: properties.receiveEpoch,
797
+ receiverTransportSession,
798
+ nextAttemptAt: now + this.remoteFullRearmCooldownMs,
799
+ outstandingAttempts: 0,
800
+ };
694
801
  this._remoteFullRearmBySession.set(properties.peerSession, rearm);
695
802
  } else {
696
- rearm.lastAttemptAt = now;
697
- }
803
+ rearm.nextAttemptAt = now + this.remoteFullRearmCooldownMs;
804
+ }
805
+ const attemptState = rearm;
806
+ const outstandingReservation = {};
807
+ attemptState.outstandingAttempts++;
808
+ this._remoteFullRearmOutstanding.add(outstandingReservation);
809
+
810
+ const attemptController = new AbortController();
811
+ const operationSignal = attemptController.signal;
812
+ attemptState.controller = attemptController;
813
+ this._remoteFullRearmsInFlight.add(attemptState);
814
+ const sourceSignals = Array.from(
815
+ new Set([context.lifecycleSignal, properties.signal]),
816
+ );
817
+ const onSourceAbort = (event: Event) => {
818
+ const source = event.currentTarget as AbortSignal;
819
+ attemptController.abort(source.reason);
820
+ };
821
+ for (const source of sourceSignals) {
822
+ source.addEventListener("abort", onSourceAbort, { once: true });
823
+ }
824
+ const attemptTimer = setTimeout(
825
+ () =>
826
+ attemptController.abort(
827
+ new Error("Replication-info V2 remote Full rearm attempt timed out"),
828
+ ),
829
+ this.remoteFullRearmAttemptTimeoutMs,
830
+ );
831
+ attemptTimer.unref?.();
832
+ let releasedOutstanding = false;
833
+ const releaseOutstanding = () => {
834
+ if (releasedOutstanding) return;
835
+ releasedOutstanding = true;
836
+ this._remoteFullRearmOutstanding.delete(outstandingReservation);
837
+ attemptState.outstandingAttempts--;
838
+ };
839
+ const refresh = Promise.resolve().then(() => {
840
+ if (operationSignal.aborted) {
841
+ throw operationSignal.reason;
842
+ }
843
+ return this.deps.refreshLocalCapability({
844
+ peerHash: properties.peerHash,
845
+ target: context.target,
846
+ peerSession: properties.peerSession,
847
+ receiveEpoch: properties.receiveEpoch,
848
+ signal: operationSignal,
849
+ requestRemoteFullRearm: true,
850
+ });
851
+ });
852
+ void refresh.then(releaseOutstanding, releaseOutstanding);
698
853
 
699
- const operationSignal = AbortSignal.any([
700
- context.lifecycleSignal,
701
- properties.signal,
702
- ]);
703
854
  let operation: Promise<void>;
704
855
  const detach = () => {
705
- if (rearm?.inFlight === operation) {
706
- rearm.inFlight = undefined;
856
+ clearTimeout(attemptTimer);
857
+ operationSignal.removeEventListener("abort", detach);
858
+ for (const source of sourceSignals) {
859
+ source.removeEventListener("abort", onSourceAbort);
860
+ }
861
+ if (attemptState.inFlight === operation) {
862
+ this._remoteFullRearmsInFlight.delete(attemptState);
863
+ attemptState.inFlight = undefined;
864
+ attemptState.controller = undefined;
707
865
  }
708
866
  };
709
- operation = Promise.resolve()
710
- .then(() => {
867
+ const raceWithAttemptSignal = <T>(promise: Promise<T>): Promise<T> =>
868
+ new Promise<T>((resolve, reject) => {
869
+ const onAbort = () => {
870
+ operationSignal.removeEventListener("abort", onAbort);
871
+ reject(operationSignal.reason);
872
+ };
873
+ operationSignal.addEventListener("abort", onAbort, { once: true });
711
874
  if (operationSignal.aborted) {
712
- throw operationSignal.reason;
875
+ onAbort();
876
+ return;
713
877
  }
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
- })
878
+ promise.then(
879
+ (value) => {
880
+ operationSignal.removeEventListener("abort", onAbort);
881
+ resolve(value);
882
+ },
883
+ (error) => {
884
+ operationSignal.removeEventListener("abort", onAbort);
885
+ reject(error);
886
+ },
887
+ );
888
+ });
889
+ operation = raceWithAttemptSignal(refresh)
723
890
  .then(() => undefined)
724
891
  .catch((error) => {
725
892
  if (
@@ -735,13 +902,15 @@ export class ReplicationInfoV2ReceiveCoordinator {
735
902
  }
736
903
  })
737
904
  .finally(() => {
738
- operationSignal.removeEventListener("abort", detach);
739
905
  detach();
740
906
  });
741
- rearm.inFlight = operation;
907
+ attemptState.inFlight = operation;
742
908
  operationSignal.addEventListener("abort", detach, { once: true });
743
- if (operationSignal.aborted) {
744
- detach();
909
+ for (const source of sourceSignals) {
910
+ if (source.aborted) {
911
+ attemptController.abort(source.reason);
912
+ break;
913
+ }
745
914
  }
746
915
  void operation;
747
916
  return true;