@peerbit/shared-log 13.2.31 → 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.
Files changed (36) hide show
  1. package/dist/src/exchange-heads.d.ts +13 -2
  2. package/dist/src/exchange-heads.d.ts.map +1 -1
  3. package/dist/src/exchange-heads.js +13 -2
  4. package/dist/src/exchange-heads.js.map +1 -1
  5. package/dist/src/index.d.ts +11 -0
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +727 -85
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/replication-announcement.d.ts +3 -1
  10. package/dist/src/replication-announcement.d.ts.map +1 -1
  11. package/dist/src/replication-announcement.js +1 -0
  12. package/dist/src/replication-announcement.js.map +1 -1
  13. package/dist/src/replication-info-v2-binding.d.ts +16 -0
  14. package/dist/src/replication-info-v2-binding.d.ts.map +1 -0
  15. package/dist/src/replication-info-v2-binding.js +30 -0
  16. package/dist/src/replication-info-v2-binding.js.map +1 -0
  17. package/dist/src/replication-info-v2-receive.d.ts +181 -0
  18. package/dist/src/replication-info-v2-receive.d.ts.map +1 -0
  19. package/dist/src/replication-info-v2-receive.js +725 -0
  20. package/dist/src/replication-info-v2-receive.js.map +1 -0
  21. package/dist/src/replication-info-v2-send.d.ts +95 -0
  22. package/dist/src/replication-info-v2-send.d.ts.map +1 -0
  23. package/dist/src/replication-info-v2-send.js +398 -0
  24. package/dist/src/replication-info-v2-send.js.map +1 -0
  25. package/dist/src/replication.d.ts +26 -4
  26. package/dist/src/replication.d.ts.map +1 -1
  27. package/dist/src/replication.js +60 -5
  28. package/dist/src/replication.js.map +1 -1
  29. package/package.json +11 -11
  30. package/src/exchange-heads.ts +15 -2
  31. package/src/index.ts +1040 -201
  32. package/src/replication-announcement.ts +10 -4
  33. package/src/replication-info-v2-binding.ts +44 -0
  34. package/src/replication-info-v2-receive.ts +1045 -0
  35. package/src/replication-info-v2-send.ts +540 -0
  36. package/src/replication.ts +39 -5
package/src/index.ts CHANGED
@@ -130,6 +130,14 @@ import {
130
130
  type CheckedPruneRetryIdentity,
131
131
  type CheckedPruneRetryState,
132
132
  } from "./checked-prune.js";
133
+ import {
134
+ CoordinatePersistenceCoordinator,
135
+ type MaybePromise,
136
+ combineCoordinateDeleteHashes,
137
+ isPromiseLike,
138
+ mapMaybePromise,
139
+ normalizedHashValues,
140
+ } from "./coordinate-persistence.js";
133
141
  import { type CPUUsage, CPUUsageIntervalLag } from "./cpu.js";
