@peerbit/shared-log 16.0.30 → 16.0.31

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/src/index.ts CHANGED
@@ -6352,6 +6352,95 @@ export class SharedLog<
6352
6352
  records.size,
6353
6353
  );
6354
6354
  const signal = deadline.signal;
6355
+ const recoveryController = new AbortController();
6356
+ const recoverySignal = AbortSignal.any([signal, recoveryController.signal]);
6357
+ const recoveryByPeer = new Map<
6358
+ string,
6359
+ {
6360
+ controller: AbortController;
6361
+ timer: ReturnType<typeof setTimeout>;
6362
+ }
6363
+ >();
6364
+ let recoveryCursor = 0;
6365
+ const recoverSelectedPeers = (selected: Set<string>) => {
6366
+ // Recovery is advisory work, not a receipt or a replacement leader plan.
6367
+ // Never occupy transfer/request slots while waiting for it. The separate
6368
+ // bounded pool rotates through fresh candidates so quiet/incomplete peers
6369
+ // cannot indefinitely hide a later recoverable peer.
6370
+ for (const [peer, state] of recoveryByPeer) {
6371
+ if (!selected.has(peer)) {
6372
+ clearTimeout(state.timer);
6373
+ state.controller.abort();
6374
+ }
6375
+ }
6376
+ const peers = [...selected];
6377
+ for (
6378
+ let visited = 0;
6379
+ visited < peers.length &&
6380
+ recoveryByPeer.size < MAX_PERSISTED_RECEIPT_REQUESTS_GLOBAL &&
6381
+ !recoverySignal.aborted;
6382
+ visited++
6383
+ ) {
6384
+ const peer = peers[recoveryCursor++ % peers.length]!;
6385
+ if (recoveryByPeer.has(peer)) continue;
6386
+ const current = this.persistedReceiptPeerSession(peer);
6387
+ if (
6388
+ current &&
6389
+ this._v2Send.isLatestConfirmedForPeer({
6390
+ peerHash: peer,
6391
+ peerSession: current.peerSession,
6392
+ receiverTransportSession: current.capabilitySession,
6393
+ })
6394
+ ) {
6395
+ continue;
6396
+ }
6397
+ const expiresAt = Math.min(
6398
+ deadline.deadline,
6399
+ Date.now() + MAX_PERSISTED_RECEIPT_ATTEMPT_MS,
6400
+ );
6401
+ const controller = new AbortController();
6402
+ const attemptSignal = AbortSignal.any([
6403
+ recoverySignal,
6404
+ controller.signal,
6405
+ ]);
6406
+ const timer = setTimeout(
6407
+ () => controller.abort(),
6408
+ Math.max(1, expiresAt - Date.now()),
6409
+ );
6410
+ timer.unref?.();
6411
+ const state = { controller, timer };
6412
+ recoveryByPeer.set(peer, state);
6413
+ void (async () => {
6414
+ // Resolve only an authenticated transport-cache key. Keep this slot
6415
+ // reserved until the lookup actually settles, even if a custom
6416
+ // resolver ignores cancellation, rather than launching duplicates.
6417
+ const key = await this._resolvePublicKeyFromHash(peer);
6418
+ if (
6419
+ attemptSignal.aborted ||
6420
+ !key ||
6421
+ key.hashcode() !== peer ||
6422
+ Date.now() >= expiresAt
6423
+ ) {
6424
+ return;
6425
+ }
6426
+ // Reuse the exact-session watchdog, capability/subscriber recovery,
6427
+ // and replacement-session handling from the public preflight. Its
6428
+ // result is ignored: settlement still checks the exact entry leaders
6429
+ // and accepts only the subsequent durable, session-bound receipts.
6430
+ await this.waitForPersistedReceiptPeerReadiness(key, {
6431
+ timeout: Math.max(1, expiresAt - Date.now()),
6432
+ signal: attemptSignal,
6433
+ });
6434
+ })()
6435
+ .catch(() => undefined)
6436
+ .finally(() => {
6437
+ clearTimeout(timer);
6438
+ if (recoveryByPeer.get(peer) === state) {
6439
+ recoveryByPeer.delete(peer);
6440
+ }
6441
+ });
6442
+ }
6443
+ };
6355
6444
  let maxAttemptMs = MAX_PERSISTED_RECEIPT_ATTEMPT_MS;
