@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.
@@ -39,7 +39,7 @@ import { Entry, Log } from "@peerbit/log";
39
39
  import { CONVERGENCE_MESSAGE_PRIORITY, SilentDelivery, } from "@peerbit/stream-interface";
40
40
  import { EntryWithRefs, createExchangeHeadsMessages, createRawExchangeHeadsMessages, } from "../exchange-heads.js";
41
41
  import { TransportMessage } from "../message.js";
42
- import { DispatchLifecycleRegistry } from "./dispatch-lifecycle.js";
42
+ import { DispatchLifecycleRegistry, isSyncDispatchCancellation, } from "./dispatch-lifecycle.js";
43
43
  import { PendingSyncStore, QUEUED_SYNC_ALIAS_REFRESH_PENDING, } from "./pending-sync-store.js";
44
44
  import { emitSyncProfileDuration, syncProfileStart } from "./profile.js";
45
45
  import { SyncPeerSlotRegistry, } from "./sync-peer-state.js";
@@ -702,13 +702,14 @@ export class SimpleSyncronizer {
702
702
  filterRecentlySentExchangeHeads(hashes, peer) {
703
703
  const peerHash = peer.hashcode();
704
704
  const now = Date.now();
705
+ const stamp = { timestamp: now };
705
706
  let recentlySent = this.recentlySentExchangeHeads.get(peerHash);
706
707
  if (!recentlySent) {
707
708
  recentlySent = new Map();
708
709
  this.recentlySentExchangeHeads.set(peerHash, recentlySent);
709
710
  }
710
- for (const [hash, timestamp] of recentlySent) {
711
- if (now - timestamp > EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS) {
711
+ for (const [hash, previous] of recentlySent) {
712
+ if (now - previous.timestamp > EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS) {
712
713
  recentlySent.delete(hash);
713
714
  }
714
715
  }
@@ -725,22 +726,25 @@ export class SimpleSyncronizer {
725
726
  if (this.isEntryRecentlyKnownByPeer?.(hash, peerHash, RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS)) {
726
727
  continue;
727
728
  }
728
- recentlySent.set(hash, now);
729
+ recentlySent.set(hash, stamp);
729
730
  out.push(hash);
730
731
  }
731
- return out;
732
- }
733
- forgetRecentlySentExchangeHeads(hashes, peer) {
734
- const recentlySent = this.recentlySentExchangeHeads.get(peer.hashcode());
735
- if (!recentlySent) {
736
- return;
737
- }
738
- for (const hash of hashes) {
739
- recentlySent.delete(hash);
740
- }
741
- if (recentlySent.size === 0) {
742
- this.recentlySentExchangeHeads.delete(peer.hashcode());
743
- }
732
+ return {
733
+ hashes: out,
734
+ rollback: () => {
735
+ // A cancelled predecessor may settle after a fresh receive shipped
736
+ // the same hash. Only undo this exact attempt's dedupe ownership.
737
+ for (const hash of out) {
738
+ if (recentlySent.get(hash) === stamp) {
739
+ recentlySent.delete(hash);
740
+ }
741
+ }
742
+ if (recentlySent.size === 0 &&
743
+ this.recentlySentExchangeHeads.get(peerHash) === recentlySent) {
744
+ this.recentlySentExchangeHeads.delete(peerHash);
745
+ }
746
+ },
747
+ };
744
748
  }
745
749
  getOrCreateSyncDispatchTargetEpoch(target) {
746
750
  let epoch = this.syncDispatchTargetEpochs.get(target);
@@ -774,7 +778,10 @@ export class SimpleSyncronizer {
774
778
  const currentEpoch = this.syncDispatchTargetEpochs.get(target);
775
779
  const epoch = expectedEpoch ??
776
780
  currentEpoch ??
777
- (options?.createTargetEpochs === false
781
+ (options?.createTargetEpochs === false ||
782
+ this.closed === true ||
783
+ callerSignal?.aborted === true ||
784
+ ownershipLifecycleController.signal.aborted
778
785
  ? undefined
779
786
  : this.getOrCreateSyncDispatchTargetEpoch(target));
780
787
  if (!epoch) {
@@ -1916,7 +1923,11 @@ export class SimpleSyncronizer {
1916
1923
  requestedCount: trackedHashes.length,
1917
1924
  requestedTotalCount: allHashes.length,
1918
1925
  attempts: 0,
1919
- targetEpoch: this.getOrCreateSyncDispatchTargetEpoch(target),
1926
+ // Rejected late repairs only need result metadata. Do not create a
1927
+ // retained dispatch epoch after the terminal admission fence.
1928
+ targetEpoch: this.closed
1929
+ ? { id: 0 }
1930
+ : this.getOrCreateSyncDispatchTargetEpoch(target),
1920
1931
  });
1921
1932
  }
1922
1933
  const session = {
@@ -1930,6 +1941,10 @@ export class SimpleSyncronizer {
1930
1941
  deferred,
1931
1942
  cancelled: false,
1932
1943
  };
1944
+ if (this.closed) {
1945
+ deferred.resolve(this.buildRepairSessionResult(session, false));
1946
+ return { id, done: deferred.promise, cancel: () => { } };
1947
+ }
1933
1948
  if (allHashes.length === 0 || targets.length === 0) {
1934
1949
  deferred.resolve(this.buildRepairSessionResult(session, true));
1935
1950
  return {
@@ -2033,8 +2048,8 @@ export class SimpleSyncronizer {
2033
2048
  }
2034
2049
  catch (error) {
2035
2050
  reservation.release();
2036
- if (!this.isSyncDispatchLifecycleActive(lifecycle) ||
2037
- !this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2051
+ if (isSyncDispatchCancellation(error, this.getSyncDispatchSignal(lifecycle, target), !this.isSyncDispatchLifecycleActive(lifecycle) ||
2052
+ !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
2038
2053
  break;
2039
2054
  }
2040
2055
  throw error;
@@ -2083,7 +2098,7 @@ export class SimpleSyncronizer {
2083
2098
  sentMessages = await this.sendRawExchangeHeads(hashes, [to.hashcode()], { signal });
2084
2099
  }
2085
2100
  catch (error) {
2086
- if (signal?.aborted) {
2101
+ if (isSyncDispatchCancellation(error, signal)) {
2087
2102
  return { messages: 0, fused: true };
2088
2103
  }
2089
2104
  throw error;
@@ -2108,7 +2123,7 @@ export class SimpleSyncronizer {
2108
2123
  messages += 1;
2109
2124
  }
2110
2125
  catch (error) {
2111
- if (signal?.aborted) {
2126
+ if (isSyncDispatchCancellation(error, signal)) {
2112
2127
  break;
2113
2128
  }
2114
2129
  throw error;
@@ -2125,7 +2140,7 @@ export class SimpleSyncronizer {
2125
2140
  * exact advertised hash set.
2126
2141
  */
2127
2142
  async shipAuthorizedMaybeSyncResponse(properties) {
2128
- const hashes = this.filterRecentlySentExchangeHeads(properties.hashes, properties.from);
2143
+ const { hashes, rollback } = this.filterRecentlySentExchangeHeads(properties.hashes, properties.from);
2129
2144
  try {
2130
2145
  const shipped = await this.shipExchangeHeads(hashes, properties.from, canReceiveRawExchangeHeads(properties.response), properties.signal);
2131
2146
  return {
@@ -2134,12 +2149,12 @@ export class SimpleSyncronizer {
2134
2149
  };
2135
2150
  }
2136
2151
  catch (error) {
2137
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2152
+ rollback();
2138
2153
  throw error;
2139
2154
  }
2140
2155
  finally {
2141
2156
  if (properties.signal.aborted) {
2142
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2157
+ rollback();
2143
2158
  }
2144
2159
  }
2145
2160
  }
@@ -2154,18 +2169,21 @@ export class SimpleSyncronizer {
2154
2169
  let entries = 0;
2155
2170
  let firstError;
2156
2171
  for (const lease of properties.leases) {
2172
+ const signal = properties.signal
2173
+ ? AbortSignal.any([lease.signal, properties.signal])
2174
+ : lease.signal;
2157
2175
  let fulfilled = false;
2158
2176
  try {
2159
2177
  const shipped = await this.shipAuthorizedMaybeSyncResponse({
2160
2178
  hashes: lease.hashes,
2161
2179
  from: properties.from,
2162
2180
  response: properties.response,
2163
- signal: lease.signal,
2181
+ signal,
2164
2182
  });
2165
2183
  messages += shipped.messages;
2166
2184
  fused ||= shipped.fused;
2167
2185
  entries += shipped.entries;
2168
- fulfilled = !lease.signal.aborted;
2186
+ fulfilled = !signal.aborted;
2169
2187
  }
2170
2188
  catch (error) {
2171
2189
  firstError ??= error;
@@ -2191,10 +2209,17 @@ export class SimpleSyncronizer {
2191
2209
  }
2192
2210
  return { messages, fused, entries };
2193
2211
  }
2194
- async onMessage(msg, context) {
2212
+ async onMessage(msg, context, options) {
2213
+ if (options?.signal?.aborted) {
2214
+ return (msg instanceof RequestMaybeSync ||
2215
+ msg instanceof ResponseMaybeSync ||
2216
+ msg instanceof ResponseMaybeSyncCapabilities ||
2217
+ msg instanceof RequestMaybeSyncCoordinate ||
2218
+ msg instanceof RequestMaybeSyncCoordinateCapabilities);
2219
+ }
2195
2220
  const from = context.from;
2196
2221
  if (msg instanceof RequestMaybeSync) {
2197
- await this.queueSync(msg.hashes, from);
2222
+ await this.queueSync(msg.hashes, from, options);
2198
2223
  return true;
2199
2224
  }
2200
2225
  else if (msg instanceof ResponseMaybeSync ||
@@ -2209,6 +2234,7 @@ export class SimpleSyncronizer {
2209
2234
  leases: pending,
2210
2235
  from,
2211
2236
  response: msg,
2237
+ signal: options?.signal,
2212
2238
  });
2213
2239
  return true;
2214
2240
  }
@@ -2224,7 +2250,7 @@ export class SimpleSyncronizer {
2224
2250
  return true;
2225
2251
  }
2226
2252
  const { release: releaseLookup, row: slotRow } = lookupPermit;
2227
- const lifecycle = this.captureSyncDispatchLifecycle([target]);
2253
+ const lifecycle = this.captureSyncDispatchLifecycle([target], options?.signal);
2228
2254
  let lifecycleFinished = false;
2229
2255
  const finishLifecycle = () => {
2230
2256
  if (lifecycleFinished) {
@@ -2274,12 +2300,22 @@ export class SimpleSyncronizer {
2274
2300
  let hashesToSend = [];
2275
2301
  let messages = 0;
2276
2302
  let fused = false;
2303
+ let rollback;
2304
+ const signal = this.getSyncDispatchSignal(lifecycle, target);
2277
2305
  try {
2278
- hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
2306
+ ({ hashes: hashesToSend, rollback } =
2307
+ this.filterRecentlySentExchangeHeads(hashes, from));
2279
2308
  // dont set priority 1 here because this will block other messages that should higher priority
2280
- ({ messages, fused } = await this.shipExchangeHeads(hashesToSend, context.from, canReceiveRawExchangeHeads(msg), this.getSyncDispatchSignal(lifecycle, target)));
2309
+ ({ messages, fused } = await this.shipExchangeHeads(hashesToSend, context.from, canReceiveRawExchangeHeads(msg), signal));
2310
+ }
2311
+ catch (error) {
2312
+ rollback?.();
2313
+ throw error;
2281
2314
  }
2282
2315
  finally {
2316
+ if (signal.aborted) {
2317
+ rollback?.();
2318
+ }
2283
2319
  releaseResponse();
2284
2320
  if (profile) {
2285
2321
  emitSyncProfileDuration(profile, exchangeStartedAt, {
@@ -2468,7 +2504,7 @@ export class SimpleSyncronizer {
2468
2504
  this.pendingSync.clearPendingSyncExpiryTimerIfIdle();
2469
2505
  }
2470
2506
  async queueSync(keys, from, options) {
2471
- if (this.closed === true || keys.length === 0) {
2507
+ if (this.closed === true || options?.signal?.aborted || keys.length === 0) {
2472
2508
  return;
2473
2509
  }
2474
2510
  // A delayed timer must not let expired claims or admission reservations
@@ -2486,6 +2522,7 @@ export class SimpleSyncronizer {
2486
2522
  const targetEpoch = this.getOrCreateSyncDispatchTargetEpoch(fromHash);
2487
2523
  const ownershipLifecycleController = this.syncDispatchLifecycleController;
2488
2524
  const isCapturedLifecycleActive = () => this.closed !== true &&
2525
+ options?.signal?.aborted !== true &&
2489
2526
  this.syncDispatchLifecycleController === ownershipLifecycleController &&
2490
2527
  !ownershipLifecycleController.signal.aborted &&
2491
2528
  this.syncDispatchTargetEpochs.get(fromHash) === targetEpoch;
@@ -2555,6 +2592,7 @@ export class SimpleSyncronizer {
2555
2592
  const dispatchableExistingRequestHashes = this.filterDispatchablePendingSyncClaims(existingRequestHashes, fromHash, targetEpoch);
2556
2593
  const existingRequest = dispatchableExistingRequestHashes.length > 0
2557
2594
  ? this.requestSync(dispatchableExistingRequestHashes, [fromHash], {
2595
+ signal: options?.signal,
2558
2596
  ownershipLifecycleController,
2559
2597
  targetEpochs: new Map([[fromHash, targetEpoch]]),
2560
2598
  createTargetEpochs: false,
@@ -2565,107 +2603,115 @@ export class SimpleSyncronizer {
2565
2603
  // when a later lifecycle check returns before joining it.
2566
2604
  void existingRequest?.catch(() => { });
2567
2605
  const resolveKnownStartedAt = syncProfileStart(profile);
2568
- try {
2569
- const knownKeys = options?.skipCheck === true || keysToCheck.length === 0
2570
- ? undefined
2571
- : await this.resolveKnownSyncKeys(keysToCheck);
2572
- if (!isCapturedLifecycleActive()) {
2573
- return;
2574
- }
2575
- if (profile) {
2576
- emitSyncProfileDuration(profile, resolveKnownStartedAt, {
2577
- name: "simple.queueSync.resolveKnown",
2578
- entries: keysToCheck.length,
2579
- count: knownKeys?.keys.size ?? 0,
2580
- details: {
2581
- checkedCoordinates: knownKeys?.checkedCoordinates === true,
2582
- checkedHashes: knownKeys?.checkedHashes === true,
2583
- skipCheck: options?.skipCheck === true,
2584
- },
2585
- });
2586
- }
2587
- if (keysToCheck.length > 0) {
2588
- // A resolver/index lookup may have populated coordinateToHash while
2589
- // it yielded. Refresh another fixed-size slice, never the full queue.
2590
- this.refreshQueuedSyncCoordinateAliases();
2591
- }
2592
- const loopStartedAt = syncProfileStart(profile);
2593
- for (let index = 0; index < keysToCheck.length; index += 1) {
2594
- const key = keysToCheck[index];
2595
- const identity = identitiesToCheck[index];
2606
+ const planning = (async () => {
2607
+ try {
2608
+ const knownKeys = options?.skipCheck === true || keysToCheck.length === 0
2609
+ ? undefined
2610
+ : await this.resolveKnownSyncKeys(keysToCheck);
2596
2611
  if (!isCapturedLifecycleActive()) {
2597
2612
  return;
2598
2613
  }
2599
- const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
2600
- if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
2601
- const consumption = this.consumePendingSyncAdmission(admission, identity);
2602
- if (consumption === "invalid") {
2614
+ if (profile) {
2615
+ emitSyncProfileDuration(profile, resolveKnownStartedAt, {
2616
+ name: "simple.queueSync.resolveKnown",
2617
+ entries: keysToCheck.length,
2618
+ count: knownKeys?.keys.size ?? 0,
2619
+ details: {
2620
+ checkedCoordinates: knownKeys?.checkedCoordinates === true,
2621
+ checkedHashes: knownKeys?.checkedHashes === true,
2622
+ skipCheck: options?.skipCheck === true,
2623
+ },
2624
+ });
2625
+ }
2626
+ if (keysToCheck.length > 0) {
2627
+ // A resolver/index lookup may have populated coordinateToHash while
2628
+ // it yielded. Refresh another fixed-size slice, never the full queue.
2629
+ this.refreshQueuedSyncCoordinateAliases();
2630
+ }
2631
+ const loopStartedAt = syncProfileStart(profile);
2632
+ for (let index = 0; index < keysToCheck.length; index += 1) {
2633
+ const key = keysToCheck[index];
2634
+ const identity = identitiesToCheck[index];
2635
+ if (!isCapturedLifecycleActive()) {
2603
2636
  return;
2604
2637
  }
2605
- continue;
2606
- }
2607
- const coordinateOrHash = queuedKeyResult ?? key;
2608
- const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
2609
- if (inFlight) {
2610
- if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
2638
+ const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
2639
+ if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
2611
2640
  const consumption = this.consumePendingSyncAdmission(admission, identity);
2612
2641
  if (consumption === "invalid") {
2613
2642
  return;
2614
2643
  }
2615
- if (consumption === "settled") {
2644
+ continue;
2645
+ }
2646
+ const coordinateOrHash = queuedKeyResult ?? key;
2647
+ const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
2648
+ if (inFlight) {
2649
+ if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
2650
+ const consumption = this.consumePendingSyncAdmission(admission, identity);
2651
+ if (consumption === "invalid") {
2652
+ return;
2653
+ }
2654
+ if (consumption === "settled") {
2655
+ continue;
2656
+ }
2657
+ this.movePendingSyncKeyExpiryEarlier(coordinateOrHash, admission.expiresAt);
2658
+ const added = this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
2659
+ if (added) {
2660
+ requestHashes.push(coordinateOrHash);
2661
+ }
2662
+ }
2663
+ }
2664
+ else {
2665
+ const has = options?.skipCheck !== true &&
2666
+ (await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys));
2667
+ if (!isCapturedLifecycleActive()) {
2668
+ return;
2669
+ }
2670
+ if (has) {
2671
+ this.clearPendingSyncAdmissionIdentity(identity);
2616
2672
  continue;
2617
2673
  }
2618
- this.movePendingSyncKeyExpiryEarlier(coordinateOrHash, admission.expiresAt);
2619
- const added = this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
2620
- if (added) {
2621
- requestHashes.push(coordinateOrHash);
2674
+ const consumption = this.consumePendingSyncAdmission(admission, identity);
2675
+ if (consumption === "invalid") {
2676
+ return;
2622
2677
  }
2678
+ if (consumption === "settled") {
2679
+ continue;
2680
+ }
2681
+ // Track the initial sender so we can retry if the first request is lost.
2682
+ this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
2683
+ requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
2623
2684
  }
2624
2685
  }
2625
- else {
2626
- const has = options?.skipCheck !== true &&
2627
- (await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys));
2628
- if (!isCapturedLifecycleActive()) {
2629
- return;
2630
- }
2631
- if (has) {
2632
- this.clearPendingSyncAdmissionIdentity(identity);
2633
- continue;
2634
- }
2635
- const consumption = this.consumePendingSyncAdmission(admission, identity);
2636
- if (consumption === "invalid") {
2637
- return;
2638
- }
2639
- if (consumption === "settled") {
2640
- continue;
2641
- }
2642
- // Track the initial sender so we can retry if the first request is lost.
2643
- this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
2644
- requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
2686
+ if (profile) {
2687
+ emitSyncProfileDuration(profile, loopStartedAt, {
2688
+ name: "simple.queueSync.plan",
2689
+ entries: keysToCheck.length,
2690
+ count: requestHashes.length,
2691
+ targets: 1,
2692
+ });
2645
2693
  }
2694
+ // Persistent admission work is complete. Do not let an unrelated
2695
+ // blocked transport send retain unused quota.
2696
+ this.releasePendingSyncAdmission(admission);
2697
+ const dispatchableRequestHashes = this.filterDispatchablePendingSyncClaims(requestHashes, fromHash, targetEpoch);
2698
+ dispatchableRequestHashes.length > 0 &&
2699
+ (await this.requestSync(dispatchableRequestHashes, [fromHash], {
2700
+ signal: options?.signal,
2701
+ ownershipLifecycleController,
2702
+ targetEpochs: new Map([[fromHash, targetEpoch]]),
2703
+ createTargetEpochs: false,
2704
+ }));
2646
2705
  }
2647
- if (profile) {
2648
- emitSyncProfileDuration(profile, loopStartedAt, {
2649
- name: "simple.queueSync.plan",
2650
- entries: keysToCheck.length,
2651
- count: requestHashes.length,
2652
- targets: 1,
2653
- });
2706
+ finally {
2707
+ this.releasePendingSyncAdmission(admission);
2654
2708
  }
2655
- // Persistent admission work is complete. Do not let an unrelated
2656
- // blocked transport send retain unused quota.
2657
- this.releasePendingSyncAdmission(admission);
2658
- const dispatchableRequestHashes = this.filterDispatchablePendingSyncClaims(requestHashes, fromHash, targetEpoch);
2659
- dispatchableRequestHashes.length > 0 &&
2660
- (await this.requestSync(dispatchableRequestHashes, [fromHash], {
2661
- ownershipLifecycleController,
2662
- targetEpochs: new Map([[fromHash, targetEpoch]]),
2663
- createTargetEpochs: false,
2664
- }));
2665
- await existingRequest;
2666
- }
2667
- finally {
2668
- this.releasePendingSyncAdmission(admission);
2709
+ })();
2710
+ // Admission quota covers the finished lookup only. The eager dispatch
2711
+ // retains its own physical lifecycle, and every receive exit joins it.
2712
+ const results = await Promise.allSettled([planning, existingRequest]);
2713
+ const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
2714
+ try {
2669
2715
  if (profile) {
2670
2716
  emitSyncProfileDuration(profile, startedAt, {
2671
2717
  name: "simple.queueSync",
@@ -2679,13 +2725,23 @@ export class SimpleSyncronizer {
2679
2725
  });
2680
2726
  }
2681
2727
  }
2728
+ catch (error) {
2729
+ errors.push(error);
2730
+ }
2731
+ if (errors.length === 1)
2732
+ throw errors[0];
2733
+ if (errors.length > 1) {
2734
+ throw new AggregateError(errors, "sync lookup and eager response failed", {
2735
+ cause: errors[0],
2736
+ });
2737
+ }
2682
2738
  }
2683
2739
  async requestSync(hashes, to, options) {
2684
2740
  const targets = [...new Set(to)];
2685
2741
  if (hashes.length === 0 || targets.length === 0) {
2686
2742
  return;
2687
2743
  }
2688
- const lifecycle = this.captureSyncDispatchLifecycle(targets, undefined, {
2744
+ const lifecycle = this.captureSyncDispatchLifecycle(targets, options?.signal, {
2689
2745
  ownershipLifecycleController: options?.ownershipLifecycleController,
2690
2746
  targetEpochs: options?.targetEpochs,
2691
2747
  createTargetEpochs: options?.createTargetEpochs,
@@ -2745,7 +2801,7 @@ export class SimpleSyncronizer {
2745
2801
  coordinateMessages += 1;
2746
2802
  }
2747
2803
  catch (error) {
2748
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2804
+ if (isSyncDispatchCancellation(error, this.getSyncDispatchSignal(lifecycle, target), !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
2749
2805
  break;
2750
2806
  }
2751
2807
  throw error;
@@ -2778,7 +2834,7 @@ export class SimpleSyncronizer {
2778
2834
  stringMessages += 1;
2779
2835
  }
2780
2836
  catch (error) {
2781
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2837
+ if (isSyncDispatchCancellation(error, this.getSyncDispatchSignal(lifecycle, target), !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
2782
2838
  break;
2783
2839
  }
2784
2840
  throw error;
@@ -3026,9 +3082,12 @@ export class SimpleSyncronizer {
3026
3082
  // Defensive observation for failures outside the loop's best-effort body.
3027
3083
  });
3028
3084
  }
3029
- async close() {
3085
+ beginClose() {
3030
3086
  this.closed = true;
3031
3087
  this.syncDispatchLifecycleController.abort();
3088
+ }
3089
+ async close() {
3090
+ this.beginClose();
3032
3091
  this.syncDispatchTargetEpochs.clear();
3033
3092
  this.clearPendingSyncAdmissions();
3034
3093
  this.syncInFlightRetryIterator = undefined;