@peerbit/shared-log 14.0.0 → 16.0.0

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 (36) hide show
  1. package/dist/src/index.d.ts +11 -15
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +84 -596
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/replication-announcement.d.ts +6 -110
  6. package/dist/src/replication-announcement.d.ts.map +1 -1
  7. package/dist/src/replication-announcement.js +2 -504
  8. package/dist/src/replication-announcement.js.map +1 -1
  9. package/dist/src/replication-info-mutation.d.ts +34 -0
  10. package/dist/src/replication-info-mutation.d.ts.map +1 -0
  11. package/dist/src/replication-info-mutation.js +2 -0
  12. package/dist/src/replication-info-mutation.js.map +1 -0
  13. package/dist/src/replication-info-v2-receive.d.ts +10 -41
  14. package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
  15. package/dist/src/replication-info-v2-receive.js +11 -192
  16. package/dist/src/replication-info-v2-receive.js.map +1 -1
  17. package/dist/src/replication-info-v2-send.d.ts +5 -6
  18. package/dist/src/replication-info-v2-send.d.ts.map +1 -1
  19. package/dist/src/replication-info-v2-send.js +9 -14
  20. package/dist/src/replication-info-v2-send.js.map +1 -1
  21. package/dist/src/replication.d.ts +0 -1
  22. package/dist/src/replication.d.ts.map +1 -1
  23. package/dist/src/replication.js +2 -17
  24. package/dist/src/replication.js.map +1 -1
  25. package/dist/src/sync/factory.d.ts +0 -1
  26. package/dist/src/sync/factory.d.ts.map +1 -1
  27. package/dist/src/sync/factory.js +20 -36
  28. package/dist/src/sync/factory.js.map +1 -1
  29. package/package.json +10 -10
  30. package/src/index.ts +115 -816
  31. package/src/replication-announcement.ts +12 -722
  32. package/src/replication-info-mutation.ts +34 -0
  33. package/src/replication-info-v2-receive.ts +17 -263
  34. package/src/replication-info-v2-send.ts +10 -23
  35. package/src/replication.ts +1 -20
  36. 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,
@@ -249,6 +246,10 @@ import {
249
246
  type ReplicationDomain,
250
247
  type ReplicationDomainConstructor,
251
248
  } from "./replication-domain.js";
249
+ import type {
250
+ AddedReplicationInfoMutation,
251
+ FullReplicationInfoMutation,
252
+ } from "./replication-info-mutation.js";
252
253
  import { ReplicationInfoV2ReceiveCoordinator } from "./replication-info-v2-receive.js";
253
254
  import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
254
255
  import {
@@ -273,7 +274,6 @@ import {
273
274
  maxReplicas,
274
275
  } from "./replication.js";
275
276
  import { ReplicatorLivenessMonitor } from "./replicator-liveness.js";
276
- import { Observer, Replicator } from "./role.js";
277
277
  import { createSyncronizer } from "./sync/factory.js";
278
278
  import type {
279
279
  SharedLogNativeWireSync,
@@ -2074,34 +2074,26 @@ export class SharedLog<
2074
2074
  // public key hash to range id to range
2075
2075
  pendingMaturity!: Map<string, Map<string, PendingMaturityRecord<R>>>; // map of peerId to timeout
2076
2076
 
2077
- // Stage-4 KEEP-OLD verdict (fence B8, split by role). The watermark's
2078
- // FENCING role rejecting late replication-info across unsubscribe and
2079
- // eviction races is fully subsumed by the per-peer receive epoch plus
2080
- // the blocked set and session identity: every unsubscribe-path `now` write
2081
- // is preceded in the same synchronous block by a blocked-add, so an
2082
- // admitted handler can never observe one. Its intra-epoch ORDERING role is
2083
- // NOT subsumed: within one (lifecycle, session, epoch, unblocked) regime
2084
- // the epoch token is constant across every message from the peer, so only
2085
- // the two apply-lane timestamp comparisons can drop an older reset
2086
- // delivered after a newer add (unordered pubsub / retransmits) — an
2087
- // identity token carries no order. Deletion is blocked until
2088
- // replication-info messages carry sender-authoritative sequence numbers
2089
- // (stage-5 schema change); the `receive admission replication-info
2090
- // ordering watermark` pins fail if the read sites are removed before then.
2091
- private latestReplicationInfoMessage!: Map<string, bigint>;
2077
+ // The legacy replication-info ordering watermark (fence B8) is deleted:
2078
+ // its FENCING role was subsumed by the per-peer receive epoch, blocked set
2079
+ // and session identity, and its intra-epoch ORDERING role existed only for
2080
+ // the legacy apply lanes. The V2 lane orders by sender-authoritative
2081
+ // sequence numbers, and legacy frames are dropped unconditionally at the
2082
+ // B1 gate before any side effect.
2092
2083
  // The replication-info blocked set (fence B5) lives on the peer-session
2093
2084
  // registry: unsubscribed peers whose replication-info is ignored until a
2094
2085
  // reconnect barrier commits. See PeerSessionRegistry._replicationInfoBlockedPeers.
2086
+ // V2 recovery scheduler state, one row per open peer session (B12: the
2087
+ // legacy request scheduler that shared this map is deleted).
2095
2088
  private _replicationInfoRequestByPeer!: Map<
2096
2089
  string,
2097
2090
  {
2098
- // Legacy scheduler: sends issued (bounded by maxAttempts). V2 recovery
2099
- // scheduler: consecutive fruitless unparks the escalation exponent
2100
- // for the next unpark delay, reset on any applied V2 progress.
2091
+ // Consecutive fruitless unparks the escalation exponent for the
2092
+ // next unpark delay, reset on any applied V2 progress.
2101
2093
  attempts: number;
2102
2094
  timer?: ReturnType<typeof setTimeout>;
2103
- peerSession?: PeerSession;
2104
- // V2 recovery scheduler only: when the current park was first observed.
2095
+ peerSession: PeerSession;
2096
+ // When the current park was first observed.
2105
2097
  parkedSinceMs?: number;
2106
2098
  }
2107
2099
  >;
@@ -2262,7 +2254,6 @@ export class SharedLog<
2262
2254
  for (const hash of this._checkedPrune?.retries.keys() ?? []) {
2263
2255
  this._checkedPrune.clearRetry(hash);
2264
2256
  }
2265
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
2266
2257
  this.joinWarmup.cancelAllJoinWarmupTargets();
2267
2258
  for (const timer of this._repairRetryTimers) {
2268
2259
  clearTimeout(timer);
@@ -3264,7 +3255,7 @@ export class SharedLog<
3264
3255
  private rebalanceParticipationDebounced:
3265
3256
  | ReturnType<typeof debounceFixedInterval>
3266
3257
  | undefined;
3267
- private _announcements!: ReplicationAnnouncementCoordinator<R>;
3258
+ private _announcements!: ReplicationAnnouncementCoordinator;
3268
3259
  private _v2Receive!: ReplicationInfoV2ReceiveCoordinator;
3269
3260
  private _v2Send!: ReplicationInfoV2SendCoordinator<R>;
3270
3261
 
@@ -3392,37 +3383,13 @@ export class SharedLog<
3392
3383
  });
3393
3384
  }
3394
3385
 
