@peerbit/shared-log 16.0.23 → 16.0.25

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
@@ -169,6 +169,7 @@ import {
169
169
  SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY,
170
170
  SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM,
171
171
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE,
172
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM,
172
173
  SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND,
173
174
  StashBackedRawExchangeHeadsMessage,
174
175
  SyncCapabilitiesMessage,
@@ -322,6 +323,86 @@ type SharedLogServicesWithFanout = {
322
323
  const getSharedLogFanoutService = (services: unknown): FanoutTree | undefined =>
323
324
  (services as SharedLogServicesWithFanout).fanout;
324
325
 
326
+ const FANOUT_OPEN_METRICS = [
327
+ ["joinReqSent", "joinReqSent"],
328
+ ["joinAcceptReceived", "joinAcceptReceived"],
329
+ ["joinRejectReceived", "joinRejectReceived"],
330
+ ["bootstrapDialAttempts", "joinBootstrapDialAttempts"],
331
+ ["bootstrapDialFailures", "joinBootstrapDialFailures"],
332
+ ["candidateDialAttempts", "joinCandidateDialAttempts"],
333
+ ["candidateDialFailures", "joinCandidateDialFailures"],
334
+ ["connectedCandidateAttempts", "joinConnectedCandidateAttempts"],
335
+ ["unconnectedCandidateAttempts", "joinUnconnectedCandidateAttempts"],
336
+ ["joinReqTimeouts", "joinReqTimeouts"],
337
+ ["deadlineExpirations", "joinDeadlineExpirations"],
338
+ ] as const;
339
+
340
+ type FanoutOpenMetric = (typeof FANOUT_OPEN_METRICS)[number][0];
341
+ type FanoutOpenMetricSnapshot = Record<FanoutOpenMetric, number>;
342
+
343
+ const snapshotFanoutOpenMetrics = (
344
+ service: FanoutTree,
345
+ topic: string,
346
+ root: string,
347
+ ): FanoutOpenMetricSnapshot | undefined => {
348
+ try {
349
+ const metrics = (
350
+ service as FanoutTree & {
351
+ getChannelMetrics: (
352
+ topic: string,
353
+ root: string,
354
+ ) => Record<string, number>;
355
+ }
356
+ ).getChannelMetrics(topic, root);
357
+ return Object.fromEntries(
358
+ FANOUT_OPEN_METRICS.map(([name, source]) => [name, metrics[source] ?? 0]),
359
+ ) as FanoutOpenMetricSnapshot;
360
+ } catch {
361
+ return;
362
+ }
363
+ };
364
+
365
+ const emitFanoutOpenProfile = (properties: {
366
+ profile?: SyncProfileFn;
367
+ startedAt: number;
368
+ service: FanoutTree;
369
+ topic: string;
370
+ root: string;
371
+ mode: "root" | "node";
372
+ outcome: "error" | "opened" | "joined";
373
+ timeoutMs?: number;
374
+ before?: FanoutOpenMetricSnapshot;
375
+ }) => {
376
+ if (!properties.profile) return;
377
+ try {
378
+ const after = snapshotFanoutOpenMetrics(
379
+ properties.service,
380
+ properties.topic,
381
+ properties.root,
382
+ );
383
+ const deltas = Object.fromEntries(
384
+ FANOUT_OPEN_METRICS.map(([name]) => [
385
+ name,
386
+ (after?.[name] ?? 0) - (properties.before?.[name] ?? 0),
387
+ ]),
388
+ ) as FanoutOpenMetricSnapshot;
389
+ emitSyncProfileDuration(properties.profile, properties.startedAt, {
390
+ name: "sharedLog.open.fanout",
391
+ component: "shared-log",
392
+ messages: deltas.joinReqSent,
393
+ details: {
394
+ configured: true,
395
+ mode: properties.mode,
396
+ outcome: properties.outcome,
397
+ configuredTimeoutMs: properties.timeoutMs,
398
+ ...deltas,
399
+ },
400
+ });
401
+ } catch {
402
+ // Diagnostics must not affect open correctness.
403
+ }
404
+ };
405
+
325
406
  type PendingIHave<T> = {
326
407
  resetTimeout: () => void;
327
408
  requesting: Map<string, Uint8Array>;
@@ -471,6 +552,18 @@ export const logger = loggerFn("peerbit:shared-log");
471
552
  const warn = logger.newScope("warn");
472
553
  const traceLogger = logger.trace as typeof logger.trace & { enabled?: boolean };
473
554
 
555
+ const emitAdvisorySyncProfileDuration = (
556
+ profile: SyncProfileFn | undefined,
557
+ startedAt: number,
558
+ event: Parameters<typeof emitSyncProfileDuration>[2],
559
+ ): void => {
560
+ try {
561
+ emitSyncProfileDuration(profile, startedAt, event);
562
+ } catch {
563
+ // Diagnostics must not change open or provider-resolution correctness.
564
+ }
565
+ };
566
+
474
567
  const canUseOptionalNativeModuleImports = (): boolean => {
475
568
  const scope = globalThis as {
476
569
  ServiceWorkerGlobalScope?: unknown;
@@ -3979,6 +4072,7 @@ export class SharedLog<
3979
4072
  peerSession: PeerSession;
3980
4073
  receiveEpoch: object | null;
3981
4074
  signal: AbortSignal;
4075
+ requestRemoteFullRearm?: boolean;
3982
4076
  }): Promise<
3983
4077
  { receiverTransportSession: bigint; requestNotBeforeMs: number } | undefined
3984
4078
  > {
@@ -3986,7 +4080,11 @@ export class SharedLog<
3986
4080
  const receiverTransportSession = this.ownTransportSession();
3987
4081
  await this.rpc.send(
3988
4082
  new SyncCapabilitiesMessage({
3989
- capabilities: this.replicationInfoV2ReceiveCapabilities(),
4083
+ capabilities:
4084
+ this.replicationInfoV2ReceiveCapabilities() |
4085
+ (properties.requestRemoteFullRearm
4086
+ ? SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM
4087
+ : 0),
3990
4088
  }),
3991
4089
  {
3992
4090
  mode: new AcknowledgeDelivery({
@@ -4053,6 +4151,7 @@ export class SharedLog<
4053
4151
  peerSession: properties.peerSession as PeerSession,
4054
4152
  receiveEpoch: properties.receiveEpoch,
4055
4153
  signal: properties.signal,
4154
+ requestRemoteFullRearm: properties.requestRemoteFullRearm,
4056
4155
  }),
4057
4156
  onRequestError: (error) => {
4058
4157
  if (
@@ -4341,16 +4440,38 @@ export class SharedLog<
4341
4440
  });
4342
4441
  channel.addEventListener("unicast", this._onFanoutUnicastFn);
4343
4442
 
4443
+ const profile = this._logProperties?.sync?.profile;
4444
+ const startedAt = syncProfileStart(profile);
4445
+ const mode =
4446
+ resolvedRoot === fanoutService.publicKeyHash ? "root" : "node";
4447
+ const before = profile
4448
+ ? snapshotFanoutOpenMetrics(fanoutService, this.topic, resolvedRoot)
4449
+ : undefined;
4450
+ let outcome: "error" | "opened" | "joined" = "error";
4344
4451
  try {
4345
4452
  const channelOptions = this.getFanoutChannelOptions(options);
4346
- if (resolvedRoot === fanoutService.publicKeyHash) {
4453
+ if (mode === "root") {
4347
4454
  await channel.openAsRoot(channelOptions);
4455
+ outcome = "opened";
4348
4456
  return;
4349
4457
  }
4350
4458
  await channel.join(channelOptions, options.join);
4459
+ outcome = "joined";
4351
4460
  } catch (error) {
4352
4461
  this._closeFanoutChannel();
4353
4462
  throw error;
4463
+ } finally {
4464
+ emitFanoutOpenProfile({
4465
+ profile,
4466
+ startedAt,
4467
+ service: fanoutService,
4468
+ topic: this.topic,
4469
+ root: resolvedRoot,
4470
+ mode,
4471
+ outcome,
4472
+ timeoutMs: options.join?.timeoutMs,
4473
+ before,
4474
+ });
4354
4475
  }
4355
4476
  }
4356
4477
 
@@ -5737,7 +5858,13 @@ export class SharedLog<
5737
5858
 
5738
5859
  private persistedReceiptPeerSession(
5739
5860
  peerHash: string,
5740
- ): { capabilitySession: bigint; peerSession: PeerSession } | undefined {
5861
+ ):
5862
+ | {
5863
+ capabilitySession: bigint;
5864
+ peerSession: PeerSession;
5865
+ receiveEpoch: object | null;
5866
+ }
5867
+ | undefined {
5741
5868
  // This is a hot receipt/transfer-loop predicate. Keep it allocation-light,
5742
5869
  // while mirroring every exact-session gate in
5743
5870
  // persistedReceiptReadinessCandidate (which additionally creates public
@@ -5770,7 +5897,7 @@ export class SharedLog<
5770
5897
  ) {
5771
5898
  return undefined;
5772
5899
  }
5773
- return { capabilitySession, peerSession };
5900
+ return { capabilitySession, peerSession, receiveEpoch };
5774
5901
  }
5775
5902
 
5776
5903
  private async waitPersistedReceiptRetry(
@@ -17088,6 +17215,8 @@ export class SharedLog<
17088
17215
  (this.node as unknown as NodeWithSharedLogNativeDefaults)
17089
17216
  .sharedLogNativeDefaults,
17090
17217
  );
17218
+ const openProfile = options?.sync?.profile;
17219
+ const openStartedAt = syncProfileStart(openProfile);
17091
17220
  this.replicas = {
17092
17221
  min:
17093
17222
  options?.replicas?.min != null
@@ -17318,6 +17447,7 @@ export class SharedLog<
17318
17447
  this.keep = options?.keep;
17319
17448
  this.pendingMaturity = new Map();
17320
17449
 
17450
+ const localStateStartedAt = syncProfileStart(openProfile);
17321
17451
  const id = sha256Base64Sync(this.log.id);
17322
17452
  const [storage, logScope] = await Promise.all([
17323
17453
  this.node.storage.sublevel(id),
@@ -17373,6 +17503,11 @@ export class SharedLog<
17373
17503
  this._entryCoordinatesIndex = await replicationIndex.init({
17374
17504
  schema: this.indexableDomain.constructorEntry,
17375
17505
  });
17506
+ emitAdvisorySyncProfileDuration(openProfile, localStateStartedAt, {
17507
+ name: "sharedLog.open.localState",
17508
+ component: "shared-log",
17509
+ });
17510
+ const blockStoreStartedAt = syncProfileStart(openProfile);
17376
17511
  const deferStandaloneNativeRangePlanner =
17377
17512
  !!options?.nativeBackbone && options.nativeRangePlanner == null;
17378
17513
  await this.openNativeRangePlanner(
@@ -17423,6 +17558,14 @@ export class SharedLog<
17423
17558
  storage as unknown as DurableBlockSublevelStore,
17424
17559
  );
17425
17560
  }
17561
+ emitAdvisorySyncProfileDuration(openProfile, blockStoreStartedAt, {
17562
+ name: "sharedLog.open.blockStore",
17563
+ component: "shared-log",
17564
+ details: {
17565
+ nativeBackbone: this._nativeBackbone != null,
17566
+ directoryConfigured: this.node.directory != null,
17567
+ },
17568
+ });
17426
17569
  this.remoteBlocks = new RemoteBlocks({
17427
17570
  local: localBlocks,
17428
17571
  publish: (message, options) =>
@@ -17433,38 +17576,76 @@ export class SharedLog<
17433
17576
  // compatible eager path with bounded validation and storage budgets.
17434
17577
  eagerBlocks: options?.eagerBlocks ?? false,
17435
17578
  resolveProviders: async (cid, opts) => {
17579
+ const profile = this._logProperties?.sync?.profile;
17436
17580
  const maxPeers = 8;
17581
+ const excluded = new Set((opts?.exclude ?? []).slice(0, maxPeers));
17582
+ const lookupPeers = opts?.refresh
17583
+ ? Math.min(maxPeers * 2, maxPeers + excluded.size)
17584
+ : maxPeers;
17585
+ const resolutionStartedAt = syncProfileStart(profile);
17437
17586
  const localCandidates =
17438
17587
  (await this.resolveCandidatePeersForHash(cid, {
17439
17588
  signal: opts?.signal,
17440
- maxPeers,
17589
+ maxPeers: lookupPeers,
17441
17590
  })) ?? [];
17442
- if (opts?.signal?.aborted) return [];
17591
+ const emitResolution = profile
17592
+ ? (
17593
+ status: "aborted" | "local" | "directory",
17594
+ targets: number,
17595
+ directoryCandidates = 0,
17596
+ reachableCandidates = 0,
17597
+ ) =>
17598
+ emitAdvisorySyncProfileDuration(profile, resolutionStartedAt, {
17599
+ name: "sharedLog.blocks.resolveProviders",
17600
+ component: "shared-log",
17601
+ count: targets,
17602
+ targets,
17603
+ details: {
17604
+ status,
17605
+ refresh: opts?.refresh === true,
17606
+ excluded: excluded.size,
17607
+ lookupPeers,
17608
+ localCandidates: localCandidates.length,
17609
+ directoryCandidates,
17610
+ reachableCandidates,
17611
+ },
17612
+ })
17613
+ : undefined;
17614
+ if (opts?.signal?.aborted) {
17615
+ emitResolution?.("aborted", 0);
17616
+ return [];
17617
+ }
17443
17618
  const locallyReachable = new Set(
17444
17619
  await this._getLocalReachablePeerHashes(this.topic),
17445
17620
  );
17621
+ if (opts?.signal?.aborted) {
17622
+ emitResolution?.("aborted", 0, 0, locallyReachable.size);
17623
+ return [];
17624
+ }
17446
17625
  const confirmed = this._checkedPrune.getConfirmedReplicators(cid);
17447
17626
  const contacted = this._checkedPrune.getContactedReplicators(cid);
17627
+ const hasProviderEvidence = (peer: string) =>
17628
+ confirmed?.has(peer) === true ||
17629
+ contacted?.has(peer) ||
17630
+ this.uniqueReplicators.has(peer);
17448
17631
  const hasLiveCandidate = localCandidates.some(
17449
- (peer) =>
17450
- locallyReachable.has(peer) &&
17451
- (confirmed?.has(peer) ||
17452
- contacted?.has(peer) ||
17453
- this.uniqueReplicators.has(peer)),
17632
+ (peer) => locallyReachable.has(peer) && hasProviderEvidence(peer),
17454
17633
  );
17455
17634
 
17456
17635
  // Only reachability corroborated by provider/replicator evidence may
17457
17636
  // bypass the initial CID lookup. Arbitrary bootstrap connections are
17458
17637
  // useful fallbacks, but are not evidence that they hold this block.
17459
17638
  if (hasLiveCandidate && !opts?.refresh) {
17460
- return localCandidates;
17639
+ const selected = localCandidates.slice(0, maxPeers);
17640
+ emitResolution?.("local", selected.length, 0, locallyReachable.size);
17641
+ return selected;
17461
17642
  }
17462
17643
 
17463
17644
  let directoryProviders: string[] = [];
17464
17645
  try {
17465
17646
  const query = (namespace: string) =>
17466
17647
  fanoutService?.queryProviders(namespace, {
17467
- want: maxPeers,
17648
+ want: lookupPeers,
17468
17649
  timeoutMs: 2_000,
17469
17650
  queryTimeoutMs: 500,
17470
17651
  bootstrapMaxPeers: 2,
@@ -17476,12 +17657,21 @@ export class SharedLog<
17476
17657
  ]);
17477
17658
  for (const result of results) {
17478
17659
  if (result.status === "fulfilled") {
17479
- directoryProviders.push(...result.value);
17660
+ directoryProviders.push(...result.value.slice(0, lookupPeers));
17480
17661
  }
17481
17662
  }
17482
17663
  } catch {
17483
17664
  // Ignore discovery failures; local evidence remains usable.
17484
17665
  }
17666
+ if (opts?.signal?.aborted) {
17667
+ emitResolution?.(
17668
+ "aborted",
17669
+ 0,
17670
+ directoryProviders.length,
17671
+ locallyReachable.size,
17672
+ );
17673
+ return [];
17674
+ }
17485
17675
 
17486
17676
  const selected: string[] = [];
17487
17677
  const selectedSet = new Set<string>();
@@ -17492,15 +17682,62 @@ export class SharedLog<
17492
17682
  selectedSet.add(peer);
17493
17683
  selected.push(peer);
17494
17684
  };
17495
- for (
17496
- let index = 0;
17497
- selected.length < maxPeers &&
17498
- (index < localCandidates.length || index < directoryProviders.length);
17499
- index++
17500
- ) {
17501
- add(localCandidates[index]);
17502
- add(directoryProviders[index]);
17503
- }
17685
+ const append = (
17686
+ providers: readonly string[],
17687
+ includeExcluded: boolean,
17688
+ predicate?: (provider: string) => boolean,
17689
+ ) => {
17690
+ for (const provider of providers) {
17691
+ if (selected.length >= maxPeers) return;
17692
+ if (
17693
+ excluded.has(provider) === includeExcluded &&
17694
+ (!predicate || predicate(provider))
17695
+ ) {
17696
+ add(provider);
17697
+ }
17698
+ }
17699
+ };
17700
+ const appendInterleaved = (includeExcluded: boolean) => {
17701
+ for (
17702
+ let index = 0;
17703
+ selected.length < maxPeers &&
17704
+ (index < localCandidates.length ||
17705
+ index < directoryProviders.length);
17706
+ index++
17707
+ ) {
17708
+ const local = localCandidates[index];
17709
+ if (local && excluded.has(local) === includeExcluded) add(local);
17710
+ const directory = directoryProviders[index];
17711
+ if (directory && excluded.has(directory) === includeExcluded) {
17712
+ add(directory);
17713
+ }
17714
+ }
17715
+ };
17716
+ if (opts?.refresh) {
17717
+ // Retry results are wider than the regular eight-peer window. Prefer
17718
+ // untried reachable holders, then the remaining fresh directory
17719
+ // evidence, without discarding attempted peers as bounded transient-
17720
+ // failure fallbacks.
17721
+ append(directoryProviders, false, (peer) =>
17722
+ locallyReachable.has(peer),
17723
+ );
17724
+ append(
17725
+ localCandidates,
17726
+ false,
17727
+ (peer) => locallyReachable.has(peer) && hasProviderEvidence(peer),
17728
+ );
17729
+ append(directoryProviders, false);
17730
+ append(localCandidates, false);
17731
+ } else {
17732
+ appendInterleaved(false);
17733
+ }
17734
+ appendInterleaved(true);
17735
+ emitResolution?.(
17736
+ "directory",
17737
+ selected.length,
17738
+ directoryProviders.length,
17739
+ locallyReachable.size,
17740
+ );
17504
17741
  return selected;
17505
17742
  },
17506
17743
  watchProviders: fanoutService
@@ -17548,7 +17785,13 @@ export class SharedLog<
17548
17785
  : undefined,
17549
17786
  });
17550
17787
 
17551
- const remoteBlocksStartPromise = this.remoteBlocks.start();
17788
+ const remoteBlocksStartedAt = syncProfileStart(openProfile);
17789
+ const remoteBlocksStartPromise = this.remoteBlocks.start().then(() => {
17790
+ emitAdvisorySyncProfileDuration(openProfile, remoteBlocksStartedAt, {
17791
+ name: "sharedLog.open.remoteBlocks",
17792
+ component: "shared-log",
17793
+ });
17794
+ });
17552
17795
  const hasIndexedReplicationInfo =
17553
17796
  (await this.replicationIndex.count({
17554
17797
  query: [
@@ -17764,6 +18007,7 @@ export class SharedLog<
17764
18007
  // joins rely on: a replicate:false observer syncing a head whose parents
17765
18008
  // are not local would fail block resolution, and Log.join treats that as
17766
18009
  // recoverable and skips the entry without persisting anything.
18010
+ const lowerLogStartedAt = syncProfileStart(openProfile);
17767
18011
  await this.log.open(this.remoteBlocks, this.node.identity, {
17768
18012
  keychain: this.node.services.keychain,
17769
18013
  resolveRemotePeers: (hash, options) =>
@@ -17795,6 +18039,10 @@ export class SharedLog<
17795
18039
  },
17796
18040
  indexer: logIndex,
17797
18041
  });
18042
+ emitAdvisorySyncProfileDuration(openProfile, lowerLogStartedAt, {
18043
+ name: "sharedLog.open.lowerLog",
18044
+ component: "shared-log",
18045
+ });
17798
18046
  this._persistedReceiptStorage = this.resolvePersistedReceiptStorage();
17799
18047
  try {
17800
18048
  const recovered =
@@ -17859,6 +18107,7 @@ export class SharedLog<
17859
18107
  this._onUnsubscription(event),
17860
18108
  );
17861
18109
  });
18110
+ const communicationStartedAt = syncProfileStart(openProfile);
17862
18111
  await Promise.all([
17863
18112
  this.rpc.open({
17864
18113
  queryType: TransportMessage,
@@ -17877,7 +18126,12 @@ export class SharedLog<
17877
18126
  this._onUnsubscriptionFn,
17878
18127
  ),
17879
18128
  ]);
18129
+ emitAdvisorySyncProfileDuration(openProfile, communicationStartedAt, {
18130
+ name: "sharedLog.open.rpcSubscriptions",
18131
+ component: "shared-log",
18132
+ });
17880
18133
 
18134
+ const providerChannelStartedAt = syncProfileStart(openProfile);
17881
18135
  const fanoutOpenPromise = this._openFanoutChannel(options?.fanout);
17882
18136
  // Mark previously-owned replication ranges as "new" only when they already exist.
17883
18137
  // Fresh opens have nothing to touch here, so skip the extra scan/write entirely.
@@ -17885,6 +18139,11 @@ export class SharedLog<
17885
18139
  ? this.updateTimestampOfOwnedReplicationRanges()
17886
18140
  : Promise.resolve();
17887
18141
  await Promise.all([fanoutOpenPromise, updateOwnedReplicationPromise]);
18142
+ emitAdvisorySyncProfileDuration(openProfile, providerChannelStartedAt, {
18143
+ name: "sharedLog.open.providerAndOwnership",
18144
+ component: "shared-log",
18145
+ details: { indexedReplicationInfo: hasIndexedReplicationInfo },
18146
+ });
17888
18147
 
17889
18148
  // if we had a previous session with replication info, and new replication info dictates that we unreplicate
17890
18149
  // we should do that. Otherwise if options is a unreplication we dont need to do anything because
@@ -17902,17 +18161,35 @@ export class SharedLog<
17902
18161
  this.node.identity.publicKey,
17903
18162
  ));
17904
18163
 
18164
+ const replicationStartedAt = syncProfileStart(openProfile);
18165
+ let replicationAction: "replace" | "resume" | "reset";
17905
18166
  if (hasIndexedReplicationInfo && isUnreplicationOptionsDefined) {
18167
+ replicationAction = "replace";
17906
18168
  await this.replicate(options?.replicate, { checkDuplicates: true });
17907
18169
  } else if (canResumeReplication) {
18170
+ replicationAction = "resume";
17908
18171
  // dont do anthing since we are alread replicating stuff
17909
18172
  } else {
18173
+ replicationAction = "reset";
17910
18174
  await this.replicate(options?.replicate, {
17911
18175
  checkDuplicates: true,
17912
18176
  reset: true,
17913
18177
  });
17914
18178
  }
18179
+ emitAdvisorySyncProfileDuration(openProfile, replicationStartedAt, {
18180
+ name: "sharedLog.open.replication",
18181
+ component: "shared-log",
18182
+ details: {
18183
+ hadIndexedState: hasIndexedReplicationInfo,
18184
+ action: replicationAction,
18185
+ },
18186
+ });
18187
+ const synchronizerStartedAt = syncProfileStart(openProfile);
17915
18188
  await this.syncronizer.open();
18189
+ emitAdvisorySyncProfileDuration(openProfile, synchronizerStartedAt, {
18190
+ name: "sharedLog.open.synchronizer",
18191
+ component: "shared-log",
18192
+ });
17916
18193
 
17917
18194
  this.interval = setInterval(() => {
17918
18195
  void this.rebalanceParticipationDebounced?.call();
@@ -17920,6 +18197,10 @@ export class SharedLog<
17920
18197
 
17921
18198
  this._instanceLifecycle!.markOpenComplete();
17922
18199
  this.scheduleReplicationStatusRefresh();
18200
+ emitAdvisorySyncProfileDuration(openProfile, openStartedAt, {
18201
+ name: "sharedLog.open.total",
18202
+ component: "shared-log",
18203
+ });
17923
18204
  }
17924
18205
 
17925
18206
  private toNativeReplicationRange(
@@ -22900,6 +23181,17 @@ export class SharedLog<
22900
23181
  if (!context.from.equals(this.node.identity.publicKey)) {
22901
23182
  const capabilityTransportSession = context.message?.header?.session;
22902
23183
  const capabilityTimestamp = context.message?.header?.timestamp;
23184
+ const previousCapabilitySession =
23185
+ this._peerSyncCapabilitySessions.get(receiveFromHash);
23186
+ const previousCapabilityTimestamp =
23187
+ this._peerSyncCapabilityTimestamps.get(receiveFromHash);
23188
+ const requestsRemoteFullRearm =
23189
+ (msg.capabilities & SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM) !==
23190
+ 0;
23191
+ // The rearm bit is a one-shot authenticated command, not a sticky
23192
+ // negotiated capability. Store and promote only the steady bits.
23193
+ const steadyCapabilities =
23194
+ msg.capabilities & ~SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM;
22903
23195
  // No await separates this from the capture above, so the captured
22904
23196
  // window state is exact: the legacy re-read of the opening map here
22905
23197
  // could never observe a different value.
@@ -22912,7 +23204,7 @@ export class SharedLog<
22912
23204
  // cleanup cannot erase it before the opening transition commits.
22913
23205
  this.observePeerSyncCapabilities({
22914
23206
  peerHash: receiveFromHash,
22915
- capabilities: msg.capabilities,
23207
+ capabilities: steadyCapabilities,
22916
23208
  transportSession: capabilityTransportSession,
22917
23209
  timestamp: capabilityTimestamp,
22918
23210
  openingSession: receiveSession!,
@@ -22920,7 +23212,7 @@ export class SharedLog<
22920
23212
  } else {
22921
23213
  const observed = this.observePeerSyncCapabilities({
22922
23214
  peerHash: receiveFromHash,
22923
- capabilities: msg.capabilities,
23215
+ capabilities: steadyCapabilities,
22924
23216
  transportSession: capabilityTransportSession,
22925
23217
  timestamp: capabilityTimestamp,
22926
23218
  });
@@ -22929,6 +23221,33 @@ export class SharedLog<
22929
23221
  context.from,
22930
23222
  receiveSession,
22931
23223
  );
23224
+ const freshExactRearm =
23225
+ requestsRemoteFullRearm &&
23226
+ capabilityTransportSession !== undefined &&
23227
+ capabilityTimestamp !== undefined &&
23228
+ previousCapabilitySession === capabilityTransportSession &&
23229
+ previousCapabilityTimestamp !== undefined &&
23230
+ capabilityTimestamp > previousCapabilityTimestamp;
23231
+ if (
23232
+ freshExactRearm &&
23233
+ this._v2Receive.isCurrentActive({
23234
+ peerHash: receiveFromHash,
23235
+ peerSession: receiveSession,
23236
+ receiveEpoch:
23237
+ this._peerSessions.receiveEpoch(receiveFromHash),
23238
+ senderTransportSession: capabilityTransportSession,
23239
+ })
23240
+ ) {
23241
+ // Rotate the receiver grant/challenge before requesting Full. A
23242
+ // sender rebuilt from no state starts at sequence one, which an
23243
+ // active receiver's old sequence fence must otherwise reject.
23244
+ this._v2Receive.advanceRecovery({
23245
+ peerHash: receiveFromHash,
23246
+ peerSession: receiveSession,
23247
+ receiveEpoch:
23248
+ this._peerSessions.receiveEpoch(receiveFromHash),
23249
+ });
23250
+ }
22932
23251
  } else if (observed && receiveSession === null) {
22933
23252
  // A capability can arrive before the sender's topic Subscribe after
22934
23253
  // reconnect. Ask that authenticated peer for its authoritative
@@ -24059,8 +24378,11 @@ export class SharedLog<
24059
24378
  throwIfInactive();
24060
24379
  }
24061
24380
 
24062
- private nudgePersistedReceiptPeerReadiness(publicKey: PublicSignKey): void {
24063
- if (this.closed) return;
24381
+ private nudgePersistedReceiptPeerReadiness(
24382
+ publicKey: PublicSignKey,
24383
+ signal: AbortSignal,
24384
+ ): void {
24385
+ if (this.closed || signal.aborted) return;
24064
24386
  const peerHash = publicKey.hashcode();
24065
24387
  const peerSession = this._peerSessions.current(peerHash);
24066
24388
  if (
@@ -24091,6 +24413,22 @@ export class SharedLog<
24091
24413
  peerSession,
24092
24414
  receiveEpoch,
24093
24415
  });
24416
+ const receiptTarget = this.persistedReceiptPeerSession(peerHash);
24417
+ if (
24418
+ receiptTarget &&
24419
+ !this._v2Send.hasCurrentStateForPeer({
24420
+ peerHash,
24421
+ peerSession: receiptTarget.peerSession,
24422
+ receiverTransportSession: receiptTarget.capabilitySession,
24423
+ })
24424
+ ) {
24425
+ this._v2Receive.reAdvertiseLocalCapabilityForRemoteFull({
24426
+ peerHash,
24427
+ peerSession: receiptTarget.peerSession,
24428
+ receiveEpoch: receiptTarget.receiveEpoch,
24429
+ signal,
24430
+ });
24431
+ }
24094
24432
  this.scheduleReplicationInfoV2Recovery(publicKey);
24095
24433
  }
24096
24434
 
@@ -24359,18 +24697,26 @@ export class SharedLog<
24359
24697
  throw new AbortError("Persisted-receipt readiness wait settled");
24360
24698
  }
24361
24699
  };
24700
+ const recoveryDelayMs = Math.max(
24701
+ 50,
24702
+ Math.min(1_000, this.waitForReplicatorRequestIntervalMs),
24703
+ );
24362
24704
  const armRecoveryTick = () => {
24363
24705
  if (settled || recoveryTimer) return;
24364
- const delayMs = Math.max(
24365
- 50,
24366
- Math.min(1_000, this.waitForReplicatorRequestIntervalMs),
24367
- );
24368
24706
  recoveryTimer = setTimeout(() => {
24369
24707
  recoveryTimer = undefined;
24370
24708
  if (!continueWait()) return;
24371
- this.nudgePersistedReceiptPeerReadiness(key);
24709
+ this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24710
+ if (checkInFlight) {
24711
+ // Confirmation can legitimately span several recovery intervals.
24712
+ // Keep repairing the exact current generation without aborting its
24713
+ // one application-confirmation waiter on every tick.
24714
+ rerun = true;
24715
+ armRecoveryTick();
24716
+ return;
24717
+ }
24372
24718
  scheduleCheck();
24373
- }, delayMs);
24719
+ }, recoveryDelayMs);
24374
24720
  recoveryTimer.unref?.();
24375
24721
  };
24376
24722
  const runCheck = async () => {
@@ -24403,6 +24749,11 @@ export class SharedLog<
24403
24749
  ) {
24404
24750
  const target = this.persistedReceiptPeerSession(peerHash);
24405
24751
  if (target) {
24752
+ // A confirmation waiter cannot create an outbound V2 stream when
24753
+ // that exact session state is missing. Nudge first, then keep the
24754
+ // recovery tick alive while the single confirmation wait remains.
24755
+ this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24756
+ armRecoveryTick();
24406
24757
  const currentConfirmationController = new AbortController();
24407
24758
  confirmationController = currentConfirmationController;
24408
24759
  try {
@@ -24447,7 +24798,7 @@ export class SharedLog<
24447
24798
  }
24448
24799
  }
24449
24800
  if (!continueWait()) return;
24450
- this.nudgePersistedReceiptPeerReadiness(key);
24801
+ this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24451
24802
  } catch (error) {
24452
24803
  if (!settled) reject(error);
24453
24804
  } finally {