@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.
@@ -10,6 +10,7 @@ const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
10
10
  const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
11
11
  const DEFAULT_LEGACY_FALLBACK_DELAY_MS = 5_000;
12
12
  const MAX_U64 = (1n << 64n) - 1n;
13
+ const MAX_BACKOFF_EXPONENT = 20;
13
14
  const bytesEqual = (left, right) => {
14
15
  if (left.byteLength !== right.byteLength) {
15
16
  return false;
@@ -41,6 +42,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
41
42
  _receiveStates;
42
43
  _cutoverPeerSessions;
43
44
  _localCapabilityReadyBySession;
45
+ _localCapabilityContextBySession;
46
+ _localCapabilityAdvertisementsByPeer;
44
47
  _reservedAdmissionsByPeer;
45
48
  now;
46
49
  requestRetryMs;
@@ -57,6 +60,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
57
60
  this._receiveStates = new Map();
58
61
  this._cutoverPeerSessions = new WeakSet();
59
62
  this._localCapabilityReadyBySession = new WeakMap();
63
+ this._localCapabilityContextBySession = new WeakMap();
64
+ this._localCapabilityAdvertisementsByPeer = new Map();
60
65
  this._reservedAdmissionsByPeer = new Map();
61
66
  }
62
67
  resetForOpen() {
@@ -64,25 +69,41 @@ export class ReplicationInfoV2ReceiveCoordinator {
64
69
  this._receiveStates = new Map();
65
70
  this._cutoverPeerSessions = new WeakSet();
66
71
  this._localCapabilityReadyBySession = new WeakMap();
72
+ this._localCapabilityContextBySession = new WeakMap();
73
+ this._localCapabilityAdvertisementsByPeer = new Map();
67
74
  this._reservedAdmissionsByPeer = new Map();
68
75
  }
69
76
  clearForClose() {
77
+ for (const advertisement of [
78
+ ...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
79
+ ]) {
80
+ this.clearLocalCapabilityAdvertisement(advertisement);
81
+ }
82
+ this._localCapabilityAdvertisementsByPeer?.clear();
70
83
  for (const state of this._receiveStates?.values() ?? []) {
71
84
  this.clearState(state);
72
85
  }
73
86
  this._receiveStates?.clear();
74
87
  this._cutoverPeerSessions = new WeakSet();
75
88
  this._localCapabilityReadyBySession = new WeakMap();
89
+ this._localCapabilityContextBySession = new WeakMap();
76
90
  }
77
91
  clearPeer(peerHash, expectedSession) {
92
+ const advertisement = this._localCapabilityAdvertisementsByPeer.get(peerHash);
93
+ if (advertisement &&
94
+ (!expectedSession || advertisement.peerSession === expectedSession)) {
95
+ this.clearLocalCapabilityAdvertisement(advertisement);
96
+ }
78
97
  const state = this._receiveStates.get(peerHash);
79
98
  if (state && (!expectedSession || state.peerSession === expectedSession)) {
80
99
  this.clearState(state);
81
100
  this._localCapabilityReadyBySession.delete(state.peerSession);
101
+ this._localCapabilityContextBySession.delete(state.peerSession);
82
102
  this._cutoverPeerSessions.delete(state.peerSession);
83
103
  }
84
104
  if (expectedSession) {
85
105
  this._localCapabilityReadyBySession.delete(expectedSession);
106
+ this._localCapabilityContextBySession.delete(expectedSession);
86
107
  this._cutoverPeerSessions.delete(expectedSession);
87
108
  }
88
109
  }
@@ -112,8 +133,345 @@ export class ReplicationInfoV2ReceiveCoordinator {
112
133
  this._receiveStates.delete(state.peerHash);
113
134
  }
114
135
  }
136
+ clearLocalCapabilityAdvertisement(state, options) {
137
+ if (state.timer) {
138
+ clearTimeout(state.timer);
139
+ state.timer = undefined;
140
+ }
141
+ state.lifecycleSignal.removeEventListener("abort", state.onLifecycleAbort);
142
+ state.controller.abort();
143
+ const wasMapped = this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state;
144
+ if (wasMapped) {
145
+ this._localCapabilityAdvertisementsByPeer.delete(state.peerHash);
146
+ }
147
+ const ready = this._localCapabilityReadyBySession.get(state.peerSession);
148
+ if (ready?.advertisement === state) {
149
+ this._localCapabilityReadyBySession.delete(state.peerSession);
150
+ }
151
+ if (wasMapped &&
152
+ !options?.preserveContext &&
153
+ this._localCapabilityContextBySession.get(state.peerSession) ===
154
+ state.context) {
155
+ this._localCapabilityContextBySession.delete(state.peerSession);
156
+ }
157
+ }
158
+ isLocalCapabilityAdvertisementOwnerCurrent(state) {
159
+ return (this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state &&
160
+ this._localCapabilityContextBySession.get(state.peerSession) ===
161
+ state.context &&
162
+ state.context.peerHash === state.peerHash &&
163
+ state.context.lifecycleSignal === state.lifecycleSignal &&
164
+ state.context.target.equals(state.target) &&
165
+ !state.controller.signal.aborted &&
166
+ !state.lifecycleSignal.aborted &&
167
+ !this.deps.isClosed() &&
168
+ this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
169
+ (state.receiverTransportSession === undefined ||
170
+ this.deps.getReceiverTransportSession() ===
171
+ state.receiverTransportSession));
172
+ }
173
+ isLocalCapabilityAdvertisementGenerationCurrent(state) {
174
+ return (this.isLocalCapabilityAdvertisementOwnerCurrent(state) &&
175
+ this.deps.isReceiveEpochCurrent(state.peerHash, state.receiveEpoch));
176
+ }
177
+ isLocalCapabilityAdvertisementReadyOpen(state) {
178
+ return (this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
179
+ this.deps.isPeerStateCurrent(state.peerHash, state.peerSession, state.receiveEpoch));
180
+ }
181
+ localCapabilityRetryDelay(state) {
182
+ const exponent = Math.max(0, state.attempts - 1);
183
+ return Math.min(this.maxRequestRetryMs, this.requestRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT));
184
+ }
185
+ armLocalCapabilityAdvertisement(state) {
186
+ if (state.timer || state.inFlight || state.ready) {
187
+ return;
188
+ }
189
+ if (state.acknowledgedReady && !state.context.legacyBarrierReleased) {
190
+ return;
191
+ }
192
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
193
+ this.clearLocalCapabilityAdvertisement(state);
194
+ return;
195
+ }
196
+ if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
197
+ return;
198
+ }
199
+ state.timer = setTimeout(() => {
200
+ state.timer = undefined;
201
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
202
+ this.clearLocalCapabilityAdvertisement(state);
203
+ return;
204
+ }
205
+ if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
206
+ return;
207
+ }
208
+ if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
209
+ this.armLocalCapabilityAdvertisement(state);
210
+ return;
211
+ }
212
+ void this.runLocalCapabilityAdvertisement(state);
213
+ }, this.localCapabilityRetryDelay(state));
214
+ state.timer.unref?.();
215
+ }
216
+ async runLocalCapabilityAdvertisement(state) {
217
+ if (state.inFlight) {
218
+ await state.inFlight;
219
+ return;
220
+ }
221
+ if (state.ready) {
222
+ return;
223
+ }
224
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
225
+ this.clearLocalCapabilityAdvertisement(state);
226
+ return;
227
+ }
228
+ if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
229
+ return;
230
+ }
231
+ if (state.acknowledgedReady) {
232
+ if (state.context.legacyBarrierReleased) {
233
+ this.promoteLocalCapabilityAdvertisement(state);
234
+ }
235
+ return;
236
+ }
237
+ if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
238
+ this.armLocalCapabilityAdvertisement(state);
239
+ return;
240
+ }
241
+ state.attempts = Math.min(state.attempts + 1, MAX_BACKOFF_EXPONENT + 1);
242
+ let operation;
243
+ operation = (async () => {
244
+ const refreshed = await this.deps.refreshLocalCapability({
245
+ peerHash: state.peerHash,
246
+ target: state.target,
247
+ peerSession: state.peerSession,
248
+ receiveEpoch: state.receiveEpoch,
249
+ signal: AbortSignal.any([
250
+ state.controller.signal,
251
+ state.lifecycleSignal,
252
+ ]),
253
+ });
254
+ if (!refreshed ||
255
+ !this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
256
+ this.deps.getReceiverTransportSession() !==
257
+ refreshed.receiverTransportSession) {
258
+ return;
259
+ }
260
+ state.receiverTransportSession = refreshed.receiverTransportSession;
261
+ state.acknowledgedReady = {
262
+ peerHash: state.peerHash,
263
+ receiveEpoch: state.receiveEpoch,
264
+ receiverTransportSession: refreshed.receiverTransportSession,
265
+ requestNotBeforeMs: refreshed.requestNotBeforeMs,
266
+ advertisement: state,
267
+ };
268
+ state.attempts = 0;
269
+ this.promoteLocalCapabilityAdvertisement(state);
270
+ })()
271
+ .catch((error) => {
272
+ if (!state.controller.signal.aborted &&
273
+ !state.lifecycleSignal.aborted &&
274
+ !this.deps.isClosed()) {
275
+ this.deps.onLocalCapabilityError?.(error);
276
+ }
277
+ })
278
+ .finally(() => {
279
+ if (state.inFlight === operation) {
280
+ state.inFlight = undefined;
281
+ }
282
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
283
+ this.clearLocalCapabilityAdvertisement(state);
284
+ }
285
+ else if (this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
286
+ !state.ready) {
287
+ if (!state.acknowledgedReady || state.context.legacyBarrierReleased) {
288
+ this.armLocalCapabilityAdvertisement(state);
289
+ }
290
+ }
291
+ });
292
+ state.inFlight = operation;
293
+ await operation;
294
+ }
295
+ /**
296
+ * Start local authenticated-apply advertisement independently of the legacy
297
+ * join path. ACK and legacy publication are a two-phase barrier: the first
298
+ * attempt always settles independently, while readiness is promoted only
299
+ * after the host releases the legacy barrier. Failed attempts leave one
300
+ * exact-session worker retrying with capped exponential backoff.
301
+ */
302
+ advertiseLocalCapability(properties) {
303
+ const peerHash = properties.target.hashcode();
304
+ if (properties.signal.aborted ||
305
+ this.deps.isClosed() ||
306
+ !this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
307
+ !this.deps.isReceiveEpochCurrent(peerHash, properties.receiveEpoch)) {
308
+ return {
309
+ firstAttempt: Promise.resolve(),
310
+ releaseLegacyBarrier: () => { },
311
+ };
312
+ }
313
+ let context = this._localCapabilityContextBySession.get(properties.peerSession);
314
+ if (context &&
315
+ (context.peerHash !== peerHash ||
316
+ !context.target.equals(properties.target) ||
317
+ context.lifecycleSignal !== properties.signal)) {
318
+ return {
319
+ firstAttempt: Promise.resolve(),
320
+ releaseLegacyBarrier: () => { },
321
+ };
322
+ }
323
+ if (!context) {
324
+ context = {
325
+ peerHash,
326
+ target: properties.target,
327
+ lifecycleSignal: properties.signal,
328
+ legacyBarrierReleased: false,
329
+ };
330
+ this._localCapabilityContextBySession.set(properties.peerSession, context);
331
+ }
332
+ let state = this._localCapabilityAdvertisementsByPeer.get(peerHash);
333
+ if (state &&
334
+ (state.peerSession !== properties.peerSession ||
335
+ state.context !== context ||
336
+ state.lifecycleSignal !== properties.signal ||
337
+ !state.target.equals(properties.target))) {
338
+ this.clearLocalCapabilityAdvertisement(state);
339
+ state = undefined;
340
+ }
341
+ if (state && state.receiveEpoch !== properties.receiveEpoch) {
342
+ this.clearLocalCapabilityAdvertisement(state, { preserveContext: true });
343
+ state = undefined;
344
+ }
345
+ if (state && !this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
346
+ this.clearLocalCapabilityAdvertisement(state);
347
+ state = undefined;
348
+ }
349
+ if (this._localCapabilityContextBySession.get(properties.peerSession) !==
350
+ context) {
351
+ return {
352
+ firstAttempt: Promise.resolve(),
353
+ releaseLegacyBarrier: () => { },
354
+ };
355
+ }
356
+ if (!state) {
357
+ const controller = new AbortController();
358
+ const advertisement = {
359
+ peerHash,
360
+ target: properties.target,
361
+ peerSession: properties.peerSession,
362
+ receiveEpoch: properties.receiveEpoch,
363
+ lifecycleSignal: properties.signal,
364
+ onLifecycleAbort: () => { },
365
+ controller,
366
+ context,
367
+ attempts: 0,
368
+ ready: false,
369
+ receiverTransportSession: this.deps.getReceiverTransportSession(),
370
+ };
371
+ advertisement.onLifecycleAbort = () => this.clearLocalCapabilityAdvertisement(advertisement);
372
+ properties.signal.addEventListener("abort", advertisement.onLifecycleAbort, {
373
+ once: true,
374
+ });
375
+ this._localCapabilityAdvertisementsByPeer.set(peerHash, advertisement);
376
+ state = advertisement;
377
+ }
378
+ const firstAttempt = state.firstAttempt ??
379
+ (state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
380
+ const advertisement = state;
381
+ return {
382
+ firstAttempt,
383
+ releaseLegacyBarrier: () => this.releaseLocalCapabilityLegacyBarrier(advertisement.peerSession, context),
384
+ };
385
+ }
386
+ releaseLocalCapabilityLegacyBarrier(peerSession, context) {
387
+ if (context.legacyBarrierReleased) {
388
+ return;
389
+ }
390
+ if (this._localCapabilityContextBySession.get(peerSession) !== context ||
391
+ context.lifecycleSignal.aborted ||
392
+ this.deps.isClosed() ||
393
+ !this.deps.isPeerSessionCurrent(context.peerHash, peerSession)) {
394
+ return;
395
+ }
396
+ context.legacyBarrierReleased = true;
397
+ const state = this._localCapabilityAdvertisementsByPeer.get(context.peerHash);
398
+ if (state?.peerSession === peerSession && state.context === context) {
399
+ if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
400
+ this.clearLocalCapabilityAdvertisement(state);
401
+ return;
402
+ }
403
+ this.promoteLocalCapabilityAdvertisement(state);
404
+ }
405
+ }
406
+ promoteLocalCapabilityAdvertisement(state) {
407
+ const ready = state.acknowledgedReady;
408
+ if (state.ready ||
409
+ !state.context.legacyBarrierReleased ||
410
+ !ready ||
411
+ ready.receiveEpoch !== state.receiveEpoch ||
412
+ ready.advertisement !== state ||
413
+ !this.isLocalCapabilityAdvertisementOwnerCurrent(state) ||
414
+ !this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
415
+ this.deps.getReceiverTransportSession() !== ready.receiverTransportSession) {
416
+ return state.ready;
417
+ }
418
+ if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
419
+ this.armLocalCapabilityAdvertisement(state);
420
+ return false;
421
+ }
422
+ if (!this.recordLocalCapabilityReady({
423
+ peerHash: state.peerHash,
424
+ peerSession: state.peerSession,
425
+ receiveEpoch: state.receiveEpoch,
426
+ receiverTransportSession: ready.receiverTransportSession,
427
+ requestNotBeforeMs: ready.requestNotBeforeMs,
428
+ }, state)) {
429
+ return false;
430
+ }
431
+ state.ready = true;
432
+ return true;
433
+ }
434
+ /**
435
+ * Re-advertise one exact current recovery epoch from the stable membership
436
+ * context captured during opening. The opening handle owns barrier release;
437
+ * recovery never bypasses a legacy snapshot or role publication still in
438
+ * progress.
439
+ */
440
+ reAdvertiseLocalCapabilityForRecovery(properties) {
441
+ const context = this._localCapabilityContextBySession.get(properties.peerSession);
442
+ if (!context ||
443
+ context.peerHash !== properties.peerHash ||
444
+ context.lifecycleSignal.aborted ||
445
+ this.deps.isClosed() ||
446
+ !this.deps.isPeerSessionCurrent(properties.peerHash, properties.peerSession) ||
447
+ !this.deps.isReceiveEpochCurrent(properties.peerHash, properties.receiveEpoch)) {
448
+ return false;
449
+ }
450
+ const state = this._receiveStates.get(properties.peerHash);
451
+ if (state?.peerSession === properties.peerSession &&
452
+ state.receiveEpoch === properties.receiveEpoch &&
453
+ state.receiverBinding !== undefined) {
454
+ return false;
455
+ }
456
+ const ready = this._localCapabilityReadyBySession.get(properties.peerSession);
457
+ if (ready?.peerHash === properties.peerHash &&
458
+ ready.receiveEpoch === properties.receiveEpoch &&
459
+ ready.receiverTransportSession === this.deps.getReceiverTransportSession()) {
460
+ return false;
461
+ }
462
+ this.advertiseLocalCapability({
463
+ target: context.target,
464
+ peerSession: properties.peerSession,
465
+ receiveEpoch: properties.receiveEpoch,
466
+ signal: context.lifecycleSignal,
467
+ });
468
+ return true;
469
+ }
115
470
  /** Record success of this session's ACKed local APPLY advertisement. */
