@peerbit/shared-log 13.2.33 → 13.2.35

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,7 @@ const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
25
25
  const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
26
26
  const DEFAULT_LEGACY_FALLBACK_DELAY_MS = 5_000;
27
27
  const MAX_U64 = (1n << 64n) - 1n;
28
+ const MAX_BACKOFF_EXPONENT = 20;
28
29
 
29
30
  const bytesEqual = (left: Uint8Array, right: Uint8Array): boolean => {
30
31
  if (left.byteLength !== right.byteLength) {
@@ -64,6 +65,7 @@ export type ReplicationInfoV2ReceivePhase =
64
65
 
65
66
  type LocalCapabilityReady = {
66
67
  peerHash: string;
68
+ receiveEpoch: object | null;
67
69
  receiverTransportSession: bigint;
68
70
  /**
69
71
  * The local capability envelope and the later RequestV2 envelope use the
@@ -71,6 +73,45 @@ type LocalCapabilityReady = {
71
73
  * be strictly newer, so never construct it in this captured millisecond.
72
74
  */
73
75
  requestNotBeforeMs: number;
76
+ advertisement?: ReplicationInfoV2LocalCapabilityAdvertisement;
77
+ };
78
+
79
+ type LocalCapabilityReadyProperties = {
80
+ peerHash: string;
81
+ peerSession: object;
82
+ receiveEpoch: object | null;
83
+ receiverTransportSession: bigint;
84
+ requestNotBeforeMs: number;
85
+ };
86
+
87
+ export type ReplicationInfoV2LocalCapabilityAdvertisementHandle = {
88
+ firstAttempt: Promise<void>;
89
+ releaseLegacyBarrier(): void;
90
+ };
91
+
92
+ export type ReplicationInfoV2LocalCapabilityContext = {
93
+ peerHash: string;
94
+ target: PublicSignKey;
95
+ lifecycleSignal: AbortSignal;
96
+ legacyBarrierReleased: boolean;
97
+ };
98
+
99
+ export type ReplicationInfoV2LocalCapabilityAdvertisement = {
100
+ peerHash: string;
101
+ target: PublicSignKey;
102
+ peerSession: object;
103
+ receiveEpoch: object | null;
104
+ lifecycleSignal: AbortSignal;
105
+ onLifecycleAbort: () => void;
106
+ controller: AbortController;
107
+ context: ReplicationInfoV2LocalCapabilityContext;
108
+ attempts: number;
109
+ ready: boolean;
110
+ acknowledgedReady?: LocalCapabilityReady;
111
+ receiverTransportSession?: bigint;
112
+ timer?: ReturnType<typeof setTimeout>;
113
+ inFlight?: Promise<void>;
114
+ firstAttempt?: Promise<void>;
74
115
  };
75
116
 
76
117
  export type ReplicationInfoV2LocalCapabilityRefresh = {
@@ -131,6 +172,11 @@ export type ReplicationInfoV2ReceiveDeps = {
131
172
  getSelfKey: () => PublicSignKey;
132
173
  getReceiverTransportSession: () => bigint;
133
174
  isClosed: () => boolean;
175
+ isPeerSessionCurrent: (peerHash: string, peerSession: object) => boolean;
176
+ isReceiveEpochCurrent: (
177
+ peerHash: string,
178
+ receiveEpoch: object | null,
179
+ ) => boolean;
134
180
  isPeerStateCurrent: (
135
181
  peerHash: string,
136
182
  peerSession: object,
@@ -153,6 +199,7 @@ export type ReplicationInfoV2ReceiveDeps = {
153
199
  signal: AbortSignal;
154
200
  }) => Promise<ReplicationInfoV2LocalCapabilityRefresh | undefined>;
155
201
  onRequestError?: (error: unknown) => void;
202
+ onLocalCapabilityError?: (error: unknown) => void;
156
203
  now?: () => number;
157
204
  requestRetryMs?: number;
158
205
  maxRequestRetryMs?: number;
@@ -169,6 +216,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
169
216
  _receiveStates!: Map<string, ReplicationInfoV2ReceiveState>;
170
217
  _cutoverPeerSessions!: WeakSet<object>;
171
218
  _localCapabilityReadyBySession!: WeakMap<object, LocalCapabilityReady>;
219
+ _localCapabilityContextBySession!: WeakMap<
220
+ object,
221
+ ReplicationInfoV2LocalCapabilityContext
222
+ >;
223
+ _localCapabilityAdvertisementsByPeer!: Map<
224
+ string,
225
+ ReplicationInfoV2LocalCapabilityAdvertisement
226
+ >;
172
227
  _reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
173
228
 
174
229
  private readonly now: () => number;
@@ -198,6 +253,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
198
253
  this._receiveStates = new Map();
199
254
  this._cutoverPeerSessions = new WeakSet();
200
255
  this._localCapabilityReadyBySession = new WeakMap();
256
+ this._localCapabilityContextBySession = new WeakMap();
257
+ this._localCapabilityAdvertisementsByPeer = new Map();
201
258
  this._reservedAdmissionsByPeer = new Map();
202
259
  }
203
260
 
@@ -206,27 +263,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
206
263
  this._receiveStates = new Map();
207
264
  this._cutoverPeerSessions = new WeakSet();
208
265
  this._localCapabilityReadyBySession = new WeakMap();
266
+ this._localCapabilityContextBySession = new WeakMap();
267
+ this._localCapabilityAdvertisementsByPeer = new Map();
209
268
  this._reservedAdmissionsByPeer = new Map();
210
269
  }
211
270
 
212
271
  clearForClose(): void {
272
+ for (const advertisement of [
273
+ ...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
274
+ ]) {
275
+ this.clearLocalCapabilityAdvertisement(advertisement);
276
+ }
277
+ this._localCapabilityAdvertisementsByPeer?.clear();
213
278
  for (const state of this._receiveStates?.values() ?? []) {
214
279
  this.clearState(state);
215
280
  }
216
281
  this._receiveStates?.clear();
217
282
  this._cutoverPeerSessions = new WeakSet();
218
283
  this._localCapabilityReadyBySession = new WeakMap();
284
+ this._localCapabilityContextBySession = new WeakMap();
219
285
  }
220
286
 
221
287
  clearPeer(peerHash: string, expectedSession?: object): void {
288
+ const advertisement =
289
+ this._localCapabilityAdvertisementsByPeer.get(peerHash);
290
+ if (
291
+ advertisement &&
292
+ (!expectedSession || advertisement.peerSession === expectedSession)
293
+ ) {
294
+ this.clearLocalCapabilityAdvertisement(advertisement);
295
+ }
222
296
  const state = this._receiveStates.get(peerHash);
223
297
  if (state && (!expectedSession || state.peerSession === expectedSession)) {
224
298
  this.clearState(state);
225
299
  this._localCapabilityReadyBySession.delete(state.peerSession);
300
+ this._localCapabilityContextBySession.delete(state.peerSession);
226
301
  this._cutoverPeerSessions.delete(state.peerSession);
227
302
  }
228
303
  if (expectedSession) {
229
304
  this._localCapabilityReadyBySession.delete(expectedSession);
305
+ this._localCapabilityContextBySession.delete(expectedSession);
230
306
  this._cutoverPeerSessions.delete(expectedSession);
231
307
  }
232
308
  }
@@ -259,14 +335,462 @@ export class ReplicationInfoV2ReceiveCoordinator {
259
335
  }
260
336
  }
261
337
 
262
- /** Record success of this session's ACKed local APPLY advertisement. */
263
- markLocalCapabilityReady(properties: {
338
+ private clearLocalCapabilityAdvertisement(
339
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
340
+ options?: { preserveContext?: boolean },
341
+ ): void {
342
+ if (state.timer) {
343
+ clearTimeout(state.timer);
344
+ state.timer = undefined;
345
+ }
346
+ state.lifecycleSignal.removeEventListener("abort", state.onLifecycleAbort);
347
+ state.controller.abort();
348
+ const wasMapped =
349
+ this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state;
350
+ if (wasMapped) {
351
+ this._localCapabilityAdvertisementsByPeer.delete(state.peerHash);
352
+ }
353
+ const ready = this._localCapabilityReadyBySession.get(state.peerSession);
354
+ if (ready?.advertisement === state) {
355
+ this._localCapabilityReadyBySession.delete(state.peerSession);
356
+ }
357
+ if (
358
+ wasMapped &&
359
+ !options?.preserveContext &&
360
+ this._localCapabilityContextBySession.get(state.peerSession) ===
361
+ state.context
362
+ ) {
363
+ this._localCapabilityContextBySession.delete(state.peerSession);
364
+ }
365
+ }
366
+
367
+ private isLocalCapabilityAdvertisementOwnerCurrent(
368
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
369
+ ): boolean {
370
+ return (
371
+ this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state &&
372
+ this._localCapabilityContextBySession.get(state.peerSession) ===
373
+ state.context &&
374
+ state.context.peerHash === state.peerHash &&
375
+ state.context.lifecycleSignal === state.lifecycleSignal &&
376
+ state.context.target.equals(state.target) &&
377
+ !state.controller.signal.aborted &&
378
+ !state.lifecycleSignal.aborted &&
379
+ !this.deps.isClosed() &&
380
+ this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
381
+ (state.receiverTransportSession === undefined ||
382
+ this.deps.getReceiverTransportSession() ===
383
+ state.receiverTransportSession)
384
+ );
385
+ }
386
+
387
+ private isLocalCapabilityAdvertisementGenerationCurrent(
388
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
389
+ ): boolean {
390
+ return (
391
+ this.isLocalCapabilityAdvertisementOwnerCurrent(state) &&
392
+ this.deps.isReceiveEpochCurrent(state.peerHash, state.receiveEpoch)
393
+ );
394
+ }
395
+
396
+ private isLocalCapabilityAdvertisementReadyOpen(
397
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
398
+ ): boolean {
399
+ return (
400
+ this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
401
+ this.deps.isPeerStateCurrent(
402
+ state.peerHash,
403
+ state.peerSession,
404
+ state.receiveEpoch,
405
+ )
406
+ );
407
+ }
408
+
409
+ private localCapabilityRetryDelay(
410
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
411
+ ): number {
412
+ const exponent = Math.max(0, state.attempts - 1);
413
+ return Math.min(
414
+ this.maxRequestRetryMs,
415
+ this.requestRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT),
416
+ );
417
+ }
418
+
419
+ private armLocalCapabilityAdvertisement(
420
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
421
+ ): void {
422
+ if (state.timer || state.inFlight || state.ready) {
423
+ return;
424
+ }
425
+ if (state.acknowledgedReady && !state.context.legacyBarrierReleased) {
426
+ return;
427
+ }
428
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
429
+ this.clearLocalCapabilityAdvertisement(state);
430
+ return;
431
+ }
432
+ if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
433
+ return;
434
+ }
435
+ state.timer = setTimeout(() => {
436
+ state.timer = undefined;
437
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
438
+ this.clearLocalCapabilityAdvertisement(state);
439
+ return;
440
+ }
441
+ if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
442
+ return;
443
+ }
444
+ if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
445
+ this.armLocalCapabilityAdvertisement(state);
446
+ return;
447
+ }
448
+ void this.runLocalCapabilityAdvertisement(state);
449
+ }, this.localCapabilityRetryDelay(state));
450
+ state.timer.unref?.();
451
+ }
452
+
453
+ private async runLocalCapabilityAdvertisement(
454
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
455
+ ): Promise<void> {
456
+ if (state.inFlight) {
457
+ await state.inFlight;
458
+ return;
459
+ }
460
+ if (state.ready) {
461
+ return;
462
+ }
463
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
464
+ this.clearLocalCapabilityAdvertisement(state);
465
+ return;
466
+ }
467
+ if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
468
+ return;
469
+ }
470
+ if (state.acknowledgedReady) {
471
+ if (state.context.legacyBarrierReleased) {
472
+ this.promoteLocalCapabilityAdvertisement(state);
473
+ }
474
+ return;
475
+ }
476
+ if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
477
+ this.armLocalCapabilityAdvertisement(state);
478
+ return;
479
+ }
480
+ state.attempts = Math.min(state.attempts + 1, MAX_BACKOFF_EXPONENT + 1);
481
+ let operation: Promise<void>;
482
+ operation = (async () => {
483
+ const refreshed = await this.deps.refreshLocalCapability({
484
+ peerHash: state.peerHash,
485
+ target: state.target,
486
+ peerSession: state.peerSession,
487
+ receiveEpoch: state.receiveEpoch,
488
+ signal: AbortSignal.any([
489
+ state.controller.signal,
490
+ state.lifecycleSignal,
491
+ ]),
492
+ });
493
+ if (
494
+ !refreshed ||
495
+ !this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
496
+ this.deps.getReceiverTransportSession() !==
497
+ refreshed.receiverTransportSession
498
+ ) {
499
+ return;
500
+ }
501
+ state.receiverTransportSession = refreshed.receiverTransportSession;
502
+ state.acknowledgedReady = {
503
+ peerHash: state.peerHash,
504
+ receiveEpoch: state.receiveEpoch,
505
+ receiverTransportSession: refreshed.receiverTransportSession,
506
+ requestNotBeforeMs: refreshed.requestNotBeforeMs,
507
+ advertisement: state,
508
+ };
509
+ state.attempts = 0;
510
+ this.promoteLocalCapabilityAdvertisement(state);
511
+ })()
512
+ .catch((error) => {
513
+ if (
514
+ !state.controller.signal.aborted &&
515
+ !state.lifecycleSignal.aborted &&
516
+ !this.deps.isClosed()
517
+ ) {
518
+ this.deps.onLocalCapabilityError?.(error);
519
+ }
520
+ })
521
+ .finally(() => {
522
+ if (state.inFlight === operation) {
523
+ state.inFlight = undefined;
524
+ }
525
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
526
+ this.clearLocalCapabilityAdvertisement(state);
527
+ } else if (
528
+ this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
529
+ !state.ready
530
+ ) {
531
+ if (!state.acknowledgedReady || state.context.legacyBarrierReleased) {
532
+ this.armLocalCapabilityAdvertisement(state);
533
+ }
534
+ }
535
+ });
536
+ state.inFlight = operation;
537
+ await operation;
538
+ }
539
+
540
+ /**
541
+ * Start local authenticated-apply advertisement independently of the legacy
542
+ * join path. ACK and legacy publication are a two-phase barrier: the first
543
+ * attempt always settles independently, while readiness is promoted only
544
+ * after the host releases the legacy barrier. Failed attempts leave one
545
+ * exact-session worker retrying with capped exponential backoff.
546
+ */
547
+ advertiseLocalCapability(properties: {
548
+ target: PublicSignKey;
549
+ peerSession: object;
550
+ receiveEpoch: object | null;
551
+ signal: AbortSignal;
552
+ }): ReplicationInfoV2LocalCapabilityAdvertisementHandle {
553
+ const peerHash = properties.target.hashcode();
554
+ if (
555
+ properties.signal.aborted ||
556
+ this.deps.isClosed() ||
557
+ !this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
558
+ !this.deps.isReceiveEpochCurrent(peerHash, properties.receiveEpoch)
559
+ ) {
560
+ return {
561
+ firstAttempt: Promise.resolve(),
562
+ releaseLegacyBarrier: () => {},
563
+ };
564
+ }
565
+ let context = this._localCapabilityContextBySession.get(
566
+ properties.peerSession,
567
+ );
568
+ if (
569
+ context &&
570
+ (context.peerHash !== peerHash ||
571
+ !context.target.equals(properties.target) ||
572
+ context.lifecycleSignal !== properties.signal)
573
+ ) {
574
+ return {
575
+ firstAttempt: Promise.resolve(),
576
+ releaseLegacyBarrier: () => {},
577
+ };
578
+ }
579
+ if (!context) {
580
+ context = {
581
+ peerHash,
582
+ target: properties.target,
583
+ lifecycleSignal: properties.signal,
584
+ legacyBarrierReleased: false,
585
+ };
586
+ this._localCapabilityContextBySession.set(
587
+ properties.peerSession,
588
+ context,
589
+ );
590
+ }
591
+ let state = this._localCapabilityAdvertisementsByPeer.get(peerHash);
592
+ if (
593
+ state &&
594
+ (state.peerSession !== properties.peerSession ||
595
+ state.context !== context ||
596
+ state.lifecycleSignal !== properties.signal ||
597
+ !state.target.equals(properties.target))
598
+ ) {
599
+ this.clearLocalCapabilityAdvertisement(state);
600
+ state = undefined;
601
+ }
602
+ if (state && state.receiveEpoch !== properties.receiveEpoch) {
603
+ this.clearLocalCapabilityAdvertisement(state, { preserveContext: true });
604
+ state = undefined;
605
+ }
606
+ if (state && !this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
607
+ this.clearLocalCapabilityAdvertisement(state);
608
+ state = undefined;
609
+ }
610
+ if (
611
+ this._localCapabilityContextBySession.get(properties.peerSession) !==
612
+ context
613
+ ) {
614
+ return {
615
+ firstAttempt: Promise.resolve(),
616
+ releaseLegacyBarrier: () => {},
617
+ };
618
+ }
619
+ if (!state) {
620
+ const controller = new AbortController();
621
+ const advertisement: ReplicationInfoV2LocalCapabilityAdvertisement = {
622
+ peerHash,
623
+ target: properties.target,
624
+ peerSession: properties.peerSession,
625
+ receiveEpoch: properties.receiveEpoch,
626
+ lifecycleSignal: properties.signal,
627
+ onLifecycleAbort: () => {},
628
+ controller,
629
+ context,
630
+ attempts: 0,
631
+ ready: false,
632
+ receiverTransportSession: this.deps.getReceiverTransportSession(),
633
+ };
634
+ advertisement.onLifecycleAbort = () =>
635
+ this.clearLocalCapabilityAdvertisement(advertisement);
636
+ properties.signal.addEventListener(
637
+ "abort",
638
+ advertisement.onLifecycleAbort,
639
+ {
640
+ once: true,
641
+ },
642
+ );
643
+ this._localCapabilityAdvertisementsByPeer.set(peerHash, advertisement);
644
+ state = advertisement;
645
+ }
646
+ const firstAttempt =
647
+ state.firstAttempt ??
648
+ (state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
649
+ const advertisement = state;
650
+ return {
651
+ firstAttempt,
652
+ releaseLegacyBarrier: () =>
653
+ this.releaseLocalCapabilityLegacyBarrier(
654
+ advertisement.peerSession,
655
+ context,
656
+ ),
657
+ };
658
+ }
659
+
660
+ private releaseLocalCapabilityLegacyBarrier(
661
+ peerSession: object,
662
+ context: ReplicationInfoV2LocalCapabilityContext,
663
+ ): void {
664
+ if (context.legacyBarrierReleased) {
665
+ return;
666
+ }
667
+ if (
668
+ this._localCapabilityContextBySession.get(peerSession) !== context ||
669
+ context.lifecycleSignal.aborted ||
670
+ this.deps.isClosed() ||
671
+ !this.deps.isPeerSessionCurrent(context.peerHash, peerSession)
672
+ ) {
673
+ return;
674
+ }
675
+ context.legacyBarrierReleased = true;
676
+ const state = this._localCapabilityAdvertisementsByPeer.get(
677
+ context.peerHash,
678
+ );
679
+ if (state?.peerSession === peerSession && state.context === context) {
680
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
681
+ this.clearLocalCapabilityAdvertisement(state);
682
+ return;
683
+ }
684
+ this.promoteLocalCapabilityAdvertisement(state);
685
+ }
686
+ }
687
+
688
+ private promoteLocalCapabilityAdvertisement(
689
+ state: ReplicationInfoV2LocalCapabilityAdvertisement,
690
+ ): boolean {
691
+ const ready = state.acknowledgedReady;
692
+ if (
693
+ state.ready ||
694
+ !state.context.legacyBarrierReleased ||
695
+ !ready ||
696
+ ready.receiveEpoch !== state.receiveEpoch ||
697
+ ready.advertisement !== state ||
698
+ !this.isLocalCapabilityAdvertisementOwnerCurrent(state) ||
699
+ !this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
700
+ this.deps.getReceiverTransportSession() !== ready.receiverTransportSession
701
+ ) {
702
+ return state.ready;
703
+ }
704
+ if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
705
+ this.armLocalCapabilityAdvertisement(state);
706
+ return false;
707
+ }
708
+ if (
709
+ !this.recordLocalCapabilityReady(
710
+ {
711
+ peerHash: state.peerHash,
712
+ peerSession: state.peerSession,
713
+ receiveEpoch: state.receiveEpoch,
714
+ receiverTransportSession: ready.receiverTransportSession,
715
+ requestNotBeforeMs: ready.requestNotBeforeMs,
716
+ },
717
+ state,
718
+ )
719
+ ) {
720
+ return false;
721
+ }
722
+ state.ready = true;
723
+ return true;
724
+ }
725
+
726
+ /**
727
+ * Re-advertise one exact current recovery epoch from the stable membership
728
+ * context captured during opening. The opening handle owns barrier release;
729
+ * recovery never bypasses a legacy snapshot or role publication still in
730
+ * progress.
731
+ */
732
+ reAdvertiseLocalCapabilityForRecovery(properties: {
264
733
  peerHash: string;
265
734
  peerSession: object;
266
735
  receiveEpoch: object | null;
267
- receiverTransportSession: bigint;
268
- requestNotBeforeMs: number;
269
736
  }): boolean {
737
+ const context = this._localCapabilityContextBySession.get(
738
+ properties.peerSession,
739
+ );
740
+ if (
741
+ !context ||
742
+ context.peerHash !== properties.peerHash ||
743
+ context.lifecycleSignal.aborted ||
744
+ this.deps.isClosed() ||
745
+ !this.deps.isPeerSessionCurrent(
746
+ properties.peerHash,
747
+ properties.peerSession,
748
+ ) ||
749
+ !this.deps.isReceiveEpochCurrent(
750
+ properties.peerHash,
751
+ properties.receiveEpoch,
752
+ )
753
+ ) {
754
+ return false;
755
+ }
756
+ const state = this._receiveStates.get(properties.peerHash);
757
+ if (
758
+ state?.peerSession === properties.peerSession &&
759
+ state.receiveEpoch === properties.receiveEpoch &&
760
+ state.receiverBinding !== undefined
761
+ ) {
762
+ return false;
763
+ }
764
+ const ready = this._localCapabilityReadyBySession.get(
765
+ properties.peerSession,
766
+ );
767
+ if (
768
+ ready?.peerHash === properties.peerHash &&
769
+ ready.receiveEpoch === properties.receiveEpoch &&
770
+ ready.receiverTransportSession === this.deps.getReceiverTransportSession()
771
+ ) {
772
+ return false;
773
+ }
774
+ this.advertiseLocalCapability({
775
+ target: context.target,
776
+ peerSession: properties.peerSession,
777
+ receiveEpoch: properties.receiveEpoch,
778
+ signal: context.lifecycleSignal,
779
+ });
780
+ return true;
781
+ }
782
+
783
+ /** Record success of this session's ACKed local APPLY advertisement. */
784
+ markLocalCapabilityReady(
785
+ properties: LocalCapabilityReadyProperties,
786
+ ): boolean {
787
+ return this.recordLocalCapabilityReady(properties);
788
+ }
789
+
790
+ private recordLocalCapabilityReady(
791
+ properties: LocalCapabilityReadyProperties,
792
+ advertisement?: ReplicationInfoV2LocalCapabilityAdvertisement,
793
+ ): boolean {
270
794
  const { peerHash, peerSession, receiverTransportSession } = properties;
271
795
  const state = this._receiveStates.get(peerHash);
272
796
  if (
@@ -283,8 +807,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
283
807
 
284
808
  const ready: LocalCapabilityReady = {
285
809
  peerHash,
810
+ receiveEpoch: properties.receiveEpoch,
286
811
  receiverTransportSession,
287
812
  requestNotBeforeMs: properties.requestNotBeforeMs,
813
+ advertisement,
288
814
  };
289
815
  this._localCapabilityReadyBySession.set(peerSession, ready);
290
816
  if (
@@ -388,7 +914,11 @@ export class ReplicationInfoV2ReceiveCoordinator {
388
914
  });
389
915
  }
390
916
  const ready = this._localCapabilityReadyBySession.get(peerSession);
391
- if (ready?.peerHash === peerHash && state.receiverBinding === undefined) {
917
+ if (
918
+ ready?.peerHash === peerHash &&
919
+ ready.receiveEpoch === receiveEpoch &&
920
+ state.receiverBinding === undefined
921
+ ) {
392
922
  this.bindLocalCapability(state, ready);
393
923
  }
394
924
  if (state.phase !== "active") {
@@ -422,7 +952,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
422
952
  };
423
953
  this._receiveStates.set(peerHash, state);
424
954
  const ready = this._localCapabilityReadyBySession.get(peerSession);
425
- if (ready?.peerHash === peerHash) {
955
+ if (ready?.peerHash === peerHash && ready.receiveEpoch === receiveEpoch) {
426
956
  this.bindLocalCapability(state, ready);
427
957
  this.armRequest(state, 0);
428
958
  }
@@ -461,6 +991,44 @@ export class ReplicationInfoV2ReceiveCoordinator {
461
991
  return true;
462
992
  }
463
993
 
994
+ /**
995
+ * Resume only a request generation that exhausted its bounded retries.
996
+ * Wait/liveness callers may nudge recovery without invalidating an active,
997
+ * timed or in-flight request and without advancing the receive epoch.
998
+ */
999
+ resumeParkedRequest(properties: {
1000
+ peerHash: string;
1001
+ peerSession: object;
1002
+ receiveEpoch: object | null;
1003
+ }): boolean {
1004
+ const state = this._receiveStates.get(properties.peerHash);
1005
+ if (
1006
+ !state ||
1007
+ state.peerSession !== properties.peerSession ||
1008
+ state.receiveEpoch !== properties.receiveEpoch ||
1009
+ !this.deps.isPeerStateCurrent(
1010
+ properties.peerHash,
1011
+ properties.peerSession,
1012
+ properties.receiveEpoch,
1013
+ ) ||
1014
+ state.phase === "active" ||
1015
+ !state.requestParked ||
1016
+ state.requestTimer !== undefined ||
1017
+ state.requestInFlight !== undefined ||
1018
+ this._reservedAdmissionsByPeer.has(properties.peerHash) ||
1019
+ state.receiverBinding === undefined ||
1020
+ state.lastSequence === MAX_U64
1021
+ ) {
1022
+ return false;
1023
+ }
1024
+ state.requestAttempts = 0;
1025
+ state.requestsSinceCapabilityRefresh = 0;
1026
+ state.capabilityRefreshRequired = true;
1027
+ state.requestParked = false;
1028
+ this.armRequest(state, 0);
1029
+ return true;
1030
+ }
1031
+
464
1032
  private transitionToResync(
465
1033
  state: ReplicationInfoV2ReceiveState,
466
1034
  options?: { force?: boolean; refreshCapability?: boolean },
@@ -624,6 +1192,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
624
1192
  const { state } = admission;
625
1193
  this._reservedAdmissionsByPeer.set(peerHash, admission);
626
1194
  state.reservedAdmission = admission;
1195
+ if (state.requestTimer) {
1196
+ clearTimeout(state.requestTimer);
1197
+ state.requestTimer = undefined;
1198
+ }
1199
+ if (state.legacyFallbackTimer) {
1200
+ clearTimeout(state.legacyFallbackTimer);
1201
+ state.legacyFallbackTimer = undefined;
1202
+ }
627
1203
  return admission;
628
1204
  }
629
1205
 
@@ -644,6 +1220,30 @@ export class ReplicationInfoV2ReceiveCoordinator {
644
1220
  (currentState !== state && currentState.phase !== "active"))
645
1221
  ) {
646
1222
  this.transitionToResync(currentState, { force: true });
1223
+ } else if (
1224
+ currentState === state &&
1225
+ !admission.committed &&
1226
+ this.isStateCurrent(state) &&
1227
+ state.phase !== "active" &&
1228
+ state.requestTimer === undefined &&
1229
+ state.requestInFlight === undefined &&
1230
+ !state.requestParked
1231
+ ) {
1232
+ // Reserving a Full pauses this generation's retry timer. If its host
1233
+ // apply lane releases without committing, restore the bounded request
1234
+ // worker so the exact current generation cannot become stranded.
1235
+ this.armRequest(state, 0);
1236
+ }
1237
+ if (
1238
+ currentState === state &&
1239
+ this.isStateCurrent(state) &&
1240
+ state.phase === "active" &&
1241
+ state.legacyFallbackFingerprint !== undefined
1242
+ ) {
1243
+ // A reservation also pauses compat sidecar recovery. Matching commits
1244
+ // clear the evidence; an uncommitted or non-matching frame restores its
1245
+ // bounded fallback without invalidating work in the host apply lane.
1246
+ this.armLegacyFallback(state);
647
1247
  }
648
1248
  }
649
1249
 
@@ -751,6 +1351,19 @@ export class ReplicationInfoV2ReceiveCoordinator {
751
1351
  if (state.legacyFallbackTimer) {
752
1352
  return true;
753
1353
  }
1354
+ this.armLegacyFallback(state);
1355
+ return true;
1356
+ }
1357
+
1358
+ private armLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
1359
+ if (
1360
+ state.legacyFallbackTimer ||
1361
+ state.phase !== "active" ||
1362
+ state.legacyFallbackFingerprint === undefined ||
1363
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1364
+ ) {
1365
+ return;
1366
+ }
754
1367
  state.legacyFallbackTimer = setTimeout(() => {
755
1368
  state.legacyFallbackTimer = undefined;
756
1369
  if (this.isStateCurrent(state) && state.phase === "active") {
@@ -761,7 +1374,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
761
1374
  }
762
1375
  }, this.legacyFallbackDelayMs);
763
1376
  state.legacyFallbackTimer.unref?.();
764
- return true;
765
1377
  }
