@peerbit/shared-log 13.2.34 → 13.2.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -2086,7 +2086,11 @@ export class SharedLog<
2086
2086
  // reconnect barrier commits. See PeerSessionRegistry._replicationInfoBlockedPeers.
2087
2087
  private _replicationInfoRequestByPeer!: Map<
2088
2088
  string,
2089
- { attempts: number; timer?: ReturnType<typeof setTimeout> }
2089
+ {
2090
+ attempts: number;
2091
+ timer?: ReturnType<typeof setTimeout>;
2092
+ peerSession?: PeerSession;
2093
+ }
2090
2094
  >;
2091
2095
  private _replicationInfoApplyQueueByPeer!: Map<string, Promise<void>>;
2092
2096
  // Range ids are global primary keys while receive lanes are per peer. Keep
@@ -3382,6 +3386,7 @@ export class SharedLog<
3382
3386
  error,
3383
3387
  ),
3384
3388
  enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
3389
+ isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
3385
3390
  isClosed: () => this.closed,
3386
3391
  getCloseSignal: () => this._closeController.signal,
3387
3392
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
@@ -3740,6 +3745,14 @@ export class SharedLog<
3740
3745
  return this._logProperties?.compatibility;
3741
3746
  }
3742
3747
 
3748
+ /**
3749
+ * Legacy replication-info is an explicit compatibility fallback. Current
3750
+ * logs never infer or re-enable it from a remote peer's capabilities.
3751
+ */
3752
+ private get legacyReplicationInfoEnabled(): boolean {
3753
+ return this.compatibility !== undefined && this.compatibility < 10;
3754
+ }
3755
+
3743
3756
  get isAdaptiveReplicating() {
3744
3757
  return this._isAdaptiveReplicating;
3745
3758
  }
@@ -14163,7 +14176,9 @@ export class SharedLog<
14163
14176
  this.domain = options?.domain
14164
14177
  ? (options.domain(this) as unknown as D)
14165
14178
  : (createReplicationDomainHash(
14166
- options?.compatibility && options?.compatibility < 10 ? "u32" : "u64",
14179
+ options?.compatibility !== undefined && options.compatibility < 10
14180
+ ? "u32"
14181
+ : "u64",
14167
14182
  )(this) as unknown as D);
14168
14183
  this.indexableDomain = createIndexableDomainFromResolution(
14169
14184
  this.domain.resolution,
@@ -14309,8 +14324,12 @@ export class SharedLog<
14309
14324
  }
14310
14325
 
14311
14326
  this._closeController = new AbortController();
