@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.
package/src/index.ts CHANGED
@@ -2086,7 +2086,11 @@ export class SharedLog<
2086
2086
  // reconnect barrier commits. See PeerSessionRegistry._replicationInfoBlockedPeers.
2087
2087
  private _replicationInfoRequestByPeer!: Map<
2088
2088
  string,
2089
- { attempts: number; timer?: ReturnType<typeof setTimeout> }
2089
+ {
2090
+ attempts: number;
2091
+ timer?: ReturnType<typeof setTimeout>;
2092
+ peerSession?: PeerSession;
2093
+ }
2090
2094
  >;
2091
2095
  private _replicationInfoApplyQueueByPeer!: Map<string, Promise<void>>;
2092
2096
  // Range ids are global primary keys while receive lanes are per peer. Keep
@@ -3382,6 +3386,7 @@ export class SharedLog<
3382
3386
  error,
3383
3387
  ),
3384
3388
  enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
3389
+ isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
3385
3390
  isClosed: () => this.closed,
3386
3391
  getCloseSignal: () => this._closeController.signal,
3387
3392
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
@@ -3415,6 +3420,8 @@ export class SharedLog<
3415
3420
  validatePersistedReplicationRangeSnapshot: (ranges) =>
3416
3421
  this.validatePersistedReplicationRangeSnapshot(ranges),
3417
3422
  isClosed: () => this.closed,
3423
+ isPeerSessionCurrent: (peerHash, peerSession) =>
3424
+ this._peerSessions.isCurrent(peerHash, peerSession),
3418
3425
  isPeerSessionOpen: (peerHash, peerSession) =>
3419
3426
  this._peerSessions.isCurrent(peerHash, peerSession) &&
3420
3427
  (peerSession as PeerSession).phase === "open" &&
@@ -3495,6 +3502,12 @@ export class SharedLog<
3495
3502
  (this.node.services.pubsub as unknown as { session: number }).session,
3496
3503
  ),
3497
3504
  isClosed: () => this.closed,
3505
+ isPeerSessionCurrent: (peerHash, peerSession) =>
3506
+ this._peerSessions.isCurrent(peerHash, peerSession) &&
3507
+ (peerSession as PeerSession).phase === "open" &&
3508
+ (peerSession as PeerSession).isActive(),
3509
+ isReceiveEpochCurrent: (peerHash, receiveEpoch) =>
3510
+ this._peerSessions.isReceiveEpochCurrent(peerHash, receiveEpoch),
3498
3511
  isPeerStateCurrent: (peerHash, peerSession, receiveEpoch) =>
3499
3512
  this._peerSessions.isCurrent(peerHash, peerSession) &&
3500
3513
  (peerSession as PeerSession).phase === "open" &&