134
142
  import {
135
143
  type DebouncedAccumulatorMap,
@@ -154,7 +162,9 @@ import {
154
162
  ResponseIPrune,
155
163
  ResponseIPruneV2,
156
164
  SYNC_CAPABILITY_RAW_EXCHANGE_HEADS,
165
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY,
157
166
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE,
167
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND,
158
168
  StashBackedRawExchangeHeadsMessage,
159
169
  SyncCapabilitiesMessage,
160
170
  collectRawExchangeHeadSendPlan,
@@ -179,14 +189,6 @@ import {
179
189
  materializeVerifiedRawExchangeHeadsMessage,
180
190
  } from "./exchange-heads.js";
181
191
  import { FanoutEnvelope } from "./fanout-envelope.js";
182
- import {
183
- CoordinatePersistenceCoordinator,
184
- type MaybePromise,
185
- combineCoordinateDeleteHashes,
186
- isPromiseLike,
187
- mapMaybePromise,
188
- normalizedHashValues,
189
- } from "./coordinate-persistence.js";
190
192
  import { InstanceLifecycle } from "./instance-lifecycle.js";
191
193
  import {
192
194
  MAX_U32,
@@ -199,10 +201,7 @@ import { JoinWarmupCoordinator, type WarmupSession } from "./join-warmup.js";
199
201
  import { LeaderPlanCache } from "./leader-plan-cache.js";
200
202
  import { TransportMessage } from "./message.js";
201
203
  import { NativeBackboneWriteThroughBlockStore } from "./native-write-through-block-store.js";
202
- import {
203
- type PeerSession,
204
- PeerSessionRegistry,
205
- } from "./peer-session.js";
204
+ import { type PeerSession, PeerSessionRegistry } from "./peer-session.js";
206
205
  import { PIDReplicationController } from "./pid.js";
207
206
  import {
208
207
  type EntryReplicated,
@@ -249,17 +248,24 @@ import {
249
248
  type ReplicationDomain,
250
249
  type ReplicationDomainConstructor,
251
250
  } from "./replication-domain.js";
251
+ import { ReplicationInfoV2ReceiveCoordinator } from "./replication-info-v2-receive.js";
252
+ import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
252
253
  import {
253
254
  AbsoluteReplicas,
255
+ AddedReplicationInfoV2Message,
254
256
  AddedReplicationSegmentMessage,
255
257
  AllReplicatingSegmentsMessage,
258
+ FullReplicationInfoV2Message,
256
259
  MinReplicas,
257
260
  ReplicationError,
261
+ type ReplicationInfoV2Message,
258
262
  type ReplicationLimits,
259
263
  ReplicationPingMessage,
260
264
  RequestReplicationInfoMessage,
265
+ RequestReplicationInfoV2Message,
261
266
  ResponseRoleMessage,
262
267
  StoppedReplicating,
268
+ StoppedReplicationInfoV2Message,
263
269
  decodeReplicas,
264
270
  encodeReplicas,
265
271
  isReplicationInfoV2Message,
@@ -350,6 +356,7 @@ type ReceiveLaneContext = {
350
356
  fromHash: string;
351
357
  session: PeerSession | null;
352
358
  lifecycleController: AbortController | undefined;
359
+ ownershipLifecycleController: AbortController;
353
360
  receiveEpoch: object | null;
354
361
  syncProfile: SyncProfileFn | undefined;
355
362
  lease: PeerReceiveLease;
@@ -556,6 +563,7 @@ type ReplicationRangeDeletionOutcome<R extends "u32" | "u64"> = {
556
563
  removed: ReplicationRangeIndexable<R>[];
557
564
  retained: ReplicationRangeIndexable<R>[];
558
565
  ownerHasRanges: boolean;
566
+ rollback: () => Promise<void>;
559
567
  error?: unknown;
560
568
  };
561
569
 
@@ -628,7 +636,8 @@ export type NativeBackboneCoordinateRollback<R extends "u32" | "u64"> = {
628
636
  generations: Map<string, number>;
629
637
  };
630
638
 
631
- export type RepairDispatchEntry<R extends "u32" | "u64"> = ResidentCoordinateEntry<R>;
639
+ export type RepairDispatchEntry<R extends "u32" | "u64"> =
640
+ ResidentCoordinateEntry<R>;
632
641
 
633
642
  type PreparedLocalAppendCommit<R extends "u32" | "u64"> = {
634
643
  hash: string;
@@ -1087,7 +1096,6 @@ const isReplicationOptionsDependentOnPreviousState = async (
1087
1096
  return false;
1088
1097
  };
1089
1098
 
1090
-
1091
1099
  export interface IndexableDomain<R extends "u32" | "u64"> {
1092
1100
  numbers: Numbers<R>;
1093
1101
  constructorEntry: new (properties: {
@@ -1986,7 +1994,8 @@ export class SharedLog<
1986
1994
  }
1987
1995
 
1988
1996
  get _nativeBackboneCoordinateJournalLastFlushMs(): number {
1989
- return this._coordinates?._nativeBackboneCoordinateJournalLastFlushMs as number;
1997
+ return this._coordinates
1998
+ ?._nativeBackboneCoordinateJournalLastFlushMs as number;
1990
1999
  }
1991
2000
 
1992
2001
  set _nativeBackboneCoordinateJournalLastFlushMs(value: number) {
@@ -2024,7 +2033,12 @@ export class SharedLog<
2024
2033
  // PeerSession the advert was staged under; promote/delete key off it.
2025
2034
  private _openingSyncCapabilitiesByPeer!: Map<
2026
2035
  string,
2027
- { epoch: object; capabilities: number }
2036
+ {
2037
+ epoch: object;
2038
+ capabilities: number;
2039
+ transportSession?: bigint;
2040
+ timestamp?: bigint;
2041
+ }
2028
2042
  >;
2029
2043
  private _onFanoutDataFn?: (arg: any) => void;
2030
2044
  private _onFanoutUnicastFn?: (arg: any) => void;
@@ -3068,7 +3082,8 @@ export class SharedLog<
3068
3082
  if (!lowerMarkerCommitted) {
3069
3083
  if (intent.coordinates.length > 0) {
3070
3084
  const mutationGenerations =
3071
- (this._coordinates._nativeCoordinateMutationGenerations ??= new Map());
3085
+ (this._coordinates._nativeCoordinateMutationGenerations ??=
3086
+ new Map());
3072
3087
  const rollback: NativeBackboneCoordinateRollback<R> = {
3073
3088
  hashes: new Set(),
3074
3089
  entries: new Map(),
@@ -3100,7 +3115,10 @@ export class SharedLog<
3100
3115
  );
3101
3116
  }
3102
3117
  }
3103
- await this._coordinates.rollbackNativeBackboneCoordinateAppendDurably("", rollback);
3118
+ await this._coordinates.rollbackNativeBackboneCoordinateAppendDurably(
3119
+ "",
3120
+ rollback,
3121
+ );
3104
3122
  }
3105
3123
  for (const document of intent.documents) {
3106
3124
  this.restoreNativeBackboneDocument({
@@ -3225,6 +3243,8 @@ export class SharedLog<
3225
3243
  | ReturnType<typeof debounceFixedInterval>
3226
3244
  | undefined;
3227
3245
  private _announcements!: ReplicationAnnouncementCoordinator<R>;
3246
+ private _v2Receive!: ReplicationInfoV2ReceiveCoordinator;
3247
+ private _v2Send!: ReplicationInfoV2SendCoordinator<R>;
3228
3248
 
3229
3249
  // A fn for debouncing the calls for pruning
3230
3250
  pruneDebouncedFn!: DebouncedAccumulatorMap<{
@@ -3286,6 +3306,10 @@ export class SharedLog<
3286
3306
  // Sync capability bits advertised by peers (SyncCapabilitiesMessage), keyed
3287
3307
  // by public key hash. Entries are dropped on unsubscribe/disconnect.
3288
3308
  private _peerSyncCapabilities!: Map<string, number>;
3309
+ // Signed transport session carried by the capability envelope. Kept in a
3310
+ // parallel map so existing capability-number consumers remain unchanged.
3311
+ private _peerSyncCapabilitySessions!: Map<string, bigint>;
3312
+ private _peerSyncCapabilityTimestamps!: Map<string, bigint>;
3289
3313
  // Pending live raw exchange-head gossip, coalesced per recipient set and
3290
3314
  // flushed at the end of the current event-loop turn (or when a batch cap
3291
3315
  // is hit). Only used when every recipient advertised raw capability.
@@ -3354,15 +3378,20 @@ export class SharedLog<
3354
3378
  queueCurrentReplicationStateAnnouncementRepair: () =>
3355
3379
  this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
3356
3380
  queueCurrentReplicationStateAnnouncementRetry: (error: unknown) =>
3357
- this._announcements.queueCurrentReplicationStateAnnouncementRetry(error),
3381
+ this._announcements.queueCurrentReplicationStateAnnouncementRetry(
3382
+ error,
3383
+ ),
3384
+ enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
3358
3385
  isClosed: () => this.closed,
3359
3386
  getCloseSignal: () => this._closeController.signal,
3360
3387
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
3361
3388
  validatePersistedReplicationRangeSnapshot: (ranges) =>
3362
3389
  this.validatePersistedReplicationRangeSnapshot(ranges),
3363
- getSubscribers: () => this.node.services.pubsub.getSubscribers(this.topic),
3390
+ getSubscribers: () =>
3391
+ this.node.services.pubsub.getSubscribers(this.topic),
3364
3392
  getSelfHash: () => this.node.identity.publicKey.hashcode(),
3365
- isBlockedPeer: (hash) => this._peerSessions.isReplicationInfoBlocked(hash),
3393
+ isBlockedPeer: (hash) =>
3394
+ this._peerSessions.isReplicationInfoBlocked(hash),
3366
3395
  getRpc: () => this.rpc,
3367
3396
  captureReplicationOwnershipLifecycle: () =>
3368
3397
  this.captureReplicationOwnershipLifecycle(),
@@ -3374,6 +3403,138 @@ export class SharedLog<
3374
3403
  });
3375
3404
  }
3376
3405
 
3406
+ private createReplicationInfoV2SendCoordinator(): ReplicationInfoV2SendCoordinator<R> {
3407
+ return new ReplicationInfoV2SendCoordinator<R>({
3408
+ getRpc: () => this.rpc,
3409
+ getSelfKey: () => this.node.identity.publicKey,
3410
+ getSenderTransportSession: () =>
3411
+ BigInt(
3412
+ (this.node.services.pubsub as unknown as { session: number }).session,
3413
+ ),
3414
+ getMyReplicationSegments: () => this.getMyReplicationSegments(),
3415
+ validatePersistedReplicationRangeSnapshot: (ranges) =>
3416
+ this.validatePersistedReplicationRangeSnapshot(ranges),
3417
+ isClosed: () => this.closed,
3418
+ isPeerSessionOpen: (peerHash, peerSession) =>
3419
+ this._peerSessions.isCurrent(peerHash, peerSession) &&
3420
+ (peerSession as PeerSession).phase === "open" &&
3421
+ !this._peerSessions.isReplicationInfoBlocked(peerHash) &&
3422
+ this._peerSessions.isReceiveCleanupGateOpen(peerHash),
3423
+ captureReplicationOwnershipLifecycle: () =>
3424
+ this.captureReplicationOwnershipLifecycle(),
3425
+ isReplicationOwnershipLifecycleActive: (controller) =>
3426
+ this.isRepairLifecycleActive(controller),
3427
+ });
3428
+ }
3429
+
3430
+ private replicationInfoV2ReceiveCapabilities(): number {
3431
+ return (
3432
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
3433
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND |
3434
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY |
3435
+ (this._logProperties?.sync?.rawExchangeHeads === true
3436
+ ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
3437
+ : 0)
3438
+ );
3439
+ }
3440
+
3441
+ private async advertiseReplicationInfoV2ReceiveCapability(properties: {
3442
+ target: PublicSignKey;
3443
+ peerSession: PeerSession;
3444
+ receiveEpoch: object | null;
3445
+ signal: AbortSignal;
3446
+ }): Promise<
3447
+ { receiverTransportSession: bigint; requestNotBeforeMs: number } | undefined
3448
+ > {
3449
+ const peerHash = properties.target.hashcode();
3450
+ const receiverTransportSession = BigInt(
3451
+ (this.node.services.pubsub as unknown as { session: number }).session,
3452
+ );
3453
+ await this.rpc.send(
3454
+ new SyncCapabilitiesMessage({
3455
+ capabilities: this.replicationInfoV2ReceiveCapabilities(),
3456
+ }),
3457
+ {
3458
+ mode: new AcknowledgeDelivery({
3459
+ redundancy: 1,
3460
+ to: [properties.target],
3461
+ }),
3462
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3463
+ signal: properties.signal,
3464
+ },
3465
+ );
3466
+ if (
3467
+ properties.signal.aborted ||
3468
+ this.closed ||
3469
+ !this._peerSessions.isCurrent(peerHash, properties.peerSession) ||
3470
+ properties.peerSession.phase !== "open" ||
3471
+ !properties.peerSession.isActive() ||
3472
+ !this._peerSessions.isReceiveEpochCurrent(
3473
+ peerHash,
3474
+ properties.receiveEpoch,
3475
+ ) ||
3476
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
3477
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash) ||
3478
+ BigInt(
3479
+ (this.node.services.pubsub as unknown as { session: number }).session,
3480
+ ) !== receiverTransportSession
3481
+ ) {
3482
+ return undefined;
3483
+ }
3484
+ return {
3485
+ receiverTransportSession,
3486
+ requestNotBeforeMs: Date.now(),
3487
+ };
3488
+ }
3489
+
3490
+ private createReplicationInfoV2ReceiveCoordinator(): ReplicationInfoV2ReceiveCoordinator {
3491
+ return new ReplicationInfoV2ReceiveCoordinator({
3492
+ getSelfKey: () => this.node.identity.publicKey,
3493
+ getReceiverTransportSession: () =>
3494
+ BigInt(
3495
+ (this.node.services.pubsub as unknown as { session: number }).session,
3496
+ ),
3497
+ isClosed: () => this.closed,
3498
+ isPeerStateCurrent: (peerHash, peerSession, receiveEpoch) =>
3499
+ this._peerSessions.isCurrent(peerHash, peerSession) &&
3500
+ (peerSession as PeerSession).phase === "open" &&
3501
+ (peerSession as PeerSession).isActive() &&
3502
+ this._peerSessions.isReceiveEpochCurrent(peerHash, receiveEpoch) &&
3503
+ !this._peerSessions.isReplicationInfoBlocked(peerHash) &&
3504
+ this._peerSessions.isReceiveCleanupGateOpen(peerHash),
3505
+ isSenderTransportSessionCurrent: (peerHash, senderTransportSession) =>
3506
+ this._peerSyncCapabilitySessions.get(peerHash) ===
3507
+ senderTransportSession,
3508
+ sendRequest: async (request, target, signal) => {
3509
+ await this.rpc.send(request, {
3510
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [target] }),
3511
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3512
+ signal,
3513
+ });
3514
+ },
3515
+ refreshLocalCapability: (properties) =>
3516
+ this.advertiseReplicationInfoV2ReceiveCapability({
3517
+ target: properties.target,
3518
+ peerSession: properties.peerSession as PeerSession,
3519
+ receiveEpoch: properties.receiveEpoch,
3520
+ signal: properties.signal,
3521
+ }),
3522
+ onRequestError: (error) => {
3523
+ if (
3524
+ isNotStartedError(error as Error) ||
3525
+ (this.closed && error instanceof AbortError)
3526
+ ) {
3527
+ return;
3528
+ }
3529
+ logger.trace(
3530
+ `Replication-info V2 recovery request failed: ${
3531
+ (error as Error)?.message ?? String(error)
3532
+ }`,
3533
+ );
3534
+ },
3535
+ });
3536
+ }
3537
+
3377
3538
  private createReplicatorLivenessMonitor(): ReplicatorLivenessMonitor {
3378
3539
  return new ReplicatorLivenessMonitor({
3379
3540
  // Sweep-driven probes and activity marks dispatch through the owner so
@@ -3404,7 +3565,8 @@ export class SharedLog<
3404
3565
  }),
3405
3566
  );
3406
3567
  },
3407
- isBlockedPeer: (hash) => this._peerSessions.isReplicationInfoBlocked(hash),
3568
+ isBlockedPeer: (hash) =>
3569
+ this._peerSessions.isReplicationInfoBlocked(hash),
3408
3570
  scheduleReplicationInfoRequests: (peer, replicationLifecycleController) =>
3409
3571
  this.scheduleReplicationInfoRequests(
3410
3572
  peer,
@@ -3488,6 +3650,8 @@ export class SharedLog<
3488
3650
  this._appendBackfillPendingByTarget = new Map();
3489
3651
  this._topicSubscribersCache = new Map();
3490
3652
  this._peerSyncCapabilities = new Map();
3653
+ this._peerSyncCapabilitySessions = new Map();
3654
+ this._peerSyncCapabilityTimestamps = new Map();
3491
3655
  this._liveRawGossipBatches = new Map();
3492
3656
  this._liveRawGossipFlushScheduled = false;
3493
3657
  this.coordinateToHash = new Cache<string>({ max: 1e6, ttl: 1e4 });
@@ -3496,6 +3660,8 @@ export class SharedLog<
3496
3660
  this._replicatorJoinEmitted = new Set();
3497
3661
  this._replicatorsReconciled = false;
3498
3662
  this._liveness = this.createReplicatorLivenessMonitor();
3663
+ this._v2Receive = this.createReplicationInfoV2ReceiveCoordinator();
3664
+ this._v2Send = this.createReplicationInfoV2SendCoordinator();
3499
3665
  this._announcements = this.createReplicationAnnouncementCoordinator();
3500
3666
  this.pendingMaturity = new Map();
3501
3667
  this._closeController = new AbortController();
@@ -3991,6 +4157,138 @@ export class SharedLog<
3991
4157
  );
3992
4158
  }
3993
4159
 
4160
+ private observePeerSyncCapabilities(properties: {
4161
+ peerHash: string;
4162
+ capabilities: number;
4163
+ transportSession?: bigint;
4164
+ timestamp?: bigint;
4165
+ openingSession?: PeerSession;
4166
+ }): boolean {
4167
+ const {
4168
+ peerHash,
4169
+ capabilities,
4170
+ transportSession,
4171
+ timestamp,
4172
+ openingSession,
4173
+ } = properties;
4174
+ if (openingSession) {
4175
+ const previous = this._openingSyncCapabilitiesByPeer.get(peerHash);
4176
+ if (
4177
+ transportSession !== undefined &&
4178
+ timestamp !== undefined &&
4179
+ previous?.epoch === openingSession &&
4180
+ previous.timestamp !== undefined &&
4181
+ previous.transportSession !== transportSession &&
4182
+ timestamp <= previous.timestamp
4183
+ ) {
4184
+ return false;
4185
+ }
4186
+ if (
4187
+ transportSession !== undefined &&
4188
+ timestamp !== undefined &&
4189
+ previous?.epoch === openingSession &&
4190
+ previous.transportSession === transportSession
4191
+ ) {
4192
+ if (
4193
+ previous.timestamp !== undefined &&
4194
+ timestamp < previous.timestamp
4195
+ ) {
4196
+ return false;
4197
+ }
4198
+ this._openingSyncCapabilitiesByPeer.set(peerHash, {
4199
+ epoch: openingSession,
4200
+ capabilities: previous.capabilities | capabilities,
4201
+ transportSession,
4202
+ timestamp:
4203
+ previous.timestamp === undefined || timestamp > previous.timestamp
4204
+ ? timestamp
4205
+ : previous.timestamp,
4206
+ });
4207
+ return true;
4208
+ }
4209
+ this._openingSyncCapabilitiesByPeer.set(peerHash, {
4210
+ epoch: openingSession,
4211
+ capabilities,
4212
+ transportSession,
4213
+ timestamp,
4214
+ });
4215
+ return true;
4216
+ }
4217
+
4218
+ if (transportSession === undefined || timestamp === undefined) {
4219
+ // Test/in-process synthetic contexts predate signed envelope captures.
4220
+ // They may exercise capability-number behavior, but can never authorize V2.
4221
+ this._peerSyncCapabilities.set(peerHash, capabilities);
4222
+ this._peerSyncCapabilitySessions.delete(peerHash);
4223
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
4224
+ this._v2Send.advancePeerCapability(peerHash);
4225
+ this._v2Receive.revokePeerCapability(peerHash);
4226
+ return true;
4227
+ }
4228
+
4229
+ const previousSession = this._peerSyncCapabilitySessions.get(peerHash);
4230
+ const previousTimestamp = this._peerSyncCapabilityTimestamps.get(peerHash);
4231
+ const sameTransportSession = previousSession === transportSession;
4232
+ if (
4233
+ previousTimestamp !== undefined &&
4234
+ ((sameTransportSession && timestamp < previousTimestamp) ||
4235
+ (!sameTransportSession && timestamp <= previousTimestamp))
4236
+ ) {
4237
+ return false;
4238
+ }
4239
+ const previousCapabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
4240
+ const nextCapabilities = sameTransportSession
4241
+ ? previousCapabilities | capabilities
4242
+ : capabilities;
4243
+ const senderGrantCapabilityMask =
4244
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
4245
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY;
4246
+ const generationAdvanced =
4247
+ !sameTransportSession ||
4248
+ (previousCapabilities & senderGrantCapabilityMask) !==
4249
+ (nextCapabilities & senderGrantCapabilityMask);
4250
+ this._peerSyncCapabilities.set(peerHash, nextCapabilities);
4251
+ this._peerSyncCapabilitySessions.set(peerHash, transportSession);
4252
+ this._peerSyncCapabilityTimestamps.set(
4253
+ peerHash,
4254
+ previousTimestamp === undefined ||
4255
+ !sameTransportSession ||
4256
+ timestamp > previousTimestamp
4257
+ ? timestamp
4258
+ : previousTimestamp,
4259
+ );
4260
+ if (generationAdvanced) {
4261
+ this._v2Send.advancePeerCapability(peerHash);
4262
+ }
4263
+ return true;
4264
+ }
4265
+
4266
+ private promoteReplicationInfoV2ReceiveCapability(
4267
+ target: PublicSignKey,
4268
+ peerSession: PeerSession,
4269
+ ): boolean {
4270
+ const peerHash = target.hashcode();
4271
+ const senderTransportSession =
4272
+ this._peerSyncCapabilitySessions.get(peerHash);
4273
+ const capabilityTimestamp =
4274
+ this._peerSyncCapabilityTimestamps.get(peerHash);
4275
+ if (
4276
+ senderTransportSession === undefined ||
4277
+ capabilityTimestamp === undefined
4278
+ ) {
4279
+ return false;
4280
+ }
4281
+ return this._v2Receive.observeCapability({
4282
+ peerHash,
4283
+ target,
4284
+ peerSession,
4285
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
4286
+ capabilities: this._peerSyncCapabilities.get(peerHash) ?? 0,
4287
+ senderTransportSession,
4288
+ capabilityTimestamp,
4289
+ });
4290
+ }
4291
+
3994
4292
  /**
3995
4293
  * Live append gossip may use the raw exchange-heads path only when we
3996
4294
  * opted into raw sync and every remote recipient advertised raw capability
@@ -4868,7 +5166,8 @@ export class SharedLog<
4868
5166
 
4869
5167
  private isReceiveOwnershipSnapshotStable(revision: number): boolean {
4870
5168
  return (
4871
- (this._instanceLifecycle?._receiveOwnershipMutationAdmissions ?? 0) === 0 &&
5169
+ (this._instanceLifecycle?._receiveOwnershipMutationAdmissions ?? 0) ===
5170
+ 0 &&
4872
5171
  (this._instanceLifecycle?._receiveOwnershipRevision ?? 0) === revision
4873
5172
  );
4874
5173
  }
@@ -5116,7 +5415,8 @@ export class SharedLog<
5116
5415
  pending: PendingIHave<T>,
5117
5416
  entry: Entry<T>,
5118
5417
  ): void {
5119
- const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
5418
+ const replicationLifecycleController =
5419
+ this._instanceLifecycle?.membershipLifecycleController;
5120
5420
  if (!this.isReplicationLifecycleActive(replicationLifecycleController)) {
5121
5421
  if (this._pendingIHave.get(entry.hash) === pending) {
5122
5422
  pending.clear();
@@ -6024,6 +6324,7 @@ export class SharedLog<
6024
6324
  };
6025
6325
  let removed = false;
6026
6326
  let removalCallCompleted = false;
6327
+ let replicationInfoRecoveryEpochAdvanced = false;
6027
6328
 
6028
6329
  // Replication-info updates already serialize per peer. Put the hash-wide
6029
6330
  // removal on the same queue so a newer reset cannot be deleted underneath
@@ -6113,6 +6414,13 @@ export class SharedLog<
6113
6414
  checkedPruneCoordinator,
6114
6415
  );
6115
6416
  }
6417
+ if (!isMe) {
6418
+ // This is the last synchronous current-generation boundary
6419
+ // before destructive work. Fence parked receives now because
6420
+ // the coherent delete can durably mutate and then fail or poison.
6421
+ this.advanceReplicationInfoRecoveryEpoch(keyHash);
6422
+ replicationInfoRecoveryEpochAdvanced = true;
6423
+ }
6116
6424
  const deletion = await this.deleteReplicationRangesCoherently(
6117
6425
  deleted,
6118
6426
  keyHash,
@@ -6136,7 +6444,6 @@ export class SharedLog<
6136
6444
  }
6137
6445
  return;
6138
6446
  }
6139
-
6140
6447
  if (options?.noEvent !== true && deleted.length > 0) {
6141
6448
  const publicKey = toLocalPublicSignKey(key);
6142
6449
  if (publicKey) {
@@ -6172,11 +6479,6 @@ export class SharedLog<
6172
6479
  await cleanupDisconnectedPeer();
6173
6480
 
6174
6481
  if (!isMe) {
6175
- // Replication-info handlers release their receive lease before joining
6176
- // this lane. Fence every handler queued behind this successful removal,
6177
- // regardless of whether it came from liveness, startup pruning, or an
6178
- // unsubscribe transition.
6179
- this.advanceReplicationInfoRecoveryEpoch(keyHash);
6180
6482
  this.rebalanceParticipationDebounced?.call();
6181
6483
  }
6182
6484
  removed = true;
@@ -6214,6 +6516,24 @@ export class SharedLog<
6214
6516
  removalCallCompleted = true;
6215
6517
  } finally {
6216
6518
  releaseReceiveCleanupGate?.();
6519
+ if (
6520
+ replicationInfoRecoveryEpochAdvanced &&
6521
+ ownsReplicationOwnershipLifecycle() &&
6522
+ ownsReplicationLifecycle() &&
6523
+ ownsSubscriptionEpoch()
6524
+ ) {
6525
+ // The first recovery arm ran while receive admission was fenced.
6526
+ // Re-arm after releasing the cleanup gate so a still-open peer can
6527
+ // deliver the authoritative Full that completes recovery.
6528
+ const peerSession = this._peerSessions.current(keyHash);
6529
+ if (peerSession?.phase === "open") {
6530
+ this._v2Receive.advanceRecovery({
6531
+ peerHash: keyHash,
6532
+ peerSession,
6533
+ receiveEpoch: this._peerSessions.receiveEpoch(keyHash),
6534
+ });
6535
+ }
6536
+ }
6217
6537
  if (isSpeculativePeerRemoval) {
6218
6538
  const cancelledByFreshActivity =
6219
6539
  removalCallCompleted &&
@@ -6289,6 +6609,7 @@ export class SharedLog<
6289
6609
  options?: {
6290
6610
  onRemoved?: (ranges: ReplicationRangeIndexable<R>[]) => void;
6291
6611
  shouldRemove?: () => boolean;
6612
+ onDurableRemoveCommitted?: () => boolean | void;
6292
6613
  },
6293
6614
  replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
6294
6615
  ): Promise<boolean> {
@@ -6307,6 +6628,7 @@ export class SharedLog<
6307
6628
  options?: {
6308
6629
  onRemoved?: (ranges: ReplicationRangeIndexable<R>[]) => void;
6309
6630
  shouldRemove?: () => boolean;
6631
+ onDurableRemoveCommitted?: () => boolean | void;
6310
6632
  },
6311
6633
  ): Promise<boolean> {
6312
6634
  if (ranges.length === 0) {
@@ -6363,6 +6685,13 @@ export class SharedLog<
6363
6685
  ownerHash,
6364
6686
  );
6365
6687
  ranges = deletion.removed;
6688
+ if (
6689
+ (options?.shouldRemove && !options.shouldRemove()) ||
6690
+ options?.onDurableRemoveCommitted?.() === false
6691
+ ) {
6692
+ await deletion.rollback();
6693
+ return false;
6694
+ }
6366
6695
  options?.onRemoved?.(ranges);
6367
6696
 
6368
6697
  if (ranges.length > 0) {
@@ -6448,6 +6777,7 @@ export class SharedLog<
6448
6777
  timestamp?: number;
6449
6778
  allowLegacyOrderedReplacementPairs?: boolean;
6450
6779
  onConfirmedDurableStateChanged?: () => void;
6780
+ onDurableApplyCommitted?: () => boolean | void;
6451
6781
  shouldApply?: () => boolean;
6452
6782
  } = {},
6453
6783
  replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
@@ -6489,6 +6819,7 @@ export class SharedLog<
6489
6819
  rebalance,
6490
6820
  allowLegacyOrderedReplacementPairs,
6491
6821
  onConfirmedDurableStateChanged,
6822
+ onDurableApplyCommitted,
6492
6823
  shouldApply,
6493
6824
  }: {
6494
6825
  reset?: boolean;
@@ -6497,6 +6828,7 @@ export class SharedLog<
6497
6828
  timestamp?: number;
6498
6829
  allowLegacyOrderedReplacementPairs?: boolean;
6499
6830
  onConfirmedDurableStateChanged?: () => void;
6831
+ onDurableApplyCommitted?: () => boolean | void;
6500
6832
  shouldApply?: () => boolean;
6501
6833
  } = {},
6502
6834
  replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
@@ -6511,6 +6843,12 @@ export class SharedLog<
6511
6843
  if (shouldApply && !shouldApply()) {
6512
6844
  return [];
6513
6845
  }
6846
+ const applySuperseded = Symbol("replication-range-apply-superseded");
6847
+ const throwIfApplySuperseded = () => {
6848
+ if (shouldApply && !shouldApply()) {
6849
+ throw applySuperseded;
6850
+ }
6851
+ };
6514
6852
  const fromHash = from.hashcode();
6515
6853
  const incomingRangesById = new Map<
6516
6854
  string,
@@ -6600,6 +6938,7 @@ export class SharedLog<
6600
6938
  let isStoppedReplicating = false;
6601
6939
  let wasReplicatorBeforeDestructiveReset = false;
6602
6940
  let resetFailureLeaveEmitted = false;
6941
+ let resetDeletionRollback: (() => Promise<void>) | undefined;
6603
6942
  const publishConfirmedResetStop = (ownerHasRanges: boolean) => {
6604
6943
  if (ownerHasRanges || resetFailureLeaveEmitted) {
6605
6944
  return;
@@ -6652,12 +6991,18 @@ export class SharedLog<
6652
6991
  } else {
6653
6992
  wasReplicatorBeforeDestructiveReset =
6654
6993
  this.uniqueReplicators.has(fromHash);
6994
+ throwIfApplySuperseded();
6655
6995
  const deletion = await this.deleteReplicationRangesCoherently(
6656
6996
  deleted,
6657
6997
  fromHash,
6658
6998
  { preserveOwnerMembership: ranges.length > 0 },
6659
6999
  );
6660
7000
  deleted = deletion.removed;
7001
+ resetDeletionRollback = deletion.rollback;
7002
+ if (shouldApply && !shouldApply()) {
7003
+ await deletion.rollback();
7004
+ return [];
7005
+ }
6661
7006
 
6662
7007
  diffs = [
6663
7008
  ...deleted.map((x) => {
@@ -6853,18 +7198,26 @@ export class SharedLog<
6853
7198
  };
6854
7199
 
6855
7200
  try {
7201
+ throwIfApplySuperseded();
6856
7202
  for (const diff of diffs) {
6857
7203
  if (diff.type !== "added") {
6858
7204
  continue;
6859
7205
  }
7206
+ throwIfApplySuperseded();
6860
7207
  appliedPositiveRanges.push(diff);
6861
7208
  await this.replicationIndex.put(diff.range);
6862
7209
  this.putNativeReplicationRange(diff.range);
7210
+ throwIfApplySuperseded();
6863
7211
  }
6864
7212
  if (reset && diffs.length > 0) {
6865
7213
  await this.updateOldestTimestampFromIndex();
7214
+ throwIfApplySuperseded();
7215
+ }
7216
+ if (onDurableApplyCommitted?.() === false) {
7217
+ throw applySuperseded;
6866
7218
  }
6867
7219
  } catch (error) {
7220
+ const superseded = error === applySuperseded;
6868
7221
  let outcomeError = error;
6869
7222
  if (appliedPositiveRanges.length > 0) {
6870
7223
  try {
@@ -6873,6 +7226,19 @@ export class SharedLog<
6873
7226
  outcomeError = poisonFromPositiveRollback(rollbackError, error);
6874
7227
  }
6875
7228
  }
7229
+ if (superseded) {
7230
+ if (resetDeletionRollback) {
7231
+ try {
7232
+ await resetDeletionRollback();
7233
+ } catch (rollbackError) {
7234
+ outcomeError = rollbackError;
7235
+ }
7236
+ }
7237
+ if (outcomeError === applySuperseded) {
7238
+ return [];
7239
+ }
7240
+ throw outcomeError;
7241
+ }
6876
7242
  if (reset) {
6877
7243
  const negativeDiffs = diffs.filter((diff) => diff.type !== "added");
6878
7244
  if (negativeDiffs.length > 0) {
@@ -8479,7 +8845,8 @@ export class SharedLog<
8479
8845
  }
8480
8846
  };
8481
8847
 
8482
- const residentEntriesByHash = this._coordinates._residentEntryCoordinatesByHash;
8848
+ const residentEntriesByHash =
8849
+ this._coordinates._residentEntryCoordinatesByHash;
8483
8850
  if (
8484
8851
  (this._nativeBackbone ?? this._nativeSharedLogState) &&
8485
8852
  residentEntriesByHash &&
@@ -8791,7 +9158,8 @@ export class SharedLog<
8791
9158
  const localLeaderHashes = new Set<string>();
8792
9159
  const unresolvedHashes = new Set(hashes);
8793
9160
  const nativePlanner = this._nativeBackbone ?? this._nativeRangePlanner;
8794
- const nativeEntryMetadata = this._coordinates.getNativeLogEntryMetadataBatch(hashes);
9161
+ const nativeEntryMetadata =
9162
+ this._coordinates.getNativeLogEntryMetadataBatch(hashes);
8795
9163
 
8796
9164
  if (nativePlanner && !this.hasCustomFindLeaders() && nativeEntryMetadata) {
8797
9165
  const nativeItems: Array<{
@@ -10292,7 +10660,8 @@ export class SharedLog<
10292
10660
  ) {
10293
10661
  try {
10294
10662
  this.restoreNativeBackboneDocument(nativeDocumentRollback);
10295
- const flushed = this._coordinates.flushNativeBackboneCoordinateJournal();
10663
+ const flushed =
10664
+ this._coordinates.flushNativeBackboneCoordinateJournal();
10296
10665
  if (isPromiseLike(flushed)) {
10297
10666
  await flushed;
10298
10667
  }
@@ -10382,11 +10751,12 @@ export class SharedLog<
10382
10751
  prepared.trimmedEntries as Array<{ hash?: string }> | undefined
10383
10752
  )?.flatMap((entry) => (entry.hash ? [entry.hash] : [])) ??
10384
10753
  [];
10385
- const coordinateRollback = this._coordinates.snapshotResidentCoordinateEntries([
10386
- ...(preparedHash ? [preparedHash] : []),
10387
- ...preparedNext,
10388
- ...nativeTrimmedHashes,
10389
- ]);
10754
+ const coordinateRollback =
10755
+ this._coordinates.snapshotResidentCoordinateEntries([
10756
+ ...(preparedHash ? [preparedHash] : []),
10757
+ ...preparedNext,
10758
+ ...nativeTrimmedHashes,
10759
+ ]);
10390
10760
  lowerPublicationRollback = {
10391
10761
  committedHashes: preparedHash ? [preparedHash] : [],
10392
10762
  trimmedEntries: prepared.trimmedEntries,
@@ -10602,7 +10972,8 @@ export class SharedLog<
10602
10972
  for (const document of lowerPublicationRollback?.documents ?? []) {
10603
10973
  this.restoreNativeBackboneDocument(document);
10604
10974
  }
10605
- const flushed = this._coordinates.flushNativeBackboneCoordinateJournal();
10975
+ const flushed =
10976
+ this._coordinates.flushNativeBackboneCoordinateJournal();
10606
10977
  if (isPromiseLike(flushed)) {
10607
10978
  await flushed;
10608
10979
  }
@@ -10697,7 +11068,10 @@ export class SharedLog<
10697
11068
  ownershipLifecycleController,
10698
11069
  );
10699
11070
  const backbone = this._nativeBackbone;
10700
- if (!backbone || !this._coordinates.canUseNativeBackboneResidentCoordinateState()) {
11071
+ if (
11072
+ !backbone ||
11073
+ !this._coordinates.canUseNativeBackboneResidentCoordinateState()
11074
+ ) {
10701
11075
  return undefined;
10702
11076
  }
10703
11077
  if (
@@ -10902,11 +11276,12 @@ export class SharedLog<
10902
11276
  const committedHash =
10903
11277
  backboneAppend.entry.cid ?? backboneAppend.entry.hash;
10904
11278
  const committedNext = backboneAppend.entry.next ?? next;
10905
- nativeCoordinateRollback = this._coordinates.snapshotResidentCoordinateEntries([
10906
- ...(committedHash ? [committedHash] : []),
10907
- ...committedNext,
10908
- ...nativeTrimmedHashes,
10909
- ]);
11279
+ nativeCoordinateRollback =
11280
+ this._coordinates.snapshotResidentCoordinateEntries([
11281
+ ...(committedHash ? [committedHash] : []),
11282
+ ...committedNext,
11283
+ ...nativeTrimmedHashes,
11284
+ ]);
10910
11285
  await this.setNativeStrictDurableTransactionOperation(
10911
11286
  nativeStrictTransaction,
10912
11287
  committedHash ? [committedHash] : [],
@@ -11217,7 +11592,8 @@ export class SharedLog<
11217
11592
  if (nativeDocumentRollback) {
11218
11593
  try {
11219
11594
  this.restoreNativeBackboneDocument(nativeDocumentRollback);
11220
- const flushed = this._coordinates.flushNativeBackboneCoordinateJournal();
11595
+ const flushed =
11596
+ this._coordinates.flushNativeBackboneCoordinateJournal();
11221
11597
  if (isPromiseLike(flushed)) {
11222
11598
  await flushed;
11223
11599
  }
@@ -11249,15 +11625,17 @@ export class SharedLog<
11249
11625
  coordinateIndex.putSharedLogCoordinateFieldsEncodedAndDeleteHashesNoReturn ||
11250
11626
  coordinateIndex.putSharedLogCoordinateFieldsAndDeleteHashesNoReturn;
11251
11627
  const persisted = hasNativeCoordinatePut
11252
- ? this._coordinates.persistBackboneCoordinateFieldsNativeTransaction({
11253
- coordinateIndex,
11254
- fields: coordinateFields,
11255
- hash: prepared.appendFacts.hash,
11256
- deleteHashes: [],
11257
- coordinates: backboneAppend.coordinate
11258
- .coordinates as NumberFromType<R>[],
11259
- skipGenericTransientCoordinateIndex: runtimeOnlyCoordinates,
11260
- })
11628
+ ? this._coordinates.persistBackboneCoordinateFieldsNativeTransaction(
11629
+ {
11630
+ coordinateIndex,
11631
+ fields: coordinateFields,
11632
+ hash: prepared.appendFacts.hash,
11633
+ deleteHashes: [],
11634
+ coordinates: backboneAppend.coordinate
11635
+ .coordinates as NumberFromType<R>[],
11636
+ skipGenericTransientCoordinateIndex: runtimeOnlyCoordinates,
11637
+ },
11638
+ )
11261
11639
  : this._coordinates.persistPreparedCoordinate({
11262
11640
  prepared: getPreparedCoordinate(),
11263
11641
  hash: prepared.appendFacts.hash,
@@ -11311,9 +11689,10 @@ export class SharedLog<
11311
11689
  if (!backboneAppend.isLeader && !delayAdaptiveRebalance) {
11312
11690
  const leaders = backboneAppend.leaders;
11313
11691
  if (leaders) {
11314
- const pruneEntry = this._coordinates.materializePreparedCoordinateEntry(
11315
- getPreparedCoordinate(),
11316
- );
11692
+ const pruneEntry =
11693
+ this._coordinates.materializePreparedCoordinateEntry(
11694
+ getPreparedCoordinate(),
11695
+ );
11317
11696
  this.pruneDebouncedFnAddIfNotKeeping({
11318
11697
  key: pruneEntry.hash,
11319
11698
  value: { entry: pruneEntry, leaders },
@@ -11984,16 +12363,17 @@ export class SharedLog<
11984
12363
  ),
11985
12364
  );
11986
12365
  const nativeTrimmedHashes = [...nativeTrimmedHashSet];
11987
- batchCoordinateRollback = this._coordinates.snapshotResidentCoordinateEntries(
11988
- committedAppends.flatMap((append) => [
11989
- ...((append.entry.cid ?? append.entry.hash)
11990
- ? [append.entry.cid ?? append.entry.hash!]
11991
- : []),
11992
- ...append.entry.next,
11993
- ...(append.trimmedHashes ??
11994
- append.trimmed.map((entry) => entry.hash)),
11995
- ]),
11996
- );
12366
+ batchCoordinateRollback =
12367
+ this._coordinates.snapshotResidentCoordinateEntries(
12368
+ committedAppends.flatMap((append) => [
12369
+ ...((append.entry.cid ?? append.entry.hash)
12370
+ ? [append.entry.cid ?? append.entry.hash!]
12371
+ : []),
12372
+ ...append.entry.next,
12373
+ ...(append.trimmedHashes ??
12374
+ append.trimmed.map((entry) => entry.hash)),
12375
+ ]),
12376
+ );
11997
12377
  await this.setNativeStrictDurableTransactionOperation(
11998
12378
  nativeStrictTransaction,
11999
12379
  committedCids,
@@ -12245,7 +12625,8 @@ export class SharedLog<
12245
12625
  for (const document of batchDocumentRollbacks) {
12246
12626
  this.restoreNativeBackboneDocument(document);
12247
12627
  }
12248
- const flushed = this._coordinates.flushNativeBackboneCoordinateJournal();
12628
+ const flushed =
12629
+ this._coordinates.flushNativeBackboneCoordinateJournal();
12249
12630
  if (isPromiseLike(flushed)) {
12250
12631
  await flushed;
12251
12632
  }
@@ -12325,20 +12706,21 @@ export class SharedLog<
12325
12706
  coordinateFields,
12326
12707
  plannedCoordinateDeleteHashes,
12327
12708
  });
12328
- const persisted = this._coordinates.persistBackboneCoordinateFieldsNativeTransaction(
12329
- {
12330
- coordinateIndex: this.entryCoordinatesIndex as PutAndDeleteIndex<
12331
- EntryReplicated<R>
12332
- >,
12333
- fields: coordinateFields,
12334
- hash: facts.hash,
12335
- deleteHashes: [],
12336
- coordinates: backboneAppend.coordinate
12337
- .coordinates as NumberFromType<R>[],
12338
- skipGenericTransientCoordinateIndex: runtimeOnlyCoordinates,
12339
- },
12340
- ownershipLifecycleController,
12341
- );
12709
+ const persisted =
12710
+ this._coordinates.persistBackboneCoordinateFieldsNativeTransaction(
12711
+ {
12712
+ coordinateIndex: this.entryCoordinatesIndex as PutAndDeleteIndex<
12713
+ EntryReplicated<R>
12714
+ >,
12715
+ fields: coordinateFields,
12716
+ hash: facts.hash,
12717
+ deleteHashes: [],
12718
+ coordinates: backboneAppend.coordinate
12719
+ .coordinates as NumberFromType<R>[],
12720
+ skipGenericTransientCoordinateIndex: runtimeOnlyCoordinates,
12721
+ },
12722
+ ownershipLifecycleController,
12723
+ );
12342
12724
  if (isPromiseLike(persisted)) {
12343
12725
  await persisted;
12344
12726
  this.throwIfReplicationOwnershipLifecycleInactive(
@@ -13831,6 +14213,8 @@ export class SharedLog<
13831
14213
  ttl: LEADER_PLAN_CACHE_TTL_MS,
13832
14214
  });
13833
14215
  this._peerSyncCapabilities = new Map();
14216
+ this._peerSyncCapabilitySessions = new Map();
14217
+ this._peerSyncCapabilityTimestamps = new Map();
13834
14218
  this._liveRawGossipBatches = new Map();
13835
14219
  this._liveRawGossipFlushScheduled = false;
13836
14220
  this.coordinateToHash = new Cache<string>({ max: 1e6, ttl: 1e4 });
@@ -13847,6 +14231,10 @@ export class SharedLog<
13847
14231
  this._lastLocalAppendAt = 0;
13848
14232
  this._announcements ??= this.createReplicationAnnouncementCoordinator();
13849
14233
  this._announcements.resetForOpen();
14234
+ this._v2Receive ??= this.createReplicationInfoV2ReceiveCoordinator();
14235
+ this._v2Receive.resetForOpen();
14236
+ this._v2Send ??= this.createReplicationInfoV2SendCoordinator();
14237
+ this._v2Send.resetForOpen();
13850
14238
  const adaptiveReplicateOptions =
13851
14239
  options?.replicate && isAdaptiveReplicatorOption(options.replicate)
13852
14240
  ? options.replicate
@@ -14588,8 +14976,11 @@ export class SharedLog<
14588
14976
  // tombstone was written. Complete that erase before the adapter can expose
14589
14977
  // any stale coordinate or document state to this backbone.
14590
14978
  await this._coordinates._nativeBackboneCoordinatePersistence.resumeDrop?.();
14591
- await this._coordinates._nativeBackboneCoordinatePersistence.hydrate(backbone);
14592
- this._coordinates._nativeBackboneCoordinateJournalLastFlushMs = Date.now();
14979
+ await this._coordinates._nativeBackboneCoordinatePersistence.hydrate(
14980
+ backbone,
14981
+ );
14982
+ this._coordinates._nativeBackboneCoordinateJournalLastFlushMs =
14983
+ Date.now();
14593
14984
  this.hydrateNativeCoordinateStateFromBackbone(backbone);
14594
14985
  return;
14595
14986
  }
@@ -14629,7 +15020,9 @@ export class SharedLog<
14629
15020
  if (!this._nativeBackbone) {
14630
15021
  return;
14631
15022
  }
14632
- const hashes = new Set(this._coordinates._residentEntryCoordinatesByHash?.keys() ?? []);
15023
+ const hashes = new Set(
15024
+ this._coordinates._residentEntryCoordinatesByHash?.keys() ?? [],
15025
+ );
14633
15026
  const iterator = this.entryCoordinatesIndex.iterate({});
14634
15027
  try {
14635
15028
  for (;;) {
@@ -14699,7 +15092,10 @@ export class SharedLog<
14699
15092
  coordinate.requestedReplicas,
14700
15093
  sharedFields.hashNumber,
14701
15094
  );
14702
- this._coordinates._residentEntryCoordinatesByHash.set(sharedFields.hash, sharedFields);
15095
+ this._coordinates._residentEntryCoordinatesByHash.set(
15096
+ sharedFields.hash,
15097
+ sharedFields,
15098
+ );
14703
15099
  for (const value of sharedFields.coordinates) {
14704
15100
  this.coordinateToHash.add(value, sharedFields.hash);
14705
15101
  }
@@ -14858,7 +15254,8 @@ export class SharedLog<
14858
15254
  this._coordinates._nativeBackboneCoordinatePersistence
14859
15255
  ) {
14860
15256
  if (
14861
- this._coordinates._nativeBackboneCoordinatePersistence.durableBarrier !== true ||
15257
+ this._coordinates._nativeBackboneCoordinatePersistence
15258
+ .durableBarrier !== true ||
14862
15259
  typeof this._nativeBackboneCoordinatePersistenceStore
14863
15260
  ?.durableBarrier !== "function"
14864
15261
  ) {
@@ -14869,11 +15266,12 @@ export class SharedLog<
14869
15266
  }
14870
15267
  if (
14871
15268
  this._coordinates._nativeBackboneCoordinatePersistence &&
14872
- (this._coordinates._nativeBackboneCoordinatePersistence.compactMaxJournalBytes !=
14873
- null ||
14874
- this._coordinates._nativeBackboneCoordinatePersistence.compactMaxJournalRecords !=
14875
- null) &&
14876
- this._coordinates._nativeBackboneCoordinatePersistence.crashSafeCompaction !== true
15269
+ (this._coordinates._nativeBackboneCoordinatePersistence
15270
+ .compactMaxJournalBytes != null ||
15271
+ this._coordinates._nativeBackboneCoordinatePersistence
15272
+ .compactMaxJournalRecords != null) &&
15273
+ this._coordinates._nativeBackboneCoordinatePersistence
15274
+ .crashSafeCompaction !== true
14877
15275
  ) {
14878
15276
  // Durable custom adapters must explicitly advertise an atomic generation
14879
15277
  // protocol before SharedLog permits automatic WAL compaction.
@@ -15099,7 +15497,8 @@ export class SharedLog<
15099
15497
  async afterOpen(): Promise<void> {
15100
15498
  await super.afterOpen();
15101
15499
  const existingSubscribersPromise = this._getTopicSubscribers(this.topic);
15102
- const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
15500
+ const replicationLifecycleController =
15501
+ this._instanceLifecycle?.membershipLifecycleController;
15103
15502
 
15104
15503
  // We do this here, because these calls requires this.closed == false
15105
15504
  void this.pruneOfflineReplicators()
@@ -15140,7 +15539,8 @@ export class SharedLog<
15140
15539
  async pruneOfflineReplicators() {
15141
15540
  // Go through all segments and wait for replicators to become reachable;
15142
15541
  // otherwise prune them away from the local membership view.
15143
- const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
15542
+ const replicationLifecycleController =
15543
+ this._instanceLifecycle?.membershipLifecycleController;
15144
15544
  try {
15145
15545
  if (
15146
15546
  !replicationLifecycleController ||
@@ -15335,10 +15735,19 @@ export class SharedLog<
15335
15735
  peerHash: string,
15336
15736
  ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
15337
15737
  ) {
15738
+ const peerSession = this._peerSessions.current(peerHash);
15739
+ const preserveV2Session =
15740
+ peerSession?.phase === "open" && peerSession.isActive();
15338
15741
  this.cancelReplicationInfoRequests(peerHash);
15339
15742
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15340
15743
  this._liveness._replicatorLastActivityAt.delete(peerHash);
15341
- this._peerSyncCapabilities.delete(peerHash);
15744
+ if (!preserveV2Session) {
15745
+ this._peerSyncCapabilities.delete(peerHash);
15746
+ this._peerSyncCapabilitySessions.delete(peerHash);
15747
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
15748
+ this._v2Receive.clearPeer(peerHash);
15749
+ this._v2Send.clearPeer(peerHash);
15750
+ }
15342
15751
  this.cleanupPendingIHavePeer(peerHash);
15343
15752
  this.cleanupCheckedPrunePeer(
15344
15753
  peerHash,
@@ -15362,8 +15771,16 @@ export class SharedLog<
15362
15771
  // when they eventually reach the apply lane. Reset the sender's
15363
15772
  // ordering watermark with the local epoch so a later arrival can be
15364
15773
  // accepted without comparing its clock to this receiver's clock.
15365
- this._peerSessions.advanceReceiveEpoch(peerHash);
15774
+ const receiveEpoch = this._peerSessions.advanceReceiveEpoch(peerHash);
15366
15775
  this.latestReplicationInfoMessage.delete(peerHash);
15776
+ const peerSession = this._peerSessions.current(peerHash);
15777
+ if (peerSession?.phase === "open") {
15778
+ this._v2Receive.advanceRecovery({
15779
+ peerHash,
15780
+ peerSession,
15781
+ receiveEpoch,
15782
+ });
15783
+ }
15367
15784
  }
15368
15785
 
15369
15786
  private async resolveCandidatePeersForHash(
@@ -15644,7 +16061,9 @@ export class SharedLog<
15644
16061
  deferredCoordinateDeleteHashes,
15645
16062
  );
15646
16063
  } else {
15647
- this._coordinates.forgetCoordinateStateForHashes(deferredCoordinateDeleteHashes);
16064
+ this._coordinates.forgetCoordinateStateForHashes(
16065
+ deferredCoordinateDeleteHashes,
16066
+ );
15648
16067
  }
15649
16068
  return deferredCoordinateDeleteHashes;
15650
16069
  }
@@ -15692,7 +16111,9 @@ export class SharedLog<
15692
16111
  deferredCoordinateDeleteHashes,
15693
16112
  );
15694
16113
  } else {
15695
- this._coordinates.forgetCoordinateStateForHashes(deferredCoordinateDeleteHashes);
16114
+ this._coordinates.forgetCoordinateStateForHashes(
16115
+ deferredCoordinateDeleteHashes,
16116
+ );
15696
16117
  }
15697
16118
  return deferredCoordinateDeleteHashes;
15698
16119
  }
@@ -16255,7 +16676,9 @@ export class SharedLog<
16255
16676
  this._wireSyncSession = undefined;
16256
16677
  }
16257
16678
  });
16258
- await capture(() => this._coordinates.closeNativeBackboneCoordinatePersistence());
16679
+ await capture(() =>
16680
+ this._coordinates.closeNativeBackboneCoordinatePersistence(),
16681
+ );
16259
16682
  await capture(() => this.syncronizer?.close());
16260
16683
 
16261
16684
  captureSync(() => {
@@ -16348,6 +16771,10 @@ export class SharedLog<
16348
16771
  this._openingSyncCapabilitiesByPeer?.clear();
16349
16772
  this._gidPeersHistory?.clear();
16350
16773
  this._peerSyncCapabilities?.clear();
16774
+ this._peerSyncCapabilitySessions?.clear();
16775
+ this._peerSyncCapabilityTimestamps?.clear();
16776
+ this._v2Receive?.clearForClose();
16777
+ this._v2Send?.clearForClose();
16351
16778
  this._liveRawGossipBatches?.clear();
16352
16779
  this._nativeSharedLogState?.clearGidPeers();
16353
16780
  this._nativeBackbone?.clearGidPeers();
@@ -16420,6 +16847,7 @@ export class SharedLog<
16420
16847
  this.preventParentAttachments();
16421
16848
  this.stopRepairLifecycle();
16422
16849
  this._instanceLifecycle?.beginTerminal("close");
16850
+ this._v2Receive?.clearForClose();
16423
16851
  const replicationRangeTerminalFence =
16424
16852
  this.acquireReplicationRangeMutationTerminalFence();
16425
16853
  const pruneRemoveTerminalFence = this.acquirePruneRemoveTerminalFence();
@@ -16480,12 +16908,16 @@ export class SharedLog<
16480
16908
  }
16481
16909
  }, 2_000);
16482
16910
  try {
16483
- await this.rpc
16484
- .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
16485
- priority: CONVERGENCE_MESSAGE_PRIORITY,
16486
- signal: abort.signal,
16487
- })
16488
- .catch(() => {});
16911
+ const reset = new AllReplicatingSegmentsMessage({ segments: [] });
16912
+ await Promise.all([
16913
+ this.rpc
16914
+ .send(reset, {
16915
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
16916
+ signal: abort.signal,
16917
+ })
16918
+ .catch(() => {}),
16919
+ this._v2Send.sendTerminalReset(abort.signal),
16920
+ ]);
16489
16921
  } finally {
16490
16922
  clearTimeout(abortTimer);
16491
16923
  }
@@ -16543,7 +16975,8 @@ export class SharedLog<
16543
16975
  }
16544
16976
  this.throwIfCheckedPruneRemoveBlocksLocalOperation("drop");
16545
16977
  this.ensureNativeDurabilityRuntimeState();
16546
- const nativePersistence = this._coordinates._nativeBackboneCoordinatePersistence;
16978
+ const nativePersistence =
16979
+ this._coordinates._nativeBackboneCoordinatePersistence;
16547
16980
  if (
16548
16981
  nativePersistence &&
16549
16982
  (typeof nativePersistence.drop !== "function" ||
@@ -16561,6 +16994,7 @@ export class SharedLog<
16561
16994
  this.preventParentAttachments();
16562
16995
  this.stopRepairLifecycle();
16563
16996
  this._instanceLifecycle?.beginTerminal("drop");
16997
+ this._v2Receive?.clearForClose();
16564
16998
  const replicationRangeTerminalFence =
16565
16999
  this.acquireReplicationRangeMutationTerminalFence();
16566
17000
  const pruneRemoveTerminalFence = this.acquirePruneRemoveTerminalFence();
@@ -16604,12 +17038,16 @@ export class SharedLog<
16604
17038
  }
16605
17039
  }, 2_000);
16606
17040
  try {
16607
- await this.rpc
16608
- .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
16609
- priority: CONVERGENCE_MESSAGE_PRIORITY,
16610
- signal: abort.signal,
16611
- })
16612
- .catch(() => {});
17041
+ const reset = new AllReplicatingSegmentsMessage({ segments: [] });
17042
+ await Promise.all([
17043
+ this.rpc
17044
+ .send(reset, {
17045
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
17046
+ signal: abort.signal,
17047
+ })
17048
+ .catch(() => {}),
17049
+ this._v2Send.sendTerminalReset(abort.signal),
17050
+ ]);
16613
17051
  } finally {
16614
17052
  clearTimeout(abortTimer);
16615
17053
  }
@@ -17126,13 +17564,6 @@ export class SharedLog<
17126
17564
  msg: TransportMessage,
17127
17565
  context: RequestContext,
17128
17566
  ): Promise<void> {
17129
- // Decode-first only: capability advertisement is not permission to use
17130
- // V2. Drop every unsolicited V2 envelope before even entering the shared
17131
- // receive envelope, so durable poison checks, leases, liveness, ordering
17132
- // watermarks, replication ranges and cleanup side effects cannot observe it.
17133
- if (isReplicationInfoV2Message(msg)) {
17134
- return;
17135
- }
17136
17567
  const stashBackedRawMessage = isStashBackedRawExchangeHeadsMessage(msg)
17137
17568
  ? msg
17138
17569
  : undefined;
@@ -17185,17 +17616,29 @@ export class SharedLog<
17185
17616
  }
17186
17617
  if (
17187
17618
  msg instanceof AllReplicatingSegmentsMessage ||
17188
- msg instanceof AddedReplicationSegmentMessage
17619
+ msg instanceof AddedReplicationSegmentMessage ||
17620
+ msg instanceof FullReplicationInfoV2Message ||
17621
+ msg instanceof AddedReplicationInfoV2Message
17189
17622
  ) {
17190
17623
  // Bound decoded untrusted vectors before per-peer/global mutation
17191
17624
  // queues, trusted-replicator authorization, or liveness side effects.
17192
17625
  this.validateReplicationRangeAnnouncement(msg.segments);
17193
- } else if (msg instanceof StoppedReplicating) {
17626
+ } else if (
17627
+ msg instanceof StoppedReplicating ||
17628
+ msg instanceof StoppedReplicationInfoV2Message
17629
+ ) {
17194
17630
  // Bound the raw decoded vector before deduplication can hide the
17195
17631
  // allocation cost, and before liveness or apply-queue side effects.
17196
17632
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
17197
17633
  }
17198
- if (!context.from.equals(this.node.identity.publicKey)) {
17634
+ if (
17635
+ !context.from.equals(this.node.identity.publicKey) &&
17636
+ !(msg instanceof RequestReplicationInfoV2Message) &&
17637
+ !isReplicationInfoV2Message(msg) &&
17638
+ !(msg instanceof AllReplicatingSegmentsMessage) &&
17639
+ !(msg instanceof AddedReplicationSegmentMessage) &&
17640
+ !(msg instanceof StoppedReplicating)
17641
+ ) {
17199
17642
  this._liveness.markReplicatorActivity(receiveFromHash);
17200
17643
  }
17201
17644
 
@@ -18240,11 +18683,14 @@ export class SharedLog<
18240
18683
  );
18241
18684
  }
18242
18685
  const reusableCoordinatePlans =
18243
- this._coordinates.createReusableReceiveCoordinatePlans(receiveGroups, {
18244
- decodedReplicaCounts: receiveReplicaCounts,
18245
- allowRoleAgeZeroPlans:
18246
- immediateReplicatingLeaderPlans !== undefined,
18247
- }) as Map<string, ReusableReceiveCoordinatePlan<R>>;
18686
+ this._coordinates.createReusableReceiveCoordinatePlans(
18687
+ receiveGroups,
18688
+ {
18689
+ decodedReplicaCounts: receiveReplicaCounts,
18690
+ allowRoleAgeZeroPlans:
18691
+ immediateReplicatingLeaderPlans !== undefined,
18692
+ },
18693
+ ) as Map<string, ReusableReceiveCoordinatePlan<R>>;
18248
18694
  if (syncProfile) {
18249
18695
  emitSyncProfileDuration(syncProfile, joinPlanStartedAt, {
18250
18696
  name: "sharedLog.receive.joinPlan",
@@ -18639,9 +19085,7 @@ export class SharedLog<
18639
19085
  }
18640
19086
  const checkedPruneStartedAt = syncProfileStart(syncProfile);
18641
19087
  const ownershipChangedDuringReceive =
18642
- !this.isReceiveOwnershipSnapshotStable(
18643
- receiveOwnershipRevision,
18644
- );
19088
+ !this.isReceiveOwnershipSnapshotStable(receiveOwnershipRevision);
18645
19089
  if (ownershipChangedDuringReceive) {
18646
19090
  const freshAuditRevision =
18647
19091
  this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
@@ -18799,6 +19243,7 @@ export class SharedLog<
18799
19243
  fromHash: receiveFromHash,
18800
19244
  session: receiveSession,
18801
19245
  lifecycleController: receiveReplicationLifecycleController,
19246
+ ownershipLifecycleController: receiveOwnershipLifecycleController,
18802
19247
  receiveEpoch: receiveReplicationInfoReceiveEpoch,
18803
19248
  syncProfile,
18804
19249
  lease: peerReceiveLease,
@@ -18822,8 +19267,16 @@ export class SharedLog<
18822
19267
  this.markEntriesKnownByPeer(msg.hashes, context.from.hashcode());
18823
19268
  this.clearRepairFrontierHashes(context.from.hashcode(), msg.hashes);
18824
19269
  return;
19270
+ } else if (isReplicationInfoV2Message(msg)) {
19271
+ await this.handleReplicationInfoV2Announcement(
19272
+ msg,
19273
+ laneRequestContext,
19274
+ lane,
19275
+ );
18825
19276
  } else if (msg instanceof SyncCapabilitiesMessage) {
18826
19277
  if (!context.from.equals(this.node.identity.publicKey)) {
19278
+ const capabilityTransportSession = context.message?.header?.session;
19279
+ const capabilityTimestamp = context.message?.header?.timestamp;
18827
19280
  // No await separates this from the capture above, so the captured
18828
19281
  // window state is exact: the legacy re-read of the opening map here
18829
19282
  // could never observe a different value.
@@ -18834,15 +19287,55 @@ export class SharedLog<
18834
19287
  // A prior unsubscribe cleanup may still be ahead of this reconnect
18835
19288
  // barrier. Stage the new generation's one-shot advertisement so that
18836
19289
  // cleanup cannot erase it before the opening transition commits.
18837
- this._openingSyncCapabilitiesByPeer.set(receiveFromHash, {
18838
- epoch: receiveSession!,
19290
+ this.observePeerSyncCapabilities({
19291
+ peerHash: receiveFromHash,
18839
19292
  capabilities: msg.capabilities,
19293
+ transportSession: capabilityTransportSession,
19294
+ timestamp: capabilityTimestamp,
19295
+ openingSession: receiveSession!,
18840
19296
  });
18841
- } else {
18842
- this._peerSyncCapabilities.set(receiveFromHash, msg.capabilities);
19297
+ } else if (
19298
+ this.observePeerSyncCapabilities({
19299
+ peerHash: receiveFromHash,
19300
+ capabilities: msg.capabilities,
19301
+ transportSession: capabilityTransportSession,
19302
+ timestamp: capabilityTimestamp,
19303
+ }) &&
19304
+ receiveSession?.phase === "open"
19305
+ ) {
19306
+ this.promoteReplicationInfoV2ReceiveCapability(
19307
+ context.from,
19308
+ receiveSession,
19309
+ );
18843
19310
  }
18844
19311
  }
18845
19312
  return;
19313
+ } else if (msg instanceof RequestReplicationInfoV2Message) {
19314
+ const requiredCapabilities =
19315
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
19316
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY;
19317
+ if (
19318
+ receiveSession &&
19319
+ ((this._peerSyncCapabilities.get(receiveFromHash) ?? 0) &
19320
+ requiredCapabilities) ===
19321
+ requiredCapabilities &&
19322
+ this._peerSyncCapabilitySessions.get(receiveFromHash) ===
19323
+ context.message.header.session &&
19324
+ this._peerSyncCapabilityTimestamps.has(receiveFromHash) &&
19325
+ context.message.header.timestamp >
19326
+ this._peerSyncCapabilityTimestamps.get(receiveFromHash)! &&
19327
+ this._v2Send.acceptRequest(msg, {
19328
+ from: context.from,
19329
+ peerSession: receiveSession,
19330
+ receiverTransportSession: context.message.header.session,
19331
+ capabilityTimestamp:
19332
+ this._peerSyncCapabilityTimestamps.get(receiveFromHash)!,
19333
+ requestTimestamp: context.message.header.timestamp,
19334
+ })
19335
+ ) {
19336
+ this._liveness.markReplicatorActivity(receiveFromHash);
19337
+ }
19338
+ return;
18846
19339
  } else if (await this.syncronizer.onMessage(msg, context)) {
18847
19340
  return; // the syncronizer has handled the message
18848
19341
  } else if (msg instanceof BlocksMessage) {
@@ -18980,9 +19473,7 @@ export class SharedLog<
18980
19473
  msg.requests,
18981
19474
  );
18982
19475
  if (
18983
- !this.isReplicationLifecycleActive(
18984
- receiveReplicationLifecycleController,
18985
- )
19476
+ !this.isReplicationLifecycleActive(receiveReplicationLifecycleController)
18986
19477
  ) {
18987
19478
  return;
18988
19479
  }
@@ -19041,9 +19532,7 @@ export class SharedLog<
19041
19532
  let entry: Entry<T> | undefined;
19042
19533
  try {
19043
19534
  if (await this.log.blocks.has(request.hash)) {
19044
- entry = (await this.log.get(request.hash)) as
19045
- | Entry<T>
19046
- | undefined;
19535
+ entry = (await this.log.get(request.hash)) as Entry<T> | undefined;
19047
19536
  }
19048
19537
  } catch {
19049
19538
  // The normal entry-admission hook will retry this path.
@@ -19084,16 +19573,11 @@ export class SharedLog<
19084
19573
  }
19085
19574
  const responseTasks: Promise<void>[] = [];
19086
19575
  for (const request of msg.requests) {
19087
- const pendingDelete = this._checkedPrune.getPendingDelete(
19088
- request.hash,
19089
- );
19576
+ const pendingDelete = this._checkedPrune.getPendingDelete(request.hash);
19090
19577
  if (pendingDelete) {
19091
19578
  responseTasks.push(
19092
19579
  Promise.resolve(
19093
- pendingDelete.resolve(
19094
- context.from.hashcode(),
19095
- request.requestId,
19096
- ),
19580
+ pendingDelete.resolve(context.from.hashcode(), request.requestId),
19097
19581
  ),
19098
19582
  );
19099
19583
  }
@@ -19157,6 +19641,7 @@ export class SharedLog<
19157
19641
  }
19158
19642
  const segments = replicationSegments.map((x) => x.toReplicationRange());
19159
19643
  this.validatePersistedReplicationRangeSnapshot(segments);
19644
+ this._v2Send.enqueueSnapshotForPeer(receiveFromHash);
19160
19645
 
19161
19646
  await this.rpc
19162
19647
  .send(new AllReplicatingSegmentsMessage({ segments }), {
@@ -19212,6 +19697,191 @@ export class SharedLog<
19212
19697
  }
19213
19698
  }
19214
19699
 
19700
+ private async handleReplicationInfoV2Announcement(
19701
+ msg: ReplicationInfoV2Message,
19702
+ context: ReceiveRequestContext,
19703
+ lane: ReceiveLaneContext,
19704
+ ): Promise<void> {
19705
+ const from = context.from;
19706
+ const fromHash = lane.fromHash;
19707
+ const receiveSession = lane.session;
19708
+ if (
19709
+ from.equals(this.node.identity.publicKey) ||
19710
+ receiveSession === null ||
19711
+ receiveSession.phase !== "open"
19712
+ ) {
19713
+ return;
19714
+ }
19715
+
19716
+ // Authenticate and reserve before the per-peer lane. One reservation per
19717
+ // peer bounds decoded-frame retention while another mutation is parked.
19718
+ const receiveState = this._v2Receive._receiveStates.get(fromHash);
19719
+ if (
19720
+ !receiveState ||
19721
+ receiveState.peerSession !== receiveSession ||
19722
+ receiveState.senderTransportSession !== context.message.header.session
19723
+ ) {
19724
+ return;
19725
+ }
19726
+ const admission = this._v2Receive.reserve(msg, {
19727
+ from,
19728
+ peerSession: receiveSession,
19729
+ receiveEpoch: lane.receiveEpoch,
19730
+ senderTransportSession: context.message.header.session,
19731
+ transportTimestamp: context.message.header.timestamp,
19732
+ });
19733
+ if (!admission) {
19734
+ return;
19735
+ }
19736
+
19737
+ lane.lease.release();
19738
+ try {
19739
+ await this.withReplicationInfoApplyQueue(fromHash, async () => {
19740
+ const hostGate = () =>
19741
+ this._instanceLifecycle!.isMembershipActiveFor(
19742
+ lane.lifecycleController,
19743
+ ) &&
19744
+ this.isRepairLifecycleActive(lane.ownershipLifecycleController) &&
19745
+ this._peerSessions.isCurrent(fromHash, receiveSession) &&
19746
+ receiveSession.phase === "open" &&
19747
+ this._peerSessions.isReceiveEpochCurrent(
19748
+ fromHash,
19749
+ lane.receiveEpoch,
19750
+ ) &&
19751
+ !this._peerSessions.isReplicationInfoBlocked(fromHash) &&
19752
+ this._peerSessions.isReceiveCleanupGateOpen(fromHash);
19753
+ if (!hostGate()) {
19754
+ return;
19755
+ }
19756
+ const exactGate = () =>
19757
+ hostGate() && this._v2Receive.isAdmissionCurrent(admission);
19758
+ let durableCommitted = false;
19759
+
19760
+ try {
19761
+ if (
19762
+ msg instanceof FullReplicationInfoV2Message ||
19763
+ msg instanceof AddedReplicationInfoV2Message
19764
+ ) {
19765
+ let mutationGateChecked = false;
19766
+ let mutationGateAdmitted = false;
19767
+ const result = await this.addReplicationRange(
19768
+ msg.segments.map((segment) =>
19769
+ segment.toReplicationRangeIndexable(from),
19770
+ ),
19771
+ from,
19772
+ {
19773
+ reset: msg instanceof FullReplicationInfoV2Message,
19774
+ checkDuplicates: true,
19775
+ timestamp: Number(context.message.header.timestamp),
19776
+ allowLegacyOrderedReplacementPairs:
19777
+ msg instanceof AddedReplicationInfoV2Message,
19778
+ shouldApply: () => {
19779
+ mutationGateChecked = true;
19780
+ mutationGateAdmitted = exactGate();
19781
+ return mutationGateAdmitted;
19782
+ },
19783
+ onDurableApplyCommitted: () => {
19784
+ if (!exactGate() || !this._v2Receive.commit(admission)) {
19785
+ return false;
19786
+ }
19787
+ durableCommitted = true;
19788
+ return true;
19789
+ },
19790
+ },
19791
+ lane.ownershipLifecycleController,
19792
+ );
19793
+ if (
19794
+ result === undefined ||
19795
+ !mutationGateChecked ||
19796
+ !mutationGateAdmitted ||
19797
+ !durableCommitted ||
19798
+ !exactGate()
19799
+ ) {
19800
+ return;
19801
+ }
19802
+ } else {
19803
+ if (!exactGate()) {
19804
+ return;
19805
+ }
19806
+ const rangesToRemove =
19807
+ await this.resolveReplicationRangesFromIdsAndKey(
19808
+ msg.segmentIds,
19809
+ from,
19810
+ );
19811
+ if (!exactGate()) {
19812
+ return;
19813
+ }
19814
+ let mutationGateAdmitted = true;
19815
+ const removedRanges: ReplicationRangeIndexable<R>[] = [];
19816
+ const removed = await this.removeReplicationRanges(
19817
+ rangesToRemove,
19818
+ from,
19819
+ {
19820
+ shouldRemove: () => {
19821
+ mutationGateAdmitted = exactGate();
19822
+ return mutationGateAdmitted;
19823
+ },
19824
+ onDurableRemoveCommitted: () => {
19825
+ if (!exactGate() || !this._v2Receive.commit(admission)) {
19826
+ return false;
19827
+ }
19828
+ durableCommitted = true;
19829
+ return true;
19830
+ },
19831
+ onRemoved: (ranges) => removedRanges.push(...ranges),
19832
+ },
19833
+ lane.ownershipLifecycleController,
19834
+ );
19835
+ if (!durableCommitted) {
19836
+ if (
19837
+ !mutationGateAdmitted ||
19838
+ !exactGate() ||
19839
+ removed ||
19840
+ !this._v2Receive.commit(admission)
19841
+ ) {
19842
+ return;
19843
+ }
19844
+ durableCommitted = true;
19845
+ }
19846
+ if (
19847
+ this._instanceLifecycle!.isMembershipActiveFor(
19848
+ lane.lifecycleController,
19849
+ ) &&
19850
+ this.isRepairLifecycleActive(lane.ownershipLifecycleController)
19851
+ ) {
19852
+ const timestamp = BigInt(Date.now());
19853
+ for (const range of removedRanges) {
19854
+ this.replicationChangeDebounceFn.add({
19855
+ range,
19856
+ type: "removed",
19857
+ timestamp,
19858
+ });
19859
+ }
19860
+ }
19861
+ }
19862
+ } catch (error) {
19863
+ this._v2Receive.requireFullAfterFailure(admission);
19864
+ throw error;
19865
+ }
19866
+
19867
+ if (!durableCommitted) {
19868
+ if (!exactGate() || !this._v2Receive.commit(admission)) {
19869
+ return;
19870
+ }
19871
+ }
19872
+ if (!hostGate()) {
19873
+ return;
19874
+ }
19875
+ this._liveness.markReplicatorActivity(fromHash);
19876
+ if (msg instanceof FullReplicationInfoV2Message) {
19877
+ this.cancelReplicationInfoRequests(fromHash);
19878
+ }
19879
+ });
19880
+ } finally {
19881
+ this._v2Receive.release(admission);
19882
+ }
19883
+ }
19884
+
19215
19885
  private async handleReplicationInfoAnnouncement(
19216
19886
  msg: AllReplicatingSegmentsMessage | AddedReplicationSegmentMessage,
19217
19887
  context: ReceiveRequestContext,
@@ -19235,6 +19905,19 @@ export class SharedLog<
19235
19905
  // (and downstream `waitForReplicator()` timeouts) under timing-sensitive joins.
19236
19906
  const from = context.from!;
19237
19907
  const fromHash = from.hashcode();
19908
+ if (this._v2Receive.isLegacyCutover(receiveSession)) {
19909
+ if (receiveSession) {
19910
+ this._v2Receive.noteLegacyAnnouncement({
19911
+ peerHash: fromHash,
19912
+ peerSession: receiveSession,
19913
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
19914
+ senderTransportSession: context.message.header.session,
19915
+ transportTimestamp: context.message.header.timestamp,
19916
+ message: msg,
19917
+ });
19918
+ }
19919
+ return;
19920
+ }
19238
19921
  // Pre-lane gate: lifecycle -> receive-epoch -> blocked, exactly the
19239
19922
  // legacy order. isMembershipActiveFor is the unit-pinned fold of
19240
19923
  // isReplicationLifecycleActive; isReceiveEpochCurrent is the
@@ -19273,6 +19956,17 @@ export class SharedLog<
19273
19956
  ) {
19274
19957
  return;
19275
19958
  }
19959
+ if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
19960
+ this._v2Receive.noteLegacyAnnouncement({
19961
+ peerHash: fromHash,
19962
+ peerSession: receiveSession,
19963
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
19964
+ senderTransportSession: context.message.header.session,
19965
+ transportTimestamp: context.message.header.timestamp,
19966
+ message: msg,
19967
+ });
19968
+ return;
19969
+ }
19276
19970
 
19277
19971
  // Process in-order to avoid races where repeated reset messages arrive
19278
19972
  // concurrently and trigger spurious "added" diffs / rebalancing.
@@ -19282,14 +19976,13 @@ export class SharedLog<
19282
19976
  }
19283
19977
 
19284
19978
  this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
19285
- this._liveness._replicatorLivenessFailures.delete(fromHash);
19286
19979
 
19287
19980
  if (this.closed) {
19288
19981
  return;
19289
19982
  }
19290
19983
 
19291
19984
  const reset = msg instanceof AllReplicatingSegmentsMessage;
19292
- await this.addReplicationRange(
19985
+ const result = await this.addReplicationRange(
19293
19986
  replicationInfoMessage.segments.map((x) =>
19294
19987
  x.toReplicationRangeIndexable(from),
19295
19988
  ),
@@ -19302,6 +19995,10 @@ export class SharedLog<
19302
19995
  msg instanceof AddedReplicationSegmentMessage,
19303
19996
  },
19304
19997
  );
19998
+ if (result === undefined) {
19999
+ return;
20000
+ }
20001
+ this._liveness.markReplicatorActivity(fromHash);
19305
20002
 
19306
20003
  // If the peer reports any replication segments, stop re-requesting.
19307
20004
  // (Empty reports can be transient during startup.)
@@ -19337,6 +20034,19 @@ export class SharedLog<
19337
20034
  return;
19338
20035
  }
19339
20036
  const fromHash = from.hashcode();
20037
+ if (this._v2Receive.isLegacyCutover(receiveSession)) {
20038
+ if (receiveSession) {
20039
+ this._v2Receive.noteLegacyAnnouncement({
20040
+ peerHash: fromHash,
20041
+ peerSession: receiveSession,
20042
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
20043
+ senderTransportSession: context.message.header.session,
20044
+ transportTimestamp: context.message.header.timestamp,
20045
+ message: msg,
20046
+ });
20047
+ }
20048
+ return;
20049
+ }
19340
20050
  // Same pre-lane gate shape as Added/All above (and the same
19341
20051
  // intentional absence of a subscription-epoch term).
19342
20052
  if (
@@ -19367,22 +20077,34 @@ export class SharedLog<
19367
20077
  ) {
19368
20078
  return;
19369
20079
  }
20080
+ if (receiveSession && this._v2Receive.isLegacyCutover(receiveSession)) {
20081
+ this._v2Receive.noteLegacyAnnouncement({
20082
+ peerHash: fromHash,
20083
+ peerSession: receiveSession,
20084
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
20085
+ senderTransportSession: context.message.header.session,
20086
+ transportTimestamp: context.message.header.timestamp,
20087
+ message: msg,
20088
+ });
20089
+ return;
20090
+ }
19370
20091
 
19371
- const previousTimestamp =
19372
- this.latestReplicationInfoMessage.get(fromHash);
20092
+ const previousTimestamp = this.latestReplicationInfoMessage.get(fromHash);
19373
20093
  if (previousTimestamp && previousTimestamp > messageTimestamp) {
19374
20094
  return;
19375
20095
  }
19376
20096
  this.latestReplicationInfoMessage.set(fromHash, messageTimestamp);
19377
- this._liveness._replicatorLivenessFailures.delete(fromHash);
19378
20097
  if (this.closed) {
19379
20098
  return;
19380
20099
  }
19381
20100
 
19382
- const rangesToRemove =
19383
- await this.resolveReplicationRangesFromIdsAndKey(segmentIds, from);
20101
+ const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(
20102
+ segmentIds,
20103
+ from,
20104
+ );
19384
20105
 
19385
20106
  await this.removeReplicationRanges(rangesToRemove, from);
20107
+ this._liveness.markReplicatorActivity(fromHash);
19386
20108
  const timestamp = BigInt(+new Date());
19387
20109
  for (const range of rangesToRemove) {
19388
20110
  this.replicationChangeDebounceFn.add({
@@ -20184,10 +20906,9 @@ export class SharedLog<
20184
20906
  entry: ShallowOrFullEntry<any> | EntryReplicated<R> | NumberFromType<R>,
20185
20907
  minReplicas: number,
20186
20908
  ): Promise<NumberFromType<R>[]> {
20187
- return this._coordinates.createCoordinates(
20188
- entry,
20189
- minReplicas,
20190
- ) as Promise<NumberFromType<R>[]>;
20909
+ return this._coordinates.createCoordinates(entry, minReplicas) as Promise<
20910
+ NumberFromType<R>[]
20911
+ >;
20191
20912
  }
20192
20913
 
20193
20914
  async getDefaultMinRoleAge(): Promise<number> {
@@ -20551,8 +21272,9 @@ export class SharedLog<
20551
21272
  this.createNativeLeaderOptions(context, firstItem.options),
20552
21273
  );
20553
21274
  const plans: EntryLeaderPlan<R>[] = [];
20554
- const persistItems: Parameters<typeof this._coordinates.persistCoordinatesBatch>[0] =
20555
- [];
21275
+ const persistItems: Parameters<
21276
+ typeof this._coordinates.persistCoordinatesBatch
21277
+ >[0] = [];
20556
21278
  for (let i = 0; i < itemArray.length; i++) {
20557
21279
  const item = itemArray[i]!;
20558
21280
  const nativePlan = nativePlans[i]!;
@@ -20614,8 +21336,9 @@ export class SharedLog<
20614
21336
  );
20615
21337
  const selfHash = this.node.identity.publicKey.hashcode();
20616
21338
  const plans: EntryLeaderPlan<R>[] = [];
20617
- const persistItems: Parameters<typeof this._coordinates.persistCoordinatesBatch>[0] =
20618
- [];
21339
+ const persistItems: Parameters<
21340
+ typeof this._coordinates.persistCoordinatesBatch
21341
+ >[0] = [];
20619
21342
  for (let i = 0; i < itemArray.length; i++) {
20620
21343
  const item = itemArray[i]!;
20621
21344
  const nativePlan = nativePlans[i]!;
@@ -22226,6 +22949,8 @@ export class SharedLog<
22226
22949
  ownerHash: string,
22227
22950
  options?: { preserveOwnerMembership?: boolean },
22228
22951
  ): Promise<ReplicationRangeDeletionOutcome<R>> {
22952
+ const wasReplicator = this.uniqueReplicators.has(ownerHash);
22953
+ const joinWasEmitted = this._replicatorJoinEmitted.has(ownerHash);
22229
22954
  if (ranges.length === 0) {
22230
22955
  const ownerHasRanges =
22231
22956
  (await this.replicationIndex.count({ query: { hash: ownerHash } })) > 0;
@@ -22237,14 +22962,24 @@ export class SharedLog<
22237
22962
  removed: [],
22238
22963
  retained: [],
22239
22964
  ownerHasRanges,
22965
+ rollback: async () => {
22966
+ if (wasReplicator) {
22967
+ this.uniqueReplicators.add(ownerHash);
22968
+ } else {
22969
+ this.uniqueReplicators.delete(ownerHash);
22970
+ }
22971
+ if (joinWasEmitted) {
22972
+ this._replicatorJoinEmitted.add(ownerHash);
22973
+ } else {
22974
+ this._replicatorJoinEmitted.delete(ownerHash);
22975
+ }
22976
+ },
22240
22977
  };
22241
22978
  }
22242
22979
 
22243
22980
  const uniqueRanges = [
22244
22981
  ...new Map(ranges.map((range) => [range.idString, range])).values(),
22245
22982
  ];
22246
- const wasReplicator = this.uniqueReplicators.has(ownerHash);
22247
- const joinWasEmitted = this._replicatorJoinEmitted.has(ownerHash);
22248
22983
  type Snapshot = {
22249
22984
  range: ReplicationRangeIndexable<R>;
22250
22985
  pending?: PendingMaturityRecord<R>;
@@ -22478,10 +23213,80 @@ export class SharedLog<
22478
23213
  );
22479
23214
  this.poisonReplicationOwnership(outcomeError);
22480
23215
  }
23216
+ let rollbackStarted = false;
23217
+ const rollback = async () => {
23218
+ if (rollbackStarted) {
23219
+ return;
23220
+ }
23221
+ rollbackStarted = true;
23222
+ const rollbackErrors: unknown[] = [];
23223
+ const removedIds = new Set(removed.map((range) => range.idString));
23224
+ for (const snapshot of snapshots) {
23225
+ if (!removedIds.has(snapshot.range.idString)) {
23226
+ continue;
23227
+ }
23228
+ try {
23229
+ await this.replicationIndex.put(snapshot.range);
23230
+ this.putNativeReplicationRange(snapshot.range);
23231
+ } catch (error) {
23232
+ rollbackErrors.push(error);
23233
+ }
23234
+ }
23235
+ for (const snapshot of snapshots) {
23236
+ if (
23237
+ !removedIds.has(snapshot.range.idString) ||
23238
+ !snapshot.pending ||
23239
+ this.pendingMaturity.get(ownerHash)?.has(snapshot.range.idString)
23240
+ ) {
23241
+ continue;
23242
+ }
23243
+ try {
23244
+ this.schedulePendingMaturity(
23245
+ snapshot.pending.range,
23246
+ snapshot.pending.from,
23247
+ {
23248
+ rebalance: snapshot.pending.rebalance,
23249
+ waitMs: Math.max(0, snapshot.pending.expiresAt - Date.now()),
23250
+ },
23251
+ snapshot.pending.ownershipLifecycleController,
23252
+ );
23253
+ } catch (error) {
23254
+ rollbackErrors.push(error);
23255
+ }
23256
+ }
23257
+ try {
23258
+ if (wasReplicator) {
23259
+ this.uniqueReplicators.add(ownerHash);
23260
+ } else {
23261
+ this.uniqueReplicators.delete(ownerHash);
23262
+ }
23263
+ if (joinWasEmitted) {
23264
+ this._replicatorJoinEmitted.add(ownerHash);
23265
+ } else {
23266
+ this._replicatorJoinEmitted.delete(ownerHash);
23267
+ }
23268
+ } catch (error) {
23269
+ rollbackErrors.push(error);
23270
+ }
23271
+ try {
23272
+ await this.updateOldestTimestampFromIndex();
23273
+ } catch (error) {
23274
+ rollbackErrors.push(error);
23275
+ }
23276
+ if (rollbackErrors.length > 0) {
23277
+ const failure = new AggregateError(
23278
+ rollbackErrors,
23279
+ "Replication-range deletion rollback failed",
23280
+ );
23281
+ this.poisonReplicationOwnership(failure);
23282
+ throw failure;
23283
+ }
23284
+ };
22481
23285
  return {
22482
23286
  removed,
22483
23287
  retained,
22484
23288
  ownerHasRanges,
23289
+ rollback,
22485
23290
  error: outcomeError,
22486
23291
  };
22487
23292
  }
@@ -22497,7 +23302,8 @@ export class SharedLog<
22497
23302
 
22498
23303
  private scheduleReplicationInfoRequests(
22499
23304
  peer: PublicSignKey,
22500
- replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController,
23305
+ replicationLifecycleController = this._instanceLifecycle
23306
+ ?.membershipLifecycleController,
22501
23307
  ) {
22502
23308
  if (
22503
23309
  !replicationLifecycleController ||
@@ -22579,7 +23385,8 @@ export class SharedLog<
22579
23385
  if (!topics.includes(this.topic)) {
22580
23386
  return;
22581
23387
  }
22582
- const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
23388
+ const replicationLifecycleController =
23389
+ this._instanceLifecycle?.membershipLifecycleController;
22583
23390
  if (
22584
23391
  !replicationLifecycleController ||
22585
23392
  !this.isReplicationLifecycleActive(replicationLifecycleController)
@@ -22590,15 +23397,19 @@ export class SharedLog<
22590
23397
  const peerHash = publicKey.hashcode();
22591
23398
  const expectedSubscriptionEpoch =
22592
23399
  subscriptionEpoch ??
22593
- this._peerSessions.rotate(
22594
- peerHash,
22595
- subscribed ? "opening" : "departing",
22596
- );
23400
+ this._peerSessions.rotate(peerHash, subscribed ? "opening" : "departing");
22597
23401
  const ownsSubscriptionEpoch = () =>
22598
23402
  this._peerSessions.isCurrent(peerHash, expectedSubscriptionEpoch);
22599
23403
  if (!ownsSubscriptionEpoch()) {
22600
23404
  return;
22601
23405
  }
23406
+ // A destination stream is scoped to exactly one topic-subscription
23407
+ // session. Abort the predecessor synchronously before either barrier can
23408
+ // yield; a late queue completion must never enter the new session.
23409
+ this._v2Receive.clearPeer(peerHash);
23410
+ this._v2Send.clearPeer(peerHash);
23411
+ this._peerSyncCapabilitySessions.delete(peerHash);
23412
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
22602
23413
  if (subscribed) {
22603
23414
  const pendingOpeningCapabilities =
22604
23415
  this._openingSyncCapabilitiesByPeer.get(peerHash);
@@ -22638,6 +23449,22 @@ export class SharedLog<
22638
23449
  peerHash,
22639
23450
  openingCapabilities.capabilities,
22640
23451
  );
23452
+ if (openingCapabilities.transportSession === undefined) {
23453
+ this._peerSyncCapabilitySessions.delete(peerHash);
23454
+ } else {
23455
+ this._peerSyncCapabilitySessions.set(
23456
+ peerHash,
23457
+ openingCapabilities.transportSession,
23458
+ );
23459
+ }
23460
+ if (openingCapabilities.timestamp === undefined) {
23461
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
23462
+ } else {
23463
+ this._peerSyncCapabilityTimestamps.set(
23464
+ peerHash,
23465
+ openingCapabilities.timestamp,
23466
+ );
23467
+ }
22641
23468
  this._openingSyncCapabilitiesByPeer.delete(peerHash);
22642
23469
  }
22643
23470
  this._peerSessions.unblockReplicationInfo(peerHash);
@@ -22716,31 +23543,30 @@ export class SharedLog<
22716
23543
  this._liveness._replicatorLivenessFailures.delete(peerHash);
22717
23544
  this._liveness.markReplicatorActivity(peerHash);
22718
23545
  this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
23546
+ this.promoteReplicationInfoV2ReceiveCapability(
23547
+ publicKey,
23548
+ expectedSubscriptionEpoch,
23549
+ );
22719
23550
 
22720
- // Decode support is advertised independently from use permission. Every
22721
- // replication-info send below and in the announcement coordinator remains
22722
- // on the legacy wire format until a later receiver-led negotiation exists.
22723
- const receiveCapabilities =
22724
- SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
22725
- (this._logProperties?.sync?.rawExchangeHeads === true
22726
- ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
22727
- : 0);
22728
- this.rpc
22729
- .send(
22730
- new SyncCapabilitiesMessage({
22731
- capabilities: receiveCapabilities,
22732
- }),
22733
- {
22734
- mode: new SilentDelivery({ redundancy: 1, to: [publicKey] }),
22735
- signal: replicationLifecycleController.signal,
22736
- },
22737
- )
22738
- .catch((error) =>
23551
+ // Decode, sender and authenticated apply readiness are separate bits. Start
23552
+ // the ACKed receiver-led negotiation before the legacy snapshot, but do not
23553
+ // hold mixed-version replication discovery behind that round trip. Attach
23554
+ // the rejection handler now because a lifecycle fence below may return before
23555
+ // awaiting the advert. RequestV2 is armed only after both sends settle.
23556
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23557
+ const localCapabilityAdvertisement =
23558
+ this.advertiseReplicationInfoV2ReceiveCapability({
23559
+ target: publicKey,
23560
+ peerSession: expectedSubscriptionEpoch,
23561
+ receiveEpoch,
23562
+ signal: replicationLifecycleController.signal,
23563
+ }).catch((error) => {
22739
23564
  this.handleReplicationLifecycleSendError(
22740
23565
  error,
22741
23566
  replicationLifecycleController,
22742
- ),
22743
- );
23567
+ );
23568
+ return undefined;
23569
+ });
22744
23570
 
22745
23571
  let replicationSegments: ReplicationRangeIndexable<R>[];
22746
23572
  try {
@@ -22810,9 +23636,9 @@ export class SharedLog<
22810
23636
  }
22811
23637
  }
22812
23638
 
22813
- // Request the remote peer's replication info. This makes joins resilient to
22814
- // timing-sensitive delivery/order issues where we may miss their initial
22815
- // replication announcement.
23639
+ // Keep legacy request-based discovery independent of the capability ACK too.
23640
+ // This makes mixed-version joins resilient to timing-sensitive delivery/order
23641
+ // issues where we may miss the remote peer's initial announcement.
22816
23642
  if (
22817
23643
  this.isReplicationLifecycleActive(replicationLifecycleController) &&
22818
23644
  ownsSubscriptionEpoch()
@@ -22822,6 +23648,16 @@ export class SharedLog<
22822
23648
  replicationLifecycleController,
22823
23649
  );
22824
23650
  }
23651
+
23652
+ const localCapabilityReady = await localCapabilityAdvertisement;
23653
+ if (localCapabilityReady) {
23654
+ this._v2Receive.markLocalCapabilityReady({
23655
+ peerHash,
23656
+ peerSession: expectedSubscriptionEpoch,
23657
+ receiveEpoch,
23658
+ ...localCapabilityReady,
23659
+ });
23660
+ }
22825
23661
  }
22826
23662
 
22827
23663
  private getClampedReplicas(customValue?: MinReplicas) {
@@ -23944,7 +24780,10 @@ export class SharedLog<
23944
24780
  if (oldPeersSet) {
23945
24781
  for (const oldPeer of oldPeersSet) {
23946
24782
  if (!currentPeers.has(oldPeer)) {
23947
- this._checkedPrune.removeRequestSent(entryReplicated.hash, oldPeer);
24783
+ this._checkedPrune.removeRequestSent(
24784
+ entryReplicated.hash,
24785
+ oldPeer,
24786
+ );
23948
24787
  }
23949
24788
  }
23950
24789
  }
@@ -24033,7 +24872,10 @@ export class SharedLog<
24033
24872
  if (oldPeersSet) {
24034
24873
  for (const oldPeer of oldPeersSet) {
24035
24874
  if (!currentPeers.has(oldPeer)) {
24036
- this._checkedPrune.removeRequestSent(entryReplicated.hash, oldPeer);
24875
+ this._checkedPrune.removeRequestSent(
24876
+ entryReplicated.hash,
24877
+ oldPeer,
24878
+ );
24037
24879
  }
24038
24880
  }
24039
24881
  }
@@ -24213,10 +25055,7 @@ export class SharedLog<
24213
25055
  }
24214
25056
 
24215
25057
  const fromHash = evt.detail.from.hashcode();
24216
- const subscriptionEpoch = this._peerSessions.rotate(
24217
- fromHash,
24218
- "departing",
24219
- );
25058
+ const subscriptionEpoch = this._peerSessions.rotate(fromHash, "departing");
24220
25059
  this._peerSessions.blockReplicationInfo(fromHash);
24221
25060
  this._recentRepairDispatch.delete(fromHash);
24222
25061