14312
- this._announcements.setupReplicationAnnouncementRetryFunction();
14313
- this._announcements.setupReplicationAnnouncementRepairFunction();
14327
+ if (this.legacyReplicationInfoEnabled) {
14328
+ this._announcements.setupReplicationAnnouncementRetryFunction();
14329
+ this._announcements.setupReplicationAnnouncementRepairFunction();
14330
+ } else {
14331
+ this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
14332
+ }
14314
14333
  this._closeController.signal.addEventListener("abort", () => {
14315
14334
  for (const [_peer, state] of this._replicationInfoRequestByPeer) {
14316
14335
  if (state.timer) clearTimeout(state.timer);
@@ -15512,7 +15531,14 @@ export class SharedLog<
15512
15531
 
15513
15532
  async afterOpen(): Promise<void> {
15514
15533
  await super.afterOpen();
15515
- const existingSubscribersPromise = this._getTopicSubscribers(this.topic);
15534
+ // Start the broader discovery eagerly, in parallel with rebalance, for its
15535
+ // routing/cache side effects. It also contains connected/provider/fanout
15536
+ // candidates that have not subscribed to this log, so only the authoritative
15537
+ // pubsub snapshot below may create subscription fallback sessions.
15538
+ const subscriberDiscoveryPromise = this._getTopicSubscribers(this.topic);
15539
+ const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(
15540
+ this.topic,
15541
+ );
15516
15542
  const replicationLifecycleController =
15517
15543
  this._instanceLifecycle?.membershipLifecycleController;
15518
15544
 
@@ -15533,6 +15559,7 @@ export class SharedLog<
15533
15559
  this._liveness.startReplicatorLivenessSweep();
15534
15560
 
15535
15561
  await this.rebalanceParticipation();
15562
+ await subscriberDiscoveryPromise;
15536
15563
 
15537
15564
  // Take into account existing subscription
15538
15565
  (await existingSubscribersPromise)?.forEach((v) => {
@@ -15542,6 +15569,14 @@ export class SharedLog<
15542
15569
  if (this.closed) {
15543
15570
  return;
15544
15571
  }
15572
+ // The live subscribe event and this after-open snapshot can report the
15573
+ // same initial transport generation. The live callback rotates its
15574
+ // PeerSession synchronously, so any current session here proves the
15575
+ // fallback is stale/duplicate. Rotating again would erase the signed
15576
+ // capability binding that the first callback just established.
15577
+ if (this._peerSessions.current(v.hashcode()) !== null) {
15578
+ return;
15579
+ }
15545
15580
  void this.runSubscriptionChangeCallback(() =>
15546
15581
  this.handleSubscriptionChange(v, [this.topic], true),
15547
15582
  );
@@ -15754,7 +15789,9 @@ export class SharedLog<
15754
15789
  const peerSession = this._peerSessions.current(peerHash);
15755
15790
  const preserveV2Session =
15756
15791
  peerSession?.phase === "open" && peerSession.isActive();
15757
- this.cancelReplicationInfoRequests(peerHash);
15792
+ if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
15793
+ this.cancelReplicationInfoRequests(peerHash);
15794
+ }
15758
15795
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15759
15796
  this._liveness._replicatorLastActivityAt.delete(peerHash);
15760
15797
  if (!preserveV2Session) {
@@ -16925,15 +16962,18 @@ export class SharedLog<
16925
16962
  }, 2_000);
16926
16963
  try {
16927
16964
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
16928
- await Promise.all([
16929
- this.rpc
16930
- .send(reset, {
16931
- priority: CONVERGENCE_MESSAGE_PRIORITY,
16932
- signal: abort.signal,
16933
- })
16934
- .catch(() => {}),
16935
- this._v2Send.sendTerminalReset(abort.signal),
16936
- ]);
16965
+ const resets = [this._v2Send.sendTerminalReset(abort.signal)];
16966
+ if (this.legacyReplicationInfoEnabled) {
16967
+ resets.push(
16968
+ this.rpc
16969
+ .send(reset, {
16970
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
16971
+ signal: abort.signal,
16972
+ })
16973
+ .catch(() => {}),
16974
+ );
16975
+ }
16976
+ await Promise.all(resets);
16937
16977
  } finally {
16938
16978
  clearTimeout(abortTimer);
16939
16979
  }
@@ -17055,15 +17095,18 @@ export class SharedLog<
17055
17095
  }, 2_000);
17056
17096
  try {
17057
17097
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17058
- await Promise.all([
17059
- this.rpc
17060
- .send(reset, {
17061
- priority: CONVERGENCE_MESSAGE_PRIORITY,
17062
- signal: abort.signal,
17063
- })
17064
- .catch(() => {}),
17065
- this._v2Send.sendTerminalReset(abort.signal),
17066
- ]);
17098
+ const resets = [this._v2Send.sendTerminalReset(abort.signal)];
17099
+ if (this.legacyReplicationInfoEnabled) {
17100
+ resets.push(
17101
+ this.rpc
17102
+ .send(reset, {
17103
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
17104
+ signal: abort.signal,
17105
+ })
17106
+ .catch(() => {}),
17107
+ );
17108
+ }
17109
+ await Promise.all(resets);
17067
17110
  } finally {
17068
17111
  clearTimeout(abortTimer);
17069
17112
  }
@@ -17589,6 +17632,19 @@ export class SharedLog<
17589
17632
  if (!context.from) {
17590
17633
  throw new Error("Missing from in update role message");
17591
17634
  }
17635
+ if (
17636
+ !this.legacyReplicationInfoEnabled &&
17637
+ (msg instanceof RequestReplicationInfoMessage ||
17638
+ msg instanceof ResponseRoleMessage ||
17639
+ msg instanceof AllReplicatingSegmentsMessage ||
17640
+ msg instanceof AddedReplicationSegmentMessage ||
17641
+ msg instanceof StoppedReplicating)
17642
+ ) {
17643
+ // These variants remain registered decode tombstones, but current logs
17644
+ // fail closed before leases, synchronizer work, liveness, watermarks or
17645
+ // mutations. Only an explicit pre-v10 compatibility open admits them.
17646
+ return;
17647
+ }
17592
17648
  // Snapshot receive ownership before any async handler gets a chance to
17593
17649
  // yield. Replication-info messages reach their branch only after the
17594
17650
  // synchronizer declines them, and a U/S transition can happen meanwhile.
@@ -19310,19 +19366,37 @@ export class SharedLog<
19310
19366
  timestamp: capabilityTimestamp,
19311
19367
  openingSession: receiveSession!,
19312
19368
  });
