@peerbit/shared-log 13.2.32 → 13.2.33

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
@@ -51,27 +51,28 @@ import PQueue from "p-queue";
51
51
  import { concat, fromString } from "uint8arrays";
52
52
  import { BlocksMessage } from "./blocks.js";
53
53
  import { CheckedPruneCoordinator, } from "./checked-prune.js";
54
+ import { CoordinatePersistenceCoordinator, combineCoordinateDeleteHashes, isPromiseLike, mapMaybePromise, normalizedHashValues, } from "./coordinate-persistence.js";
54
55
  import { CPUUsageIntervalLag } from "./cpu.js";
55
56
  import { debouncedAccumulatorMap, } from "./debounce.js";
56
57
  import { NativeDurableCommitError, NoPeersError, isNotStartedError, } from "./errors.js";
57
58
  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
59
  import { FanoutEnvelope } from "./fanout-envelope.js";
59
- import { CoordinatePersistenceCoordinator, combineCoordinateDeleteHashes, isPromiseLike, mapMaybePromise, normalizedHashValues, } from "./coordinate-persistence.js";
60
60
  import { InstanceLifecycle } from "./instance-lifecycle.js";
61
61
  import { MAX_U32, MAX_U64, createNumbers, } from "./integers.js";
62
62
  import { JoinWarmupCoordinator } from "./join-warmup.js";
63
63
  import { LeaderPlanCache } from "./leader-plan-cache.js";
64
64
  import { TransportMessage } from "./message.js";
65
65
  import { NativeBackboneWriteThroughBlockStore } from "./native-write-through-block-store.js";
66
- import { PeerSessionRegistry, } from "./peer-session.js";
66
+ 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";
71
70
  import { createReplicationDomainHash, } from "./replication-domain-hash.js";
72
71
  import { createReplicationDomainTime, } from "./replication-domain-time.js";
73
72
  import {} from "./replication-domain.js";
74
- import { AbsoluteReplicas, AddedReplicationSegmentMessage, AllReplicatingSegmentsMessage, MinReplicas, ReplicationError, ReplicationPingMessage, RequestReplicationInfoMessage, RequestReplicationInfoV2Message, ResponseRoleMessage, StoppedReplicating, decodeReplicas, encodeReplicas, isReplicationInfoV2Message, maxReplicas, } from "./replication.js";
73
+ import { ReplicationInfoV2ReceiveCoordinator } from "./replication-info-v2-receive.js";
74
+ import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
75
+ import { AbsoluteReplicas, AddedReplicationInfoV2Message, AddedReplicationSegmentMessage, AllReplicatingSegmentsMessage, FullReplicationInfoV2Message, MinReplicas, ReplicationError, ReplicationPingMessage, RequestReplicationInfoMessage, RequestReplicationInfoV2Message, ResponseRoleMessage, StoppedReplicating, StoppedReplicationInfoV2Message, decodeReplicas, encodeReplicas, isReplicationInfoV2Message, maxReplicas, } from "./replication.js";
75
76
  import { ReplicatorLivenessMonitor } from "./replicator-liveness.js";
76
77
  import { Observer, Replicator } from "./role.js";
77
78
  import { createSyncronizer } from "./sync/factory.js";
