@peerbit/shared-log 16.0.22 → 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,16 +3261,102 @@ 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
3349
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
3234
3350
  const requiredCapabilities = SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS |
3235
3351
  SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM;
3236
- if (capabilitySession == null ||
3352
+ if (this.closed ||
3353
+ capabilitySession == null ||
3237
3354
  !peerSession ||
3238
3355
  peerSession.phase !== "open" ||
3239
- !this._peerSessions.isCurrent(peerHash, peerSession) ||
3356
+ !peerSession.isActive() ||
3357
+ this._peerSessions.isReplicationInfoBlocked(peerHash) ||
3358
+ !this._peerSessions.isReceiveCleanupGateOpen(peerHash) ||
3359
+ !this.uniqueReplicators.has(peerHash) ||
3240
3360
  !this._peerSyncCapabilityTimestamps.has(peerHash) ||
3241
3361
  ((this._peerSyncCapabilities.get(peerHash) ?? 0) &
3242
3362
  requiredCapabilities) !==
@@ -5188,8 +5308,11 @@ let SharedLog = (() => {
5188
5308
  ? checkedPruneCoordinator.fencePeerRemoval(keyHash)
5189
5309
  : undefined;
5190
5310
  const blockPeerReceiveAdmission = () => {
5191
- releaseReceiveCleanupGate ??=
5192
- this._peerSessions.acquireReceiveCleanupGate(keyHash);
5311
+ if (!releaseReceiveCleanupGate) {
5312
+ releaseReceiveCleanupGate =
5313
+ this._peerSessions.acquireReceiveCleanupGate(keyHash);
5314
+ this.dispatchPersistedReceiptReadinessChange(keyHash);
5315
+ }
5193
5316
  };
5194
5317
  if (!isMe && !isSpeculativePeerRemoval) {
5195
5318
  // Revoke this peer's receipts synchronously, before this removal can
@@ -5377,7 +5500,10 @@ let SharedLog = (() => {
5377
5500
  removalCallCompleted = true;
5378
5501
  }
5379
5502
  finally {
5380
- releaseReceiveCleanupGate?.();
5503
+ if (releaseReceiveCleanupGate) {
5504
+ releaseReceiveCleanupGate();
5505
+ this.dispatchPersistedReceiptReadinessChange(keyHash);
5506
+ }
5381
5507
  if (replicationInfoRecoveryEpochAdvanced &&
5382
5508
  ownsReplicationOwnershipLifecycle() &&
5383
5509
  ownsReplicationLifecycle() &&
@@ -11037,6 +11163,10 @@ let SharedLog = (() => {
11037
11163
  this._peerSyncCapabilities = new Map();
11038
11164
  this._peerSyncCapabilitySessions = new Map();
11039
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();
11040
11170
  this._persistedReceiptStorage = undefined;
11041
11171
  this._persistedReceiptRequestsInFlight = new Map();
11042
11172
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -12302,6 +12432,7 @@ let SharedLog = (() => {
12302
12432
  }
12303
12433
  this.cleanupPendingIHavePeer(peerHash);
12304
12434
  this.cleanupCheckedPrunePeer(peerHash, ownershipLifecycleController, this._checkedPrune);
12435
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
12305
12436
  }
12306
12437
  cleanupPendingIHavePeer(peerHash) {
12307
12438
  for (const [hash, pending] of this._pendingIHave) {
@@ -12324,6 +12455,7 @@ let SharedLog = (() => {
12324
12455
  receiveEpoch,
12325
12456
  });
12326
12457
  }
12458
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
12327
12459
  }
12328
12460
  async resolveCandidatePeersForHash(hash, options) {
12329
12461
  if (options?.signal?.aborted)
@@ -13497,6 +13629,7 @@ let SharedLog = (() => {
13497
13629
  this._peerSyncCapabilities?.clear();
13498
13630
  this._peerSyncCapabilitySessions?.clear();
13499
13631
  this._peerSyncCapabilityTimestamps?.clear();
13632
+ this._persistedReceiptReadinessGenerations = new WeakMap();
13500
13633
  this._persistedReceiptStorage = undefined;
13501
13634
  this._persistedReceiptRequestsInFlight?.clear();
13502
13635
  this._persistedReceiptRequestsInFlightTotal = 0;
@@ -15612,10 +15745,12 @@ let SharedLog = (() => {
15612
15745
  return;
15613
15746
  }
15614
15747
  else if (msg instanceof ReplicationInfoV2AppliedMessage) {
15615
- this._v2Send.acceptApplied(msg, {
15748
+ if (this._v2Send.acceptApplied(msg, {
15616
15749
  from: context.from,
15617
15750
  receiverTransportSession: context.message.header.session,
15618
- });
15751
+ })) {
15752
+ this.dispatchPersistedReceiptReadinessChange(receiveFromHash);
15753
+ }
15619
15754
  return;
15620
15755
  }
15621
15756
  else if (isReplicationInfoV2Message(msg)) {
@@ -16239,6 +16374,7 @@ let SharedLog = (() => {
16239
16374
  // A committed V2 announcement is applied progress: the peer answers,
16240
16375
  // so recovery re-solicitation may restart from the base interval.
16241
16376
  this.resetReplicationInfoV2RecoveryEscalation(fromHash);
16377
+ this.dispatchPersistedReceiptReadinessChange(fromHash);
16242
16378
  });
16243
16379
  }
16244
16380
  finally {
@@ -16513,18 +16649,401 @@ let SharedLog = (() => {
16513
16649
  }
16514
16650
  throwIfInactive();
16515
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
+ }
16516
17026
  async waitForReplicator(key, options) {
17027
+ if (options?.signal?.aborted) {
17028
+ throw new AbortError();
17029
+ }
16517
17030
  const deferred = pDefer();
16518
17031
  const timeoutMs = options?.timeout ?? this.waitForReplicatorTimeout;
16519
17032
  const resolvedRoleAge = options?.eager
16520
17033
  ? undefined
16521
17034
  : (options?.roleAge ?? (await this.getDefaultMinRoleAge()));
17035
+ if (options?.signal?.aborted) {
17036
+ throw new AbortError();
17037
+ }
16522
17038
  let settled = false;
16523
17039
  let timer;
16524
17040
  let requestTimer;
17041
+ let checkInFlight = false;
17042
+ let checkAgain = false;
16525
17043
  const clear = () => {
16526
- this.events.removeEventListener("replicator:mature", check);
16527
- this.events.removeEventListener("replication:change", check);
17044
+ checkAgain = false;
17045
+ this.events.removeEventListener("replicator:mature", runCheck);
17046
+ this.events.removeEventListener("replication:change", runCheck);
16528
17047
  options?.signal?.removeEventListener("abort", onAbort);
16529
17048
  if (timer != null) {
16530
17049
  clearTimeout(timer);
@@ -16643,10 +17162,32 @@ let SharedLog = (() => {
16643
17162
  await iterator?.close();
16644
17163
  }
16645
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);
16646
17189
  requestReplicationInfo();
16647
- check();
16648
- this.events.addEventListener("replicator:mature", check);
16649
- this.events.addEventListener("replication:change", check);
17190
+ runCheck();
16650
17191
  return deferred.promise.finally(clear);
16651
17192
  }
16652
17193
  async waitForReplicators(options) {
@@ -18567,6 +19108,7 @@ let SharedLog = (() => {
18567
19108
  if (!ownsSubscriptionEpoch()) {
18568
19109
  return;
18569
19110
  }
19111
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
18570
19112
  // A reconnect can arrive before the previous exact-session recovery tick
18571
19113
  // observes its stale session. Retire that job synchronously so it cannot
18572
19114
  // suppress the replacement session's scheduler in the shared peer slot.
@@ -18709,6 +19251,7 @@ let SharedLog = (() => {
18709
19251
  signal: replicationLifecycleController.signal,
18710
19252
  });
18711
19253
  this.scheduleReplicationInfoV2Recovery(publicKey, replicationLifecycleController);
19254
+ this.dispatchPersistedReceiptReadinessChange(peerHash);
18712
19255
  }
18713
19256
  getClampedReplicas(customValue) {
18714
19257
  if (!customValue) {