6356
6445
  let initialTransferPending = transferOnFirstRound;
6357
6446
  let needsInitialLeaderCheck = true;
@@ -6418,6 +6507,7 @@ export class SharedLog<
6418
6507
  // transport epoch. A revision/session change purges them before they can
6419
6508
  // survive an away-and-back leader transition or combine with a later peer.
6420
6509
  const hashesByPeer = new Map<string, string[]>();
6510
+ const recoveryCandidates = new Set<string>();
6421
6511
  const entryArray = [...records.values()];
6422
6512
  const leadersByEntry = await this.planPersistedDeliveryLeaders(
6423
6513
  entryArray,
@@ -6462,6 +6552,7 @@ export class SharedLog<
6462
6552
  if (acknowledgements.size >= minAcks) continue;
6463
6553
  for (const peer of leaders.keys()) {
6464
6554
  if (peer === selfHash) continue;
6555
+ recoveryCandidates.add(peer);
6465
6556
  const current = this.persistedReceiptPeerSession(peer);
6466
6557
  if (!current) continue;
6467
6558
  if (acknowledgements.has(peer)) continue;
@@ -6471,6 +6562,7 @@ export class SharedLog<
6471
6562
  }
6472
6563
  }
6473
6564
  if (!isRoundOwnershipCurrent()) continue;
6565
+ recoverSelectedPeers(recoveryCandidates);
6474
6566
 
6475
6567
  const operationQueue = new PQueue({
6476
6568
  concurrency: MAX_PERSISTED_RECEIPT_REQUESTS_GLOBAL,
@@ -6831,6 +6923,11 @@ export class SharedLog<
6831
6923
  }
6832
6924
  throw new PersistedDeliveryError(error, committedHashes);
6833
6925
  } finally {
6926
+ recoveryController.abort();
6927
+ for (const state of recoveryByPeer.values()) {
6928
+ clearTimeout(state.timer);
6929
+ state.controller.abort();
6930
+ }
6834
6931
  if (ownedDeadline) deadline.dispose();
6835
6932
  }
6836
6933
  }
@@ -20953,6 +21050,10 @@ export class SharedLog<
20953
21050
  const pruneRemoveTerminalFence = this.acquirePruneRemoveTerminalFence();