@@ -625,7 +626,8 @@ let SharedLog = (() => {
625
626
  value;
626
627
  }
627
628
  get _nativeBackboneCoordinateJournalLastFlushMs() {
628
- return this._coordinates?._nativeBackboneCoordinateJournalLastFlushMs;
629
+ return this._coordinates
630
+ ?._nativeBackboneCoordinateJournalLastFlushMs;
629
631
  }
630
632
  set _nativeBackboneCoordinateJournalLastFlushMs(value) {
631
633
  (this._coordinates ??=
@@ -1498,7 +1500,8 @@ let SharedLog = (() => {
1498
1500
  }
1499
1501
  if (!lowerMarkerCommitted) {
1500
1502
  if (intent.coordinates.length > 0) {
1501
- const mutationGenerations = (this._coordinates._nativeCoordinateMutationGenerations ??= new Map());
1503
+ const mutationGenerations = (this._coordinates._nativeCoordinateMutationGenerations ??=
1504
+ new Map());
1502
1505
  const rollback = {
1503
1506
  hashes: new Set(),
1504
1507
  entries: new Map(),
@@ -1609,6 +1612,7 @@ let SharedLog = (() => {
1609
1612
  // A fn that we can call many times that recalculates the participation role
1610
1613
  rebalanceParticipationDebounced;
1611
1614
  _announcements;
1615
+ _v2Receive;
1612
1616
  _v2Send;
1613
1617
  // A fn for debouncing the calls for pruning
1614
1618
  pruneDebouncedFn;
@@ -1727,6 +1731,78 @@ let SharedLog = (() => {
1727
1731
  isReplicationOwnershipLifecycleActive: (controller) => this.isRepairLifecycleActive(controller),
1728
1732
  });
1729
1733
  }
1734
+ replicationInfoV2ReceiveCapabilities() {
1735
+ return (SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
1736
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND |
1737
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY |
1738
+ (this._logProperties?.sync?.rawExchangeHeads === true
1739
+ ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
1740
+ : 0));
1741
+ }
1742
+ async advertiseReplicationInfoV2ReceiveCapability(properties) {
1743
+ const peerHash = properties.target.hashcode();
1744
+ const receiverTransportSession = BigInt(this.node.services.pubsub.session);
1745
+ await this.rpc.send(new SyncCapabilitiesMessage({
1746
+ capabilities: this.replicationInfoV2ReceiveCapabilities(),
1747
+ }), {
1748
+ mode: new AcknowledgeDelivery({
1749
+ redundancy: 1,
1750
+ to: [properties.target],
1751
+ }),
1752
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1753
+ signal: properties.signal,
1754
+ });
1755
+ if (properties.signal.aborted ||
1756
+ this.closed ||
1757
+ !this._peerSessions.isCurrent(peerHash, properties.peerSession) ||
1758
+ properties.peerSession.phase !== "open" ||
1759
+ !properties.peerSession.isActive() ||
1760
+ !this._peerSessions.isReceiveEpochCurrent(peerHash, properties.receiveEpoch) ||
1761
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
1762
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash) ||
1763
+ BigInt(this.node.services.pubsub.session) !== receiverTransportSession) {
1764
+ return undefined;
1765
+ }
1766
+ return {
1767
+ receiverTransportSession,
1768
+ requestNotBeforeMs: Date.now(),
1769
+ };
1770
+ }
1771
+ createReplicationInfoV2ReceiveCoordinator() {
1772
+ return new ReplicationInfoV2ReceiveCoordinator({
1773
+ getSelfKey: () => this.node.identity.publicKey,
1774
+ getReceiverTransportSession: () => BigInt(this.node.services.pubsub.session),
1775
+ isClosed: () => this.closed,
1776
+ isPeerStateCurrent: (peerHash, peerSession, receiveEpoch) => this._peerSessions.isCurrent(peerHash, peerSession) &&
1777
+ peerSession.phase === "open" &&
1778
+ peerSession.isActive() &&
1779
+ this._peerSessions.isReceiveEpochCurrent(peerHash, receiveEpoch) &&
1780
+ !this._peerSessions.isReplicationInfoBlocked(peerHash) &&
1781
+ this._peerSessions.isReceiveCleanupGateOpen(peerHash),
1782
+ isSenderTransportSessionCurrent: (peerHash, senderTransportSession) => this._peerSyncCapabilitySessions.get(peerHash) ===
1783
+ senderTransportSession,
1784
+ sendRequest: async (request, target, signal) => {
1785
+ await this.rpc.send(request, {
1786
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [target] }),
1787
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1788
+ signal,
1789
+ });
1790
+ },
1791
+ refreshLocalCapability: (properties) => this.advertiseReplicationInfoV2ReceiveCapability({
1792
+ target: properties.target,
1793
+ peerSession: properties.peerSession,
1794
+ receiveEpoch: properties.receiveEpoch,
1795
+ signal: properties.signal,
1796
+ }),
1797
+ onRequestError: (error) => {
1798
+ if (isNotStartedError(error) ||
1799
+ (this.closed && error instanceof AbortError)) {
1800
+ return;
1801
+ }
1802
+ logger.trace(`Replication-info V2 recovery request failed: ${error?.message ?? String(error)}`);
1803
+ },
1804
+ });
1805
+ }
1730
1806
  createReplicatorLivenessMonitor() {
1731
1807
  return new ReplicatorLivenessMonitor({
1732
1808
  // Sweep-driven probes and activity marks dispatch through the owner so
@@ -1831,6 +1907,7 @@ let SharedLog = (() => {
1831
1907
  this._replicatorJoinEmitted = new Set();
1832
1908
  this._replicatorsReconciled = false;
1833
1909
  this._liveness = this.createReplicatorLivenessMonitor();
1910
+ this._v2Receive = this.createReplicationInfoV2ReceiveCoordinator();
1834
1911
  this._v2Send = this.createReplicationInfoV2SendCoordinator();
1835
1912
  this._announcements = this.createReplicationAnnouncementCoordinator();
1836
1913
  this.pendingMaturity = new Map();
@@ -2226,11 +2303,20 @@ let SharedLog = (() => {
2226
2303
  const { peerHash, capabilities, transportSession, timestamp, openingSession, } = properties;
2227
2304
  if (openingSession) {
2228
2305
  const previous = this._openingSyncCapabilitiesByPeer.get(peerHash);
2306
+ if (transportSession !== undefined &&
2307
+ timestamp !== undefined &&
2308
+ previous?.epoch === openingSession &&
2309
+ previous.timestamp !== undefined &&
2310
+ previous.transportSession !== transportSession &&
2311
+ timestamp <= previous.timestamp) {
2312
+ return false;
2313
+ }
2229
2314
  if (transportSession !== undefined &&
2230
2315
  timestamp !== undefined &&
2231
2316
  previous?.epoch === openingSession &&
2232
2317
  previous.transportSession === transportSession) {
2233
- if (previous.timestamp !== undefined && timestamp < previous.timestamp) {
2318
+ if (previous.timestamp !== undefined &&
2319
+ timestamp < previous.timestamp) {
2234
2320
  return false;
2235
2321
  }
2236
2322
  this._openingSyncCapabilitiesByPeer.set(peerHash, {
@@ -2258,24 +2344,26 @@ let SharedLog = (() => {
2258
2344
  this._peerSyncCapabilitySessions.delete(peerHash);
2259
2345
  this._peerSyncCapabilityTimestamps.delete(peerHash);
2260
2346
  this._v2Send.advancePeerCapability(peerHash);
2347
+ this._v2Receive.revokePeerCapability(peerHash);
2261
2348
  return true;
2262
2349
  }
2263
2350
  const previousSession = this._peerSyncCapabilitySessions.get(peerHash);
2264
2351
  const previousTimestamp = this._peerSyncCapabilityTimestamps.get(peerHash);
2265
2352
  const sameTransportSession = previousSession === transportSession;
2266
- if (sameTransportSession &&
2267
- previousTimestamp !== undefined &&
2268
- timestamp < previousTimestamp) {
2353
+ if (previousTimestamp !== undefined &&
2354
+ ((sameTransportSession && timestamp < previousTimestamp) ||
2355
+ (!sameTransportSession && timestamp <= previousTimestamp))) {
2269
2356
  return false;
2270
2357
  }
2271
2358
  const previousCapabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
2272
2359
  const nextCapabilities = sameTransportSession
2273
2360
  ? previousCapabilities | capabilities
2274
2361
  : capabilities;
2362
+ const senderGrantCapabilityMask = SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
2363
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY;
2275
2364
  const generationAdvanced = !sameTransportSession ||
2276
- previousTimestamp === undefined ||
2277
- timestamp > previousTimestamp ||
2278
- nextCapabilities !== previousCapabilities;
2365
+ (previousCapabilities & senderGrantCapabilityMask) !==
2366
+ (nextCapabilities & senderGrantCapabilityMask);
2279
2367
  this._peerSyncCapabilities.set(peerHash, nextCapabilities);
2280
2368
  this._peerSyncCapabilitySessions.set(peerHash, transportSession);
2281
2369
  this._peerSyncCapabilityTimestamps.set(peerHash, previousTimestamp === undefined ||
@@ -2288,6 +2376,24 @@ let SharedLog = (() => {
2288
2376
  }
2289
2377
  return true;
2290
2378
  }
2379
+ promoteReplicationInfoV2ReceiveCapability(target, peerSession) {
2380
+ const peerHash = target.hashcode();
2381
+ const senderTransportSession = this._peerSyncCapabilitySessions.get(peerHash);
2382
+ const capabilityTimestamp = this._peerSyncCapabilityTimestamps.get(peerHash);
2383
+ if (senderTransportSession === undefined ||
2384
+ capabilityTimestamp === undefined) {
2385
+ return false;
2386
+ }
2387
+ return this._v2Receive.observeCapability({
2388
+ peerHash,
2389
+ target,
2390
+ peerSession,
2391
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
2392
+ capabilities: this._peerSyncCapabilities.get(peerHash) ?? 0,
2393
+ senderTransportSession,
2394
+ capabilityTimestamp,
2395
+ });
2396
+ }
2291
2397
  /**
2292
2398
  * Live append gossip may use the raw exchange-heads path only when we
2293
2399
  * opted into raw sync and every remote recipient advertised raw capability
@@ -2984,7 +3090,8 @@ let SharedLog = (() => {
2984
3090
  return undefined;
2985
3091
  }
2986
3092
  isReceiveOwnershipSnapshotStable(revision) {
2987
- return ((this._instanceLifecycle?._receiveOwnershipMutationAdmissions ?? 0) === 0 &&
3093
+ return ((this._instanceLifecycle?._receiveOwnershipMutationAdmissions ?? 0) ===
3094
+ 0 &&
2988
3095
  (this._instanceLifecycle?._receiveOwnershipRevision ?? 0) === revision);
2989
3096
  }
2990
3097
  canCacheLeaderSelectionContext(options) {
@@ -3771,6 +3878,7 @@ let SharedLog = (() => {
3771
3878
  };
3772
3879
  let removed = false;
3773
3880
  let removalCallCompleted = false;
3881
+ let replicationInfoRecoveryEpochAdvanced = false;
3774
3882
  // Replication-info updates already serialize per peer. Put the hash-wide
3775
3883
  // removal on the same queue so a newer reset cannot be deleted underneath
3776
3884
  // itself by an older unsubscribe callback.
@@ -3840,6 +3948,13 @@ let SharedLog = (() => {
3840
3948
  // before mutating ownership.
3841
3949
  this.cleanupCheckedPrunePeer(keyHash, replicationOwnershipLifecycleController, checkedPruneCoordinator);
3842
3950
  }
3951
+ if (!isMe) {
3952
+ // This is the last synchronous current-generation boundary
3953
+ // before destructive work. Fence parked receives now because
3954
+ // the coherent delete can durably mutate and then fail or poison.
3955
+ this.advanceReplicationInfoRecoveryEpoch(keyHash);
3956
+ replicationInfoRecoveryEpochAdvanced = true;
3957
+ }
3843
3958
  const deletion = await this.deleteReplicationRangesCoherently(deleted, keyHash);
3844
3959
  deleted = deletion.removed;
3845
3960
  ownerHasRanges = deletion.ownerHasRanges;
@@ -3885,11 +4000,6 @@ let SharedLog = (() => {
3885
4000
  // through replication-info updates without a topic unsubscribe event.
3886
4001
  await cleanupDisconnectedPeer();
3887
4002
  if (!isMe) {
3888
- // Replication-info handlers release their receive lease before joining
3889
- // this lane. Fence every handler queued behind this successful removal,
3890
- // regardless of whether it came from liveness, startup pruning, or an
3891
- // unsubscribe transition.
3892
- this.advanceReplicationInfoRecoveryEpoch(keyHash);
3893
4003
  this.rebalanceParticipationDebounced?.call();
3894
4004
  }
3895
4005
  removed = true;
@@ -3920,6 +4030,22 @@ let SharedLog = (() => {
3920
4030
  }
3921
4031
  finally {
3922
4032
  releaseReceiveCleanupGate?.();
4033
+ if (replicationInfoRecoveryEpochAdvanced &&
4034
+ ownsReplicationOwnershipLifecycle() &&
4035
+ ownsReplicationLifecycle() &&
4036
+ ownsSubscriptionEpoch()) {
4037
+ // The first recovery arm ran while receive admission was fenced.
4038
+ // Re-arm after releasing the cleanup gate so a still-open peer can
4039
+ // deliver the authoritative Full that completes recovery.
4040
+ const peerSession = this._peerSessions.current(keyHash);
4041
+ if (peerSession?.phase === "open") {
4042
+ this._v2Receive.advanceRecovery({
4043
+ peerHash: keyHash,
4044
+ peerSession,
4045
+ receiveEpoch: this._peerSessions.receiveEpoch(keyHash),
4046
+ });
4047
+ }
4048
+ }
3923
4049
  if (isSpeculativePeerRemoval) {
3924
4050
  const cancelledByFreshActivity = removalCallCompleted &&
3925
4051
  !removed &&
@@ -4012,6 +4138,11 @@ let SharedLog = (() => {
4012
4138
  }
4013
4139
  const deletion = await this.deleteReplicationRangesCoherently(ranges, ownerHash);
4014
4140
  ranges = deletion.removed;
4141
+ if ((options?.shouldRemove && !options.shouldRemove()) ||
4142
+ options?.onDurableRemoveCommitted?.() === false) {
4143
+ await deletion.rollback();
4144
+ return false;
4145
+ }
4015
4146
  options?.onRemoved?.(ranges);
4016
4147
  if (ranges.length > 0) {
4017
4148
  this.events.dispatchEvent(new CustomEvent("replication:change", {
@@ -4075,7 +4206,7 @@ let SharedLog = (() => {
4075
4206
  this.throwIfReplicationOwnershipLifecycleInactive(replicationOwnershipLifecycleController);
4076
4207
  return this.withReceiveOwnershipMutationQueue(() => this.addReplicationRangeUnlocked(ranges, from, options, replicationOwnershipLifecycleController), replicationOwnershipLifecycleController);
4077
4208
  }
4078
- async addReplicationRangeUnlocked(ranges, from, { reset, checkDuplicates, timestamp: ts, rebalance, allowLegacyOrderedReplacementPairs, onConfirmedDurableStateChanged, shouldApply, } = {}, replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
4209
+ async addReplicationRangeUnlocked(ranges, from, { reset, checkDuplicates, timestamp: ts, rebalance, allowLegacyOrderedReplacementPairs, onConfirmedDurableStateChanged, onDurableApplyCommitted, shouldApply, } = {}, replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
4079
4210
  this.validateReplicationRangeAnnouncement(ranges);
4080
4211
  this.throwIfReplicationOwnershipLifecycleInactive(replicationOwnershipLifecycleController);
4081
4212
  // This predicate is intentionally checked inside the global ownership lane.
@@ -4084,6 +4215,12 @@ let SharedLog = (() => {
4084
4215
  if (shouldApply && !shouldApply()) {
4085
4216
  return [];
4086
4217
  }
4218
+ const applySuperseded = Symbol("replication-range-apply-superseded");
4219
+ const throwIfApplySuperseded = () => {
4220
+ if (shouldApply && !shouldApply()) {
4221
+ throw applySuperseded;
4222
+ }
4223
+ };
4087
4224
  const fromHash = from.hashcode();
4088
4225
  const incomingRangesById = new Map();
4089
4226
  const incomingRangeCountsById = new Map();
@@ -4142,6 +4279,7 @@ let SharedLog = (() => {
4142
4279
  let isStoppedReplicating = false;
4143
4280
  let wasReplicatorBeforeDestructiveReset = false;
4144
4281
  let resetFailureLeaveEmitted = false;
4282
+ let resetDeletionRollback;
4145
4283
  const publishConfirmedResetStop = (ownerHasRanges) => {
4146
4284
  if (ownerHasRanges || resetFailureLeaveEmitted) {
4147
4285
  return;
@@ -4182,8 +4320,14 @@ let SharedLog = (() => {
4182
4320
  else {
4183
4321
  wasReplicatorBeforeDestructiveReset =
4184
4322
  this.uniqueReplicators.has(fromHash);
4323
+ throwIfApplySuperseded();
4185
4324
  const deletion = await this.deleteReplicationRangesCoherently(deleted, fromHash, { preserveOwnerMembership: ranges.length > 0 });
4186
4325
  deleted = deletion.removed;
4326
+ resetDeletionRollback = deletion.rollback;
4327
+ if (shouldApply && !shouldApply()) {
4328
+ await deletion.rollback();
4329
+ return [];
4330
+ }
4187
4331
  diffs = [
4188
4332
  ...deleted.map((x) => {
4189
4333
  return { range: x, type: "removed", timestamp };
@@ -4339,19 +4483,27 @@ let SharedLog = (() => {
4339
4483
  return failure;
4340
4484
  };
4341
4485
  try {
4486
+ throwIfApplySuperseded();
4342
4487
  for (const diff of diffs) {
4343
4488
  if (diff.type !== "added") {
4344
4489
  continue;
4345
4490
  }
4491
+ throwIfApplySuperseded();
4346
4492
  appliedPositiveRanges.push(diff);
4347
4493
  await this.replicationIndex.put(diff.range);
4348
4494
  this.putNativeReplicationRange(diff.range);
4495
+ throwIfApplySuperseded();
4349
4496
  }
4350
4497
  if (reset && diffs.length > 0) {
4351
4498
  await this.updateOldestTimestampFromIndex();
4499
+ throwIfApplySuperseded();
4500
+ }
4501
+ if (onDurableApplyCommitted?.() === false) {
4502
+ throw applySuperseded;
4352
4503
  }
4353
4504
  }
4354
4505
  catch (error) {
4506
+ const superseded = error === applySuperseded;
4355
4507
  let outcomeError = error;
4356
4508
  if (appliedPositiveRanges.length > 0) {
4357
4509
  try {
@@ -4361,6 +4513,20 @@ let SharedLog = (() => {
4361
4513
  outcomeError = poisonFromPositiveRollback(rollbackError, error);
4362
4514
  }
4363
4515
  }
4516
+ if (superseded) {
4517
+ if (resetDeletionRollback) {
4518
+ try {
4519
+ await resetDeletionRollback();
4520
+ }
4521
+ catch (rollbackError) {
4522
+ outcomeError = rollbackError;
4523
+ }
4524
+ }
4525
+ if (outcomeError === applySuperseded) {
4526
+ return [];
4527
+ }
4528
+ throw outcomeError;
4529
+ }
4364
4530
  if (reset) {
4365
4531
  const negativeDiffs = diffs.filter((diff) => diff.type !== "added");
4366
4532
  if (negativeDiffs.length > 0) {
@@ -6967,7 +7133,8 @@ let SharedLog = (() => {
6967
7133
  appendLocallyPreparedPayloadNativeBackboneStorageTransaction(payloadData, appendOptions, properties, minReplicasValue, runtimeOnlyCoordinates, ownershipLifecycleController) {
6968
7134
  this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
6969
7135
  const backbone = this._nativeBackbone;
6970
- if (!backbone || !this._coordinates.canUseNativeBackboneResidentCoordinateState()) {
7136
+ if (!backbone ||
7137
+ !this._coordinates.canUseNativeBackboneResidentCoordinateState()) {
6971
7138
  return undefined;
6972
7139
  }
6973
7140
  if (properties?.nativeBackboneDocumentDeleteKey &&
@@ -7098,11 +7265,12 @@ let SharedLog = (() => {
7098
7265
  backboneAppend.trimmed.map((entry) => entry.hash);
7099
7266
  const committedHash = backboneAppend.entry.cid ?? backboneAppend.entry.hash;
7100
7267
  const committedNext = backboneAppend.entry.next ?? next;
7101
- nativeCoordinateRollback = this._coordinates.snapshotResidentCoordinateEntries([
7102
- ...(committedHash ? [committedHash] : []),
7103
- ...committedNext,
7104
- ...nativeTrimmedHashes,
7105
- ]);
7268
+ nativeCoordinateRollback =
7269
+ this._coordinates.snapshotResidentCoordinateEntries([
7270
+ ...(committedHash ? [committedHash] : []),
7271
+ ...committedNext,
7272
+ ...nativeTrimmedHashes,
7273
+ ]);
7106
7274
  await this.setNativeStrictDurableTransactionOperation(nativeStrictTransaction, committedHash ? [committedHash] : [], nativeTrimmedHashes, nativeCoordinateRollback, combineCoordinateDeleteHashes(committedNext, nativeTrimmedHashes));
7107
7275
  const rollbackCommitted = async (cause, committedCids) => {
7108
7276
  durableWrapper?.cancelNativeDeleteCleanup?.(nativeDeleteCleanupToken);
@@ -7770,14 +7938,15 @@ let SharedLog = (() => {
7770
7938
  const nativeTrimmedHashSet = new Set(committedAppends.flatMap((append) => append.trimmedHashes ??
7771
7939
  append.trimmed.map((entry) => entry.hash)));
7772
7940
  const nativeTrimmedHashes = [...nativeTrimmedHashSet];
7773
- batchCoordinateRollback = this._coordinates.snapshotResidentCoordinateEntries(committedAppends.flatMap((append) => [
7774
- ...((append.entry.cid ?? append.entry.hash)
7775
- ? [append.entry.cid ?? append.entry.hash]
7776
- : []),
7777
- ...append.entry.next,
7778
- ...(append.trimmedHashes ??
7779
- append.trimmed.map((entry) => entry.hash)),
7780
- ]));
7941
+ batchCoordinateRollback =
7942
+ this._coordinates.snapshotResidentCoordinateEntries(committedAppends.flatMap((append) => [
7943
+ ...((append.entry.cid ?? append.entry.hash)
7944
+ ? [append.entry.cid ?? append.entry.hash]
7945
+ : []),
7946
+ ...append.entry.next,
7947
+ ...(append.trimmedHashes ??
7948
+ append.trimmed.map((entry) => entry.hash)),
7949
+ ]));
7781
7950
  await this.setNativeStrictDurableTransactionOperation(nativeStrictTransaction, committedCids, nativeTrimmedHashes, batchCoordinateRollback, committedAppends.flatMap((append) => [
7782
7951
  ...append.entry.next,
7783
7952
  ...(append.trimmedHashes ??
@@ -9004,6 +9173,8 @@ let SharedLog = (() => {
9004
9173
  this._lastLocalAppendAt = 0;
9005
9174
  this._announcements ??= this.createReplicationAnnouncementCoordinator();
9006
9175
  this._announcements.resetForOpen();
9176
+ this._v2Receive ??= this.createReplicationInfoV2ReceiveCoordinator();
9177
+ this._v2Receive.resetForOpen();
9007
9178
  this._v2Send ??= this.createReplicationInfoV2SendCoordinator();
9008
9179
  this._v2Send.resetForOpen();
9009
9180
  const adaptiveReplicateOptions = options?.replicate && isAdaptiveReplicatorOption(options.replicate)
@@ -9575,7 +9746,8 @@ let SharedLog = (() => {
9575
9746
  // any stale coordinate or document state to this backbone.
9576
9747
  await this._coordinates._nativeBackboneCoordinatePersistence.resumeDrop?.();
9577
9748
  await this._coordinates._nativeBackboneCoordinatePersistence.hydrate(backbone);
9578
- this._coordinates._nativeBackboneCoordinateJournalLastFlushMs = Date.now();
9749
+ this._coordinates._nativeBackboneCoordinateJournalLastFlushMs =
9750
+ Date.now();
9579
9751
  this.hydrateNativeCoordinateStateFromBackbone(backbone);
9580
9752
  return;
9581
9753
  }
@@ -9787,18 +9959,20 @@ let SharedLog = (() => {
9787
9959
  }
9788
9960
  if (this.node.directory != null &&
9789
9961
  this._coordinates._nativeBackboneCoordinatePersistence) {
9790
- if (this._coordinates._nativeBackboneCoordinatePersistence.durableBarrier !== true ||
9962
+ if (this._coordinates._nativeBackboneCoordinatePersistence
9963
+ .durableBarrier !== true ||
9791
9964
  typeof this._nativeBackboneCoordinatePersistenceStore
9792
9965
  ?.durableBarrier !== "function") {
9793
9966
  throw new Error("Durable nativeBackbone coordinate persistence requires an explicit physical durability barrier");
9794
9967
  }
9795
9968
  }
9796
9969
  if (this._coordinates._nativeBackboneCoordinatePersistence &&
9797
- (this._coordinates._nativeBackboneCoordinatePersistence.compactMaxJournalBytes !=
9798
- null ||
9799
- this._coordinates._nativeBackboneCoordinatePersistence.compactMaxJournalRecords !=
9800
- null) &&
9801
- this._coordinates._nativeBackboneCoordinatePersistence.crashSafeCompaction !== true) {
9970
+ (this._coordinates._nativeBackboneCoordinatePersistence
9971
+ .compactMaxJournalBytes != null ||
9972
+ this._coordinates._nativeBackboneCoordinatePersistence
9973
+ .compactMaxJournalRecords != null) &&
9974
+ this._coordinates._nativeBackboneCoordinatePersistence
9975
+ .crashSafeCompaction !== true) {
9802
9976
  // Durable custom adapters must explicitly advertise an atomic generation
9803
9977
  // protocol before SharedLog permits automatic WAL compaction.
9804
9978
  throw new Error("Durable native coordinate persistence compaction thresholds require crashSafeCompaction");
@@ -10121,13 +10295,18 @@ let SharedLog = (() => {
10121
10295
  }
10122
10296
  }
10123
10297
  cleanupPeerDisconnectTracking(peerHash, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
10298
+ const peerSession = this._peerSessions.current(peerHash);
10299
+ const preserveV2Session = peerSession?.phase === "open" && peerSession.isActive();
10124
10300
  this.cancelReplicationInfoRequests(peerHash);
10125
10301
  this._liveness._replicatorLivenessFailures.delete(peerHash);
10126
10302
  this._liveness._replicatorLastActivityAt.delete(peerHash);
10127
- this._peerSyncCapabilities.delete(peerHash);
10128
- this._peerSyncCapabilitySessions.delete(peerHash);
10129
- this._peerSyncCapabilityTimestamps.delete(peerHash);
10130
- this._v2Send.clearPeer(peerHash);
10303
+ if (!preserveV2Session) {
10304
+ this._peerSyncCapabilities.delete(peerHash);
10305
+ this._peerSyncCapabilitySessions.delete(peerHash);
10306
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
10307
+ this._v2Receive.clearPeer(peerHash);
10308
+ this._v2Send.clearPeer(peerHash);
10309
+ }
10131
10310
  this.cleanupPendingIHavePeer(peerHash);
10132
10311
  this.cleanupCheckedPrunePeer(peerHash, ownershipLifecycleController, this._checkedPrune);
10133
10312
  }
@@ -10145,8 +10324,16 @@ let SharedLog = (() => {
10145
10324
  // when they eventually reach the apply lane. Reset the sender's
10146
10325
  // ordering watermark with the local epoch so a later arrival can be
10147
10326
  // accepted without comparing its clock to this receiver's clock.
10148
- this._peerSessions.advanceReceiveEpoch(peerHash);
10327
+ const receiveEpoch = this._peerSessions.advanceReceiveEpoch(peerHash);
10149
10328
  this.latestReplicationInfoMessage.delete(peerHash);
10329
+ const peerSession = this._peerSessions.current(peerHash);
10330
+ if (peerSession?.phase === "open") {
10331
+ this._v2Receive.advanceRecovery({
10332
+ peerHash,
10333
+ peerSession,
10334
+ receiveEpoch,
10335
+ });
10336
+ }
10150
10337
  }
10151
10338
  async resolveCandidatePeersForHash(hash, options) {
10152
10339
  if (options?.signal?.aborted)
@@ -10946,6 +11133,7 @@ let SharedLog = (() => {
10946
11133
  this._peerSyncCapabilities?.clear();
10947
11134
  this._peerSyncCapabilitySessions?.clear();
10948
11135
  this._peerSyncCapabilityTimestamps?.clear();
11136
+ this._v2Receive?.clearForClose();
10949
11137
  this._v2Send?.clearForClose();
10950
11138
  this._liveRawGossipBatches?.clear();
10951
11139
  this._nativeSharedLogState?.clearGidPeers();
@@ -11009,6 +11197,7 @@ let SharedLog = (() => {
11009
11197
  this.preventParentAttachments();
11010
11198
  this.stopRepairLifecycle();
11011
11199
  this._instanceLifecycle?.beginTerminal("close");
11200
+ this._v2Receive?.clearForClose();
11012
11201
  const replicationRangeTerminalFence = this.acquireReplicationRangeMutationTerminalFence();
11013
11202
  const pruneRemoveTerminalFence = this.acquirePruneRemoveTerminalFence();
11014
11203
  try {
@@ -11069,10 +11258,12 @@ let SharedLog = (() => {
11069
11258
  try {
11070
11259
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
11071
11260
  await Promise.all([
11072
- this.rpc.send(reset, {
11261
+ this.rpc
11262
+ .send(reset, {
11073
11263
  priority: CONVERGENCE_MESSAGE_PRIORITY,
11074
11264
  signal: abort.signal,
11075
- }).catch(() => { }),
11265
+ })
11266
+ .catch(() => { }),
11076
11267
  this._v2Send.sendTerminalReset(abort.signal),
11077
11268
  ]);
11078
11269
  }
@@ -11151,6 +11342,7 @@ let SharedLog = (() => {
11151
11342
  this.preventParentAttachments();
11152
11343
  this.stopRepairLifecycle();
11153
11344
  this._instanceLifecycle?.beginTerminal("drop");
11345
+ this._v2Receive?.clearForClose();
11154
11346
  const replicationRangeTerminalFence = this.acquireReplicationRangeMutationTerminalFence();
11155
11347
  const pruneRemoveTerminalFence = this.acquirePruneRemoveTerminalFence();
11156
11348
  try {
@@ -11194,10 +11386,12 @@ let SharedLog = (() => {
11194
11386
  try {
11195
11387
  const reset = new AllReplicatingSegmentsMessage({ segments: [] });
11196
11388
  await Promise.all([
11197
- this.rpc.send(reset, {
11389
+ this.rpc
11390
+ .send(reset, {
11198
11391
  priority: CONVERGENCE_MESSAGE_PRIORITY,
11199
11392
  signal: abort.signal,
11200
- }).catch(() => { }),
11393
+ })
11394
+ .catch(() => { }),
11201
11395
  this._v2Send.sendTerminalReset(abort.signal),
11202
11396
  ]);
11203
11397
  }
@@ -11614,13 +11808,6 @@ let SharedLog = (() => {
11614
11808
  }
11615
11809
  // Callback for receiving a message from the network
11616
11810
  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
- }
11624
11811
  const stashBackedRawMessage = isStashBackedRawExchangeHeadsMessage(msg)
11625
11812
  ? msg
11626
11813
  : undefined;
@@ -11661,18 +11848,25 @@ let SharedLog = (() => {
11661
11848
  msg = msg.toReplicationInfoMessage(); // migration
11662
11849
  }
11663
11850
  if (msg instanceof AllReplicatingSegmentsMessage ||
11664
- msg instanceof AddedReplicationSegmentMessage) {
11851
+ msg instanceof AddedReplicationSegmentMessage ||
11852
+ msg instanceof FullReplicationInfoV2Message ||
11853
+ msg instanceof AddedReplicationInfoV2Message) {
11665
11854
  // Bound decoded untrusted vectors before per-peer/global mutation
11666
11855
  // queues, trusted-replicator authorization, or liveness side effects.
11667
11856
  this.validateReplicationRangeAnnouncement(msg.segments);
11668
11857
  }
11669
- else if (msg instanceof StoppedReplicating) {
11858
+ else if (msg instanceof StoppedReplicating ||
11859
+ msg instanceof StoppedReplicationInfoV2Message) {
11670
11860
  // Bound the raw decoded vector before deduplication can hide the
11671
11861
  // allocation cost, and before liveness or apply-queue side effects.
11672
11862
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
11673
11863
  }
11674
11864
  if (!context.from.equals(this.node.identity.publicKey) &&
11675
- !(msg instanceof RequestReplicationInfoV2Message)) {
11865
+ !(msg instanceof RequestReplicationInfoV2Message) &&
11866
+ !isReplicationInfoV2Message(msg) &&
11867
+ !(msg instanceof AllReplicatingSegmentsMessage) &&
11868
+ !(msg instanceof AddedReplicationSegmentMessage) &&
11869
+ !(msg instanceof StoppedReplicating)) {
11676
11870
  this._liveness.markReplicatorActivity(receiveFromHash);
11677
11871
  }
11678
11872
  const syncProfile = this._logProperties?.sync?.profile;
@@ -12988,6 +13182,7 @@ let SharedLog = (() => {
12988
13182
  fromHash: receiveFromHash,
12989
13183
  session: receiveSession,
12990
13184
  lifecycleController: receiveReplicationLifecycleController,
13185
+ ownershipLifecycleController: receiveOwnershipLifecycleController,
12991
13186
  receiveEpoch: receiveReplicationInfoReceiveEpoch,
12992
13187
  syncProfile,
12993
13188
  lease: peerReceiveLease,
@@ -13013,6 +13208,9 @@ let SharedLog = (() => {
13013
13208
  this.clearRepairFrontierHashes(context.from.hashcode(), msg.hashes);
13014
13209
  return;
13015
13210
  }
13211
+ else if (isReplicationInfoV2Message(msg)) {
13212
+ await this.handleReplicationInfoV2Announcement(msg, laneRequestContext, lane);
13213
+ }
13016
13214
  else if (msg instanceof SyncCapabilitiesMessage) {
13017
13215
  if (!context.from.equals(this.node.identity.publicKey)) {
13018
13216
  const capabilityTransportSession = context.message?.header?.session;
@@ -13033,13 +13231,14 @@ let SharedLog = (() => {
13033
13231
  openingSession: receiveSession,
13034
13232
  });
13035
13233
  }
13036
- else {
13037
- this.observePeerSyncCapabilities({
13038
- peerHash: receiveFromHash,
13039
- capabilities: msg.capabilities,
13040
- transportSession: capabilityTransportSession,
13041
- timestamp: capabilityTimestamp,
13042
- });
13234
+ else if (this.observePeerSyncCapabilities({
13235
+ peerHash: receiveFromHash,
13236
+ capabilities: msg.capabilities,
13237
+ transportSession: capabilityTransportSession,
13238
+ timestamp: capabilityTimestamp,
13239
+ }) &&
13240
+ receiveSession?.phase === "open") {
13241
+ this.promoteReplicationInfoV2ReceiveCapability(context.from, receiveSession);
13043
13242
  }
13044
13243
  }
13045
13244
  return;
@@ -13060,6 +13259,7 @@ let SharedLog = (() => {
13060
13259
  from: context.from,
13061
13260
  peerSession: receiveSession,
13062
13261
  receiverTransportSession: context.message.header.session,
13262
+ capabilityTimestamp: this._peerSyncCapabilityTimestamps.get(receiveFromHash),
13063
13263
  requestTimestamp: context.message.header.timestamp,
13064
13264
  })) {
13065
13265
  this._liveness.markReplicatorActivity(receiveFromHash);
@@ -13335,6 +13535,147 @@ let SharedLog = (() => {
13335
13535
  }
13336
13536
  }
13337
13537
  }
13538
+ async handleReplicationInfoV2Announcement(msg, context, lane) {
13539
+ const from = context.from;
13540
+ const fromHash = lane.fromHash;
13541
+ const receiveSession = lane.session;
13542
+ if (from.equals(this.node.identity.publicKey) ||
13543
+ receiveSession === null ||
13544
+ receiveSession.phase !== "open") {
13545
+ return;
13546
+ }
13547
+ // Authenticate and reserve before the per-peer lane. One reservation per
13548
+ // peer bounds decoded-frame retention while another mutation is parked.
13549
+ const receiveState = this._v2Receive._receiveStates.get(fromHash);
13550
+ if (!receiveState ||
13551
+ receiveState.peerSession !== receiveSession ||
13552
+ receiveState.senderTransportSession !== context.message.header.session) {
13553
+ return;
13554
+ }
13555
+ const admission = this._v2Receive.reserve(msg, {
13556
+ from,
13557
+ peerSession: receiveSession,
13558
+ receiveEpoch: lane.receiveEpoch,
13559
+ senderTransportSession: context.message.header.session,
13560
+ transportTimestamp: context.message.header.timestamp,
13561
+ });
13562
+ if (!admission) {
13563
+ return;
13564
+ }
13565
+ lane.lease.release();
13566
+ try {
13567
+ await this.withReplicationInfoApplyQueue(fromHash, async () => {
13568
+ const hostGate = () => this._instanceLifecycle.isMembershipActiveFor(lane.lifecycleController) &&
13569
+ this.isRepairLifecycleActive(lane.ownershipLifecycleController) &&
13570
+ this._peerSessions.isCurrent(fromHash, receiveSession) &&
13571
+ receiveSession.phase === "open" &&
13572
+ this._peerSessions.isReceiveEpochCurrent(fromHash, lane.receiveEpoch) &&
13573
+ !this._peerSessions.isReplicationInfoBlocked(fromHash) &&
13574
+ this._peerSessions.isReceiveCleanupGateOpen(fromHash);
13575
+ if (!hostGate()) {
13576
+ return;
13577
+ }
13578
+ const exactGate = () => hostGate() && this._v2Receive.isAdmissionCurrent(admission);
13579
+ let durableCommitted = false;
13580
+ try {
13581
+ if (msg instanceof FullReplicationInfoV2Message ||
13582
+ msg instanceof AddedReplicationInfoV2Message) {
13583
+ let mutationGateChecked = false;
13584
+ let mutationGateAdmitted = false;
13585
+ const result = await this.addReplicationRange(msg.segments.map((segment) => segment.toReplicationRangeIndexable(from)), from, {
13586
+ reset: msg instanceof FullReplicationInfoV2Message,
13587
+ checkDuplicates: true,
13588
+ timestamp: Number(context.message.header.timestamp),
13589
+ allowLegacyOrderedReplacementPairs: msg instanceof AddedReplicationInfoV2Message,
13590
+ shouldApply: () => {
13591
+ mutationGateChecked = true;
13592
+ mutationGateAdmitted = exactGate();
13593
+ return mutationGateAdmitted;
13594
+ },
13595
+ onDurableApplyCommitted: () => {
13596
+ if (!exactGate() || !this._v2Receive.commit(admission)) {
13597
+ return false;
13598
+ }
13599
+ durableCommitted = true;
13600
+ return true;
13601
+ },
13602
+ }, lane.ownershipLifecycleController);
13603
+ if (result === undefined ||
13604
+ !mutationGateChecked ||
13605
+ !mutationGateAdmitted ||
13606
+ !durableCommitted ||
13607
+ !exactGate()) {
13608
+ return;
13609
+ }
13610
+ }
13611
+ else {
13612
+ if (!exactGate()) {
13613
+ return;
13614
+ }
13615
+ const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(msg.segmentIds, from);
13616
+ if (!exactGate()) {
13617
+ return;
13618
+ }
13619
+ let mutationGateAdmitted = true;
13620
+ const removedRanges = [];
13621
+ const removed = await this.removeReplicationRanges(rangesToRemove, from, {
13622
+ shouldRemove: () => {
13623
+ mutationGateAdmitted = exactGate();
13624
+ return mutationGateAdmitted;
13625
+ },
13626
+ onDurableRemoveCommitted: () => {
13627
+ if (!exactGate() || !this._v2Receive.commit(admission)) {
13628
+ return false;
13629
+ }
13630
+ durableCommitted = true;
13631
+ return true;
13632
+ },
13633
+ onRemoved: (ranges) => removedRanges.push(...ranges),
13634
+ }, lane.ownershipLifecycleController);
13635
+ if (!durableCommitted) {
13636
+ if (!mutationGateAdmitted ||
13637
+ !exactGate() ||
13638
+ removed ||
13639
+ !this._v2Receive.commit(admission)) {
13640
+ return;
13641
+ }
13642
+ durableCommitted = true;
13643
+ }
13644
+ if (this._instanceLifecycle.isMembershipActiveFor(lane.lifecycleController) &&
13645
+ this.isRepairLifecycleActive(lane.ownershipLifecycleController)) {
13646
+ const timestamp = BigInt(Date.now());
13647
+ for (const range of removedRanges) {
13648
+ this.replicationChangeDebounceFn.add({
13649
+ range,
13650
+ type: "removed",
13651
+ timestamp,
13652
+ });
13653
+ }
13654
+ }
13655
+ }
13656
+ }
13657
+ catch (error) {
13658
+ this._v2Receive.requireFullAfterFailure(admission);
13659
+ throw error;
13660
+ }
13661
+ if (!durableCommitted) {
13662
+ if (!exactGate() || !this._v2Receive.commit(admission)) {
13663
+ return;
13664
+ }
13665
+ }
13666
+ if (!hostGate()) {
13667
+ return;
13668
+ }
13669
+ this._liveness.markReplicatorActivity(fromHash);
13670
+ if (msg instanceof FullReplicationInfoV2Message) {
13671
+ this.cancelReplicationInfoRequests(fromHash);
13672
+ }
13673
+ });
13674
+ }
13675
+ finally {
13676
+ this._v2Receive.release(admission);
13677
+ }
13678
+ }
13338
13679
  async handleReplicationInfoAnnouncement(msg, context, lane) {
13339
13680
  const receiveFromHash = lane.fromHash;
13340
13681
  const receiveSession = lane.session;
@@ -13350,6 +13691,19 @@ let SharedLog = (() => {
13350
13691
  // (and downstream `waitForReplicator()` timeouts) under timing-sensitive joins.
13351
13692
  const from = context.from;
13352
13693
  const fromHash = from.hashcode();
13694
+ if (this._v2Receive.isLegacyCutover(receiveSession)) {
13695
+ if (receiveSession) {
13696
+ this._v2Receive.noteLegacyAnnouncement({
13697
+ peerHash: fromHash,
13698
+ peerSession: receiveSession,
13699
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
13700
+ senderTransportSession: context.message.header.session,
13701
+ transportTimestamp: context.message.header.timestamp,
13702
+ message: msg,
13703
+ });
13704
+ }
13705
+ return;
13706
+ }
13353
13707
  // Pre-lane gate: lifecycle -> receive-epoch -> blocked, exactly the
13354
13708
  // legacy order. isMembershipActiveFor is the unit-pinned fold of
13355
13709
  // isReplicationLifecycleActive; isReceiveEpochCurrent is the
@@ -13374,6 +13728,17 @@ let SharedLog = (() => {
13374
13728
  this._peerSessions.isReplicationInfoBlocked(fromHash)) {
13375
13729
  return;
13376
13730
  }
13731
+ if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
13732
+ this._v2Receive.noteLegacyAnnouncement({
13733
+ peerHash: fromHash,
13734
+ peerSession: receiveSession,
13735
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
13736
+ senderTransportSession: context.message.header.session,
13737
+ transportTimestamp: context.message.header.timestamp,
13738
+ message: msg,
13739
+ });
13740
+ return;
13741
+ }
13377
13742
  // Process in-order to avoid races where repeated reset messages arrive
13378
13743
  // concurrently and trigger spurious "added" diffs / rebalancing.
13379
13744
  const prev = this.latestReplicationInfoMessage.get(fromHash);
@@ -13381,17 +13746,20 @@ let SharedLog = (() => {
13381
13746
  return;
13382
13747
  }
13383
13748
  this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
13384
- this._liveness._replicatorLivenessFailures.delete(fromHash);
13385
13749
  if (this.closed) {
13386
13750
  return;
13387
13751
  }
13388
13752
  const reset = msg instanceof AllReplicatingSegmentsMessage;
13389
- await this.addReplicationRange(replicationInfoMessage.segments.map((x) => x.toReplicationRangeIndexable(from)), from, {
13753
+ const result = await this.addReplicationRange(replicationInfoMessage.segments.map((x) => x.toReplicationRangeIndexable(from)), from, {
13390
13754
  reset,
13391
13755
  checkDuplicates: true,
13392
13756
  timestamp: Number(messageTimestamp),
13393
13757
  allowLegacyOrderedReplacementPairs: msg instanceof AddedReplicationSegmentMessage,
13394
13758
  });
13759
+ if (result === undefined) {
13760
+ return;
13761
+ }
13762
+ this._liveness.markReplicatorActivity(fromHash);
13395
13763
  // If the peer reports any replication segments, stop re-requesting.
13396
13764
  // (Empty reports can be transient during startup.)
13397
13765
  if (replicationInfoMessage.segments.length > 0) {
@@ -13418,6 +13786,19 @@ let SharedLog = (() => {
13418
13786
  return;
13419
13787
  }
13420
13788
  const fromHash = from.hashcode();
13789
+ if (this._v2Receive.isLegacyCutover(receiveSession)) {
13790
+ if (receiveSession) {
13791
+ this._v2Receive.noteLegacyAnnouncement({
13792
+ peerHash: fromHash,
13793
+ peerSession: receiveSession,
13794
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
13795
+ senderTransportSession: context.message.header.session,
13796
+ transportTimestamp: context.message.header.timestamp,
13797
+ message: msg,
13798
+ });
13799
+ }
13800
+ return;
13801
+ }
13421
13802
  // Same pre-lane gate shape as Added/All above (and the same
13422
13803
  // intentional absence of a subscription-epoch term).
13423
13804
  if (!this._instanceLifecycle.isMembershipActiveFor(receiveReplicationLifecycleController) ||
@@ -13434,17 +13815,28 @@ let SharedLog = (() => {
13434
13815
  this._peerSessions.isReplicationInfoBlocked(fromHash)) {
13435
13816
  return;
13436
13817
  }
13818
+ if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
13819
+ this._v2Receive.noteLegacyAnnouncement({
13820
+ peerHash: fromHash,
13821
+ peerSession: receiveSession,
13822
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
13823
+ senderTransportSession: context.message.header.session,
13824
+ transportTimestamp: context.message.header.timestamp,
13825
+ message: msg,
13826
+ });
13827
+ return;
13828
+ }
13437
13829
  const previousTimestamp = this.latestReplicationInfoMessage.get(fromHash);
13438
13830
  if (previousTimestamp && previousTimestamp > messageTimestamp) {
13439
13831
  return;
13440
13832
  }
13441
13833
  this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
13442
- this._liveness._replicatorLivenessFailures.delete(fromHash);
13443
13834
  if (this.closed) {
13444
13835
  return;
13445
13836
  }
13446
13837
  const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(segmentIds, from);
13447
13838
  await this.removeReplicationRanges(rangesToRemove, from);
13839
+ this._liveness.markReplicatorActivity(fromHash);
13448
13840
  const timestamp = BigInt(+new Date());
13449
13841
  for (const range of rangesToRemove) {
13450
13842
  this.replicationChangeDebounceFn.add({
@@ -15313,6 +15705,8 @@ let SharedLog = (() => {
15313
15705
  * This method must run inside the global replication-range mutation lane.
15314
15706
  */
15315
15707
  async deleteReplicationRangesCoherently(ranges, ownerHash, options) {
15708
+ const wasReplicator = this.uniqueReplicators.has(ownerHash);
15709
+ const joinWasEmitted = this._replicatorJoinEmitted.has(ownerHash);
15316
15710
  if (ranges.length === 0) {
15317
15711
  const ownerHasRanges = (await this.replicationIndex.count({ query: { hash: ownerHash } })) > 0;
15318
15712
  if (!ownerHasRanges) {
@@ -15323,13 +15717,25 @@ let SharedLog = (() => {
15323
15717
  removed: [],
15324
15718
  retained: [],
15325
15719
  ownerHasRanges,
15720
+ rollback: async () => {
15721
+ if (wasReplicator) {
15722
+ this.uniqueReplicators.add(ownerHash);
15723
+ }
15724
+ else {
15725
+ this.uniqueReplicators.delete(ownerHash);
15726
+ }
15727
+ if (joinWasEmitted) {
15728
+ this._replicatorJoinEmitted.add(ownerHash);
15729
+ }
15730
+ else {
15731
+ this._replicatorJoinEmitted.delete(ownerHash);
15732
+ }
15733
+ },
15326
15734
  };
15327
15735
  }
15328
15736
  const uniqueRanges = [
15329
15737
  ...new Map(ranges.map((range) => [range.idString, range])).values(),
15330
15738
  ];
15331
- const wasReplicator = this.uniqueReplicators.has(ownerHash);
15332
- const joinWasEmitted = this._replicatorJoinEmitted.has(ownerHash);
15333
15739
  const snapshots = uniqueRanges.map((range) => {
15334
15740
  const pending = this.pendingMaturity.get(ownerHash)?.get(range.idString);
15335
15741
  return {
@@ -15516,10 +15922,76 @@ let SharedLog = (() => {
15516
15922
  outcomeError = new AggregateError(errors, "Replication-range deletion and post-state reconciliation failed");
15517
15923
  this.poisonReplicationOwnership(outcomeError);
15518
15924
  }
15925
+ let rollbackStarted = false;
15926
+ const rollback = async () => {
15927
+ if (rollbackStarted) {
15928
+ return;
15929
+ }
15930
+ rollbackStarted = true;
15931
+ const rollbackErrors = [];
15932
+ const removedIds = new Set(removed.map((range) => range.idString));
15933
+ for (const snapshot of snapshots) {
15934
+ if (!removedIds.has(snapshot.range.idString)) {
15935
+ continue;
15936
+ }
15937
+ try {
15938
+ await this.replicationIndex.put(snapshot.range);
15939
+ this.putNativeReplicationRange(snapshot.range);
15940
+ }
15941
+ catch (error) {
15942
+ rollbackErrors.push(error);
15943
+ }
15944
+ }
15945
+ for (const snapshot of snapshots) {
15946
+ if (!removedIds.has(snapshot.range.idString) ||
15947
+ !snapshot.pending ||
15948
+ this.pendingMaturity.get(ownerHash)?.has(snapshot.range.idString)) {
15949
+ continue;
15950
+ }
15951
+ try {
15952
+ this.schedulePendingMaturity(snapshot.pending.range, snapshot.pending.from, {
15953
+ rebalance: snapshot.pending.rebalance,
15954
+ waitMs: Math.max(0, snapshot.pending.expiresAt - Date.now()),
15955
+ }, snapshot.pending.ownershipLifecycleController);
15956
+ }
15957
+ catch (error) {
15958
+ rollbackErrors.push(error);
15959
+ }
15960
+ }
15961
+ try {
15962
+ if (wasReplicator) {
15963
+ this.uniqueReplicators.add(ownerHash);
15964
+ }
15965
+ else {
15966
+ this.uniqueReplicators.delete(ownerHash);
15967
+ }
15968
+ if (joinWasEmitted) {
15969
+ this._replicatorJoinEmitted.add(ownerHash);
15970
+ }
15971
+ else {
15972
+ this._replicatorJoinEmitted.delete(ownerHash);
15973
+ }
15974
+ }
15975
+ catch (error) {
15976
+ rollbackErrors.push(error);
15977
+ }
15978
+ try {
15979
+ await this.updateOldestTimestampFromIndex();
15980
+ }
15981
+ catch (error) {
15982
+ rollbackErrors.push(error);
15983
+ }
15984
+ if (rollbackErrors.length > 0) {
15985
+ const failure = new AggregateError(rollbackErrors, "Replication-range deletion rollback failed");
15986
+ this.poisonReplicationOwnership(failure);
15987
+ throw failure;
15988
+ }
15989
+ };
15519
15990
  return {
15520
15991
  removed,
15521
15992
  retained,
15522
15993
  ownerHasRanges,
15994
+ rollback,
15523
15995
  error: outcomeError,
15524
15996
  };
15525
15997
  }
@@ -15532,7 +16004,8 @@ let SharedLog = (() => {
15532
16004
  }
15533
16005
  this._replicationInfoRequestByPeer.delete(peerHash);
15534
16006
  }
15535
- scheduleReplicationInfoRequests(peer, replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController) {
16007
+ scheduleReplicationInfoRequests(peer, replicationLifecycleController = this._instanceLifecycle
16008
+ ?.membershipLifecycleController) {
15536
16009
  if (!replicationLifecycleController ||
15537
16010
  !this.isReplicationLifecycleActive(replicationLifecycleController)) {
15538
16011
  return;
@@ -15606,6 +16079,7 @@ let SharedLog = (() => {
15606
16079
  // A destination stream is scoped to exactly one topic-subscription
15607
16080
  // session. Abort the predecessor synchronously before either barrier can
15608
16081
  // yield; a late queue completion must never enter the new session.
16082
+ this._v2Receive.clearPeer(peerHash);
15609
16083
  this._v2Send.clearPeer(peerHash);
15610
16084
  this._peerSyncCapabilitySessions.delete(peerHash);
15611
16085
  this._peerSyncCapabilityTimestamps.delete(peerHash);
@@ -15720,23 +16194,22 @@ let SharedLog = (() => {
15720
16194
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15721
16195
  this._liveness.markReplicatorActivity(peerHash);
15722
16196
  this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
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] }),
16197
+ this.promoteReplicationInfoV2ReceiveCapability(publicKey, expectedSubscriptionEpoch);
16198
+ // Decode, sender and authenticated apply readiness are separate bits. Start
16199
+ // the ACKed receiver-led negotiation before the legacy snapshot, but do not
16200
+ // hold mixed-version replication discovery behind that round trip. Attach
16201
+ // the rejection handler now because a lifecycle fence below may return before
16202
+ // awaiting the advert. RequestV2 is armed only after both sends settle.
16203
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
16204
+ const localCapabilityAdvertisement = this.advertiseReplicationInfoV2ReceiveCapability({
16205
+ target: publicKey,
16206
+ peerSession: expectedSubscriptionEpoch,
16207
+ receiveEpoch,
15737
16208
  signal: replicationLifecycleController.signal,
15738
- })
15739
- .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
16209
+ }).catch((error) => {
16210
+ this.handleReplicationLifecycleSendError(error, replicationLifecycleController);
16211
+ return undefined;
16212
+ });
15740
16213
  let replicationSegments;
15741
16214
  try {
15742
16215
  replicationSegments = await this.getMyReplicationSegments();
@@ -15782,13 +16255,22 @@ let SharedLog = (() => {
15782
16255
  .catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
15783
16256
  }
15784
16257
  }
15785
- // Request the remote peer's replication info. This makes joins resilient to
15786
- // timing-sensitive delivery/order issues where we may miss their initial
15787
- // replication announcement.
16258
+ // Keep legacy request-based discovery independent of the capability ACK too.
16259
+ // This makes mixed-version joins resilient to timing-sensitive delivery/order
16260
+ // issues where we may miss the remote peer's initial announcement.
15788
16261
  if (this.isReplicationLifecycleActive(replicationLifecycleController) &&
15789
16262
  ownsSubscriptionEpoch()) {
15790
16263
  this.scheduleReplicationInfoRequests(publicKey, replicationLifecycleController);
15791
16264
  }
16265
+ const localCapabilityReady = await localCapabilityAdvertisement;
16266
+ if (localCapabilityReady) {
16267
+ this._v2Receive.markLocalCapabilityReady({
16268
+ peerHash,
16269
+ peerSession: expectedSubscriptionEpoch,
16270
+ receiveEpoch,
16271
+ ...localCapabilityReady,
16272
+ });
16273
+ }
15792
16274
  }
15793
16275
  getClampedReplicas(customValue) {
15794
16276
  if (!customValue) {