@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.d.ts +80 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +768 -34
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +11 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +24 -5
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +21 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +104 -30
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +1101 -35
- package/src/replication-info-v2-receive.ts +32 -5
- package/src/replication-info-v2-send.ts +168 -38
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,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
|
-
|
|
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
|
-
!
|
|
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
|
-
|
|
3240
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
16336
|
-
this.events.removeEventListener("
|
|
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
|
-
|
|
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) {
|