@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.
@@ -20,7 +20,10 @@ import {
20
20
  } from "../exchange-heads.js";
21
21
  import { TransportMessage } from "../message.js";
22
22
  import type { EntryReplicated } from "../ranges.js";
23
- import { DispatchLifecycleRegistry } from "./dispatch-lifecycle.js";
23
+ import {
24
+ DispatchLifecycleRegistry,
25
+ isSyncDispatchCancellation,
26
+ } from "./dispatch-lifecycle.js";
24
27
  import type {
25
28
  HashSymbolHashListResolver,
26
29
  HashSymbolResolver,
@@ -586,7 +589,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
586
589
  ) => boolean;
587
590
  private peerSupportsRawExchangeHeads?: (peer: string) => boolean;
588
591
  private sendRawExchangeHeads?: RawExchangeHeadsSender;
589
- private recentlySentExchangeHeads: Map<string, Map<string, number>>;
592
+ private recentlySentExchangeHeads: Map<
593
+ string,
594
+ Map<string, { timestamp: number }>
595
+ >;
590
596
  private pendingMaybeSyncResponses: Map<
591
597
  string,
592
598
  Map<string, PendingMaybeSyncResponseAuthorization>
@@ -774,16 +780,17 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
774
780
  private filterRecentlySentExchangeHeads(
775
781
  hashes: Iterable<string>,
776
782
  peer: PublicSignKey,
777
- ): string[] {
783
+ ): { hashes: string[]; rollback: () => void } {
778
784
  const peerHash = peer.hashcode();
779
785
  const now = Date.now();
786
+ const stamp = { timestamp: now };
780
787
  let recentlySent = this.recentlySentExchangeHeads.get(peerHash);
781
788
  if (!recentlySent) {
782
789
  recentlySent = new Map();
783
790
  this.recentlySentExchangeHeads.set(peerHash, recentlySent);
784
791
  }
785
- for (const [hash, timestamp] of recentlySent) {
786
- if (now - timestamp > EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS) {
792
+ for (const [hash, previous] of recentlySent) {
793
+ if (now - previous.timestamp > EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS) {
787
794
  recentlySent.delete(hash);
788
795
  }
789
796
  }
@@ -806,26 +813,27 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
806
813
  ) {
807
814
  continue;
808
815
  }
809
- recentlySent.set(hash, now);
816
+ recentlySent.set(hash, stamp);
810
817
  out.push(hash);
811
818
  }
812
- return out;
813
- }
814
-
815
- private forgetRecentlySentExchangeHeads(
816
- hashes: Iterable<string>,
817
- peer: PublicSignKey,
818
- ): void {
819
- const recentlySent = this.recentlySentExchangeHeads.get(peer.hashcode());
820
- if (!recentlySent) {
821
- return;
822
- }
823
- for (const hash of hashes) {
824
- recentlySent.delete(hash);
825
- }
826
- if (recentlySent.size === 0) {
827
- this.recentlySentExchangeHeads.delete(peer.hashcode());
828
- }
819
+ return {
820
+ hashes: out,
821
+ rollback: () => {
822
+ // A cancelled predecessor may settle after a fresh receive shipped
823
+ // the same hash. Only undo this exact attempt's dedupe ownership.
824
+ for (const hash of out) {
825
+ if (recentlySent.get(hash) === stamp) {
826
+ recentlySent.delete(hash);
827
+ }
828
+ }
829
+ if (
830
+ recentlySent.size === 0 &&
831
+ this.recentlySentExchangeHeads.get(peerHash) === recentlySent
832
+ ) {
833
+ this.recentlySentExchangeHeads.delete(peerHash);
834
+ }
835
+ },
836
+ };
829
837
  }
830
838
 
