@peerbit/shared-log 13.2.30 → 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, 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, 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);
@@ -11508,6 +11614,13 @@ let SharedLog = (() => {
11508
11614
  }
11509
11615
  // Callback for receiving a message from the network
11510
11616
  async onMessage(msg, context) {
11617
+ // Decode-first only: capability advertisement is not permission to use
11618
+ // V2. Drop every unsolicited V2 envelope before even entering the shared
11619
+ // receive envelope, so durable poison checks, leases, liveness, ordering
11620
+ // watermarks, replication ranges and cleanup side effects cannot observe it.
11621
+ if (isReplicationInfoV2Message(msg)) {
11622
+ return;
11623
+ }
11511
11624
  const stashBackedRawMessage = isStashBackedRawExchangeHeadsMessage(msg)
11512
11625
  ? msg
11513
11626
  : undefined;
@@ -11558,7 +11671,8 @@ let SharedLog = (() => {
11558
11671
  // allocation cost, and before liveness or apply-queue side effects.
11559
11672
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
11560
11673
  }
11561
- if (!context.from.equals(this.node.identity.publicKey)) {
11674
+ if (!context.from.equals(this.node.identity.publicKey) &&
11675
+ !(msg instanceof RequestReplicationInfoV2Message)) {
11562
11676
  this._liveness.markReplicatorActivity(receiveFromHash);
11563
11677
  }
11564
11678
  const syncProfile = this._logProperties?.sync?.profile;
@@ -12901,6 +13015,8 @@ let SharedLog = (() => {
12901
13015
  }
12902
13016
  else if (msg instanceof SyncCapabilitiesMessage) {
12903
13017
  if (!context.from.equals(this.node.identity.publicKey)) {
13018
+ const capabilityTransportSession = context.message?.header?.session;
13019
+ const capabilityTimestamp = context.message?.header?.timestamp;
12904
13020
  // No await separates this from the capture above, so the captured
12905
13021
  // window state is exact: the legacy re-read of the opening map here
12906
13022
  // could never observe a different value.
@@ -12909,17 +13025,47 @@ let SharedLog = (() => {
12909
13025
  // A prior unsubscribe cleanup may still be ahead of this reconnect
12910
13026
  // barrier. Stage the new generation's one-shot advertisement so that
12911
13027
  // cleanup cannot erase it before the opening transition commits.
12912
- this._openingSyncCapabilitiesByPeer.set(receiveFromHash, {
12913
- epoch: receiveSession,
13028
+ this.observePeerSyncCapabilities({
13029
+ peerHash: receiveFromHash,
12914
13030
  capabilities: msg.capabilities,
13031
+ transportSession: capabilityTransportSession,
13032
+ timestamp: capabilityTimestamp,
13033
+ openingSession: receiveSession,
12915
13034
  });
12916
13035
  }
12917
13036
  else {
12918
- this._peerSyncCapabilities.set(receiveFromHash, msg.capabilities);
13037
+ this.observePeerSyncCapabilities({
13038
+ peerHash: receiveFromHash,
13039
+ capabilities: msg.capabilities,
13040
+ transportSession: capabilityTransportSession,
13041
+ timestamp: capabilityTimestamp,
13042
+ });
12919
13043
  }
12920
13044
  }
12921
13045
  return;
12922
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
+ }
12923
13069
  else if (await this.syncronizer.onMessage(msg, context)) {
12924
13070
  return; // the syncronizer has handled the message
12925
13071
  }
@@ -13155,6 +13301,7 @@ let SharedLog = (() => {
13155
13301
  }
13156
13302
  const segments = replicationSegments.map((x) => x.toReplicationRange());
13157
13303
  this.validatePersistedReplicationRangeSnapshot(segments);
13304
+ this._v2Send.enqueueSnapshotForPeer(receiveFromHash);
13158
13305
  await this.rpc
13159
13306
  .send(new AllReplicatingSegmentsMessage({ segments }), {
13160
13307
  mode: new AcknowledgeDelivery({
@@ -15456,6 +15603,12 @@ let SharedLog = (() => {
15456
15603
  if (!ownsSubscriptionEpoch()) {
15457
15604
  return;
15458
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);
15459
15612
  if (subscribed) {
15460
15613
  const pendingOpeningCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
15461
15614
  if (pendingOpeningCapabilities &&
@@ -15486,6 +15639,18 @@ let SharedLog = (() => {
15486
15639
  const openingCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
15487
15640
  if (openingCapabilities?.epoch === expectedSubscriptionEpoch) {
15488
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
+ }
15489
15654
  this._openingSyncCapabilitiesByPeer.delete(peerHash);
15490
15655
  }
15491
15656
  this._peerSessions.unblockReplicationInfo(peerHash);
@@ -15555,19 +15720,23 @@ let SharedLog = (() => {
15555
15720
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15556
15721
  this._liveness.markReplicatorActivity(peerHash);
15557
15722
  this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
15558
- if (this._logProperties?.sync?.rawExchangeHeads === true) {
15559
- // One-shot capability advertisement so live append gossip can pick
15560
- // the raw exchange-heads path for this peer without a per-request
15561
- // round trip. Peers that do not know the message drop it.
15562
- this.rpc
15563
- .send(new SyncCapabilitiesMessage({
15564
- capabilities: SYNC_CAPABILITY_RAW_EXCHANGE_HEADS,
15565
- }), {
15566
- mode: new SilentDelivery({ redundancy: 1, to: [publicKey] }),
15567
- signal: replicationLifecycleController.signal,
15568
- })
15569
- .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
15570
- }
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.
15727
+ const receiveCapabilities = SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
15728
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND |
15729
+ (this._logProperties?.sync?.rawExchangeHeads === true
15730
+ ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
15731
+ : 0);
15732
+ this.rpc
15733
+ .send(new SyncCapabilitiesMessage({
15734
+ capabilities: receiveCapabilities,
15735
+ }), {
15736
+ mode: new SilentDelivery({ redundancy: 1, to: [publicKey] }),
15737
+ signal: replicationLifecycleController.signal,
15738
+ })
15739
+ .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
15571
15740
  let replicationSegments;
15572
15741
  try {
15573
15742
  replicationSegments = await this.getMyReplicationSegments();