@peerbit/shared-log 16.0.21 → 16.0.23

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
@@ -575,6 +575,7 @@ const PERSISTED_RECEIPT_RETRY_MS = 50;
575
575
  const MAX_PERSISTED_RECEIPT_ATTEMPT_MS = 2_000;
576
576
  const MAX_PERSISTED_RECEIPT_REQUESTS_GLOBAL = 8;
577
577
  const MAX_PERSISTED_RECEIPT_REQUESTS_PER_PEER = 2;
578
+ const MAX_PERSISTED_RECEIPT_READINESS_WAITERS = 1_024;
578
579
  const PERSISTED_RECEIPT_INGRESS_PEER_REQUEST_CAPACITY = 16;
579
580
  const PERSISTED_RECEIPT_INGRESS_PEER_HASH_CAPACITY = 8_192;
580
581
  const PERSISTED_RECEIPT_INGRESS_PEER_REQUESTS_PER_SECOND = 8;
@@ -1847,6 +1848,16 @@ let SharedLog = (() => {
1847
1848
  // parallel map so existing capability-number consumers remain unchanged.
1848
1849
  _peerSyncCapabilitySessions;
1849
1850
  _peerSyncCapabilityTimestamps;
1851
+ // design-note: these fields cache a stable, public diagnostics token for the
1852
+ // composite of PeerSession identity, receive epoch, and signed capability
1853
+ // session. They are not consulted to admit or fence asynchronous work. A
1854
+ // separate opaque token is necessary because exposing any of those internal
1855
+ // identities would leak protocol/session values, while PeerSession alone does
1856
+ // not change when receive or capability state is replaced.
1857
+ _persistedReceiptReadinessGenerations;
1858
+ _persistedReceiptReadinessGenerationPrefix;
1859
+ _persistedReceiptReadinessGenerationCounter;
1860
+ _persistedReceiptReadinessWaiters;
1850
1861
  _persistedReceiptStorage;
1851
1862
  _persistedReceiptRequestsInFlight;
1852
1863
  _persistedReceiptRequestsInFlightTotal;
@@ -2121,6 +2132,10 @@ let SharedLog = (() => {
2121
2132
  this._peerSyncCapabilities = new Map();
2122
2133
  this._peerSyncCapabilitySessions = new Map();
2123
2134
  this._peerSyncCapabilityTimestamps = new Map();
2135
+ this._persistedReceiptReadinessGenerations = new WeakMap();
2136
+ this._persistedReceiptReadinessGenerationPrefix = toHexString(randomBytes(8));
2137
+ this._persistedReceiptReadinessGenerationCounter = 0;
2138
+ this._persistedReceiptReadinessWaiters = new Set();
2124
2139
  this._persistedReceiptStorage = undefined;
2125
2140
  this._persistedReceiptRequestsInFlight = new Map();
2126
2141
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -2898,14 +2913,20 @@ let SharedLog = (() => {
2898
2913
  timestamp < previous.timestamp) {
2899
2914
  return false;
2900
2915
  }
2916
+ const nextCapabilities = previous.capabilities | capabilities;
2917
+ const nextTimestamp = previous.timestamp === undefined || timestamp > previous.timestamp
2918
+ ? timestamp
2919
+ : previous.timestamp;
2901
2920
  this._openingSyncCapabilitiesByPeer.set(peerHash, {
2902
2921
  epoch: openingSession,
2903
- capabilities: previous.capabilities | capabilities,
2922
+ capabilities: nextCapabilities,
2904
2923
  transportSession,
2905
- timestamp: previous.timestamp === undefined || timestamp > previous.timestamp
2906
- ? timestamp
2907
- : previous.timestamp,
2924
+ timestamp: nextTimestamp,
2908
2925
  });
2926
+ if (previous.capabilities !== nextCapabilities ||
2927
+ previous.timestamp === undefined) {
2928
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
2929
+ }
2909
2930
  return true;
2910
2931
  }
2911
2932
  this._openingSyncCapabilitiesByPeer.set(peerHash, {
@@ -2914,16 +2935,23 @@ let SharedLog = (() => {
2914
2935
  transportSession,
2915
2936
  timestamp,
2916
2937
  });
2938
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
2917
2939
  return true;
2918
2940
  }
2919
2941
  if (transportSession === undefined || timestamp === undefined) {
2920
2942
  // Test/in-process synthetic contexts predate signed envelope captures.
2921
2943
  // They may exercise capability-number behavior, but can never authorize V2.
2944
+ const readinessChanged = this._peerSyncCapabilities.get(peerHash) !== capabilities ||
2945
+ this._peerSyncCapabilitySessions.has(peerHash) ||
2946
+ this._peerSyncCapabilityTimestamps.has(peerHash);
2922
2947
  this._peerSyncCapabilities.set(peerHash, capabilities);
2923
2948
  this._peerSyncCapabilitySessions.delete(peerHash);
2924
2949
  this._peerSyncCapabilityTimestamps.delete(peerHash);
2925
2950
  this._v2Send.advancePeerCapability(peerHash);
2926
2951
  this._v2Receive.revokePeerCapability(peerHash);
2952
+ if (readinessChanged) {
2953
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
2954
+ }
2927
2955
  return true;
2928
2956
  }
2929
2957
  const previousSession = this._peerSyncCapabilitySessions.get(peerHash);
@@ -2943,6 +2971,9 @@ let SharedLog = (() => {
2943
2971
  const generationAdvanced = !sameTransportSession ||
2944
2972
  (previousCapabilities & senderGrantCapabilityMask) !==
2945
2973
  (nextCapabilities & senderGrantCapabilityMask);
2974
+ const readinessChanged = !sameTransportSession ||
2975
+ previousTimestamp === undefined ||
2976
+ previousCapabilities !== nextCapabilities;
2946
2977
  this._peerSyncCapabilities.set(peerHash, nextCapabilities);
2947
2978
  this._peerSyncCapabilitySessions.set(peerHash, transportSession);
2948
2979
  this._peerSyncCapabilityTimestamps.set(peerHash, previousTimestamp === undefined ||
@@ -2956,6 +2987,9 @@ let SharedLog = (() => {
2956
2987
  // recovery re-solicitation may restart from the base interval.
2957
2988
  this.resetReplicationInfoV2RecoveryEscalation(peerHash);
2958
2989
  }
2990
+ if (readinessChanged) {
2991
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
2992
+ }
2959
2993
  return true;
2960
2994
  }
2961
2995
  promoteReplicationInfoV2ReceiveCapability(target, peerSession) {
@@ -3227,17 +3261,112 @@ let SharedLog = (() => {
3227
3261
  }
3228
3262
  return this.sendFusedRawExchangeHeadsPlan(plan, to, options);
3229
3263
  }
3264
+ persistedReceiptReadinessGeneration(peerSession, receiveEpoch, capabilitySession) {
3265
+ const current = this._persistedReceiptReadinessGenerations.get(peerSession);
3266
+ if (current?.receiveEpoch === receiveEpoch &&
3267
+ current.capabilitySession === capabilitySession) {
3268
+ return current.generation;
3269
+ }
3270
+ const generation = `${this._persistedReceiptReadinessGenerationPrefix}:${(++this
3271
+ ._persistedReceiptReadinessGenerationCounter).toString(36)}`;
3272
+ this._persistedReceiptReadinessGenerations.set(peerSession, {
3273
+ receiveEpoch,
3274
+ capabilitySession,
3275
+ generation,
3276
+ });
3277
+ return generation;
3278
+ }
3279
+ pendingPersistedReceiptReadiness(reason, generation) {
3280
+ return Object.freeze({
3281
+ status: "pending",
3282
+ reason,
3283
+ ...(generation === undefined ? {} : { generation }),
3284
+ });
3285
+ }
3286
+ unsupportedPersistedReceiptReadiness(reason, generation) {
3287
+ return Object.freeze({
3288
+ status: "unsupported",
3289
+ reason,
3290
+ generation,
3291
+ });
3292
+ }
3293
+ dispatchPersistedReceiptReadinessChange(peerHash) {
3294
+ this.events.dispatchEvent(new CustomEvent("persisted-receipt:readiness", { detail: Object.freeze({ peerHash }) }));
3295
+ }
3296
+ persistedReceiptReadinessCandidate(peerHash) {
3297
+ if (this.closed) {
3298
+ return this.pendingPersistedReceiptReadiness("closed");
3299
+ }
3300
+ const peerSession = this._peerSessions.current(peerHash);
3301
+ if (!peerSession) {
3302
+ return this.pendingPersistedReceiptReadiness("no-current-session");
3303
+ }
3304
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
3305
+ const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
3306
+ const generation = this.persistedReceiptReadinessGeneration(peerSession, receiveEpoch, capabilitySession);
3307
+ if (peerSession.phase !== "open" ||
3308
+ !peerSession.isActive() ||
3309
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
3310
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash)) {
3311
+ return this.pendingPersistedReceiptReadiness("session-opening", generation);
3312
+ }
3313
+ if (capabilitySession === undefined ||
3314
+ !this._peerSyncCapabilityTimestamps.has(peerHash)) {
3315
+ return this.pendingPersistedReceiptReadiness("capability-pending", generation);
3316
+ }
3317
+ const capabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
3318
+ if ((capabilities & SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS) === 0) {
3319
+ return this.unsupportedPersistedReceiptReadiness("persisted-receipts-unsupported", generation);
3320
+ }
3321
+ if ((capabilities & SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM) === 0) {
3322
+ return this.unsupportedPersistedReceiptReadiness("replication-confirmation-unsupported", generation);
3323
+ }
3324
+ if (!this._v2Receive.isCurrentActive({
3325
+ peerHash,
3326
+ peerSession,
3327
+ receiveEpoch,
3328
+ senderTransportSession: capabilitySession,
3329
+ })) {
3330
+ return this.pendingPersistedReceiptReadiness("replication-state-pending", generation);
3331
+ }
3332
+ if (!this.uniqueReplicators.has(peerHash)) {
3333
+ return this.pendingPersistedReceiptReadiness("not-replicating", generation);
3334
+ }
3335
+ return {
3336
+ capabilitySession,
3337
+ peerSession,
3338
+ receiveEpoch,
3339
+ generation,
3340
+ };
3341
+ }
3230
3342
  persistedReceiptPeerSession(peerHash) {
3343
+ // This is a hot receipt/transfer-loop predicate. Keep it allocation-light,
3344
+ // while mirroring every exact-session gate in
3345
+ // persistedReceiptReadinessCandidate (which additionally creates public
3346
+ // reason/generation snapshots).
3231
3347
  const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
3232
3348
  const peerSession = this._peerSessions.current(peerHash);
3233
- if (capabilitySession == null ||
3349
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
3350
+ const requiredCapabilities = SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS |
3351
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM;
3352
+ if (this.closed ||
3353
+ capabilitySession == null ||
3234
3354
  !peerSession ||
3235
3355
  peerSession.phase !== "open" ||
3236
- !this._peerSessions.isCurrent(peerHash, peerSession) ||
3356
+ !peerSession.isActive() ||
3357
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
3358
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash) ||
3359
+ !this.uniqueReplicators.has(peerHash) ||
3237
3360
  !this._peerSyncCapabilityTimestamps.has(peerHash) ||
3238
3361
  ((this._peerSyncCapabilities.get(peerHash) ?? 0) &
3239
- SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS) ===
3240
- 0) {
3362
+ requiredCapabilities) !==
3363
+ requiredCapabilities ||
3364
+ !this._v2Receive.isCurrentActive({
3365
+ peerHash,
3366
+ peerSession,
3367
+ receiveEpoch,
3368
+ senderTransportSession: capabilitySession,
3369
+ })) {
3241
3370
  return undefined;
3242
3371
  }
3243
3372
  return { capabilitySession, peerSession };
@@ -3392,7 +3521,7 @@ let SharedLog = (() => {
3392
3521
  }
3393
3522
  return (await this.planEntryLeaderBatch(items, ownershipLifecycleController)).map((plan) => plan.leaders);
3394
3523
  }
3395
- async settlePersistedDelivery(input, replicas, delivery, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(), persistedDeadline, transferOnFirstRound = false) {
3524
+ async settlePersistedDelivery(input, replicas, delivery, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(), persistedDeadline, transferOnFirstRound = false, onFreshLeaderPlan) {
3396
3525
  const minAcks = Math.floor(delivery.minAcks);
3397
3526
  const records = new Map(input.map((record) => [record.canonicalHash, record]));
3398
3527
  if (records.size === 0)
@@ -3451,6 +3580,9 @@ let SharedLog = (() => {
3451
3580
  const hashesByPeer = new Map();
3452
3581
  const entryArray = [...records.values()];
3453
3582
  const leadersByEntry = await this.planPersistedDeliveryLeaders(entryArray, replicas, ownershipLifecycleController);
3583
+ if (!isRoundOwnershipCurrent())
3584
+ continue;
3585
+ onFreshLeaderPlan?.(leadersByEntry, ownershipRevision);
3454
3586
  if (!isRoundOwnershipCurrent())
3455
3587
  continue;
3456
3588
  const selfHash = this.node.identity.publicKey.hashcode();
@@ -3540,6 +3672,31 @@ let SharedLog = (() => {
3540
3672
  current.capabilitySession === captured.capabilitySession &&
3541
3673
  current.peerSession === captured.peerSession);
3542
3674
  };
3675
+ try {
3676
+ await this._v2Send.confirmLatestForPeer({
3677
+ peerHash: peer,
3678
+ peerSession: captured.peerSession,
3679
+ receiverTransportSession: captured.capabilitySession,
3680
+ }, {
3681
+ timeout: getAttemptTimeout(),
3682
+ signal: roundSignal,
3683
+ });
3684
+ }
3685
+ catch {
3686
+ // The exact receiver generation did not prove that it applied
3687
+ // our latest role state. Replan instead of transferring to a
3688
+ // peer that can still make a stale admission decision.
3689
+ if (transferAllOnRound && isPeerRoundCurrent()) {
3690
+ const state = ensureRepairState();
3691
+ for (const hash of hashes)
3692
+ state.hashes.add(hash);
3693
+ }
3694
+ return;
3695
+ }
3696
+ if (!isPeerRoundCurrent()) {
3697
+ purgePeerDeliveryState(peer);
3698
+ return;
3699
+ }
3543
3700
  const repairs = repairsByPeer.get(peer)?.hashes;
3544
3701
  const transferHashes = transferAllOnRound
3545
3702
  ? hashes
@@ -3692,6 +3849,11 @@ let SharedLog = (() => {
3692
3849
  return false;
3693
3850
  }
3694
3851
  const validatedLeaders = await this.planPersistedDeliveryLeaders(entryArray, replicas, ownershipLifecycleController);
3852
+ if (!isRoundOwnershipCurrent())
3853
+ return false;
3854
+ onFreshLeaderPlan?.(validatedLeaders, ownershipRevision);
3855
+ if (!isRoundOwnershipCurrent())
3856
+ return false;
3695
3857
  for (let index = 0; index < entryArray.length; index++) {
3696
3858
  if (signal.aborted || !isRoundOwnershipCurrent()) {
3697
3859
  if (signal.aborted) {
@@ -3762,6 +3924,51 @@ let SharedLog = (() => {
3762
3924
  deadline.dispose();
3763
3925
  }
3764
3926
  }
3927
+ async collectDeferredAppendBackfillExtras(entry, replicas, baseLeaders, nativeDeliveryPlan, ownershipLifecycleController) {
3928
+ const ownershipRevision = this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
3929
+ const ownershipWasStable = this.isReceiveOwnershipSnapshotStable(ownershipRevision);
3930
+ const assignmentExtraLeaders = new Map();
3931
+ const deliveryExtraTargets = new Set();
3932
+ if (nativeDeliveryPlan) {
3933
+ for (const peer of nativeDeliveryPlan.repairTargets) {
3934
+ // The sampled entry leaders are replaced by settlement's fresh plan.
3935
+ // Retain only native repair additions that were outside that base map.
3936
+ if (!baseLeaders.has(peer))
3937
+ deliveryExtraTargets.add(peer);
3938
+ }
3939
+ }
3940
+ else {
3941
+ const selfHash = this.node.identity.publicKey.hashcode();
3942
+ const fullReplicaDeliveryCandidates = await this.getNativeFullReplicaDeliveryCandidates(replicas, selfHash);
3943
+ this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
3944
+ if (replicas >= Math.max(1, fullReplicaDeliveryCandidates.size)) {
3945
+ for (const peer of fullReplicaDeliveryCandidates) {
3946
+ if (!baseLeaders.has(peer)) {
3947
+ assignmentExtraLeaders.set(peer, { intersecting: true });
3948
+ }
3949
+ }
3950
+ }
3951
+ }
3952
+ const referenceLeaders = new Map();
3953
+ for await (const message of createExchangeHeadsMessages(this.log, [
3954
+ entry,
3955
+ ])) {
3956
+ this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
3957
+ await this._mergeLeadersFromGidReferences(message, replicas, referenceLeaders, ownershipLifecycleController, { freshLeaderPlan: true });
3958
+ }
3959
+ this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
3960
+ for (const peer of referenceLeaders.keys()) {
3961
+ deliveryExtraTargets.add(peer);
3962
+ }
3963
+ return {
3964
+ assignmentExtraLeaders,
3965
+ deliveryExtraTargets,
3966
+ extrasOwnershipRevision: ownershipWasStable &&
3967
+ this.isReceiveOwnershipSnapshotStable(ownershipRevision)
3968
+ ? ownershipRevision
3969
+ : undefined,
3970
+ };
3971
+ }
3765
3972
  async _appendDeliverToReplicators(entry, coordinates, minReplicasValue, leaders, selfHash, isLeader, deliveryArg, nativeDeliveryPlan, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
3766
3973
  const throwIfInactive = () => this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
3767
3974
  throwIfInactive();
@@ -3828,7 +4035,7 @@ let SharedLog = (() => {
3828
4035
  if (!delivery) {
3829
4036
  for (const peer of nativeDeliveryPlan.repairTargets) {
3830
4037
  throwIfInactive();
3831
- this.queueAppendBackfill(peer, entryReplicatedForRepair);
4038
+ this.queueAppendBackfill(peer, entryReplicatedForRepair, ownershipLifecycleController);
3832
4039
  }
3833
4040
  if (nativeDeliveryPlan.defaultSendSilent) {
3834
4041
  const rawTargets = this.canUseLiveRawGossip(nativeDeliveryPlan.sendTo, selfHash);
@@ -3888,7 +4095,7 @@ let SharedLog = (() => {
3888
4095
  }
3889
4096
  for (const peer of nativeDeliveryPlan.repairTargets) {
3890
4097
  throwIfInactive();
3891
- this.queueAppendBackfill(peer, entryReplicatedForRepair);
4098
+ this.queueAppendBackfill(peer, entryReplicatedForRepair, ownershipLifecycleController);
3892
4099
  }
3893
4100
  continue;
3894
4101
  }
@@ -3940,7 +4147,7 @@ let SharedLog = (() => {
3940
4147
  // delivery acks, we still need a targeted backfill source of truth for the
3941
4148
  // authoritative recipients or one entry can get stuck at 2/3 replicas
3942
4149
  // forever. Best-effort fallback subscribers are not repair-worthy.
3943
- this.queueAppendBackfill(peer, entryReplicatedForRepair);
4150
+ this.queueAppendBackfill(peer, entryReplicatedForRepair, ownershipLifecycleController);
3944
4151
  }
3945
4152
  if (isLeader) {
3946
4153
  const rawTargets = this.canUseLiveRawGossip(set, selfHash);
@@ -4033,7 +4240,7 @@ let SharedLog = (() => {
4033
4240
  // Direct append delivery is intentionally optimistic. Queue one delayed,
4034
4241
  // batched maybe-sync pass for the intended recipients so stable 3-peer
4035
4242
  // append workloads do not depend on perfect first-try delivery ordering.
4036
- this.queueAppendBackfill(peer, entryReplicatedForRepair);
4243
+ this.queueAppendBackfill(peer, entryReplicatedForRepair, ownershipLifecycleController);
4037
4244
  }
4038
4245
  }
4039
4246
  if (pending.length > 0) {
@@ -4041,7 +4248,7 @@ let SharedLog = (() => {
4041
4248
  throwIfInactive();
4042
4249
  }
4043
4250
  }
4044
- async _mergeLeadersFromGidReferences(message, minReplicasValue, leaders, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
4251
+ async _mergeLeadersFromGidReferences(message, minReplicasValue, leaders, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(), options) {
4045
4252
  const throwIfInactive = () => this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
4046
4253
  throwIfInactive();
4047
4254
  const gidReferences = message.heads[0]?.gidRefrences;
@@ -4057,10 +4264,10 @@ let SharedLog = (() => {
4057
4264
  throwIfInactive();
4058
4265
  let found;
4059
4266
  if (coordinates == null) {
4060
- found = await this.findLeadersFromEntry(gidEntry, minReplicasValue, undefined, ownershipLifecycleController);
4267
+ found = await this.findLeadersFromEntry(gidEntry, minReplicasValue, options?.freshLeaderPlan ? { freshLeaderPlan: true } : undefined, ownershipLifecycleController);
4061
4268
  }
4062
4269
  else {
4063
- found = await this._findLeaders(coordinates, undefined, ownershipLifecycleController);
4270
+ found = await this._findLeaders(coordinates, options?.freshLeaderPlan ? { freshLeaderPlan: true } : undefined, ownershipLifecycleController);
4064
4271
  }
4065
4272
  throwIfInactive();
4066
4273
  for (const [key, value] of found) {
@@ -4322,7 +4529,9 @@ let SharedLog = (() => {
4322
4529
  (this._instanceLifecycle?._receiveOwnershipRevision ?? 0) === revision);
4323
4530
  }
4324
4531
  canCacheLeaderSelectionContext(options) {
4325
- return options?.roleAge == null && options?.candidates == null;
4532
+ return (options?.roleAge == null &&
4533
+ options?.candidates == null &&
4534
+ options?.freshLeaderPlan !== true);
4326
4535
  }
4327
4536
  cloneLeaderSelectionContext(context) {
4328
4537
  return {
@@ -5099,8 +5308,11 @@ let SharedLog = (() => {
5099
5308
  ? checkedPruneCoordinator.fencePeerRemoval(keyHash)
5100
5309
  : undefined;
5101
5310
  const blockPeerReceiveAdmission = () => {
5102
- releaseReceiveCleanupGate ??=
5103
- this._peerSessions.acquireReceiveCleanupGate(keyHash);
5311
+ if (!releaseReceiveCleanupGate) {
5312
+ releaseReceiveCleanupGate =
5313
+ this._peerSessions.acquireReceiveCleanupGate(keyHash);
5314
+ this.dispatchPersistedReceiptReadinessChange(keyHash);
5315
+ }
5104
5316
  };
5105
5317
  if (!isMe && !isSpeculativePeerRemoval) {
5106
5318
  // Revoke this peer's receipts synchronously, before this removal can
@@ -5288,7 +5500,10 @@ let SharedLog = (() => {
5288
5500
  removalCallCompleted = true;
5289
5501
  }
5290
5502
  finally {
5291
- releaseReceiveCleanupGate?.();
5503
+ if (releaseReceiveCleanupGate) {
5504
+ releaseReceiveCleanupGate();
5505
+ this.dispatchPersistedReceiptReadinessChange(keyHash);
5506
+ }
5292
5507
  if (replicationInfoRecoveryEpochAdvanced &&
5293
5508
  ownsReplicationOwnershipLifecycle() &&
5294
5509
  ownsReplicationLifecycle() &&
@@ -6648,8 +6863,7 @@ let SharedLog = (() => {
6648
6863
  }
6649
6864
  });
6650
6865
  }
6651
- flushAppendBackfill(repairLifecycleController = this._instanceLifecycle
6652
- ?.ownershipLifecycleController) {
6866
+ flushAppendBackfill(repairLifecycleController) {
6653
6867
  if (!this.isRepairLifecycleActive(repairLifecycleController) ||
6654
6868
  this._appendBackfillPendingByTarget.size === 0) {
6655
6869
  return;
@@ -6662,9 +6876,7 @@ let SharedLog = (() => {
6662
6876
  }, repairLifecycleController);
6663
6877
  }
6664
6878
  }
6665
- queueAppendBackfill(target, entry) {
6666
- const repairLifecycleController = this._instanceLifecycle
6667
- ?.ownershipLifecycleController;
6879
+ queueAppendBackfill(target, entry, repairLifecycleController) {
6668
6880
  if (!this.isRepairLifecycleActive(repairLifecycleController)) {
6669
6881
  return;
6670
6882
  }
@@ -6695,6 +6907,53 @@ let SharedLog = (() => {
6695
6907
  this._repairRetryTimers.add(timer);
6696
6908
  this._appendBackfillTimer = timer;
6697
6909
  }
6910
+ queuePersistedAppendBackfill(source, leaders, replicas, repairLifecycleController, ownershipRevision) {
6911
+ try {
6912
+ if (!this.isRepairLifecycleActive(repairLifecycleController) ||
6913
+ !this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
6914
+ return;
6915
+ }
6916
+ const assignmentLeaders = new Map(leaders);
6917
+ const deliveryTargets = new Set(leaders.keys());
6918
+ if (source.extrasOwnershipRevision === ownershipRevision) {
6919
+ for (const [peer, sample] of source.assignmentExtraLeaders) {
6920
+ assignmentLeaders.set(peer, sample);
6921
+ }
6922
+ for (const peer of source.deliveryExtraTargets) {
6923
+ deliveryTargets.add(peer);
6924
+ }
6925
+ }
6926
+ const repairEntry = this.createEntryReplicatedForRepair({
6927
+ entry: source.entry,
6928
+ coordinates: source.coordinates,
6929
+ leaders: assignmentLeaders,
6930
+ replicas,
6931
+ });
6932
+ const selfHash = this.node.identity.publicKey.hashcode();
6933
+ for (const peer of assignmentLeaders.keys()) {
6934
+ deliveryTargets.add(peer);
6935
+ }
6936
+ for (const peer of deliveryTargets) {
6937
+ if (peer === selfHash)
6938
+ continue;
6939
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision))
6940
+ return;
6941
+ try {
6942
+ this.queueAppendBackfill(peer, repairEntry, repairLifecycleController);
6943
+ }
6944
+ catch (error) {
6945
+ if (this.isRepairLifecycleActive(repairLifecycleController)) {
6946
+ logger.error(error);
6947
+ }
6948
+ }
6949
+ }
6950
+ }
6951
+ catch (error) {
6952
+ if (this.isRepairLifecycleActive(repairLifecycleController)) {
6953
+ logger.error(error);
6954
+ }
6955
+ }
6956
+ }
6698
6957
  dispatchMaybeMissingEntries(target, entries, options, repairLifecycleController = this._instanceLifecycle
6699
6958
  ?.ownershipLifecycleController) {
6700
6959
  if (entries.size === 0 ||
@@ -7850,6 +8109,10 @@ let SharedLog = (() => {
7850
8109
  let committedHashes;
7851
8110
  let persistedAppendCommit;
7852
8111
  let persistedPlanningRecord;
8112
+ let persistedBackfillSource;
8113
+ let persistedBackfillLeaders;
8114
+ let persistedBackfillOwnershipRevision;
8115
+ let localAppendProcessed = false;
7853
8116
  if (persistedDelivery) {
7854
8117
  appendOptions.__peerbitOnLocalCommit = (hashes, entries) => {
7855
8118
  // The lower log reports the exact hashes immediately after their
@@ -7884,11 +8147,24 @@ let SharedLog = (() => {
7884
8147
  await this.processLocalAppend(processingEntry, result.removed, options, {
7885
8148
  minReplicasValue,
7886
8149
  appendFacts: persistedAppendCommit,
8150
+ // Persisted settlement must confirm each exact receiver generation before
8151
+ // using its transfer as receipt evidence. Keep the optimistic append path
8152
+ // out of that ordering decision.
8153
+ captureDeferredBackfillSource: persistedDelivery
8154
+ ? (source) => {
8155
+ persistedBackfillSource = source;
8156
+ }
8157
+ : undefined,
7887
8158
  ownershipLifecycleController,
7888
8159
  });
8160
+ localAppendProcessed = true;
7889
8161
  throwIfDeliveryAborted();
7890
8162
  if (persistedDelivery && persistedDeadline) {
7891
- await this.settlePersistedDelivery([persistedPlanningRecord], minReplicasValue, persistedDelivery, ownershipLifecycleController, persistedDeadline);
8163
+ await this.settlePersistedDelivery([persistedPlanningRecord], minReplicasValue, persistedDelivery, ownershipLifecycleController, persistedDeadline, true, (leadersByEntry, ownershipRevision) => {
8164
+ const leaders = leadersByEntry[0];
8165
+ persistedBackfillLeaders = leaders ? new Map(leaders) : undefined;
8166
+ persistedBackfillOwnershipRevision = ownershipRevision;
8167
+ });
7892
8168
  }
7893
8169
  this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
7894
8170
  return result;
@@ -7900,6 +8176,20 @@ let SharedLog = (() => {
7900
8176
  throw error;
7901
8177
  }
7902
8178
  finally {
8179
+ if (persistedBackfillSource &&
8180
+ persistedBackfillLeaders &&
8181
+ persistedBackfillOwnershipRevision !== undefined &&
8182
+ localAppendProcessed) {
8183
+ // A persisted quorum changes the return condition, not the configured
8184
+ // replication degree. Reuse settlement's latest fresh leader plan rather
8185
+ // than carrying an optimistic target or extending the receipt deadline
8186
+ // with another plan. Fence best-effort repair to this append's ownership
8187
+ // generation, and never let it mask the primary result.
8188
+ try {
8189
+ this.queuePersistedAppendBackfill(persistedBackfillSource, persistedBackfillLeaders, minReplicasValue, ownershipLifecycleController, persistedBackfillOwnershipRevision);
8190
+ }
8191
+ catch { }
8192
+ }
7903
8193
  persistedDeadline?.dispose();
7904
8194
  }
7905
8195
  }
@@ -10544,7 +10834,28 @@ let SharedLog = (() => {
10544
10834
  }, ownershipLifecycleController));
10545
10835
  }
10546
10836
  this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
10547
- if (options?.target !== "none") {
10837
+ if (properties.captureDeferredBackfillSource) {
10838
+ let extras = {
10839
+ assignmentExtraLeaders: new Map(),
10840
+ deliveryExtraTargets: new Set(),
10841
+ };
10842
+ try {
10843
+ extras = await this.collectDeferredAppendBackfillExtras(entry, properties.minReplicasValue, leaders, nativeDeliveryPlan, ownershipLifecycleController);
10844
+ }
10845
+ catch (error) {
10846
+ if (this.isRepairLifecycleActive(ownershipLifecycleController)) {
10847
+ logger.error(error);
10848
+ }
10849
+ }
10850
+ this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
10851
+ properties.captureDeferredBackfillSource({
10852
+ entry,
10853
+ coordinates: [...coordinates],
10854
+ ...extras,
10855
+ });
10856
+ }
10857
+ if (options?.target !== "none" &&
10858
+ !properties.captureDeferredBackfillSource) {
10548
10859
  const hasDelivery = !(deliveryArg === undefined || deliveryArg === false);
10549
10860
  if (target === "all" && hasDelivery) {
10550
10861
  throw new Error(`delivery options are not supported with target="all"; fanout broadcast is fire-and-forward`);
@@ -10852,6 +11163,10 @@ let SharedLog = (() => {
10852
11163
  this._peerSyncCapabilities = new Map();
10853
11164
  this._peerSyncCapabilitySessions = new Map();
10854
11165
  this._peerSyncCapabilityTimestamps = new Map();
11166
+ this._persistedReceiptReadinessGenerations = new WeakMap();
11167
+ this._persistedReceiptReadinessGenerationPrefix = toHexString(randomBytes(8));
11168
+ this._persistedReceiptReadinessGenerationCounter = 0;
11169
+ this._persistedReceiptReadinessWaiters = new Set();
10855
11170
  this._persistedReceiptStorage = undefined;
10856
11171
  this._persistedReceiptRequestsInFlight = new Map();
10857
11172
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -12117,6 +12432,7 @@ let SharedLog = (() => {
12117
12432
  }
12118
12433
  this.cleanupPendingIHavePeer(peerHash);
12119
12434
  this.cleanupCheckedPrunePeer(peerHash, ownershipLifecycleController, this._checkedPrune);
12435
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
12120
12436
  }
12121
12437
  cleanupPendingIHavePeer(peerHash) {
12122
12438
  for (const [hash, pending] of this._pendingIHave) {
@@ -12139,6 +12455,7 @@ let SharedLog = (() => {
12139
12455
  receiveEpoch,
12140
12456
  });
12141
12457
  }
12458
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
12142
12459
  }
12143
12460
  async resolveCandidatePeersForHash(hash, options) {
12144
12461
  if (options?.signal?.aborted)
@@ -13312,6 +13629,7 @@ let SharedLog = (() => {
13312
13629
  this._peerSyncCapabilities?.clear();
13313
13630
  this._peerSyncCapabilitySessions?.clear();
13314
13631
  this._peerSyncCapabilityTimestamps?.clear();
13632
+ this._persistedReceiptReadinessGenerations = new WeakMap();
13315
13633
  this._persistedReceiptStorage = undefined;
13316
13634
  this._persistedReceiptRequestsInFlight?.clear();
13317
13635
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -15427,10 +15745,12 @@ let SharedLog = (() => {
15427
15745
  return;
15428
15746
  }
15429
15747
  else if (msg instanceof ReplicationInfoV2AppliedMessage) {
15430
- this._v2Send.acceptApplied(msg, {
15748
+ if (this._v2Send.acceptApplied(msg, {
15431
15749
  from: context.from,
15432
15750
  receiverTransportSession: context.message.header.session,
15433
- });
15751
+ })) {
15752
+ this.dispatchPersistedReceiptReadinessChange(receiveFromHash);
15753
+ }
15434
15754
  return;
15435
15755
  }
15436
15756
  else if (isReplicationInfoV2Message(msg)) {
@@ -15584,6 +15904,12 @@ let SharedLog = (() => {
15584
15904
  this._peerSyncCapabilitySessions.get(lane.fromHash) ===
15585
15905
  context.message.header.session &&
15586
15906
  this._peerSyncCapabilityTimestamps.has(lane.fromHash) &&
15907
+ this._v2Receive.isCurrentActive({
15908
+ peerHash: lane.fromHash,
15909
+ peerSession: session,
15910
+ receiveEpoch: lane.receiveEpoch,
15911
+ senderTransportSession: context.message.header.session,
15912
+ }) &&
15587
15913
  this.isRepairLifecycleActive(lane.ownershipLifecycleController));
15588
15914
  }
15589
15915
  admitPersistedReceiptIngress(peer, transportSession, hashCount, now = Date.now()) {
@@ -16048,6 +16374,7 @@ let SharedLog = (() => {
16048
16374
  // A committed V2 announcement is applied progress: the peer answers,
16049
16375
  // so recovery re-solicitation may restart from the base interval.
16050
16376
  this.resetReplicationInfoV2RecoveryEscalation(fromHash);
16377
+ this.dispatchPersistedReceiptReadinessChange(fromHash);
16051
16378
  });
16052
16379
  }
16053
16380
  finally {
@@ -16322,18 +16649,401 @@ let SharedLog = (() => {
16322
16649
  }
16323
16650
  throwIfInactive();
16324
16651
  }
16652
+ nudgePersistedReceiptPeerReadiness(publicKey) {
16653
+ if (this.closed)
16654
+ return;
16655
+ const peerHash = publicKey.hashcode();
16656
+ const peerSession = this._peerSessions.current(peerHash);
16657
+ if (!peerSession ||
16658
+ peerSession.phase === "departing" ||
16659
+ (peerSession.phase === "opening" &&
16660
+ !peerSession.openingBarrierActive)) {
16661
+ // A barrier rejection deliberately leaves the current session in its
16662
+ // fail-closed opening phase after the barrier window has settled. Ask the
16663
+ // authenticated peer for a fresh subscriber snapshot so the replacement
16664
+ // session can recover; never rotate a barrier that is still in flight.
16665
+ this.requestSubscriberSnapshotForCapability(publicKey);
16666
+ return;
16667
+ }
16668
+ if (peerSession.phase !== "open" || !peerSession.isActive()) {
16669
+ return;
16670
+ }
16671
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
16672
+ this.promoteReplicationInfoV2ReceiveCapability(publicKey, peerSession);
16673
+ this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
16674
+ peerHash,
16675
+ peerSession,
16676
+ receiveEpoch,
16677
+ });
16678
+ this._v2Receive.ensureRequestProgress({
16679
+ peerHash,
16680
+ peerSession,
16681
+ receiveEpoch,
16682
+ });
16683
+ this.scheduleReplicationInfoV2Recovery(publicKey);
16684
+ }
16685
+ /**
16686
+ * Inspect whether one public key's exact current connection generation can
16687
+ * supply persisted-receipt evidence. The returned object is frozen and never
16688
+ * exposes the internal PeerSession token. When `entries` are supplied, the
16689
+ * peer must also be present in a fresh leader plan for every entry.
16690
+ *
16691
+ * This is advisory preflight state. Persisted delivery repeats every
16692
+ * generation, leadership, ownership and storage check at receipt time; a
16693
+ * `ready` snapshot is never itself authority to dispose a source copy.
16694
+ */
16695
+ async getPersistedReceiptPeerReadiness(key, options = {}) {
16696
+ return this.inspectPersistedReceiptPeerReadiness(key, options);
16697
+ }
16698
+ async inspectPersistedReceiptPeerReadiness(key, options, assertContinue) {
16699
+ // Capture and validate caller-owned planning input before consulting live
16700
+ // peer state. Invalid options must not appear to work merely because the
16701
+ // peer is currently absent, then fail later when the same session connects.
16702
+ const entries = options.entries ? [...options.entries] : [];
16703
+ const replicas = options.replicas ??
16704
+ (entries.length > 0 ? this.replicas.min.getValue(this) : undefined);
16705
+ if (replicas !== undefined) {
16706
+ if (!Number.isSafeInteger(replicas) || replicas <= 0) {
16707
+ throw new RangeError("Persisted-receipt readiness replicas must be a positive integer");
16708
+ }
16709
+ checkMinReplicasLimit(replicas);
16710
+ }
16711
+ const peerHash = key.hashcode();
16712
+ const captured = this.persistedReceiptReadinessCandidate(peerHash);
16713
+ if ("status" in captured) {
16714
+ return captured;
16715
+ }
16716
+ assertContinue?.();
16717
+ if (entries.length > 0) {
16718
+ const ownershipLifecycleController = this.captureReplicationOwnershipLifecycle();
16719
+ const ownershipRevision = this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
16720
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
16721
+ return this.pendingPersistedReceiptReadiness("ownership-changing", captured.generation);
16722
+ }
16723
+ for (const entry of entries) {
16724
+ assertContinue?.();
16725
+ const leaders = await this.findLeadersFromEntry(entry, replicas, { freshLeaderPlan: true }, ownershipLifecycleController);
16726
+ assertContinue?.();
16727
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
16728
+ return this.pendingPersistedReceiptReadiness("ownership-changing", captured.generation);
16729
+ }
16730
+ const current = this.persistedReceiptReadinessCandidate(peerHash);
16731
+ if ("status" in current) {
16732
+ return current;
16733
+ }
16734
+ if (current.peerSession !== captured.peerSession ||
16735
+ current.receiveEpoch !== captured.receiveEpoch ||
16736
+ current.capabilitySession !== captured.capabilitySession) {
16737
+ return this.pendingPersistedReceiptReadiness("replication-state-pending", current.generation);
16738
+ }
16739
+ if (!leaders.has(peerHash)) {
16740
+ return this.pendingPersistedReceiptReadiness("not-entry-leader", captured.generation);
16741
+ }
16742
+ }
16743
+ }
16744
+ assertContinue?.();
16745
+ const current = this.persistedReceiptReadinessCandidate(peerHash);
16746
+ if ("status" in current) {
16747
+ return current;
16748
+ }
16749
+ if (current.peerSession !== captured.peerSession ||
16750
+ current.receiveEpoch !== captured.receiveEpoch ||
16751
+ current.capabilitySession !== captured.capabilitySession) {
16752
+ return this.pendingPersistedReceiptReadiness("replication-state-pending", current.generation);
16753
+ }
16754
+ if (!this._v2Send.isLatestConfirmedForPeer({
16755
+ peerHash,
16756
+ peerSession: captured.peerSession,
16757
+ receiverTransportSession: captured.capabilitySession,
16758
+ })) {
16759
+ return this.pendingPersistedReceiptReadiness("replication-confirmation-pending", captured.generation);
16760
+ }
16761
+ return Object.freeze({
16762
+ status: "ready",
16763
+ generation: captured.generation,
16764
+ });
16765
+ }
16766
+ /**
16767
+ * Wait for a public key's current (or replacement) connection generation to
16768
+ * become persisted-receipt ready. Transition listeners are installed before
16769
+ * the first asynchronous inspection, and a bounded recovery tick repairs
16770
+ * missed subscriber/capability wakes without retaining stale PeerSessions.
16771
+ * This waiter is advisory only; the following persisted delivery remains the
16772
+ * operation that proves the requested remote durability quorum.
16773
+ */
16774
+ async waitForPersistedReceiptPeerReadiness(key, options = {}) {
16775
+ if (this.closed) {
16776
+ throw new ClosedError();
16777
+ }
16778
+ const timeoutMs = options.timeout ?? this.waitForReplicatorTimeout;
16779
+ if (!Number.isSafeInteger(timeoutMs) ||
16780
+ timeoutMs <= 0 ||
16781
+ timeoutMs > MAX_PERSISTED_DELIVERY_TIMEOUT_MS) {
16782
+ throw new RangeError(`Persisted-receipt readiness timeout must be an integer from 1 to ${MAX_PERSISTED_DELIVERY_TIMEOUT_MS} milliseconds`);
16783
+ }
16784
+ if (options.signal?.aborted) {
16785
+ throw options.signal.reason instanceof Error
16786
+ ? options.signal.reason
16787
+ : new AbortError("Persisted-receipt readiness wait aborted");
16788
+ }
16789
+ // Capture caller-owned inputs before reserving a waiter slot. A throwing
16790
+ // iterator/key implementation must not strand capacity permanently.
16791
+ const entries = options.entries ? [...options.entries] : undefined;
16792
+ const inspectOptions = {
16793
+ ...(entries ? { entries } : {}),
16794
+ ...(options.replicas === undefined ? {} : { replicas: options.replicas }),
16795
+ };
16796
+ const peerHash = key.hashcode();
16797
+ const waiterSet = this._persistedReceiptReadinessWaiters;
16798
+ if (waiterSet.size >= MAX_PERSISTED_RECEIPT_READINESS_WAITERS) {
16799
+ throw new RangeError(`Too many pending persisted-receipt readiness waits (maximum ${MAX_PERSISTED_RECEIPT_READINESS_WAITERS})`);
16800
+ }
16801
+ const waiterToken = {};
16802
+ waiterSet.add(waiterToken);
16803
+ const deadline = Date.now() + timeoutMs;
16804
+ const closeSignal = this._closeController.signal;
16805
+ const operationController = new AbortController();
16806
+ const operationSignal = AbortSignal.any([options.signal, closeSignal, operationController.signal].filter((value) => value !== undefined));
16807
+ const deferred = pDefer();
16808
+ let settled = false;
16809
+ let checkScheduled = false;
16810
+ let checkInFlight = false;
16811
+ let rerun = false;
16812
+ let recoveryTimer;
16813
+ let confirmationController;
16814
+ let lastSnapshot;
16815
+ const createTimeoutError = () => {
16816
+ const suffix = lastSnapshot
16817
+ ? ` (last status: ${lastSnapshot.status}${"reason" in lastSnapshot ? `/${lastSnapshot.reason}` : ""})`
16818
+ : "";
16819
+ return new TimeoutError(`Timeout waiting for persisted-receipt readiness from ${peerHash}${suffix}`);
16820
+ };
16821
+ const cleanup = () => {
16822
+ waiterSet.delete(waiterToken);
16823
+ this.events.removeEventListener("persisted-receipt:readiness", onReadinessChange);
16824
+ this.events.removeEventListener("replication:change", onRoleChange);
16825
+ this.events.removeEventListener("replicator:mature", onRoleChange);
16826
+ options.signal?.removeEventListener("abort", onCallerAbort);
16827
+ closeSignal.removeEventListener("abort", onClose);
16828
+ if (recoveryTimer) {
16829
+ clearTimeout(recoveryTimer);
16830
+ recoveryTimer = undefined;
16831
+ }
16832
+ confirmationController?.abort(new AbortError("Persisted-receipt readiness generation changed"));
16833
+ confirmationController = undefined;
16834
+ operationController.abort(new AbortError("Persisted-receipt readiness wait settled"));
16835
+ };
16836
+ const resolve = (snapshot) => {
16837
+ if (settled)
16838
+ return;
16839
+ settled = true;
16840
+ cleanup();
16841
+ deferred.resolve(snapshot);
16842
+ };
16843
+ const reject = (error) => {
16844
+ if (settled)
16845
+ return;
16846
+ settled = true;
16847
+ cleanup();
16848
+ deferred.reject(error instanceof Error ? error : new Error(String(error)));
16849
+ };
16850
+ const onCallerAbort = () => reject(options.signal?.reason instanceof Error
16851
+ ? options.signal.reason
16852
+ : new AbortError("Persisted-receipt readiness wait aborted"));
16853
+ const onClose = () => reject(new ClosedError());
16854
+ const continueWait = () => {
16855
+ if (settled)
16856
+ return false;
16857
+ if (closeSignal.aborted) {
16858
+ onClose();
16859
+ return false;
16860
+ }
16861
+ if (options.signal?.aborted) {
16862
+ onCallerAbort();
16863
+ return false;
16864
+ }
16865
+ if (Date.now() >= deadline) {
16866
+ reject(createTimeoutError());
16867
+ return false;
16868
+ }
16869
+ return true;
16870
+ };
16871
+ const assertInspectionCurrent = () => {
16872
+ if (!continueWait()) {
16873
+ throw new AbortError("Persisted-receipt readiness wait settled");
16874
+ }
16875
+ };
16876
+ const armRecoveryTick = () => {
16877
+ if (settled || recoveryTimer)
16878
+ return;
16879
+ const delayMs = Math.max(50, Math.min(1_000, this.waitForReplicatorRequestIntervalMs));
16880
+ recoveryTimer = setTimeout(() => {
16881
+ recoveryTimer = undefined;
16882
+ if (!continueWait())
16883
+ return;
16884
+ this.nudgePersistedReceiptPeerReadiness(key);
16885
+ scheduleCheck();
16886
+ }, delayMs);
16887
+ recoveryTimer.unref?.();
16888
+ };
16889
+ const runCheck = async () => {
16890
+ checkScheduled = false;
16891
+ if (!continueWait())
16892
+ return;
16893
+ if (checkInFlight) {
16894
+ rerun = true;
16895
+ return;
16896
+ }
16897
+ checkInFlight = true;
16898
+ try {
16899
+ let snapshot = await this.inspectPersistedReceiptPeerReadiness(key, inspectOptions, assertInspectionCurrent);
16900
+ lastSnapshot = snapshot;
16901
+ if (!continueWait())
16902
+ return;
16903
+ if (rerun)
16904
+ return;
16905
+ if (snapshot.status === "ready") {
16906
+ // A wake observed while the asynchronous inspection was running may
16907
+ // already have invalidated this snapshot. Drain that coalesced wake
16908
+ // before publishing readiness.
16909
+ resolve(snapshot);
16910
+ return;
16911
+ }
16912
+ if (snapshot.status === "pending" &&
16913
+ snapshot.reason === "replication-confirmation-pending") {
16914
+ const target = this.persistedReceiptPeerSession(peerHash);
16915
+ if (target) {
16916
+ const currentConfirmationController = new AbortController();
16917
+ confirmationController = currentConfirmationController;
16918
+ try {
16919
+ await this._v2Send.confirmLatestForPeer({
16920
+ peerHash,
16921
+ peerSession: target.peerSession,
16922
+ receiverTransportSession: target.capabilitySession,
16923
+ }, {
16924
+ timeout: Math.max(1, deadline - Date.now()),
16925
+ signal: AbortSignal.any([
16926
+ operationSignal,
16927
+ currentConfirmationController.signal,
16928
+ ]),
16929
+ });
16930
+ }
16931
+ catch (error) {
16932
+ if (!continueWait())
16933
+ return;
16934
+ if (!(error instanceof AbortError)) {
16935
+ throw error;
16936
+ }
16937
+ rerun = true;
16938
+ }
16939
+ finally {
16940
+ if (confirmationController === currentConfirmationController) {
16941
+ confirmationController = undefined;
16942
+ }
16943
+ }
16944
+ if (!continueWait())
16945
+ return;
16946
+ snapshot = await this.inspectPersistedReceiptPeerReadiness(key, inspectOptions, assertInspectionCurrent);
16947
+ lastSnapshot = snapshot;
16948
+ if (!continueWait())
16949
+ return;
16950
+ if (rerun)
16951
+ return;
16952
+ if (snapshot.status === "ready") {
16953
+ resolve(snapshot);
16954
+ return;
16955
+ }
16956
+ }
16957
+ }
16958
+ if (!continueWait())
16959
+ return;
16960
+ this.nudgePersistedReceiptPeerReadiness(key);
16961
+ }
16962
+ catch (error) {
16963
+ if (!settled)
16964
+ reject(error);
16965
+ }
16966
+ finally {
16967
+ checkInFlight = false;
16968
+ if (!settled && rerun) {
16969
+ rerun = false;
16970
+ scheduleCheck();
16971
+ }
16972
+ else {
16973
+ armRecoveryTick();
16974
+ }
16975
+ }
16976
+ };
16977
+ const scheduleCheck = (interruptConfirmation = false) => {
16978
+ if (settled)
16979
+ return;
16980
+ if (recoveryTimer) {
16981
+ clearTimeout(recoveryTimer);
16982
+ recoveryTimer = undefined;
16983
+ }
16984
+ if (checkInFlight) {
16985
+ rerun = true;
16986
+ if (interruptConfirmation) {
16987
+ confirmationController?.abort(new AbortError("Persisted-receipt readiness changed during confirmation"));
16988
+ }
16989
+ return;
16990
+ }
16991
+ if (checkScheduled)
16992
+ return;
16993
+ checkScheduled = true;
16994
+ void Promise.resolve().then(runCheck);
16995
+ };
16996
+ const onReadinessChange = (event) => {
16997
+ if (event.detail.peerHash === peerHash)
16998
+ scheduleCheck(true);
16999
+ };
17000
+ const onRoleChange = (event) => {
17001
+ if ((entries?.length ?? 0) > 0 ||
17002
+ event.detail.publicKey.hashcode() === peerHash) {
17003
+ scheduleCheck(true);
17004
+ }
17005
+ };
17006
+ // Register wake sources before the first state inspection. EventTarget does
17007
+ // not replay a transition that fired between an async check and registration.
17008
+ this.events.addEventListener("persisted-receipt:readiness", onReadinessChange);
17009
+ this.events.addEventListener("replication:change", onRoleChange);
17010
+ this.events.addEventListener("replicator:mature", onRoleChange);
17011
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
17012
+ closeSignal.addEventListener("abort", onClose, { once: true });
17013
+ if (options.signal?.aborted) {
17014
+ onCallerAbort();
17015
+ }
17016
+ else if (closeSignal.aborted) {
17017
+ onClose();
17018
+ }
17019
+ else {
17020
+ scheduleCheck();
17021
+ }
17022
+ const timeout = setTimeout(() => reject(createTimeoutError()), timeoutMs);
17023
+ timeout.unref?.();
17024
+ return deferred.promise.finally(() => clearTimeout(timeout));
17025
+ }
16325
17026
  async waitForReplicator(key, options) {
17027
+ if (options?.signal?.aborted) {
17028
+ throw new AbortError();
17029
+ }
16326
17030
  const deferred = pDefer();
16327
17031
  const timeoutMs = options?.timeout ?? this.waitForReplicatorTimeout;
16328
17032
  const resolvedRoleAge = options?.eager
16329
17033
  ? undefined
16330
17034
  : (options?.roleAge ?? (await this.getDefaultMinRoleAge()));
17035
+ if (options?.signal?.aborted) {
17036
+ throw new AbortError();
17037
+ }
16331
17038
  let settled = false;
16332
17039
  let timer;
16333
17040
  let requestTimer;
17041
+ let checkInFlight = false;
17042
+ let checkAgain = false;
16334
17043
  const clear = () => {
16335
- this.events.removeEventListener("replicator:mature", check);
16336
- this.events.removeEventListener("replication:change", check);
17044
+ checkAgain = false;
17045
+ this.events.removeEventListener("replicator:mature", runCheck);
17046
+ this.events.removeEventListener("replication:change", runCheck);
16337
17047
  options?.signal?.removeEventListener("abort", onAbort);
16338
17048
  if (timer != null) {
16339
17049
  clearTimeout(timer);
@@ -16452,10 +17162,32 @@ let SharedLog = (() => {
16452
17162
  await iterator?.close();
16453
17163
  }
16454
17164
  };
17165
+ const runCheck = () => {
17166
+ if (settled)
17167
+ return;
17168
+ if (checkInFlight) {
17169
+ checkAgain = true;
17170
+ return;
17171
+ }
17172
+ // Reserve synchronously before `check()` can dispatch/re-enter from an
17173
+ // index implementation's first `next()` call.
17174
+ checkInFlight = true;
17175
+ void check()
17176
+ .catch((error) => reject(error instanceof Error ? error : new Error(String(error))))
17177
+ .finally(() => {
17178
+ checkInFlight = false;
17179
+ if (!settled && checkAgain) {
17180
+ checkAgain = false;
17181
+ runCheck();
17182
+ }
17183
+ });
17184
+ };
17185
+ // Register before the first asynchronous index read. EventTarget does not
17186
+ // replay a maturity/change event that fires while that read is in flight.
17187
+ this.events.addEventListener("replicator:mature", runCheck);
17188
+ this.events.addEventListener("replication:change", runCheck);
16455
17189
  requestReplicationInfo();
16456
- check();
16457
- this.events.addEventListener("replicator:mature", check);
16458
- this.events.addEventListener("replication:change", check);
17190
+ runCheck();
16459
17191
  return deferred.promise.finally(clear);
16460
17192
  }
16461
17193
  async waitForReplicators(options) {
@@ -18376,6 +19108,7 @@ let SharedLog = (() => {
18376
19108
  if (!ownsSubscriptionEpoch()) {
18377
19109
  return;
18378
19110
  }
19111
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
18379
19112
  // A reconnect can arrive before the previous exact-session recovery tick
18380
19113
  // observes its stale session. Retire that job synchronously so it cannot
18381
19114
  // suppress the replacement session's scheduler in the shared peer slot.
@@ -18518,6 +19251,7 @@ let SharedLog = (() => {
18518
19251
  signal: replicationLifecycleController.signal,
18519
19252
  });
18520
19253
  this.scheduleReplicationInfoV2Recovery(publicKey, replicationLifecycleController);
19254
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
18521
19255
  }
18522
19256
  getClampedReplicas(customValue) {
18523
19257
  if (!customValue) {