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