3395
- private createReplicationAnnouncementCoordinator(): ReplicationAnnouncementCoordinator<R> {
3396
- return new ReplicationAnnouncementCoordinator<R>({
3397
- // Route re-entrant queueing through the owner so coordinator spies keep
3398
- // observing it (the poison guard assertions in events.spec.ts depend on
3399
- // this).
3400
- queueCurrentReplicationStateAnnouncementRepair: () =>
3401
- this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
3402
- queueCurrentReplicationStateAnnouncementRetry: (error: unknown) =>
3403
- this._announcements.queueCurrentReplicationStateAnnouncementRetry(
3404
- error,
3405
- ),
3386
+ private createReplicationAnnouncementCoordinator(): ReplicationAnnouncementCoordinator {
3387
+ return new ReplicationAnnouncementCoordinator({
3406
3388
  enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
3407
- isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
3408
- isClosed: () => this.closed,
3409
- getCloseSignal: () => this._closeController.signal,
3410
- getMyReplicationSegments: () => this.getMyReplicationSegments(),
3411
- validatePersistedReplicationRangeSnapshot: (ranges) =>
3412
- this.validatePersistedReplicationRangeSnapshot(ranges),
3413
- getSubscribers: () =>
3414
- this.node.services.pubsub.getSubscribers(this.topic),
3415
- getSelfHash: () => this.node.identity.publicKey.hashcode(),
3416
- isBlockedPeer: (hash) =>
3417
- this._peerSessions.isReplicationInfoBlocked(hash),
3418
- getRpc: () => this.rpc,
3419
3389
  captureReplicationOwnershipLifecycle: () =>
3420
3390
  this.captureReplicationOwnershipLifecycle(),
3421
3391
  throwIfReplicationOwnershipLifecycleInactive: (controller) =>
3422
3392
  this.throwIfReplicationOwnershipLifecycleInactive(controller),
3423
- isAdaptiveReplicating: () => this._isAdaptiveReplicating,
3424
- callRebalanceParticipationDebounced: () =>
3425
- this.rebalanceParticipationDebounced?.call(),
3426
3393
  });
3427
3394
  }
3428
3395
 
@@ -3649,7 +3616,6 @@ export class SharedLog<
3649
3616
  this._admittedPruneRemoves = new Set();
3650
3617
  this._pendingIHave = new Map();
3651
3618
  this._pendingIHaveCallbacks = new Set();
3652
- this.latestReplicationInfoMessage = new Map();
3653
3619
  this._replicationInfoRequestByPeer = new Map();
3654
3620
  this._subscriberSnapshotRequestsByPeer = new Map();
3655
3621
  this._replicationInfoApplyQueueByPeer = new Map();
@@ -3760,29 +3726,10 @@ export class SharedLog<
3760
3726
  this._nativeStrictDurableTransactionsClosing ??= false;
3761
3727
  }
3762
3728
 
3763
- get compatibility(): number | undefined {
3764
- // B12: the open option was removed and any defined value rejects at
3765
- // open(); this is permanently undefined and dies with the residual
3766
- // gates in a later cleanup stage.
3767
- return (this._logProperties as any)?.compatibility;
3768
- }
3769
-
3770
- /**
3771
- * Legacy replication-info is an explicit compatibility fallback. Current
3772
- * logs never infer or re-enable it from a remote peer's capabilities.
3773
- */
3774
- private get legacyReplicationInfoEnabled(): boolean {
3775
- return this.compatibility !== undefined && this.compatibility < 10;
3776
- }
3777
-
3778
3729
  get isAdaptiveReplicating() {
3779
3730
  return this._isAdaptiveReplicating;
3780
3731
  }
3781
3732
 
3782
- private get v8Behaviour() {
3783
- return (this.compatibility ?? Number.MAX_VALUE) < 9;
3784
- }
3785
-
3786
3733
  private getFanoutChannelOptions(
3787
3734
  options?: SharedLogFanoutOptions,
3788
3735
  ): Omit<FanoutTreeChannelOptions, "role"> {
@@ -4687,8 +4634,7 @@ export class SharedLog<
4687
4634
  (leaders.size === 0 || (leaders.size === 1 && leaders.has(selfHash)))
4688
4635
  ) {
4689
4636
  const allowSubscriberFallback =
4690
- this.syncronizer instanceof SimpleSyncronizer ||
4691
- (this.compatibility ?? Number.MAX_VALUE) < 10;
4637
+ this.syncronizer instanceof SimpleSyncronizer;
4692
4638
  if (!allowSubscriberFallback) {
4693
4639
  return;
4694
4640
  }
@@ -4837,8 +4783,7 @@ export class SharedLog<
4837
4783
  const set = new Set(leaders.keys());
4838
4784
  let hasRemotePeers = set.has(selfHash) ? set.size > 1 : set.size > 0;
4839
4785
  const allowSubscriberFallback =
4840
- this.syncronizer instanceof SimpleSyncronizer ||
4841
- (this.compatibility ?? Number.MAX_VALUE) < 10;
4786
+ this.syncronizer instanceof SimpleSyncronizer;
4842
4787
  if (!hasRemotePeers && allowSubscriberFallback) {
4843
4788
  try {
4844
4789
  const subscribers = await this._getTopicSubscribers(this.topic);
@@ -5301,28 +5246,6 @@ export class SharedLog<
5301
5246
  };
5302
5247
  }
5303
5248
 
5304
- // @deprecated
5305
- private getRoleFromReplicationSegments(
5306
- segments: ReplicationRangeIndexable<R>[],
5307
- ) {
5308
- if (segments.length > 1) {
5309
- throw new Error(
5310
- "More than one replication segment found. Can only use one segment for compatbility with v8",
5311
- );
5312
- }
5313
-
5314
- if (segments.length > 0) {
5315
- const segment = segments[0].toReplicationRange();
5316
- return new Replicator({
5317
- factor: (segment.factor as number) / MAX_U32,
5318
- offset: (segment.offset as number) / MAX_U32,
5319
- });
5320
- }
5321
-
5322
- // TODO this is not accurate but might be good enough
5323
- return new Observer();
5324
- }
5325
-
5326
5249
  private isTerminating() {
5327
5250
  return (
5328
5251
  this.acceptsParentAttachments === false ||
@@ -5600,12 +5523,7 @@ export class SharedLog<
5600
5523
  }
5601
5524
 
5602
5525
  private onRebalanceParticipationError(error: Error): void {
5603
- if (
5604
- this.closed ||
5605
- isNotStartedError(error) ||
5606
- (isTransientReplicationAnnouncementError(error) &&
5607
- this._announcements._replicationAnnouncementRetryPending)
5608
- ) {
5526
+ if (this.closed || isNotStartedError(error)) {
5609
5527
  return;
5610
5528
  }
5611
5529
 
@@ -5714,7 +5632,7 @@ export class SharedLog<
5714
5632
  mergeSegments?: boolean;
5715
5633
  rebalance?: boolean;
5716
5634
  announce?: (
5717
- msg: AddedReplicationSegmentMessage | AllReplicatingSegmentsMessage,
5635
+ msg: AddedReplicationInfoMutation | FullReplicationInfoMutation,
5718
5636
  ) => void;
5719
5637
  } = {},
5720
5638
  replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
@@ -5950,9 +5868,11 @@ export class SharedLog<
5950
5868
 
5951
5869
  if (confirmedPreliminaryRemovals.length > 0) {
5952
5870
  await this._announcements.sendReplicationAnnouncement(
5953
- new StoppedReplicating({
5954
- segmentIds: confirmedPreliminaryRemovals.map((x) => x.id),
5955
- }),
5871
+ {
5872
+ stopped: {
5873
+ segmentIds: confirmedPreliminaryRemovals.map((x) => x.id),
5874
+ },
5875
+ },
5956
5876
  replicationOwnershipLifecycleController,
5957
5877
  );
5958
5878
  this.throwIfReplicationOwnershipLifecycleInactive(
@@ -5977,7 +5897,7 @@ export class SharedLog<
5977
5897
  );
5978
5898
  this.validatePersistedReplicationRangeSnapshot(segments);
5979
5899
  await this._announcements.sendReplicationAnnouncement(
5980
- new AllReplicatingSegmentsMessage({ segments }),
5900
+ { full: { segments } },
5981
5901
  replicationOwnershipLifecycleController,
5982
5902
  );
5983
5903
  } catch (error) {
@@ -6033,7 +5953,7 @@ export class SharedLog<
6033
5953
  rebalance?: boolean;
6034
5954
  mergeSegments?: boolean;
6035
5955
  announce?: (
6036
- msg: AllReplicatingSegmentsMessage | AddedReplicationSegmentMessage,
5956
+ msg: FullReplicationInfoMutation | AddedReplicationInfoMutation,
6037
5957
  ) => void;
6038
5958
  },
6039
5959
  ) {
@@ -6274,7 +6194,7 @@ export class SharedLog<
6274
6194
  }
6275
6195
  try {
6276
6196
  await this._announcements.sendReplicationAnnouncement(
6277
- new StoppedReplicating({ segmentIds: removedSegmentIds }),
6197
+ { stopped: { segmentIds: removedSegmentIds } },
6278
6198
  replicationOwnershipLifecycleController,
6279
6199
  );
6280
6200
  this.throwIfReplicationOwnershipLifecycleInactive(
@@ -6571,7 +6491,7 @@ export class SharedLog<
6571
6491
  if (isMe && !ownerHasRanges) {
6572
6492
  try {
6573
6493
  await this._announcements.sendReplicationAnnouncement(
6574
- new AllReplicatingSegmentsMessage({ segments: [] }),
6494
+ { full: { segments: [] } },
6575
6495
  replicationOwnershipLifecycleController,
6576
6496
  );
6577
6497
  } catch (error) {
@@ -7494,7 +7414,7 @@ export class SharedLog<
7494
7414
  rebalance?: boolean;
7495
7415
  shouldApply?: () => boolean;
7496
7416
  announce?: (
7497
- msg: AllReplicatingSegmentsMessage | AddedReplicationSegmentMessage,
7417
+ msg: FullReplicationInfoMutation | AddedReplicationInfoMutation,
7498
7418
  ) => void;
7499
7419
  } = {},
7500
7420
  replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
@@ -7541,7 +7461,7 @@ export class SharedLog<
7541
7461
  );
7542
7462
  this.validatePersistedReplicationRangeSnapshot(segments);
7543
7463
  await this._announcements.sendReplicationAnnouncement(
7544
- new AllReplicatingSegmentsMessage({ segments }),
7464
+ { full: { segments } },
7545
7465
  replicationOwnershipLifecycleController,
7546
7466
  );
7547
7467
  } catch (error) {
@@ -7592,17 +7512,17 @@ export class SharedLog<
7592
7512
  }
7593
7513
 
7594
7514
  let message:
7595
- | AllReplicatingSegmentsMessage
7596
- | AddedReplicationSegmentMessage
7515
+ | FullReplicationInfoMutation
7516
+ | AddedReplicationInfoMutation
7597
7517
  | undefined = undefined;
7598
7518
  if (options.reset) {
7599
- message = new AllReplicatingSegmentsMessage({
7600
- segments: added.map((x) => x.range.toReplicationRange()),
7601
- });
7519
+ message = {
7520
+ full: { segments: added.map((x) => x.range.toReplicationRange()) },
7521
+ };
7602
7522
  } else {
7603
- message = new AddedReplicationSegmentMessage({
7604
- segments: added.map((x) => x.range.toReplicationRange()),
7605
- });
7523
+ message = {
7524
+ added: { segments: added.map((x) => x.range.toReplicationRange()) },
7525
+ };
7606
7526
  }
7607
7527
  if (options.announce) {
7608
7528
  return options.announce(message);
@@ -14241,12 +14161,7 @@ export class SharedLog<
14241
14161
 
14242
14162
  this.domain = options?.domain
14243
14163
  ? (options.domain(this) as unknown as D)
14244
- : (createReplicationDomainHash(
14245
- (options as any)?.compatibility !== undefined &&
14246
- (options as any).compatibility < 10
14247
- ? "u32"
14248
- : "u64",
14249
- )(this) as unknown as D);
14164
+ : (createReplicationDomainHash("u64")(this) as unknown as D);
14250
14165
  this.indexableDomain = createIndexableDomainFromResolution(
14251
14166
  this.domain.resolution,
14252
14167
  );
@@ -14256,7 +14171,6 @@ export class SharedLog<
14256
14171
  this._admittedPruneRemoves = new Set();
14257
14172
  this._pendingIHave = new Map();
14258
14173
  this._pendingIHaveCallbacks = new Set();
14259
- this.latestReplicationInfoMessage = new Map();
14260
14174
  this._replicationInfoRequestByPeer = new Map();
14261
14175
  this._subscriberSnapshotRequestsByPeer = new Map();
14262
14176
  // Terminal close/drop drains the previous lifecycle before another open can
@@ -14329,7 +14243,6 @@ export class SharedLog<
14329
14243
  this._liveness.resetForOpen();
14330
14244
  this._lastLocalAppendAt = 0;
14331
14245
  this._announcements ??= this.createReplicationAnnouncementCoordinator();
14332
- this._announcements.resetForOpen();
14333
14246
  this._v2Receive ??= this.createReplicationInfoV2ReceiveCoordinator();
14334
14247
  this._v2Receive.resetForOpen();
14335
14248
  this._v2Send ??= this.createReplicationInfoV2SendCoordinator();
@@ -14392,12 +14305,6 @@ export class SharedLog<
14392
14305
  }
14393
14306
 
14394
14307
  this._closeController = new AbortController();
14395
- if (this.legacyReplicationInfoEnabled) {
14396
- this._announcements.setupReplicationAnnouncementRetryFunction();
14397
- this._announcements.setupReplicationAnnouncementRepairFunction();
14398
- } else {
14399
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
14400
- }
14401
14308
  this._closeController.signal.addEventListener("abort", () => {
14402
14309
  for (const [_peer, state] of this._replicationInfoRequestByPeer) {
14403
14310
  if (state.timer) clearTimeout(state.timer);
@@ -14853,7 +14760,6 @@ export class SharedLog<
14853
14760
  sendOptions?: { priority?: number; signal?: AbortSignal },
14854
14761
  ) => this.trySendFusedRawExchangeHeads(hashes, to, sendOptions),
14855
14762
  warn,
14856
- compatibility: (this._logProperties as any)?.compatibility,
14857
14763
  resolution: this.domain.resolution,
14858
14764
  sync: options?.sync,
14859
14765
  syncronizer: options?.syncronizer,
@@ -15857,7 +15763,7 @@ export class SharedLog<
15857
15763
  const peerSession = this._peerSessions.current(peerHash);
15858
15764
  const preserveV2Session =
15859
15765
  peerSession?.phase === "open" && peerSession.isActive();
15860
- if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
15766
+ if (!preserveV2Session) {
15861
15767
  this.cancelReplicationInfoRequests(peerHash);
15862
15768
  }
15863
15769
  this._liveness._replicatorLivenessFailures.delete(peerHash);
@@ -15889,11 +15795,8 @@ export class SharedLog<
15889
15795
 
15890
15796
  private advanceReplicationInfoRecoveryEpoch(peerHash: string) {
15891
15797
  // Handlers admitted before a successful peer removal must not restore state
15892
- // when they eventually reach the apply lane. Reset the sender's
15893
- // ordering watermark with the local epoch so a later arrival can be
15894
- // accepted without comparing its clock to this receiver's clock.
15798
+ // when they eventually reach the apply lane.
15895
15799
  const receiveEpoch = this._peerSessions.advanceReceiveEpoch(peerHash);
15896
- this.latestReplicationInfoMessage.delete(peerHash);
15897
15800
  const peerSession = this._peerSessions.current(peerHash);
15898
15801
  if (peerSession?.phase === "open") {
15899
15802
  this._v2Receive.advanceRecovery({
@@ -16782,15 +16685,6 @@ export class SharedLog<
16782
16685
  firstError ??= error;
16783
16686
  }
16784
16687
  };
16785
- // A borsh-deserialized instance that is closed before ever being opened
16786
- // has no coordinators (they are created in open()); the old inline code
16787
- // only touched plain fields here and never threw.
16788
- captureSync(() =>
16789
- this._announcements?.cancelCurrentReplicationStateAnnouncementRetry(),
16790
- );
16791
- if (this._announcements) {
16792
- this._announcements.replicationAnnouncementRetryDebounced = undefined;
16793
- }
16794
16688
  captureSync(() => {
16795
16689
  if (this._wireSyncSession) {
16796
16690
  this._wireSyncSession.unregisterTopic(this.topic);
@@ -16884,7 +16778,6 @@ export class SharedLog<
16884
16778
  captureSync(() => {
16885
16779
  this._pendingIHave?.clear();
16886
16780
  this._pendingIHaveCallbacks?.clear();
16887
- this.latestReplicationInfoMessage?.clear();
16888
16781
  this._peerSessions?.clearReceiveEpochsForClose();
16889
16782
  this._peerSessions?.clearCleanupGatesForClose();
16890
16783
  this._activeReceiveHandlersByPeer?.clear();
@@ -16985,7 +16878,6 @@ export class SharedLog<
16985
16878
  await pruneRemoveTerminalFence.drained;
16986
16879
  await this.drainPendingIHaveCallbacks();
16987
16880
  this.ensureNativeDurabilityRuntimeState();
16988
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
16989
16881
  } catch (error) {
16990
16882
  // The terminal preamble has already disabled parent attachments and the
16991
16883
  // network lifecycle. Keep mutation admission fenced for an exact retry.
@@ -17029,19 +16921,7 @@ export class SharedLog<
17029
16921
  }
17030
16922
  }, 2_000);
17031
16923
  try {
17032
- const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17033
- const resets = [this._v2Send.sendTerminalReset(abort.signal)];
17034
- if (this.legacyReplicationInfoEnabled) {
17035
- resets.push(
17036
- this.rpc
17037
- .send(reset, {
17038
- priority: CONVERGENCE_MESSAGE_PRIORITY,
17039
- signal: abort.signal,
17040
- })
17041
- .catch(() => {}),
17042
- );
17043
- }
17044
- await Promise.all(resets);
16924
+ await this._v2Send.sendTerminalReset(abort.signal);
17045
16925
  } finally {
17046
16926
  clearTimeout(abortTimer);
17047
16927
  }
@@ -17134,7 +17014,6 @@ export class SharedLog<
17134
17014
  await replicationRangeTerminalFence.drained;
17135
17015
  await pruneRemoveTerminalFence.drained;
17136
17016
  await this.drainPendingIHaveCallbacks();
17137
- this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
17138
17017
  } catch (error) {
17139
17018
  // The terminal preamble is not safely reversible. Preserve the fence until
17140
17019
  // a retry finishes cleanup.
@@ -17162,19 +17041,7 @@ export class SharedLog<
17162
17041
  }
17163
17042
  }, 2_000);
17164
17043
  try {
17165
- const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17166
- const resets = [this._v2Send.sendTerminalReset(abort.signal)];
17167
- if (this.legacyReplicationInfoEnabled) {
17168
- resets.push(
17169
- this.rpc
17170
- .send(reset, {
17171
- priority: CONVERGENCE_MESSAGE_PRIORITY,
17172
- signal: abort.signal,
17173
- })
17174
- .catch(() => {}),
17175
- );
17176
- }
17177
- await Promise.all(resets);
17044
+ await this._v2Send.sendTerminalReset(abort.signal);
17178
17045
  } finally {
17179
17046
  clearTimeout(abortTimer);
17180
17047
  }
@@ -17701,16 +17568,16 @@ export class SharedLog<
17701
17568
  throw new Error("Missing from in update role message");
17702
17569
  }
17703
17570
  if (
17704
- !this.legacyReplicationInfoEnabled &&
17705
- (msg instanceof RequestReplicationInfoMessage ||
17706
- msg instanceof ResponseRoleMessage ||
17707
- msg instanceof AllReplicatingSegmentsMessage ||
17708
- msg instanceof AddedReplicationSegmentMessage ||
17709
- msg instanceof StoppedReplicating)
17571
+ msg instanceof RequestReplicationInfoMessage ||
17572
+ msg instanceof ResponseRoleMessage ||
17573
+ msg instanceof AllReplicatingSegmentsMessage ||
17574
+ msg instanceof AddedReplicationSegmentMessage ||
17575
+ msg instanceof StoppedReplicating
17710
17576
  ) {
17711
- // These variants remain registered decode tombstones, but current logs
17712
- // fail closed before leases, synchronizer work, liveness, watermarks or
17713
- // mutations. Only an explicit pre-v10 compatibility open admits them.
17577
+ // These variants remain registered decode tombstones, but logs fail
17578
+ // closed unconditionally before leases, synchronizer work, liveness,
17579
+ // watermarks or mutations (B12: the compatibility opens that once
17580
+ // admitted them reject at open()).
17714
17581
  return;
17715
17582
  }
17716
17583
  // Snapshot receive ownership before any async handler gets a chance to
@@ -17751,22 +17618,14 @@ export class SharedLog<
17751
17618
  this.captureReplicationOwnershipLifecycle();
17752
17619
  const receiveReplicationInfoReceiveEpoch =
17753
17620
  this._peerSessions.receiveEpoch(receiveFromHash);
17754
- if (msg instanceof ResponseRoleMessage) {
17755
- msg = msg.toReplicationInfoMessage(); // migration
17756
- }
17757
17621
  if (
17758
- msg instanceof AllReplicatingSegmentsMessage ||
17759
- msg instanceof AddedReplicationSegmentMessage ||
17760
17622
  msg instanceof FullReplicationInfoV2Message ||
17761
17623
  msg instanceof AddedReplicationInfoV2Message
17762
17624
  ) {
17763
17625
  // Bound decoded untrusted vectors before per-peer/global mutation
17764
17626
  // queues, trusted-replicator authorization, or liveness side effects.
17765
17627
  this.validateReplicationRangeAnnouncement(msg.segments);
17766
- } else if (
17767
- msg instanceof StoppedReplicating ||
17768
- msg instanceof StoppedReplicationInfoV2Message
17769
- ) {
17628
+ } else if (msg instanceof StoppedReplicationInfoV2Message) {
17770
17629
  // Bound the raw decoded vector before deduplication can hide the
17771
17630
  // allocation cost, and before liveness or apply-queue side effects.
17772
17631
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
@@ -17774,10 +17633,7 @@ export class SharedLog<
17774
17633
  if (
17775
17634
  !context.from.equals(this.node.identity.publicKey) &&
17776
17635
  !(msg instanceof RequestReplicationInfoV2Message) &&
17777
- !isReplicationInfoV2Message(msg) &&
17778
- !(msg instanceof AllReplicatingSegmentsMessage) &&
17779
- !(msg instanceof AddedReplicationSegmentMessage) &&
17780
- !(msg instanceof StoppedReplicating)
17636
+ !isReplicationInfoV2Message(msg)
17781
17637
  ) {
17782
17638
  this._liveness.markReplicatorActivity(receiveFromHash);
17783
17639
  }
@@ -19492,23 +19348,6 @@ export class SharedLog<
19492
19348
  });
19493
19349
  } else if (msg instanceof ReplicationPingMessage) {
19494
19350
  // No-op: used as an ACKed unicast liveness probe.
19495
- } else if (msg instanceof RequestReplicationInfoMessage) {
19496
- await this.handleRequestReplicationInfo(
19497
- msg,
19498
- laneRequestContext,
19499
- lane,
19500
- );
19501
- } else if (
19502
- msg instanceof AllReplicatingSegmentsMessage ||
19503
- msg instanceof AddedReplicationSegmentMessage
19504
- ) {
19505
- await this.handleReplicationInfoAnnouncement(
19506
- msg,
19507
- laneRequestContext,
19508
- lane,
19509
- );
19510
- } else if (msg instanceof StoppedReplicating) {
19511
- await this.handleStoppedReplicating(msg, laneRequestContext, lane);
19512
19351
  } else {
19513
19352
  throw new Error("Unexpected message");
19514
19353
  }
@@ -19737,113 +19576,6 @@ export class SharedLog<
19737
19576
  }
19738
19577
  }
19739
19578
 
19740
- private async handleRequestReplicationInfo(
19741
- _msg: RequestReplicationInfoMessage,
19742
- context: ReceiveRequestContext,
19743
- lane: ReceiveLaneContext,
19744
- ): Promise<void> {
19745
- const receiveFromHash = lane.fromHash;
19746
- const receiveSession = lane.session;
19747
- const receiveReplicationLifecycleController = lane.lifecycleController;
19748
- if (context.from.equals(this.node.identity.publicKey)) {
19749
- return;
19750
- }
19751
- const replicationLifecycleController =
19752
- receiveReplicationLifecycleController;
19753
- if (
19754
- !replicationLifecycleController ||
19755
- !this._peerSessions.isReceiveAdmissionOpen(
19756
- receiveFromHash,
19757
- receiveSession,
19758
- replicationLifecycleController,
19759
- )
19760
- ) {
19761
- return;
19762
- }
19763
-
19764
- let replicationSegments: ReplicationRangeIndexable<R>[];
19765
- try {
19766
- replicationSegments = await this.getMyReplicationSegments();
19767
- } catch (error) {
19768
- if (
19769
- !this._peerSessions.isReceiveAdmissionOpen(
19770
- receiveFromHash,
19771
- receiveSession,
19772
- replicationLifecycleController,
19773
- ) &&
19774
- isNotStartedError(error as Error)
19775
- ) {
19776
- return;
19777
- }
19778
- throw error;
19779
- }
19780
- if (
19781
- !this._peerSessions.isReceiveAdmissionOpen(
19782
- receiveFromHash,
19783
- receiveSession,
19784
- replicationLifecycleController,
19785
- )
19786
- ) {
19787
- return;
19788
- }
19789
- const segments = replicationSegments.map((x) => x.toReplicationRange());
19790
- this.validatePersistedReplicationRangeSnapshot(segments);
19791
- this._v2Send.enqueueSnapshotForPeer(receiveFromHash);
19792
-
19793
- await this.rpc
19794
- .send(new AllReplicatingSegmentsMessage({ segments }), {
19795
- mode: new AcknowledgeDelivery({
19796
- to: [context.from],
19797
- redundancy: 1,
19798
- }),
19799
- signal: replicationLifecycleController.signal,
19800
- })
19801
- .catch((error) =>
19802
- this.handleReplicationLifecycleSendError(
19803
- error,
19804
- replicationLifecycleController,
19805
- ),
19806
- );
19807
- if (
19808
- !this._peerSessions.isReceiveAdmissionOpen(
19809
- receiveFromHash,
19810
- receiveSession,
19811
- replicationLifecycleController,
19812
- )
19813
- ) {
19814
- return;
19815
- }
19816
-
19817
- // for backwards compatibility (v8) remove this when we are sure that all nodes are v9+
19818
- if (this.v8Behaviour) {
19819
- const role = this.getRoleFromReplicationSegments(replicationSegments);
19820
- if (role instanceof Replicator) {
19821
- const fixedSettings = !this._isAdaptiveReplicating;
19822
- if (fixedSettings) {
19823
- await this.rpc
19824
- .send(
19825
- new ResponseRoleMessage({
19826
- role,
19827
- }),
19828
- {
19829
- mode: new SilentDelivery({
19830
- to: [context.from],
19831
- redundancy: 1,
19832
- }),
19833
- signal: replicationLifecycleController.signal,
19834
- },
19835
- )
19836
- .catch((error) =>
19837
- this.handleReplicationLifecycleSendError(
19838
- error,
19839
- replicationLifecycleController,
19840
- ),
19841
- );
19842
- }
19843
- }
19844
- }
19845
- }
19846
-
19847
19579
  private async handleReplicationInfoV2Announcement(
19848
19580
  msg: ReplicationInfoV2Message,
19849
19581
  context: ReceiveRequestContext,
@@ -20023,251 +19755,12 @@ export class SharedLog<
20023
19755
  // A committed V2 announcement is applied progress: the peer answers,
20024
19756
  // so recovery re-solicitation may restart from the base interval.
20025
19757
  this.resetReplicationInfoV2RecoveryEscalation(fromHash);
20026
- if (
20027
- msg instanceof FullReplicationInfoV2Message &&
20028
- this.legacyReplicationInfoEnabled
20029
- ) {
20030
- this.cancelReplicationInfoRequests(fromHash);
20031
- }
20032
19758
  });
20033
19759
  } finally {
20034
19760
  this._v2Receive.release(admission);
20035
19761
  }
20036
19762
  }
