@peerbit/shared-log 16.0.21 → 16.0.22

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.
@@ -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 countAppliedPeers(revision: bigint): number {
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.revision) >= waiter.minPeers) {
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(revision: bigint): boolean {
363
+ private hasConfirmationFor(
364
+ state: ReplicationInfoV2SendState,
365
+ revision: bigint,
366
+ ): boolean {
303
367
  for (const waiter of this._confirmations) {
304
- if (waiter.revision <= revision) {
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
- * Wait until the latest committed local replication revision is reported as
356
- * durably applied by the configured number of current peers. Revisions newer
357
- * than this call satisfy it, so rapid updates coalesce to one authoritative
358
- * snapshot without weakening older callers' guarantees.
359
- */
360
- confirmLatest(
361
- options: ReplicationInfoV2ConfirmationOptions = {},
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 (options.signal?.aborted) {
444
+ if (properties.signal?.aborted) {
385
445
  return Promise.reject(
386
- options.signal.reason instanceof Error
387
- ? options.signal.reason
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: options.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 (options.signal) {
473
+ if (properties.signal) {
417
474
  waiter.onAbort = () => {
418
475
  this.clearConfirmationWaiter(waiter);
419
476
  reject(
420
- options.signal!.reason instanceof Error
421
- ? options.signal!.reason
477
+ properties.signal!.reason instanceof Error
478
+ ? properties.signal!.reason
422
479
  : new AbortError("Replication confirmation aborted"),
423
480
  );
424
481
  };
425
- options.signal.addEventListener("abort", waiter.onAbort, {
482
+ properties.signal.addEventListener("abort", waiter.onAbort, {
426
483
  once: true,
427
484
  });
428
485
  }
@@ -431,6 +488,61 @@ 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
+
434
546
  private trackRetiringWorker(state: ReplicationInfoV2SendState): void {
435
547
  const worker = state.worker;
436
548
  if (!worker) {
@@ -973,7 +1085,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
973
1085
  });
974
1086
  state.inFlightSequence = undefined;
975
1087
  if (
976
- this.hasConfirmationFor(request.revision) &&
1088
+ this.hasConfirmationFor(state, request.revision) &&
977
1089
  this.supportsApplicationConfirmation(state) &&
978
1090
  this.isCurrent(state)
979
1091
  ) {