116
471
  markLocalCapabilityReady(properties) {
472
+ return this.recordLocalCapabilityReady(properties);
473
+ }
474
+ recordLocalCapabilityReady(properties, advertisement) {
117
475
  const { peerHash, peerSession, receiverTransportSession } = properties;
118
476
  const state = this._receiveStates.get(peerHash);
119
477
  if (this.deps.isClosed() ||
@@ -123,8 +481,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
123
481
  }
124
482
  const ready = {
125
483
  peerHash,
484
+ receiveEpoch: properties.receiveEpoch,
126
485
  receiverTransportSession,
127
486
  requestNotBeforeMs: properties.requestNotBeforeMs,
487
+ advertisement,
128
488
  };
129
489
  this._localCapabilityReadyBySession.set(peerSession, ready);
130
490
  if (state?.peerSession === peerSession &&
@@ -198,7 +558,9 @@ export class ReplicationInfoV2ReceiveCoordinator {
198
558
  });
199
559
  }
200
560
  const ready = this._localCapabilityReadyBySession.get(peerSession);
201
- if (ready?.peerHash === peerHash && state.receiverBinding === undefined) {
561
+ if (ready?.peerHash === peerHash &&
562
+ ready.receiveEpoch === receiveEpoch &&
563
+ state.receiverBinding === undefined) {
202
564
  this.bindLocalCapability(state, ready);
203
565
  }
204
566
  if (state.phase !== "active") {
@@ -231,7 +593,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
231
593
  };
232
594
  this._receiveStates.set(peerHash, state);
233
595
  const ready = this._localCapabilityReadyBySession.get(peerSession);
234
- if (ready?.peerHash === peerHash) {
596
+ if (ready?.peerHash === peerHash && ready.receiveEpoch === receiveEpoch) {
235
597
  this.bindLocalCapability(state, ready);
236
598
  this.armRequest(state, 0);
237
599
  }
@@ -260,6 +622,33 @@ export class ReplicationInfoV2ReceiveCoordinator {
260
622
  });
261
623
  return true;
262
624
  }