20037
19763
 
20038
- private async handleReplicationInfoAnnouncement(
20039
- msg: AllReplicatingSegmentsMessage | AddedReplicationSegmentMessage,
20040
- context: ReceiveRequestContext,
20041
- lane: ReceiveLaneContext,
20042
- ): Promise<void> {
20043
- const receiveFromHash = lane.fromHash;
20044
- const receiveSession = lane.session;
20045
- const receiveReplicationLifecycleController = lane.lifecycleController;
20046
- const receiveReplicationInfoReceiveEpoch = lane.receiveEpoch;
20047
- const peerReceiveLease = lane.lease;
20048
- if (context.from.equals(this.node.identity.publicKey)) {
20049
- return;
20050
- }
20051
-
20052
- const replicationInfoMessage = msg as
20053
- | AllReplicatingSegmentsMessage
20054
- | AddedReplicationSegmentMessage;
20055
-
20056
- // Process replication updates even if the sender isn't yet considered "ready" by
20057
- // `Program.waitFor()`. Dropping these messages can lead to missing replicator info
20058
- // (and downstream `waitForReplicator()` timeouts) under timing-sensitive joins.
20059
- const from = context.from!;
20060
- const fromHash = from.hashcode();
20061
- if (this._v2Receive.isLegacyCutover(receiveSession)) {
20062
- if (receiveSession) {
20063
- this._v2Receive.noteLegacyAnnouncement({
20064
- peerHash: fromHash,
20065
- peerSession: receiveSession,
20066
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20067
- senderTransportSession: context.message.header.session,
20068
- transportTimestamp: context.message.header.timestamp,
20069
- message: msg,
20070
- });
20071
- }
20072
- return;
20073
- }
20074
- // Pre-lane gate: lifecycle -> receive-epoch -> blocked, exactly the
20075
- // legacy order. isMembershipActiveFor is the unit-pinned fold of
20076
- // isReplicationLifecycleActive; isReceiveEpochCurrent is the
20077
- // relocated `===`-with-`?? null`. The DELIBERATE absence of a
20078
- // subscription-epoch term is preserved: the lease already validated
20079
- // it, and the in-lane recheck owns post-await staleness.
20080
- if (
20081
- !this._instanceLifecycle!.isMembershipActiveFor(
20082
- receiveReplicationLifecycleController,
20083
- ) ||
20084
- !this._peerSessions.isReceiveEpochCurrent(
20085
- receiveFromHash,
20086
- receiveReplicationInfoReceiveEpoch,
20087
- ) ||
20088
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20089
- ) {
20090
- return;
20091
- }
20092
- const messageTimestamp = context.message.header.timestamp;
20093
- peerReceiveLease.release();
20094
- await this.withReplicationInfoApplyQueue(fromHash, async () => {
20095
- try {
20096
- // The peer may have unsubscribed after this message was queued.
20097
- // In-lane gate: lifecycle -> subscription-epoch -> receive-epoch
20098
- // -> blocked, term for term as before the session migration.
20099
- if (
20100
- !this._instanceLifecycle!.isMembershipActiveFor(
20101
- receiveReplicationLifecycleController,
20102
- ) ||
20103
- !this._peerSessions.isCurrent(fromHash, receiveSession) ||
20104
- !this._peerSessions.isReceiveEpochCurrent(
20105
- fromHash,
20106
- receiveReplicationInfoReceiveEpoch,
20107
- ) ||
20108
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20109
- ) {
20110
- return;
20111
- }
20112
- if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
20113
- this._v2Receive.noteLegacyAnnouncement({
20114
- peerHash: fromHash,
20115
- peerSession: receiveSession,
20116
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20117
- senderTransportSession: context.message.header.session,
20118
- transportTimestamp: context.message.header.timestamp,
20119
- message: msg,
20120
- });
20121
- return;
20122
- }
20123
-
20124
- // Process in-order to avoid races where repeated reset messages arrive
20125
- // concurrently and trigger spurious "added" diffs / rebalancing.
20126
- const prev = this.latestReplicationInfoMessage.get(fromHash);
20127
- if (prev && prev > messageTimestamp) {
20128
- return;
20129
- }
20130
-
20131
- this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
20132
-
20133
- if (this.closed) {
20134
- return;
20135
- }
20136
-
20137
- const reset = msg instanceof AllReplicatingSegmentsMessage;
20138
- const result = await this.addReplicationRange(
20139
- replicationInfoMessage.segments.map((x) =>
20140
- x.toReplicationRangeIndexable(from),
20141
- ),
20142
- from,
20143
- {
20144
- reset,
20145
- checkDuplicates: true,
20146
- timestamp: Number(messageTimestamp),
20147
- allowLegacyOrderedReplacementPairs:
20148
- msg instanceof AddedReplicationSegmentMessage,
20149
- },
20150
- );
20151
- if (result === undefined) {
20152
- return;
20153
- }
20154
- this._liveness.markReplicatorActivity(fromHash);
20155
-
20156
- // If the peer reports any replication segments, stop re-requesting.
20157
- // (Empty reports can be transient during startup.)
20158
- if (replicationInfoMessage.segments.length > 0) {
20159
- this.cancelReplicationInfoRequests(fromHash);
20160
- }
20161
- } catch (e) {
20162
- if (isNotStartedError(e as Error)) {
20163
- return;
20164
- }
20165
- logger.error(
20166
- `Failed to apply replication settings from '${fromHash}': ${
20167
- (e as any)?.message ?? e
20168
- }`,
20169
- );
20170
- }
20171
- });
20172
- }
20173
-
20174
- private async handleStoppedReplicating(
20175
- msg: StoppedReplicating,
20176
- context: ReceiveRequestContext,
20177
- lane: ReceiveLaneContext,
20178
- ): Promise<void> {
20179
- const receiveFromHash = lane.fromHash;
20180
- const receiveSession = lane.session;
20181
- const receiveReplicationLifecycleController = lane.lifecycleController;
20182
- const receiveReplicationInfoReceiveEpoch = lane.receiveEpoch;
20183
- const peerReceiveLease = lane.lease;
20184
- const from = context.from!;
20185
- const segmentIds = msg.segmentIds;
20186
- if (from.equals(this.node.identity.publicKey)) {
20187
- return;
20188
- }
20189
- const fromHash = from.hashcode();
20190
- if (this._v2Receive.isLegacyCutover(receiveSession)) {
20191
- if (receiveSession) {
20192
- this._v2Receive.noteLegacyAnnouncement({
20193
- peerHash: fromHash,
20194
- peerSession: receiveSession,
20195
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20196
- senderTransportSession: context.message.header.session,
20197
- transportTimestamp: context.message.header.timestamp,
20198
- message: msg,
20199
- });
20200
- }
20201
- return;
20202
- }
20203
- // Same pre-lane gate shape as Added/All above (and the same
20204
- // intentional absence of a subscription-epoch term).
20205
- if (
20206
- !this._instanceLifecycle!.isMembershipActiveFor(
20207
- receiveReplicationLifecycleController,
20208
- ) ||
20209
- !this._peerSessions.isReceiveEpochCurrent(
20210
- receiveFromHash,
20211
- receiveReplicationInfoReceiveEpoch,
20212
- ) ||
20213
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20214
- ) {
20215
- return;
20216
- }
20217
- const messageTimestamp = context.message.header.timestamp;
20218
- peerReceiveLease.release();
20219
- await this.withReplicationInfoApplyQueue(fromHash, async () => {
20220
- if (
20221
- !this._instanceLifecycle!.isMembershipActiveFor(
20222
- receiveReplicationLifecycleController,
20223
- ) ||
20224
- !this._peerSessions.isCurrent(fromHash, receiveSession) ||
20225
- !this._peerSessions.isReceiveEpochCurrent(
20226
- fromHash,
20227
- receiveReplicationInfoReceiveEpoch,
20228
- ) ||
20229
- this._peerSessions.isReplicationInfoBlocked(fromHash)
20230
- ) {
20231
- return;
20232
- }
20233
- if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
20234
- this._v2Receive.noteLegacyAnnouncement({
20235
- peerHash: fromHash,
20236
- peerSession: receiveSession,
20237
- receiveEpoch: receiveReplicationInfoReceiveEpoch,
20238
- senderTransportSession: context.message.header.session,
20239
- transportTimestamp: context.message.header.timestamp,
20240
- message: msg,
20241
- });
20242
- return;
20243
- }
20244
-
20245
- const previousTimestamp = this.latestReplicationInfoMessage.get(fromHash);
20246
- if (previousTimestamp && previousTimestamp > messageTimestamp) {
20247
- return;
20248
- }
20249
- this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
20250
- if (this.closed) {
20251
- return;
20252
- }
20253
-
20254
- const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(
20255
- segmentIds,
20256
- from,
20257
- );
20258
-
20259
- await this.removeReplicationRanges(rangesToRemove, from);
20260
- this._liveness.markReplicatorActivity(fromHash);
20261
- const timestamp = BigInt(+new Date());
20262
- for (const range of rangesToRemove) {
20263
- this.replicationChangeDebounceFn.add({
20264
- range,
20265
- type: "removed",
20266
- timestamp,
20267
- });
20268
- }
20269
- });
20270
- }
20271
19764
 
