@peerbit/shared-log 16.0.21 → 16.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +80 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +768 -34
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +11 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +24 -5
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +21 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +104 -30
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +1101 -35
- package/src/replication-info-v2-receive.ts +32 -5
- package/src/replication-info-v2-send.ts +168 -38
|
@@ -1235,6 +1235,31 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
1235
1235
|
);
|
|
1236
1236
|
}
|
|
1237
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* Exact active receive generation used by durability-sensitive callers.
|
|
1240
|
+
* An open PeerSession alone is not evidence that its authoritative Full has
|
|
1241
|
+
* crossed the host apply lane, especially during a same-key reconnect.
|
|
1242
|
+
*/
|
|
1243
|
+
isCurrentActive(properties: {
|
|
1244
|
+
peerHash: string;
|
|
1245
|
+
peerSession: object;
|
|
1246
|
+
receiveEpoch: object | null;
|
|
1247
|
+
senderTransportSession: bigint;
|
|
1248
|
+
}): boolean {
|
|
1249
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
1250
|
+
return (
|
|
1251
|
+
state !== undefined &&
|
|
1252
|
+
state.peerHash === properties.peerHash &&
|
|
1253
|
+
state.peerSession === properties.peerSession &&
|
|
1254
|
+
state.receiveEpoch === properties.receiveEpoch &&
|
|
1255
|
+
state.senderTransportSession === properties.senderTransportSession &&
|
|
1256
|
+
state.phase === "active" &&
|
|
1257
|
+
state.reservedAdmission === undefined &&
|
|
1258
|
+
!this._reservedAdmissionsByPeer.has(properties.peerHash) &&
|
|
1259
|
+
this.isStateCurrent(state)
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1238
1263
|
commit(admission: ReplicationInfoV2ReceiveAdmission): boolean {
|
|
1239
1264
|
if (admission.committed) {
|
|
1240
1265
|
return this.isAdmissionCurrent(admission);
|
|
@@ -1294,9 +1319,12 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
1294
1319
|
const state = this._receiveStates.get(properties.from.hashcode());
|
|
1295
1320
|
if (
|
|
1296
1321
|
!state ||
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1322
|
+
!this.isCurrentActive({
|
|
1323
|
+
peerHash: properties.from.hashcode(),
|
|
1324
|
+
peerSession: properties.peerSession,
|
|
1325
|
+
receiveEpoch: properties.receiveEpoch,
|
|
1326
|
+
senderTransportSession: properties.senderTransportSession,
|
|
1327
|
+
}) ||
|
|
1300
1328
|
!state.target.equals(properties.from) ||
|
|
1301
1329
|
!state.receiverBinding ||
|
|
1302
1330
|
!bytesEqual(request.receiverChallenge, state.receiverBinding) ||
|
|
@@ -1304,8 +1332,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
1304
1332
|
!bytesEqual(request.senderEpoch, state.senderEpoch) ||
|
|
1305
1333
|
request.sequence <= 0n ||
|
|
1306
1334
|
state.lastSequence === undefined ||
|
|
1307
|
-
state.lastSequence < request.sequence
|
|
1308
|
-
!this.isStateCurrent(state)
|
|
1335
|
+
state.lastSequence < request.sequence
|
|
1309
1336
|
) {
|
|
1310
1337
|
return undefined;
|
|
1311
1338
|
}
|
|
@@ -46,9 +46,16 @@ type ApplicationConfirmationRequest = {
|
|
|
46
46
|
controller: AbortController;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
type ApplicationConfirmationTarget = {
|
|
50
|
+
peerHash: string;
|
|
51
|
+
peerSession: object;
|
|
52
|
+
receiverTransportSession: bigint;
|
|
53
|
+
};
|
|
54
|
+
|
|
49
55
|
type ApplicationConfirmationWaiter = {
|
|
50
56
|
revision: bigint;
|
|
51
57
|
minPeers: number;
|
|
58
|
+
target?: ApplicationConfirmationTarget;
|
|
52
59
|
resolve: () => void;
|
|
53
60
|
reject: (error: unknown) => void;
|
|
54
61
|
timer?: ReturnType<typeof setTimeout>;
|
|
@@ -193,6 +200,10 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
clearPeer(peerHash: string, expectedSession?: object): void {
|
|
203
|
+
this.rejectTargetConfirmations(
|
|
204
|
+
{ peerHash, peerSession: expectedSession },
|
|
205
|
+
new AbortError("Replication confirmation destination changed"),
|
|
206
|
+
);
|
|
196
207
|
const state = this._sendStates.get(peerHash);
|
|
197
208
|
if (!state || (expectedSession && state.peerSession !== expectedSession)) {
|
|
198
209
|
return;
|
|
@@ -201,6 +212,10 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
201
212
|
}
|
|
202
213
|
|
|
203
214
|
advancePeerCapability(peerHash: string): void {
|
|
215
|
+
this.rejectTargetConfirmations(
|
|
216
|
+
{ peerHash },
|
|
217
|
+
new AbortError("Replication confirmation destination changed"),
|
|
218
|
+
);
|
|
204
219
|
const state = this._sendStates.get(peerHash);
|
|
205
220
|
if (state) {
|
|
206
221
|
this.clearState(state);
|
|
@@ -209,6 +224,14 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
209
224
|
|
|
210
225
|
private clearState(state: ReplicationInfoV2SendState): void {
|
|
211
226
|
this.trackRetiringWorker(state);
|
|
227
|
+
this.rejectTargetConfirmations(
|
|
228
|
+
{
|
|
229
|
+
peerHash: state.peerHash,
|
|
230
|
+
peerSession: state.peerSession,
|
|
231
|
+
receiverTransportSession: state.receiverTransportSession,
|
|
232
|
+
},
|
|
233
|
+
new AbortError("Replication confirmation destination changed"),
|
|
234
|
+
);
|
|
212
235
|
if (state.retryTimer) {
|
|
213
236
|
clearTimeout(state.retryTimer);
|
|
214
237
|
state.retryTimer = undefined;
|
|
@@ -251,7 +274,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
251
274
|
waiter.onAbort = undefined;
|
|
252
275
|
for (const state of this._sendStates.values()) {
|
|
253
276
|
const requested = state.applicationConfirmationRequest;
|
|
254
|
-
if (requested && !this.hasConfirmationFor(requested.revision)) {
|
|
277
|
+
if (requested && !this.hasConfirmationFor(state, requested.revision)) {
|
|
255
278
|
state.applicationConfirmationRequest = undefined;
|
|
256
279
|
requested.controller.abort(
|
|
257
280
|
new AbortError("Replication confirmation no longer pending"),
|
|
@@ -275,12 +298,50 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
275
298
|
}
|
|
276
299
|
}
|
|
277
300
|
|
|
278
|
-
private
|
|
301
|
+
private rejectTargetConfirmations(
|
|
302
|
+
target: {
|
|
303
|
+
peerHash: string;
|
|
304
|
+
peerSession?: object;
|
|
305
|
+
receiverTransportSession?: bigint;
|
|
306
|
+
},
|
|
307
|
+
error: unknown,
|
|
308
|
+
): void {
|
|
309
|
+
for (const waiter of [...this._confirmations]) {
|
|
310
|
+
if (
|
|
311
|
+
waiter.target === undefined ||
|
|
312
|
+
waiter.target.peerHash !== target.peerHash ||
|
|
313
|
+
(target.peerSession !== undefined &&
|
|
314
|
+
waiter.target.peerSession !== target.peerSession) ||
|
|
315
|
+
(target.receiverTransportSession !== undefined &&
|
|
316
|
+
waiter.target.receiverTransportSession !==
|
|
317
|
+
target.receiverTransportSession)
|
|
318
|
+
) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
this.clearConfirmationWaiter(waiter);
|
|
322
|
+
waiter.reject(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private matchesConfirmationTarget(
|
|
327
|
+
state: ReplicationInfoV2SendState,
|
|
328
|
+
target: ApplicationConfirmationTarget | undefined,
|
|
329
|
+
): boolean {
|
|
330
|
+
return (
|
|
331
|
+
target === undefined ||
|
|
332
|
+
(state.peerHash === target.peerHash &&
|
|
333
|
+
state.peerSession === target.peerSession &&
|
|
334
|
+
state.receiverTransportSession === target.receiverTransportSession)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private countAppliedPeers(waiter: ApplicationConfirmationWaiter): number {
|
|
279
339
|
let applied = 0;
|
|
280
340
|
for (const state of this._sendStates.values()) {
|
|
281
341
|
if (
|
|
282
342
|
state.appliedRevision !== undefined &&
|
|
283
|
-
state.appliedRevision >= revision &&
|
|
343
|
+
state.appliedRevision >= waiter.revision &&
|
|
344
|
+
this.matchesConfirmationTarget(state, waiter.target) &&
|
|
284
345
|
this.isDestinationReady(state) &&
|
|
285
346
|
this.supportsApplicationConfirmation(state)
|
|
286
347
|
) {
|
|
@@ -292,16 +353,22 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
292
353
|
|
|
293
354
|
private settleConfirmations(): void {
|
|
294
355
|
for (const waiter of [...this._confirmations]) {
|
|
295
|
-
if (this.countAppliedPeers(waiter
|
|
356
|
+
if (this.countAppliedPeers(waiter) >= waiter.minPeers) {
|
|
296
357
|
this.clearConfirmationWaiter(waiter);
|
|
297
358
|
waiter.resolve();
|
|
298
359
|
}
|
|
299
360
|
}
|
|
300
361
|
}
|
|
301
362
|
|
|
302
|
-
private hasConfirmationFor(
|
|
363
|
+
private hasConfirmationFor(
|
|
364
|
+
state: ReplicationInfoV2SendState,
|
|
365
|
+
revision: bigint,
|
|
366
|
+
): boolean {
|
|
303
367
|
for (const waiter of this._confirmations) {
|
|
304
|
-
if (
|
|
368
|
+
if (
|
|
369
|
+
waiter.revision <= revision &&
|
|
370
|
+
this.matchesConfirmationTarget(state, waiter.target)
|
|
371
|
+
) {
|
|
305
372
|
return true;
|
|
306
373
|
}
|
|
307
374
|
}
|
|
@@ -327,7 +394,8 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
327
394
|
for (const state of [...this._sendStates.values()]) {
|
|
328
395
|
if (
|
|
329
396
|
this.isCurrent(state) &&
|
|
330
|
-
this.supportsApplicationConfirmation(state)
|
|
397
|
+
this.supportsApplicationConfirmation(state) &&
|
|
398
|
+
this.hasConfirmationFor(state, this._revision)
|
|
331
399
|
) {
|
|
332
400
|
const revision = this._revision;
|
|
333
401
|
if (
|
|
@@ -351,22 +419,14 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
351
419
|
this.scheduleConfirmationRetry();
|
|
352
420
|
}
|
|
353
421
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
): Promise<void> {
|
|
363
|
-
const minPeers = options.minPeers ?? 1;
|
|
364
|
-
const timeout = options.timeout ?? DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
365
|
-
if (!Number.isSafeInteger(minPeers) || minPeers <= 0) {
|
|
366
|
-
return Promise.reject(
|
|
367
|
-
new RangeError("Replication confirmation minPeers must be positive"),
|
|
368
|
-
);
|
|
369
|
-
}
|
|
422
|
+
private waitForConfirmation(properties: {
|
|
423
|
+
minPeers: number;
|
|
424
|
+
target?: ApplicationConfirmationTarget;
|
|
425
|
+
timeout?: number;
|
|
426
|
+
signal?: AbortSignal;
|
|
427
|
+
timeoutMessage: (revision: bigint) => string;
|
|
428
|
+
}): Promise<void> {
|
|
429
|
+
const timeout = properties.timeout ?? DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
370
430
|
if (
|
|
371
431
|
!Number.isSafeInteger(timeout) ||
|
|
372
432
|
timeout <= 0 ||
|
|
@@ -381,10 +441,10 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
381
441
|
if (this.deps.isClosed()) {
|
|
382
442
|
return Promise.reject(new AbortError("Replication-info sender closed"));
|
|
383
443
|
}
|
|
384
|
-
if (
|
|
444
|
+
if (properties.signal?.aborted) {
|
|
385
445
|
return Promise.reject(
|
|
386
|
-
|
|
387
|
-
?
|
|
446
|
+
properties.signal.reason instanceof Error
|
|
447
|
+
? properties.signal.reason
|
|
388
448
|
: new AbortError("Replication confirmation aborted"),
|
|
389
449
|
);
|
|
390
450
|
}
|
|
@@ -399,30 +459,27 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
399
459
|
return new Promise<void>((resolve, reject) => {
|
|
400
460
|
const waiter: ApplicationConfirmationWaiter = {
|
|
401
461
|
revision: this._revision,
|
|
402
|
-
minPeers,
|
|
462
|
+
minPeers: properties.minPeers,
|
|
463
|
+
target: properties.target ? { ...properties.target } : undefined,
|
|
403
464
|
resolve,
|
|
404
465
|
reject,
|
|
405
|
-
signal:
|
|
466
|
+
signal: properties.signal,
|
|
406
467
|
};
|
|
407
468
|
waiter.timer = setTimeout(() => {
|
|
408
469
|
this.clearConfirmationWaiter(waiter);
|
|
409
|
-
reject(
|
|
410
|
-
new TimeoutError(
|
|
411
|
-
`Timed out waiting for ${minPeers} peer${minPeers === 1 ? "" : "s"} to apply replication revision ${waiter.revision}`,
|
|
412
|
-
),
|
|
413
|
-
);
|
|
470
|
+
reject(new TimeoutError(properties.timeoutMessage(waiter.revision)));
|
|
414
471
|
}, timeout);
|
|
415
472
|
waiter.timer.unref?.();
|
|
416
|
-
if (
|
|
473
|
+
if (properties.signal) {
|
|
417
474
|
waiter.onAbort = () => {
|
|
418
475
|
this.clearConfirmationWaiter(waiter);
|
|
419
476
|
reject(
|
|
420
|
-
|
|
421
|
-
?
|
|
477
|
+
properties.signal!.reason instanceof Error
|
|
478
|
+
? properties.signal!.reason
|
|
422
479
|
: new AbortError("Replication confirmation aborted"),
|
|
423
480
|
);
|
|
424
481
|
};
|
|
425
|
-
|
|
482
|
+
properties.signal.addEventListener("abort", waiter.onAbort, {
|
|
426
483
|
once: true,
|
|
427
484
|
});
|
|
428
485
|
}
|
|
@@ -431,6 +488,79 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
431
488
|
});
|
|
432
489
|
}
|
|
433
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Wait until the latest committed local replication revision is reported as
|
|
493
|
+
* durably applied by the configured number of current peers. Revisions newer
|
|
494
|
+
* than this call satisfy it, so rapid updates coalesce to one authoritative
|
|
495
|
+
* snapshot without weakening older callers' guarantees.
|
|
496
|
+
*/
|
|
497
|
+
confirmLatest(
|
|
498
|
+
options: ReplicationInfoV2ConfirmationOptions = {},
|
|
499
|
+
): Promise<void> {
|
|
500
|
+
const minPeers = options.minPeers ?? 1;
|
|
501
|
+
if (!Number.isSafeInteger(minPeers) || minPeers <= 0) {
|
|
502
|
+
return Promise.reject(
|
|
503
|
+
new RangeError("Replication confirmation minPeers must be positive"),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
return this.waitForConfirmation({
|
|
507
|
+
minPeers,
|
|
508
|
+
timeout: options.timeout,
|
|
509
|
+
signal: options.signal,
|
|
510
|
+
timeoutMessage: (revision) =>
|
|
511
|
+
`Timed out waiting for ${minPeers} peer${minPeers === 1 ? "" : "s"} to apply replication revision ${revision}`,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Confirm the latest revision on one exact destination generation. Multiple
|
|
517
|
+
* callers for that peer share the state's single snapshot/query worker, so
|
|
518
|
+
* persisted batches remain bounded by peers rather than entries.
|
|
519
|
+
*/
|
|
520
|
+
confirmLatestForPeer(
|
|
521
|
+
target: ApplicationConfirmationTarget,
|
|
522
|
+
options: Omit<ReplicationInfoV2ConfirmationOptions, "minPeers"> = {},
|
|
523
|
+
): Promise<void> {
|
|
524
|
+
if (
|
|
525
|
+
!this.deps.isPeerSessionCurrent(target.peerHash, target.peerSession) ||
|
|
526
|
+
!this.deps.isPeerSessionOpen(target.peerHash, target.peerSession) ||
|
|
527
|
+
this.deps.supportsApplicationConfirmation?.(
|
|
528
|
+
target.peerHash,
|
|
529
|
+
target.receiverTransportSession,
|
|
530
|
+
) !== true
|
|
531
|
+
) {
|
|
532
|
+
return Promise.reject(
|
|
533
|
+
new AbortError("Replication confirmation destination is not current"),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return this.waitForConfirmation({
|
|
537
|
+
minPeers: 1,
|
|
538
|
+
target,
|
|
539
|
+
timeout: options.timeout,
|
|
540
|
+
signal: options.signal,
|
|
541
|
+
timeoutMessage: (revision) =>
|
|
542
|
+
`Timed out waiting for peer ${target.peerHash} to apply replication revision ${revision}`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Read-only counterpart to {@link confirmLatestForPeer}. The target must be
|
|
548
|
+
* the exact current peer/transport generation and its latest local role
|
|
549
|
+
* revision must already have crossed the receiver's durable V2 apply lane.
|
|
550
|
+
*/
|
|
551
|
+
isLatestConfirmedForPeer(target: ApplicationConfirmationTarget): boolean {
|
|
552
|
+
const state = this._sendStates.get(target.peerHash);
|
|
553
|
+
return (
|
|
554
|
+
state !== undefined &&
|
|
555
|
+
state.peerSession === target.peerSession &&
|
|
556
|
+
state.receiverTransportSession === target.receiverTransportSession &&
|
|
557
|
+
state.appliedRevision !== undefined &&
|
|
558
|
+
state.appliedRevision >= this._revision &&
|
|
559
|
+
this.isCurrent(state) &&
|
|
560
|
+
this.supportsApplicationConfirmation(state)
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
434
564
|
private trackRetiringWorker(state: ReplicationInfoV2SendState): void {
|
|
435
565
|
const worker = state.worker;
|
|
436
566
|
if (!worker) {
|
|
@@ -973,7 +1103,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
973
1103
|
});
|
|
974
1104
|
state.inFlightSequence = undefined;
|
|
975
1105
|
if (
|
|
976
|
-
this.hasConfirmationFor(request.revision) &&
|
|
1106
|
+
this.hasConfirmationFor(state, request.revision) &&
|
|
977
1107
|
this.supportsApplicationConfirmation(state) &&
|
|
978
1108
|
this.isCurrent(state)
|
|
979
1109
|
) {
|