766
1378
 
767
1379
  private clearLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
@@ -888,8 +1500,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
888
1500
  this._receiveStates.get(state.peerHash) !== state ||
889
1501
  state.controller.signal.aborted ||
890
1502
  this.deps.isClosed() ||
891
- (this._reservedAdmissionsByPeer.has(state.peerHash) &&
892
- this._reservedAdmissionsByPeer.get(state.peerHash)?.state !== state) ||
1503
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
893
1504
  state.receiverBinding === undefined ||
894
1505
  state.phase === "active" ||
895
1506
  state.requestParked ||
@@ -919,6 +1530,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
919
1530
  private async refreshGrant(
920
1531
  state: ReplicationInfoV2ReceiveState,
921
1532
  ): Promise<boolean> {
1533
+ const version = state.version;
922
1534
  const refreshed = await this.deps.refreshLocalCapability({
923
1535
  peerHash: state.peerHash,
924
1536
  target: state.target,
@@ -928,6 +1540,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
928
1540
  });
929
1541
  if (
930
1542
  !refreshed ||
1543
+ state.version !== version ||
1544
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
931
1545
  !this.isStateCurrent(state) ||
932
1546
  this.deps.getReceiverTransportSession() !==
933
1547
  refreshed.receiverTransportSession
@@ -937,6 +1551,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
937
1551
 
938
1552
  const ready: LocalCapabilityReady = {
939
1553
  peerHash: state.peerHash,
1554
+ receiveEpoch: state.receiveEpoch,
940
1555
  receiverTransportSession: refreshed.receiverTransportSession,
941
1556
  requestNotBeforeMs: refreshed.requestNotBeforeMs,
942
1557
  };
@@ -958,7 +1573,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
958
1573
  if (state.requestInFlight) {
959
1574
  return;
960
1575
  }
961
- if (!this.isStateCurrent(state)) {
1576
+ if (
1577
+ !this.isStateCurrent(state) ||
1578
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1579
+ ) {
962
1580
  return;
963
1581
  }
964
1582
  if (
@@ -981,13 +1599,17 @@ export class ReplicationInfoV2ReceiveCoordinator {
981
1599
  return;
982
1600
  }
983
1601
  }
984
- if (!this.isStateCurrent(state)) {
1602
+ if (
1603
+ !this.isStateCurrent(state) ||
1604
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1605
+ ) {
985
1606
  return;
986
1607
  }
987
1608
  const ready = this._localCapabilityReadyBySession.get(state.peerSession);
988
1609
  if (
989
1610
  !ready ||
990
1611
  ready.peerHash !== state.peerHash ||
1612
+ ready.receiveEpoch !== state.receiveEpoch ||
991
1613
  ready.receiverTransportSession !== state.receiverTransportSession
992
1614
  ) {
993
1615
  return;
@@ -1026,6 +1648,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1026
1648
  if (
1027
1649
  this._receiveStates.get(state.peerHash) === state &&
1028
1650
  !state.controller.signal.aborted &&
1651
+ !this._reservedAdmissionsByPeer.has(state.peerHash) &&
1029
1652
  !state.requestTimer &&
1030
1653
  state.phase !== "active"
1031
1654
  ) {