@peerbit/shared-log 16.0.30 → 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(
@@ -879,7 +887,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
879
887
  const epoch =
880
888
  expectedEpoch ??
881
889
  currentEpoch ??
882
- (options?.createTargetEpochs === false
890
+ (options?.createTargetEpochs === false ||
891
+ this.closed === true ||
892
+ callerSignal?.aborted === true ||
893
+ ownershipLifecycleController.signal.aborted
883
894
  ? undefined
884
895
  : this.getOrCreateSyncDispatchTargetEpoch(target));
885
896
  if (!epoch) {
@@ -2362,7 +2373,11 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2362
2373
  requestedCount: trackedHashes.length,
2363
2374
  requestedTotalCount: allHashes.length,
2364
2375
  attempts: 0,
2365
- targetEpoch: this.getOrCreateSyncDispatchTargetEpoch(target),
2376
+ // Rejected late repairs only need result metadata. Do not create a
2377
+ // retained dispatch epoch after the terminal admission fence.
2378
+ targetEpoch: this.closed
2379
+ ? { id: 0 }
2380
+ : this.getOrCreateSyncDispatchTargetEpoch(target),
2366
2381
  });
2367
2382
  }
2368
2383
 
@@ -2377,6 +2392,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2377
2392
  deferred,
2378
2393
  cancelled: false,
2379
2394
  };
2395
+ if (this.closed) {
2396
+ deferred.resolve(this.buildRepairSessionResult(session, false));
2397
+ return { id, done: deferred.promise, cancel: () => {} };
2398
+ }
2380
2399
 
2381
2400
  if (allHashes.length === 0 || targets.length === 0) {
2382
2401
  deferred.resolve(this.buildRepairSessionResult(session, true));
@@ -2520,8 +2539,12 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2520
2539
  } catch (error) {
2521
2540
  reservation.release();
2522
2541
  if (
2523
- !this.isSyncDispatchLifecycleActive(lifecycle) ||
2524
- !this.isSyncDispatchLifecycleActive(lifecycle, target)
2542
+ isSyncDispatchCancellation(
2543
+ error,
2544
+ this.getSyncDispatchSignal(lifecycle, target),
2545
+ !this.isSyncDispatchLifecycleActive(lifecycle) ||
2546
+ !this.isSyncDispatchLifecycleActive(lifecycle, target),
2547
+ )
2525
2548
  ) {
2526
2549
  break;
2527
2550
  }
@@ -2583,7 +2606,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2583
2606
  { signal },
2584
2607
  );
2585
2608
  } catch (error) {
2586
- if (signal?.aborted) {
2609
+ if (isSyncDispatchCancellation(error, signal)) {
2587
2610
  return { messages: 0, fused: true };
2588
2611
  }
2589
2612
  throw error;
@@ -2611,7 +2634,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2611
2634
  });
2612
2635
  messages += 1;
2613
2636
  } catch (error) {
2614
- if (signal?.aborted) {
2637
+ if (isSyncDispatchCancellation(error, signal)) {
2615
2638
  break;
2616
2639
  }
2617
2640
  throw error;
@@ -2634,7 +2657,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2634
2657
  response: ResponseMaybeSync | ResponseMaybeSyncCapabilities;
2635
2658
  signal: AbortSignal;
2636
2659
  }): Promise<{ messages: number; fused: boolean; entries: number }> {
2637
- const hashes = this.filterRecentlySentExchangeHeads(
2660
+ const { hashes, rollback } = this.filterRecentlySentExchangeHeads(
2638
2661
  properties.hashes,
2639
2662
  properties.from,
2640
2663
  );
@@ -2650,11 +2673,11 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2650
2673
  entries: hashes.length,
2651
2674
  };
2652
2675
  } catch (error) {
2653
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2676
+ rollback();
2654
2677
  throw error;
2655
2678
  } finally {
2656
2679
  if (properties.signal.aborted) {
2657
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2680
+ rollback();
2658
2681
  }
2659
2682
  }
2660
2683
  }
@@ -2663,6 +2686,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2663
2686
  leases: AuthorizedMaybeSyncResponseLease[];
2664
2687
  from: PublicSignKey;
2665
2688
  response: ResponseMaybeSync | ResponseMaybeSyncCapabilities;
2689
+ signal?: AbortSignal;
2666
2690
  source?: string;