19313
- } else if (
19314
- this.observePeerSyncCapabilities({
19369
+ } else {
19370
+ const observed = this.observePeerSyncCapabilities({
19315
19371
  peerHash: receiveFromHash,
19316
19372
  capabilities: msg.capabilities,
19317
19373
  transportSession: capabilityTransportSession,
19318
19374
  timestamp: capabilityTimestamp,
19319
- }) &&
19320
- receiveSession?.phase === "open"
19321
- ) {
19322
- this.promoteReplicationInfoV2ReceiveCapability(
19323
- context.from,
19324
- receiveSession,
19325
- );
19375
+ });
19376
+ if (observed && receiveSession?.phase === "open") {
19377
+ this.promoteReplicationInfoV2ReceiveCapability(
19378
+ context.from,
19379
+ receiveSession,
19380
+ );
19381
+ } else if (observed && receiveSession === null) {
19382
+ // A capability can arrive before the sender's topic Subscribe after
19383
+ // reconnect. Ask that authenticated peer for its authoritative
19384
+ // subscriber snapshot; the resulting Subscribe creates the real
19385
+ // PeerSession and completes the symmetric capability handshake.
19386
+ // Never synthesize membership from capability traffic alone.
19387
+ void Promise.resolve()
19388
+ .then(() =>
19389
+ this.node.services.pubsub.requestSubscribers(
19390
+ this.topic,
19391
+ context.from,
19392
+ ),
19393
+ )
19394
+ .catch((error) => {
19395
+ if (!isNotStartedError(error as Error)) {
19396
+ logger.error(error?.toString?.() ?? String(error));
19397
+ }
19398
+ });
19399
+ }
19326
19400
  }
19327
19401
  }
19328
19402
  return;
@@ -19889,7 +19963,10 @@ export class SharedLog<
19889
19963
  return;
19890
19964
  }
19891
19965
  this._liveness.markReplicatorActivity(fromHash);
19892
- if (msg instanceof FullReplicationInfoV2Message) {
19966
+ if (
19967
+ msg instanceof FullReplicationInfoV2Message &&
19968
+ this.legacyReplicationInfoEnabled
19969
+ ) {
19893
19970
  this.cancelReplicationInfoRequests(fromHash);
19894
19971
  }
19895
19972
  });
@@ -20553,6 +20630,7 @@ export class SharedLog<
20553
20630
  }, timeoutMs);
20554
20631
 
20555
20632
  let requestAttempts = 0;
20633
+ let subscriberSnapshotInFlight: Promise<void> | undefined;
20556
20634
  const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
20557
20635
  const maxRequestAttempts =
20558
20636
  this.waitForReplicatorRequestMaxAttempts ??
@@ -20560,6 +20638,26 @@ export class SharedLog<
20560
20638
  WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
20561
20639
  Math.ceil(timeoutMs / requestIntervalMs),
20562
20640
  );
20641
+ const requestSubscriberSnapshot = () => {
20642
+ if (subscriberSnapshotInFlight) {
20643
+ return;
20644
+ }
20645
+ subscriberSnapshotInFlight = Promise.resolve()
20646
+ .then(async () => {
20647
+ if (settled || this.closed) {
20648
+ return;
20649
+ }
20650
+ await this.node.services.pubsub.requestSubscribers(this.topic, key);
20651
+ })
20652
+ .catch((error) => {
20653
+ if (!isNotStartedError(error as Error)) {
20654
+ logger.error(error?.toString?.() ?? String(error));
20655
+ }
20656
+ })
20657
+ .finally(() => {
20658
+ subscriberSnapshotInFlight = undefined;
20659
+ });
20660
+ };
20563
20661
 