625
+ /**
626
+ * Resume only a request generation that exhausted its bounded retries.
627
+ * Wait/liveness callers may nudge recovery without invalidating an active,
628
+ * timed or in-flight request and without advancing the receive epoch.
629
+ */
630
+ resumeParkedRequest(properties) {
631
+ const state = this._receiveStates.get(properties.peerHash);
632
+ if (!state ||
633
+ state.peerSession !== properties.peerSession ||
634
+ state.receiveEpoch !== properties.receiveEpoch ||
635
+ !this.deps.isPeerStateCurrent(properties.peerHash, properties.peerSession, properties.receiveEpoch) ||
636
+ state.phase === "active" ||
637
+ !state.requestParked ||
638
+ state.requestTimer !== undefined ||
639
+ state.requestInFlight !== undefined ||
640
+ this._reservedAdmissionsByPeer.has(properties.peerHash) ||
641
+ state.receiverBinding === undefined ||
642
+ state.lastSequence === MAX_U64) {
643
+ return false;
644
+ }
645
+ state.requestAttempts = 0;
646
+ state.requestsSinceCapabilityRefresh = 0;
647
+ state.capabilityRefreshRequired = true;
648
+ state.requestParked = false;
649
+ this.armRequest(state, 0);
650
+ return true;
651
+ }
263
652
  transitionToResync(state, options) {
264
653
  const shouldRestart = state.phase !== "resync" ||
265
654
  options?.force === true ||
@@ -385,6 +774,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
385
774
  const { state } = admission;
386
775
  this._reservedAdmissionsByPeer.set(peerHash, admission);
387
776
  state.reservedAdmission = admission;
777
+ if (state.requestTimer) {
778
+ clearTimeout(state.requestTimer);
779
+ state.requestTimer = undefined;
780
+ }
781
+ if (state.legacyFallbackTimer) {
782
+ clearTimeout(state.legacyFallbackTimer);
783
+ state.legacyFallbackTimer = undefined;
784
+ }
388
785
  return admission;
389
786
  }
