@peerbit/shared-log 13.2.33 → 13.2.34
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.map +1 -1
- package/dist/src/index.js +61 -53
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +73 -4
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +366 -2
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +20 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +155 -54
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +82 -75
- package/src/replication-info-v2-receive.ts +538 -6
- package/src/replication-info-v2-send.ts +206 -64
|
@@ -25,6 +25,7 @@ const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
|
|
|
25
25
|
const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
|
|
26
26
|
const DEFAULT_LEGACY_FALLBACK_DELAY_MS = 5_000;
|
|
27
27
|
const MAX_U64 = (1n << 64n) - 1n;
|
|
28
|
+
const MAX_BACKOFF_EXPONENT = 20;
|
|
28
29
|
|
|
29
30
|
const bytesEqual = (left: Uint8Array, right: Uint8Array): boolean => {
|
|
30
31
|
if (left.byteLength !== right.byteLength) {
|
|
@@ -64,6 +65,7 @@ export type ReplicationInfoV2ReceivePhase =
|
|
|
64
65
|
|
|
65
66
|
type LocalCapabilityReady = {
|
|
66
67
|
peerHash: string;
|
|
68
|
+
receiveEpoch: object | null;
|
|
67
69
|
receiverTransportSession: bigint;
|
|
68
70
|
/**
|
|
69
71
|
* The local capability envelope and the later RequestV2 envelope use the
|
|
@@ -71,6 +73,45 @@ type LocalCapabilityReady = {
|
|
|
71
73
|
* be strictly newer, so never construct it in this captured millisecond.
|
|
72
74
|
*/
|
|
73
75
|
requestNotBeforeMs: number;
|
|
76
|
+
advertisement?: ReplicationInfoV2LocalCapabilityAdvertisement;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type LocalCapabilityReadyProperties = {
|
|
80
|
+
peerHash: string;
|
|
81
|
+
peerSession: object;
|
|
82
|
+
receiveEpoch: object | null;
|
|
83
|
+
receiverTransportSession: bigint;
|
|
84
|
+
requestNotBeforeMs: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ReplicationInfoV2LocalCapabilityAdvertisementHandle = {
|
|
88
|
+
firstAttempt: Promise<void>;
|
|
89
|
+
releaseLegacyBarrier(): void;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type ReplicationInfoV2LocalCapabilityContext = {
|
|
93
|
+
peerHash: string;
|
|
94
|
+
target: PublicSignKey;
|
|
95
|
+
lifecycleSignal: AbortSignal;
|
|
96
|
+
legacyBarrierReleased: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type ReplicationInfoV2LocalCapabilityAdvertisement = {
|
|
100
|
+
peerHash: string;
|
|
101
|
+
target: PublicSignKey;
|
|
102
|
+
peerSession: object;
|
|
103
|
+
receiveEpoch: object | null;
|
|
104
|
+
lifecycleSignal: AbortSignal;
|
|
105
|
+
onLifecycleAbort: () => void;
|
|
106
|
+
controller: AbortController;
|
|
107
|
+
context: ReplicationInfoV2LocalCapabilityContext;
|
|
108
|
+
attempts: number;
|
|
109
|
+
ready: boolean;
|
|
110
|
+
acknowledgedReady?: LocalCapabilityReady;
|
|
111
|
+
receiverTransportSession?: bigint;
|
|
112
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
113
|
+
inFlight?: Promise<void>;
|
|
114
|
+
firstAttempt?: Promise<void>;
|
|
74
115
|
};
|
|
75
116
|
|
|
76
117
|
export type ReplicationInfoV2LocalCapabilityRefresh = {
|
|
@@ -131,6 +172,11 @@ export type ReplicationInfoV2ReceiveDeps = {
|
|
|
131
172
|
getSelfKey: () => PublicSignKey;
|
|
132
173
|
getReceiverTransportSession: () => bigint;
|
|
133
174
|
isClosed: () => boolean;
|
|
175
|
+
isPeerSessionCurrent: (peerHash: string, peerSession: object) => boolean;
|
|
176
|
+
isReceiveEpochCurrent: (
|
|
177
|
+
peerHash: string,
|
|
178
|
+
receiveEpoch: object | null,
|
|
179
|
+
) => boolean;
|
|
134
180
|
isPeerStateCurrent: (
|
|
135
181
|
peerHash: string,
|
|
136
182
|
peerSession: object,
|
|
@@ -153,6 +199,7 @@ export type ReplicationInfoV2ReceiveDeps = {
|
|
|
153
199
|
signal: AbortSignal;
|
|
154
200
|
}) => Promise<ReplicationInfoV2LocalCapabilityRefresh | undefined>;
|
|
155
201
|
onRequestError?: (error: unknown) => void;
|
|
202
|
+
onLocalCapabilityError?: (error: unknown) => void;
|
|
156
203
|
now?: () => number;
|
|
157
204
|
requestRetryMs?: number;
|
|
158
205
|
maxRequestRetryMs?: number;
|
|
@@ -169,6 +216,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
169
216
|
_receiveStates!: Map<string, ReplicationInfoV2ReceiveState>;
|
|
170
217
|
_cutoverPeerSessions!: WeakSet<object>;
|
|
171
218
|
_localCapabilityReadyBySession!: WeakMap<object, LocalCapabilityReady>;
|
|
219
|
+
_localCapabilityContextBySession!: WeakMap<
|
|
220
|
+
object,
|
|
221
|
+
ReplicationInfoV2LocalCapabilityContext
|
|
222
|
+
>;
|
|
223
|
+
_localCapabilityAdvertisementsByPeer!: Map<
|
|
224
|
+
string,
|
|
225
|
+
ReplicationInfoV2LocalCapabilityAdvertisement
|
|
226
|
+
>;
|
|
172
227
|
_reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
|
|
173
228
|
|
|
174
229
|
private readonly now: () => number;
|
|
@@ -198,6 +253,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
198
253
|
this._receiveStates = new Map();
|
|
199
254
|
this._cutoverPeerSessions = new WeakSet();
|
|
200
255
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
256
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
257
|
+
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
201
258
|
this._reservedAdmissionsByPeer = new Map();
|
|
202
259
|
}
|
|
203
260
|
|
|
@@ -206,27 +263,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
206
263
|
this._receiveStates = new Map();
|
|
207
264
|
this._cutoverPeerSessions = new WeakSet();
|
|
208
265
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
266
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
267
|
+
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
209
268
|
this._reservedAdmissionsByPeer = new Map();
|
|
210
269
|
}
|
|
211
270
|
|
|
212
271
|
clearForClose(): void {
|
|
272
|
+
for (const advertisement of [
|
|
273
|
+
...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
|
|
274
|
+
]) {
|
|
275
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
276
|
+
}
|
|
277
|
+
this._localCapabilityAdvertisementsByPeer?.clear();
|
|
213
278
|
for (const state of this._receiveStates?.values() ?? []) {
|
|
214
279
|
this.clearState(state);
|
|
215
280
|
}
|
|
216
281
|
this._receiveStates?.clear();
|
|
217
282
|
this._cutoverPeerSessions = new WeakSet();
|
|
218
283
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
284
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
219
285
|
}
|
|
220
286
|
|
|
221
287
|
clearPeer(peerHash: string, expectedSession?: object): void {
|
|
288
|
+
const advertisement =
|
|
289
|
+
this._localCapabilityAdvertisementsByPeer.get(peerHash);
|
|
290
|
+
if (
|
|
291
|
+
advertisement &&
|
|
292
|
+
(!expectedSession || advertisement.peerSession === expectedSession)
|
|
293
|
+
) {
|
|
294
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
295
|
+
}
|
|
222
296
|
const state = this._receiveStates.get(peerHash);
|
|
223
297
|
if (state && (!expectedSession || state.peerSession === expectedSession)) {
|
|
224
298
|
this.clearState(state);
|
|
225
299
|
this._localCapabilityReadyBySession.delete(state.peerSession);
|
|
300
|
+
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
226
301
|
this._cutoverPeerSessions.delete(state.peerSession);
|
|
227
302
|
}
|
|
228
303
|
if (expectedSession) {
|
|
229
304
|
this._localCapabilityReadyBySession.delete(expectedSession);
|
|
305
|
+
this._localCapabilityContextBySession.delete(expectedSession);
|
|
230
306
|
this._cutoverPeerSessions.delete(expectedSession);
|
|
231
307
|
}
|
|
232
308
|
}
|
|
@@ -259,14 +335,462 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
259
335
|
}
|
|
260
336
|
}
|
|
261
337
|
|
|
262
|
-
|
|
263
|
-
|
|
338
|
+
private clearLocalCapabilityAdvertisement(
|
|
339
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
340
|
+
options?: { preserveContext?: boolean },
|
|
341
|
+
): void {
|
|
342
|
+
if (state.timer) {
|
|
343
|
+
clearTimeout(state.timer);
|
|
344
|
+
state.timer = undefined;
|
|
345
|
+
}
|
|
346
|
+
state.lifecycleSignal.removeEventListener("abort", state.onLifecycleAbort);
|
|
347
|
+
state.controller.abort();
|
|
348
|
+
const wasMapped =
|
|
349
|
+
this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state;
|
|
350
|
+
if (wasMapped) {
|
|
351
|
+
this._localCapabilityAdvertisementsByPeer.delete(state.peerHash);
|
|
352
|
+
}
|
|
353
|
+
const ready = this._localCapabilityReadyBySession.get(state.peerSession);
|
|
354
|
+
if (ready?.advertisement === state) {
|
|
355
|
+
this._localCapabilityReadyBySession.delete(state.peerSession);
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
wasMapped &&
|
|
359
|
+
!options?.preserveContext &&
|
|
360
|
+
this._localCapabilityContextBySession.get(state.peerSession) ===
|
|
361
|
+
state.context
|
|
362
|
+
) {
|
|
363
|
+
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private isLocalCapabilityAdvertisementOwnerCurrent(
|
|
368
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
369
|
+
): boolean {
|
|
370
|
+
return (
|
|
371
|
+
this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state &&
|
|
372
|
+
this._localCapabilityContextBySession.get(state.peerSession) ===
|
|
373
|
+
state.context &&
|
|
374
|
+
state.context.peerHash === state.peerHash &&
|
|
375
|
+
state.context.lifecycleSignal === state.lifecycleSignal &&
|
|
376
|
+
state.context.target.equals(state.target) &&
|
|
377
|
+
!state.controller.signal.aborted &&
|
|
378
|
+
!state.lifecycleSignal.aborted &&
|
|
379
|
+
!this.deps.isClosed() &&
|
|
380
|
+
this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
|
|
381
|
+
(state.receiverTransportSession === undefined ||
|
|
382
|
+
this.deps.getReceiverTransportSession() ===
|
|
383
|
+
state.receiverTransportSession)
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private isLocalCapabilityAdvertisementGenerationCurrent(
|
|
388
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
389
|
+
): boolean {
|
|
390
|
+
return (
|
|
391
|
+
this.isLocalCapabilityAdvertisementOwnerCurrent(state) &&
|
|
392
|
+
this.deps.isReceiveEpochCurrent(state.peerHash, state.receiveEpoch)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private isLocalCapabilityAdvertisementReadyOpen(
|
|
397
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
398
|
+
): boolean {
|
|
399
|
+
return (
|
|
400
|
+
this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
|
|
401
|
+
this.deps.isPeerStateCurrent(
|
|
402
|
+
state.peerHash,
|
|
403
|
+
state.peerSession,
|
|
404
|
+
state.receiveEpoch,
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private localCapabilityRetryDelay(
|
|
410
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
411
|
+
): number {
|
|
412
|
+
const exponent = Math.max(0, state.attempts - 1);
|
|
413
|
+
return Math.min(
|
|
414
|
+
this.maxRequestRetryMs,
|
|
415
|
+
this.requestRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private armLocalCapabilityAdvertisement(
|
|
420
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
421
|
+
): void {
|
|
422
|
+
if (state.timer || state.inFlight || state.ready) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (state.acknowledgedReady && !state.context.legacyBarrierReleased) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
429
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
state.timer = setTimeout(() => {
|
|
436
|
+
state.timer = undefined;
|
|
437
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
438
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
445
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
void this.runLocalCapabilityAdvertisement(state);
|
|
449
|
+
}, this.localCapabilityRetryDelay(state));
|
|
450
|
+
state.timer.unref?.();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private async runLocalCapabilityAdvertisement(
|
|
454
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
if (state.inFlight) {
|
|
457
|
+
await state.inFlight;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (state.ready) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
464
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (state.acknowledgedReady) {
|
|
471
|
+
if (state.context.legacyBarrierReleased) {
|
|
472
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
477
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
state.attempts = Math.min(state.attempts + 1, MAX_BACKOFF_EXPONENT + 1);
|
|
481
|
+
let operation: Promise<void>;
|
|
482
|
+
operation = (async () => {
|
|
483
|
+
const refreshed = await this.deps.refreshLocalCapability({
|
|
484
|
+
peerHash: state.peerHash,
|
|
485
|
+
target: state.target,
|
|
486
|
+
peerSession: state.peerSession,
|
|
487
|
+
receiveEpoch: state.receiveEpoch,
|
|
488
|
+
signal: AbortSignal.any([
|
|
489
|
+
state.controller.signal,
|
|
490
|
+
state.lifecycleSignal,
|
|
491
|
+
]),
|
|
492
|
+
});
|
|
493
|
+
if (
|
|
494
|
+
!refreshed ||
|
|
495
|
+
!this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
|
|
496
|
+
this.deps.getReceiverTransportSession() !==
|
|
497
|
+
refreshed.receiverTransportSession
|
|
498
|
+
) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
state.receiverTransportSession = refreshed.receiverTransportSession;
|
|
502
|
+
state.acknowledgedReady = {
|
|
503
|
+
peerHash: state.peerHash,
|
|
504
|
+
receiveEpoch: state.receiveEpoch,
|
|
505
|
+
receiverTransportSession: refreshed.receiverTransportSession,
|
|
506
|
+
requestNotBeforeMs: refreshed.requestNotBeforeMs,
|
|
507
|
+
advertisement: state,
|
|
508
|
+
};
|
|
509
|
+
state.attempts = 0;
|
|
510
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
511
|
+
})()
|
|
512
|
+
.catch((error) => {
|
|
513
|
+
if (
|
|
514
|
+
!state.controller.signal.aborted &&
|
|
515
|
+
!state.lifecycleSignal.aborted &&
|
|
516
|
+
!this.deps.isClosed()
|
|
517
|
+
) {
|
|
518
|
+
this.deps.onLocalCapabilityError?.(error);
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
.finally(() => {
|
|
522
|
+
if (state.inFlight === operation) {
|
|
523
|
+
state.inFlight = undefined;
|
|
524
|
+
}
|
|
525
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
526
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
527
|
+
} else if (
|
|
528
|
+
this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
|
|
529
|
+
!state.ready
|
|
530
|
+
) {
|
|
531
|
+
if (!state.acknowledgedReady || state.context.legacyBarrierReleased) {
|
|
532
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
state.inFlight = operation;
|
|
537
|
+
await operation;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Start local authenticated-apply advertisement independently of the legacy
|
|
542
|
+
* join path. ACK and legacy publication are a two-phase barrier: the first
|
|
543
|
+
* attempt always settles independently, while readiness is promoted only
|
|
544
|
+
* after the host releases the legacy barrier. Failed attempts leave one
|
|
545
|
+
* exact-session worker retrying with capped exponential backoff.
|
|
546
|
+
*/
|
|
547
|
+
advertiseLocalCapability(properties: {
|
|
548
|
+
target: PublicSignKey;
|
|
549
|
+
peerSession: object;
|
|
550
|
+
receiveEpoch: object | null;
|
|
551
|
+
signal: AbortSignal;
|
|
552
|
+
}): ReplicationInfoV2LocalCapabilityAdvertisementHandle {
|
|
553
|
+
const peerHash = properties.target.hashcode();
|
|
554
|
+
if (
|
|
555
|
+
properties.signal.aborted ||
|
|
556
|
+
this.deps.isClosed() ||
|
|
557
|
+
!this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
|
|
558
|
+
!this.deps.isReceiveEpochCurrent(peerHash, properties.receiveEpoch)
|
|
559
|
+
) {
|
|
560
|
+
return {
|
|
561
|
+
firstAttempt: Promise.resolve(),
|
|
562
|
+
releaseLegacyBarrier: () => {},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
let context = this._localCapabilityContextBySession.get(
|
|
566
|
+
properties.peerSession,
|
|
567
|
+
);
|
|
568
|
+
if (
|
|
569
|
+
context &&
|
|
570
|
+
(context.peerHash !== peerHash ||
|
|
571
|
+
!context.target.equals(properties.target) ||
|
|
572
|
+
context.lifecycleSignal !== properties.signal)
|
|
573
|
+
) {
|
|
574
|
+
return {
|
|
575
|
+
firstAttempt: Promise.resolve(),
|
|
576
|
+
releaseLegacyBarrier: () => {},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (!context) {
|
|
580
|
+
context = {
|
|
581
|
+
peerHash,
|
|
582
|
+
target: properties.target,
|
|
583
|
+
lifecycleSignal: properties.signal,
|
|
584
|
+
legacyBarrierReleased: false,
|
|
585
|
+
};
|
|
586
|
+
this._localCapabilityContextBySession.set(
|
|
587
|
+
properties.peerSession,
|
|
588
|
+
context,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
let state = this._localCapabilityAdvertisementsByPeer.get(peerHash);
|
|
592
|
+
if (
|
|
593
|
+
state &&
|
|
594
|
+
(state.peerSession !== properties.peerSession ||
|
|
595
|
+
state.context !== context ||
|
|
596
|
+
state.lifecycleSignal !== properties.signal ||
|
|
597
|
+
!state.target.equals(properties.target))
|
|
598
|
+
) {
|
|
599
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
600
|
+
state = undefined;
|
|
601
|
+
}
|
|
602
|
+
if (state && state.receiveEpoch !== properties.receiveEpoch) {
|
|
603
|
+
this.clearLocalCapabilityAdvertisement(state, { preserveContext: true });
|
|
604
|
+
state = undefined;
|
|
605
|
+
}
|
|
606
|
+
if (state && !this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
607
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
608
|
+
state = undefined;
|
|
609
|
+
}
|
|
610
|
+
if (
|
|
611
|
+
this._localCapabilityContextBySession.get(properties.peerSession) !==
|
|
612
|
+
context
|
|
613
|
+
) {
|
|
614
|
+
return {
|
|
615
|
+
firstAttempt: Promise.resolve(),
|
|
616
|
+
releaseLegacyBarrier: () => {},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (!state) {
|
|
620
|
+
const controller = new AbortController();
|
|
621
|
+
const advertisement: ReplicationInfoV2LocalCapabilityAdvertisement = {
|
|
622
|
+
peerHash,
|
|
623
|
+
target: properties.target,
|
|
624
|
+
peerSession: properties.peerSession,
|
|
625
|
+
receiveEpoch: properties.receiveEpoch,
|
|
626
|
+
lifecycleSignal: properties.signal,
|
|
627
|
+
onLifecycleAbort: () => {},
|
|
628
|
+
controller,
|
|
629
|
+
context,
|
|
630
|
+
attempts: 0,
|
|
631
|
+
ready: false,
|
|
632
|
+
receiverTransportSession: this.deps.getReceiverTransportSession(),
|
|
633
|
+
};
|
|
634
|
+
advertisement.onLifecycleAbort = () =>
|
|
635
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
636
|
+
properties.signal.addEventListener(
|
|
637
|
+
"abort",
|
|
638
|
+
advertisement.onLifecycleAbort,
|
|
639
|
+
{
|
|
640
|
+
once: true,
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
this._localCapabilityAdvertisementsByPeer.set(peerHash, advertisement);
|
|
644
|
+
state = advertisement;
|
|
645
|
+
}
|
|
646
|
+
const firstAttempt =
|
|
647
|
+
state.firstAttempt ??
|
|
648
|
+
(state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
|
|
649
|
+
const advertisement = state;
|
|
650
|
+
return {
|
|
651
|
+
firstAttempt,
|
|
652
|
+
releaseLegacyBarrier: () =>
|
|
653
|
+
this.releaseLocalCapabilityLegacyBarrier(
|
|
654
|
+
advertisement.peerSession,
|
|
655
|
+
context,
|
|
656
|
+
),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private releaseLocalCapabilityLegacyBarrier(
|
|
661
|
+
peerSession: object,
|
|
662
|
+
context: ReplicationInfoV2LocalCapabilityContext,
|
|
663
|
+
): void {
|
|
664
|
+
if (context.legacyBarrierReleased) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (
|
|
668
|
+
this._localCapabilityContextBySession.get(peerSession) !== context ||
|
|
669
|
+
context.lifecycleSignal.aborted ||
|
|
670
|
+
this.deps.isClosed() ||
|
|
671
|
+
!this.deps.isPeerSessionCurrent(context.peerHash, peerSession)
|
|
672
|
+
) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
context.legacyBarrierReleased = true;
|
|
676
|
+
const state = this._localCapabilityAdvertisementsByPeer.get(
|
|
677
|
+
context.peerHash,
|
|
678
|
+
);
|
|
679
|
+
if (state?.peerSession === peerSession && state.context === context) {
|
|
680
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
681
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private promoteLocalCapabilityAdvertisement(
|
|
689
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
690
|
+
): boolean {
|
|
691
|
+
const ready = state.acknowledgedReady;
|
|
692
|
+
if (
|
|
693
|
+
state.ready ||
|
|
694
|
+
!state.context.legacyBarrierReleased ||
|
|
695
|
+
!ready ||
|
|
696
|
+
ready.receiveEpoch !== state.receiveEpoch ||
|
|
697
|
+
ready.advertisement !== state ||
|
|
698
|
+
!this.isLocalCapabilityAdvertisementOwnerCurrent(state) ||
|
|
699
|
+
!this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
|
|
700
|
+
this.deps.getReceiverTransportSession() !== ready.receiverTransportSession
|
|
701
|
+
) {
|
|
702
|
+
return state.ready;
|
|
703
|
+
}
|
|
704
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
705
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
if (
|
|
709
|
+
!this.recordLocalCapabilityReady(
|
|
710
|
+
{
|
|
711
|
+
peerHash: state.peerHash,
|
|
712
|
+
peerSession: state.peerSession,
|
|
713
|
+
receiveEpoch: state.receiveEpoch,
|
|
714
|
+
receiverTransportSession: ready.receiverTransportSession,
|
|
715
|
+
requestNotBeforeMs: ready.requestNotBeforeMs,
|
|
716
|
+
},
|
|
717
|
+
state,
|
|
718
|
+
)
|
|
719
|
+
) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
state.ready = true;
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Re-advertise one exact current recovery epoch from the stable membership
|
|
728
|
+
* context captured during opening. The opening handle owns barrier release;
|
|
729
|
+
* recovery never bypasses a legacy snapshot or role publication still in
|
|
730
|
+
* progress.
|
|
731
|
+
*/
|
|
732
|
+
reAdvertiseLocalCapabilityForRecovery(properties: {
|
|
264
733
|
peerHash: string;
|
|
265
734
|
peerSession: object;
|
|
266
735
|
receiveEpoch: object | null;
|
|
267
|
-
receiverTransportSession: bigint;
|
|
268
|
-
requestNotBeforeMs: number;
|
|
269
736
|
}): boolean {
|
|
737
|
+
const context = this._localCapabilityContextBySession.get(
|
|
738
|
+
properties.peerSession,
|
|
739
|
+
);
|
|
740
|
+
if (
|
|
741
|
+
!context ||
|
|
742
|
+
context.peerHash !== properties.peerHash ||
|
|
743
|
+
context.lifecycleSignal.aborted ||
|
|
744
|
+
this.deps.isClosed() ||
|
|
745
|
+
!this.deps.isPeerSessionCurrent(
|
|
746
|
+
properties.peerHash,
|
|
747
|
+
properties.peerSession,
|
|
748
|
+
) ||
|
|
749
|
+
!this.deps.isReceiveEpochCurrent(
|
|
750
|
+
properties.peerHash,
|
|
751
|
+
properties.receiveEpoch,
|
|
752
|
+
)
|
|
753
|
+
) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
757
|
+
if (
|
|
758
|
+
state?.peerSession === properties.peerSession &&
|
|
759
|
+
state.receiveEpoch === properties.receiveEpoch &&
|
|
760
|
+
state.receiverBinding !== undefined
|
|
761
|
+
) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
const ready = this._localCapabilityReadyBySession.get(
|
|
765
|
+
properties.peerSession,
|
|
766
|
+
);
|
|
767
|
+
if (
|
|
768
|
+
ready?.peerHash === properties.peerHash &&
|
|
769
|
+
ready.receiveEpoch === properties.receiveEpoch &&
|
|
770
|
+
ready.receiverTransportSession === this.deps.getReceiverTransportSession()
|
|
771
|
+
) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
this.advertiseLocalCapability({
|
|
775
|
+
target: context.target,
|
|
776
|
+
peerSession: properties.peerSession,
|
|
777
|
+
receiveEpoch: properties.receiveEpoch,
|
|
778
|
+
signal: context.lifecycleSignal,
|
|
779
|
+
});
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Record success of this session's ACKed local APPLY advertisement. */
|
|
784
|
+
markLocalCapabilityReady(
|
|
785
|
+
properties: LocalCapabilityReadyProperties,
|
|
786
|
+
): boolean {
|
|
787
|
+
return this.recordLocalCapabilityReady(properties);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private recordLocalCapabilityReady(
|
|
791
|
+
properties: LocalCapabilityReadyProperties,
|
|
792
|
+
advertisement?: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
793
|
+
): boolean {
|
|
270
794
|
const { peerHash, peerSession, receiverTransportSession } = properties;
|
|
271
795
|
const state = this._receiveStates.get(peerHash);
|
|
272
796
|
if (
|
|
@@ -283,8 +807,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
283
807
|
|
|
284
808
|
const ready: LocalCapabilityReady = {
|
|
285
809
|
peerHash,
|
|
810
|
+
receiveEpoch: properties.receiveEpoch,
|
|
286
811
|
receiverTransportSession,
|
|
287
812
|
requestNotBeforeMs: properties.requestNotBeforeMs,
|
|
813
|
+
advertisement,
|
|
288
814
|
};
|
|
289
815
|
this._localCapabilityReadyBySession.set(peerSession, ready);
|
|
290
816
|
if (
|
|
@@ -388,7 +914,11 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
388
914
|
});
|
|
389
915
|
}
|
|
390
916
|
const ready = this._localCapabilityReadyBySession.get(peerSession);
|
|
391
|
-
if (
|
|
917
|
+
if (
|
|
918
|
+
ready?.peerHash === peerHash &&
|
|
919
|
+
ready.receiveEpoch === receiveEpoch &&
|
|
920
|
+
state.receiverBinding === undefined
|
|
921
|
+
) {
|
|
392
922
|
this.bindLocalCapability(state, ready);
|
|
393
923
|
}
|
|
394
924
|
if (state.phase !== "active") {
|
|
@@ -422,7 +952,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
422
952
|
};
|
|
423
953
|
this._receiveStates.set(peerHash, state);
|
|
424
954
|
const ready = this._localCapabilityReadyBySession.get(peerSession);
|
|
425
|
-
if (ready?.peerHash === peerHash) {
|
|
955
|
+
if (ready?.peerHash === peerHash && ready.receiveEpoch === receiveEpoch) {
|
|
426
956
|
this.bindLocalCapability(state, ready);
|
|
427
957
|
this.armRequest(state, 0);
|
|
428
958
|
}
|
|
@@ -937,6 +1467,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
937
1467
|
|
|
938
1468
|
const ready: LocalCapabilityReady = {
|
|
939
1469
|
peerHash: state.peerHash,
|
|
1470
|
+
receiveEpoch: state.receiveEpoch,
|
|
940
1471
|
receiverTransportSession: refreshed.receiverTransportSession,
|
|
941
1472
|
requestNotBeforeMs: refreshed.requestNotBeforeMs,
|
|
942
1473
|
};
|
|
@@ -988,6 +1519,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
988
1519
|
if (
|
|
989
1520
|
!ready ||
|
|
990
1521
|
ready.peerHash !== state.peerHash ||
|
|
1522
|
+
ready.receiveEpoch !== state.receiveEpoch ||
|
|
991
1523
|
ready.receiverTransportSession !== state.receiverTransportSession
|
|
992
1524
|
) {
|
|
993
1525
|
return;
|