@peerbit/shared-log 13.2.31 → 13.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -154,7 +154,9 @@ import {
154
154
  ResponseIPrune,
155
155
  ResponseIPruneV2,
156
156
  SYNC_CAPABILITY_RAW_EXCHANGE_HEADS,
157
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY,
157
158
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE,
159
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND,
158
160
  StashBackedRawExchangeHeadsMessage,
159
161
  SyncCapabilitiesMessage,
160
162
  collectRawExchangeHeadSendPlan,
@@ -235,6 +237,7 @@ import {
235
237
  ReplicationAnnouncementCoordinator,
236
238
  isTransientReplicationAnnouncementError,
237
239
  } from "./replication-announcement.js";
240
+ import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
238
241
  import {
239
242
  type ReplicationDomainHash,
240
243
  createReplicationDomainHash,
@@ -258,6 +261,7 @@ import {
258
261
  type ReplicationLimits,
259
262
  ReplicationPingMessage,
260
263
  RequestReplicationInfoMessage,
264
+ RequestReplicationInfoV2Message,
261
265
  ResponseRoleMessage,
262
266
  StoppedReplicating,
263
267
  decodeReplicas,
@@ -2024,7 +2028,12 @@ export class SharedLog<
2024
2028
  // PeerSession the advert was staged under; promote/delete key off it.
2025
2029
  private _openingSyncCapabilitiesByPeer!: Map<
2026
2030
  string,
2027
- { epoch: object; capabilities: number }
2031
+ {
2032
+ epoch: object;
2033
+ capabilities: number;
2034
+ transportSession?: bigint;
2035
+ timestamp?: bigint;
2036
+ }
2028
2037
  >;
2029
2038
  private _onFanoutDataFn?: (arg: any) => void;
2030
2039
  private _onFanoutUnicastFn?: (arg: any) => void;
@@ -3225,6 +3234,7 @@ export class SharedLog<
3225
3234
  | ReturnType<typeof debounceFixedInterval>
3226
3235
  | undefined;
3227
3236
  private _announcements!: ReplicationAnnouncementCoordinator<R>;
3237
+ private _v2Send!: ReplicationInfoV2SendCoordinator<R>;
3228
3238
 
3229
3239
  // A fn for debouncing the calls for pruning
3230
3240
  pruneDebouncedFn!: DebouncedAccumulatorMap<{
@@ -3286,6 +3296,10 @@ export class SharedLog<
3286
3296
  // Sync capability bits advertised by peers (SyncCapabilitiesMessage), keyed
3287
3297
  // by public key hash. Entries are dropped on unsubscribe/disconnect.
3288
3298
  private _peerSyncCapabilities!: Map<string, number>;
3299
+ // Signed transport session carried by the capability envelope. Kept in a
3300
+ // parallel map so existing capability-number consumers remain unchanged.
3301
+ private _peerSyncCapabilitySessions!: Map<string, bigint>;
3302
+ private _peerSyncCapabilityTimestamps!: Map<string, bigint>;
3289
3303
  // Pending live raw exchange-head gossip, coalesced per recipient set and
3290
3304
  // flushed at the end of the current event-loop turn (or when a batch cap
3291
3305
  // is hit). Only used when every recipient advertised raw capability.
@@ -3355,6 +3369,7 @@ export class SharedLog<
3355
3369
  this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
3356
3370
  queueCurrentReplicationStateAnnouncementRetry: (error: unknown) =>
3357
3371
  this._announcements.queueCurrentReplicationStateAnnouncementRetry(error),
3372
+ enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
3358
3373
  isClosed: () => this.closed,
3359
3374
  getCloseSignal: () => this._closeController.signal,
3360
3375
  getMyReplicationSegments: () => this.getMyReplicationSegments(),
@@ -3374,6 +3389,30 @@ export class SharedLog<
3374
3389
  });
3375
3390
  }
3376
3391
 
3392
+ private createReplicationInfoV2SendCoordinator(): ReplicationInfoV2SendCoordinator<R> {
3393
+ return new ReplicationInfoV2SendCoordinator<R>({
3394
+ getRpc: () => this.rpc,
3395
+ getSelfKey: () => this.node.identity.publicKey,
3396
+ getSenderTransportSession: () =>
3397
+ BigInt(
3398
+ (this.node.services.pubsub as unknown as { session: number }).session,
3399
+ ),
3400
+ getMyReplicationSegments: () => this.getMyReplicationSegments(),
3401
+ validatePersistedReplicationRangeSnapshot: (ranges) =>
3402
+ this.validatePersistedReplicationRangeSnapshot(ranges),
3403
+ isClosed: () => this.closed,
3404
+ isPeerSessionOpen: (peerHash, peerSession) =>
3405
+ this._peerSessions.isCurrent(peerHash, peerSession) &&
3406
+ (peerSession as PeerSession).phase === "open" &&
3407
+ !this._peerSessions.isReplicationInfoBlocked(peerHash) &&
3408
+ this._peerSessions.isReceiveCleanupGateOpen(peerHash),
3409
+ captureReplicationOwnershipLifecycle: () =>
3410
+ this.captureReplicationOwnershipLifecycle(),
3411
+ isReplicationOwnershipLifecycleActive: (controller) =>
3412
+ this.isRepairLifecycleActive(controller),
3413
+ });
3414
+ }
3415
+
3377
3416
  private createReplicatorLivenessMonitor(): ReplicatorLivenessMonitor {
3378
3417
  return new ReplicatorLivenessMonitor({
3379
3418
  // Sweep-driven probes and activity marks dispatch through the owner so
@@ -3488,6 +3527,8 @@ export class SharedLog<
3488
3527
  this._appendBackfillPendingByTarget = new Map();
3489
3528
  this._topicSubscribersCache = new Map();
3490
3529
  this._peerSyncCapabilities = new Map();
3530
+ this._peerSyncCapabilitySessions = new Map();
3531
+ this._peerSyncCapabilityTimestamps = new Map();
3491
3532
  this._liveRawGossipBatches = new Map();
3492
3533
  this._liveRawGossipFlushScheduled = false;
3493
3534
  this.coordinateToHash = new Cache<string>({ max: 1e6, ttl: 1e4 });
@@ -3496,6 +3537,7 @@ export class SharedLog<
3496
3537
  this._replicatorJoinEmitted = new Set();
3497
3538
  this._replicatorsReconciled = false;
3498
3539
  this._liveness = this.createReplicatorLivenessMonitor();
3540
+ this._v2Send = this.createReplicationInfoV2SendCoordinator();
3499
3541
  this._announcements = this.createReplicationAnnouncementCoordinator();
3500
3542
  this.pendingMaturity = new Map();
3501
3543
  this._closeController = new AbortController();
@@ -3991,6 +4033,97 @@ export class SharedLog<
3991
4033
  );
3992
4034
  }
3993
4035
 
4036
+ private observePeerSyncCapabilities(properties: {
4037
+ peerHash: string;
4038
+ capabilities: number;
4039
+ transportSession?: bigint;
4040
+ timestamp?: bigint;
4041
+ openingSession?: PeerSession;
4042
+ }): boolean {
4043
+ const {
4044
+ peerHash,
4045
+ capabilities,
4046
+ transportSession,
4047
+ timestamp,
4048
+ openingSession,
4049
+ } = properties;
4050
+ if (openingSession) {
4051
+ const previous = this._openingSyncCapabilitiesByPeer.get(peerHash);
4052
+ if (
4053
+ transportSession !== undefined &&
4054
+ timestamp !== undefined &&
4055
+ previous?.epoch === openingSession &&
4056
+ previous.transportSession === transportSession
4057
+ ) {
4058
+ if (previous.timestamp !== undefined && timestamp < previous.timestamp) {
4059
+ return false;
4060
+ }
4061
+ this._openingSyncCapabilitiesByPeer.set(peerHash, {
4062
+ epoch: openingSession,
4063
+ capabilities: previous.capabilities | capabilities,
4064
+ transportSession,
4065
+ timestamp:
4066
+ previous.timestamp === undefined || timestamp > previous.timestamp
4067
+ ? timestamp
4068
+ : previous.timestamp,
4069
+ });
4070
+ return true;
4071
+ }
4072
+ this._openingSyncCapabilitiesByPeer.set(peerHash, {
4073
+ epoch: openingSession,
4074
+ capabilities,
4075
+ transportSession,
4076
+ timestamp,
4077
+ });
4078
+ return true;
4079
+ }
4080
+
4081
+ if (transportSession === undefined || timestamp === undefined) {
4082
+ // Test/in-process synthetic contexts predate signed envelope captures.
4083
+ // They may exercise capability-number behavior, but can never authorize V2.
4084
+ this._peerSyncCapabilities.set(peerHash, capabilities);
4085
+ this._peerSyncCapabilitySessions.delete(peerHash);
4086
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
4087
+ this._v2Send.advancePeerCapability(peerHash);
4088
+ return true;
4089
+ }
4090
+
4091
+ const previousSession = this._peerSyncCapabilitySessions.get(peerHash);
4092
+ const previousTimestamp = this._peerSyncCapabilityTimestamps.get(peerHash);
4093
+ const sameTransportSession = previousSession === transportSession;
4094
+ if (
4095
+ sameTransportSession &&
4096
+ previousTimestamp !== undefined &&
4097
+ timestamp < previousTimestamp
4098
+ ) {
4099
+ return false;
4100
+ }
4101
+ const previousCapabilities =
4102
+ this._peerSyncCapabilities.get(peerHash) ?? 0;
4103
+ const nextCapabilities = sameTransportSession
4104
+ ? previousCapabilities | capabilities
4105
+ : capabilities;
4106
+ const generationAdvanced =
4107
+ !sameTransportSession ||
4108
+ previousTimestamp === undefined ||
4109
+ timestamp > previousTimestamp ||
4110
+ nextCapabilities !== previousCapabilities;
4111
+ this._peerSyncCapabilities.set(peerHash, nextCapabilities);
4112
+ this._peerSyncCapabilitySessions.set(peerHash, transportSession);
4113
+ this._peerSyncCapabilityTimestamps.set(
4114
+ peerHash,
4115
+ previousTimestamp === undefined ||
4116
+ !sameTransportSession ||
4117
+ timestamp > previousTimestamp
4118
+ ? timestamp
4119
+ : previousTimestamp,
4120
+ );
4121
+ if (generationAdvanced) {
4122
+ this._v2Send.advancePeerCapability(peerHash);
4123
+ }
4124
+ return true;
4125
+ }
4126
+
3994
4127
  /**
3995
4128
  * Live append gossip may use the raw exchange-heads path only when we
3996
4129
  * opted into raw sync and every remote recipient advertised raw capability
@@ -13831,6 +13964,8 @@ export class SharedLog<
13831
13964
  ttl: LEADER_PLAN_CACHE_TTL_MS,
13832
13965
  });
13833
13966
  this._peerSyncCapabilities = new Map();
13967
+ this._peerSyncCapabilitySessions = new Map();
13968
+ this._peerSyncCapabilityTimestamps = new Map();
13834
13969
  this._liveRawGossipBatches = new Map();
13835
13970
  this._liveRawGossipFlushScheduled = false;
13836
13971
  this.coordinateToHash = new Cache<string>({ max: 1e6, ttl: 1e4 });
@@ -13847,6 +13982,8 @@ export class SharedLog<
13847
13982
  this._lastLocalAppendAt = 0;
13848
13983
  this._announcements ??= this.createReplicationAnnouncementCoordinator();
13849
13984
  this._announcements.resetForOpen();
13985
+ this._v2Send ??= this.createReplicationInfoV2SendCoordinator();
13986
+ this._v2Send.resetForOpen();
13850
13987
  const adaptiveReplicateOptions =
13851
13988
  options?.replicate && isAdaptiveReplicatorOption(options.replicate)
13852
13989
  ? options.replicate
@@ -15339,6 +15476,9 @@ export class SharedLog<
15339
15476
  this._liveness._replicatorLivenessFailures.delete(peerHash);
15340
15477
  this._liveness._replicatorLastActivityAt.delete(peerHash);
15341
15478
  this._peerSyncCapabilities.delete(peerHash);
15479
+ this._peerSyncCapabilitySessions.delete(peerHash);
15480
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
15481
+ this._v2Send.clearPeer(peerHash);
15342
15482
  this.cleanupPendingIHavePeer(peerHash);
15343
15483
  this.cleanupCheckedPrunePeer(
15344
15484
  peerHash,
@@ -16348,6 +16488,9 @@ export class SharedLog<
16348
16488
  this._openingSyncCapabilitiesByPeer?.clear();
16349
16489
  this._gidPeersHistory?.clear();
16350
16490
  this._peerSyncCapabilities?.clear();
16491
+ this._peerSyncCapabilitySessions?.clear();
16492
+ this._peerSyncCapabilityTimestamps?.clear();
16493
+ this._v2Send?.clearForClose();
16351
16494
  this._liveRawGossipBatches?.clear();
16352
16495
  this._nativeSharedLogState?.clearGidPeers();
16353
16496
  this._nativeBackbone?.clearGidPeers();
@@ -16480,12 +16623,14 @@ export class SharedLog<
16480
16623
  }
16481
16624
  }, 2_000);
16482
16625
  try {
16483
- await this.rpc
16484
- .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
16626
+ const reset = new AllReplicatingSegmentsMessage({ segments: [] });
16627
+ await Promise.all([
16628
+ this.rpc.send(reset, {
16485
16629
  priority: CONVERGENCE_MESSAGE_PRIORITY,
16486
16630
  signal: abort.signal,
16487
- })
16488
- .catch(() => {});
16631
+ }).catch(() => {}),
16632
+ this._v2Send.sendTerminalReset(abort.signal),
16633
+ ]);
16489
16634
  } finally {
16490
16635
  clearTimeout(abortTimer);
16491
16636
  }
@@ -16604,12 +16749,14 @@ export class SharedLog<
16604
16749
  }
16605
16750
  }, 2_000);
16606
16751
  try {
16607
- await this.rpc
16608
- .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
16752
+ const reset = new AllReplicatingSegmentsMessage({ segments: [] });
16753
+ await Promise.all([
16754
+ this.rpc.send(reset, {
16609
16755
  priority: CONVERGENCE_MESSAGE_PRIORITY,
16610
16756
  signal: abort.signal,
16611
- })
16612
- .catch(() => {});
16757
+ }).catch(() => {}),
16758
+ this._v2Send.sendTerminalReset(abort.signal),
16759
+ ]);
16613
16760
  } finally {
16614
16761
  clearTimeout(abortTimer);
16615
16762
  }
@@ -17195,7 +17342,10 @@ export class SharedLog<
17195
17342
  // allocation cost, and before liveness or apply-queue side effects.
17196
17343
  this.validateStoppedReplicationAnnouncement(msg.segmentIds);
17197
17344
  }
17198
- if (!context.from.equals(this.node.identity.publicKey)) {
17345
+ if (
17346
+ !context.from.equals(this.node.identity.publicKey) &&
17347
+ !(msg instanceof RequestReplicationInfoV2Message)
17348
+ ) {
17199
17349
  this._liveness.markReplicatorActivity(receiveFromHash);
17200
17350
  }
17201
17351
 
@@ -18824,6 +18974,8 @@ export class SharedLog<
18824
18974
  return;
18825
18975
  } else if (msg instanceof SyncCapabilitiesMessage) {
18826
18976
  if (!context.from.equals(this.node.identity.publicKey)) {
18977
+ const capabilityTransportSession = context.message?.header?.session;
18978
+ const capabilityTimestamp = context.message?.header?.timestamp;
18827
18979
  // No await separates this from the capture above, so the captured
18828
18980
  // window state is exact: the legacy re-read of the opening map here
18829
18981
  // could never observe a different value.
@@ -18834,15 +18986,47 @@ export class SharedLog<
18834
18986
  // A prior unsubscribe cleanup may still be ahead of this reconnect
18835
18987
  // barrier. Stage the new generation's one-shot advertisement so that
18836
18988
  // cleanup cannot erase it before the opening transition commits.
18837
- this._openingSyncCapabilitiesByPeer.set(receiveFromHash, {
18838
- epoch: receiveSession!,
18989
+ this.observePeerSyncCapabilities({
18990
+ peerHash: receiveFromHash,
18839
18991
  capabilities: msg.capabilities,
18992
+ transportSession: capabilityTransportSession,
18993
+ timestamp: capabilityTimestamp,
18994
+ openingSession: receiveSession!,
18840
18995
  });
18841
18996
  } else {
18842
- this._peerSyncCapabilities.set(receiveFromHash, msg.capabilities);
18997
+ this.observePeerSyncCapabilities({
18998
+ peerHash: receiveFromHash,
18999
+ capabilities: msg.capabilities,
19000
+ transportSession: capabilityTransportSession,
19001
+ timestamp: capabilityTimestamp,
19002
+ });
18843
19003
  }
18844
19004
  }
18845
19005
  return;
19006
+ } else if (msg instanceof RequestReplicationInfoV2Message) {
19007
+ const requiredCapabilities =
19008
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
19009
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY;
19010
+ if (
19011
+ receiveSession &&
19012
+ ((this._peerSyncCapabilities.get(receiveFromHash) ?? 0) &
19013
+ requiredCapabilities) ===
19014
+ requiredCapabilities &&
19015
+ this._peerSyncCapabilitySessions.get(receiveFromHash) ===
19016
+ context.message.header.session &&
19017
+ this._peerSyncCapabilityTimestamps.has(receiveFromHash) &&
19018
+ context.message.header.timestamp >
19019
+ this._peerSyncCapabilityTimestamps.get(receiveFromHash)! &&
19020
+ this._v2Send.acceptRequest(msg, {
19021
+ from: context.from,
19022
+ peerSession: receiveSession,
19023
+ receiverTransportSession: context.message.header.session,
19024
+ requestTimestamp: context.message.header.timestamp,
19025
+ })
19026
+ ) {
19027
+ this._liveness.markReplicatorActivity(receiveFromHash);
19028
+ }
19029
+ return;
18846
19030
  } else if (await this.syncronizer.onMessage(msg, context)) {
18847
19031
  return; // the syncronizer has handled the message
18848
19032
  } else if (msg instanceof BlocksMessage) {
@@ -19157,6 +19341,7 @@ export class SharedLog<
19157
19341
  }
19158
19342
  const segments = replicationSegments.map((x) => x.toReplicationRange());
19159
19343
  this.validatePersistedReplicationRangeSnapshot(segments);
19344
+ this._v2Send.enqueueSnapshotForPeer(receiveFromHash);
19160
19345
 
19161
19346
  await this.rpc
19162
19347
  .send(new AllReplicatingSegmentsMessage({ segments }), {
@@ -22599,6 +22784,12 @@ export class SharedLog<
22599
22784
  if (!ownsSubscriptionEpoch()) {
22600
22785
  return;
22601
22786
  }
22787
+ // A destination stream is scoped to exactly one topic-subscription
22788
+ // session. Abort the predecessor synchronously before either barrier can
22789
+ // yield; a late queue completion must never enter the new session.
22790
+ this._v2Send.clearPeer(peerHash);
22791
+ this._peerSyncCapabilitySessions.delete(peerHash);
22792
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
22602
22793
  if (subscribed) {
22603
22794
  const pendingOpeningCapabilities =
22604
22795
  this._openingSyncCapabilitiesByPeer.get(peerHash);
@@ -22638,6 +22829,22 @@ export class SharedLog<
22638
22829
  peerHash,
22639
22830
  openingCapabilities.capabilities,
22640
22831
  );
22832
+ if (openingCapabilities.transportSession === undefined) {
22833
+ this._peerSyncCapabilitySessions.delete(peerHash);
22834
+ } else {
22835
+ this._peerSyncCapabilitySessions.set(
22836
+ peerHash,
22837
+ openingCapabilities.transportSession,
22838
+ );
22839
+ }
22840
+ if (openingCapabilities.timestamp === undefined) {
22841
+ this._peerSyncCapabilityTimestamps.delete(peerHash);
22842
+ } else {
22843
+ this._peerSyncCapabilityTimestamps.set(
22844
+ peerHash,
22845
+ openingCapabilities.timestamp,
22846
+ );
22847
+ }
22641
22848
  this._openingSyncCapabilitiesByPeer.delete(peerHash);
22642
22849
  }
22643
22850
  this._peerSessions.unblockReplicationInfo(peerHash);
@@ -22717,11 +22924,13 @@ export class SharedLog<
22717
22924
  this._liveness.markReplicatorActivity(peerHash);
22718
22925
  this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
22719
22926
 
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.
22927
+ // Decode support and receiver-led negotiation readiness are separate bits.
22928
+ // This node never initiates a request in this rollout; normal replication
22929
+ // announcements remain legacy unless a peer explicitly grants one signed,
22930
+ // session-bound V2 destination stream.
22723
22931
  const receiveCapabilities =
22724
22932
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
22933
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND |
22725
22934
  (this._logProperties?.sync?.rawExchangeHeads === true
22726
22935
  ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
22727
22936
  : 0);
@@ -204,8 +204,16 @@ export type ReplicationAnnouncementDeps<R extends "u32" | "u64"> = {
204
204
  // Owner-routed so coordinator spies keep observing re-entrant queueing.
205
205
  queueCurrentReplicationStateAnnouncementRepair: () => void;
206
206
  queueCurrentReplicationStateAnnouncementRetry: (error: unknown) => boolean;
207
+ // Best-effort sidecar. The legacy publish remains the primary operation and
208
+ // retains its exact rejection/retry semantics during mixed-version rollout.
209
+ enqueueReplicationInfoV2: (message: LegacyReplicationInfoMessage) => void;
207
210
  };
208
211
 
212
+ type LegacyReplicationInfoMessage =
213
+ | AllReplicatingSegmentsMessage
214
+ | AddedReplicationSegmentMessage
215
+ | StoppedReplicating;
216
+
209
217
  export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
210
218
  replicationAnnouncementRetryDebounced:
211
219
  | ReturnType<typeof debounceFixedInterval>
@@ -611,10 +619,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
611
619
  }
612
620
 
613
621
  async sendReplicationAnnouncement(
614
- message:
615
- | AllReplicatingSegmentsMessage
616
- | AddedReplicationSegmentMessage
617
- | StoppedReplicating,
622
+ message: LegacyReplicationInfoMessage,
618
623
  ownershipLifecycleController = this.deps.captureReplicationOwnershipLifecycle(),
619
624
  options?: { shouldSend?: () => boolean },
620
625
  ): Promise<void> {
@@ -639,6 +644,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
639
644
  // after that stale send settles.
640
645
  this.rotateAnnouncementSession();
641
646
  this.advanceCurrentReplicationStateAnnouncementRepairGeneration();
647
+ this.deps.enqueueReplicationInfoV2(message);
642
648
  try {
643
649
  await this.deps.getRpc().send(message, {
644
650
  priority: CONVERGENCE_MESSAGE_PRIORITY,