@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.d.ts +78 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +559 -16
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +6 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +15 -0
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +796 -18
- package/src/replication-info-v2-send.ts +18 -0
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:
|
|
2922
|
+
capabilities: nextCapabilities,
|
|
2904
2923
|
transportSession,
|
|
2905
|
-
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 (
|
|
3352
|
+
if (this.closed ||
|
|
3353
|
+
capabilitySession == null ||
|
|
3237
3354
|
!peerSession ||
|
|
3238
3355
|
peerSession.phase !== "open" ||
|
|
3239
|
-
!
|
|
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
|
-
|
|
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
|
-
|
|
16527
|
-
this.events.removeEventListener("
|
|
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
|
-
|
|
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) {
|