20272
19765
  async calculateTotalParticipation(options?: { sum?: boolean }) {
20273
19766
  if (options?.sum) {
@@ -20556,7 +20049,7 @@ export class SharedLog<
20556
20049
  throwIfInactive();
20557
20050
 
20558
20051
  if (options?.replicate) {
20559
- let messageToSend: AddedReplicationSegmentMessage | undefined = undefined;
20052
+ let messageToSend: AddedReplicationInfoMutation | undefined = undefined;
20560
20053
 
20561
20054
  if (assumeSynced) {
20562
20055
  throwIfInactive();
@@ -20584,14 +20077,14 @@ export class SharedLog<
20584
20077
  // in one large message instead
20585
20078
  announce: (msg) => {
20586
20079
  throwIfInactive();
20587
- if (msg instanceof AllReplicatingSegmentsMessage) {
20080
+ if (!("added" in msg)) {
20588
20081
  throw new Error("Unexpected");
20589
20082
  }
20590
20083
 
20591
20084
  if (messageToSend) {
20592
20085
  // merge segments to make it into one messages
20593
- for (const segment of msg.segments) {
20594
- messageToSend.segments.push(segment);
20086
+ for (const segment of msg.added.segments) {
20087
+ messageToSend.added.segments.push(segment);
20595
20088
  }
20596
20089
  } else {
20597
20090
  messageToSend = msg;
@@ -20730,34 +20223,20 @@ export class SharedLog<
20730
20223
 
20731
20224
  requestAttempts++;
20732
20225
 
20733
- if (this.legacyReplicationInfoEnabled) {
20734
- this.rpc
20735
- .send(new RequestReplicationInfoMessage(), {
20736
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
20737
- })
20738
- .catch((e) => {
20739
- // Best-effort: missing peers / unopened RPC should not fail the wait logic.
20740
- if (isNotStartedError(e as Error)) {
20741
- return;
20742
- }
20743
- logger.error(e?.toString?.() ?? String(e));
20744
- });
20745
- } else {
20746
- const peerHash = key.hashcode();
20747
- const peerSession = this._peerSessions.current(peerHash);
20748
- if (peerSession?.phase === "open") {
20749
- this._v2Receive.resumeParkedRequest({
20750
- peerHash,
20751
- peerSession,
20752
- receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
20753
- });
20754
- } else if (peerSession === null || peerSession.phase === "departing") {
20755
- // A peer can be known to routing before its SharedLog topic
20756
- // subscription has been observed. Legacy requests used to bootstrap
20757
- // that case directly; V2 needs an authoritative Subscribe snapshot
20758
- // before it can create a fenced PeerSession and request a Full.
20759
- requestSubscriberSnapshot();
20760
- }
20226
+ const peerHash = key.hashcode();
20227
+ const peerSession = this._peerSessions.current(peerHash);
20228
+ if (peerSession?.phase === "open") {
20229
+ this._v2Receive.resumeParkedRequest({
20230
+ peerHash,
20231
+ peerSession,
20232
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
20233
+ });
20234
+ } else if (peerSession === null || peerSession.phase === "departing") {
20235
+ // A peer can be known to routing before its SharedLog topic
20236
+ // subscription has been observed. Legacy requests used to bootstrap
20237
+ // that case directly; V2 needs an authoritative Subscribe snapshot
20238
+ // before it can create a fenced PeerSession and request a Full.
20239
+ requestSubscriberSnapshot();
20761
20240
  }
20762
20241
 
20763
20242
  if (requestAttempts < maxRequestAttempts) {
@@ -23501,7 +22980,7 @@ export class SharedLog<
23501
22980
  */
23502
22981
  private resetReplicationInfoV2RecoveryEscalation(peerHash: string) {
23503
22982
  const state = this._replicationInfoRequestByPeer.get(peerHash);
23504
- if (!state || state.peerSession === undefined) {
22983
+ if (!state) {
23505
22984
  return;
23506
22985
  }
23507
22986
  state.attempts = 0;
@@ -23618,6 +23097,13 @@ export class SharedLog<
23618
23097
  tick();
23619
23098
  }
23620
23099
 
23100
+ /**
23101
+ * Collapsed B12 shell: the legacy request-polling body (bounded
23102
+ * RequestReplicationInfoMessage ticks) is deleted; V2 recovery is the
23103
+ * only scheduler. Retained as a named seam rather than inlined at the
23104
+ * callers because the liveness monitor wiring and several suites
23105
+ * stub/spy it by name.
23106
+ */
23621
23107
  private scheduleReplicationInfoRequests(
23622
23108
  peer: PublicSignKey,
23623
23109
  replicationLifecycleController = this._instanceLifecycle
@@ -23629,75 +23115,10 @@ export class SharedLog<
23629
23115
  ) {
23630
23116
  return;
23631
23117
  }
23632
- if (!this.legacyReplicationInfoEnabled) {
23633
- this.scheduleReplicationInfoV2Recovery(
23634
- peer,
23635
- replicationLifecycleController,
23636
- );
23637
- return;
23638
- }
23639
- const peerHash = peer.hashcode();
23640
- const requestStates = this._replicationInfoRequestByPeer;
23641
- if (requestStates.has(peerHash)) {
23642
- return;
23643
- }
23644
-
23645
- const state: { attempts: number; timer?: ReturnType<typeof setTimeout> } = {
23646
- attempts: 0,
23647
- };
23648
- requestStates.set(peerHash, state);
23649
- const cancel = () => {
23650
- if (requestStates.get(peerHash) !== state) {
23651
- return;
23652
- }
23653
- if (state.timer) {
23654
- clearTimeout(state.timer);
23655
- }
23656
- requestStates.delete(peerHash);
23657
- };
23658
-
23659
- const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
23660
- const maxAttempts =
23661
- this.waitForReplicatorRequestMaxAttempts ??
23662
- Math.max(
23663
- WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
23664
- Math.ceil(this.waitForReplicatorTimeout / intervalMs),
23665
- );
23666
-
23667
- const tick = () => {
23668
- if (!this.isReplicationLifecycleActive(replicationLifecycleController)) {
23669
- cancel();
23670
- return;
23671
- }
23672
- state.attempts++;
23673
-
23674
- this.rpc
23675
- .send(new RequestReplicationInfoMessage(), {
23676
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [peer] }),
23677
- signal: replicationLifecycleController.signal,
23678
- })
23679
- .catch((e) => {
23680
- // Best-effort: missing peers / unopened RPC should not fail join flows.
23681
- if (
23682
- isNotStartedError(e as Error) ||
23683
- (replicationLifecycleController.signal.aborted &&
23684
- e instanceof AbortError)
23685
- ) {
23686
- return;
23687
- }
23688
- logger.error(e?.toString?.() ?? String(e));
23689
- });
23690
-
23691
- if (state.attempts >= maxAttempts) {
23692
- cancel();
23693
- return;
23694
- }
23695
-
23696
- state.timer = setTimeout(tick, intervalMs);
23697
- state.timer.unref?.();
23698
- };
23699
-
23700
- tick();
23118
+ this.scheduleReplicationInfoV2Recovery(
23119
+ peer,
23120
+ replicationLifecycleController,
23121
+ );
23701
23122
  }
23702
23123
 
23703
23124
  async handleSubscriptionChange(
@@ -23780,10 +23201,6 @@ export class SharedLog<
23780
23201
  ) {
23781
23202
  return;
23782
23203
  }
23783
- // The timestamp watermark belongs to the previous subscription epoch.
23784
- // Sender clocks are not synchronized, so carrying a local unsubscribe
23785
- // timestamp forward could reject every valid announcement after reconnect.
23786
- this.latestReplicationInfoMessage.delete(peerHash);
23787
23204
  this._pendingReplicatorLeaveByPeer.delete(peerHash);
23788
23205
  const openingCapabilities =
23789
23206
  this._openingSyncCapabilitiesByPeer.get(peerHash);
@@ -23837,12 +23254,6 @@ export class SharedLog<
23837
23254
  this.joinWarmup._warmupSessionsByTarget.get(peerHash) ?? null;
23838
23255
  this.joinWarmup.cancelJoinWarmupTarget(peerHash);
23839
23256
 
23840
- const now = BigInt(+new Date());
23841
- const previous = this.latestReplicationInfoMessage.get(peerHash);
23842
- if (!previous || previous < now) {
23843
- this.latestReplicationInfoMessage.set(peerHash, now);
23844
- }
23845
-
23846
23257
  let removed = false;
23847
23258
  try {
23848
23259
  // Unsubscribe can race with the peer's final replication reset message.
@@ -23893,116 +23304,23 @@ export class SharedLog<
23893
23304
 
23894
23305
  // Decode, sender and authenticated apply readiness are separate bits. An
23895
23306
  // ACKed capability advert is the local half of receiver-led negotiation;
23896
- // readiness is promoted only after the legacy startup path has completed.
23897
- // This preserves mixed-version discovery without putting capability ACK
23898
- // latency on the subscription callback's critical path.
23307
+ // readiness is promoted through the coordinator once the ACK arrives.
23308
+ // This keeps capability ACK latency off the subscription callback's
23309
+ // critical path.
23899
23310
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23900
- const localCapabilityAdvertisement =
23901
- this._v2Receive.advertiseLocalCapability({
23902
- target: publicKey,
23903
- peerSession: expectedSubscriptionEpoch,
23904
- receiveEpoch,
23905
- signal: replicationLifecycleController.signal,
23906
- });
23907
- if (!this.legacyReplicationInfoEnabled) {
23908
- // Current logs have no legacy startup work to order ahead of RequestV2.
23909
- // Releasing is synchronous and exact-session fenced; the ACK may still
23910
- // arrive later and promote readiness through the coordinator.
23911
- localCapabilityAdvertisement.releaseLegacyBarrier();
23912
- this.scheduleReplicationInfoV2Recovery(
23913
- publicKey,
23914
- replicationLifecycleController,
23915
- );
23916
- return;
23917
- }
23918
-
23919
- try {
23920
- let replicationSegments: ReplicationRangeIndexable<R>[];
23921
- try {
23922
- replicationSegments = await this.getMyReplicationSegments();
23923
- } catch (error) {
23924
- if (
23925
- !this.isReplicationLifecycleActive(replicationLifecycleController) &&
23926
- isNotStartedError(error as Error)
23927
- ) {
23928
- return;
23929
- }
23930
- throw error;
23931
- }
23932
- if (
23933
- !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23934
- !ownsSubscriptionEpoch()
23935
- ) {
23936
- return;
23937
- }
23938
- if (replicationSegments.length > 0) {
23939
- const segments = replicationSegments.map((x) => x.toReplicationRange());
23940
- this.validatePersistedReplicationRangeSnapshot(segments);
23941
- await this.rpc
23942
- .send(
23943
- new AllReplicatingSegmentsMessage({
23944
- segments,
23945
- }),
23946
- {
23947
- mode: new AcknowledgeDelivery({
23948
- redundancy: 1,
23949
- to: [publicKey],
23950
- }),
23951
- signal: replicationLifecycleController.signal,
23952
- },
23953
- )
23954
- .catch((error) =>
23955
- this.handleReplicationLifecycleSendError(
23956
- error,
23957
- replicationLifecycleController,
23958
- ),
23959
- );
23960
- if (
23961
- !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23962
- !ownsSubscriptionEpoch()
23963
- ) {
23964
- return;
23965
- }
23966
-
23967
- if (this.v8Behaviour) {
23968
- // for backwards compatibility
23969
- await this.rpc
23970
- .send(
23971
- new ResponseRoleMessage({
23972
- role: this.getRoleFromReplicationSegments(replicationSegments),
23973
- }),
23974
- {
23975
- mode: new AcknowledgeDelivery({
23976
- redundancy: 1,
23977
- to: [publicKey],
23978
- }),
23979
- signal: replicationLifecycleController.signal,
23980
- },
23981
- )
23982
- .catch((error) =>
23983
- this.handleReplicationLifecycleSendError(
23984
- error,
23985
- replicationLifecycleController,
23986
- ),
23987
- );
23988
- }
23989
- }
23990
-
23991
- // Keep legacy request-based discovery independent of the capability ACK.
23992
- // This makes mixed-version joins resilient to timing-sensitive delivery/order
23993
- // issues where we may miss the remote peer's initial announcement.
23994
- if (
23995
- this.isReplicationLifecycleActive(replicationLifecycleController) &&
23996
- ownsSubscriptionEpoch()
23997
- ) {
23998
- this.scheduleReplicationInfoRequests(
23999
- publicKey,
24000
- replicationLifecycleController,
24001
- );
24002
- }
24003
- } finally {
24004
- localCapabilityAdvertisement.releaseLegacyBarrier();
24005
- }
23311
+ // B12: there is no legacy startup work to order ahead of RequestV2, so
23312
+ // the two-phase legacy barrier is gone — an ACKed advert promotes
23313
+ // readiness through the coordinator as soon as it lands.
23314
+ this._v2Receive.advertiseLocalCapability({
23315
+ target: publicKey,
23316
+ peerSession: expectedSubscriptionEpoch,
23317
+ receiveEpoch,
23318
+ signal: replicationLifecycleController.signal,
23319
+ });
23320
+ this.scheduleReplicationInfoV2Recovery(
23321
+ publicKey,
23322
+ replicationLifecycleController,
23323
+ );
24006
23324
  }
24007
23325
 
24008
23326
  private getClampedReplicas(customValue?: MinReplicas) {
@@ -25403,15 +24721,6 @@ export class SharedLog<
25403
24721
  const subscriptionEpoch = this._peerSessions.rotate(fromHash, "departing");
25404
24722
  this._peerSessions.blockReplicationInfo(fromHash);
25405
24723
  this._recentRepairDispatch.delete(fromHash);
25406
-
25407
- // Keep a per-peer timestamp watermark when we observe an unsubscribe. This
25408
- // prevents late/out-of-order replication-info messages from re-introducing
25409
- // stale segments for a peer that has already left the topic.
25410
- const now = BigInt(+new Date());
25411
- const prev = this.latestReplicationInfoMessage.get(fromHash);
25412
- if (!prev || prev < now) {
25413
- this.latestReplicationInfoMessage.set(fromHash, now);
25414
- }
25415
24724
  this.invalidateSharedLogTopicSubscribersCache();
25416
24725
 
25417
24726
  return this.handleSubscriptionChange(
@@ -25575,26 +24884,16 @@ export class SharedLog<
25575
24884
  return false;
25576
24885
  }
25577
24886
 
25578
- try {
25579
- await this.startAnnounceReplicating(
25580
- [dynamicRange],
25581
- {
25582
- checkDuplicates: false,
25583
- reset: false,
25584
- shouldApply: isCurrent,
25585
- },
25586
- ownershipLifecycleController,
25587
- );
25588
- if (!isCurrent()) return false;
25589
- } catch (error) {
25590
- if (
25591
- isTransientReplicationAnnouncementError(error) &&
25592
- this._announcements._replicationAnnouncementRetryPending
25593
- ) {
25594
- return false;
25595
- }
25596
- throw error;
25597
- }
24887
+ await this.startAnnounceReplicating(
24888
+ [dynamicRange],
24889
+ {
24890
+ checkDuplicates: false,
24891
+ reset: false,
24892
+ shouldApply: isCurrent,
24893
+ },
24894
+ ownershipLifecycleController,
24895
+ );
24896
+ if (!isCurrent()) return false;
25598
24897
 
25599
24898
  /* await this._updateRole(newRole, onRoleChange); */
25600
24899
  if (isCurrent()) {