@@ -3532,6 +3545,8 @@ export class SharedLog<
3532
3545
  }`,
3533
3546
  );
3534
3547
  },
3548
+ onLocalCapabilityError: (error) =>
3549
+ this.handleReplicationLifecycleSendError(error),
3535
3550
  });
3536
3551
  }
3537
3552
 
@@ -3730,6 +3745,14 @@ export class SharedLog<
3730
3745
  return this._logProperties?.compatibility;
3731
3746
  }
3732
3747
 
3748
+ /**
3749
+ * Legacy replication-info is an explicit compatibility fallback. Current
3750
+ * logs never infer or re-enable it from a remote peer's capabilities.
3751
+ */
3752
+ private get legacyReplicationInfoEnabled(): boolean {
3753
+ return this.compatibility !== undefined && this.compatibility < 10;
3754
+ }
3755
+
3733
3756
  get isAdaptiveReplicating() {
3734
3757
  return this._isAdaptiveReplicating;
3735
3758
  }
@@ -6527,10 +6550,16 @@ export class SharedLog<
6527
6550
  // deliver the authoritative Full that completes recovery.
6528
6551
  const peerSession = this._peerSessions.current(keyHash);
6529
6552
  if (peerSession?.phase === "open") {
6553
+ const receiveEpoch = this._peerSessions.receiveEpoch(keyHash);
6530
6554
  this._v2Receive.advanceRecovery({
6531
6555
  peerHash: keyHash,
6532
6556
  peerSession,
6533
- receiveEpoch: this._peerSessions.receiveEpoch(keyHash),
6557
+ receiveEpoch,
6558
+ });
6559
+ this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
6560
+ peerHash: keyHash,
6561
+ peerSession,
6562
+ receiveEpoch,
6534
6563
  });
6535
6564
  }
6536
6565
  }
@@ -14147,7 +14176,9 @@ export class SharedLog<
14147
14176
  this.domain = options?.domain
14148
14177
  ? (options.domain(this) as unknown as D)
14149
14178
  : (createReplicationDomainHash(
14150
- options?.compatibility && options?.compatibility < 10 ? "u32" : "u64",
14179
+ options?.compatibility !== undefined && options.compatibility < 10
14180
+ ? "u32"
14181
+ : "u64",
14151
14182
  )(this) as unknown as D);
14152
14183
  this.indexableDomain = createIndexableDomainFromResolution(
14153
14184
  this.domain.resolution,
@@ -14293,8 +14324,12 @@ export class SharedLog<
14293
14324
  }
14294
14325
 
14295
14326
  this._closeController = new AbortController();
14296
- this._announcements.setupReplicationAnnouncementRetryFunction();
14297
- this._announcements.setupReplicationAnnouncementRepairFunction();
14327
+ if (this.legacyReplicationInfoEnabled) {
14328
+ this._announcements.setupReplicationAnnouncementRetryFunction();
14329
+ this._announcements.setupReplicationAnnouncementRepairFunction();
14330
+ } else {
14331
+ this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
14332
+ }
14298
14333
  this._closeController.signal.addEventListener("abort", () => {
14299
14334
  for (const [_peer, state] of this._replicationInfoRequestByPeer) {
14300
14335
  if (state.timer) clearTimeout(state.timer);
@@ -15496,7 +15531,14 @@ export class SharedLog<
15496
15531
 
15497
15532
  async afterOpen(): Promise<void> {
15498
15533
  await super.afterOpen();
15499
- const existingSubscribersPromise = this._getTopicSubscribers(this.topic);
15534
+ // Start the broader discovery eagerly, in parallel with rebalance, for its
15535
+ // routing/cache side effects. It also contains connected/provider/fanout
15536
+ // candidates that have not subscribed to this log, so only the authoritative
15537
+ // pubsub snapshot below may create subscription fallback sessions.
15538
+ const subscriberDiscoveryPromise = this._getTopicSubscribers(this.topic);
15539
+ const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(
15540
+ this.topic,
15541
+ );
15500
15542
  const replicationLifecycleController =
15501
15543
  this._instanceLifecycle?.membershipLifecycleController;
15502
15544
 
@@ -15517,6 +15559,7 @@ export class SharedLog<
15517
15559
  this._liveness.startReplicatorLivenessSweep();
15518
15560
 
15519
15561
  await this.rebalanceParticipation();
15562
+ await subscriberDiscoveryPromise;
15520
15563
 
15521
15564
  // Take into account existing subscription
15522
15565
  (await existingSubscribersPromise)?.forEach((v) => {
@@ -15526,6 +15569,14 @@ export class SharedLog<
15526
15569
  if (this.closed) {
15527
15570
  return;
15528
15571
  }
15572
+ // The live subscribe event and this after-open snapshot can report the
15573
+ // same initial transport generation. The live callback rotates its
15574
+ // PeerSession synchronously, so any current session here proves the
15575
+ // fallback is stale/duplicate. Rotating again would erase the signed
15576
+ // capability binding that the first callback just established.
15577
+ if (this._peerSessions.current(v.hashcode()) !== null) {
15578
+ return;
15579
+ }
15529
15580
  void this.runSubscriptionChangeCallback(() =>
15530
15581
  this.handleSubscriptionChange(v, [this.topic], true),
15531
15582
  );
@@ -15738,7 +15789,9 @@ export class SharedLog<
15738
15789
  const peerSession = this._peerSessions.current(peerHash);
15739
15790
  const preserveV2Session =
15740
15791
  peerSession?.phase === "open" && peerSession.isActive();
15741
- this.cancelReplicationInfoRequests(peerHash);
15792
+ if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
15793
+ this.cancelReplicationInfoRequests(peerHash);
15794
+ }
15742
15795
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15743
15796
  this._liveness._replicatorLastActivityAt.delete(peerHash);
15744
15797
  if (!preserveV2Session) {
@@ -16909,15 +16962,18 @@ export class SharedLog<
16909
16962
  }, 2_000);
16910
16963
  try {
16911
16964
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
16912
- await Promise.all([
16913
- this.rpc
16914
- .send(reset, {
16915
- priority: CONVERGENCE_MESSAGE_PRIORITY,
16916
- signal: abort.signal,
16917
- })
16918
- .catch(() => {}),
16919
- this._v2Send.sendTerminalReset(abort.signal),
16920
- ]);
16965
+ const resets = [this._v2Send.sendTerminalReset(abort.signal)];
16966
+ if (this.legacyReplicationInfoEnabled) {
16967
+ resets.push(
16968
+ this.rpc
16969
+ .send(reset, {
16970
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
16971
+ signal: abort.signal,
16972
+ })
16973
+ .catch(() => {}),
16974
+ );
16975
+ }
16976
+ await Promise.all(resets);
16921
16977
  } finally {
16922
16978
  clearTimeout(abortTimer);
16923
16979
  }
@@ -17039,15 +17095,18 @@ export class SharedLog<
17039
17095
  }, 2_000);
17040
17096
  try {
17041
17097
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17042
- await Promise.all([
17043
- this.rpc
17044
- .send(reset, {
17045
- priority: CONVERGENCE_MESSAGE_PRIORITY,
17046
- signal: abort.signal,
17047
- })
17048
- .catch(() => {}),
17049
- this._v2Send.sendTerminalReset(abort.signal),
17050
- ]);
17098
+ const resets = [this._v2Send.sendTerminalReset(abort.signal)];
17099
+ if (this.legacyReplicationInfoEnabled) {
17100
+ resets.push(
17101
+ this.rpc
17102
+ .send(reset, {
17103
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
17104
+ signal: abort.signal,
17105
+ })
17106
+ .catch(() => {}),
17107
+ );
17108
+ }
17109
+ await Promise.all(resets);
17051
17110
  } finally {
17052
17111
  clearTimeout(abortTimer);
17053
17112
  }
@@ -17573,6 +17632,19 @@ export class SharedLog<
17573
17632
  if (!context.from) {
17574
17633
  throw new Error("Missing from in update role message");
17575
17634
  }
17635
+ if (
17636
+ !this.legacyReplicationInfoEnabled &&
17637
+ (msg instanceof RequestReplicationInfoMessage ||
17638
+ msg instanceof ResponseRoleMessage ||
17639
+ msg instanceof AllReplicatingSegmentsMessage ||
17640
+ msg instanceof AddedReplicationSegmentMessage ||
17641
+ msg instanceof StoppedReplicating)
17642
+ ) {
17643
+ // These variants remain registered decode tombstones, but current logs
17644
+ // fail closed before leases, synchronizer work, liveness, watermarks or
17645
+ // mutations. Only an explicit pre-v10 compatibility open admits them.
17646
+ return;
17647
+ }
17576
17648
  // Snapshot receive ownership before any async handler gets a chance to
17577
17649
  // yield. Replication-info messages reach their branch only after the
17578
17650
  // synchronizer declines them, and a U/S transition can happen meanwhile.
@@ -19294,19 +19366,37 @@ export class SharedLog<
19294
19366
  timestamp: capabilityTimestamp,
19295
19367
  openingSession: receiveSession!,
19296
19368
  });
19297
- } else if (
19298
- this.observePeerSyncCapabilities({
19369
+ } else {
19370
+ const observed = this.observePeerSyncCapabilities({
19299
19371
  peerHash: receiveFromHash,
19300
19372
  capabilities: msg.capabilities,
19301
19373
  transportSession: capabilityTransportSession,
19302
19374
  timestamp: capabilityTimestamp,
19303
- }) &&
19304
- receiveSession?.phase === "open"
19305
- ) {
19306
- this.promoteReplicationInfoV2ReceiveCapability(
19307
- context.from,
19308
- receiveSession,
19309
- );
19375
+ });
19376
+ if (observed && receiveSession?.phase === "open") {
19377
+ this.promoteReplicationInfoV2ReceiveCapability(
19378
+ context.from,
19379
+ receiveSession,
19380
+ );
19381
+ } else if (observed && receiveSession === null) {
19382
+ // A capability can arrive before the sender's topic Subscribe after
19383
+ // reconnect. Ask that authenticated peer for its authoritative
19384
+ // subscriber snapshot; the resulting Subscribe creates the real
19385
+ // PeerSession and completes the symmetric capability handshake.
19386
+ // Never synthesize membership from capability traffic alone.
19387
+ void Promise.resolve()
19388
+ .then(() =>
19389
+ this.node.services.pubsub.requestSubscribers(
19390
+ this.topic,
19391
+ context.from,
19392
+ ),
19393
+ )
19394
+ .catch((error) => {
19395
+ if (!isNotStartedError(error as Error)) {
19396
+ logger.error(error?.toString?.() ?? String(error));
19397
+ }
19398
+ });
19399
+ }
19310
19400
  }
19311
19401
  }
19312
19402
  return;
@@ -19873,7 +19963,10 @@ export class SharedLog<
19873
19963
  return;
19874
19964
  }
19875
19965
  this._liveness.markReplicatorActivity(fromHash);
19876
- if (msg instanceof FullReplicationInfoV2Message) {
19966
+ if (
19967
+ msg instanceof FullReplicationInfoV2Message &&
19968
+ this.legacyReplicationInfoEnabled
19969
+ ) {
19877
19970
  this.cancelReplicationInfoRequests(fromHash);
19878
19971
  }
19879
19972
  });
@@ -20537,6 +20630,7 @@ export class SharedLog<
20537
20630
  }, timeoutMs);
20538
20631
 
20539
20632
  let requestAttempts = 0;
20633
+ let subscriberSnapshotInFlight: Promise<void> | undefined;
20540
20634
  const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
20541
20635
  const maxRequestAttempts =
20542
20636
  this.waitForReplicatorRequestMaxAttempts ??
@@ -20544,6 +20638,26 @@ export class SharedLog<
20544
20638
  WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
20545
20639
  Math.ceil(timeoutMs / requestIntervalMs),
20546
20640
  );
20641
+ const requestSubscriberSnapshot = () => {
20642
+ if (subscriberSnapshotInFlight) {
20643
+ return;
20644
+ }
20645
+ subscriberSnapshotInFlight = Promise.resolve()
20646
+ .then(async () => {
20647
+ if (settled || this.closed) {
20648
+ return;
20649
+ }
20650
+ await this.node.services.pubsub.requestSubscribers(this.topic, key);
20651
+ })
20652
+ .catch((error) => {
20653
+ if (!isNotStartedError(error as Error)) {
20654
+ logger.error(error?.toString?.() ?? String(error));
20655
+ }
20656
+ })
20657
+ .finally(() => {
20658
+ subscriberSnapshotInFlight = undefined;
20659
+ });
20660
+ };
20547
20661
 
20548
20662
  const requestReplicationInfo = () => {
20549
20663
  if (settled || this.closed) {
@@ -20556,17 +20670,35 @@ export class SharedLog<
20556
20670
 
20557
20671
  requestAttempts++;
20558
20672
 
20559
- this.rpc
20560
- .send(new RequestReplicationInfoMessage(), {
20561
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
20562
- })
20563
- .catch((e) => {
20564
- // Best-effort: missing peers / unopened RPC should not fail the wait logic.
20565
- if (isNotStartedError(e as Error)) {
20566
- return;
20567
- }
20568
- logger.error(e?.toString?.() ?? String(e));
20569
- });
20673
+ if (this.legacyReplicationInfoEnabled) {
20674
+ this.rpc
20675
+ .send(new RequestReplicationInfoMessage(), {
20676
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
20677
+ })
20678
+ .catch((e) => {
20679
+ // Best-effort: missing peers / unopened RPC should not fail the wait logic.
20680
+ if (isNotStartedError(e as Error)) {
20681
+ return;
20682
+ }
20683
+ logger.error(e?.toString?.() ?? String(e));
20684
+ });
20685
+ } else {
20686
+ const peerHash = key.hashcode();
20687
+ const peerSession = this._peerSessions.current(peerHash);
20688
+ if (peerSession?.phase === "open") {
20689
+ this._v2Receive.resumeParkedRequest({
20690
+ peerHash,
20691
+ peerSession,
20692
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
20693
+ });
20694
+ } else if (peerSession === null || peerSession.phase === "departing") {
20695
+ // A peer can be known to routing before its SharedLog topic
20696
+ // subscription has been observed. Legacy requests used to bootstrap
20697
+ // that case directly; V2 needs an authoritative Subscribe snapshot
20698
+ // before it can create a fenced PeerSession and request a Full.
20699
+ requestSubscriberSnapshot();
20700
+ }
20701
+ }
20570
20702
 
20571
20703
  if (requestAttempts < maxRequestAttempts) {
20572
20704
  requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
@@ -23300,6 +23432,73 @@ export class SharedLog<
23300
23432
  this._replicationInfoRequestByPeer.delete(peerHash);
23301
23433
  }
23302
23434
 
23435
+ private scheduleReplicationInfoV2Recovery(
23436
+ peer: PublicSignKey,
23437
+ replicationLifecycleController = this._instanceLifecycle
23438
+ ?.membershipLifecycleController,
23439
+ ) {
23440
+ if (
23441
+ !replicationLifecycleController ||
23442
+ !this.isReplicationLifecycleActive(replicationLifecycleController)
23443
+ ) {
23444
+ return;
23445
+ }
23446
+ const peerHash = peer.hashcode();
23447
+ const peerSession = this._peerSessions.current(peerHash);
23448
+ if (!peerSession || peerSession.phase !== "open") {
23449
+ return;
23450
+ }
23451
+ const requestStates = this._replicationInfoRequestByPeer;
23452
+ const existing = requestStates.get(peerHash);
23453
+ if (existing?.peerSession === peerSession) {
23454
+ return;
23455
+ }
23456
+ if (existing) {
23457
+ if (existing.timer) {
23458
+ clearTimeout(existing.timer);
23459
+ }
23460
+ requestStates.delete(peerHash);
23461
+ }
23462
+ const state: {
23463
+ attempts: number;
23464
+ timer?: ReturnType<typeof setTimeout>;
23465
+ peerSession: PeerSession;
23466
+ } = {
23467
+ attempts: 0,
23468
+ peerSession,
23469
+ };
23470
+ requestStates.set(peerHash, state);
23471
+ const cancel = () => {
23472
+ if (requestStates.get(peerHash) !== state) {
23473
+ return;
23474
+ }
23475
+ if (state.timer) {
23476
+ clearTimeout(state.timer);
23477
+ }
23478
+ requestStates.delete(peerHash);
23479
+ };
23480
+ const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
23481
+ const tick = () => {
23482
+ if (
23483
+ !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23484
+ peerSession.phase !== "open" ||
23485
+ !this._peerSessions.isCurrent(peerHash, peerSession)
23486
+ ) {
23487
+ cancel();
23488
+ return;
23489
+ }
23490
+ this._v2Receive.resumeParkedRequest({
23491
+ peerHash,
23492
+ peerSession,
23493
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
23494
+ });
23495
+ state.attempts++;
23496
+ state.timer = setTimeout(tick, intervalMs);
23497
+ state.timer.unref?.();
23498
+ };
23499
+ tick();
23500
+ }
23501
+
23303
23502
  private scheduleReplicationInfoRequests(
23304
23503
  peer: PublicSignKey,
23305
23504
  replicationLifecycleController = this._instanceLifecycle
@@ -23311,6 +23510,13 @@ export class SharedLog<
23311
23510
  ) {
23312
23511
  return;
23313
23512
  }
23513
+ if (!this.legacyReplicationInfoEnabled) {
23514
+ this.scheduleReplicationInfoV2Recovery(
23515
+ peer,
23516
+ replicationLifecycleController,
23517
+ );
23518
+ return;
23519
+ }
23314
23520
  const peerHash = peer.hashcode();
23315
23521
  const requestStates = this._replicationInfoRequestByPeer;
23316
23522
  if (requestStates.has(peerHash)) {
@@ -23344,7 +23550,6 @@ export class SharedLog<
23344
23550
  cancel();
23345
23551
  return;
23346
23552
  }
23347
-
23348
23553
  state.attempts++;
23349
23554
 
23350
23555
  this.rpc
@@ -23381,6 +23586,7 @@ export class SharedLog<
23381
23586
  topics: string[],
23382
23587
  subscribed: boolean,
23383
23588
  subscriptionEpoch?: PeerSession,
23589
+ subscriptionTransportSession?: bigint,
23384
23590
  ) {
23385
23591
  if (!topics.includes(this.topic)) {
23386
23592
  return;
@@ -23403,13 +23609,31 @@ export class SharedLog<
23403
23609
  if (!ownsSubscriptionEpoch()) {
23404
23610
  return;
23405
23611
  }
23612
+ // A reconnect can arrive before the previous exact-session recovery tick
23613
+ // observes its stale session. Retire that job synchronously so it cannot
23614
+ // suppress the replacement session's scheduler in the shared peer slot.
23615
+ this.cancelReplicationInfoRequests(peerHash);
23406
23616
  // A destination stream is scoped to exactly one topic-subscription
23407
23617
  // session. Abort the predecessor synchronously before either barrier can
23408
23618
  // yield; a late queue completion must never enter the new session.
23409
23619
  this._v2Receive.clearPeer(peerHash);
23410
23620
  this._v2Send.clearPeer(peerHash);
23411
- this._peerSyncCapabilitySessions.delete(peerHash);
23412
- this._peerSyncCapabilityTimestamps.delete(peerHash);
23621
+ const capabilityTransportSession =
23622
+ this._peerSyncCapabilitySessions.get(peerHash);
23623
+ const canInheritCapability =
23624
+ subscribed &&
23625
+ (subscriptionTransportSession !== undefined
23626
+ ? capabilityTransportSession === subscriptionTransportSession
23627
+ : !expectedSubscriptionEpoch.hasPredecessor);
23628
+ if (!canInheritCapability) {
23629
+ // Departures and successor openings revoke the signed transport binding.
23630
+ // A live successor may inherit a capability that arrived just before its
23631
+ // Subscribe only when both signed frames belong to the same transport
23632
+ // generation. Snapshot fallbacks lack that proof, so only their first
23633
+ // opening may inherit pre-opening capability state.
23634
+ this._peerSyncCapabilitySessions.delete(peerHash);
23635
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
23636
+ }
23413
23637
  if (subscribed) {
23414
23638
  const pendingOpeningCapabilities =
23415
23639
  this._openingSyncCapabilitiesByPeer.get(peerHash);
@@ -23548,76 +23772,57 @@ export class SharedLog<
23548
23772
  expectedSubscriptionEpoch,
23549
23773
  );
23550
23774
 
23551
- // Decode, sender and authenticated apply readiness are separate bits. Start
23552
- // the ACKed receiver-led negotiation before the legacy snapshot, but do not
23553
- // hold mixed-version replication discovery behind that round trip. Attach
23554
- // the rejection handler now because a lifecycle fence below may return before
23555
- // awaiting the advert. RequestV2 is armed only after both sends settle.
23775
+ // Decode, sender and authenticated apply readiness are separate bits. An
23776
+ // ACKed capability advert is the local half of receiver-led negotiation;
23777
+ // readiness is promoted only after the legacy startup path has completed.
23778
+ // This preserves mixed-version discovery without putting capability ACK
23779
+ // latency on the subscription callback's critical path.
23556
23780
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23557
23781
  const localCapabilityAdvertisement =
23558
- this.advertiseReplicationInfoV2ReceiveCapability({
23782
+ this._v2Receive.advertiseLocalCapability({
23559
23783
  target: publicKey,
23560
23784
  peerSession: expectedSubscriptionEpoch,
23561
23785
  receiveEpoch,
23562
23786
  signal: replicationLifecycleController.signal,
23563
- }).catch((error) => {
23564
- this.handleReplicationLifecycleSendError(
23565
- error,
23566
- replicationLifecycleController,
23567
- );
23568
- return undefined;
23569
23787
  });
23788
+ if (!this.legacyReplicationInfoEnabled) {
23789
+ // Current logs have no legacy startup work to order ahead of RequestV2.
23790
+ // Releasing is synchronous and exact-session fenced; the ACK may still
23791
+ // arrive later and promote readiness through the coordinator.
23792
+ localCapabilityAdvertisement.releaseLegacyBarrier();
23793
+ this.scheduleReplicationInfoV2Recovery(
23794
+ publicKey,
23795
+ replicationLifecycleController,
23796
+ );
23797
+ return;
23798
+ }
23570
23799
 
23571
- let replicationSegments: ReplicationRangeIndexable<R>[];
23572
23800
  try {
23573
- replicationSegments = await this.getMyReplicationSegments();
23574
- } catch (error) {
23575
- if (
23576
- !this.isReplicationLifecycleActive(replicationLifecycleController) &&
23577
- isNotStartedError(error as Error)
23578
- ) {
23579
- return;
23801
+ let replicationSegments: ReplicationRangeIndexable<R>[];
23802
+ try {
23803
+ replicationSegments = await this.getMyReplicationSegments();
23804
+ } catch (error) {
23805
+ if (
23806
+ !this.isReplicationLifecycleActive(replicationLifecycleController) &&
23807
+ isNotStartedError(error as Error)
23808
+ ) {
23809
+ return;
23810
+ }
23811
+ throw error;
23580
23812
  }
23581
- throw error;
23582
- }
23583
- if (
23584
- !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23585
- !ownsSubscriptionEpoch()
23586
- ) {
23587
- return;
23588
- }
23589
- if (replicationSegments.length > 0) {
23590
- const segments = replicationSegments.map((x) => x.toReplicationRange());
23591
- this.validatePersistedReplicationRangeSnapshot(segments);
23592
- await this.rpc
23593
- .send(
23594
- new AllReplicatingSegmentsMessage({
23595
- segments,
23596
- }),
23597
- {
23598
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] }),
23599
- signal: replicationLifecycleController.signal,
23600
- },
23601
- )
23602
- .catch((error) =>
23603
- this.handleReplicationLifecycleSendError(
23604
- error,
23605
- replicationLifecycleController,
23606
- ),
23607
- );
23608
23813
  if (
23609
23814
  !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23610
23815
  !ownsSubscriptionEpoch()
23611
23816
  ) {
23612
23817
  return;
23613
23818
  }
23614
-
23615
- if (this.v8Behaviour) {
23616
- // for backwards compatibility
23819
+ if (replicationSegments.length > 0) {
23820
+ const segments = replicationSegments.map((x) => x.toReplicationRange());
23821
+ this.validatePersistedReplicationRangeSnapshot(segments);
23617
23822
  await this.rpc
23618
23823
  .send(
23619
- new ResponseRoleMessage({
23620
- role: this.getRoleFromReplicationSegments(replicationSegments),
23824
+ new AllReplicatingSegmentsMessage({
23825
+ segments,
23621
23826
  }),
23622
23827
  {
23623
23828
  mode: new AcknowledgeDelivery({
@@ -23633,30 +23838,51 @@ export class SharedLog<
23633
23838
  replicationLifecycleController,
23634
23839
  ),
23635
23840
  );
23636
- }
23637
- }
23841
+ if (
23842
+ !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23843
+ !ownsSubscriptionEpoch()
23844
+ ) {
23845
+ return;
23846
+ }
23638
23847
 
23639
- // Keep legacy request-based discovery independent of the capability ACK too.
23640
- // This makes mixed-version joins resilient to timing-sensitive delivery/order
23641
- // issues where we may miss the remote peer's initial announcement.
23642
- if (
23643
- this.isReplicationLifecycleActive(replicationLifecycleController) &&
23644
- ownsSubscriptionEpoch()
23645
- ) {
23646
- this.scheduleReplicationInfoRequests(
23647
- publicKey,
23648
- replicationLifecycleController,
23649
- );
23650
- }
23848
+ if (this.v8Behaviour) {
23849
+ // for backwards compatibility
23850
+ await this.rpc
23851
+ .send(
23852
+ new ResponseRoleMessage({
23853
+ role: this.getRoleFromReplicationSegments(replicationSegments),
23854
+ }),
23855
+ {
23856
+ mode: new AcknowledgeDelivery({
23857
+ redundancy: 1,
23858
+ to: [publicKey],
23859
+ }),
23860
+ signal: replicationLifecycleController.signal,
23861
+ },
23862
+ )
23863
+ .catch((error) =>
23864
+ this.handleReplicationLifecycleSendError(
23865
+ error,
23866
+ replicationLifecycleController,
23867
+ ),
23868
+ );
23869
+ }
23870
+ }
23651
23871
 
23652
- const localCapabilityReady = await localCapabilityAdvertisement;
23653
- if (localCapabilityReady) {
23654
- this._v2Receive.markLocalCapabilityReady({
23655
- peerHash,
23656
- peerSession: expectedSubscriptionEpoch,
23657
- receiveEpoch,
23658
- ...localCapabilityReady,
23659
- });
23872
+ // Keep legacy request-based discovery independent of the capability ACK.
23873
+ // This makes mixed-version joins resilient to timing-sensitive delivery/order
23874
+ // issues where we may miss the remote peer's initial announcement.
23875
+ if (
23876
+ this.isReplicationLifecycleActive(replicationLifecycleController) &&
23877
+ ownsSubscriptionEpoch()
23878
+ ) {
23879
+ this.scheduleReplicationInfoRequests(
23880
+ publicKey,
23881
+ replicationLifecycleController,
23882
+ );
23883
+ }
23884
+ } finally {
23885
+ localCapabilityAdvertisement.releaseLegacyBarrier();
23660
23886
  }
23661
23887
  }
23662
23888
 
@@ -25098,6 +25324,7 @@ export class SharedLog<
25098
25324
  evt.detail.topics,
25099
25325
  true,
25100
25326
  subscriptionEpoch,
25327
+ evt.detail.session,
25101
25328
  );
25102
25329
  }
25103
25330