@peerbit/shared-log 13.2.33 → 13.2.35
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 +7 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +284 -91
- package/dist/src/index.js.map +1 -1
- package/dist/src/peer-session.d.ts +2 -1
- package/dist/src/peer-session.d.ts.map +1 -1
- package/dist/src/peer-session.js +8 -2
- package/dist/src/peer-session.js.map +1 -1
- package/dist/src/replication-announcement.d.ts +1 -0
- package/dist/src/replication-announcement.d.ts.map +1 -1
- package/dist/src/replication-announcement.js +16 -2
- package/dist/src/replication-announcement.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +84 -4
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +441 -7
- 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 +177 -55
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/dist/src/sync/factory.js +1 -1
- package/dist/src/sync/factory.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +350 -123
- package/src/peer-session.ts +8 -0
- package/src/replication-announcement.ts +17 -2
- package/src/replication-info-v2-receive.ts +634 -11
- package/src/replication-info-v2-send.ts +227 -65
- package/src/sync/factory.ts +1 -1
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
const logger = loggerFn("peerbit:shared-log:replication-info-v2-send");
|
|
22
22
|
|
|
23
23
|
const MAX_U64 = (1n << 64n) - 1n;
|
|
24
|
+
const DEFAULT_SEND_RETRY_MS = 1_000;
|
|
25
|
+
const DEFAULT_MAX_SEND_RETRY_MS = 30_000;
|
|
26
|
+
const MAX_BACKOFF_EXPONENT = 20;
|
|
24
27
|
|
|
25
28
|
export { deriveReplicationInfoV2ReceiverBinding } from "./replication-info-v2-binding.js";
|
|
26
29
|
|
|
@@ -45,10 +48,13 @@ export type ReplicationInfoV2SendState = {
|
|
|
45
48
|
receiverChallenge: Uint8Array;
|
|
46
49
|
senderEpoch: Uint8Array;
|
|
47
50
|
ownershipLifecycleController: AbortController;
|
|
51
|
+
ownershipAbortListener: () => void;
|
|
48
52
|
nextSequence: bigint;
|
|
49
53
|
established: boolean;
|
|
50
54
|
suspended: boolean;
|
|
51
55
|
inFlightSequence?: bigint;
|
|
56
|
+
retryTimer?: ReturnType<typeof setTimeout>;
|
|
57
|
+
retryAttempts: number;
|
|
52
58
|
controller: AbortController;
|
|
53
59
|
pending?: SendRequest;
|
|
54
60
|
worker?: Promise<void>;
|
|
@@ -63,11 +69,14 @@ export type ReplicationInfoV2SendDeps<R extends "u32" | "u64"> = {
|
|
|
63
69
|
ranges: readonly { mode: unknown }[],
|
|
64
70
|
) => void;
|
|
65
71
|
isClosed: () => boolean;
|
|
72
|
+
isPeerSessionCurrent: (peerHash: string, peerSession: object) => boolean;
|
|
66
73
|
isPeerSessionOpen: (peerHash: string, peerSession: object) => boolean;
|
|
67
74
|
captureReplicationOwnershipLifecycle: () => AbortController;
|
|
68
75
|
isReplicationOwnershipLifecycleActive: (
|
|
69
76
|
controller: AbortController,
|
|
70
77
|
) => boolean;
|
|
78
|
+
sendRetryMs?: number;
|
|
79
|
+
maxSendRetryMs?: number;
|
|
71
80
|
};
|
|
72
81
|
|
|
73
82
|
const bytesEqual = (left: Uint8Array, right: Uint8Array): boolean => {
|
|
@@ -93,7 +102,15 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
93
102
|
_spentPeerSessions!: WeakSet<object>;
|
|
94
103
|
_retiringWorkersByPeer!: Map<string, Promise<void>>;
|
|
95
104
|
|
|
105
|
+
private readonly sendRetryMs: number;
|
|
106
|
+
private readonly maxSendRetryMs: number;
|
|
107
|
+
|
|
96
108
|
constructor(private readonly deps: ReplicationInfoV2SendDeps<R>) {
|
|
109
|
+
this.sendRetryMs = Math.max(1, deps.sendRetryMs ?? DEFAULT_SEND_RETRY_MS);
|
|
110
|
+
this.maxSendRetryMs = Math.max(
|
|
111
|
+
this.sendRetryMs,
|
|
112
|
+
deps.maxSendRetryMs ?? DEFAULT_MAX_SEND_RETRY_MS,
|
|
113
|
+
);
|
|
97
114
|
this._senderEpoch = randomBytes(32);
|
|
98
115
|
this._sendStates = new Map();
|
|
99
116
|
this._spentPeerSessions = new WeakSet();
|
|
@@ -131,6 +148,14 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
131
148
|
|
|
132
149
|
private clearState(state: ReplicationInfoV2SendState): void {
|
|
133
150
|
this.trackRetiringWorker(state);
|
|
151
|
+
if (state.retryTimer) {
|
|
152
|
+
clearTimeout(state.retryTimer);
|
|
153
|
+
state.retryTimer = undefined;
|
|
154
|
+
}
|
|
155
|
+
state.ownershipLifecycleController.signal.removeEventListener(
|
|
156
|
+
"abort",
|
|
157
|
+
state.ownershipAbortListener,
|
|
158
|
+
);
|
|
134
159
|
state.controller.abort();
|
|
135
160
|
if (this._sendStates.get(state.peerHash) === state) {
|
|
136
161
|
this._sendStates.delete(state.peerHash);
|
|
@@ -163,20 +188,67 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
163
188
|
!this.deps.isClosed() &&
|
|
164
189
|
!state.controller.signal.aborted &&
|
|
165
190
|
this._sendStates.get(state.peerHash) === state &&
|
|
166
|
-
this.deps.
|
|
191
|
+
this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
|
|
167
192
|
this.deps.getSenderTransportSession() === state.senderTransportSession
|
|
168
193
|
);
|
|
169
194
|
}
|
|
170
195
|
|
|
171
|
-
private
|
|
196
|
+
private isDestinationReady(state: ReplicationInfoV2SendState): boolean {
|
|
172
197
|
return (
|
|
173
198
|
this.isDestinationCurrent(state) &&
|
|
199
|
+
this.deps.isPeerSessionOpen(state.peerHash, state.peerSession)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private isCurrent(state: ReplicationInfoV2SendState): boolean {
|
|
204
|
+
return (
|
|
205
|
+
this.isDestinationReady(state) &&
|
|
174
206
|
this.deps.isReplicationOwnershipLifecycleActive(
|
|
175
207
|
state.ownershipLifecycleController,
|
|
176
208
|
)
|
|
177
209
|
);
|
|
178
210
|
}
|
|
179
211
|
|
|
212
|
+
private retireSpentState(state: ReplicationInfoV2SendState): void {
|
|
213
|
+
this._spentPeerSessions.add(state.peerSession);
|
|
214
|
+
this.clearState(state);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Collapse every interrupted normal-send path to one authoritative snapshot.
|
|
219
|
+
* A closed readiness gate is temporary while the exact PeerSession remains
|
|
220
|
+
* current, so it parks instead of destroying the receiver binding. Sequence
|
|
221
|
+
* exhaustion is terminal even when readiness or ownership changed at the same
|
|
222
|
+
* time as an in-flight transport attempt.
|
|
223
|
+
*/
|
|
224
|
+
private parkSnapshotForRetry(state: ReplicationInfoV2SendState): void {
|
|
225
|
+
state.inFlightSequence = undefined;
|
|
226
|
+
if (state.nextSequence > MAX_U64) {
|
|
227
|
+
this.retireSpentState(state);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!this.isDestinationCurrent(state)) {
|
|
231
|
+
this.clearState(state);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
236
|
+
state.ownershipLifecycleController,
|
|
237
|
+
)
|
|
238
|
+
) {
|
|
239
|
+
if (state.retryTimer) {
|
|
240
|
+
clearTimeout(state.retryTimer);
|
|
241
|
+
state.retryTimer = undefined;
|
|
242
|
+
}
|
|
243
|
+
state.pending = undefined;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
state.suspended = true;
|
|
248
|
+
state.pending = { kind: "snapshot" };
|
|
249
|
+
this.scheduleRetry(state);
|
|
250
|
+
}
|
|
251
|
+
|
|
180
252
|
/**
|
|
181
253
|
* Accept a signed receiver request. An exact newer retry asks for another
|
|
182
254
|
* full snapshot without resetting the epoch/sequence. A different challenge
|
|
@@ -207,6 +279,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
207
279
|
if (
|
|
208
280
|
this._retiringWorkersByPeer.has(peerHash) ||
|
|
209
281
|
this._spentPeerSessions.has(properties.peerSession) ||
|
|
282
|
+
!this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
|
|
210
283
|
!this.deps.isPeerSessionOpen(peerHash, properties.peerSession)
|
|
211
284
|
) {
|
|
212
285
|
return false;
|
|
@@ -224,6 +297,16 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
224
297
|
return false;
|
|
225
298
|
}
|
|
226
299
|
}
|
|
300
|
+
if (
|
|
301
|
+
previous &&
|
|
302
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
303
|
+
previous.ownershipLifecycleController,
|
|
304
|
+
)
|
|
305
|
+
) {
|
|
306
|
+
// Ownership teardown retains the binding only so close/drop can send its
|
|
307
|
+
// terminal empty Full. A later request must not revive normal delivery.
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
227
310
|
if (previous) {
|
|
228
311
|
const sameBinding =
|
|
229
312
|
previous.peerSession === properties.peerSession &&
|
|
@@ -239,7 +322,12 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
239
322
|
}
|
|
240
323
|
previous.lastRequestTimestamp = properties.requestTimestamp;
|
|
241
324
|
previous.capabilityTimestamp = properties.capabilityTimestamp;
|
|
325
|
+
if (previous.retryTimer) {
|
|
326
|
+
clearTimeout(previous.retryTimer);
|
|
327
|
+
previous.retryTimer = undefined;
|
|
328
|
+
}
|
|
242
329
|
previous.suspended = false;
|
|
330
|
+
previous.pending = { kind: "snapshot" };
|
|
243
331
|
this.enqueueState(previous, { kind: "snapshot" });
|
|
244
332
|
return true;
|
|
245
333
|
}
|
|
@@ -256,6 +344,13 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
256
344
|
|
|
257
345
|
const ownershipLifecycleController =
|
|
258
346
|
this.deps.captureReplicationOwnershipLifecycle();
|
|
347
|
+
if (
|
|
348
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
349
|
+
ownershipLifecycleController,
|
|
350
|
+
)
|
|
351
|
+
) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
259
354
|
const state: ReplicationInfoV2SendState = {
|
|
260
355
|
peerHash,
|
|
261
356
|
target: properties.from,
|
|
@@ -273,12 +368,26 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
273
368
|
senderTransportSession,
|
|
274
369
|
}),
|
|
275
370
|
senderEpoch: this._senderEpoch.slice(),
|
|
371
|
+
ownershipAbortListener: () => {},
|
|
276
372
|
nextSequence: 1n,
|
|
277
373
|
established: false,
|
|
278
374
|
suspended: false,
|
|
375
|
+
retryAttempts: 0,
|
|
279
376
|
controller: new AbortController(),
|
|
280
377
|
ownershipLifecycleController,
|
|
281
378
|
};
|
|
379
|
+
state.ownershipAbortListener = () => {
|
|
380
|
+
if (state.retryTimer) {
|
|
381
|
+
clearTimeout(state.retryTimer);
|
|
382
|
+
state.retryTimer = undefined;
|
|
383
|
+
}
|
|
384
|
+
state.pending = undefined;
|
|
385
|
+
};
|
|
386
|
+
ownershipLifecycleController.signal.addEventListener(
|
|
387
|
+
"abort",
|
|
388
|
+
state.ownershipAbortListener,
|
|
389
|
+
{ once: true },
|
|
390
|
+
);
|
|
282
391
|
this._sendStates.set(peerHash, state);
|
|
283
392
|
this.enqueueState(state, { kind: "snapshot" });
|
|
284
393
|
return true;
|
|
@@ -301,20 +410,15 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
301
410
|
state: ReplicationInfoV2SendState,
|
|
302
411
|
request: SendRequest,
|
|
303
412
|
): void {
|
|
304
|
-
if (!this.isCurrent(state)) {
|
|
305
|
-
state
|
|
306
|
-
if (
|
|
307
|
-
!this.isDestinationCurrent(state) ||
|
|
308
|
-
this.deps.isReplicationOwnershipLifecycleActive(
|
|
309
|
-
state.ownershipLifecycleController,
|
|
310
|
-
)
|
|
311
|
-
) {
|
|
312
|
-
this.clearState(state);
|
|
313
|
-
}
|
|
413
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
414
|
+
this.parkSnapshotForRetry(state);
|
|
314
415
|
return;
|
|
315
416
|
}
|
|
316
417
|
if (state.suspended) {
|
|
317
|
-
|
|
418
|
+
// Delivery is ambiguous while the backoff is armed. Never retain a
|
|
419
|
+
// potentially stale delta: one authoritative Full represents every
|
|
420
|
+
// mutation that arrives before the retry fires.
|
|
421
|
+
this.parkSnapshotForRetry(state);
|
|
318
422
|
return;
|
|
319
423
|
}
|
|
320
424
|
|
|
@@ -324,12 +428,14 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
324
428
|
worker = Promise.resolve()
|
|
325
429
|
.then(() => this.runWorker(state))
|
|
326
430
|
.catch((error) => {
|
|
327
|
-
|
|
431
|
+
// Sequence cleanup and exhaustion fencing must happen before any
|
|
432
|
+
// readiness/ownership classification in the recovery path.
|
|
433
|
+
this.parkSnapshotForRetry(state);
|
|
434
|
+
if (
|
|
435
|
+
this._sendStates.get(state.peerHash) === state &&
|
|
328
436
|
this.deps.isReplicationOwnershipLifecycleActive(
|
|
329
437
|
state.ownershipLifecycleController,
|
|
330
|
-
)
|
|
331
|
-
if (
|
|
332
|
-
ownershipActive &&
|
|
438
|
+
) &&
|
|
333
439
|
!state.controller.signal.aborted &&
|
|
334
440
|
!this.deps.isClosed()
|
|
335
441
|
) {
|
|
@@ -339,28 +445,6 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
339
445
|
(error as Error)?.message ?? String(error),
|
|
340
446
|
);
|
|
341
447
|
}
|
|
342
|
-
state.pending = undefined;
|
|
343
|
-
const ordinaryFailure =
|
|
344
|
-
ownershipActive && this.isDestinationCurrent(state);
|
|
345
|
-
if (ordinaryFailure) {
|
|
346
|
-
if (state.inFlightSequence !== undefined) {
|
|
347
|
-
state.inFlightSequence = undefined;
|
|
348
|
-
if (state.nextSequence > MAX_U64) {
|
|
349
|
-
this._spentPeerSessions.add(state.peerSession);
|
|
350
|
-
this.clearState(state);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// A delivery error is ambiguous: the receiver may already have
|
|
355
|
-
// applied this sequence. Retain the exact grant, stop ordinary
|
|
356
|
-
// deltas, and require a newer same-challenge request to resume
|
|
357
|
-
// with an authoritative Full at the next safe sequence.
|
|
358
|
-
state.suspended = true;
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (!this.isDestinationCurrent(state)) {
|
|
362
|
-
this.clearState(state);
|
|
363
|
-
}
|
|
364
448
|
})
|
|
365
449
|
.finally(() => {
|
|
366
450
|
if (state.worker === worker) {
|
|
@@ -369,7 +453,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
369
453
|
// before this promise reaction clears `worker`. Re-arm that item
|
|
370
454
|
// here so the one-slot bound cannot become a stranded queue.
|
|
371
455
|
const pending = state.pending;
|
|
372
|
-
if (pending
|
|
456
|
+
if (pending) {
|
|
373
457
|
state.pending = undefined;
|
|
374
458
|
this.enqueueState(state, pending);
|
|
375
459
|
}
|
|
@@ -389,6 +473,51 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
389
473
|
state.pending = { kind: "snapshot" };
|
|
390
474
|
}
|
|
391
475
|
|
|
476
|
+
private retryDelay(state: ReplicationInfoV2SendState): number {
|
|
477
|
+
const exponent = Math.max(0, state.retryAttempts - 1);
|
|
478
|
+
return Math.min(
|
|
479
|
+
this.maxSendRetryMs,
|
|
480
|
+
this.sendRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private scheduleRetry(state: ReplicationInfoV2SendState): void {
|
|
485
|
+
if (state.retryTimer) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (state.nextSequence > MAX_U64) {
|
|
489
|
+
this.retireSpentState(state);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!this.isDestinationCurrent(state)) {
|
|
493
|
+
this.clearState(state);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (
|
|
497
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
498
|
+
state.ownershipLifecycleController,
|
|
499
|
+
)
|
|
500
|
+
) {
|
|
501
|
+
state.pending = undefined;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
state.retryAttempts = Math.min(
|
|
505
|
+
state.retryAttempts + 1,
|
|
506
|
+
MAX_BACKOFF_EXPONENT + 1,
|
|
507
|
+
);
|
|
508
|
+
state.retryTimer = setTimeout(() => {
|
|
509
|
+
state.retryTimer = undefined;
|
|
510
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
511
|
+
this.parkSnapshotForRetry(state);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
state.suspended = false;
|
|
515
|
+
state.pending = { kind: "snapshot" };
|
|
516
|
+
this.enqueueState(state, { kind: "snapshot" });
|
|
517
|
+
}, this.retryDelay(state));
|
|
518
|
+
state.retryTimer.unref?.();
|
|
519
|
+
}
|
|
520
|
+
|
|
392
521
|
private async createMessage(
|
|
393
522
|
state: ReplicationInfoV2SendState,
|
|
394
523
|
request: SendRequest,
|
|
@@ -427,19 +556,19 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
427
556
|
}
|
|
428
557
|
|
|
429
558
|
private async runWorker(state: ReplicationInfoV2SendState): Promise<void> {
|
|
430
|
-
while (
|
|
559
|
+
while (true) {
|
|
560
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
561
|
+
this.parkSnapshotForRetry(state);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
431
564
|
const request = state.pending;
|
|
432
565
|
if (!request) {
|
|
433
566
|
return;
|
|
434
567
|
}
|
|
435
568
|
state.pending = undefined;
|
|
436
|
-
if (state.nextSequence > MAX_U64) {
|
|
437
|
-
this.clearState(state);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
569
|
const message = await this.createMessage(state, request);
|
|
442
|
-
if (!this.isCurrent(state)) {
|
|
570
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
571
|
+
this.parkSnapshotForRetry(state);
|
|
443
572
|
return;
|
|
444
573
|
}
|
|
445
574
|
// Consume the sequence before the transport attempt. From this point on
|
|
@@ -460,15 +589,12 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
460
589
|
]),
|
|
461
590
|
});
|
|
462
591
|
state.inFlightSequence = undefined;
|
|
463
|
-
if (!this.isCurrent(state)) {
|
|
592
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
593
|
+
this.parkSnapshotForRetry(state);
|
|
464
594
|
return;
|
|
465
595
|
}
|
|
596
|
+
state.retryAttempts = 0;
|
|
466
597
|
state.established = true;
|
|
467
|
-
if (state.nextSequence > MAX_U64) {
|
|
468
|
-
this._spentPeerSessions.add(state.peerSession);
|
|
469
|
-
this.clearState(state);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
598
|
}
|
|
473
599
|
}
|
|
474
600
|
|
|
@@ -490,24 +616,60 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
490
616
|
continue;
|
|
491
617
|
}
|
|
492
618
|
state.pending = undefined;
|
|
619
|
+
const sequence = state.nextSequence;
|
|
620
|
+
state.nextSequence += 1n;
|
|
493
621
|
const message = new FullReplicationInfoV2Message({
|
|
494
622
|
receiverChallenge: state.receiverChallenge.slice(),
|
|
495
623
|
senderEpoch: state.senderEpoch.slice(),
|
|
496
|
-
sequence
|
|
624
|
+
sequence,
|
|
497
625
|
segments: [],
|
|
498
626
|
});
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
627
|
+
if (sequence === MAX_U64) {
|
|
628
|
+
// Retire the exhausted normal stream before transport invocation. The
|
|
629
|
+
// terminal attempt remains valid because it carries its caller-owned
|
|
630
|
+
// signal rather than the state controller that clearState aborts.
|
|
631
|
+
this.retireSpentState(state);
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
sends.push(
|
|
635
|
+
Promise.resolve(
|
|
636
|
+
this.deps.getRpc().send(message, {
|
|
637
|
+
mode: new AcknowledgeDelivery({
|
|
638
|
+
to: [state.target],
|
|
639
|
+
redundancy: 1,
|
|
640
|
+
}),
|
|
641
|
+
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
642
|
+
signal,
|
|
643
|
+
}),
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
// Keep one synchronous transport failure isolated to its destination;
|
|
648
|
+
// the sequence was already consumed and later peers must still reset.
|
|
649
|
+
sends.push(Promise.reject(error));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (sends.length === 0 || signal.aborted) {
|
|
653
|
+
return;
|
|
509
654
|
}
|
|
510
|
-
|
|
655
|
+
|
|
656
|
+
// Terminal reset is best-effort and the caller owns its close/drop bound.
|
|
657
|
+
// A transport is expected to observe the signal, but a disconnected
|
|
658
|
+
// acknowledgement path can leave its promise pending indefinitely. Do not
|
|
659
|
+
// let such a transport retain the whole terminal operation past the bound.
|
|
660
|
+
await new Promise<void>((resolve) => {
|
|
661
|
+
let finished = false;
|
|
662
|
+
const finish = () => {
|
|
663
|
+
if (finished) return;
|
|
664
|
+
finished = true;
|
|
665
|
+
signal.removeEventListener("abort", finish);
|
|
666
|
+
resolve();
|
|
667
|
+
};
|
|
668
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
669
|
+
void Promise.allSettled(sends).then(finish);
|
|
670
|
+
// Close the check/listener race if the caller aborted synchronously.
|
|
671
|
+
if (signal.aborted) finish();
|
|
672
|
+
});
|
|
511
673
|
}
|
|
512
674
|
|
|
513
675
|
async drain(signal?: AbortSignal): Promise<void> {
|
package/src/sync/factory.ts
CHANGED
|
@@ -114,7 +114,7 @@ export function createSyncronizer<R extends "u32" | "u64">(
|
|
|
114
114
|
sendRawExchangeHeads: props.sendRawExchangeHeads,
|
|
115
115
|
});
|
|
116
116
|
} else {
|
|
117
|
-
if (props.compatibility && props.compatibility < 10) {
|
|
117
|
+
if (props.compatibility !== undefined && props.compatibility < 10) {
|
|
118
118
|
return new SimpleSyncronizer({
|
|
119
119
|
log: props.log,
|
|
120
120
|
rpc: props.rpc,
|