@peerbit/shared-log 15.0.0 → 16.0.1

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.
Files changed (53) hide show
  1. package/dist/src/checked-prune.d.ts.map +1 -1
  2. package/dist/src/checked-prune.js +12 -0
  3. package/dist/src/checked-prune.js.map +1 -1
  4. package/dist/src/index.d.ts +7 -13
  5. package/dist/src/index.d.ts.map +1 -1
  6. package/dist/src/index.js +81 -598
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/instance-lifecycle.d.ts.map +1 -1
  9. package/dist/src/instance-lifecycle.js +9 -0
  10. package/dist/src/instance-lifecycle.js.map +1 -1
  11. package/dist/src/peer-session.d.ts.map +1 -1
  12. package/dist/src/peer-session.js +9 -0
  13. package/dist/src/peer-session.js.map +1 -1
  14. package/dist/src/pid.d.ts +0 -2
  15. package/dist/src/pid.d.ts.map +1 -1
  16. package/dist/src/pid.js +0 -5
  17. package/dist/src/pid.js.map +1 -1
  18. package/dist/src/replication-announcement.d.ts +3 -106
  19. package/dist/src/replication-announcement.d.ts.map +1 -1
  20. package/dist/src/replication-announcement.js +0 -521
  21. package/dist/src/replication-announcement.js.map +1 -1
  22. package/dist/src/replication-info-v2-receive.d.ts +14 -43
  23. package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
  24. package/dist/src/replication-info-v2-receive.js +24 -199
  25. package/dist/src/replication-info-v2-receive.js.map +1 -1
  26. package/dist/src/replication-info-v2-send.d.ts +0 -1
  27. package/dist/src/replication-info-v2-send.d.ts.map +1 -1
  28. package/dist/src/replication-info-v2-send.js +0 -6
  29. package/dist/src/replication-info-v2-send.js.map +1 -1
  30. package/dist/src/replication.d.ts +0 -1
  31. package/dist/src/replication.d.ts.map +1 -1
  32. package/dist/src/replication.js +2 -17
  33. package/dist/src/replication.js.map +1 -1
  34. package/dist/src/role.d.ts +0 -4
  35. package/dist/src/role.d.ts.map +1 -1
  36. package/dist/src/role.js +0 -9
  37. package/dist/src/role.js.map +1 -1
  38. package/dist/src/sync/factory.d.ts +0 -1
  39. package/dist/src/sync/factory.d.ts.map +1 -1
  40. package/dist/src/sync/factory.js +20 -36
  41. package/dist/src/sync/factory.js.map +1 -1
  42. package/package.json +8 -8
  43. package/src/checked-prune.ts +12 -0
  44. package/src/index.ts +104 -817
  45. package/src/instance-lifecycle.ts +9 -0
  46. package/src/peer-session.ts +9 -0
  47. package/src/pid.ts +0 -6
  48. package/src/replication-announcement.ts +8 -739
  49. package/src/replication-info-v2-receive.ts +25 -271
  50. package/src/replication-info-v2-send.ts +0 -7
  51. package/src/replication.ts +1 -20
  52. package/src/role.ts +0 -13
  53. package/src/sync/factory.ts +24 -38
package/src/index.ts CHANGED
@@ -231,10 +231,7 @@ import {
231
231
  shouldAssigneToRangeBoundary as shouldAssignToRangeBoundary,
232
232
  toRebalance,
233
233
  } from "./ranges.js";
234
- import {
235
- ReplicationAnnouncementCoordinator,
236
- isTransientReplicationAnnouncementError,
237
- } from "./replication-announcement.js";
234
+ import { ReplicationAnnouncementCoordinator } from "./replication-announcement.js";
238
235
  import {
239
236
  type ReplicationDomainHash,
240
237
  createReplicationDomainHash,
@@ -277,7 +274,6 @@ import {
277
274
  maxReplicas,
278
275
  } from "./replication.js";
279
276
  import { ReplicatorLivenessMonitor } from "./replicator-liveness.js";
280
- import { Observer, Replicator } from "./role.js";
281
277
  import { createSyncronizer } from "./sync/factory.js";
282
278
  import type {
283
279
  SharedLogNativeWireSync,
@@ -2021,7 +2017,6 @@ export class SharedLog<
2021
2017
 
2022
2018
  uniqueReplicators!: Set<string>;
2023
2019
  private _replicatorJoinEmitted!: Set<string>;
2024
- private _replicatorsReconciled!: boolean;
2025
2020
 
2026
2021
  /* private _totalParticipation!: number; */
2027
2022
 
@@ -2078,34 +2073,26 @@ export class SharedLog<
2078
2073
  // public key hash to range id to range
2079
2074
  pendingMaturity!: Map<string, Map<string, PendingMaturityRecord<R>>>; // map of peerId to timeout
2080
2075
 
2081
- // Stage-4 KEEP-OLD verdict (fence B8, split by role). The watermark's
2082
- // FENCING role rejecting late replication-info across unsubscribe and
2083
- // eviction races is fully subsumed by the per-peer receive epoch plus
2084
- // the blocked set and session identity: every unsubscribe-path `now` write
2085
- // is preceded in the same synchronous block by a blocked-add, so an
2086
- // admitted handler can never observe one. Its intra-epoch ORDERING role is
2087
- // NOT subsumed: within one (lifecycle, session, epoch, unblocked) regime
2088
- // the epoch token is constant across every message from the peer, so only
2089
- // the two apply-lane timestamp comparisons can drop an older reset
2090
- // delivered after a newer add (unordered pubsub / retransmits) — an
2091
- // identity token carries no order. Deletion is blocked until
2092
- // replication-info messages carry sender-authoritative sequence numbers
2093
- // (stage-5 schema change); the `receive admission replication-info
2094
- // ordering watermark` pins fail if the read sites are removed before then.
2095
- private latestReplicationInfoMessage!: Map<string, bigint>;
2076
+ // The legacy replication-info ordering watermark (fence B8) is deleted:
2077
+ // its FENCING role was subsumed by the per-peer receive epoch, blocked set
2078
+ // and session identity, and its intra-epoch ORDERING role existed only for
2079
+ // the legacy apply lanes. The V2 lane orders by sender-authoritative
2080
+ // sequence numbers, and legacy frames are dropped unconditionally at the
2081
+ // B1 gate before any side effect.
2096
2082
  // The replication-info blocked set (fence B5) lives on the peer-session
2097
2083
  // registry: unsubscribed peers whose replication-info is ignored until a
2098
2084
  // reconnect barrier commits. See PeerSessionRegistry._replicationInfoBlockedPeers.
2085
+ // V2 recovery scheduler state, one row per open peer session (B12: the
2086
+ // legacy request scheduler that shared this map is deleted).
2099
2087
  private _replicationInfoRequestByPeer!: Map<
2100
2088
  string,
2101
2089
  {
2102
- // Legacy scheduler: sends issued (bounded by maxAttempts). V2 recovery
2103
- // scheduler: consecutive fruitless unparks the escalation exponent
2104
- // for the next unpark delay, reset on any applied V2 progress.
2090
+ // Consecutive fruitless unparks the escalation exponent for the
2091
+ // next unpark delay, reset on any applied V2 progress.
2105
2092
  attempts: number;
2106
2093
  timer?: ReturnType<typeof setTimeout>;
2107
- peerSession?: PeerSession;
2108
- // V2 recovery scheduler only: when the current park was first observed.
2094
+ peerSession: PeerSession;
2095
+ // When the current park was first observed.
2109
2096
  parkedSinceMs?: number;
2110
2097
  }
2111
2098
  >;
@@ -2266,7 +2253,6 @@ export class SharedLog<
2266
2253
  for (const hash of this._checkedPrune?.retries.keys() ?? []) {
2267
2254
  this._checkedPrune.clearRetry(hash);
2268
2255
  }
2269
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
2270
2256
  this.joinWarmup.cancelAllJoinWarmupTargets();
2271
2257
  for (const timer of this._repairRetryTimers) {
2272
2258
  clearTimeout(timer);
@@ -3268,7 +3254,7 @@ export class SharedLog<
3268
3254
  private rebalanceParticipationDebounced:
3269
3255
  | ReturnType<typeof debounceFixedInterval>
3270
3256
  | undefined;
3271
- private _announcements!: ReplicationAnnouncementCoordinator<R>;
3257
+ private _announcements!: ReplicationAnnouncementCoordinator;
3272
3258
  private _v2Receive!: ReplicationInfoV2ReceiveCoordinator;
3273
3259
  private _v2Send!: ReplicationInfoV2SendCoordinator<R>;
3274
3260
 
@@ -3396,37 +3382,13 @@ export class SharedLog<
3396
3382
  });
3397
3383
  }