831
839
  private getOrCreateSyncDispatchTargetEpoch(
@@ -881,6 +889,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
881
889
  currentEpoch ??
882
890
  (options?.createTargetEpochs === false ||
883
891
  this.closed === true ||
892
+ callerSignal?.aborted === true ||
884
893
  ownershipLifecycleController.signal.aborted
885
894
  ? undefined
886
895
  : this.getOrCreateSyncDispatchTargetEpoch(target));
@@ -2530,8 +2539,12 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2530
2539
  } catch (error) {
2531
2540
  reservation.release();
2532
2541
  if (
2533
- !this.isSyncDispatchLifecycleActive(lifecycle) ||
2534
- !this.isSyncDispatchLifecycleActive(lifecycle, target)
2542
+ isSyncDispatchCancellation(
2543
+ error,
2544
+ this.getSyncDispatchSignal(lifecycle, target),
2545
+ !this.isSyncDispatchLifecycleActive(lifecycle) ||
2546
+ !this.isSyncDispatchLifecycleActive(lifecycle, target),
2547
+ )
2535
2548
  ) {
2536
2549
  break;
2537
2550
  }
@@ -2593,7 +2606,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2593
2606
  { signal },
2594
2607
  );
2595
2608
  } catch (error) {
2596
- if (signal?.aborted) {
2609
+ if (isSyncDispatchCancellation(error, signal)) {
2597
2610
  return { messages: 0, fused: true };
2598
2611
  }
2599
2612
  throw error;
@@ -2621,7 +2634,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2621
2634
  });
2622
2635
  messages += 1;
2623
2636
  } catch (error) {
2624
- if (signal?.aborted) {
2637
+ if (isSyncDispatchCancellation(error, signal)) {
2625
2638
  break;
2626
2639
  }
2627
2640
  throw error;
@@ -2644,7 +2657,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2644
2657
  response: ResponseMaybeSync | ResponseMaybeSyncCapabilities;
2645
2658
  signal: AbortSignal;
2646
2659
  }): Promise<{ messages: number; fused: boolean; entries: number }> {
2647
- const hashes = this.filterRecentlySentExchangeHeads(
2660
+ const { hashes, rollback } = this.filterRecentlySentExchangeHeads(
2648
2661
  properties.hashes,
2649
2662
  properties.from,
2650
2663
  );
@@ -2660,11 +2673,11 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2660
2673
  entries: hashes.length,
2661
2674
  };
2662
2675
  } catch (error) {
2663
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2676
+ rollback();
2664
2677
  throw error;
2665
2678
  } finally {
2666
2679
  if (properties.signal.aborted) {
2667
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2680
+ rollback();
2668
2681
  }
2669
2682
  }
2670
2683
  }
@@ -2673,6 +2686,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2673
2686
  leases: AuthorizedMaybeSyncResponseLease[];
2674
2687
  from: PublicSignKey;
2675
2688
  response: ResponseMaybeSync | ResponseMaybeSyncCapabilities;
2689
+ signal?: AbortSignal;
2676
2690
  source?: string;