390
787
  release(admission) {
@@ -403,6 +800,27 @@ export class ReplicationInfoV2ReceiveCoordinator {
403
800
  (currentState !== state && currentState.phase !== "active"))) {
404
801
  this.transitionToResync(currentState, { force: true });
405
802
  }
803
+ else if (currentState === state &&
804
+ !admission.committed &&
805
+ this.isStateCurrent(state) &&
806
+ state.phase !== "active" &&
807
+ state.requestTimer === undefined &&
808
+ state.requestInFlight === undefined &&
809
+ !state.requestParked) {
810
+ // Reserving a Full pauses this generation's retry timer. If its host
811
+ // apply lane releases without committing, restore the bounded request
812
+ // worker so the exact current generation cannot become stranded.
813
+ this.armRequest(state, 0);
814
+ }
815
+ if (currentState === state &&
816
+ this.isStateCurrent(state) &&
817
+ state.phase === "active" &&
818
+ state.legacyFallbackFingerprint !== undefined) {
819
+ // A reservation also pauses compat sidecar recovery. Matching commits
820
+ // clear the evidence; an uncommitted or non-matching frame restores its
821
+ // bounded fallback without invalidating work in the host apply lane.
822
+ this.armLegacyFallback(state);
823
+ }
406
824
  }
407
825
  /**
408
826
  * B9 can lose its sender grant while keeping the topic session open. Its
@@ -485,6 +903,16 @@ export class ReplicationInfoV2ReceiveCoordinator {
485
903
  if (state.legacyFallbackTimer) {
486
904
  return true;
487
905
  }
906
+ this.armLegacyFallback(state);
907
+ return true;
908
+ }
909
+ armLegacyFallback(state) {
910
+ if (state.legacyFallbackTimer ||
911
+ state.phase !== "active" ||
912
+ state.legacyFallbackFingerprint === undefined ||
913
+ this._reservedAdmissionsByPeer.has(state.peerHash)) {
914
+ return;
915
+ }
488
916
  state.legacyFallbackTimer = setTimeout(() => {
489
917
  state.legacyFallbackTimer = undefined;
490
918
  if (this.isStateCurrent(state) && state.phase === "active") {
@@ -495,7 +923,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
495
923
  }
496
924
  }, this.legacyFallbackDelayMs);
497
925
  state.legacyFallbackTimer.unref?.();
498
- return true;
499
926
  }
500
927
  clearLegacyFallback(state) {
501
928
  if (state.legacyFallbackTimer) {
@@ -594,8 +1021,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
594
1021
  if (this._receiveStates.get(state.peerHash) !== state ||
595
1022
  state.controller.signal.aborted ||
596
1023
  this.deps.isClosed() ||
597
- (this._reservedAdmissionsByPeer.has(state.peerHash) &&
598
- this._reservedAdmissionsByPeer.get(state.peerHash)?.state !== state) ||
1024
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
599
1025
  state.receiverBinding === undefined ||
600
1026
  state.phase === "active" ||
601
1027
  state.requestParked ||
@@ -614,6 +1040,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
614
1040
  return Math.min(this.maxRequestRetryMs, this.requestRetryMs * 2 ** Math.min(exponent, 20));
615
1041
  }
616
1042
  async refreshGrant(state) {
1043
+ const version = state.version;
617
1044
  const refreshed = await this.deps.refreshLocalCapability({
618
1045
  peerHash: state.peerHash,
619
1046
  target: state.target,
@@ -622,6 +1049,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
622
1049
  signal: state.controller.signal,
623
1050
  });
624
1051
  if (!refreshed ||
1052
+ state.version !== version ||
1053
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
625
1054
  !this.isStateCurrent(state) ||
626
1055
  this.deps.getReceiverTransportSession() !==
627
1056
  refreshed.receiverTransportSession) {
@@ -629,6 +1058,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
629
1058
  }
630
1059
  const ready = {
631
1060
  peerHash: state.peerHash,
1061
+ receiveEpoch: state.receiveEpoch,
632
1062
  receiverTransportSession: refreshed.receiverTransportSession,
633
1063
  requestNotBeforeMs: refreshed.requestNotBeforeMs,
634
1064
  };
@@ -647,7 +1077,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
647
1077
  if (state.requestInFlight) {
648
1078
  return;
649
1079
  }
650
- if (!this.isStateCurrent(state)) {
1080
+ if (!this.isStateCurrent(state) ||
1081
+ this._reservedAdmissionsByPeer.has(state.peerHash)) {
651
1082
  return;
652
1083
  }
653
1084
  if (state.phase === "active" ||
@@ -667,12 +1098,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
667
1098
  return;
668
1099
  }
669
1100
  }
670
- if (!this.isStateCurrent(state)) {
1101
+ if (!this.isStateCurrent(state) ||
1102
+ this._reservedAdmissionsByPeer.has(state.peerHash)) {
671
1103
  return;
672
1104
  }
673
1105
  const ready = this._localCapabilityReadyBySession.get(state.peerSession);
674
1106
  if (!ready ||
675
1107
  ready.peerHash !== state.peerHash ||
1108
+ ready.receiveEpoch !== state.receiveEpoch ||
676
1109
  ready.receiverTransportSession !== state.receiverTransportSession) {
677
1110
  return;
678
1111
  }
@@ -705,6 +1138,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
705
1138
  }
706
1139
  if (this._receiveStates.get(state.peerHash) === state &&
707
1140
  !state.controller.signal.aborted &&
1141
+ !this._reservedAdmissionsByPeer.has(state.peerHash) &&
708
1142
  !state.requestTimer &&
709
1143
  state.phase !== "active") {
710
1144
  if (state.requestsSinceCapabilityRefresh >= 3) {