3398
3384
 
3399
- private createReplicationAnnouncementCoordinator(): ReplicationAnnouncementCoordinator<R> {
3400
- return new ReplicationAnnouncementCoordinator<R>({
3401
- // Route re-entrant queueing through the owner so coordinator spies keep
3402
- // observing it (the poison guard assertions in events.spec.ts depend on
3403
- // this).
3404
- queueCurrentReplicationStateAnnouncementRepair: () =>
3405
- this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
3406
- queueCurrentReplicationStateAnnouncementRetry: (error: unknown) =>
3407
- this._announcements.queueCurrentReplicationStateAnnouncementRetry(
3408
- error,
3409
- ),
3385
+ private createReplicationAnnouncementCoordinator(): ReplicationAnnouncementCoordinator {
3386
+ return new ReplicationAnnouncementCoordinator({
3410
3387
  enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
3411
- isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
3412
- isClosed: () => this.closed,
3413
- getCloseSignal: () => this._closeController.signal,
3414
- getMyReplicationSegments: () => this.getMyReplicationSegments(),
3415
- validatePersistedReplicationRangeSnapshot: (ranges) =>
3416
- this.validatePersistedReplicationRangeSnapshot(ranges),
3417
- getSubscribers: () =>
3418
- this.node.services.pubsub.getSubscribers(this.topic),
3419
- getSelfHash: () => this.node.identity.publicKey.hashcode(),
3420
- isBlockedPeer: (hash) =>
3421
- this._peerSessions.isReplicationInfoBlocked(hash),
3422
- getRpc: () => this.rpc,
3423
3388
  captureReplicationOwnershipLifecycle: () =>
3424
3389
  this.captureReplicationOwnershipLifecycle(),
3425
3390
  throwIfReplicationOwnershipLifecycleInactive: (controller) =>
3426
3391
  this.throwIfReplicationOwnershipLifecycleInactive(controller),
3427
- isAdaptiveReplicating: () => this._isAdaptiveReplicating,
3428
- callRebalanceParticipationDebounced: () =>
3429
- this.rebalanceParticipationDebounced?.call(),
3430
3392
  });
3431
3393
  }
3432
3394
 
@@ -3653,7 +3615,6 @@ export class SharedLog<
3653
3615
  this._admittedPruneRemoves = new Set();
3654
3616
  this._pendingIHave = new Map();
3655
3617
  this._pendingIHaveCallbacks = new Set();
3656
- this.latestReplicationInfoMessage = new Map();
3657
3618
  this._replicationInfoRequestByPeer = new Map();
3658
3619
  this._subscriberSnapshotRequestsByPeer = new Map();
3659
3620
  this._replicationInfoApplyQueueByPeer = new Map();
@@ -3696,7 +3657,6 @@ export class SharedLog<
3696
3657
  this.recentlyRebalanced = new Cache<string>({ max: 1e4, ttl: 1e5 });
3697
3658
  this.uniqueReplicators = new Set();
3698
3659
  this._replicatorJoinEmitted = new Set();
3699
- this._replicatorsReconciled = false;
3700
3660
  this._liveness = this.createReplicatorLivenessMonitor();
3701
3661
  this._v2Receive = this.createReplicationInfoV2ReceiveCoordinator();
3702
3662
  this._v2Send = this.createReplicationInfoV2SendCoordinator();
@@ -3764,29 +3724,10 @@ export class SharedLog<
3764
3724
  this._nativeStrictDurableTransactionsClosing ??= false;
3765
3725
  }
3766
3726
 
3767
- get compatibility(): number | undefined {
3768
- // B12: the open option was removed and any defined value rejects at
3769
- // open(); this is permanently undefined and dies with the residual
3770
- // gates in a later cleanup stage.
3771
- return (this._logProperties as any)?.compatibility;
3772
- }
3773
-
3774
- /**
3775
- * Legacy replication-info is an explicit compatibility fallback. Current
3776
- * logs never infer or re-enable it from a remote peer's capabilities.
3777
- */
3778
- private get legacyReplicationInfoEnabled(): boolean {
3779
- return this.compatibility !== undefined && this.compatibility < 10;
3780
- }
3781
-
3782
3727
  get isAdaptiveReplicating() {
3783
3728
  return this._isAdaptiveReplicating;
3784
3729
  }
3785
3730
 
3786
- private get v8Behaviour() {
3787
- return (this.compatibility ?? Number.MAX_VALUE) < 9;
3788
- }
3789
-
3790
3731
  private getFanoutChannelOptions(
3791
3732
  options?: SharedLogFanoutOptions,
3792
3733
  ): Omit<FanoutTreeChannelOptions, "role"> {
@@ -4691,8 +4632,7 @@ export class SharedLog<
4691
4632
  (leaders.size === 0 || (leaders.size === 1 && leaders.has(selfHash)))
4692
4633
  ) {
4693
4634
  const allowSubscriberFallback =
4694
- this.syncronizer instanceof SimpleSyncronizer ||
4695
- (this.compatibility ?? Number.MAX_VALUE) < 10;
4635
+ this.syncronizer instanceof SimpleSyncronizer;
4696
4636
  if (!allowSubscriberFallback) {
4697
4637
  return;
4698
4638
  }
@@ -4841,8 +4781,7 @@ export class SharedLog<
4841
4781
  const set = new Set(leaders.keys());
4842
4782
  let hasRemotePeers = set.has(selfHash) ? set.size > 1 : set.size > 0;
4843
4783
  const allowSubscriberFallback =
4844
- this.syncronizer instanceof SimpleSyncronizer ||
4845
- (this.compatibility ?? Number.MAX_VALUE) < 10;
4784
+ this.syncronizer instanceof SimpleSyncronizer;
4846
4785
  if (!hasRemotePeers && allowSubscriberFallback) {
4847
4786
  try {
4848
4787
  const subscribers = await this._getTopicSubscribers(this.topic);
@@ -5305,28 +5244,6 @@ export class SharedLog<
5305
5244
  };
5306
5245
  }
5307
5246
 
5308
- // @deprecated
5309
- private getRoleFromReplicationSegments(
5310
- segments: ReplicationRangeIndexable<R>[],
5311
- ) {
5312
- if (segments.length > 1) {
5313
- throw new Error(
5314
- "More than one replication segment found. Can only use one segment for compatbility with v8",
5315
- );
5316
- }
5317
-
5318
- if (segments.length > 0) {
5319
- const segment = segments[0].toReplicationRange();
5320
- return new Replicator({
5321
- factor: (segment.factor as number) / MAX_U32,
5322
- offset: (segment.offset as number) / MAX_U32,
5323
- });
5324
- }
5325
-
5326
- // TODO this is not accurate but might be good enough
5327
- return new Observer();
5328
- }
5329
-
5330
5247
  private isTerminating() {
5331
5248
  return (
5332
5249
  this.acceptsParentAttachments === false ||
@@ -5604,12 +5521,7 @@ export class SharedLog<
5604
5521
  }
5605
5522
 
5606
5523
  private onRebalanceParticipationError(error: Error): void {
5607
- if (
5608
- this.closed ||
5609
- isNotStartedError(error) ||
5610
- (isTransientReplicationAnnouncementError(error) &&
5611
- this._announcements._replicationAnnouncementRetryPending)
5612
- ) {
5524
+ if (this.closed || isNotStartedError(error)) {
5613
5525
  return;
5614
5526
  }
5615
5527
 
@@ -6865,7 +6777,7 @@ export class SharedLog<
6865
6777
  rebalance?: boolean;
6866
6778
  checkDuplicates?: boolean;
6867
6779
  timestamp?: number;
6868
- allowLegacyOrderedReplacementPairs?: boolean;
6780
+ allowOrderedReplacementPairs?: boolean;
6869
6781
  onConfirmedDurableStateChanged?: () => void;
6870
6782
  onDurableApplyCommitted?: () => boolean | void;
6871
6783
  shouldApply?: () => boolean;
@@ -6907,7 +6819,7 @@ export class SharedLog<
6907
6819
  checkDuplicates,
6908
6820
  timestamp: ts,
6909
6821
  rebalance,
6910
- allowLegacyOrderedReplacementPairs,
6822
+ allowOrderedReplacementPairs,
6911
6823
  onConfirmedDurableStateChanged,
6912
6824
  onDurableApplyCommitted,
6913
6825
  shouldApply,
@@ -6916,7 +6828,7 @@ export class SharedLog<
6916
6828
  rebalance?: boolean;
6917
6829
  checkDuplicates?: boolean;
6918
6830
  timestamp?: number;
6919
- allowLegacyOrderedReplacementPairs?: boolean;
6831
+ allowOrderedReplacementPairs?: boolean;
6920
6832
  onConfirmedDurableStateChanged?: () => void;
6921
6833
  onDurableApplyCommitted?: () => boolean | void;
6922
6834
  shouldApply?: () => boolean;
@@ -6955,17 +6867,19 @@ export class SharedLog<
6955
6867
  incomingRangeCountsById.set(range.idString, count);
6956
6868
  if (
6957
6869
  count > 1 &&
6958
- (!allowLegacyOrderedReplacementPairs || reset === true || count > 2)
6870
+ (!allowOrderedReplacementPairs || reset === true || count > 2)
6959
6871
  ) {
6960
6872
  throw new Error(
6961
6873
  `Duplicate replication range id in announcement: ${range.idString}`,
6962
6874
  );
6963
6875
  }
6964
- // Released peers represented a non-reset replacement as the retired
6965
- // geometry followed by the current geometry under the same id. The
6966
- // sender is already authorized to replace that id with the final item,
6967
- // so collapsing an exact two-item incremental pair to its last item
6968
- // preserves rolling-upgrade compatibility without broadening authority.
6876
+ // Rolling-upgrade relaxation on the live V2 Added path: a peer may
6877
+ // still express a non-reset replacement as the retired geometry
6878
+ // followed by the current geometry under one id. The sender is already
6879
+ // authorized to replace that id with the final item, so collapsing an
6880
+ // EXACT two-item incremental pair to its last item accepts the older
6881
+ // wire shape without broadening authority. Deliberately narrow — reset
6882
+ // announcements and any run longer than two still fail as duplicates.
6969
6883
  incomingRangesById.set(range.idString, range);
6970
6884
  }
6971
6885
  const incomingRanges = [...incomingRangesById.values()];
@@ -14247,12 +14161,7 @@ export class SharedLog<
14247
14161
 
14248
14162
  this.domain = options?.domain
14249
14163
  ? (options.domain(this) as unknown as D)
14250
- : (createReplicationDomainHash(
14251
- (options as any)?.compatibility !== undefined &&
14252
- (options as any).compatibility < 10
14253
- ? "u32"
14254
- : "u64",
14255
- )(this) as unknown as D);
14164
+ : (createReplicationDomainHash("u64")(this) as unknown as D);
14256
14165
  this.indexableDomain = createIndexableDomainFromResolution(
14257
14166
  this.domain.resolution,
14258
14167
  );
@@ -14262,7 +14171,6 @@ export class SharedLog<
14262
14171
  this._admittedPruneRemoves = new Set();
14263
14172
  this._pendingIHave = new Map();
14264
14173
  this._pendingIHaveCallbacks = new Set();
14265
- this.latestReplicationInfoMessage = new Map();
14266
14174
  this._replicationInfoRequestByPeer = new Map();
14267
14175
  this._subscriberSnapshotRequestsByPeer = new Map();
14268
14176
  // Terminal close/drop drains the previous lifecycle before another open can
@@ -14327,7 +14235,6 @@ export class SharedLog<
14327
14235
 
14328
14236
  this.uniqueReplicators = new Set();
14329
14237
  this._replicatorJoinEmitted = new Set();
14330
- this._replicatorsReconciled = false;
14331
14238
  // Deserialized instances never ran the constructor; create the monitor and
14332
14239
  // coordinator lazily on first open. Reopens keep the SAME instances so
14333
14240
  // stale async continuations observe resets via property lookup.
@@ -14335,7 +14242,6 @@ export class SharedLog<
14335
14242
  this._liveness.resetForOpen();
14336
14243
  this._lastLocalAppendAt = 0;
14337
14244
  this._announcements ??= this.createReplicationAnnouncementCoordinator();
14338
- this._announcements.resetForOpen();
14339
14245
  this._v2Receive ??= this.createReplicationInfoV2ReceiveCoordinator();
14340
14246
  this._v2Receive.resetForOpen();
14341
14247
  this._v2Send ??= this.createReplicationInfoV2SendCoordinator();
@@ -14398,12 +14304,6 @@ export class SharedLog<
14398
14304
  }
14399
14305
 
14400
14306
  this._closeController = new AbortController();
14401
- if (this.legacyReplicationInfoEnabled) {
14402
- this._announcements.setupReplicationAnnouncementRetryFunction();
14403
- this._announcements.setupReplicationAnnouncementRepairFunction();
14404
- } else {
14405
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
14406
- }
14407
14307
  this._closeController.signal.addEventListener("abort", () => {
14408
14308
  for (const [_peer, state] of this._replicationInfoRequestByPeer) {
14409
14309
  if (state.timer) clearTimeout(state.timer);
@@ -14859,7 +14759,6 @@ export class SharedLog<
14859
14759
  sendOptions?: { priority?: number; signal?: AbortSignal },
14860
14760
  ) => this.trySendFusedRawExchangeHeads(hashes, to, sendOptions),
14861
14761
  warn,
14862
- compatibility: (this._logProperties as any)?.compatibility,
14863
14762
  resolution: this.domain.resolution,
14864
14763
  sync: options?.sync,
14865
14764
  syncronizer: options?.syncronizer,
@@ -15613,16 +15512,8 @@ export class SharedLog<
15613
15512
  const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(
15614
15513
  this.topic,
15615
15514
  );
15616
- const replicationLifecycleController =
15617
- this._instanceLifecycle?.membershipLifecycleController;
15618
-
15619
15515
  // We do this here, because these calls requires this.closed == false
15620
15516
  void this.pruneOfflineReplicators()
15621
- .then(() => {
15622
- if (this.isReplicationLifecycleActive(replicationLifecycleController)) {
15623
- this._replicatorsReconciled = true;
15624
- }
15625
- })
15626
15517
  .catch((error) => {
15627
15518
  if (isNotStartedError(error as Error)) {
15628
15519
  return;
@@ -15863,7 +15754,7 @@ export class SharedLog<
15863
15754
  const peerSession = this._peerSessions.current(peerHash);
15864
15755
  const preserveV2Session =
15865
15756
  peerSession?.phase === "open" && peerSession.isActive();
15866
- if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
15757
+ if (!preserveV2Session) {
15867
15758
  this.cancelReplicationInfoRequests(peerHash);
15868
15759
  }
15869
15760
  this._liveness._replicatorLivenessFailures.delete(peerHash);
@@ -15895,11 +15786,8 @@ export class SharedLog<
15895
15786
 
15896
15787
  private advanceReplicationInfoRecoveryEpoch(peerHash: string) {
15897
15788
  // Handlers admitted before a successful peer removal must not restore state
15898
- // when they eventually reach the apply lane. Reset the sender's
15899
- // ordering watermark with the local epoch so a later arrival can be
15900
- // accepted without comparing its clock to this receiver's clock.
15789
+ // when they eventually reach the apply lane.
15901
15790
  const receiveEpoch = this._peerSessions.advanceReceiveEpoch(peerHash);
15902
- this.latestReplicationInfoMessage.delete(peerHash);
15903
15791
  const peerSession = this._peerSessions.current(peerHash);
15904
15792
  if (peerSession?.phase === "open") {
15905
15793
  this._v2Receive.advanceRecovery({
@@ -16788,15 +16676,6 @@ export class SharedLog<
16788
16676
  firstError ??= error;
16789
16677
  }
16790
16678
  };
16791
- // A borsh-deserialized instance that is closed before ever being opened
16792
- // has no coordinators (they are created in open()); the old inline code
16793
- // only touched plain fields here and never threw.
16794
- captureSync(() =>
16795
- this._announcements?.cancelCurrentReplicationStateAnnouncementRetry(),
16796
- );
16797
- if (this._announcements) {
16798
- this._announcements.replicationAnnouncementRetryDebounced = undefined;
16799
- }
16800
16679
  captureSync(() => {
16801
16680
  if (this._wireSyncSession) {
16802
16681
  this._wireSyncSession.unregisterTopic(this.topic);
@@ -16890,7 +16769,6 @@ export class SharedLog<
16890
16769
  captureSync(() => {
16891
16770
  this._pendingIHave?.clear();
16892
16771
  this._pendingIHaveCallbacks?.clear();
16893
- this.latestReplicationInfoMessage?.clear();
16894
16772
  this._peerSessions?.clearReceiveEpochsForClose();
16895
16773
  this._peerSessions?.clearCleanupGatesForClose();
16896
16774
  this._activeReceiveHandlersByPeer?.clear();
@@ -16991,7 +16869,6 @@ export class SharedLog<
16991
16869
  await pruneRemoveTerminalFence.drained;
16992
16870
  await this.drainPendingIHaveCallbacks();
16993
16871
  this.ensureNativeDurabilityRuntimeState();
16994
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
16995
16872
  } catch (error) {
16996
16873
  // The terminal preamble has already disabled parent attachments and the
16997
16874
  // network lifecycle. Keep mutation admission fenced for an exact retry.
@@ -17035,19 +16912,7 @@ export class SharedLog<
17035
16912
  }
17036
16913
  }, 2_000);
17037
16914
  try {
17038
- const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17039
- const resets = [this._v2Send.sendTerminalReset(abort.signal)];
17040
- if (this.legacyReplicationInfoEnabled) {
17041
- resets.push(
17042
- this.rpc
17043
- .send(reset, {
17044
- priority: CONVERGENCE_MESSAGE_PRIORITY,
17045
- signal: abort.signal,
17046
- })
17047
- .catch(() => {}),
17048
- );
17049
- }
17050
- await Promise.all(resets);
16915
+ await this._v2Send.sendTerminalReset(abort.signal);
17051
16916
  } finally {
17052
16917
  clearTimeout(abortTimer);
17053
16918
  }
@@ -17140,7 +17005,6 @@ export class SharedLog<
17140
17005
  await replicationRangeTerminalFence.drained;
17141
17006
  await pruneRemoveTerminalFence.drained;
17142
17007
  await this.drainPendingIHaveCallbacks();
17143
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
17144
17008
  } catch (error) {
17145
17009
  // The terminal preamble is not safely reversible. Preserve the fence until
17146
17010
  // a retry finishes cleanup.
@@ -17168,19 +17032,7 @@ export class SharedLog<
17168
17032
  }
17169
17033
  }, 2_000);
17170
17034
  try {
17171
- const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17172
- const resets = [this._v2Send.sendTerminalReset(abort.signal)];
17173
- if (this.legacyReplicationInfoEnabled) {
17174
- resets.push(
17175
- this.rpc
17176
- .send(reset, {
17177
- priority: CONVERGENCE_MESSAGE_PRIORITY,
17178
- signal: abort.signal,
17179
- })
17180
- .catch(() => {}),
17181
- );
17182
- }
17183
- await Promise.all(resets);
17035
+ await this._v2Send.sendTerminalReset(abort.signal);
17184
17036
  } finally {
17185
17037
  clearTimeout(abortTimer);
17186
17038
  }
@@ -17707,16 +17559,16 @@ export class SharedLog<
17707
17559
  throw new Error("Missing from in update role message");
17708
17560
  }
17709
17561
  if (
17710
- !this.legacyReplicationInfoEnabled &&
17711
- (msg instanceof RequestReplicationInfoMessage ||
17712
- msg instanceof ResponseRoleMessage ||
17713
- msg instanceof AllReplicatingSegmentsMessage ||
17714
- msg instanceof AddedReplicationSegmentMessage ||
17715
- msg instanceof StoppedReplicating)
17562
+ msg instanceof RequestReplicationInfoMessage ||
17563
+ msg instanceof ResponseRoleMessage ||
17564
+ msg instanceof AllReplicatingSegmentsMessage ||
17565
+ msg instanceof AddedReplicationSegmentMessage ||
17566
+ msg instanceof StoppedReplicating
17716
17567
  ) {
17717
- // These variants remain registered decode tombstones, but current logs
17718
- // fail closed before leases, synchronizer work, liveness, watermarks or
17719
- // mutations. Only an explicit pre-v10 compatibility open admits them.
17568
+ // These variants remain registered decode tombstones, but logs fail
17569
+ // closed unconditionally before leases, synchronizer work, liveness,
17570
+ // watermarks or mutations (B12: the compatibility opens that once
17571
+ // admitted them reject at open()).
17720
17572
  return;
17721
17573
  }
17722
17574
  // Snapshot receive ownership before any async handler gets a chance to
@@ -17757,22 +17609,14 @@ export class SharedLog<
17757
17609
  this.captureReplicationOwnershipLifecycle();
17758
17610
  const receiveReplicationInfoReceiveEpoch =
17759
17611
  this._peerSessions.receiveEpoch(receiveFromHash);
17760
- if (msg instanceof ResponseRoleMessage) {
17761
- msg = msg.toReplicationInfoMessage(); // migration
17762
- }
17763
17612
  if (
17764
- msg instanceof AllReplicatingSegmentsMessage ||
17765
- msg instanceof AddedReplicationSegmentMessage ||
17766
17613
  msg instanceof FullReplicationInfoV2Message ||
17767
17614
  msg instanceof AddedReplicationInfoV2Message
17768
17615
  ) {
17769
17616
  // Bound decoded untrusted vectors before per-peer/global mutation
17770
17617
  // queues, trusted-replicator authorization, or liveness side effects.
17771
17618
  this.validateReplicationRangeAnnouncement(msg.segments);
17772
- } else if (
17773
- msg instanceof StoppedReplicating ||
17774
- msg instanceof StoppedReplicationInfoV2Message
17775
- ) {
17619
+ } else if (msg instanceof StoppedReplicationInfoV2Message) {
17776
17620
  // Bound the raw decoded vector before deduplication can hide the
17777
17621
  // allocation cost, and before liveness or apply-queue side effects.
17778
17622
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
@@ -17780,10 +17624,7 @@ export class SharedLog<
17780
17624
  if (
17781
17625
  !context.from.equals(this.node.identity.publicKey) &&
17782
17626
  !(msg instanceof RequestReplicationInfoV2Message) &&
17783
- !isReplicationInfoV2Message(msg) &&
17784
- !(msg instanceof AllReplicatingSegmentsMessage) &&
17785
- !(msg instanceof AddedReplicationSegmentMessage) &&
17786
- !(msg instanceof StoppedReplicating)
17627
+ !isReplicationInfoV2Message(msg)
17787
17628
  ) {
17788
17629
  this._liveness.markReplicatorActivity(receiveFromHash);
17789
17630
  }
@@ -19498,23 +19339,6 @@ export class SharedLog<
19498
19339
  });
19499
19340
  } else if (msg instanceof ReplicationPingMessage) {
19500
19341
  // No-op: used as an ACKed unicast liveness probe.
19501
- } else if (msg instanceof RequestReplicationInfoMessage) {
19502
- await this.handleRequestReplicationInfo(
19503
- msg,
19504
- laneRequestContext,
19505
- lane,
19506
- );
19507
- } else if (
19508
- msg instanceof AllReplicatingSegmentsMessage ||
19509
- msg instanceof AddedReplicationSegmentMessage
19510
- ) {
19511
- await this.handleReplicationInfoAnnouncement(
19512
- msg,
19513
- laneRequestContext,
19514
- lane,
19515
- );
19516
- } else if (msg instanceof StoppedReplicating) {
19517
- await this.handleStoppedReplicating(msg, laneRequestContext, lane);
19518
19342
  } else {
19519
19343
  throw new Error("Unexpected message");
19520
19344
  }
@@ -19743,113 +19567,6 @@ export class SharedLog<
19743
19567
  }
19744
19568
  }
19745
19569
 
19746
- private async handleRequestReplicationInfo(
19747
- _msg: RequestReplicationInfoMessage,
19748
- context: ReceiveRequestContext,
19749
- lane: ReceiveLaneContext,
19750
- ): Promise<void> {
19751
- const receiveFromHash = lane.fromHash;
19752
- const receiveSession = lane.session;
19753
- const receiveReplicationLifecycleController = lane.lifecycleController;
19754
- if (context.from.equals(this.node.identity.publicKey)) {
19755
- return;
19756
- }
19757
- const replicationLifecycleController =
19758
- receiveReplicationLifecycleController;
19759
- if (
19760
- !replicationLifecycleController ||
19761
- !this._peerSessions.isReceiveAdmissionOpen(
19762
- receiveFromHash,
19763
- receiveSession,
19764
- replicationLifecycleController,
19765
- )
19766
- ) {
19767
- return;
19768
- }
19769
-
19770
- let replicationSegments: ReplicationRangeIndexable<R>[];
19771
- try {
19772
- replicationSegments = await this.getMyReplicationSegments();
19773
- } catch (error) {
19774
- if (
19775
- !this._peerSessions.isReceiveAdmissionOpen(
19776
- receiveFromHash,
19777
- receiveSession,
19778
- replicationLifecycleController,
19779
- ) &&
19780
- isNotStartedError(error as Error)
19781
- ) {
19782
- return;
19783
- }
19784
- throw error;
19785
- }
19786
- if (
19787
- !this._peerSessions.isReceiveAdmissionOpen(
19788
- receiveFromHash,
19789
- receiveSession,
19790
- replicationLifecycleController,
19791
- )
19792
- ) {
19793
- return;
19794
- }
19795
- const segments = replicationSegments.map((x) => x.toReplicationRange());
19796
- this.validatePersistedReplicationRangeSnapshot(segments);
19797
- this._v2Send.enqueueSnapshotForPeer(receiveFromHash);
19798
-
19799
- await this.rpc
19800
- .send(new AllReplicatingSegmentsMessage({ segments }), {
19801
- mode: new AcknowledgeDelivery({
19802
- to: [context.from],
19803
- redundancy: 1,
19804
- }),
19805
- signal: replicationLifecycleController.signal,
19806
- })
19807
- .catch((error) =>
19808
- this.handleReplicationLifecycleSendError(
19809
- error,
19810
- replicationLifecycleController,
19811
- ),
19812
- );
19813
- if (
19814
- !this._peerSessions.isReceiveAdmissionOpen(
19815
- receiveFromHash,
19816
- receiveSession,
19817
- replicationLifecycleController,
19818
- )
19819
- ) {
19820
- return;
19821
- }
19822
-
19823
- // for backwards compatibility (v8) remove this when we are sure that all nodes are v9+
19824
- if (this.v8Behaviour) {
19825
- const role = this.getRoleFromReplicationSegments(replicationSegments);
19826
- if (role instanceof Replicator) {
19827
- const fixedSettings = !this._isAdaptiveReplicating;
19828
- if (fixedSettings) {
19829
- await this.rpc
19830
- .send(
19831
- new ResponseRoleMessage({
19832
- role,
19833
- }),
19834
- {
19835
- mode: new SilentDelivery({
19836
- to: [context.from],
19837
- redundancy: 1,
19838
- }),
19839
- signal: replicationLifecycleController.signal,
19840
- },
19841
- )
19842
- .catch((error) =>
19843
- this.handleReplicationLifecycleSendError(
19844
- error,
19845
- replicationLifecycleController,
19846
- ),
19847
- );
19848
- }
19849
- }
19850
- }
19851
- }
19852
-
19853
19570
  private async handleReplicationInfoV2Announcement(
19854
19571
  msg: ReplicationInfoV2Message,
19855
19572
  context: ReceiveRequestContext,
@@ -19926,7 +19643,7 @@ export class SharedLog<
19926
19643
  reset: msg instanceof FullReplicationInfoV2Message,
19927
19644
  checkDuplicates: true,
19928
19645
  timestamp: Number(context.message.header.timestamp),
19929
- allowLegacyOrderedReplacementPairs:
19646
+ allowOrderedReplacementPairs:
19930
19647
  msg instanceof AddedReplicationInfoV2Message,
19931
19648
  shouldApply: () => {
19932
19649
  mutationGateChecked = true;
@@ -20029,252 +19746,12 @@ export class SharedLog<
20029
19746
  // A committed V2 announcement is applied progress: the peer answers,
20030
19747
  // so recovery re-solicitation may restart from the base interval.
20031
19748
  this.resetReplicationInfoV2RecoveryEscalation(fromHash);
20032
- if (
20033
- msg instanceof FullReplicationInfoV2Message &&
20034
- this.legacyReplicationInfoEnabled
20035
- ) {
20036
- this.cancelReplicationInfoRequests(fromHash);
20037
- }
20038
19749
  });
20039
19750
  } finally {
20040
19751
  this._v2Receive.release(admission);
20041
19752
  }
20042
19753
  }
20043
19754
 
20044
- private async handleReplicationInfoAnnouncement(
20045
- msg: AllReplicatingSegmentsMessage | AddedReplicationSegmentMessage,
20046
- context: ReceiveRequestContext,
20047
- lane: ReceiveLaneContext,
20048
- ): Promise<void> {
20049
- const receiveFromHash = lane.fromHash;
20050
- const receiveSession = lane.session;
20051
- const receiveReplicationLifecycleController = lane.lifecycleController;
20052
- const receiveReplicationInfoReceiveEpoch = lane.receiveEpoch;
20053
- const peerReceiveLease = lane.lease;
20054
- if (context.from.equals(this.node.identity.publicKey)) {
20055
- return;
20056
- }
20057
-
20058
- const replicationInfoMessage = msg as
20059
- | AllReplicatingSegmentsMessage
20060
- | AddedReplicationSegmentMessage;
20061
-
20062
- // Process replication updates even if the sender isn't yet considered "ready" by
20063
- // `Program.waitFor()`. Dropping these messages can lead to missing replicator info
20064
- // (and downstream `waitForReplicator()` timeouts) under timing-sensitive joins.
20065
- const from = context.from!;
20066
- const fromHash = from.hashcode();
20067
- if (this._v2Receive.isLegacyCutover(receiveSession)) {
20068
- if (receiveSession) {
20069
- this._v2Receive.noteLegacyAnnouncement({
20070
- peerHash: fromHash,
20071
- peerSession: receiveSession,
20072
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20073
- senderTransportSession: context.message.header.session,
20074
- transportTimestamp: context.message.header.timestamp,
20075
- message: msg,
20076
- });
20077
- }
20078
- return;
20079
- }
20080
- // Pre-lane gate: lifecycle -> receive-epoch -> blocked, exactly the
20081
- // legacy order. isMembershipActiveFor is the unit-pinned fold of
20082
- // isReplicationLifecycleActive; isReceiveEpochCurrent is the
20083
- // relocated `===`-with-`?? null`. The DELIBERATE absence of a
20084
- // subscription-epoch term is preserved: the lease already validated
20085
- // it, and the in-lane recheck owns post-await staleness.
20086
- if (
20087
- !this._instanceLifecycle!.isMembershipActiveFor(
20088
- receiveReplicationLifecycleController,
20089
- ) ||
20090
- !this._peerSessions.isReceiveEpochCurrent(
20091
- receiveFromHash,
20092
- receiveReplicationInfoReceiveEpoch,
20093
- ) ||
20094
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20095
- ) {
20096
- return;
20097
- }
20098
- const messageTimestamp = context.message.header.timestamp;
20099
- peerReceiveLease.release();
20100
- await this.withReplicationInfoApplyQueue(fromHash, async () => {
20101
- try {
20102
- // The peer may have unsubscribed after this message was queued.
20103
- // In-lane gate: lifecycle -> subscription-epoch -> receive-epoch
20104
- // -> blocked, term for term as before the session migration.
20105
- if (
20106
- !this._instanceLifecycle!.isMembershipActiveFor(
20107
- receiveReplicationLifecycleController,
20108
- ) ||
20109
- !this._peerSessions.isCurrent(fromHash, receiveSession) ||
20110
- !this._peerSessions.isReceiveEpochCurrent(
20111
- fromHash,
20112
- receiveReplicationInfoReceiveEpoch,
20113
- ) ||
20114
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20115
- ) {
20116
- return;
20117
- }
20118
- if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
20119
- this._v2Receive.noteLegacyAnnouncement({
20120
- peerHash: fromHash,
20121
- peerSession: receiveSession,
20122
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20123
- senderTransportSession: context.message.header.session,
20124
- transportTimestamp: context.message.header.timestamp,
20125
- message: msg,
20126
- });
20127
- return;
20128
- }
20129
-
20130
- // Process in-order to avoid races where repeated reset messages arrive
20131
- // concurrently and trigger spurious "added" diffs / rebalancing.
20132
- const prev = this.latestReplicationInfoMessage.get(fromHash);
20133
- if (prev && prev > messageTimestamp) {
20134
- return;
20135
- }
20136
-
20137
- this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
20138
-
20139
- if (this.closed) {
20140
- return;
20141
- }
20142
-
20143
- const reset = msg instanceof AllReplicatingSegmentsMessage;
20144
- const result = await this.addReplicationRange(
20145
- replicationInfoMessage.segments.map((x) =>
20146
- x.toReplicationRangeIndexable(from),
20147
- ),
20148
- from,
20149
- {
20150
- reset,
20151
- checkDuplicates: true,
20152
- timestamp: Number(messageTimestamp),
20153
- allowLegacyOrderedReplacementPairs:
20154
- msg instanceof AddedReplicationSegmentMessage,
20155
- },
20156
- );
20157
- if (result === undefined) {
20158
- return;
20159
- }
20160
- this._liveness.markReplicatorActivity(fromHash);
20161
-
20162
- // If the peer reports any replication segments, stop re-requesting.
20163
- // (Empty reports can be transient during startup.)
20164
- if (replicationInfoMessage.segments.length > 0) {
20165
- this.cancelReplicationInfoRequests(fromHash);
20166
- }
20167
- } catch (e) {
20168
- if (isNotStartedError(e as Error)) {
20169
- return;
20170
- }
20171
- logger.error(
20172
- `Failed to apply replication settings from '${fromHash}': ${
20173
- (e as any)?.message ?? e
20174
- }`,
20175
- );
20176
- }
20177
- });
20178
- }
20179
-
20180
- private async handleStoppedReplicating(
20181
- msg: StoppedReplicating,
20182
- context: ReceiveRequestContext,
20183
- lane: ReceiveLaneContext,
20184
- ): Promise<void> {
20185
- const receiveFromHash = lane.fromHash;
20186
- const receiveSession = lane.session;
20187
- const receiveReplicationLifecycleController = lane.lifecycleController;
20188
- const receiveReplicationInfoReceiveEpoch = lane.receiveEpoch;
20189
- const peerReceiveLease = lane.lease;
20190
- const from = context.from!;
20191
- const segmentIds = msg.segmentIds;
20192
- if (from.equals(this.node.identity.publicKey)) {
20193
- return;
20194
- }
20195
- const fromHash = from.hashcode();
20196
- if (this._v2Receive.isLegacyCutover(receiveSession)) {
20197
- if (receiveSession) {
20198
- this._v2Receive.noteLegacyAnnouncement({
20199
- peerHash: fromHash,
20200
- peerSession: receiveSession,
20201
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20202
- senderTransportSession: context.message.header.session,
20203
- transportTimestamp: context.message.header.timestamp,
20204
- message: msg,
20205
- });
20206
- }
20207
- return;
20208
- }
20209
- // Same pre-lane gate shape as Added/All above (and the same
20210
- // intentional absence of a subscription-epoch term).
20211
- if (
20212
- !this._instanceLifecycle!.isMembershipActiveFor(
20213
- receiveReplicationLifecycleController,
20214
- ) ||
20215
- !this._peerSessions.isReceiveEpochCurrent(
20216
- receiveFromHash,
20217
- receiveReplicationInfoReceiveEpoch,
20218
- ) ||
20219
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20220
- ) {
20221
- return;
20222
- }
20223
- const messageTimestamp = context.message.header.timestamp;
20224
- peerReceiveLease.release();
20225
- await this.withReplicationInfoApplyQueue(fromHash, async () => {
20226
- if (
20227
- !this._instanceLifecycle!.isMembershipActiveFor(
20228
- receiveReplicationLifecycleController,
20229
- ) ||
20230
- !this._peerSessions.isCurrent(fromHash, receiveSession) ||
20231
- !this._peerSessions.isReceiveEpochCurrent(
20232
- fromHash,
20233
- receiveReplicationInfoReceiveEpoch,
20234
- ) ||
20235
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20236
- ) {
20237
- return;
20238
- }
20239
- if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
20240
- this._v2Receive.noteLegacyAnnouncement({
20241
- peerHash: fromHash,
20242
- peerSession: receiveSession,
20243
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20244
- senderTransportSession: context.message.header.session,
20245
- transportTimestamp: context.message.header.timestamp,
20246
- message: msg,
20247
- });
20248
- return;
20249
- }
20250
-
20251
- const previousTimestamp = this.latestReplicationInfoMessage.get(fromHash);
20252
- if (previousTimestamp && previousTimestamp > messageTimestamp) {
20253
- return;
20254
- }
20255
- this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
20256
- if (this.closed) {
20257
- return;
20258
- }
20259
-
20260
- const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(
20261
- segmentIds,
20262
- from,
20263
- );
20264
-
20265
- await this.removeReplicationRanges(rangesToRemove, from);
20266
- this._liveness.markReplicatorActivity(fromHash);
20267
- const timestamp = BigInt(+new Date());
20268
- for (const range of rangesToRemove) {
20269
- this.replicationChangeDebounceFn.add({
20270
- range,
20271
- type: "removed",
20272
- timestamp,
20273
- });
20274
- }
20275
- });
20276
- }
20277
-
20278
19755
  async calculateTotalParticipation(options?: { sum?: boolean }) {
20279
19756
  if (options?.sum) {
20280
19757
  const ranges = await this.replicationIndex.iterate().all();
@@ -20736,34 +20213,20 @@ export class SharedLog<
20736
20213
 
20737
20214
  requestAttempts++;
20738
20215
 
20739
- if (this.legacyReplicationInfoEnabled) {
20740
- this.rpc
20741
- .send(new RequestReplicationInfoMessage(), {
20742
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
20743
- })
20744
- .catch((e) => {
20745
- // Best-effort: missing peers / unopened RPC should not fail the wait logic.
20746
- if (isNotStartedError(e as Error)) {
20747
- return;
20748
- }
20749
- logger.error(e?.toString?.() ?? String(e));
20750
- });
20751
- } else {
20752
- const peerHash = key.hashcode();
20753
- const peerSession = this._peerSessions.current(peerHash);
20754
- if (peerSession?.phase === "open") {
20755
- this._v2Receive.resumeParkedRequest({
20756
- peerHash,
20757
- peerSession,
20758
- receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
20759
- });
20760
- } else if (peerSession === null || peerSession.phase === "departing") {
20761
- // A peer can be known to routing before its SharedLog topic
20762
- // subscription has been observed. Legacy requests used to bootstrap
20763
- // that case directly; V2 needs an authoritative Subscribe snapshot
20764
- // before it can create a fenced PeerSession and request a Full.
20765
- requestSubscriberSnapshot();
20766
- }
20216
+ const peerHash = key.hashcode();
20217
+ const peerSession = this._peerSessions.current(peerHash);
20218
+ if (peerSession?.phase === "open") {
20219
+ this._v2Receive.resumeParkedRequest({
20220
+ peerHash,
20221
+ peerSession,
20222
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
20223
+ });
20224
+ } else if (peerSession === null || peerSession.phase === "departing") {
20225
+ // A peer can be known to routing before its SharedLog topic
20226
+ // subscription has been observed. Legacy requests used to bootstrap
20227
+ // that case directly; V2 needs an authoritative Subscribe snapshot
20228
+ // before it can create a fenced PeerSession and request a Full.
20229
+ requestSubscriberSnapshot();
20767
20230
  }
20768
20231
 
20769
20232
  if (requestAttempts < maxRequestAttempts) {
@@ -23507,7 +22970,7 @@ export class SharedLog<
23507
22970
  */
23508
22971
  private resetReplicationInfoV2RecoveryEscalation(peerHash: string) {
23509
22972
  const state = this._replicationInfoRequestByPeer.get(peerHash);
23510
- if (!state || state.peerSession === undefined) {
22973
+ if (!state) {
23511
22974
  return;
23512
22975
  }
23513
22976
  state.attempts = 0;
@@ -23590,7 +23053,11 @@ export class SharedLog<
23590
23053
  }
23591
23054
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23592
23055
  if (
23593
- !this._v2Receive.isRequestParked({ peerHash, peerSession, receiveEpoch })
23056
+ !this._v2Receive.isRequestParked({
23057
+ peerHash,
23058
+ peerSession,
23059
+ receiveEpoch,
23060
+ })
23594
23061
  ) {
23595
23062
  // Active, or a bounded request cycle is still running its own
23596
23063
  // exponential retries. Keep polling for the next park.
@@ -23624,6 +23091,13 @@ export class SharedLog<
23624
23091
  tick();
23625
23092
  }
23626
23093
 
23094
+ /**
23095
+ * Collapsed B12 shell: the legacy request-polling body (bounded
23096
+ * RequestReplicationInfoMessage ticks) is deleted; V2 recovery is the
23097
+ * only scheduler. Retained as a named seam rather than inlined at the
23098
+ * callers because the liveness monitor wiring and several suites
23099
+ * stub/spy it by name.
23100
+ */
23627
23101
  private scheduleReplicationInfoRequests(
23628
23102
  peer: PublicSignKey,
23629
23103
  replicationLifecycleController = this._instanceLifecycle
@@ -23635,75 +23109,10 @@ export class SharedLog<
23635
23109
  ) {
23636
23110
  return;
23637
23111
  }
23638
- if (!this.legacyReplicationInfoEnabled) {
23639
- this.scheduleReplicationInfoV2Recovery(
23640
- peer,
23641
- replicationLifecycleController,
23642
- );
23643
- return;
23644
- }
23645
- const peerHash = peer.hashcode();
23646
- const requestStates = this._replicationInfoRequestByPeer;
23647
- if (requestStates.has(peerHash)) {
23648
- return;
23649
- }
23650
-
23651
- const state: { attempts: number; timer?: ReturnType<typeof setTimeout> } = {
23652
- attempts: 0,
23653
- };
23654
- requestStates.set(peerHash, state);
23655
- const cancel = () => {
23656
- if (requestStates.get(peerHash) !== state) {
23657
- return;
23658
- }
23659
- if (state.timer) {
23660
- clearTimeout(state.timer);
23661
- }
23662
- requestStates.delete(peerHash);
23663
- };
23664
-
23665
- const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
23666
- const maxAttempts =
23667
- this.waitForReplicatorRequestMaxAttempts ??
23668
- Math.max(
23669
- WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
23670
- Math.ceil(this.waitForReplicatorTimeout / intervalMs),
23671
- );
23672
-
23673
- const tick = () => {
23674
- if (!this.isReplicationLifecycleActive(replicationLifecycleController)) {
23675
- cancel();
23676
- return;
23677
- }
23678
- state.attempts++;
23679
-
23680
- this.rpc
23681
- .send(new RequestReplicationInfoMessage(), {
23682
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [peer] }),
23683
- signal: replicationLifecycleController.signal,
23684
- })
23685
- .catch((e) => {
23686
- // Best-effort: missing peers / unopened RPC should not fail join flows.
23687
- if (
23688
- isNotStartedError(e as Error) ||
23689
- (replicationLifecycleController.signal.aborted &&
23690
- e instanceof AbortError)
23691
- ) {
23692
- return;
23693
- }
23694
- logger.error(e?.toString?.() ?? String(e));
23695
- });
23696
-
23697
- if (state.attempts >= maxAttempts) {
23698
- cancel();
23699
- return;
23700
- }
23701
-
23702
- state.timer = setTimeout(tick, intervalMs);
23703
- state.timer.unref?.();
23704
- };
23705
-
23706
- tick();
23112
+ this.scheduleReplicationInfoV2Recovery(
23113
+ peer,
23114
+ replicationLifecycleController,
23115
+ );
23707
23116
  }
23708
23117
 
23709
23118
  async handleSubscriptionChange(
@@ -23786,10 +23195,6 @@ export class SharedLog<
23786
23195
  ) {
23787
23196
  return;
23788
23197
  }
23789
- // The timestamp watermark belongs to the previous subscription epoch.
23790
- // Sender clocks are not synchronized, so carrying a local unsubscribe
23791
- // timestamp forward could reject every valid announcement after reconnect.
23792
- this.latestReplicationInfoMessage.delete(peerHash);
23793
23198
  this._pendingReplicatorLeaveByPeer.delete(peerHash);
23794
23199
  const openingCapabilities =
23795
23200
  this._openingSyncCapabilitiesByPeer.get(peerHash);
@@ -23843,12 +23248,6 @@ export class SharedLog<
23843
23248
  this.joinWarmup._warmupSessionsByTarget.get(peerHash) ?? null;
23844
23249
  this.joinWarmup.cancelJoinWarmupTarget(peerHash);
23845
23250
 
23846
- const now = BigInt(+new Date());
23847
- const previous = this.latestReplicationInfoMessage.get(peerHash);
23848
- if (!previous || previous < now) {
23849
- this.latestReplicationInfoMessage.set(peerHash, now);
23850
- }
23851
-
23852
23251
  let removed = false;
23853
23252
  try {
23854
23253
  // Unsubscribe can race with the peer's final replication reset message.
@@ -23899,116 +23298,23 @@ export class SharedLog<
23899
23298
 
23900
23299
  // Decode, sender and authenticated apply readiness are separate bits. An
23901
23300
  // ACKed capability advert is the local half of receiver-led negotiation;
23902
- // readiness is promoted only after the legacy startup path has completed.
23903
- // This preserves mixed-version discovery without putting capability ACK
23904
- // latency on the subscription callback's critical path.
23301
+ // readiness is promoted through the coordinator once the ACK arrives.
23302
+ // This keeps capability ACK latency off the subscription callback's
23303
+ // critical path.
23905
23304
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23906
- const localCapabilityAdvertisement =
23907
- this._v2Receive.advertiseLocalCapability({
23908
- target: publicKey,
23909
- peerSession: expectedSubscriptionEpoch,
23910
- receiveEpoch,
23911
- signal: replicationLifecycleController.signal,
23912
- });
23913
- if (!this.legacyReplicationInfoEnabled) {
23914
- // Current logs have no legacy startup work to order ahead of RequestV2.
23915
- // Releasing is synchronous and exact-session fenced; the ACK may still
23916
- // arrive later and promote readiness through the coordinator.
23917
- localCapabilityAdvertisement.releaseLegacyBarrier();
23918
- this.scheduleReplicationInfoV2Recovery(
23919
- publicKey,
23920
- replicationLifecycleController,
23921
- );
23922
- return;
23923
- }
23924
-
23925
- try {
23926
- let replicationSegments: ReplicationRangeIndexable<R>[];
23927
- try {
23928
- replicationSegments = await this.getMyReplicationSegments();
23929
- } catch (error) {
23930
- if (
23931
- !this.isReplicationLifecycleActive(replicationLifecycleController) &&
23932
- isNotStartedError(error as Error)
23933
- ) {
23934
- return;
23935
- }
23936
- throw error;
23937
- }
23938
- if (
23939
- !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23940
- !ownsSubscriptionEpoch()
23941
- ) {
23942
- return;
23943
- }
23944
- if (replicationSegments.length > 0) {
23945
- const segments = replicationSegments.map((x) => x.toReplicationRange());
23946
- this.validatePersistedReplicationRangeSnapshot(segments);
23947
- await this.rpc
23948
- .send(
23949
- new AllReplicatingSegmentsMessage({
23950
- segments,
23951
- }),
23952
- {
23953
- mode: new AcknowledgeDelivery({
23954
- redundancy: 1,
23955
- to: [publicKey],
23956
- }),
23957
- signal: replicationLifecycleController.signal,
23958
- },
23959
- )
23960
- .catch((error) =>
23961
- this.handleReplicationLifecycleSendError(
23962
- error,
23963
- replicationLifecycleController,
23964
- ),
23965
- );
23966
- if (
23967
- !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23968
- !ownsSubscriptionEpoch()
23969
- ) {
23970
- return;
23971
- }
23972
-
23973
- if (this.v8Behaviour) {
23974
- // for backwards compatibility
23975
- await this.rpc
23976
- .send(
23977
- new ResponseRoleMessage({
23978
- role: this.getRoleFromReplicationSegments(replicationSegments),
23979
- }),
23980
- {
23981
- mode: new AcknowledgeDelivery({
23982
- redundancy: 1,
23983
- to: [publicKey],
23984
- }),
23985
- signal: replicationLifecycleController.signal,
23986
- },
23987
- )
23988
- .catch((error) =>
23989
- this.handleReplicationLifecycleSendError(
23990
- error,
23991
- replicationLifecycleController,
23992
- ),
23993
- );
23994
- }
23995
- }
23996
-
23997
- // Keep legacy request-based discovery independent of the capability ACK.
23998
- // This makes mixed-version joins resilient to timing-sensitive delivery/order
23999
- // issues where we may miss the remote peer's initial announcement.
24000
- if (
24001
- this.isReplicationLifecycleActive(replicationLifecycleController) &&
24002
- ownsSubscriptionEpoch()
24003
- ) {
24004
- this.scheduleReplicationInfoRequests(
24005
- publicKey,
24006
- replicationLifecycleController,
24007
- );
24008
- }
24009
- } finally {
24010
- localCapabilityAdvertisement.releaseLegacyBarrier();
24011
- }
23305
+ // B12: there is no legacy startup work to order ahead of RequestV2, so
23306
+ // the two-phase legacy barrier is gone — an ACKed advert promotes
23307
+ // readiness through the coordinator as soon as it lands.
23308
+ this._v2Receive.advertiseLocalCapability({
23309
+ target: publicKey,
23310
+ peerSession: expectedSubscriptionEpoch,
23311
+ receiveEpoch,
23312
+ signal: replicationLifecycleController.signal,
23313
+ });
23314
+ this.scheduleReplicationInfoV2Recovery(
23315
+ publicKey,
23316
+ replicationLifecycleController,
23317
+ );
24012
23318
  }
24013
23319
 
24014
23320
  private getClampedReplicas(customValue?: MinReplicas) {
@@ -25409,15 +24715,6 @@ export class SharedLog<
25409
24715
  const subscriptionEpoch = this._peerSessions.rotate(fromHash, "departing");
25410
24716
  this._peerSessions.blockReplicationInfo(fromHash);
25411
24717
  this._recentRepairDispatch.delete(fromHash);
25412
-
25413
- // Keep a per-peer timestamp watermark when we observe an unsubscribe. This
25414
- // prevents late/out-of-order replication-info messages from re-introducing
25415
- // stale segments for a peer that has already left the topic.
25416
- const now = BigInt(+new Date());
25417
- const prev = this.latestReplicationInfoMessage.get(fromHash);
25418
- if (!prev || prev < now) {
25419
- this.latestReplicationInfoMessage.set(fromHash, now);
25420
- }
25421
24718
  this.invalidateSharedLogTopicSubscribersCache();
25422
24719
 
25423
24720
  return this.handleSubscriptionChange(
@@ -25581,26 +24878,16 @@ export class SharedLog<
25581
24878
  return false;
25582
24879
  }
25583
24880
 
25584
- try {
25585
- await this.startAnnounceReplicating(
25586
- [dynamicRange],
25587
- {
25588
- checkDuplicates: false,
25589
- reset: false,
25590
- shouldApply: isCurrent,
25591
- },
25592
- ownershipLifecycleController,
25593
- );
25594
- if (!isCurrent()) return false;
25595
- } catch (error) {
25596
- if (
25597
- isTransientReplicationAnnouncementError(error) &&
25598
- this._announcements._replicationAnnouncementRetryPending
25599
- ) {
25600
- return false;
25601
- }
25602
- throw error;
25603
- }
24881
+ await this.startAnnounceReplicating(
24882
+ [dynamicRange],
24883
+ {
24884
+ checkDuplicates: false,
24885
+ reset: false,
24886
+ shouldApply: isCurrent,
24887
+ },
24888
+ ownershipLifecycleController,
24889
+ );
24890
+ if (!isCurrent()) return false;
25604
24891
 
25605
24892
  /* await this._updateRole(newRole, onRoleChange); */
25606
24893
  if (isCurrent()) {