20564
20662
  const requestReplicationInfo = () => {
20565
20663
  if (settled || this.closed) {
@@ -20572,17 +20670,35 @@ export class SharedLog<
20572
20670
 
20573
20671
  requestAttempts++;
20574
20672
 
20575
- this.rpc
20576
- .send(new RequestReplicationInfoMessage(), {
20577
- mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
20578
- })
20579
- .catch((e) => {
20580
- // Best-effort: missing peers / unopened RPC should not fail the wait logic.
20581
- if (isNotStartedError(e as Error)) {
20582
- return;
20583
- }
20584
- logger.error(e?.toString?.() ?? String(e));
20585
- });
20673
+ if (this.legacyReplicationInfoEnabled) {
20674
+ this.rpc
20675
+ .send(new RequestReplicationInfoMessage(), {
20676
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
20677
+ })
20678
+ .catch((e) => {
20679
+ // Best-effort: missing peers / unopened RPC should not fail the wait logic.
20680
+ if (isNotStartedError(e as Error)) {
20681
+ return;
20682
+ }
20683
+ logger.error(e?.toString?.() ?? String(e));
20684
+ });
20685
+ } else {
20686
+ const peerHash = key.hashcode();
20687
+ const peerSession = this._peerSessions.current(peerHash);
20688
+ if (peerSession?.phase === "open") {
20689
+ this._v2Receive.resumeParkedRequest({
20690
+ peerHash,
20691
+ peerSession,
20692
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
20693
+ });
20694
+ } else if (peerSession === null || peerSession.phase === "departing") {
20695
+ // A peer can be known to routing before its SharedLog topic
20696
+ // subscription has been observed. Legacy requests used to bootstrap
20697
+ // that case directly; V2 needs an authoritative Subscribe snapshot
20698
+ // before it can create a fenced PeerSession and request a Full.
20699
+ requestSubscriberSnapshot();
20700
+ }
20701
+ }
20586
20702
 
20587
20703
  if (requestAttempts < maxRequestAttempts) {
20588
20704
  requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
@@ -23316,6 +23432,73 @@ export class SharedLog<
23316
23432
  this._replicationInfoRequestByPeer.delete(peerHash);
23317
23433
  }
23318
23434
 
23435
+ private scheduleReplicationInfoV2Recovery(
23436
+ peer: PublicSignKey,
23437
+ replicationLifecycleController = this._instanceLifecycle
23438
+ ?.membershipLifecycleController,
23439
+ ) {
23440
+ if (
23441
+ !replicationLifecycleController ||
23442
+ !this.isReplicationLifecycleActive(replicationLifecycleController)
23443
+ ) {
23444
+ return;
23445
+ }
23446
+ const peerHash = peer.hashcode();
23447
+ const peerSession = this._peerSessions.current(peerHash);
23448
+ if (!peerSession || peerSession.phase !== "open") {
23449
+ return;
23450
+ }
23451
+ const requestStates = this._replicationInfoRequestByPeer;
23452
+ const existing = requestStates.get(peerHash);
23453
+ if (existing?.peerSession === peerSession) {
23454
+ return;
23455
+ }
23456
+ if (existing) {
23457
+ if (existing.timer) {
23458
+ clearTimeout(existing.timer);
23459
+ }
23460
+ requestStates.delete(peerHash);
23461
+ }
23462
+ const state: {
23463
+ attempts: number;
23464
+ timer?: ReturnType<typeof setTimeout>;
23465
+ peerSession: PeerSession;
23466
+ } = {
23467
+ attempts: 0,
23468
+ peerSession,
23469
+ };
23470
+ requestStates.set(peerHash, state);
23471
+ const cancel = () => {
23472
+ if (requestStates.get(peerHash) !== state) {
23473
+ return;
23474
+ }
23475
+ if (state.timer) {
23476
+ clearTimeout(state.timer);
23477
+ }
23478
+ requestStates.delete(peerHash);
23479
+ };
23480
+ const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
23481
+ const tick = () => {
23482
+ if (
23483
+ !this.isReplicationLifecycleActive(replicationLifecycleController) ||
23484
+ peerSession.phase !== "open" ||
23485
+ !this._peerSessions.isCurrent(peerHash, peerSession)
23486
+ ) {
23487
+ cancel();
23488
+ return;
23489
+ }
23490
+ this._v2Receive.resumeParkedRequest({
23491
+ peerHash,
23492
+ peerSession,
23493
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
23494
+ });
23495
+ state.attempts++;
23496
+ state.timer = setTimeout(tick, intervalMs);
23497
+ state.timer.unref?.();
23498
+ };
23499
+ tick();
23500
+ }
23501
+
23319
23502
  private scheduleReplicationInfoRequests(
23320
23503
  peer: PublicSignKey,
23321
23504
  replicationLifecycleController = this._instanceLifecycle
@@ -23327,6 +23510,13 @@ export class SharedLog<
23327
23510
  ) {
23328
23511
  return;
23329
23512
  }
23513
+ if (!this.legacyReplicationInfoEnabled) {
23514
+ this.scheduleReplicationInfoV2Recovery(
23515
+ peer,
23516
+ replicationLifecycleController,
23517
+ );
23518
+ return;
23519
+ }
23330
23520
  const peerHash = peer.hashcode();
23331
23521
  const requestStates = this._replicationInfoRequestByPeer;
23332
23522
  if (requestStates.has(peerHash)) {
@@ -23360,7 +23550,6 @@ export class SharedLog<
23360
23550
  cancel();
23361
23551
  return;
23362
23552
  }
23363
-
23364
23553
  state.attempts++;
23365
23554
 
23366
23555
  this.rpc
@@ -23397,6 +23586,7 @@ export class SharedLog<
23397
23586
  topics: string[],
23398
23587
  subscribed: boolean,
23399
23588
  subscriptionEpoch?: PeerSession,
23589
+ subscriptionTransportSession?: bigint,
23400
23590
  ) {
23401
23591
  if (!topics.includes(this.topic)) {
23402
23592
  return;
@@ -23419,13 +23609,31 @@ export class SharedLog<
23419
23609
  if (!ownsSubscriptionEpoch()) {
23420
23610
  return;
23421
23611
  }
23612
+ // A reconnect can arrive before the previous exact-session recovery tick
23613
+ // observes its stale session. Retire that job synchronously so it cannot
23614
+ // suppress the replacement session's scheduler in the shared peer slot.
23615
+ this.cancelReplicationInfoRequests(peerHash);
23422
23616
  // A destination stream is scoped to exactly one topic-subscription
23423
23617
  // session. Abort the predecessor synchronously before either barrier can
23424
23618
  // yield; a late queue completion must never enter the new session.
23425
23619
  this._v2Receive.clearPeer(peerHash);
23426
23620
  this._v2Send.clearPeer(peerHash);
23427
- this._peerSyncCapabilitySessions.delete(peerHash);
23428
- this._peerSyncCapabilityTimestamps.delete(peerHash);
23621
+ const capabilityTransportSession =
23622
+ this._peerSyncCapabilitySessions.get(peerHash);
23623
+ const canInheritCapability =
23624
+ subscribed &&
23625
+ (subscriptionTransportSession !== undefined
23626
+ ? capabilityTransportSession === subscriptionTransportSession
23627
+ : !expectedSubscriptionEpoch.hasPredecessor);
23628
+ if (!canInheritCapability) {
23629
+ // Departures and successor openings revoke the signed transport binding.
23630
+ // A live successor may inherit a capability that arrived just before its
23631
+ // Subscribe only when both signed frames belong to the same transport
23632
+ // generation. Snapshot fallbacks lack that proof, so only their first
23633
+ // opening may inherit pre-opening capability state.
23634
+ this._peerSyncCapabilitySessions.delete(peerHash);
23635
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
23636
+ }
23429
23637
  if (subscribed) {
23430
23638
  const pendingOpeningCapabilities =
23431
23639
  this._openingSyncCapabilitiesByPeer.get(peerHash);
@@ -23577,6 +23785,17 @@ export class SharedLog<
23577
23785
  receiveEpoch,
23578
23786
  signal: replicationLifecycleController.signal,
23579
23787
  });
23788
+ if (!this.legacyReplicationInfoEnabled) {
23789
+ // Current logs have no legacy startup work to order ahead of RequestV2.
23790
+ // Releasing is synchronous and exact-session fenced; the ACK may still
23791
+ // arrive later and promote readiness through the coordinator.
23792
+ localCapabilityAdvertisement.releaseLegacyBarrier();
23793
+ this.scheduleReplicationInfoV2Recovery(
23794
+ publicKey,
23795
+ replicationLifecycleController,
23796
+ );
23797
+ return;
23798
+ }
23580
23799
 
23581
23800
  try {
23582
23801
  let replicationSegments: ReplicationRangeIndexable<R>[];
@@ -25105,6 +25324,7 @@ export class SharedLog<
25105
25324
  evt.detail.topics,
25106
25325
  true,
25107
25326
  subscriptionEpoch,
25327
+ evt.detail.session,
25108
25328
  );
25109
25329
  }
25110
25330
 
@@ -31,6 +31,11 @@ export type PeerReceiveAdmissionOptions = {
31
31
  export class PeerSession {
32
32
  readonly peerHash: string;
33
33
  readonly kind: PeerSessionKind;
34
+ // True when rotate() superseded an earlier subscription generation. This is
35
+ // deliberately independent of the predecessor's phase/kind: a newer
36
+ // transport can announce a subscription without first delivering an
37
+ // unsubscribe, and must not inherit the old transport's signed capability.
38
+ readonly hasPredecessor: boolean;
34
39
  // The lifecycle controller live at rotation. All current seams pair the
35
40
  // epoch check with a lifecycle check against a controller captured in the
36
41
  // same synchronous window as the epoch advance; capturing it here
@@ -59,10 +64,12 @@ export class PeerSession {
59
64
  peerHash: string,
60
65
  kind: PeerSessionKind,
61
66
  replicationLifecycleController: AbortController | undefined,
67
+ hasPredecessor: boolean,
62
68
  ) {
63
69
  this.peerHash = peerHash;
64
70
  this.kind = kind;
65
71
  this.replicationLifecycleController = replicationLifecycleController;
72
+ this.hasPredecessor = hasPredecessor;
66
73
  this.phase = kind;
67
74
  }
68
75
 
@@ -278,6 +285,7 @@ export class PeerSessionRegistry {
278
285
  peerHash,
279
286
  kind,
280
287
  this.deps.getReplicationLifecycleController(),
288
+ previous !== undefined,
281
289
  );
282
290
  this.sessions.set(peerHash, next);
283
291
  return next;
@@ -204,9 +204,10 @@ export type ReplicationAnnouncementDeps<R extends "u32" | "u64"> = {
204
204
  // Owner-routed so coordinator spies keep observing re-entrant queueing.
205
205
  queueCurrentReplicationStateAnnouncementRepair: () => void;
206
206
  queueCurrentReplicationStateAnnouncementRetry: (error: unknown) => boolean;
207
- // Best-effort sidecar. The legacy publish remains the primary operation and
208
- // retains its exact rejection/retry semantics during mixed-version rollout.
207
+ // V2 is always current. Legacy publication is enabled only for an explicit
208
+ // compatibility open and retains its exact rejection/retry semantics there.
209
209
  enqueueReplicationInfoV2: (message: LegacyReplicationInfoMessage) => void;
210
+ isLegacyReplicationInfoEnabled: () => boolean;
210
211
  };
211
212
 
212
213
  type LegacyReplicationInfoMessage =
@@ -270,6 +271,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
270
271
 
271
272
  queueCurrentReplicationStateAnnouncementRetry(error: unknown): boolean {
272
273
  if (
274
+ !this.deps.isLegacyReplicationInfoEnabled() ||
273
275
  this.deps.isClosed() ||
274
276
  this.deps.getCloseSignal().aborted ||
275
277
  this._replicationAnnouncementRetryController.signal.aborted ||
@@ -362,6 +364,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
362
364
 
363
365
  queueCurrentReplicationStateAnnouncementRepair(): void {
364
366
  if (
367
+ !this.deps.isLegacyReplicationInfoEnabled() ||
365
368
  this.deps.isClosed() ||
366
369
  this.deps.getCloseSignal().aborted ||
367
370
  this._replicationAnnouncementRepairController.signal.aborted ||
@@ -437,6 +440,11 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
437
440
  async repairCurrentReplicationStateAnnouncement(
438
441
  context?: ReplicationAnnouncementRepairWorkerContext,
439
442
  ): Promise<void> {
443
+ if (!this.deps.isLegacyReplicationInfoEnabled()) {
444
+ this._announcementRepairBinding.pending = false;
445
+ this._announcementRepairBinding.targets.clear();
446
+ return;
447
+ }
440
448
  if (!this._announcementRepairBinding.pending) {
441
449
  return;
442
450
  }
@@ -645,6 +653,9 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
645
653
  this.rotateAnnouncementSession();
646
654
  this.advanceCurrentReplicationStateAnnouncementRepairGeneration();
647
655
  this.deps.enqueueReplicationInfoV2(message);
656
+ if (!this.deps.isLegacyReplicationInfoEnabled()) {
657
+ return;
658
+ }
648
659
  try {
649
660
  await this.deps.getRpc().send(message, {
650
661
  priority: CONVERGENCE_MESSAGE_PRIORITY,
@@ -677,6 +688,10 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
677
688
  }
678
689
 
679
690
  async retryCurrentReplicationStateAnnouncement(): Promise<void> {
691
+ if (!this.deps.isLegacyReplicationInfoEnabled()) {
692
+ this._replicationAnnouncementRetryPending = false;
693
+ return;
694
+ }
680
695
  const session = this._announcementSession;
681
696
  const controller = this._replicationAnnouncementRetryController;
682
697
  try {