@peerbit/shared-log 13.2.31 → 13.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -54,7 +54,7 @@ import { CheckedPruneCoordinator, } from "./checked-prune.js";
54
54
  import { CPUUsageIntervalLag } from "./cpu.js";
55
55
  import { debouncedAccumulatorMap, } from "./debounce.js";
56
56
  import { NativeDurableCommitError, NoPeersError, isNotStartedError, } from "./errors.js";
57
- import { EXCHANGE_HEADS_REPAIR_HINT, EntryWithRefs, ExchangeHeadsMessage, MAX_RAW_EXCHANGE_MESSAGE_SIZE, RawEntryWithRefs, RawExchangeHeadsMessage, RequestIPrune, RequestIPruneV2, ResponseIPrune, ResponseIPruneV2, SYNC_CAPABILITY_RAW_EXCHANGE_HEADS, SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE, StashBackedRawExchangeHeadsMessage, SyncCapabilitiesMessage, collectRawExchangeHeadSendPlan, createExchangeHeadsMessages, createRawExchangeHeadsMessages, getExchangeHeadHash, getPreparedRawExchangeGid, getPreparedRawExchangeHashNumber, getPreparedRawExchangeHeadAppendFacts, getPreparedRawExchangeHeadGid, getPreparedRawExchangeHeadRequestedReplicas, getPreparedRawExchangeHeadShallowEntry, getPreparedRawExchangeHeadSignatureVerified, getPreparedRawExchangeNext, getPreparedRawExchangeRequestedReplicas, getPreparedRawExchangeTimestamp, getRawExchangeHeadByteLength, getRawExchangeHeadStashIndexes, initExchangeHeadEntry, isPreparedRawEntryWithRefs, isStashBackedRawExchangeHeadsMessage, materializeVerifiedRawExchangeHeadsMessage, } from "./exchange-heads.js";
57
+ import { EXCHANGE_HEADS_REPAIR_HINT, EntryWithRefs, ExchangeHeadsMessage, MAX_RAW_EXCHANGE_MESSAGE_SIZE, RawEntryWithRefs, RawExchangeHeadsMessage, RequestIPrune, RequestIPruneV2, ResponseIPrune, ResponseIPruneV2, SYNC_CAPABILITY_RAW_EXCHANGE_HEADS, SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY, SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE, SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND, StashBackedRawExchangeHeadsMessage, SyncCapabilitiesMessage, collectRawExchangeHeadSendPlan, createExchangeHeadsMessages, createRawExchangeHeadsMessages, getExchangeHeadHash, getPreparedRawExchangeGid, getPreparedRawExchangeHashNumber, getPreparedRawExchangeHeadAppendFacts, getPreparedRawExchangeHeadGid, getPreparedRawExchangeHeadRequestedReplicas, getPreparedRawExchangeHeadShallowEntry, getPreparedRawExchangeHeadSignatureVerified, getPreparedRawExchangeNext, getPreparedRawExchangeRequestedReplicas, getPreparedRawExchangeTimestamp, getRawExchangeHeadByteLength, getRawExchangeHeadStashIndexes, initExchangeHeadEntry, isPreparedRawEntryWithRefs, isStashBackedRawExchangeHeadsMessage, materializeVerifiedRawExchangeHeadsMessage, } from "./exchange-heads.js";
58
58
  import { FanoutEnvelope } from "./fanout-envelope.js";
59
59
  import { CoordinatePersistenceCoordinator, combineCoordinateDeleteHashes, isPromiseLike, mapMaybePromise, normalizedHashValues, } from "./coordinate-persistence.js";
60
60
  import { InstanceLifecycle } from "./instance-lifecycle.js";
@@ -67,10 +67,11 @@ import { PeerSessionRegistry, } from "./peer-session.js";
67
67
  import { PIDReplicationController } from "./pid.js";
68
68
  import { EntryReplicatedU32, EntryReplicatedU64, ReplicationIntent, ReplicationRangeIndexableU32, ReplicationRangeIndexableU64, ReplicationRangeMessage, appromixateCoverage, calculateCoverage, countCoveringRangesSameOwner, createAssignedRangesQuery, debounceAggregationChanges, getAllMergeCandiates, getCoverSet, getSamples, isEntryReplicated, isMatured, isReplicationRangeMessage, mergeRanges, minimumWidthToCover, shouldAssigneToRangeBoundary as shouldAssignToRangeBoundary, toRebalance, } from "./ranges.js";