2677
2691
  }): Promise<{ messages: number; fused: boolean; entries: number }> {
2678
2692
  if (properties.leases.length === 0) {
@@ -2685,18 +2699,21 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2685
2699
  let entries = 0;
2686
2700
  let firstError: unknown;
2687
2701
  for (const lease of properties.leases) {
2702
+ const signal = properties.signal
2703
+ ? AbortSignal.any([lease.signal, properties.signal])
2704
+ : lease.signal;
2688
2705
  let fulfilled = false;
2689
2706
  try {
2690
2707
  const shipped = await this.shipAuthorizedMaybeSyncResponse({
2691
2708
  hashes: lease.hashes,
2692
2709
  from: properties.from,
2693
2710
  response: properties.response,
2694
- signal: lease.signal,
2711
+ signal,
2695
2712
  });
2696
2713
  messages += shipped.messages;
2697
2714
  fused ||= shipped.fused;
2698
2715
  entries += shipped.entries;
2699
- fulfilled = !lease.signal.aborted;
2716
+ fulfilled = !signal.aborted;
2700
2717
  } catch (error) {
2701
2718
  firstError ??= error;
2702
2719
  } finally {
@@ -2724,10 +2741,20 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2724
2741
  async onMessage(
2725
2742
  msg: TransportMessage,
2726
2743
  context: RequestContext,
2744
+ options?: { signal?: AbortSignal },
2727
2745
  ): Promise<boolean> {
2746
+ if (options?.signal?.aborted) {
2747
+ return (
2748
+ msg instanceof RequestMaybeSync ||
2749
+ msg instanceof ResponseMaybeSync ||
2750
+ msg instanceof ResponseMaybeSyncCapabilities ||
2751
+ msg instanceof RequestMaybeSyncCoordinate ||
2752
+ msg instanceof RequestMaybeSyncCoordinateCapabilities
2753
+ );
2754
+ }
2728
2755
  const from = context.from!;
2729
2756
  if (msg instanceof RequestMaybeSync) {
2730
- await this.queueSync(msg.hashes, from);
2757
+ await this.queueSync(msg.hashes, from, options);
2731
2758
  return true;
2732
2759
  } else if (
2733
2760
  msg instanceof ResponseMaybeSync ||
@@ -2744,6 +2771,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2744
2771
  leases: pending,
2745
2772
  from,
2746
2773
  response: msg,
2774
+ signal: options?.signal,
2747
2775
  });
2748
2776
  return true;
2749
2777
  } else if (
@@ -2762,7 +2790,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2762
2790
  return true;
2763
2791
  }
2764
2792
  const { release: releaseLookup, row: slotRow } = lookupPermit;
2765
- const lifecycle = this.captureSyncDispatchLifecycle([target]);
2793
+ const lifecycle = this.captureSyncDispatchLifecycle(
2794
+ [target],
2795
+ options?.signal,
2796
+ );
2766
2797
  let lifecycleFinished = false;
2767
2798
  const finishLifecycle = () => {
2768
2799
  if (lifecycleFinished) {
@@ -2820,16 +2851,25 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2820
2851
  let hashesToSend: string[] = [];
2821
2852
  let messages = 0;
2822
2853
  let fused = false;
2854
+ let rollback: (() => void) | undefined;
2855
+ const signal = this.getSyncDispatchSignal(lifecycle, target);
2823
2856
  try {
2824
- hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
2857
+ ({ hashes: hashesToSend, rollback } =
2858
+ this.filterRecentlySentExchangeHeads(hashes, from));
2825
2859
  // dont set priority 1 here because this will block other messages that should higher priority
2826
2860
  ({ messages, fused } = await this.shipExchangeHeads(
2827
2861
  hashesToSend,
2828
2862
  context.from!,
2829
2863
  canReceiveRawExchangeHeads(msg),
2830
- this.getSyncDispatchSignal(lifecycle, target),
2864
+ signal,
2831
2865
  ));
2866
+ } catch (error) {
2867
+ rollback?.();
2868
+ throw error;
2832
2869
  } finally {
2870
+ if (signal.aborted) {
2871
+ rollback?.();
2872
+ }
2833
2873
  releaseResponse();
2834
2874
  if (profile) {
2835
2875
  emitSyncProfileDuration(profile, exchangeStartedAt, {
@@ -3095,9 +3135,9 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3095
3135
  async queueSync(
3096
3136
  keys: SyncableKey[],
3097
3137
  from: PublicSignKey,
3098
- options?: { skipCheck?: boolean },
3138
+ options?: { skipCheck?: boolean; signal?: AbortSignal },
3099
3139
  ) {
3100
- if (this.closed === true || keys.length === 0) {
3140
+ if (this.closed === true || options?.signal?.aborted || keys.length === 0) {
3101
3141
  return;
3102
3142
  }
3103
3143
  // A delayed timer must not let expired claims or admission reservations
@@ -3124,6 +3164,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3124
3164
  const ownershipLifecycleController = this.syncDispatchLifecycleController;
3125
3165
  const isCapturedLifecycleActive = () =>
3126
3166
  this.closed !== true &&
3167
+ options?.signal?.aborted !== true &&
3127
3168
  this.syncDispatchLifecycleController === ownershipLifecycleController &&
3128
3169
  !ownershipLifecycleController.signal.aborted &&
3129
3170
  this.syncDispatchTargetEpochs.get(fromHash) === targetEpoch;
@@ -3219,6 +3260,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3219
3260
  const existingRequest =
3220
3261
  dispatchableExistingRequestHashes.length > 0
3221
3262
  ? this.requestSync(dispatchableExistingRequestHashes, [fromHash], {
3263
+ signal: options?.signal,
3222
3264
  ownershipLifecycleController,
3223
3265
  targetEpochs: new Map([[fromHash, targetEpoch]]),
3224
3266
  createTargetEpochs: false,
@@ -3229,54 +3271,42 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3229
3271
  // when a later lifecycle check returns before joining it.
3230
3272
  void existingRequest?.catch(() => {});
3231
3273
  const resolveKnownStartedAt = syncProfileStart(profile);
3232
- try {
3233
- const knownKeys =
3234
- options?.skipCheck === true || keysToCheck.length === 0
3235
- ? undefined
3236
- : await this.resolveKnownSyncKeys(keysToCheck);
3237
- if (!isCapturedLifecycleActive()) {
3238
- return;
3239
- }
3240
- if (profile) {
3241
- emitSyncProfileDuration(profile, resolveKnownStartedAt, {
3242
- name: "simple.queueSync.resolveKnown",
3243
- entries: keysToCheck.length,
3244
- count: knownKeys?.keys.size ?? 0,
3245
- details: {
3246
- checkedCoordinates: knownKeys?.checkedCoordinates === true,
3247
- checkedHashes: knownKeys?.checkedHashes === true,
3248
- skipCheck: options?.skipCheck === true,
3249
- },
3250
- });
3251
- }
3252
-
3253
- if (keysToCheck.length > 0) {
3254
- // A resolver/index lookup may have populated coordinateToHash while
3255
- // it yielded. Refresh another fixed-size slice, never the full queue.
3256
- this.refreshQueuedSyncCoordinateAliases();
3257
- }
3258
- const loopStartedAt = syncProfileStart(profile);
3259
- for (let index = 0; index < keysToCheck.length; index += 1) {
3260
- const key = keysToCheck[index]!;
3261
- const identity = identitiesToCheck[index]!;
3274
+ const planning = (async () => {
3275
+ try {
3276
+ const knownKeys =
3277
+ options?.skipCheck === true || keysToCheck.length === 0
3278
+ ? undefined
3279
+ : await this.resolveKnownSyncKeys(keysToCheck);
3262
3280
  if (!isCapturedLifecycleActive()) {
3263
3281
  return;
3264
3282
  }
3265
- const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
3266
- if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
3267
- const consumption = this.consumePendingSyncAdmission(
3268
- admission!,
3269
- identity,
3270
- );
3271
- if (consumption === "invalid") {
3283
+ if (profile) {
3284
+ emitSyncProfileDuration(profile, resolveKnownStartedAt, {
3285
+ name: "simple.queueSync.resolveKnown",
3286
+ entries: keysToCheck.length,
3287
+ count: knownKeys?.keys.size ?? 0,
3288
+ details: {
3289
+ checkedCoordinates: knownKeys?.checkedCoordinates === true,
3290
+ checkedHashes: knownKeys?.checkedHashes === true,
3291
+ skipCheck: options?.skipCheck === true,
3292
+ },
3293
+ });
3294
+ }
3295
+
3296
+ if (keysToCheck.length > 0) {
3297
+ // A resolver/index lookup may have populated coordinateToHash while
3298
+ // it yielded. Refresh another fixed-size slice, never the full queue.
3299
+ this.refreshQueuedSyncCoordinateAliases();
3300
+ }
3301
+ const loopStartedAt = syncProfileStart(profile);
3302
+ for (let index = 0; index < keysToCheck.length; index += 1) {
3303
+ const key = keysToCheck[index]!;
3304
+ const identity = identitiesToCheck[index]!;
3305
+ if (!isCapturedLifecycleActive()) {
3272
3306
  return;
3273
3307
  }
3274
- continue;
3275
- }
3276
- const coordinateOrHash = queuedKeyResult ?? key;
3277
- const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
3278
- if (inFlight) {
3279
- if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
3308
+ const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
3309
+ if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
3280
3310
  const consumption = this.consumePendingSyncAdmission(
3281
3311
  admission!,
3282
3312
  identity,
@@ -3284,79 +3314,104 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3284
3314
  if (consumption === "invalid") {
3285
3315
  return;
3286
3316
  }
3287
- if (consumption === "settled") {
3317
+ continue;
3318
+ }
3319
+ const coordinateOrHash = queuedKeyResult ?? key;
3320
+ const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
3321
+ if (inFlight) {
3322
+ if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
3323
+ const consumption = this.consumePendingSyncAdmission(
3324
+ admission!,
3325
+ identity,
3326
+ );
3327
+ if (consumption === "invalid") {
3328
+ return;
3329
+ }
3330
+ if (consumption === "settled") {
3331
+ continue;
3332
+ }
3333
+ this.movePendingSyncKeyExpiryEarlier(
3334
+ coordinateOrHash,
3335
+ admission!.expiresAt,
3336
+ );
3337
+ const added = this.addPendingSyncClaim(
3338
+ coordinateOrHash,
3339
+ from,
3340
+ admission!.expiresAt,
3341
+ );
3342
+ if (added) {
3343
+ requestHashes.push(coordinateOrHash);
3344
+ }
3345
+ }
3346
+ } else {
3347
+ const has =
3348
+ options?.skipCheck !== true &&
3349
+ (await this.checkHasCoordinateOrHash(
3350
+ coordinateOrHash,
3351
+ knownKeys,
3352
+ ));
3353
+ if (!isCapturedLifecycleActive()) {
3354
+ return;
3355
+ }
3356
+ if (has) {
3357
+ this.clearPendingSyncAdmissionIdentity(identity);
3288
3358
  continue;
3289
3359
  }
3290
- this.movePendingSyncKeyExpiryEarlier(
3291
- coordinateOrHash,
3292
- admission!.expiresAt,
3360
+ const consumption = this.consumePendingSyncAdmission(
3361
+ admission!,
3362
+ identity,
3293
3363
  );
3294
- const added = this.addPendingSyncClaim(
3364
+ if (consumption === "invalid") {
3365
+ return;
3366
+ }
3367
+ if (consumption === "settled") {
3368
+ continue;
3369
+ }
3370
+ // Track the initial sender so we can retry if the first request is lost.
3371
+ this.addPendingSyncClaim(
3295
3372
  coordinateOrHash,
3296
3373
  from,
3297
3374
  admission!.expiresAt,
3298
3375
  );
3299
- if (added) {
3300
- requestHashes.push(coordinateOrHash);
3301
- }
3302
- }
3303
- } else {
3304
- const has =
3305
- options?.skipCheck !== true &&
3306
- (await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys));
3307
- if (!isCapturedLifecycleActive()) {
3308
- return;
3309
- }
3310
- if (has) {
3311
- this.clearPendingSyncAdmissionIdentity(identity);
3312
- continue;
3313
- }
3314
- const consumption = this.consumePendingSyncAdmission(
3315
- admission!,
3316
- identity,
3317
- );
3318
- if (consumption === "invalid") {
3319
- return;
3320
- }
3321
- if (consumption === "settled") {
3322
- continue;
3376
+ requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
3323
3377
  }
3324
- // Track the initial sender so we can retry if the first request is lost.
3325
- this.addPendingSyncClaim(
3326
- coordinateOrHash,
3327
- from,
3328
- admission!.expiresAt,
3329
- );
3330
- requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
3331
3378
  }
3332
- }
3333
- if (profile) {
3334
- emitSyncProfileDuration(profile, loopStartedAt, {
3335
- name: "simple.queueSync.plan",
3336
- entries: keysToCheck.length,
3337
- count: requestHashes.length,
3338
- targets: 1,
3339
- });
3340
- }
3379
+ if (profile) {
3380
+ emitSyncProfileDuration(profile, loopStartedAt, {
3381
+ name: "simple.queueSync.plan",
3382
+ entries: keysToCheck.length,
3383
+ count: requestHashes.length,
3384
+ targets: 1,
3385
+ });
3386
+ }
3341
3387
 
3342
- // Persistent admission work is complete. Do not let an unrelated
3343
- // blocked transport send retain unused quota.
3344
- this.releasePendingSyncAdmission(admission);
3345
- const dispatchableRequestHashes =
3346
- this.filterDispatchablePendingSyncClaims(
3347
- requestHashes,
3348
- fromHash,
3349
- targetEpoch,
3350
- );
3351
- dispatchableRequestHashes.length > 0 &&
3352
- (await this.requestSync(dispatchableRequestHashes, [fromHash], {
3353
- ownershipLifecycleController,
3354
- targetEpochs: new Map([[fromHash, targetEpoch]]),
3355
- createTargetEpochs: false,
3356
- }));
3357
- await existingRequest;
3358
- } finally {
3359
- this.releasePendingSyncAdmission(admission);
3388
+ // Persistent admission work is complete. Do not let an unrelated
3389
+ // blocked transport send retain unused quota.
3390
+ this.releasePendingSyncAdmission(admission);
3391
+ const dispatchableRequestHashes =
3392
+ this.filterDispatchablePendingSyncClaims(
3393
+ requestHashes,
3394
+ fromHash,
3395
+ targetEpoch,
3396
+ );
3397
+ dispatchableRequestHashes.length > 0 &&
3398
+ (await this.requestSync(dispatchableRequestHashes, [fromHash], {
3399
+ signal: options?.signal,
3400
+ ownershipLifecycleController,
3401
+ targetEpochs: new Map([[fromHash, targetEpoch]]),
3402
+ createTargetEpochs: false,
3403
+ }));
3404
+ } finally {
3405
+ this.releasePendingSyncAdmission(admission);
3406
+ }
3407
+ })();
3408
+ // Admission quota covers the finished lookup only. The eager dispatch
3409
+ // retains its own physical lifecycle, and every receive exit joins it.
3410
+ const results = await Promise.allSettled([planning, existingRequest]);
3411
+ const errors = results.flatMap((result) =>
3412
+ result.status === "rejected" ? [result.reason] : [],
3413
+ );
3414
+ try {
3360
3415
  if (profile) {
3361
3416
  emitSyncProfileDuration(profile, startedAt, {
3362
3417
  name: "simple.queueSync",
@@ -3369,6 +3424,18 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3369
3424
  },
3370
3425
  });
3371
3426
  }
3427
+ } catch (error) {
3428
+ errors.push(error);
3429
+ }
3430
+ if (errors.length === 1) throw errors[0];
3431
+ if (errors.length > 1) {
3432
+ throw new AggregateError(
3433
+ errors,
3434
+ "sync lookup and eager response failed",
3435
+ {
3436
+ cause: errors[0],
3437
+ },
3438
+ );
3372
3439
  }
3373
3440
  }
3374
3441
 
@@ -3376,6 +3443,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3376
3443
  hashes: SyncableKey[],
3377
3444
  to: Set<string> | string[],
3378
3445
  options?: {
3446
+ signal?: AbortSignal;
3379
3447
  ownershipLifecycleController?: AbortController;
3380
3448
  targetEpochs?: Map<string, SyncDispatchTargetEpoch>;
3381
3449
  createTargetEpochs?: boolean;
@@ -3385,11 +3453,15 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3385
3453
  if (hashes.length === 0 || targets.length === 0) {
3386
3454
  return;
3387
3455
  }
3388
- const lifecycle = this.captureSyncDispatchLifecycle(targets, undefined, {
3389
- ownershipLifecycleController: options?.ownershipLifecycleController,
3390
- targetEpochs: options?.targetEpochs,
3391
- createTargetEpochs: options?.createTargetEpochs,
3392
- });
3456
+ const lifecycle = this.captureSyncDispatchLifecycle(
3457
+ targets,
3458
+ options?.signal,
3459
+ {
3460
+ ownershipLifecycleController: options?.ownershipLifecycleController,
3461
+ targetEpochs: options?.targetEpochs,
3462
+ createTargetEpochs: options?.createTargetEpochs,
3463
+ },
3464
+ );
3393
3465
  const profile = this.syncOptions?.profile;
3394
3466
  const startedAt = syncProfileStart(profile);
3395
3467
  let coordinateMessages = 0;
@@ -3453,7 +3525,13 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3453
3525
  );
3454
3526
  coordinateMessages += 1;
3455
3527
  } catch (error) {
3456
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3528
+ if (
3529
+ isSyncDispatchCancellation(
3530
+ error,
3531
+ this.getSyncDispatchSignal(lifecycle, target),
3532
+ !this.isSyncDispatchLifecycleActive(lifecycle, target),
3533
+ )
3534
+ ) {
3457
3535
  break;
3458
3536
  }
3459
3537
  throw error;
@@ -3489,7 +3567,13 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3489
3567
  );
3490
3568
  stringMessages += 1;
3491
3569
  } catch (error) {
3492
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3570
+ if (
3571
+ isSyncDispatchCancellation(
3572
+ error,
3573
+ this.getSyncDispatchSignal(lifecycle, target),
3574
+ !this.isSyncDispatchLifecycleActive(lifecycle, target),
3575
+ )
3576
+ ) {
3493
3577
  break;
3494
3578
  }
3495
3579
  throw error;