@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/dist/src/index.js CHANGED
@@ -1701,6 +1701,7 @@ let SharedLog = (() => {
1701
1701
  queueCurrentReplicationStateAnnouncementRepair: () => this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
1702
1702
  queueCurrentReplicationStateAnnouncementRetry: (error) => this._announcements.queueCurrentReplicationStateAnnouncementRetry(error),
1703
1703
  enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
1704
+ isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
1704
1705
  isClosed: () => this.closed,
1705
1706
  getCloseSignal: () => this._closeController.signal,
1706
1707
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
@@ -1723,6 +1724,7 @@ let SharedLog = (() => {
1723
1724
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
1724
1725
  validatePersistedReplicationRangeSnapshot: (ranges) => this.validatePersistedReplicationRangeSnapshot(ranges),
1725
1726
  isClosed: () => this.closed,
1727
+ isPeerSessionCurrent: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession),
1726
1728
  isPeerSessionOpen: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession) &&
1727
1729
  peerSession.phase === "open" &&
1728
1730
  !this._peerSessions.isReplicationInfoBlocked(peerHash) &&
@@ -1773,6 +1775,10 @@ let SharedLog = (() => {
1773
1775
  getSelfKey: () => this.node.identity.publicKey,
1774
1776
  getReceiverTransportSession: () => BigInt(this.node.services.pubsub.session),
1775
1777
  isClosed: () => this.closed,
1778
+ isPeerSessionCurrent: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession) &&
1779
+ peerSession.phase === "open" &&
1780
+ peerSession.isActive(),
1781
+ isReceiveEpochCurrent: (peerHash, receiveEpoch) => this._peerSessions.isReceiveEpochCurrent(peerHash, receiveEpoch),
1776
1782
  isPeerStateCurrent: (peerHash, peerSession, receiveEpoch) => this._peerSessions.isCurrent(peerHash, peerSession) &&
1777
1783
  peerSession.phase === "open" &&
1778
1784
  peerSession.isActive() &&
@@ -1801,6 +1807,7 @@ let SharedLog = (() => {
1801
1807
  }
1802
1808
  logger.trace(`Replication-info V2 recovery request failed: ${error?.message ?? String(error)}`);
1803
1809
  },
1810
+ onLocalCapabilityError: (error) => this.handleReplicationLifecycleSendError(error),
1804
1811
  });
1805
1812
  }