69
69
  import { ReplicationAnnouncementCoordinator, isTransientReplicationAnnouncementError, } from "./replication-announcement.js";
70
+ import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
70
71
  import { createReplicationDomainHash, } from "./replication-domain-hash.js";
71
72
  import { createReplicationDomainTime, } from "./replication-domain-time.js";
72
73
  import {} from "./replication-domain.js";
73
- import { AbsoluteReplicas, AddedReplicationSegmentMessage, AllReplicatingSegmentsMessage, MinReplicas, ReplicationError, ReplicationPingMessage, RequestReplicationInfoMessage, ResponseRoleMessage, StoppedReplicating, decodeReplicas, encodeReplicas, isReplicationInfoV2Message, maxReplicas, } from "./replication.js";
74
+ import { AbsoluteReplicas, AddedReplicationSegmentMessage, AllReplicatingSegmentsMessage, MinReplicas, ReplicationError, ReplicationPingMessage, RequestReplicationInfoMessage, RequestReplicationInfoV2Message, ResponseRoleMessage, StoppedReplicating, decodeReplicas, encodeReplicas, isReplicationInfoV2Message, maxReplicas, } from "./replication.js";
74
75
  import { ReplicatorLivenessMonitor } from "./replicator-liveness.js";
75
76
  import { Observer, Replicator } from "./role.js";
76
77
  import { createSyncronizer } from "./sync/factory.js";