2667
2691
  }): Promise<{ messages: number; fused: boolean; entries: number }> {
2668
2692
  if (properties.leases.length === 0) {
@@ -2675,18 +2699,21 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2675
2699
  let entries = 0;
2676
2700
  let firstError: unknown;
2677
2701
  for (const lease of properties.leases) {
2702
+ const signal = properties.signal
2703
+ ? AbortSignal.any([lease.signal, properties.signal])
2704
+ : lease.signal;
2678
2705
  let fulfilled = false;
2679
2706
  try {
2680
2707
  const shipped = await this.shipAuthorizedMaybeSyncResponse({
2681
2708
  hashes: lease.hashes,
2682
2709
  from: properties.from,
2683
2710
  response: properties.response,
2684
- signal: lease.signal,
2711
+ signal,
2685
2712
  });
2686
2713
  messages += shipped.messages;
2687
2714
  fused ||= shipped.fused;
2688
2715
  entries += shipped.entries;
2689
- fulfilled = !lease.signal.aborted;
2716
+ fulfilled = !signal.aborted;
2690
2717
  } catch (error) {
2691
2718
  firstError ??= error;
2692
2719
  } finally {
@@ -2714,10 +2741,20 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2714
2741
  async onMessage(
2715
2742
  msg: TransportMessage,
2716
2743
  context: RequestContext,
2744
+ options?: { signal?: AbortSignal },
2717
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
+ }
2718
2755
  const from = context.from!;
2719
2756
  if (msg instanceof RequestMaybeSync) {
2720
- await this.queueSync(msg.hashes, from);
2757
+ await this.queueSync(msg.hashes, from, options);
2721
2758
  return true;
2722
2759
  } else if (
2723
2760
  msg instanceof ResponseMaybeSync ||
@@ -2734,6 +2771,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2734
2771
  leases: pending,
2735
2772
  from,
2736
2773
  response: msg,
2774
+ signal: options?.signal,
2737
2775
  });
2738
2776
  return true;
2739
2777
  } else if (
@@ -2752,7 +2790,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2752
2790
  return true;
2753
2791
  }
2754
2792
  const { release: releaseLookup, row: slotRow } = lookupPermit;
2755
- const lifecycle = this.captureSyncDispatchLifecycle([target]);
2793
+ const lifecycle = this.captureSyncDispatchLifecycle(
2794
+ [target],
2795
+ options?.signal,
2796
+ );
2756
2797
  let lifecycleFinished = false;
2757
2798
  const finishLifecycle = () => {
2758
2799
  if (lifecycleFinished) {
@@ -2810,16 +2851,25 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2810
2851
  let hashesToSend: string[] = [];
2811
2852
  let messages = 0;
2812
2853
  let fused = false;
2854
+ let rollback: (() => void) | undefined;
2855
+ const signal = this.getSyncDispatchSignal(lifecycle, target);
2813
2856
  try {
2814
- hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
2857
+ ({ hashes: hashesToSend, rollback } =
2858
+ this.filterRecentlySentExchangeHeads(hashes, from));
2815
2859
  // dont set priority 1 here because this will block other messages that should higher priority
2816
2860
  ({ messages, fused } = await this.shipExchangeHeads(
2817
2861
  hashesToSend,
2818
2862
  context.from!,
2819
2863
  canReceiveRawExchangeHeads(msg),
2820
- this.getSyncDispatchSignal(lifecycle, target),
2864
+ signal,
2821
2865
  ));
2866
+ } catch (error) {
2867
+ rollback?.();
2868
+ throw error;
2822
2869
  } finally {
2870
+ if (signal.aborted) {
2871
+ rollback?.();
2872
+ }
2823
2873
  releaseResponse();
2824
2874
  if (profile) {
2825
2875
  emitSyncProfileDuration(profile, exchangeStartedAt, {
@@ -3085,9 +3135,9 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3085
3135
  async queueSync(
3086
3136
  keys: SyncableKey[],
3087
3137
  from: PublicSignKey,
3088
- options?: { skipCheck?: boolean },
3138
+ options?: { skipCheck?: boolean; signal?: AbortSignal },
3089
3139
  ) {
3090
- if (this.closed === true || keys.length === 0) {
3140
+ if (this.closed === true || options?.signal?.aborted || keys.length === 0) {
3091
3141
  return;
3092
3142
  }
3093
3143
  // A delayed timer must not let expired claims or admission reservations
@@ -3114,6 +3164,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3114
3164
  const ownershipLifecycleController = this.syncDispatchLifecycleController;
3115
3165
  const isCapturedLifecycleActive = () =>
3116
3166
  this.closed !== true &&
3167
+ options?.signal?.aborted !== true &&
3117
3168
  this.syncDispatchLifecycleController === ownershipLifecycleController &&
3118
3169
  !ownershipLifecycleController.signal.aborted &&
3119
3170
  this.syncDispatchTargetEpochs.get(fromHash) === targetEpoch;
@@ -3209,6 +3260,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3209
3260
  const existingRequest =
3210
3261
  dispatchableExistingRequestHashes.length > 0
3211
3262
  ? this.requestSync(dispatchableExistingRequestHashes, [fromHash], {
3263
+ signal: options?.signal,
3212
3264
  ownershipLifecycleController,
3213
3265
  targetEpochs: new Map([[fromHash, targetEpoch]]),
3214
3266
  createTargetEpochs: false,
@@ -3219,54 +3271,42 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3219
3271
  // when a later lifecycle check returns before joining it.
3220
3272
  void existingRequest?.catch(() => {});
3221
3273
  const resolveKnownStartedAt = syncProfileStart(profile);
3222
- try {
3223
- const knownKeys =
3224
- options?.skipCheck === true || keysToCheck.length === 0
3225
- ? undefined
3226
- : await this.resolveKnownSyncKeys(keysToCheck);
3227
- if (!isCapturedLifecycleActive()) {
3228
- return;
3229
- }
3230
- if (profile) {
3231
- emitSyncProfileDuration(profile, resolveKnownStartedAt, {
3232
- name: "simple.queueSync.resolveKnown",
3233
- entries: keysToCheck.length,
3234
- count: knownKeys?.keys.size ?? 0,
3235
- details: {
3236
- checkedCoordinates: knownKeys?.checkedCoordinates === true,
3237
- checkedHashes: knownKeys?.checkedHashes === true,
3238
- skipCheck: options?.skipCheck === true,
3239
- },
3240
- });
3241
- }
3242
-
3243
- if (keysToCheck.length > 0) {
3244
- // A resolver/index lookup may have populated coordinateToHash while
3245
- // it yielded. Refresh another fixed-size slice, never the full queue.
3246
- this.refreshQueuedSyncCoordinateAliases();
3247
- }
3248
- const loopStartedAt = syncProfileStart(profile);
3249
- for (let index = 0; index < keysToCheck.length; index += 1) {
3250
- const key = keysToCheck[index]!;
3251
- 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);
3252
3280
  if (!isCapturedLifecycleActive()) {
3253
3281
  return;
3254
3282
  }
3255
- const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
3256
- if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
3257
- const consumption = this.consumePendingSyncAdmission(
3258
- admission!,
3259
- identity,
3260
- );
3261
- 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()) {
3262
3306
  return;
3263
3307
  }
3264
- continue;
3265
- }
3266
- const coordinateOrHash = queuedKeyResult ?? key;
3267
- const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
3268
- if (inFlight) {
3269
- if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
3308
+ const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
3309
+ if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
3270
3310
  const consumption = this.consumePendingSyncAdmission(
3271
3311
  admission!,
3272
3312
  identity,
@@ -3274,79 +3314,104 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3274
3314
  if (consumption === "invalid") {
3275
3315
  return;
3276
3316
  }
3277
- 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);
3278
3358
  continue;
3279
3359
  }
3280
- this.movePendingSyncKeyExpiryEarlier(
3281
- coordinateOrHash,
3282
- admission!.expiresAt,
3360
+ const consumption = this.consumePendingSyncAdmission(
3361
+ admission!,
3362
+ identity,
3283
3363
  );
3284
- 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(
3285
3372
  coordinateOrHash,
3286
3373
  from,
3287
3374
  admission!.expiresAt,
3288
3375
  );
3289
- if (added) {
3290
- requestHashes.push(coordinateOrHash);
3291
- }
3376
+ requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
3292
3377
  }
3293
- } else {
3294
- const has =
3295
- options?.skipCheck !== true &&
3296
- (await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys));
3297
- if (!isCapturedLifecycleActive()) {
3298
- return;
3299
- }
3300
- if (has) {
3301
- this.clearPendingSyncAdmissionIdentity(identity);
3302
- continue;
3303
- }
3304
- const consumption = this.consumePendingSyncAdmission(
3305
- admission!,
3306
- identity,
3307
- );
3308
- if (consumption === "invalid") {
3309
- return;
3310
- }
3311
- if (consumption === "settled") {
3312
- continue;
3313
- }
3314
- // Track the initial sender so we can retry if the first request is lost.
3315
- this.addPendingSyncClaim(
3316
- coordinateOrHash,
3317
- from,
3318
- admission!.expiresAt,
3319
- );
3320
- requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
3321
3378
  }
3322
- }
3323
- if (profile) {
3324
- emitSyncProfileDuration(profile, loopStartedAt, {
3325
- name: "simple.queueSync.plan",
3326
- entries: keysToCheck.length,
3327
- count: requestHashes.length,
3328
- targets: 1,
3329
- });
3330
- }
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
+ }
3331
3387
 
3332
- // Persistent admission work is complete. Do not let an unrelated
3333
- // blocked transport send retain unused quota.
3334
- this.releasePendingSyncAdmission(admission);
3335
- const dispatchableRequestHashes =
3336
- this.filterDispatchablePendingSyncClaims(
3337
- requestHashes,
3338
- fromHash,
3339
- targetEpoch,
3340
- );
3341
- dispatchableRequestHashes.length > 0 &&
3342
- (await this.requestSync(dispatchableRequestHashes, [fromHash], {
3343
- ownershipLifecycleController,
3344
- targetEpochs: new Map([[fromHash, targetEpoch]]),
3345
- createTargetEpochs: false,
3346
- }));
3347
- await existingRequest;
3348
- } finally {
3349
- 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 {
3350
3415
  if (profile) {
3351
3416
  emitSyncProfileDuration(profile, startedAt, {
3352
3417
  name: "simple.queueSync",
@@ -3359,6 +3424,18 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3359
3424
  },
3360
3425
  });
3361
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
+ );
3362
3439
  }
3363
3440
  }
3364
3441
 
@@ -3366,6 +3443,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3366
3443
  hashes: SyncableKey[],
3367
3444
  to: Set<string> | string[],
3368
3445
  options?: {
3446
+ signal?: AbortSignal;
3369
3447
  ownershipLifecycleController?: AbortController;
3370
3448
  targetEpochs?: Map<string, SyncDispatchTargetEpoch>;
3371
3449
  createTargetEpochs?: boolean;
@@ -3375,11 +3453,15 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3375
3453
  if (hashes.length === 0 || targets.length === 0) {
3376
3454
  return;
3377
3455
  }
3378
- const lifecycle = this.captureSyncDispatchLifecycle(targets, undefined, {
3379
- ownershipLifecycleController: options?.ownershipLifecycleController,
3380
- targetEpochs: options?.targetEpochs,
3381
- createTargetEpochs: options?.createTargetEpochs,
3382
- });
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
+ );
3383
3465
  const profile = this.syncOptions?.profile;
3384
3466
  const startedAt = syncProfileStart(profile);
3385
3467
  let coordinateMessages = 0;
@@ -3443,7 +3525,13 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3443
3525
  );
3444
3526
  coordinateMessages += 1;
3445
3527
  } catch (error) {
3446
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3528
+ if (
3529
+ isSyncDispatchCancellation(
3530
+ error,
3531
+ this.getSyncDispatchSignal(lifecycle, target),
3532
+ !this.isSyncDispatchLifecycleActive(lifecycle, target),
3533
+ )
3534
+ ) {
3447
3535
  break;
3448
3536
  }
3449
3537
  throw error;
@@ -3479,7 +3567,13 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3479
3567
  );
3480
3568
  stringMessages += 1;
3481
3569
  } catch (error) {
3482
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3570
+ if (
3571
+ isSyncDispatchCancellation(
3572
+ error,
3573
+ this.getSyncDispatchSignal(lifecycle, target),
3574
+ !this.isSyncDispatchLifecycleActive(lifecycle, target),
3575
+ )
3576
+ ) {
3483
3577
  break;
3484
3578
  }
3485
3579
  throw error;
@@ -3755,9 +3849,13 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3755
3849
  });
3756
3850
  }
3757
3851
 
3758
- async close() {
3852
+ beginClose(): void {
3759
3853
  this.closed = true;
3760
3854
  this.syncDispatchLifecycleController.abort();
3855
+ }
3856
+
3857
+ async close() {
3858
+ this.beginClose();
3761
3859
  this.syncDispatchTargetEpochs.clear();
3762
3860
  this.clearPendingSyncAdmissions();
3763
3861
  this.syncInFlightRetryIterator = undefined;