1806
1813
  createReplicatorLivenessMonitor() {
@@ -1968,6 +1975,13 @@ let SharedLog = (() => {
1968
1975
  get compatibility() {
1969
1976
  return this._logProperties?.compatibility;
1970
1977
  }
1978
+ /**
1979
+ * Legacy replication-info is an explicit compatibility fallback. Current
1980
+ * logs never infer or re-enable it from a remote peer's capabilities.
1981
+ */
1982
+ get legacyReplicationInfoEnabled() {
1983
+ return this.compatibility !== undefined && this.compatibility < 10;
1984
+ }
1971
1985
  get isAdaptiveReplicating() {
1972
1986
  return this._isAdaptiveReplicating;
1973
1987
  }
@@ -4039,10 +4053,16 @@ let SharedLog = (() => {
4039
4053
  // deliver the authoritative Full that completes recovery.
4040
4054
  const peerSession = this._peerSessions.current(keyHash);
4041
4055
  if (peerSession?.phase === "open") {
4056
+ const receiveEpoch = this._peerSessions.receiveEpoch(keyHash);
4042
4057
  this._v2Receive.advanceRecovery({
4043
4058
  peerHash: keyHash,
4044
4059
  peerSession,
4045
- receiveEpoch: this._peerSessions.receiveEpoch(keyHash),
4060
+ receiveEpoch,
4061
+ });
4062
+ this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
4063
+ peerHash: keyHash,
4064
+ peerSession,
4065
+ receiveEpoch,
4046
4066
  });
4047
4067
  }
4048
4068
  }
@@ -9096,7 +9116,9 @@ let SharedLog = (() => {
9096
9116
  this._logProperties = options;
9097
9117
  this.domain = options?.domain
9098
9118
  ? options.domain(this)
9099
- : createReplicationDomainHash(options?.compatibility && options?.compatibility < 10 ? "u32" : "u64")(this);
9119
+ : createReplicationDomainHash(options?.compatibility !== undefined && options.compatibility < 10
9120
+ ? "u32"
9121
+ : "u64")(this);
9100
9122
  this.indexableDomain = createIndexableDomainFromResolution(this.domain.resolution);
9101
9123
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
9102
9124
  this._checkedPrune = new CheckedPruneCoordinator();
@@ -9214,8 +9236,13 @@ let SharedLog = (() => {
9214
9236
  throw new Error("waitForReplicatorRequestMaxAttempts must be a positive number");
9215
9237
  }
9216
9238
  this._closeController = new AbortController();
9217
- this._announcements.setupReplicationAnnouncementRetryFunction();
9218
- this._announcements.setupReplicationAnnouncementRepairFunction();
9239
+ if (this.legacyReplicationInfoEnabled) {
9240
+ this._announcements.setupReplicationAnnouncementRetryFunction();
9241
+ this._announcements.setupReplicationAnnouncementRepairFunction();
9242
+ }
9243
+ else {
9244
+ this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
9245
+ }
9219
9246
  this._closeController.signal.addEventListener("abort", () => {
9220
9247
  for (const [_peer, state] of this._replicationInfoRequestByPeer) {
9221
9248
  if (state.timer)
@@ -10134,7 +10161,12 @@ let SharedLog = (() => {
10134
10161
  }
10135
10162
  async afterOpen() {
10136
10163
  await super.afterOpen();
10137
- const existingSubscribersPromise = this._getTopicSubscribers(this.topic);
10164
+ // Start the broader discovery eagerly, in parallel with rebalance, for its
10165
+ // routing/cache side effects. It also contains connected/provider/fanout
10166
+ // candidates that have not subscribed to this log, so only the authoritative
10167
+ // pubsub snapshot below may create subscription fallback sessions.
10168
+ const subscriberDiscoveryPromise = this._getTopicSubscribers(this.topic);
10169
+ const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(this.topic);
10138
10170
  const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
10139
10171
  // We do this here, because these calls requires this.closed == false
10140
10172
  void this.pruneOfflineReplicators()
@@ -10151,6 +10183,7 @@ let SharedLog = (() => {
10151
10183
  });
10152
10184
  this._liveness.startReplicatorLivenessSweep();
10153
10185
  await this.rebalanceParticipation();
10186
+ await subscriberDiscoveryPromise;
10154
10187
  // Take into account existing subscription
10155
10188
  (await existingSubscribersPromise)?.forEach((v) => {
10156
10189
  if (v.equals(this.node.identity.publicKey)) {
@@ -10159,6 +10192,14 @@ let SharedLog = (() => {
10159
10192
  if (this.closed) {
10160
10193
  return;
10161
10194
  }
10195
+ // The live subscribe event and this after-open snapshot can report the
10196
+ // same initial transport generation. The live callback rotates its
10197
+ // PeerSession synchronously, so any current session here proves the
10198
+ // fallback is stale/duplicate. Rotating again would erase the signed
10199
+ // capability binding that the first callback just established.
10200
+ if (this._peerSessions.current(v.hashcode()) !== null) {
10201
+ return;
10202
+ }
10162
10203
  void this.runSubscriptionChangeCallback(() => this.handleSubscriptionChange(v, [this.topic], true));
10163
10204
  });
10164
10205
  }
@@ -10297,7 +10338,9 @@ let SharedLog = (() => {
10297
10338
  cleanupPeerDisconnectTracking(peerHash, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
10298
10339
  const peerSession = this._peerSessions.current(peerHash);
10299
10340
  const preserveV2Session = peerSession?.phase === "open" && peerSession.isActive();
10300
- this.cancelReplicationInfoRequests(peerHash);
10341
+ if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
10342
+ this.cancelReplicationInfoRequests(peerHash);
10343
+ }
10301
10344
  this._liveness._replicatorLivenessFailures.delete(peerHash);
10302
10345
  this._liveness._replicatorLastActivityAt.delete(peerHash);
10303
10346
  if (!preserveV2Session) {
@@ -11257,15 +11300,16 @@ let SharedLog = (() => {
11257
11300
  }, 2_000);
11258
11301
  try {
11259
11302
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
11260
- await Promise.all([
11261
- this.rpc
11303
+ const resets = [this._v2Send.sendTerminalReset(abort.signal)];
11304
+ if (this.legacyReplicationInfoEnabled) {
11305
+ resets.push(this.rpc
11262
11306
  .send(reset, {
11263
11307
  priority: CONVERGENCE_MESSAGE_PRIORITY,
11264
11308
  signal: abort.signal,
11265
11309
  })
11266
- .catch(() => { }),
11267
- this._v2Send.sendTerminalReset(abort.signal),
11268
- ]);
11310
+ .catch(() => { }));
11311
+ }
11312
+ await Promise.all(resets);
11269
11313
  }
11270
11314
  finally {
11271
11315
  clearTimeout(abortTimer);
@@ -11385,15 +11429,16 @@ let SharedLog = (() => {
11385
11429
  }, 2_000);
11386
11430
  try {
11387
11431
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
11388
- await Promise.all([
11389
- this.rpc
11432
+ const resets = [this._v2Send.sendTerminalReset(abort.signal)];
11433
+ if (this.legacyReplicationInfoEnabled) {
11434
+ resets.push(this.rpc
11390
11435
  .send(reset, {
11391
11436
  priority: CONVERGENCE_MESSAGE_PRIORITY,
11392
11437
  signal: abort.signal,
11393
11438
  })
11394
- .catch(() => { }),
11395
- this._v2Send.sendTerminalReset(abort.signal),
11396
- ]);
11439
+ .catch(() => { }));
11440
+ }
11441
+ await Promise.all(resets);
11397
11442
  }
11398
11443
  finally {
11399
11444
  clearTimeout(abortTimer);
@@ -11817,6 +11862,17 @@ let SharedLog = (() => {
11817
11862
  if (!context.from) {
11818
11863
  throw new Error("Missing from in update role message");
11819
11864
  }
11865
+ if (!this.legacyReplicationInfoEnabled &&
11866
+ (msg instanceof RequestReplicationInfoMessage ||
11867
+ msg instanceof ResponseRoleMessage ||
11868
+ msg instanceof AllReplicatingSegmentsMessage ||
11869
+ msg instanceof AddedReplicationSegmentMessage ||
11870
+ msg instanceof StoppedReplicating)) {
11871
+ // These variants remain registered decode tombstones, but current logs
11872
+ // fail closed before leases, synchronizer work, liveness, watermarks or
11873
+ // mutations. Only an explicit pre-v10 compatibility open admits them.
11874
+ return;
11875
+ }
11820
11876
  // Snapshot receive ownership before any async handler gets a chance to
11821
11877
  // yield. Replication-info messages reach their branch only after the
11822
11878
  // synchronizer declines them, and a U/S transition can happen meanwhile.
@@ -13231,14 +13287,30 @@ let SharedLog = (() => {
13231
13287
  openingSession: receiveSession,
13232
13288
  });
13233
13289
  }
13234
- else if (this.observePeerSyncCapabilities({
13235
- peerHash: receiveFromHash,
13236
- capabilities: msg.capabilities,
13237
- transportSession: capabilityTransportSession,
13238
- timestamp: capabilityTimestamp,
13239
- }) &&
13240
- receiveSession?.phase === "open") {
13241
- this.promoteReplicationInfoV2ReceiveCapability(context.from, receiveSession);
13290
+ else {
13291
+ const observed = this.observePeerSyncCapabilities({
13292
+ peerHash: receiveFromHash,
13293
+ capabilities: msg.capabilities,
13294
+ transportSession: capabilityTransportSession,
13295
+ timestamp: capabilityTimestamp,
13296
+ });
13297
+ if (observed && receiveSession?.phase === "open") {
13298
+ this.promoteReplicationInfoV2ReceiveCapability(context.from, receiveSession);
13299
+ }
13300
+ else if (observed && receiveSession === null) {
13301
+ // A capability can arrive before the sender's topic Subscribe after
13302
+ // reconnect. Ask that authenticated peer for its authoritative
13303
+ // subscriber snapshot; the resulting Subscribe creates the real
13304
+ // PeerSession and completes the symmetric capability handshake.
13305
+ // Never synthesize membership from capability traffic alone.
13306
+ void Promise.resolve()
13307
+ .then(() => this.node.services.pubsub.requestSubscribers(this.topic, context.from))
13308
+ .catch((error) => {
13309
+ if (!isNotStartedError(error)) {
13310
+ logger.error(error?.toString?.() ?? String(error));
13311
+ }
13312
+ });
13313
+ }
13242
13314
  }
13243
13315
  }
13244
13316
  return;
@@ -13667,7 +13739,8 @@ let SharedLog = (() => {
13667
13739
  return;
13668
13740
  }
13669
13741
  this._liveness.markReplicatorActivity(fromHash);
13670
- if (msg instanceof FullReplicationInfoV2Message) {
13742
+ if (msg instanceof FullReplicationInfoV2Message &&
13743
+ this.legacyReplicationInfoEnabled) {
13671
13744
  this.cancelReplicationInfoRequests(fromHash);
13672
13745
  }
13673
13746
  });
@@ -14171,9 +14244,30 @@ let SharedLog = (() => {
14171
14244
  reject(new TimeoutError(`Timeout waiting for replicator ${key.hashcode()}`));
14172
14245
  }, timeoutMs);
14173
14246
  let requestAttempts = 0;
14247
+ let subscriberSnapshotInFlight;
14174
14248
  const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
14175
14249
  const maxRequestAttempts = this.waitForReplicatorRequestMaxAttempts ??
14176
14250
  Math.max(WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS, Math.ceil(timeoutMs / requestIntervalMs));
14251
+ const requestSubscriberSnapshot = () => {
14252
+ if (subscriberSnapshotInFlight) {
14253
+ return;
14254
+ }
14255
+ subscriberSnapshotInFlight = Promise.resolve()
14256
+ .then(async () => {
14257
+ if (settled || this.closed) {
14258
+ return;
14259
+ }
14260
+ await this.node.services.pubsub.requestSubscribers(this.topic, key);
14261
+ })
14262
+ .catch((error) => {
14263
+ if (!isNotStartedError(error)) {
14264
+ logger.error(error?.toString?.() ?? String(error));
14265
+ }
14266
+ })
14267
+ .finally(() => {
14268
+ subscriberSnapshotInFlight = undefined;
14269
+ });
14270
+ };
14177
14271
  const requestReplicationInfo = () => {
14178
14272
  if (settled || this.closed) {
14179
14273
  return;
@@ -14182,17 +14276,37 @@ let SharedLog = (() => {
14182
14276
  return;
14183
14277
  }
14184
14278
  requestAttempts++;
14185
- this.rpc
14186
- .send(new RequestReplicationInfoMessage(), {
14187
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
14188
- })
14189
- .catch((e) => {
14190
- // Best-effort: missing peers / unopened RPC should not fail the wait logic.
14191
- if (isNotStartedError(e)) {
14192
- return;
14279
+ if (this.legacyReplicationInfoEnabled) {
14280
+ this.rpc
14281
+ .send(new RequestReplicationInfoMessage(), {
14282
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
14283
+ })
14284
+ .catch((e) => {
14285
+ // Best-effort: missing peers / unopened RPC should not fail the wait logic.
14286
+ if (isNotStartedError(e)) {
14287
+ return;
14288
+ }
14289
+ logger.error(e?.toString?.() ?? String(e));
14290
+ });
14291
+ }
14292
+ else {
14293
+ const peerHash = key.hashcode();
14294
+ const peerSession = this._peerSessions.current(peerHash);
14295
+ if (peerSession?.phase === "open") {
14296
+ this._v2Receive.resumeParkedRequest({
14297
+ peerHash,
14298
+ peerSession,
14299
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
14300
+ });
14193
14301
  }
14194
- logger.error(e?.toString?.() ?? String(e));
14195
- });
14302
+ else if (peerSession === null || peerSession.phase === "departing") {
14303
+ // A peer can be known to routing before its SharedLog topic
14304
+ // subscription has been observed. Legacy requests used to bootstrap
14305
+ // that case directly; V2 needs an authoritative Subscribe snapshot
14306
+ // before it can create a fenced PeerSession and request a Full.
14307
+ requestSubscriberSnapshot();
14308
+ }
14309
+ }
14196
14310
  if (requestAttempts < maxRequestAttempts) {
14197
14311
  requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
14198
14312
  }
@@ -16004,12 +16118,71 @@ let SharedLog = (() => {
16004
16118
  }
16005
16119
  this._replicationInfoRequestByPeer.delete(peerHash);
16006
16120
  }
16121
+ scheduleReplicationInfoV2Recovery(peer, replicationLifecycleController = this._instanceLifecycle
16122
+ ?.membershipLifecycleController) {
16123
+ if (!replicationLifecycleController ||
16124
+ !this.isReplicationLifecycleActive(replicationLifecycleController)) {
16125
+ return;
16126
+ }
16127
+ const peerHash = peer.hashcode();
16128
+ const peerSession = this._peerSessions.current(peerHash);
16129
+ if (!peerSession || peerSession.phase !== "open") {
16130
+ return;
16131
+ }
16132
+ const requestStates = this._replicationInfoRequestByPeer;
16133
+ const existing = requestStates.get(peerHash);
16134
+ if (existing?.peerSession === peerSession) {
16135
+ return;
16136
+ }
16137
+ if (existing) {
16138
+ if (existing.timer) {
16139
+ clearTimeout(existing.timer);
16140
+ }
16141
+ requestStates.delete(peerHash);
16142
+ }
16143
+ const state = {
16144
+ attempts: 0,
16145
+ peerSession,
16146
+ };
16147
+ requestStates.set(peerHash, state);
16148
+ const cancel = () => {
16149
+ if (requestStates.get(peerHash) !== state) {
16150
+ return;
16151
+ }
16152
+ if (state.timer) {
16153
+ clearTimeout(state.timer);
16154
+ }
16155
+ requestStates.delete(peerHash);
16156
+ };
16157
+ const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
16158
+ const tick = () => {
16159
+ if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
16160
+ peerSession.phase !== "open" ||
16161
+ !this._peerSessions.isCurrent(peerHash, peerSession)) {
16162
+ cancel();
16163
+ return;
16164
+ }
16165
+ this._v2Receive.resumeParkedRequest({
16166
+ peerHash,
16167
+ peerSession,
16168
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
16169
+ });
16170
+ state.attempts++;
16171
+ state.timer = setTimeout(tick, intervalMs);
16172
+ state.timer.unref?.();
16173
+ };
16174
+ tick();
16175
+ }
16007
16176
  scheduleReplicationInfoRequests(peer, replicationLifecycleController = this._instanceLifecycle
16008
16177
  ?.membershipLifecycleController) {
16009
16178
  if (!replicationLifecycleController ||
16010
16179
  !this.isReplicationLifecycleActive(replicationLifecycleController)) {
16011
16180
  return;
16012
16181
  }
16182
+ if (!this.legacyReplicationInfoEnabled) {
16183
+ this.scheduleReplicationInfoV2Recovery(peer, replicationLifecycleController);
16184
+ return;
16185
+ }
16013
16186
  const peerHash = peer.hashcode();
16014
16187
  const requestStates = this._replicationInfoRequestByPeer;
16015
16188
  if (requestStates.has(peerHash)) {
@@ -16060,7 +16233,7 @@ let SharedLog = (() => {
16060
16233
  };
16061
16234
  tick();
16062
16235
  }
16063
- async handleSubscriptionChange(publicKey, topics, subscribed, subscriptionEpoch) {
16236
+ async handleSubscriptionChange(publicKey, topics, subscribed, subscriptionEpoch, subscriptionTransportSession) {
16064
16237
  if (!topics.includes(this.topic)) {
16065
16238
  return;
16066
16239
  }
@@ -16076,13 +16249,29 @@ let SharedLog = (() => {
16076
16249
  if (!ownsSubscriptionEpoch()) {
16077
16250
  return;
16078
16251
  }
16252
+ // A reconnect can arrive before the previous exact-session recovery tick
16253
+ // observes its stale session. Retire that job synchronously so it cannot
16254
+ // suppress the replacement session's scheduler in the shared peer slot.
16255
+ this.cancelReplicationInfoRequests(peerHash);
16079
16256
  // A destination stream is scoped to exactly one topic-subscription
16080
16257
  // session. Abort the predecessor synchronously before either barrier can
16081
16258
  // yield; a late queue completion must never enter the new session.
16082
16259
  this._v2Receive.clearPeer(peerHash);
16083
16260
  this._v2Send.clearPeer(peerHash);
16084
- this._peerSyncCapabilitySessions.delete(peerHash);
16085
- this._peerSyncCapabilityTimestamps.delete(peerHash);
16261
+ const capabilityTransportSession = this._peerSyncCapabilitySessions.get(peerHash);
16262
+ const canInheritCapability = subscribed &&
16263
+ (subscriptionTransportSession !== undefined
16264
+ ? capabilityTransportSession === subscriptionTransportSession
16265
+ : !expectedSubscriptionEpoch.hasPredecessor);
16266
+ if (!canInheritCapability) {
16267
+ // Departures and successor openings revoke the signed transport binding.
16268
+ // A live successor may inherit a capability that arrived just before its
16269
+ // Subscribe only when both signed frames belong to the same transport
16270
+ // generation. Snapshot fallbacks lack that proof, so only their first
16271
+ // opening may inherit pre-opening capability state.
16272
+ this._peerSyncCapabilitySessions.delete(peerHash);
16273
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
16274
+ }
16086
16275
  if (subscribed) {
16087
16276
  const pendingOpeningCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
16088
16277
  if (pendingOpeningCapabilities &&
@@ -16195,56 +16384,48 @@ let SharedLog = (() => {
16195
16384
  this._liveness.markReplicatorActivity(peerHash);
16196
16385
  this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
16197
16386
  this.promoteReplicationInfoV2ReceiveCapability(publicKey, expectedSubscriptionEpoch);
16198
- // Decode, sender and authenticated apply readiness are separate bits. Start
16199
- // the ACKed receiver-led negotiation before the legacy snapshot, but do not
16200
- // hold mixed-version replication discovery behind that round trip. Attach
16201
- // the rejection handler now because a lifecycle fence below may return before
16202
- // awaiting the advert. RequestV2 is armed only after both sends settle.
16387
+ // Decode, sender and authenticated apply readiness are separate bits. An
16388
+ // ACKed capability advert is the local half of receiver-led negotiation;
16389
+ // readiness is promoted only after the legacy startup path has completed.
16390
+ // This preserves mixed-version discovery without putting capability ACK
16391
+ // latency on the subscription callback's critical path.
16203
16392
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
16204
- const localCapabilityAdvertisement = this.advertiseReplicationInfoV2ReceiveCapability({
16393
+ const localCapabilityAdvertisement = this._v2Receive.advertiseLocalCapability({
16205
16394
  target: publicKey,
16206
16395
  peerSession: expectedSubscriptionEpoch,
16207
16396
  receiveEpoch,
16208
16397
  signal: replicationLifecycleController.signal,
16209
- }).catch((error) => {
16210
- this.handleReplicationLifecycleSendError(error, replicationLifecycleController);
16211
- return undefined;
16212
16398
  });
16213
- let replicationSegments;
16214
- try {
16215
- replicationSegments = await this.getMyReplicationSegments();
16216
- }
16217
- catch (error) {
16218
- if (!this.isReplicationLifecycleActive(replicationLifecycleController) &&
16219
- isNotStartedError(error)) {
16220
- return;
16221
- }
16222
- throw error;
16223
- }
16224
- if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
16225
- !ownsSubscriptionEpoch()) {
16399
+ if (!this.legacyReplicationInfoEnabled) {
16400
+ // Current logs have no legacy startup work to order ahead of RequestV2.
16401
+ // Releasing is synchronous and exact-session fenced; the ACK may still
16402
+ // arrive later and promote readiness through the coordinator.
16403
+ localCapabilityAdvertisement.releaseLegacyBarrier();
16404
+ this.scheduleReplicationInfoV2Recovery(publicKey, replicationLifecycleController);
16226
16405
  return;
16227
16406
  }
16228
- if (replicationSegments.length > 0) {
16229
- const segments = replicationSegments.map((x) => x.toReplicationRange());
16230
- this.validatePersistedReplicationRangeSnapshot(segments);
16231
- await this.rpc
16232
- .send(new AllReplicatingSegmentsMessage({
16233
- segments,
16234
- }), {
16235
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] }),
16236
- signal: replicationLifecycleController.signal,
16237
- })
16238
- .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
16407
+ try {
16408
+ let replicationSegments;
16409
+ try {
16410
+ replicationSegments = await this.getMyReplicationSegments();
16411
+ }
16412
+ catch (error) {
16413
+ if (!this.isReplicationLifecycleActive(replicationLifecycleController) &&
16414
+ isNotStartedError(error)) {
16415
+ return;
16416
+ }
16417
+ throw error;
16418
+ }
16239
16419
  if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
16240
16420
  !ownsSubscriptionEpoch()) {
16241
16421
  return;
16242
16422
  }
16243
- if (this.v8Behaviour) {
16244
- // for backwards compatibility
16423
+ if (replicationSegments.length > 0) {
16424
+ const segments = replicationSegments.map((x) => x.toReplicationRange());
16425
+ this.validatePersistedReplicationRangeSnapshot(segments);
16245
16426
  await this.rpc
16246
- .send(new ResponseRoleMessage({
16247
- role: this.getRoleFromReplicationSegments(replicationSegments),
16427
+ .send(new AllReplicatingSegmentsMessage({
16428
+ segments,
16248
16429
  }), {
16249
16430
  mode: new AcknowledgeDelivery({
16250
16431
  redundancy: 1,
@@ -16253,23 +16434,35 @@ let SharedLog = (() => {
16253
16434
  signal: replicationLifecycleController.signal,
16254
16435
  })
16255
16436
  .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
16437
+ if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
16438
+ !ownsSubscriptionEpoch()) {
16439
+ return;
16440
+ }
16441
+ if (this.v8Behaviour) {
16442
+ // for backwards compatibility
16443
+ await this.rpc
16444
+ .send(new ResponseRoleMessage({
16445
+ role: this.getRoleFromReplicationSegments(replicationSegments),
16446
+ }), {
16447
+ mode: new AcknowledgeDelivery({
16448
+ redundancy: 1,
16449
+ to: [publicKey],
16450
+ }),
16451
+ signal: replicationLifecycleController.signal,
16452
+ })
16453
+ .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
16454
+ }
16455
+ }
16456
+ // Keep legacy request-based discovery independent of the capability ACK.
16457
+ // This makes mixed-version joins resilient to timing-sensitive delivery/order
16458
+ // issues where we may miss the remote peer's initial announcement.
16459
+ if (this.isReplicationLifecycleActive(replicationLifecycleController) &&
16460
+ ownsSubscriptionEpoch()) {
16461
+ this.scheduleReplicationInfoRequests(publicKey, replicationLifecycleController);
16256
16462
  }
16257
16463
  }
16258
- // Keep legacy request-based discovery independent of the capability ACK too.
16259
- // This makes mixed-version joins resilient to timing-sensitive delivery/order
16260
- // issues where we may miss the remote peer's initial announcement.
16261
- if (this.isReplicationLifecycleActive(replicationLifecycleController) &&
16262
- ownsSubscriptionEpoch()) {
16263
- this.scheduleReplicationInfoRequests(publicKey, replicationLifecycleController);
16264
- }
16265
- const localCapabilityReady = await localCapabilityAdvertisement;
16266
- if (localCapabilityReady) {
16267
- this._v2Receive.markLocalCapabilityReady({
16268
- peerHash,
16269
- peerSession: expectedSubscriptionEpoch,
16270
- receiveEpoch,
16271
- ...localCapabilityReady,
16272
- });
16464
+ finally {
16465
+ localCapabilityAdvertisement.releaseLegacyBarrier();
16273
16466
  }
16274
16467
  }
16275
16468
  getClampedReplicas(customValue) {
@@ -17267,7 +17460,7 @@ let SharedLog = (() => {
17267
17460
  this.remoteBlocks.onReachable(evt.detail.from);
17268
17461
  this._peerSessions.blockReplicationInfo(fromHash);
17269
17462
  this.invalidateSharedLogTopicSubscribersCache();
17270
- await this.handleSubscriptionChange(evt.detail.from, evt.detail.topics, true, subscriptionEpoch);
17463
+ await this.handleSubscriptionChange(evt.detail.from, evt.detail.topics, true, subscriptionEpoch, evt.detail.session);
17271
17464
  }
17272
17465
  async rebalanceParticipation(ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(), rebalanceParticipationDebounced = this.rebalanceParticipationDebounced) {
17273
17466
  // Stage 3: the lifecycle owns all three identity terms. `lifecycle` may