@@ -1608,6 +1609,7 @@ let SharedLog = (() => {
1608
1609
  // A fn that we can call many times that recalculates the participation role
1609
1610
  rebalanceParticipationDebounced;
1610
1611
  _announcements;
1612
+ _v2Send;
1611
1613
  // A fn for debouncing the calls for pruning
1612
1614
  pruneDebouncedFn;
1613
1615
  _checkedPruneAuditTimer;
@@ -1637,6 +1639,10 @@ let SharedLog = (() => {
1637
1639
  // Sync capability bits advertised by peers (SyncCapabilitiesMessage), keyed
1638
1640
  // by public key hash. Entries are dropped on unsubscribe/disconnect.
1639
1641
  _peerSyncCapabilities;
1642
+ // Signed transport session carried by the capability envelope. Kept in a
1643
+ // parallel map so existing capability-number consumers remain unchanged.
1644
+ _peerSyncCapabilitySessions;
1645
+ _peerSyncCapabilityTimestamps;
1640
1646
  // Pending live raw exchange-head gossip, coalesced per recipient set and
1641
1647
  // flushed at the end of the current event-loop turn (or when a batch cap
1642
1648
  // is hit). Only used when every recipient advertised raw capability.
@@ -1690,6 +1696,7 @@ let SharedLog = (() => {
1690
1696
  // this).
1691
1697
  queueCurrentReplicationStateAnnouncementRepair: () => this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
1692
1698
  queueCurrentReplicationStateAnnouncementRetry: (error) => this._announcements.queueCurrentReplicationStateAnnouncementRetry(error),
1699
+ enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
1693
1700
  isClosed: () => this.closed,
1694
1701
  getCloseSignal: () => this._closeController.signal,
1695
1702
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
@@ -1704,6 +1711,22 @@ let SharedLog = (() => {
1704
1711
  callRebalanceParticipationDebounced: () => this.rebalanceParticipationDebounced?.call(),
1705
1712
  });
1706
1713
  }
1714
+ createReplicationInfoV2SendCoordinator() {
1715
+ return new ReplicationInfoV2SendCoordinator({
1716
+ getRpc: () => this.rpc,
1717
+ getSelfKey: () => this.node.identity.publicKey,
1718
+ getSenderTransportSession: () => BigInt(this.node.services.pubsub.session),
1719
+ getMyReplicationSegments: () => this.getMyReplicationSegments(),
1720
+ validatePersistedReplicationRangeSnapshot: (ranges) => this.validatePersistedReplicationRangeSnapshot(ranges),
1721
+ isClosed: () => this.closed,
1722
+ isPeerSessionOpen: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession) &&
1723
+ peerSession.phase === "open" &&
1724
+ !this._peerSessions.isReplicationInfoBlocked(peerHash) &&
1725
+ this._peerSessions.isReceiveCleanupGateOpen(peerHash),
1726
+ captureReplicationOwnershipLifecycle: () => this.captureReplicationOwnershipLifecycle(),
1727
+ isReplicationOwnershipLifecycleActive: (controller) => this.isRepairLifecycleActive(controller),
1728
+ });
1729
+ }
1707
1730
  createReplicatorLivenessMonitor() {
1708
1731
  return new ReplicatorLivenessMonitor({
1709
1732
  // Sweep-driven probes and activity marks dispatch through the owner so
@@ -1798,6 +1821,8 @@ let SharedLog = (() => {
1798
1821
  this._appendBackfillPendingByTarget = new Map();
1799
1822
  this._topicSubscribersCache = new Map();
1800
1823
  this._peerSyncCapabilities = new Map();
1824
+ this._peerSyncCapabilitySessions = new Map();
1825
+ this._peerSyncCapabilityTimestamps = new Map();
1801
1826
  this._liveRawGossipBatches = new Map();
1802
1827
  this._liveRawGossipFlushScheduled = false;
1803
1828
  this.coordinateToHash = new Cache({ max: 1e6, ttl: 1e4 });
@@ -1806,6 +1831,7 @@ let SharedLog = (() => {
1806
1831
  this._replicatorJoinEmitted = new Set();
1807
1832
  this._replicatorsReconciled = false;
1808
1833
  this._liveness = this.createReplicatorLivenessMonitor();
1834
+ this._v2Send = this.createReplicationInfoV2SendCoordinator();
1809
1835
  this._announcements = this.createReplicationAnnouncementCoordinator();
1810
1836
  this.pendingMaturity = new Map();
1811
1837
  this._closeController = new AbortController();
@@ -2196,6 +2222,72 @@ let SharedLog = (() => {
2196
2222
  SYNC_CAPABILITY_RAW_EXCHANGE_HEADS) !==
2197
2223
  0);
2198
2224
  }
2225
+ observePeerSyncCapabilities(properties) {
2226
+ const { peerHash, capabilities, transportSession, timestamp, openingSession, } = properties;
2227
+ if (openingSession) {
2228
+ const previous = this._openingSyncCapabilitiesByPeer.get(peerHash);
2229
+ if (transportSession !== undefined &&
2230
+ timestamp !== undefined &&
2231
+ previous?.epoch === openingSession &&
2232
+ previous.transportSession === transportSession) {
2233
+ if (previous.timestamp !== undefined && timestamp < previous.timestamp) {
2234
+ return false;
2235
+ }
2236
+ this._openingSyncCapabilitiesByPeer.set(peerHash, {
2237
+ epoch: openingSession,
2238
+ capabilities: previous.capabilities | capabilities,
2239
+ transportSession,
2240
+ timestamp: previous.timestamp === undefined || timestamp > previous.timestamp
2241
+ ? timestamp
2242
+ : previous.timestamp,
2243
+ });
2244
+ return true;
2245
+ }
2246
+ this._openingSyncCapabilitiesByPeer.set(peerHash, {
2247
+ epoch: openingSession,
2248
+ capabilities,
2249
+ transportSession,
2250
+ timestamp,
2251
+ });
2252
+ return true;
2253
+ }
2254
+ if (transportSession === undefined || timestamp === undefined) {
2255
+ // Test/in-process synthetic contexts predate signed envelope captures.
2256
+ // They may exercise capability-number behavior, but can never authorize V2.
2257
+ this._peerSyncCapabilities.set(peerHash, capabilities);
2258
+ this._peerSyncCapabilitySessions.delete(peerHash);
2259
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
2260
+ this._v2Send.advancePeerCapability(peerHash);
2261
+ return true;
2262
+ }
2263
+ const previousSession = this._peerSyncCapabilitySessions.get(peerHash);
2264
+ const previousTimestamp = this._peerSyncCapabilityTimestamps.get(peerHash);
2265
+ const sameTransportSession = previousSession === transportSession;
2266
+ if (sameTransportSession &&
2267
+ previousTimestamp !== undefined &&
2268
+ timestamp < previousTimestamp) {
2269
+ return false;
2270
+ }
2271
+ const previousCapabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
2272
+ const nextCapabilities = sameTransportSession
2273
+ ? previousCapabilities | capabilities
2274
+ : capabilities;
2275
+ const generationAdvanced = !sameTransportSession ||
2276
+ previousTimestamp === undefined ||
2277
+ timestamp > previousTimestamp ||
2278
+ nextCapabilities !== previousCapabilities;
2279
+ this._peerSyncCapabilities.set(peerHash, nextCapabilities);
2280
+ this._peerSyncCapabilitySessions.set(peerHash, transportSession);
2281
+ this._peerSyncCapabilityTimestamps.set(peerHash, previousTimestamp === undefined ||
2282
+ !sameTransportSession ||
2283
+ timestamp > previousTimestamp
2284
+ ? timestamp
2285
+ : previousTimestamp);
2286
+ if (generationAdvanced) {
2287
+ this._v2Send.advancePeerCapability(peerHash);
2288
+ }
2289
+ return true;
2290
+ }
2199
2291
  /**
2200
2292
  * Live append gossip may use the raw exchange-heads path only when we
2201
2293
  * opted into raw sync and every remote recipient advertised raw capability
@@ -8895,6 +8987,8 @@ let SharedLog = (() => {
8895
8987
  ttl: LEADER_PLAN_CACHE_TTL_MS,
8896
8988
  });
8897
8989
  this._peerSyncCapabilities = new Map();
8990
+ this._peerSyncCapabilitySessions = new Map();
8991
+ this._peerSyncCapabilityTimestamps = new Map();
8898
8992
  this._liveRawGossipBatches = new Map();
8899
8993
  this._liveRawGossipFlushScheduled = false;
8900
8994
  this.coordinateToHash = new Cache({ max: 1e6, ttl: 1e4 });
@@ -8910,6 +9004,8 @@ let SharedLog = (() => {
8910
9004
  this._lastLocalAppendAt = 0;
8911
9005
  this._announcements ??= this.createReplicationAnnouncementCoordinator();
8912
9006
  this._announcements.resetForOpen();
9007
+ this._v2Send ??= this.createReplicationInfoV2SendCoordinator();
9008
+ this._v2Send.resetForOpen();
8913
9009
  const adaptiveReplicateOptions = options?.replicate && isAdaptiveReplicatorOption(options.replicate)
8914
9010
  ? options.replicate
8915
9011
  : undefined;
@@ -10029,6 +10125,9 @@ let SharedLog = (() => {
10029
10125
  this._liveness._replicatorLivenessFailures.delete(peerHash);
10030
10126
  this._liveness._replicatorLastActivityAt.delete(peerHash);
10031
10127
  this._peerSyncCapabilities.delete(peerHash);
10128
+ this._peerSyncCapabilitySessions.delete(peerHash);
10129
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
10130
+ this._v2Send.clearPeer(peerHash);
10032
10131
  this.cleanupPendingIHavePeer(peerHash);
10033
10132
  this.cleanupCheckedPrunePeer(peerHash, ownershipLifecycleController, this._checkedPrune);
10034
10133
  }
@@ -10845,6 +10944,9 @@ let SharedLog = (() => {
10845
10944
  this._openingSyncCapabilitiesByPeer?.clear();
10846
10945
  this._gidPeersHistory?.clear();
10847
10946
  this._peerSyncCapabilities?.clear();
10947
+ this._peerSyncCapabilitySessions?.clear();
10948
+ this._peerSyncCapabilityTimestamps?.clear();
10949
+ this._v2Send?.clearForClose();
10848
10950
  this._liveRawGossipBatches?.clear();
10849
10951
  this._nativeSharedLogState?.clearGidPeers();
10850
10952
  this._nativeBackbone?.clearGidPeers();
@@ -10965,12 +11067,14 @@ let SharedLog = (() => {
10965
11067
  }
10966
11068
  }, 2_000);
10967
11069
  try {
10968
- await this.rpc
10969
- .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
10970
- priority: CONVERGENCE_MESSAGE_PRIORITY,
10971
- signal: abort.signal,
10972
- })
10973
- .catch(() => { });
11070
+ const reset = new AllReplicatingSegmentsMessage({ segments: [] });
11071
+ await Promise.all([
11072
+ this.rpc.send(reset, {
11073
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
11074
+ signal: abort.signal,
11075
+ }).catch(() => { }),
11076
+ this._v2Send.sendTerminalReset(abort.signal),
11077
+ ]);
10974
11078
  }
10975
11079
  finally {
10976
11080
  clearTimeout(abortTimer);
@@ -11088,12 +11192,14 @@ let SharedLog = (() => {
11088
11192
  }
11089
11193
  }, 2_000);
11090
11194
  try {
11091
- await this.rpc
11092
- .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
11093
- priority: CONVERGENCE_MESSAGE_PRIORITY,
11094
- signal: abort.signal,
11095
- })
11096
- .catch(() => { });
11195
+ const reset = new AllReplicatingSegmentsMessage({ segments: [] });
11196
+ await Promise.all([
11197
+ this.rpc.send(reset, {
11198
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
11199
+ signal: abort.signal,
11200
+ }).catch(() => { }),
11201
+ this._v2Send.sendTerminalReset(abort.signal),
11202
+ ]);
11097
11203
  }
11098
11204
  finally {
11099
11205
  clearTimeout(abortTimer);
@@ -11565,7 +11671,8 @@ let SharedLog = (() => {
11565
11671
  // allocation cost, and before liveness or apply-queue side effects.
11566
11672
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
11567
11673
  }
11568
- if (!context.from.equals(this.node.identity.publicKey)) {
11674
+ if (!context.from.equals(this.node.identity.publicKey) &&
11675
+ !(msg instanceof RequestReplicationInfoV2Message)) {
11569
11676
  this._liveness.markReplicatorActivity(receiveFromHash);
11570
11677
  }
11571
11678
  const syncProfile = this._logProperties?.sync?.profile;
@@ -12908,6 +13015,8 @@ let SharedLog = (() => {
12908
13015
  }
12909
13016
  else if (msg instanceof SyncCapabilitiesMessage) {
12910
13017
  if (!context.from.equals(this.node.identity.publicKey)) {
13018
+ const capabilityTransportSession = context.message?.header?.session;
13019
+ const capabilityTimestamp = context.message?.header?.timestamp;
12911
13020
  // No await separates this from the capture above, so the captured
12912
13021
  // window state is exact: the legacy re-read of the opening map here
12913
13022
  // could never observe a different value.
@@ -12916,17 +13025,47 @@ let SharedLog = (() => {
12916
13025
  // A prior unsubscribe cleanup may still be ahead of this reconnect
12917
13026
  // barrier. Stage the new generation's one-shot advertisement so that
12918
13027
  // cleanup cannot erase it before the opening transition commits.
12919
- this._openingSyncCapabilitiesByPeer.set(receiveFromHash, {
12920
- epoch: receiveSession,
13028
+ this.observePeerSyncCapabilities({
13029
+ peerHash: receiveFromHash,
12921
13030
  capabilities: msg.capabilities,
13031
+ transportSession: capabilityTransportSession,
13032
+ timestamp: capabilityTimestamp,
13033
+ openingSession: receiveSession,
12922
13034
  });
12923
13035
  }
12924
13036
  else {
12925
- this._peerSyncCapabilities.set(receiveFromHash, msg.capabilities);
13037
+ this.observePeerSyncCapabilities({
13038
+ peerHash: receiveFromHash,
13039
+ capabilities: msg.capabilities,
13040
+ transportSession: capabilityTransportSession,
13041
+ timestamp: capabilityTimestamp,
13042
+ });
12926
13043
  }
12927
13044
  }
12928
13045
  return;
12929
13046
  }
13047
+ else if (msg instanceof RequestReplicationInfoV2Message) {
13048
+ const requiredCapabilities = SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
13049
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY;
13050
+ if (receiveSession &&
13051
+ ((this._peerSyncCapabilities.get(receiveFromHash) ?? 0) &
13052
+ requiredCapabilities) ===
13053
+ requiredCapabilities &&
13054
+ this._peerSyncCapabilitySessions.get(receiveFromHash) ===
13055
+ context.message.header.session &&
13056
+ this._peerSyncCapabilityTimestamps.has(receiveFromHash) &&
13057
+ context.message.header.timestamp >
13058
+ this._peerSyncCapabilityTimestamps.get(receiveFromHash) &&
13059
+ this._v2Send.acceptRequest(msg, {
13060
+ from: context.from,
13061
+ peerSession: receiveSession,
13062
+ receiverTransportSession: context.message.header.session,
13063
+ requestTimestamp: context.message.header.timestamp,
13064
+ })) {
13065
+ this._liveness.markReplicatorActivity(receiveFromHash);
13066
+ }
13067
+ return;
13068
+ }
12930
13069
  else if (await this.syncronizer.onMessage(msg, context)) {
12931
13070
  return; // the syncronizer has handled the message
12932
13071
  }
@@ -13162,6 +13301,7 @@ let SharedLog = (() => {
13162
13301
  }
13163
13302
  const segments = replicationSegments.map((x) => x.toReplicationRange());
13164
13303
  this.validatePersistedReplicationRangeSnapshot(segments);
13304
+ this._v2Send.enqueueSnapshotForPeer(receiveFromHash);
13165
13305
  await this.rpc
13166
13306
  .send(new AllReplicatingSegmentsMessage({ segments }), {
13167
13307
  mode: new AcknowledgeDelivery({
@@ -15463,6 +15603,12 @@ let SharedLog = (() => {
15463
15603
  if (!ownsSubscriptionEpoch()) {
15464
15604
  return;
15465
15605
  }
15606
+ // A destination stream is scoped to exactly one topic-subscription
15607
+ // session. Abort the predecessor synchronously before either barrier can
15608
+ // yield; a late queue completion must never enter the new session.
15609
+ this._v2Send.clearPeer(peerHash);
15610
+ this._peerSyncCapabilitySessions.delete(peerHash);
15611
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
15466
15612
  if (subscribed) {
15467
15613
  const pendingOpeningCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
15468
15614
  if (pendingOpeningCapabilities &&
@@ -15493,6 +15639,18 @@ let SharedLog = (() => {
15493
15639
  const openingCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
15494
15640
  if (openingCapabilities?.epoch === expectedSubscriptionEpoch) {
15495
15641
  this._peerSyncCapabilities.set(peerHash, openingCapabilities.capabilities);
15642
+ if (openingCapabilities.transportSession === undefined) {
15643
+ this._peerSyncCapabilitySessions.delete(peerHash);
15644
+ }
15645
+ else {
15646
+ this._peerSyncCapabilitySessions.set(peerHash, openingCapabilities.transportSession);
15647
+ }
15648
+ if (openingCapabilities.timestamp === undefined) {
15649
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
15650
+ }
15651
+ else {
15652
+ this._peerSyncCapabilityTimestamps.set(peerHash, openingCapabilities.timestamp);
15653
+ }
15496
15654
  this._openingSyncCapabilitiesByPeer.delete(peerHash);
15497
15655
  }
15498
15656
  this._peerSessions.unblockReplicationInfo(peerHash);
@@ -15562,10 +15720,12 @@ let SharedLog = (() => {
15562
15720
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15563
15721
  this._liveness.markReplicatorActivity(peerHash);
15564
15722
  this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
15565
- // Decode support is advertised independently from use permission. Every
15566
- // replication-info send below and in the announcement coordinator remains
15567
- // on the legacy wire format until a later receiver-led negotiation exists.
15723
+ // Decode support and receiver-led negotiation readiness are separate bits.
15724
+ // This node never initiates a request in this rollout; normal replication
15725
+ // announcements remain legacy unless a peer explicitly grants one signed,
15726
+ // session-bound V2 destination stream.
15568
15727
  const receiveCapabilities = SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
15728
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND |
15569
15729
  (this._logProperties?.sync?.rawExchangeHeads === true
15570
15730
  ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
15571
15731
  : 0);