20954
21051
  try {
20955
21052
  this.stopSubscriptionChangeCallbackAdmission();
21053
+ // A receive may be awaiting a synchronizer response shipment. Cancel
21054
+ // its dispatch generation before draining that receive, rather than
21055
+ // waiting for _close() to reach the synchronizer's final teardown.
21056
+ this.syncronizer?.beginClose?.();
20956
21057
  this.joinWarmup.cancelAllJoinWarmupTargets();
20957
21058
  await this.drainSubscriptionChangeCallbacks();
20958
21059
  // An already-admitted subscription callback can create a fresh warmup
@@ -21091,6 +21192,7 @@ export class SharedLog<
21091
21192
  const pruneRemoveTerminalFence = this.acquirePruneRemoveTerminalFence();
21092
21193
  try {
21093
21194
  this.stopSubscriptionChangeCallbackAdmission();
21195
+ this.syncronizer?.beginClose?.();
21094
21196
  this.joinWarmup.cancelAllJoinWarmupTargets();
21095
21197
  await this.drainSubscriptionChangeCallbacks();
21096
21198
  // An already-admitted subscription callback can create a fresh warmup
package/src/sync/index.ts CHANGED
@@ -251,6 +251,13 @@ export interface Syncronizer<R extends "u32" | "u64"> {
251
251
  onPeerDisconnected(key: PublicSignKey | string): void;
252
252
 
253
253
  open(): Promise<void> | void;
254
+ /**
255
+ * Synchronously fence dispatch admission and cancel the current generation's
256
+ * network waits before SharedLog drains admitted receives. Do not stop shared
257
+ * indexes or discard physical-work accounting here: close() still runs after
258
+ * those receives settle. Optional for existing custom synchronizers.
259
+ */
260
+ beginClose?(): void;
254
261
  close(): Promise<void> | void;
255
262
 
256
263
  get pending(): number;
@@ -1127,6 +1127,9 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
1127
1127
  timeoutMs?: number;
1128
1128
  retryIntervalsMs?: number[];
1129
1129
  }): RepairSession {
1130
+ if (this.ratelessClosed) {
1131
+ return this.simple.startRepairSession(properties);
1132
+ }
1130
1133
  const mode = properties.mode ?? "best-effort";
1131
1134
  const targets = [...new Set(properties.targets)];
1132
1135
  const timeoutMs = Math.max(
@@ -3303,13 +3306,16 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3303
3306
  return this.simple.open();
3304
3307
  }
3305
3308
 
3306
- close(): Promise<void> | void {
3309
+ beginClose(): void {
3307
3310
  this.ratelessClosed = true;
3308
- // Abort ownership first. Process abort listeners then cancel any in-flight
3309
- // StartSync/MoreSymbols send before they detach or free native state.
3310
3311
  const reason = new Error("rateless synchronizer closed");
3311
3312
  this.cancelRatelessRepairSessions(reason);
3312
3313
  this.ratelessDispatchLifecycleController.abort(reason);
3314
+ this.simple.beginClose();
3315
+ }
3316
+
3317
+ close(): Promise<void> | void {
3318
+ this.beginClose();
3313
3319
  for (const obj of [...this.ingoingSyncProcesses.values()]) {
3314
3320
  obj.free();
3315
3321
  }
@@ -879,7 +879,9 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
879
879
  const epoch =
880
880
  expectedEpoch ??
881
881
  currentEpoch ??
882
- (options?.createTargetEpochs === false
882
+ (options?.createTargetEpochs === false ||
883
+ this.closed === true ||
884
+ ownershipLifecycleController.signal.aborted
883
885
  ? undefined
884
886
  : this.getOrCreateSyncDispatchTargetEpoch(target));
885
887
  if (!epoch) {
@@ -2362,7 +2364,11 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2362
2364
  requestedCount: trackedHashes.length,
2363
2365
  requestedTotalCount: allHashes.length,
2364
2366
  attempts: 0,
2365
- targetEpoch: this.getOrCreateSyncDispatchTargetEpoch(target),
2367
+ // Rejected late repairs only need result metadata. Do not create a
2368
+ // retained dispatch epoch after the terminal admission fence.
2369
+ targetEpoch: this.closed
2370
+ ? { id: 0 }
2371
+ : this.getOrCreateSyncDispatchTargetEpoch(target),
2366
2372
  });
2367
2373
  }
2368
2374
 
@@ -2377,6 +2383,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2377
2383
  deferred,
2378
2384
  cancelled: false,
2379
2385
  };
2386
+ if (this.closed) {
2387
+ deferred.resolve(this.buildRepairSessionResult(session, false));
2388
+ return { id, done: deferred.promise, cancel: () => {} };
2389
+ }
2380
2390
 
2381
2391
  if (allHashes.length === 0 || targets.length === 0) {
2382
2392
  deferred.resolve(this.buildRepairSessionResult(session, true));
@@ -3755,9 +3765,13 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3755
3765
  });
3756
3766
  }
3757
3767
 
3758
- async close() {
3768
+ beginClose(): void {
3759
3769
  this.closed = true;
3760
3770
  this.syncDispatchLifecycleController.abort();
3771
+ }
3772
+
3773
+ async close() {
3774
+ this.beginClose();
3761
3775
  this.syncDispatchTargetEpochs.clear();
3762
3776
  this.clearPendingSyncAdmissions();
3763
3777
  this.syncInFlightRetryIterator = undefined;