@peerbit/shared-log 16.0.22 → 16.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -81,6 +81,54 @@ import { emitSyncProfileDuration, emitSyncProfileEvent, syncProfileStart, } from
81
81
  import { ConfirmEntriesMessage, RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS, RequestPersistedEntriesV1, SYNC_MESSAGE_PRIORITY, SimpleSyncronizer, } from "./sync/simple.js";
82
82
  import { groupByGid, tryGroupByGidSync } from "./utils.js";
83
83
  const getSharedLogFanoutService = (services) => services.fanout;
84
+ const FANOUT_OPEN_METRICS = [
85
+ ["joinReqSent", "joinReqSent"],
86
+ ["joinAcceptReceived", "joinAcceptReceived"],
87
+ ["joinRejectReceived", "joinRejectReceived"],
88
+ ["bootstrapDialAttempts", "joinBootstrapDialAttempts"],
89
+ ["bootstrapDialFailures", "joinBootstrapDialFailures"],
90
+ ["candidateDialAttempts", "joinCandidateDialAttempts"],
91
+ ["candidateDialFailures", "joinCandidateDialFailures"],
92
+ ["connectedCandidateAttempts", "joinConnectedCandidateAttempts"],
93
+ ["unconnectedCandidateAttempts", "joinUnconnectedCandidateAttempts"],
94
+ ["joinReqTimeouts", "joinReqTimeouts"],
95
+ ["deadlineExpirations", "joinDeadlineExpirations"],
96
+ ];
97
+ const snapshotFanoutOpenMetrics = (service, topic, root) => {
98
+ try {
99
+ const metrics = service.getChannelMetrics(topic, root);
100
+ return Object.fromEntries(FANOUT_OPEN_METRICS.map(([name, source]) => [name, metrics[source] ?? 0]));
101
+ }
102
+ catch {
103
+ return;
104
+ }
105
+ };
106
+ const emitFanoutOpenProfile = (properties) => {
107
+ if (!properties.profile)
108
+ return;
109
+ try {
110
+ const after = snapshotFanoutOpenMetrics(properties.service, properties.topic, properties.root);
111
+ const deltas = Object.fromEntries(FANOUT_OPEN_METRICS.map(([name]) => [
112
+ name,
113
+ (after?.[name] ?? 0) - (properties.before?.[name] ?? 0),
114
+ ]));
115
+ emitSyncProfileDuration(properties.profile, properties.startedAt, {
116
+ name: "sharedLog.open.fanout",
117
+ component: "shared-log",
118
+ messages: deltas.joinReqSent,
119
+ details: {
120
+ configured: true,
121
+ mode: properties.mode,
122
+ outcome: properties.outcome,
123
+ configuredTimeoutMs: properties.timeoutMs,
124
+ ...deltas,
125
+ },
126
+ });
127
+ }
128
+ catch {
129
+ // Diagnostics must not affect open correctness.
130
+ }
131
+ };
84
132
  const createOneShotPeerReceiveLease = (releaseFn) => {
85
133
  let released = false;
86
134
  return {
@@ -133,6 +181,14 @@ export { ExchangeHeadsMessage, RawExchangeHeadsMessage, StashBackedRawExchangeHe
133
181
  export const logger = loggerFn("peerbit:shared-log");
134
182
  const warn = logger.newScope("warn");
135
183
  const traceLogger = logger.trace;
184
+ const emitAdvisorySyncProfileDuration = (profile, startedAt, event) => {
185
+ try {
186
+ emitSyncProfileDuration(profile, startedAt, event);
187
+ }
188
+ catch {
189
+ // Diagnostics must not change open or provider-resolution correctness.
190
+ }
191
+ };
136
192
  const canUseOptionalNativeModuleImports = () => {
137
193
  const scope = globalThis;
138
194
  const serviceWorkerGlobalScope = scope.ServiceWorkerGlobalScope;
@@ -575,6 +631,7 @@ const PERSISTED_RECEIPT_RETRY_MS = 50;
575
631
  const MAX_PERSISTED_RECEIPT_ATTEMPT_MS = 2_000;
576
632
  const MAX_PERSISTED_RECEIPT_REQUESTS_GLOBAL = 8;
577
633
  const MAX_PERSISTED_RECEIPT_REQUESTS_PER_PEER = 2;
634
+ const MAX_PERSISTED_RECEIPT_READINESS_WAITERS = 1_024;
578
635
  const PERSISTED_RECEIPT_INGRESS_PEER_REQUEST_CAPACITY = 16;
579
636
  const PERSISTED_RECEIPT_INGRESS_PEER_HASH_CAPACITY = 8_192;
580
637
  const PERSISTED_RECEIPT_INGRESS_PEER_REQUESTS_PER_SECOND = 8;
@@ -1847,6 +1904,16 @@ let SharedLog = (() => {
1847
1904
  // parallel map so existing capability-number consumers remain unchanged.
1848
1905
  _peerSyncCapabilitySessions;
1849
1906
  _peerSyncCapabilityTimestamps;
1907
+ // design-note: these fields cache a stable, public diagnostics token for the
1908
+ // composite of PeerSession identity, receive epoch, and signed capability
1909
+ // session. They are not consulted to admit or fence asynchronous work. A
1910
+ // separate opaque token is necessary because exposing any of those internal
1911
+ // identities would leak protocol/session values, while PeerSession alone does
1912
+ // not change when receive or capability state is replaced.
1913
+ _persistedReceiptReadinessGenerations;
1914
+ _persistedReceiptReadinessGenerationPrefix;
1915
+ _persistedReceiptReadinessGenerationCounter;
1916
+ _persistedReceiptReadinessWaiters;
1850
1917
  _persistedReceiptStorage;
1851
1918
  _persistedReceiptRequestsInFlight;
1852
1919
  _persistedReceiptRequestsInFlightTotal;
@@ -2121,6 +2188,10 @@ let SharedLog = (() => {
2121
2188
  this._peerSyncCapabilities = new Map();
2122
2189
  this._peerSyncCapabilitySessions = new Map();
2123
2190
  this._peerSyncCapabilityTimestamps = new Map();
2191
+ this._persistedReceiptReadinessGenerations = new WeakMap();
2192
+ this._persistedReceiptReadinessGenerationPrefix = toHexString(randomBytes(8));
2193
+ this._persistedReceiptReadinessGenerationCounter = 0;
2194
+ this._persistedReceiptReadinessWaiters = new Set();
2124
2195
  this._persistedReceiptStorage = undefined;
2125
2196
  this._persistedReceiptRequestsInFlight = new Map();
2126
2197
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -2237,18 +2308,40 @@ let SharedLog = (() => {
2237
2308
  void this._onFanoutUnicast(detail).catch((error) => logger.error(error));
2238
2309
  });
2239
2310
  channel.addEventListener("unicast", this._onFanoutUnicastFn);
2311
+ const profile = this._logProperties?.sync?.profile;
2312
+ const startedAt = syncProfileStart(profile);
2313
+ const mode = resolvedRoot === fanoutService.publicKeyHash ? "root" : "node";
2314
+ const before = profile
2315
+ ? snapshotFanoutOpenMetrics(fanoutService, this.topic, resolvedRoot)
2316
+ : undefined;
2317
+ let outcome = "error";
2240
2318
  try {
2241
2319
  const channelOptions = this.getFanoutChannelOptions(options);
2242
- if (resolvedRoot === fanoutService.publicKeyHash) {
2320
+ if (mode === "root") {
2243
2321
  await channel.openAsRoot(channelOptions);
2322
+ outcome = "opened";
2244
2323
  return;
2245
2324
  }
2246
2325
  await channel.join(channelOptions, options.join);
2326
+ outcome = "joined";
2247
2327
  }
2248
2328
  catch (error) {
2249
2329
  this._closeFanoutChannel();
2250
2330
  throw error;
2251
2331
  }
2332
+ finally {
2333
+ emitFanoutOpenProfile({
2334
+ profile,
2335
+ startedAt,
2336
+ service: fanoutService,
2337
+ topic: this.topic,
2338
+ root: resolvedRoot,
2339
+ mode,
2340
+ outcome,
2341
+ timeoutMs: options.join?.timeoutMs,
2342
+ before,
2343
+ });
2344
+ }
2252
2345
  }
2253
2346
  _closeFanoutChannel() {
2254
2347
  if (this._fanoutChannel) {
@@ -2898,14 +2991,20 @@ let SharedLog = (() => {
2898
2991
  timestamp < previous.timestamp) {
2899
2992
  return false;
2900
2993
  }
2994
+ const nextCapabilities = previous.capabilities | capabilities;
2995
+ const nextTimestamp = previous.timestamp === undefined || timestamp > previous.timestamp
2996
+ ? timestamp
2997
+ : previous.timestamp;
2901
2998
  this._openingSyncCapabilitiesByPeer.set(peerHash, {
2902
2999
  epoch: openingSession,
2903
- capabilities: previous.capabilities | capabilities,
3000
+ capabilities: nextCapabilities,
2904
3001
  transportSession,
2905
- timestamp: previous.timestamp === undefined || timestamp > previous.timestamp
2906
- ? timestamp
2907
- : previous.timestamp,
3002
+ timestamp: nextTimestamp,
2908
3003
  });
3004
+ if (previous.capabilities !== nextCapabilities ||
3005
+ previous.timestamp === undefined) {
3006
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
3007
+ }
2909
3008
  return true;
2910
3009
  }
2911
3010
  this._openingSyncCapabilitiesByPeer.set(peerHash, {
@@ -2914,16 +3013,23 @@ let SharedLog = (() => {
2914
3013
  transportSession,
2915
3014
  timestamp,
2916
3015
  });
3016
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
2917
3017
  return true;
2918
3018
  }
2919
3019
  if (transportSession === undefined || timestamp === undefined) {
2920
3020
  // Test/in-process synthetic contexts predate signed envelope captures.
2921
3021
  // They may exercise capability-number behavior, but can never authorize V2.
3022
+ const readinessChanged = this._peerSyncCapabilities.get(peerHash) !== capabilities ||
3023
+ this._peerSyncCapabilitySessions.has(peerHash) ||
3024
+ this._peerSyncCapabilityTimestamps.has(peerHash);
2922
3025
  this._peerSyncCapabilities.set(peerHash, capabilities);
2923
3026
  this._peerSyncCapabilitySessions.delete(peerHash);
2924
3027
  this._peerSyncCapabilityTimestamps.delete(peerHash);
2925
3028
  this._v2Send.advancePeerCapability(peerHash);
2926
3029
  this._v2Receive.revokePeerCapability(peerHash);
3030
+ if (readinessChanged) {
3031
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
3032
+ }
2927
3033
  return true;
2928
3034
  }
2929
3035
  const previousSession = this._peerSyncCapabilitySessions.get(peerHash);
@@ -2943,6 +3049,9 @@ let SharedLog = (() => {
2943
3049
  const generationAdvanced = !sameTransportSession ||
2944
3050
  (previousCapabilities & senderGrantCapabilityMask) !==
2945
3051
  (nextCapabilities & senderGrantCapabilityMask);
3052
+ const readinessChanged = !sameTransportSession ||
3053
+ previousTimestamp === undefined ||
3054
+ previousCapabilities !== nextCapabilities;
2946
3055
  this._peerSyncCapabilities.set(peerHash, nextCapabilities);
2947
3056
  this._peerSyncCapabilitySessions.set(peerHash, transportSession);
2948
3057
  this._peerSyncCapabilityTimestamps.set(peerHash, previousTimestamp === undefined ||
@@ -2956,6 +3065,9 @@ let SharedLog = (() => {
2956
3065
  // recovery re-solicitation may restart from the base interval.
2957
3066
  this.resetReplicationInfoV2RecoveryEscalation(peerHash);
2958
3067
  }
3068
+ if (readinessChanged) {
3069
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
3070
+ }
2959
3071
  return true;
2960
3072
  }
2961
3073
  promoteReplicationInfoV2ReceiveCapability(target, peerSession) {
@@ -3227,16 +3339,102 @@ let SharedLog = (() => {
3227
3339
  }
3228
3340
  return this.sendFusedRawExchangeHeadsPlan(plan, to, options);
3229
3341
  }
3342
+ persistedReceiptReadinessGeneration(peerSession, receiveEpoch, capabilitySession) {
3343
+ const current = this._persistedReceiptReadinessGenerations.get(peerSession);
3344
+ if (current?.receiveEpoch === receiveEpoch &&
3345
+ current.capabilitySession === capabilitySession) {
3346
+ return current.generation;
3347
+ }
3348
+ const generation = `${this._persistedReceiptReadinessGenerationPrefix}:${(++this
3349
+ ._persistedReceiptReadinessGenerationCounter).toString(36)}`;
3350
+ this._persistedReceiptReadinessGenerations.set(peerSession, {
3351
+ receiveEpoch,
3352
+ capabilitySession,
3353
+ generation,
3354
+ });
3355
+ return generation;
3356
+ }
3357
+ pendingPersistedReceiptReadiness(reason, generation) {
3358
+ return Object.freeze({
3359
+ status: "pending",
3360
+ reason,
3361
+ ...(generation === undefined ? {} : { generation }),
3362
+ });
3363
+ }
3364
+ unsupportedPersistedReceiptReadiness(reason, generation) {
3365
+ return Object.freeze({
3366
+ status: "unsupported",
3367
+ reason,
3368
+ generation,
3369
+ });
3370
+ }
3371
+ dispatchPersistedReceiptReadinessChange(peerHash) {
3372
+ this.events.dispatchEvent(new CustomEvent("persisted-receipt:readiness", { detail: Object.freeze({ peerHash }) }));
3373
+ }
3374
+ persistedReceiptReadinessCandidate(peerHash) {
3375
+ if (this.closed) {
3376
+ return this.pendingPersistedReceiptReadiness("closed");
3377
+ }
3378
+ const peerSession = this._peerSessions.current(peerHash);
3379
+ if (!peerSession) {
3380
+ return this.pendingPersistedReceiptReadiness("no-current-session");
3381
+ }
3382
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
3383
+ const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
3384
+ const generation = this.persistedReceiptReadinessGeneration(peerSession, receiveEpoch, capabilitySession);
3385
+ if (peerSession.phase !== "open" ||
3386
+ !peerSession.isActive() ||
3387
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
3388
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash)) {
3389
+ return this.pendingPersistedReceiptReadiness("session-opening", generation);
3390
+ }
3391
+ if (capabilitySession === undefined ||
3392
+ !this._peerSyncCapabilityTimestamps.has(peerHash)) {
3393
+ return this.pendingPersistedReceiptReadiness("capability-pending", generation);
3394
+ }
3395
+ const capabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
3396
+ if ((capabilities & SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS) === 0) {
3397
+ return this.unsupportedPersistedReceiptReadiness("persisted-receipts-unsupported", generation);
3398
+ }
3399
+ if ((capabilities & SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM) === 0) {
3400
+ return this.unsupportedPersistedReceiptReadiness("replication-confirmation-unsupported", generation);
3401
+ }
3402
+ if (!this._v2Receive.isCurrentActive({
3403
+ peerHash,
3404
+ peerSession,
3405
+ receiveEpoch,
3406
+ senderTransportSession: capabilitySession,
3407
+ })) {
3408
+ return this.pendingPersistedReceiptReadiness("replication-state-pending", generation);
3409
+ }
3410
+ if (!this.uniqueReplicators.has(peerHash)) {
3411
+ return this.pendingPersistedReceiptReadiness("not-replicating", generation);
3412
+ }
3413
+ return {
3414
+ capabilitySession,
3415
+ peerSession,
3416
+ receiveEpoch,
3417
+ generation,
3418
+ };
3419
+ }
3230
3420
  persistedReceiptPeerSession(peerHash) {
3421
+ // This is a hot receipt/transfer-loop predicate. Keep it allocation-light,
3422
+ // while mirroring every exact-session gate in
3423
+ // persistedReceiptReadinessCandidate (which additionally creates public
3424
+ // reason/generation snapshots).
3231
3425
  const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
3232
3426
  const peerSession = this._peerSessions.current(peerHash);
3233
3427
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
3234
3428
  const requiredCapabilities = SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS |
3235
3429
  SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM;
3236
- if (capabilitySession == null ||
3430
+ if (this.closed ||
3431
+ capabilitySession == null ||
3237
3432
  !peerSession ||
3238
3433
  peerSession.phase !== "open" ||
3239
- !this._peerSessions.isCurrent(peerHash, peerSession) ||
3434
+ !peerSession.isActive() ||
3435
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
3436
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash) ||
3437
+ !this.uniqueReplicators.has(peerHash) ||
3240
3438
  !this._peerSyncCapabilityTimestamps.has(peerHash) ||
3241
3439
  ((this._peerSyncCapabilities.get(peerHash) ?? 0) &
3242
3440
  requiredCapabilities) !==
@@ -5188,8 +5386,11 @@ let SharedLog = (() => {
5188
5386
  ? checkedPruneCoordinator.fencePeerRemoval(keyHash)
5189
5387
  : undefined;
5190
5388
  const blockPeerReceiveAdmission = () => {
5191
- releaseReceiveCleanupGate ??=
5192
- this._peerSessions.acquireReceiveCleanupGate(keyHash);
5389
+ if (!releaseReceiveCleanupGate) {
5390
+ releaseReceiveCleanupGate =
5391
+ this._peerSessions.acquireReceiveCleanupGate(keyHash);
5392
+ this.dispatchPersistedReceiptReadinessChange(keyHash);
5393
+ }
5193
5394
  };
5194
5395
  if (!isMe && !isSpeculativePeerRemoval) {
5195
5396
  // Revoke this peer's receipts synchronously, before this removal can
@@ -5377,7 +5578,10 @@ let SharedLog = (() => {
5377
5578
  removalCallCompleted = true;
5378
5579
  }
5379
5580
  finally {
5380
- releaseReceiveCleanupGate?.();
5581
+ if (releaseReceiveCleanupGate) {
5582
+ releaseReceiveCleanupGate();
5583
+ this.dispatchPersistedReceiptReadinessChange(keyHash);
5584
+ }
5381
5585
  if (replicationInfoRecoveryEpochAdvanced &&
5382
5586
  ownsReplicationOwnershipLifecycle() &&
5383
5587
  ownsReplicationLifecycle() &&
@@ -10957,6 +11161,8 @@ let SharedLog = (() => {
10957
11161
  const recoveringNativeDurableFailure = this._nativeDurableCommitFailure !== undefined;
10958
11162
  options = applySharedLogNativeDefaults(options, this.node
10959
11163
  .sharedLogNativeDefaults);
11164
+ const openProfile = options?.sync?.profile;
11165
+ const openStartedAt = syncProfileStart(openProfile);
10960
11166
  this.replicas = {
10961
11167
  min: options?.replicas?.min != null
10962
11168
  ? typeof options?.replicas?.min === "number"
@@ -11037,6 +11243,10 @@ let SharedLog = (() => {
11037
11243
  this._peerSyncCapabilities = new Map();
11038
11244
  this._peerSyncCapabilitySessions = new Map();
11039
11245
  this._peerSyncCapabilityTimestamps = new Map();
11246
+ this._persistedReceiptReadinessGenerations = new WeakMap();
11247
+ this._persistedReceiptReadinessGenerationPrefix = toHexString(randomBytes(8));
11248
+ this._persistedReceiptReadinessGenerationCounter = 0;
11249
+ this._persistedReceiptReadinessWaiters = new Set();
11040
11250
  this._persistedReceiptStorage = undefined;
11041
11251
  this._persistedReceiptRequestsInFlight = new Map();
11042
11252
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -11119,6 +11329,7 @@ let SharedLog = (() => {
11119
11329
  this._isTrustedReplicator = options?.canReplicate;
11120
11330
  this.keep = options?.keep;
11121
11331
  this.pendingMaturity = new Map();
11332
+ const localStateStartedAt = syncProfileStart(openProfile);
11122
11333
  const id = sha256Base64Sync(this.log.id);
11123
11334
  const [storage, logScope] = await Promise.all([
11124
11335
  this.node.storage.sublevel(id),
@@ -11173,6 +11384,11 @@ let SharedLog = (() => {
11173
11384
  this._entryCoordinatesIndex = await replicationIndex.init({
11174
11385
  schema: this.indexableDomain.constructorEntry,
11175
11386
  });
11387
+ emitAdvisorySyncProfileDuration(openProfile, localStateStartedAt, {
11388
+ name: "sharedLog.open.localState",
11389
+ component: "shared-log",
11390
+ });
11391
+ const blockStoreStartedAt = syncProfileStart(openProfile);
11176
11392
  const deferStandaloneNativeRangePlanner = !!options?.nativeBackbone && options.nativeRangePlanner == null;
11177
11393
  await this.openNativeRangePlanner(deferStandaloneNativeRangePlanner ? false : options?.nativeRangePlanner);
11178
11394
  this._nativeBackbone = await this.openNativeBackbone(options?.nativeBackbone);
@@ -11213,6 +11429,14 @@ let SharedLog = (() => {
11213
11429
  else {
11214
11430
  localBlocks = await createDefaultDurableBlockStore(storage);
11215
11431
  }
11432
+ emitAdvisorySyncProfileDuration(openProfile, blockStoreStartedAt, {
11433
+ name: "sharedLog.open.blockStore",
11434
+ component: "shared-log",
11435
+ details: {
11436
+ nativeBackbone: this._nativeBackbone != null,
11437
+ directoryConfigured: this.node.directory != null,
11438
+ },
11439
+ });
11216
11440
  this.remoteBlocks = new RemoteBlocks({
11217
11441
  local: localBlocks,
11218
11442
  publish: (message, options) => this.rpc.send(new BlocksMessage(message), options),
@@ -11222,30 +11446,61 @@ let SharedLog = (() => {
11222
11446
  // compatible eager path with bounded validation and storage budgets.
11223
11447
  eagerBlocks: options?.eagerBlocks ?? false,
11224
11448
  resolveProviders: async (cid, opts) => {
11449
+ const profile = this._logProperties?.sync?.profile;
11225
11450
  const maxPeers = 8;
11451
+ const excluded = new Set((opts?.exclude ?? []).slice(0, maxPeers));
11452
+ const lookupPeers = opts?.refresh
11453
+ ? Math.min(maxPeers * 2, maxPeers + excluded.size)
11454
+ : maxPeers;
11455
+ const resolutionStartedAt = syncProfileStart(profile);
11226
11456
  const localCandidates = (await this.resolveCandidatePeersForHash(cid, {
11227
11457
  signal: opts?.signal,
11228
- maxPeers,
11458
+ maxPeers: lookupPeers,
11229
11459
  })) ?? [];
11230
- if (opts?.signal?.aborted)
11460
+ const emitResolution = profile
11461
+ ? (status, targets, directoryCandidates = 0, reachableCandidates = 0) => emitAdvisorySyncProfileDuration(profile, resolutionStartedAt, {
11462
+ name: "sharedLog.blocks.resolveProviders",
11463
+ component: "shared-log",
11464
+ count: targets,
11465
+ targets,
11466
+ details: {
11467
+ status,
11468
+ refresh: opts?.refresh === true,
11469
+ excluded: excluded.size,
11470
+ lookupPeers,
11471
+ localCandidates: localCandidates.length,
11472
+ directoryCandidates,
11473
+ reachableCandidates,
11474
+ },
11475
+ })
11476
+ : undefined;
11477
+ if (opts?.signal?.aborted) {
11478
+ emitResolution?.("aborted", 0);
11231
11479
  return [];
11480
+ }
11232
11481
  const locallyReachable = new Set(await this._getLocalReachablePeerHashes(this.topic));
11482
+ if (opts?.signal?.aborted) {
11483
+ emitResolution?.("aborted", 0, 0, locallyReachable.size);
11484
+ return [];
11485
+ }
11233
11486
  const confirmed = this._checkedPrune.getConfirmedReplicators(cid);
11234
11487
  const contacted = this._checkedPrune.getContactedReplicators(cid);
11235
- const hasLiveCandidate = localCandidates.some((peer) => locallyReachable.has(peer) &&
11236
- (confirmed?.has(peer) ||
11237
- contacted?.has(peer) ||
11238
- this.uniqueReplicators.has(peer)));
11488
+ const hasProviderEvidence = (peer) => confirmed?.has(peer) === true ||
11489
+ contacted?.has(peer) ||
11490
+ this.uniqueReplicators.has(peer);
11491
+ const hasLiveCandidate = localCandidates.some((peer) => locallyReachable.has(peer) && hasProviderEvidence(peer));
11239
11492
  // Only reachability corroborated by provider/replicator evidence may
11240
11493
  // bypass the initial CID lookup. Arbitrary bootstrap connections are
11241
11494
  // useful fallbacks, but are not evidence that they hold this block.
11242
11495
  if (hasLiveCandidate && !opts?.refresh) {
11243
- return localCandidates;
11496
+ const selected = localCandidates.slice(0, maxPeers);
11497
+ emitResolution?.("local", selected.length, 0, locallyReachable.size);
11498
+ return selected;
11244
11499
  }
11245
11500
  let directoryProviders = [];
11246
11501
  try {
11247
11502
  const query = (namespace) => fanoutService?.queryProviders(namespace, {
11248
- want: maxPeers,
11503
+ want: lookupPeers,
11249
11504
  timeoutMs: 2_000,
11250
11505
  queryTimeoutMs: 500,
11251
11506
  bootstrapMaxPeers: 2,
@@ -11257,13 +11512,17 @@ let SharedLog = (() => {
11257
11512
  ]);
11258
11513
  for (const result of results) {
11259
11514
  if (result.status === "fulfilled") {
11260
- directoryProviders.push(...result.value);
11515
+ directoryProviders.push(...result.value.slice(0, lookupPeers));
11261
11516
  }
11262
11517
  }
11263
11518
  }
11264
11519
  catch {
11265
11520
  // Ignore discovery failures; local evidence remains usable.
11266
11521
  }
11522
+ if (opts?.signal?.aborted) {
11523
+ emitResolution?.("aborted", 0, directoryProviders.length, locallyReachable.size);
11524
+ return [];
11525
+ }
11267
11526
  const selected = [];
11268
11527
  const selectedSet = new Set();
11269
11528
  const add = (peer) => {
@@ -11273,11 +11532,44 @@ let SharedLog = (() => {
11273
11532
  selectedSet.add(peer);
11274
11533
  selected.push(peer);
11275
11534
  };
11276
- for (let index = 0; selected.length < maxPeers &&
11277
- (index < localCandidates.length || index < directoryProviders.length); index++) {
11278
- add(localCandidates[index]);
11279
- add(directoryProviders[index]);
11535
+ const append = (providers, includeExcluded, predicate) => {
11536
+ for (const provider of providers) {
11537
+ if (selected.length >= maxPeers)
11538
+ return;
11539
+ if (excluded.has(provider) === includeExcluded &&
11540
+ (!predicate || predicate(provider))) {
11541
+ add(provider);
11542
+ }
11543
+ }
11544
+ };
11545
+ const appendInterleaved = (includeExcluded) => {
11546
+ for (let index = 0; selected.length < maxPeers &&
11547
+ (index < localCandidates.length ||
11548
+ index < directoryProviders.length); index++) {
11549
+ const local = localCandidates[index];
11550
+ if (local && excluded.has(local) === includeExcluded)
11551
+ add(local);
11552
+ const directory = directoryProviders[index];
11553
+ if (directory && excluded.has(directory) === includeExcluded) {
11554
+ add(directory);
11555
+ }
11556
+ }
11557
+ };
11558
+ if (opts?.refresh) {
11559
+ // Retry results are wider than the regular eight-peer window. Prefer
11560
+ // untried reachable holders, then the remaining fresh directory
11561
+ // evidence, without discarding attempted peers as bounded transient-
11562
+ // failure fallbacks.
11563
+ append(directoryProviders, false, (peer) => locallyReachable.has(peer));
11564
+ append(localCandidates, false, (peer) => locallyReachable.has(peer) && hasProviderEvidence(peer));
11565
+ append(directoryProviders, false);
11566
+ append(localCandidates, false);
11567
+ }
11568
+ else {
11569
+ appendInterleaved(false);
11280
11570
  }
11571
+ appendInterleaved(true);
11572
+ emitResolution?.("directory", selected.length, directoryProviders.length, locallyReachable.size);
11281
11573
  return selected;
11282
11574
  },
11283
11575
  watchProviders: fanoutService
@@ -11324,7 +11616,13 @@ let SharedLog = (() => {
11324
11616
  }
11325
11617
  : undefined,
11326
11618
  });
11327
- const remoteBlocksStartPromise = this.remoteBlocks.start();
11619
+ const remoteBlocksStartedAt = syncProfileStart(openProfile);
11620
+ const remoteBlocksStartPromise = this.remoteBlocks.start().then(() => {
11621
+ emitAdvisorySyncProfileDuration(openProfile, remoteBlocksStartedAt, {
11622
+ name: "sharedLog.open.remoteBlocks",
11623
+ component: "shared-log",
11624
+ });
11625
+ });
11328
11626
  const hasIndexedReplicationInfo = (await this.replicationIndex.count({
11329
11627
  query: [
11330
11628
  new StringMatch({
@@ -11483,6 +11781,7 @@ let SharedLog = (() => {
11483
11781
  // joins rely on: a replicate:false observer syncing a head whose parents
11484
11782
  // are not local would fail block resolution, and Log.join treats that as
11485
11783
  // recoverable and skips the entry without persisting anything.
11784
+ const lowerLogStartedAt = syncProfileStart(openProfile);
11486
11785
  await this.log.open(this.remoteBlocks, this.node.identity, {
11487
11786
  keychain: this.node.services.keychain,
11488
11787
  resolveRemotePeers: (hash, options) => this.resolveCandidatePeersForHash(hash, {
@@ -11513,6 +11812,10 @@ let SharedLog = (() => {
11513
11812
  },
11514
11813
  indexer: logIndex,
11515
11814
  });
11815
+ emitAdvisorySyncProfileDuration(openProfile, lowerLogStartedAt, {
11816
+ name: "sharedLog.open.lowerLog",
11817
+ component: "shared-log",
11818
+ });
11516
11819
  this._persistedReceiptStorage = this.resolvePersistedReceiptStorage();
11517
11820
  try {
11518
11821
  const recovered = await this.recoverNativeStrictDurableTransactionIntent();
@@ -11564,6 +11867,7 @@ let SharedLog = (() => {
11564
11867
  ((event) => {
11565
11868
  void this.runSubscriptionChangeCallback(() => this._onUnsubscription(event));
11566
11869
  });
11870
+ const communicationStartedAt = syncProfileStart(openProfile);
11567
11871
  await Promise.all([
11568
11872
  this.rpc.open({
11569
11873
  queryType: TransportMessage,
@@ -11575,6 +11879,11 @@ let SharedLog = (() => {
11575
11879
  this.node.services.pubsub.addEventListener("subscribe", this._onSubscriptionFn),
11576
11880
  this.node.services.pubsub.addEventListener("unsubscribe", this._onUnsubscriptionFn),
11577
11881
  ]);
11882
+ emitAdvisorySyncProfileDuration(openProfile, communicationStartedAt, {
11883
+ name: "sharedLog.open.rpcSubscriptions",
11884
+ component: "shared-log",
11885
+ });
11886
+ const providerChannelStartedAt = syncProfileStart(openProfile);
11578
11887
  const fanoutOpenPromise = this._openFanoutChannel(options?.fanout);
11579
11888
  // Mark previously-owned replication ranges as "new" only when they already exist.
11580
11889
  // Fresh opens have nothing to touch here, so skip the extra scan/write entirely.
@@ -11582,30 +11891,57 @@ let SharedLog = (() => {
11582
11891
  ? this.updateTimestampOfOwnedReplicationRanges()
11583
11892
  : Promise.resolve();
11584
11893
  await Promise.all([fanoutOpenPromise, updateOwnedReplicationPromise]);
11894
+ emitAdvisorySyncProfileDuration(openProfile, providerChannelStartedAt, {
11895
+ name: "sharedLog.open.providerAndOwnership",
11896
+ component: "shared-log",
11897
+ details: { indexedReplicationInfo: hasIndexedReplicationInfo },
11898
+ });
11585
11899
  // if we had a previous session with replication info, and new replication info dictates that we unreplicate
11586
11900
  // we should do that. Otherwise if options is a unreplication we dont need to do anything because
11587
11901
  // we are already unreplicated (as we are just opening)
11588
11902
  const isUnreplicationOptionsDefined = isUnreplicationOptions(options?.replicate);
11589
11903
  const canResumeReplication = hasIndexedReplicationInfo &&
11590
11904
  (await isReplicationOptionsDependentOnPreviousState(options?.replicate, this.replicationIndex, this.node.identity.publicKey));
11905
+ const replicationStartedAt = syncProfileStart(openProfile);
11906
+ let replicationAction;
11591
11907
  if (hasIndexedReplicationInfo && isUnreplicationOptionsDefined) {
11908
+ replicationAction = "replace";
11592
11909
  await this.replicate(options?.replicate, { checkDuplicates: true });
11593
11910
  }
11594
11911
  else if (canResumeReplication) {
11912
+ replicationAction = "resume";
11595
11913
  // dont do anthing since we are alread replicating stuff
11596
11914
  }
11597
11915
  else {
11916
+ replicationAction = "reset";
11598
11917
  await this.replicate(options?.replicate, {
11599
11918
  checkDuplicates: true,
11600
11919
  reset: true,
11601
11920
  });
11602
11921
  }
11922
+ emitAdvisorySyncProfileDuration(openProfile, replicationStartedAt, {
11923
+ name: "sharedLog.open.replication",
11924
+ component: "shared-log",
11925
+ details: {
11926
+ hadIndexedState: hasIndexedReplicationInfo,
11927
+ action: replicationAction,
11928
+ },
11929
+ });
11930
+ const synchronizerStartedAt = syncProfileStart(openProfile);
11603
11931
  await this.syncronizer.open();
11932
+ emitAdvisorySyncProfileDuration(openProfile, synchronizerStartedAt, {
11933
+ name: "sharedLog.open.synchronizer",
11934
+ component: "shared-log",
11935
+ });
11604
11936
  this.interval = setInterval(() => {
11605
11937
  void this.rebalanceParticipationDebounced?.call();
11606
11938
  }, RECALCULATE_PARTICIPATION_DEBOUNCE_INTERVAL);
11607
11939
  this._instanceLifecycle.markOpenComplete();
11608
11940
  this.scheduleReplicationStatusRefresh();
11941
+ emitAdvisorySyncProfileDuration(openProfile, openStartedAt, {
11942
+ name: "sharedLog.open.total",
11943
+ component: "shared-log",
11944
+ });
11609
11945
  }
11610
11946
  toNativeReplicationRange(range) {
11611
11947
  return {
@@ -12302,6 +12638,7 @@ let SharedLog = (() => {
12302
12638
  }
12303
12639
  this.cleanupPendingIHavePeer(peerHash);
12304
12640
  this.cleanupCheckedPrunePeer(peerHash, ownershipLifecycleController, this._checkedPrune);
12641
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
12305
12642
  }
12306
12643
  cleanupPendingIHavePeer(peerHash) {
12307
12644
  for (const [hash, pending] of this._pendingIHave) {
@@ -12324,6 +12661,7 @@ let SharedLog = (() => {
12324
12661
  receiveEpoch,
12325
12662
  });
12326
12663
  }
12664
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
12327
12665
  }
12328
12666
  async resolveCandidatePeersForHash(hash, options) {
12329
12667
  if (options?.signal?.aborted)
@@ -13497,6 +13835,7 @@ let SharedLog = (() => {
13497
13835
  this._peerSyncCapabilities?.clear();
13498
13836
  this._peerSyncCapabilitySessions?.clear();
13499
13837
  this._peerSyncCapabilityTimestamps?.clear();
13838
+ this._persistedReceiptReadinessGenerations = new WeakMap();
13500
13839
  this._persistedReceiptStorage = undefined;
13501
13840
  this._persistedReceiptRequestsInFlight?.clear();
13502
13841
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -15612,10 +15951,12 @@ let SharedLog = (() => {
15612
15951
  return;
15613
15952
  }
15614
15953
  else if (msg instanceof ReplicationInfoV2AppliedMessage) {
15615
- this._v2Send.acceptApplied(msg, {
15954
+ if (this._v2Send.acceptApplied(msg, {
15616
15955
  from: context.from,
15617
15956
  receiverTransportSession: context.message.header.session,
15618
- });
15957
+ })) {
15958
+ this.dispatchPersistedReceiptReadinessChange(receiveFromHash);
15959
+ }
15619
15960
  return;
15620
15961
  }
15621
15962
  else if (isReplicationInfoV2Message(msg)) {
@@ -16239,6 +16580,7 @@ let SharedLog = (() => {
16239
16580
  // A committed V2 announcement is applied progress: the peer answers,
16240
16581
  // so recovery re-solicitation may restart from the base interval.
16241
16582
  this.resetReplicationInfoV2RecoveryEscalation(fromHash);
16583
+ this.dispatchPersistedReceiptReadinessChange(fromHash);
16242
16584
  });
16243
16585
  }
16244
16586
  finally {
@@ -16513,18 +16855,401 @@ let SharedLog = (() => {
16513
16855
  }
16514
16856
  throwIfInactive();
16515
16857
  }
16858
+ nudgePersistedReceiptPeerReadiness(publicKey) {
16859
+ if (this.closed)
16860
+ return;
16861
+ const peerHash = publicKey.hashcode();
16862
+ const peerSession = this._peerSessions.current(peerHash);
16863
+ if (!peerSession ||
16864
+ peerSession.phase === "departing" ||
16865
+ (peerSession.phase === "opening" &&
16866
+ !peerSession.openingBarrierActive)) {
16867
+ // A barrier rejection deliberately leaves the current session in its
16868
+ // fail-closed opening phase after the barrier window has settled. Ask the
16869
+ // authenticated peer for a fresh subscriber snapshot so the replacement
16870
+ // session can recover; never rotate a barrier that is still in flight.
16871
+ this.requestSubscriberSnapshotForCapability(publicKey);
16872
+ return;
16873
+ }
16874
+ if (peerSession.phase !== "open" || !peerSession.isActive()) {
16875
+ return;
16876
+ }
16877
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
16878
+ this.promoteReplicationInfoV2ReceiveCapability(publicKey, peerSession);
16879
+ this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
16880
+ peerHash,
16881
+ peerSession,
16882
+ receiveEpoch,
16883
+ });
16884
+ this._v2Receive.ensureRequestProgress({
16885
+ peerHash,
16886
+ peerSession,
16887
+ receiveEpoch,
16888
+ });
16889
+ this.scheduleReplicationInfoV2Recovery(publicKey);
16890
+ }
16891
+ /**
16892
+ * Inspect whether one public key's exact current connection generation can
16893
+ * supply persisted-receipt evidence. The returned object is frozen and never
16894
+ * exposes the internal PeerSession token. When `entries` are supplied, the
16895
+ * peer must also be present in a fresh leader plan for every entry.
16896
+ *
16897
+ * This is advisory preflight state. Persisted delivery repeats every
16898
+ * generation, leadership, ownership and storage check at receipt time; a
16899
+ * `ready` snapshot is never itself authority to dispose a source copy.
16900
+ */
16901
+ async getPersistedReceiptPeerReadiness(key, options = {}) {
16902
+ return this.inspectPersistedReceiptPeerReadiness(key, options);
16903
+ }
16904
+ async inspectPersistedReceiptPeerReadiness(key, options, assertContinue) {
16905
+ // Capture and validate caller-owned planning input before consulting live
16906
+ // peer state. Invalid options must not appear to work merely because the
16907
+ // peer is currently absent, then fail later when the same session connects.
16908
+ const entries = options.entries ? [...options.entries] : [];
16909
+ const replicas = options.replicas ??
16910
+ (entries.length > 0 ? this.replicas.min.getValue(this) : undefined);
16911
+ if (replicas !== undefined) {
16912
+ if (!Number.isSafeInteger(replicas) || replicas <= 0) {
16913
+ throw new RangeError("Persisted-receipt readiness replicas must be a positive integer");
16914
+ }
16915
+ checkMinReplicasLimit(replicas);
16916
+ }
16917
+ const peerHash = key.hashcode();
16918
+ const captured = this.persistedReceiptReadinessCandidate(peerHash);
16919
+ if ("status" in captured) {
16920
+ return captured;
16921
+ }
16922
+ assertContinue?.();
16923
+ if (entries.length > 0) {
16924
+ const ownershipLifecycleController = this.captureReplicationOwnershipLifecycle();
16925
+ const ownershipRevision = this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
16926
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
16927
+ return this.pendingPersistedReceiptReadiness("ownership-changing", captured.generation);
16928
+ }
16929
+ for (const entry of entries) {
16930
+ assertContinue?.();
16931
+ const leaders = await this.findLeadersFromEntry(entry, replicas, { freshLeaderPlan: true }, ownershipLifecycleController);
16932
+ assertContinue?.();
16933
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
16934
+ return this.pendingPersistedReceiptReadiness("ownership-changing", captured.generation);
16935
+ }
16936
+ const current = this.persistedReceiptReadinessCandidate(peerHash);
16937
+ if ("status" in current) {
16938
+ return current;
16939
+ }
16940
+ if (current.peerSession !== captured.peerSession ||
16941
+ current.receiveEpoch !== captured.receiveEpoch ||
16942
+ current.capabilitySession !== captured.capabilitySession) {
16943
+ return this.pendingPersistedReceiptReadiness("replication-state-pending", current.generation);
16944
+ }
16945
+ if (!leaders.has(peerHash)) {
16946
+ return this.pendingPersistedReceiptReadiness("not-entry-leader", captured.generation);
16947
+ }
16948
+ }
16949
+ }
16950
+ assertContinue?.();
16951
+ const current = this.persistedReceiptReadinessCandidate(peerHash);
16952
+ if ("status" in current) {
16953
+ return current;
16954
+ }
16955
+ if (current.peerSession !== captured.peerSession ||
16956
+ current.receiveEpoch !== captured.receiveEpoch ||
16957
+ current.capabilitySession !== captured.capabilitySession) {
16958
+ return this.pendingPersistedReceiptReadiness("replication-state-pending", current.generation);
16959
+ }
16960
+ if (!this._v2Send.isLatestConfirmedForPeer({
16961
+ peerHash,
16962
+ peerSession: captured.peerSession,
16963
+ receiverTransportSession: captured.capabilitySession,
16964
+ })) {
16965
+ return this.pendingPersistedReceiptReadiness("replication-confirmation-pending", captured.generation);
16966
+ }
16967
+ return Object.freeze({
16968
+ status: "ready",
16969
+ generation: captured.generation,
16970
+ });
16971
+ }
16972
+ /**
16973
+ * Wait for a public key's current (or replacement) connection generation to
16974
+ * become persisted-receipt ready. Transition listeners are installed before
16975
+ * the first asynchronous inspection, and a bounded recovery tick repairs
16976
+ * missed subscriber/capability wakes without retaining stale PeerSessions.
16977
+ * This waiter is advisory only; the following persisted delivery remains the
16978
+ * operation that proves the requested remote durability quorum.
16979
+ */
16980
+ async waitForPersistedReceiptPeerReadiness(key, options = {}) {
16981
+ if (this.closed) {
16982
+ throw new ClosedError();
16983
+ }
16984
+ const timeoutMs = options.timeout ?? this.waitForReplicatorTimeout;
16985
+ if (!Number.isSafeInteger(timeoutMs) ||
16986
+ timeoutMs <= 0 ||
16987
+ timeoutMs > MAX_PERSISTED_DELIVERY_TIMEOUT_MS) {
16988
+ throw new RangeError(`Persisted-receipt readiness timeout must be an integer from 1 to ${MAX_PERSISTED_DELIVERY_TIMEOUT_MS} milliseconds`);
16989
+ }
16990
+ if (options.signal?.aborted) {
16991
+ throw options.signal.reason instanceof Error
16992
+ ? options.signal.reason
16993
+ : new AbortError("Persisted-receipt readiness wait aborted");
16994
+ }
16995
+ // Capture caller-owned inputs before reserving a waiter slot. A throwing
16996
+ // iterator/key implementation must not strand capacity permanently.
16997
+ const entries = options.entries ? [...options.entries] : undefined;
16998
+ const inspectOptions = {
16999
+ ...(entries ? { entries } : {}),
17000
+ ...(options.replicas === undefined ? {} : { replicas: options.replicas }),
17001
+ };
17002
+ const peerHash = key.hashcode();
17003
+ const waiterSet = this._persistedReceiptReadinessWaiters;
17004
+ if (waiterSet.size >= MAX_PERSISTED_RECEIPT_READINESS_WAITERS) {
17005
+ throw new RangeError(`Too many pending persisted-receipt readiness waits (maximum ${MAX_PERSISTED_RECEIPT_READINESS_WAITERS})`);
17006
+ }
17007
+ const waiterToken = {};
17008
+ waiterSet.add(waiterToken);
17009
+ const deadline = Date.now() + timeoutMs;
17010
+ const closeSignal = this._closeController.signal;
17011
+ const operationController = new AbortController();
17012
+ const operationSignal = AbortSignal.any([options.signal, closeSignal, operationController.signal].filter((value) => value !== undefined));
17013
+ const deferred = pDefer();
17014
+ let settled = false;
17015
+ let checkScheduled = false;
17016
+ let checkInFlight = false;
17017
+ let rerun = false;
17018
+ let recoveryTimer;
17019
+ let confirmationController;
17020
+ let lastSnapshot;
17021
+ const createTimeoutError = () => {
17022
+ const suffix = lastSnapshot
17023
+ ? ` (last status: ${lastSnapshot.status}${"reason" in lastSnapshot ? `/${lastSnapshot.reason}` : ""})`
17024
+ : "";
17025
+ return new TimeoutError(`Timeout waiting for persisted-receipt readiness from ${peerHash}${suffix}`);
17026
+ };
17027
+ const cleanup = () => {
17028
+ waiterSet.delete(waiterToken);
17029
+ this.events.removeEventListener("persisted-receipt:readiness", onReadinessChange);
17030
+ this.events.removeEventListener("replication:change", onRoleChange);
17031
+ this.events.removeEventListener("replicator:mature", onRoleChange);
17032
+ options.signal?.removeEventListener("abort", onCallerAbort);
17033
+ closeSignal.removeEventListener("abort", onClose);
17034
+ if (recoveryTimer) {
17035
+ clearTimeout(recoveryTimer);
17036
+ recoveryTimer = undefined;
17037
+ }
17038
+ confirmationController?.abort(new AbortError("Persisted-receipt readiness generation changed"));
17039
+ confirmationController = undefined;
17040
+ operationController.abort(new AbortError("Persisted-receipt readiness wait settled"));
17041
+ };
17042
+ const resolve = (snapshot) => {
17043
+ if (settled)
17044
+ return;
17045
+ settled = true;
17046
+ cleanup();
17047
+ deferred.resolve(snapshot);
17048
+ };
17049
+ const reject = (error) => {
17050
+ if (settled)
17051
+ return;
17052
+ settled = true;
17053
+ cleanup();
17054
+ deferred.reject(error instanceof Error ? error : new Error(String(error)));
17055
+ };
17056
+ const onCallerAbort = () => reject(options.signal?.reason instanceof Error
17057
+ ? options.signal.reason
17058
+ : new AbortError("Persisted-receipt readiness wait aborted"));
17059
+ const onClose = () => reject(new ClosedError());
17060
+ const continueWait = () => {
17061
+ if (settled)
17062
+ return false;
17063
+ if (closeSignal.aborted) {
17064
+ onClose();
17065
+ return false;
17066
+ }
17067
+ if (options.signal?.aborted) {
17068
+ onCallerAbort();
17069
+ return false;
17070
+ }
17071
+ if (Date.now() >= deadline) {
17072
+ reject(createTimeoutError());
17073
+ return false;
17074
+ }
17075
+ return true;
17076
+ };
17077
+ const assertInspectionCurrent = () => {
17078
+ if (!continueWait()) {
17079
+ throw new AbortError("Persisted-receipt readiness wait settled");
17080
+ }
17081
+ };
17082
+ const armRecoveryTick = () => {
17083
+ if (settled || recoveryTimer)
17084
+ return;
17085
+ const delayMs = Math.max(50, Math.min(1_000, this.waitForReplicatorRequestIntervalMs));
17086
+ recoveryTimer = setTimeout(() => {
17087
+ recoveryTimer = undefined;
17088
+ if (!continueWait())
17089
+ return;
17090
+ this.nudgePersistedReceiptPeerReadiness(key);
17091
+ scheduleCheck();
17092
+ }, delayMs);
17093
+ recoveryTimer.unref?.();
17094
+ };
17095
+ const runCheck = async () => {
17096
+ checkScheduled = false;
17097
+ if (!continueWait())
17098
+ return;
17099
+ if (checkInFlight) {
17100
+ rerun = true;
17101
+ return;
17102
+ }
17103
+ checkInFlight = true;
17104
+ try {
17105
+ let snapshot = await this.inspectPersistedReceiptPeerReadiness(key, inspectOptions, assertInspectionCurrent);
17106
+ lastSnapshot = snapshot;
17107
+ if (!continueWait())
17108
+ return;
17109
+ if (rerun)
17110
+ return;
17111
+ if (snapshot.status === "ready") {
17112
+ // A wake observed while the asynchronous inspection was running may
17113
+ // already have invalidated this snapshot. Drain that coalesced wake
17114
+ // before publishing readiness.
17115
+ resolve(snapshot);
17116
+ return;
17117
+ }
17118
+ if (snapshot.status === "pending" &&
17119
+ snapshot.reason === "replication-confirmation-pending") {
17120
+ const target = this.persistedReceiptPeerSession(peerHash);
17121
+ if (target) {
17122
+ const currentConfirmationController = new AbortController();
17123
+ confirmationController = currentConfirmationController;
17124
+ try {
17125
+ await this._v2Send.confirmLatestForPeer({
17126
+ peerHash,
17127
+ peerSession: target.peerSession,
17128
+ receiverTransportSession: target.capabilitySession,
17129
+ }, {
17130
+ timeout: Math.max(1, deadline - Date.now()),
17131
+ signal: AbortSignal.any([
17132
+ operationSignal,
17133
+ currentConfirmationController.signal,
17134
+ ]),
17135
+ });
17136
+ }
17137
+ catch (error) {
17138
+ if (!continueWait())
17139
+ return;
17140
+ if (!(error instanceof AbortError)) {
17141
+ throw error;
17142
+ }
17143
+ rerun = true;
17144
+ }
17145
+ finally {
17146
+ if (confirmationController === currentConfirmationController) {
17147
+ confirmationController = undefined;
17148
+ }
17149
+ }
17150
+ if (!continueWait())
17151
+ return;
17152
+ snapshot = await this.inspectPersistedReceiptPeerReadiness(key, inspectOptions, assertInspectionCurrent);
17153
+ lastSnapshot = snapshot;
17154
+ if (!continueWait())
17155
+ return;
17156
+ if (rerun)
17157
+ return;
17158
+ if (snapshot.status === "ready") {
17159
+ resolve(snapshot);
17160
+ return;
17161
+ }
17162
+ }
17163
+ }
17164
+ if (!continueWait())
17165
+ return;
17166
+ this.nudgePersistedReceiptPeerReadiness(key);
17167
+ }
17168
+ catch (error) {
17169
+ if (!settled)
17170
+ reject(error);
17171
+ }
17172
+ finally {
17173
+ checkInFlight = false;
17174
+ if (!settled && rerun) {
17175
+ rerun = false;
17176
+ scheduleCheck();
17177
+ }
17178
+ else {
17179
+ armRecoveryTick();
17180
+ }
17181
+ }
17182
+ };
17183
+ const scheduleCheck = (interruptConfirmation = false) => {
17184
+ if (settled)
17185
+ return;
17186
+ if (recoveryTimer) {
17187
+ clearTimeout(recoveryTimer);
17188
+ recoveryTimer = undefined;
17189
+ }
17190
+ if (checkInFlight) {
17191
+ rerun = true;
17192
+ if (interruptConfirmation) {
17193
+ confirmationController?.abort(new AbortError("Persisted-receipt readiness changed during confirmation"));
17194
+ }
17195
+ return;
17196
+ }
17197
+ if (checkScheduled)
17198
+ return;
17199
+ checkScheduled = true;
17200
+ void Promise.resolve().then(runCheck);
17201
+ };
17202
+ const onReadinessChange = (event) => {
17203
+ if (event.detail.peerHash === peerHash)
17204
+ scheduleCheck(true);
17205
+ };
17206
+ const onRoleChange = (event) => {
17207
+ if ((entries?.length ?? 0) > 0 ||
17208
+ event.detail.publicKey.hashcode() === peerHash) {
17209
+ scheduleCheck(true);
17210
+ }
17211
+ };
17212
+ // Register wake sources before the first state inspection. EventTarget does
17213
+ // not replay a transition that fired between an async check and registration.
17214
+ this.events.addEventListener("persisted-receipt:readiness", onReadinessChange);
17215
+ this.events.addEventListener("replication:change", onRoleChange);
17216
+ this.events.addEventListener("replicator:mature", onRoleChange);
17217
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
17218
+ closeSignal.addEventListener("abort", onClose, { once: true });
17219
+ if (options.signal?.aborted) {
17220
+ onCallerAbort();
17221
+ }
17222
+ else if (closeSignal.aborted) {
17223
+ onClose();
17224
+ }
17225
+ else {
17226
+ scheduleCheck();
17227
+ }
17228
+ const timeout = setTimeout(() => reject(createTimeoutError()), timeoutMs);
17229
+ timeout.unref?.();
17230
+ return deferred.promise.finally(() => clearTimeout(timeout));
17231
+ }
16516
17232
  async waitForReplicator(key, options) {
17233
+ if (options?.signal?.aborted) {
17234
+ throw new AbortError();
17235
+ }
16517
17236
  const deferred = pDefer();
16518
17237
  const timeoutMs = options?.timeout ?? this.waitForReplicatorTimeout;
16519
17238
  const resolvedRoleAge = options?.eager
16520
17239
  ? undefined
16521
17240
  : (options?.roleAge ?? (await this.getDefaultMinRoleAge()));
17241
+ if (options?.signal?.aborted) {
17242
+ throw new AbortError();
17243
+ }
16522
17244
  let settled = false;
16523
17245
  let timer;
16524
17246
  let requestTimer;
17247
+ let checkInFlight = false;
17248
+ let checkAgain = false;
16525
17249
  const clear = () => {
16526
- this.events.removeEventListener("replicator:mature", check);
16527
- this.events.removeEventListener("replication:change", check);
17250
+ checkAgain = false;
17251
+ this.events.removeEventListener("replicator:mature", runCheck);
17252
+ this.events.removeEventListener("replication:change", runCheck);
16528
17253
  options?.signal?.removeEventListener("abort", onAbort);
16529
17254
  if (timer != null) {
16530
17255
  clearTimeout(timer);
@@ -16643,10 +17368,32 @@ let SharedLog = (() => {
16643
17368
  await iterator?.close();
16644
17369
  }
16645
17370
  };
17371
+ const runCheck = () => {
17372
+ if (settled)
17373
+ return;
17374
+ if (checkInFlight) {
17375
+ checkAgain = true;
17376
+ return;
17377
+ }
17378
+ // Reserve synchronously before `check()` can dispatch/re-enter from an
17379
+ // index implementation's first `next()` call.
17380
+ checkInFlight = true;
17381
+ void check()
17382
+ .catch((error) => reject(error instanceof Error ? error : new Error(String(error))))
17383
+ .finally(() => {
17384
+ checkInFlight = false;
17385
+ if (!settled && checkAgain) {
17386
+ checkAgain = false;
17387
+ runCheck();
17388
+ }
17389
+ });
17390
+ };
17391
+ // Register before the first asynchronous index read. EventTarget does not
17392
+ // replay a maturity/change event that fires while that read is in flight.
17393
+ this.events.addEventListener("replicator:mature", runCheck);
17394
+ this.events.addEventListener("replication:change", runCheck);
16646
17395
  requestReplicationInfo();
16647
- check();
16648
- this.events.addEventListener("replicator:mature", check);
16649
- this.events.addEventListener("replication:change", check);
17396
+ runCheck();
16650
17397
  return deferred.promise.finally(clear);
16651
17398
  }
16652
17399
  async waitForReplicators(options) {
@@ -18567,6 +19314,7 @@ let SharedLog = (() => {
18567
19314
  if (!ownsSubscriptionEpoch()) {
18568
19315
  return;
18569
19316
  }
19317
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
18570
19318
  // A reconnect can arrive before the previous exact-session recovery tick
18571
19319
  // observes its stale session. Retire that job synchronously so it cannot
18572
19320
  // suppress the replacement session's scheduler in the shared peer slot.
@@ -18709,6 +19457,7 @@ let SharedLog = (() => {
18709
19457
  signal: replicationLifecycleController.signal,
18710
19458
  });
18711
19459
  this.scheduleReplicationInfoV2Recovery(publicKey, replicationLifecycleController);
19460
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
18712
19461
  }
18713
19462
  getClampedReplicas(customValue) {
18714
19463
  if (!customValue) {