@peerbit/shared-log 16.0.31 → 16.0.32

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.
@@ -16,6 +16,26 @@
16
16
  // caller-specific fields such as epochs, batches and retained-work
17
17
  // counters); the registry owns the per-target active sets, the listener
18
18
  // add/remove pairing and the dispose gating.
19
+ import { AbortError } from "@peerbit/time";
20
+
21
+ /** Local receive drain cancellation; never a transport or persistence failure. */
22
+ export class SyncReceiveAbortError extends AbortError {}
23
+
24
+ export const isSyncDispatchCancellation = (
25
+ error: unknown,
26
+ signal: AbortSignal | undefined,
27
+ inactive = signal?.aborted === true,
28
+ ): boolean => {
29
+ if (!inactive) return false;
30
+ // Keep the existing owner/disconnect error policy. Only the new receive
31
+ // cancellation must distinguish unrelated failures that race its abort.
32
+ if (!(signal?.reason instanceof SyncReceiveAbortError)) return true;
33
+ return (
34
+ error === signal.reason ||
35
+ error instanceof AbortError ||
36
+ (error instanceof Error && error.name === "AbortError")
37
+ );
38
+ };
19
39
 
20
40
  export interface DispatchTargetLifecycleBase<LC> {
21
41
  lifecycle: LC;
package/src/sync/index.ts CHANGED
@@ -231,6 +231,9 @@ export interface Syncronizer<R extends "u32" | "u64"> {
231
231
  onMessage(
232
232
  message: TransportMessage,
233
233
  context: RequestContext,
234
+ // Exact admitted receive lifetime, not a peer-hash-wide disconnect.
235
+ // Implementations must still settle physical work before returning.
236
+ options?: { signal?: AbortSignal },
234
237
  ): Promise<boolean> | boolean;
235
238
 
236
239
  onReceivedEntries(properties: {
@@ -27,7 +27,9 @@ import { TransportMessage } from "../message.js";
27
27
  import { type EntryReplicated } from "../ranges.js";
28
28
  import {
29
29
  DispatchLifecycleRegistry,
30
+ SyncReceiveAbortError,
30
31
  isOwnershipGenerationActive,
32
+ isSyncDispatchCancellation,
31
33
  isTrackedSessionActive,
32
34
  } from "./dispatch-lifecycle.js";
33
35
  import type {
@@ -827,6 +829,7 @@ type OutgoingRatelessSyncProcess = {
827
829
  lastSeqNo: bigint;
828
830
  }) => { symbols: CodedSymbolBatch; exhaustedAfterSend: boolean } | undefined;
829
831
  startSimpleFallback: () => Promise<void>;
832
+ cancelSimpleFallback: (reason: unknown) => void;
830
833
  simpleFallbackStarted: boolean;
831
834
  free: (reason?: unknown) => void;
832
835
  processController: AbortController;
@@ -863,6 +866,7 @@ type IncomingRatelessSyncProcess = {
863
866
  symbols: CodedSymbolInput;
864
867
  }) => Promise<IncomingRatelessProcessResult>;
865
868
  requestAll: () => Promise<void>;
869
+ drain: () => Promise<void>;
866
870
  fallbackToSimple: (reason?: unknown) => Promise<void>;
867
871
  free: (reason?: unknown) => void;
868
872
  complete: () => void;
@@ -1571,6 +1575,35 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
1571
1575
  );
1572
1576
  }
1573
1577
 
1578
+ private async settleIncomingReceive(
1579
+ process: IncomingRatelessSyncProcess,
1580
+ signal: AbortSignal,
1581
+ ): Promise<void> {
1582
+ const errors: unknown[] = [];
1583
+ try {
1584
+ await process.drain();
1585
+ } catch (error) {
1586
+ errors.push(error);
1587
+ }
1588
+ if (signal.aborted) {
1589
+ try {
1590
+ process.free(signal.reason);
1591
+ } catch (error) {
1592
+ errors.push(error);
1593
+ }
1594
+ }
1595
+ if (errors.length === 1) throw errors[0];
1596
+ if (errors.length > 1) {
1597
+ throw new AggregateError(
1598
+ errors,
1599
+ "incoming receive drain and cleanup failed",
1600
+ {
1601
+ cause: errors[0],
1602
+ },
1603
+ );
1604
+ }
1605
+ }
1606
+
1574
1607
  async onMaybeMissingEntries(properties: {
1575
1608
  entries: Map<string, SyncEntryCoordinates<D>>;
1576
1609
  targets: string[];
@@ -2008,6 +2041,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2008
2041
  let symbolsProduced = startSyncSymbols.length;
2009
2042
  let symbolBudgetExhausted = false;
2010
2043
  let simpleFallbackPromise: Promise<void> | undefined;
2044
+ const simpleFallbackController = new AbortController();
2011
2045
  const symbolBudget = getOutgoingRatelessSymbolBudget(
2012
2046
  properties.coordinates.length,
2013
2047
  startSyncSymbols.length,
@@ -2055,7 +2089,14 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2055
2089
  this.simple.onMaybeMissingHashes({
2056
2090
  hashes: properties.outgoingHashes,
2057
2091
  targets: [target],
2058
- signal: lifecycle.callerSignal,
2092
+ // Encoder retirement must not cancel an admitted fallback.
2093
+ // A receive awaiting that exact memoized operation can do so.
2094
+ signal: lifecycle.callerSignal
2095
+ ? AbortSignal.any([
2096
+ lifecycle.callerSignal,
2097
+ simpleFallbackController.signal,
2098
+ ])
2099
+ : simpleFallbackController.signal,
2059
2100
  }),
2060
2101
  );
2061
2102
  }
@@ -2150,6 +2191,8 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2150
2191
  return { symbols, exhaustedAfterSend };
2151
2192
  },
2152
2193
  startSimpleFallback,
2194
+ cancelSimpleFallback: (reason) =>
2195
+ simpleFallbackController.abort(reason),
2153
2196
  simpleFallbackStarted: false,
2154
2197
  free: clear,
2155
2198
  outgoingHashes: properties.outgoingHashes,
@@ -2386,7 +2429,74 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2386
2429
  async onMessage(
2387
2430
  message: TransportMessage,
2388
2431
  context: RequestContext,
2432
+ options?: { signal?: AbortSignal },
2389
2433
  ): Promise<boolean> {
2434
+ const signal = options?.signal;
2435
+ if (!signal) {
2436
+ return this.onMessageForReceive(message, context);
2437
+ }
2438
+ let cancel: (() => void) | undefined;
2439
+ let settle: (() => Promise<void>) | undefined;
2440
+ const onAbort = () => cancel?.();
2441
+ signal.addEventListener("abort", onAbort, { once: true });
2442
+ const errors: unknown[] = [];
2443
+ let handled = false;
2444
+ try {
2445
+ handled = await this.onMessageForReceive(message, context, {
2446
+ signal,
2447
+ bind: (cancelWork, settleWork) => {
2448
+ cancel = cancelWork;
2449
+ settle = settleWork;
2450
+ if (signal.aborted) {
2451
+ onAbort();
2452
+ }
2453
+ },
2454
+ });
2455
+ } catch (error) {
2456
+ errors.push(error);
2457
+ } finally {
2458
+ // Keep cancellation connected while a bounded fallback's logical
2459
+ // completion still leaves its lower send physically draining.
2460
+ try {
2461
+ await settle?.();
2462
+ } catch (error) {
2463
+ if (!errors.includes(error)) {
2464
+ errors.push(error);
2465
+ }
2466
+ } finally {
2467
+ signal.removeEventListener("abort", onAbort);
2468
+ }
2469
+ }
2470
+ if (errors.length === 1) throw errors[0];
2471
+ if (errors.length > 1) {
2472
+ throw new AggregateError(
2473
+ errors,
2474
+ "sync receive and physical cleanup failed",
2475
+ {
2476
+ cause: errors[0],
2477
+ },
2478
+ );
2479
+ }
2480
+ return handled;
2481
+ }
2482
+
2483
+ private async onMessageForReceive(
2484
+ message: TransportMessage,
2485
+ context: RequestContext,
2486
+ options?: {
2487
+ signal: AbortSignal;
2488
+ bind: (cancel: () => void, settle?: () => Promise<void>) => void;
2489
+ },
2490
+ ): Promise<boolean> {
2491
+ if (options?.signal.aborted) {
2492
+ return (
2493
+ message instanceof StartSync ||
2494
+ message instanceof MoreSymbols ||
2495
+ message instanceof RequestMoreSymbols ||
2496
+ message instanceof RequestAll ||
2497
+ (await this.simple.onMessage(message, context, options))
2498
+ );
2499
+ }
2390
2500
  const profile = this.properties.sync?.profile;
2391
2501
  if (message instanceof StartSync) {
2392
2502
  const from = context.from;
@@ -2511,6 +2621,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2511
2621
  symbolBudget: getIncomingRatelessSymbolBudget(message.symbols.length),
2512
2622
  process: async () => undefined,
2513
2623
  requestAll: async () => {},
2624
+ drain: async () => {},
2514
2625
  fallbackToSimple: async () => {},
2515
2626
  free,
2516
2627
  complete,
@@ -2522,6 +2633,10 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2522
2633
  onOwnershipAbort,
2523
2634
  { once: true },
2524
2635
  );
2636
+ options?.bind(
2637
+ () => controller.abort(options.signal.reason),
2638
+ () => this.settleIncomingReceive(obj, options.signal),
2639
+ );
2525
2640
  obj.deadlineTimeout = setTimeout(() => {
2526
2641
  void obj.fallbackToSimple(
2527
2642
  new Error("incoming rateless process deadline exceeded"),
@@ -2534,6 +2649,9 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2534
2649
  }
2535
2650
 
2536
2651
  let requestAllPromise: Promise<void> | undefined;
2652
+ obj.drain = async () => {
2653
+ await requestAllPromise;
2654
+ };
2537
2655
  obj.requestAll = () => {
2538
2656
  if (requestAllPromise) {
2539
2657
  return requestAllPromise;
@@ -2566,6 +2684,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2566
2684
  syncId,
2567
2685
  });
2568
2686
  }
2687
+ } catch (error) {
2688
+ // Classify before complete() aborts the process as normal cleanup;
2689
+ // that cleanup must not disguise a genuine lower-send failure.
2690
+ if (!isSyncDispatchCancellation(error, controller.signal)) {
2691
+ throw error;
2692
+ }
2569
2693
  } finally {
2570
2694
  fallbackSendPending = false;
2571
2695
  if (this.isIncomingSyncProcessActive(obj)) {
@@ -2643,7 +2767,8 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2643
2767
  const processAborted = controller.signal.aborted;
2644
2768
  free(error);
2645
2769
  if (
2646
- processAborted ||
2770
+ (processAborted &&
2771
+ isSyncDispatchCancellation(error, controller.signal)) ||
2647
2772
  !this.isIncomingSyncGenerationActive(ownershipLifecycleController)
2648
2773
  ) {
2649
2774
  return true;
@@ -2924,7 +3049,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2924
3049
  },
2925
3050
  );
2926
3051
  } catch (error) {
2927
- if (!this.isIncomingSyncProcessActive(obj)) {
3052
+ if (
3053
+ isSyncDispatchCancellation(
3054
+ error,
3055
+ controller.signal,
3056
+ !this.isIncomingSyncProcessActive(obj),
3057
+ )
3058
+ ) {
2928
3059
  return true;
2929
3060
  }
2930
3061
  free(error);
@@ -2959,6 +3090,10 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
2959
3090
  ) {
2960
3091
  return true;
2961
3092
  }
3093
+ options?.bind(
3094
+ () => obj.controller.abort(options.signal.reason),
3095
+ () => this.settleIncomingReceive(obj, options.signal),
3096
+ );
2962
3097
  let outProcess: IncomingRatelessProcessResult;
2963
3098
  try {
2964
3099
  outProcess = await obj.process(message);
@@ -3005,7 +3140,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3005
3140
  signal: obj.controller.signal,
3006
3141
  },
3007
3142
  );
3008
- } catch {
3143
+ } catch (error) {
3144
+ if (
3145
+ obj.controller.signal.reason instanceof SyncReceiveAbortError &&
3146
+ !isSyncDispatchCancellation(error, obj.controller.signal)
3147
+ ) {
3148
+ throw error;
3149
+ }
3009
3150
  if (profile) {
3010
3151
  emitSyncProfileDuration(profile, sendStartedAt, {
3011
3152
  name: "rateless.sendRequestMoreSymbols",
@@ -3039,6 +3180,14 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3039
3180
  if (context.from?.hashcode() !== obj.target) {
3040
3181
  return true;
3041
3182
  }
3183
+ options?.bind(
3184
+ () => obj.processController.abort(options.signal.reason),
3185
+ async () => {
3186
+ if (options.signal.aborted) {
3187
+ obj.free(options.signal.reason);
3188
+ }
3189
+ },
3190
+ );
3042
3191
  const signal = obj.signal;
3043
3192
  if (signal.aborted) {
3044
3193
  return true;
@@ -3066,7 +3215,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3066
3215
  },
3067
3216
  );
3068
3217
  } catch (error) {
3069
- if (signal?.aborted) {
3218
+ if (isSyncDispatchCancellation(error, signal)) {
3070
3219
  return true;
3071
3220
  }
3072
3221
  throw error;
@@ -3101,7 +3250,14 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3101
3250
  }
3102
3251
  // RequestAll ends only this target's rateless attempt. Other target
3103
3252
  // encoders and response authorizations remain independently owned.
3104
- const fallback = p.startSimpleFallback();
3253
+ let fallback: Promise<void> | undefined;
3254
+ options?.bind(
3255
+ () => p.cancelSimpleFallback(options.signal.reason),
3256
+ async () => {
3257
+ await fallback;
3258
+ },
3259
+ );
3260
+ fallback = p.startSimpleFallback();
3105
3261
  p.free();
3106
3262
  await fallback;
3107
3263
  return true;
@@ -3174,13 +3330,17 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3174
3330
  entries: 0,
3175
3331
  };
3176
3332
  try {
3333
+ const signal = options?.signal
3334
+ ? AbortSignal.any([response.signal, options.signal])
3335
+ : response.signal;
3177
3336
  responseShipment =
3178
3337
  await this.simple.shipAuthorizedMaybeSyncResponse({
3179
3338
  hashes: response.authorized,
3180
3339
  from,
3181
3340
  response: message,
3182
- signal: response.signal,
3341
+ signal,
3183
3342
  });
3343
+ rollbackRatelessAuthorization = signal.aborted;
3184
3344
  } catch (error) {
3185
3345
  firstError = error;
3186
3346
  rollbackRatelessAuthorization = true;
@@ -3209,6 +3369,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3209
3369
  leases: simpleLeases,
3210
3370
  from,
3211
3371
  response: simpleMessage,
3372
+ signal: options?.signal,
3212
3373
  });
3213
3374
  } catch (error) {
3214
3375
  firstError ??= error;
@@ -3229,7 +3390,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
3229
3390
  }
3230
3391
  }
3231
3392
  }
3232
- return this.simple.onMessage(message, context);
3393
+ return this.simple.onMessage(message, context, options);
3233
3394
  }
3234
3395
 
3235
3396
  onReceivedEntries(properties: {