@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.
@@ -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);
@@ -776,6 +780,7 @@ export class SimpleSyncronizer {
776
780
  currentEpoch ??
777
781
  (options?.createTargetEpochs === false ||
778
782
  this.closed === true ||
783
+ callerSignal?.aborted === true ||
779
784
  ownershipLifecycleController.signal.aborted
780
785
  ? undefined
781
786
  : this.getOrCreateSyncDispatchTargetEpoch(target));
@@ -2043,8 +2048,8 @@ export class SimpleSyncronizer {
2043
2048
  }
2044
2049
  catch (error) {
2045
2050
  reservation.release();
2046
- if (!this.isSyncDispatchLifecycleActive(lifecycle) ||
2047
- !this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2051
+ if (isSyncDispatchCancellation(error, this.getSyncDispatchSignal(lifecycle, target), !this.isSyncDispatchLifecycleActive(lifecycle) ||
2052
+ !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
2048
2053
  break;
2049
2054
  }
2050
2055
  throw error;
@@ -2093,7 +2098,7 @@ export class SimpleSyncronizer {
2093
2098
  sentMessages = await this.sendRawExchangeHeads(hashes, [to.hashcode()], { signal });
2094
2099
  }
2095
2100
  catch (error) {
2096
- if (signal?.aborted) {
2101
+ if (isSyncDispatchCancellation(error, signal)) {
2097
2102
  return { messages: 0, fused: true };
2098
2103
  }
2099
2104
  throw error;
@@ -2118,7 +2123,7 @@ export class SimpleSyncronizer {
2118
2123
  messages += 1;
2119
2124
  }
2120
2125
  catch (error) {
2121
- if (signal?.aborted) {
2126
+ if (isSyncDispatchCancellation(error, signal)) {
2122
2127
  break;
2123
2128
  }
2124
2129
  throw error;
@@ -2135,7 +2140,7 @@ export class SimpleSyncronizer {
2135
2140
  * exact advertised hash set.
2136
2141
  */
2137
2142
  async shipAuthorizedMaybeSyncResponse(properties) {
2138
- const hashes = this.filterRecentlySentExchangeHeads(properties.hashes, properties.from);
2143
+ const { hashes, rollback } = this.filterRecentlySentExchangeHeads(properties.hashes, properties.from);
2139
2144
  try {
2140
2145
  const shipped = await this.shipExchangeHeads(hashes, properties.from, canReceiveRawExchangeHeads(properties.response), properties.signal);
2141
2146
  return {
@@ -2144,12 +2149,12 @@ export class SimpleSyncronizer {
2144
2149
  };
2145
2150
  }
2146
2151
  catch (error) {
2147
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2152
+ rollback();
2148
2153
  throw error;
2149
2154
  }
2150
2155
  finally {
2151
2156
  if (properties.signal.aborted) {
2152
- this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2157
+ rollback();
2153
2158
  }
2154
2159
  }
2155
2160
  }
@@ -2164,18 +2169,21 @@ export class SimpleSyncronizer {
2164
2169
  let entries = 0;
2165
2170
  let firstError;
2166
2171
  for (const lease of properties.leases) {
2172
+ const signal = properties.signal
2173
+ ? AbortSignal.any([lease.signal, properties.signal])
2174
+ : lease.signal;
2167
2175
  let fulfilled = false;
2168
2176
  try {
2169
2177
  const shipped = await this.shipAuthorizedMaybeSyncResponse({
2170
2178
  hashes: lease.hashes,
2171
2179
  from: properties.from,
2172
2180
  response: properties.response,
2173
- signal: lease.signal,
2181
+ signal,
2174
2182
  });
2175
2183
  messages += shipped.messages;
2176
2184
  fused ||= shipped.fused;
2177
2185
  entries += shipped.entries;
2178
- fulfilled = !lease.signal.aborted;
2186
+ fulfilled = !signal.aborted;
2179
2187
  }
2180
2188
  catch (error) {
2181
2189
  firstError ??= error;
@@ -2201,10 +2209,17 @@ export class SimpleSyncronizer {
2201
2209
  }
2202
2210
  return { messages, fused, entries };
2203
2211
  }
2204
- 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
+ }
2205
2220
  const from = context.from;
2206
2221
  if (msg instanceof RequestMaybeSync) {
2207
- await this.queueSync(msg.hashes, from);
2222
+ await this.queueSync(msg.hashes, from, options);
2208
2223
  return true;
2209
2224
  }
2210
2225
  else if (msg instanceof ResponseMaybeSync ||
@@ -2219,6 +2234,7 @@ export class SimpleSyncronizer {
2219
2234
  leases: pending,
2220
2235
  from,
2221
2236
  response: msg,
2237
+ signal: options?.signal,
2222
2238
  });
2223
2239
  return true;
2224
2240
  }
@@ -2234,7 +2250,7 @@ export class SimpleSyncronizer {
2234
2250
  return true;
2235
2251
  }
2236
2252
  const { release: releaseLookup, row: slotRow } = lookupPermit;
2237
- const lifecycle = this.captureSyncDispatchLifecycle([target]);
2253
+ const lifecycle = this.captureSyncDispatchLifecycle([target], options?.signal);
2238
2254
  let lifecycleFinished = false;
2239
2255
  const finishLifecycle = () => {
2240
2256
  if (lifecycleFinished) {
@@ -2284,12 +2300,22 @@ export class SimpleSyncronizer {
2284
2300
  let hashesToSend = [];
2285
2301
  let messages = 0;
2286
2302
  let fused = false;
2303
+ let rollback;
2304
+ const signal = this.getSyncDispatchSignal(lifecycle, target);
2287
2305
  try {
2288
- hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
2306
+ ({ hashes: hashesToSend, rollback } =
2307
+ this.filterRecentlySentExchangeHeads(hashes, from));
2289
2308
  // dont set priority 1 here because this will block other messages that should higher priority
2290
- ({ 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;
2291
2314
  }
2292
2315
  finally {
2316
+ if (signal.aborted) {
2317
+ rollback?.();
2318
+ }
2293
2319
  releaseResponse();
2294
2320
  if (profile) {
2295
2321
  emitSyncProfileDuration(profile, exchangeStartedAt, {
@@ -2478,7 +2504,7 @@ export class SimpleSyncronizer {
2478
2504
  this.pendingSync.clearPendingSyncExpiryTimerIfIdle();
2479
2505
  }
2480
2506
  async queueSync(keys, from, options) {
2481
- if (this.closed === true || keys.length === 0) {
2507
+ if (this.closed === true || options?.signal?.aborted || keys.length === 0) {
2482
2508
  return;
2483
2509
  }
2484
2510
  // A delayed timer must not let expired claims or admission reservations
@@ -2496,6 +2522,7 @@ export class SimpleSyncronizer {
2496
2522
  const targetEpoch = this.getOrCreateSyncDispatchTargetEpoch(fromHash);
2497
2523
  const ownershipLifecycleController = this.syncDispatchLifecycleController;
2498
2524
  const isCapturedLifecycleActive = () => this.closed !== true &&
2525
+ options?.signal?.aborted !== true &&
2499
2526
  this.syncDispatchLifecycleController === ownershipLifecycleController &&
2500
2527
  !ownershipLifecycleController.signal.aborted &&
2501
2528
  this.syncDispatchTargetEpochs.get(fromHash) === targetEpoch;
@@ -2565,6 +2592,7 @@ export class SimpleSyncronizer {
2565
2592
  const dispatchableExistingRequestHashes = this.filterDispatchablePendingSyncClaims(existingRequestHashes, fromHash, targetEpoch);
2566
2593
  const existingRequest = dispatchableExistingRequestHashes.length > 0
2567
2594
  ? this.requestSync(dispatchableExistingRequestHashes, [fromHash], {
2595
+ signal: options?.signal,
2568
2596
  ownershipLifecycleController,
2569
2597
  targetEpochs: new Map([[fromHash, targetEpoch]]),
2570
2598
  createTargetEpochs: false,
@@ -2575,107 +2603,115 @@ export class SimpleSyncronizer {
2575
2603
  // when a later lifecycle check returns before joining it.
2576
2604
  void existingRequest?.catch(() => { });
2577
2605
  const resolveKnownStartedAt = syncProfileStart(profile);
2578
- try {
2579
- const knownKeys = options?.skipCheck === true || keysToCheck.length === 0
2580
- ? undefined
2581
- : await this.resolveKnownSyncKeys(keysToCheck);
2582
- if (!isCapturedLifecycleActive()) {
2583
- return;
2584
- }
2585
- if (profile) {
2586
- emitSyncProfileDuration(profile, resolveKnownStartedAt, {
2587
- name: "simple.queueSync.resolveKnown",
2588
- entries: keysToCheck.length,
2589
- count: knownKeys?.keys.size ?? 0,
2590
- details: {
2591
- checkedCoordinates: knownKeys?.checkedCoordinates === true,
2592
- checkedHashes: knownKeys?.checkedHashes === true,
2593
- skipCheck: options?.skipCheck === true,
2594
- },
2595
- });
2596
- }
2597
- if (keysToCheck.length > 0) {
2598
- // A resolver/index lookup may have populated coordinateToHash while
2599
- // it yielded. Refresh another fixed-size slice, never the full queue.
2600
- this.refreshQueuedSyncCoordinateAliases();
2601
- }
2602
- const loopStartedAt = syncProfileStart(profile);
2603
- for (let index = 0; index < keysToCheck.length; index += 1) {
2604
- const key = keysToCheck[index];
2605
- 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);
2606
2611
  if (!isCapturedLifecycleActive()) {
2607
2612
  return;
2608
2613
  }
2609
- const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
2610
- if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
2611
- const consumption = this.consumePendingSyncAdmission(admission, identity);
2612
- 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()) {
2613
2636
  return;
2614
2637
  }
2615
- continue;
2616
- }
2617
- const coordinateOrHash = queuedKeyResult ?? key;
2618
- const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
2619
- if (inFlight) {
2620
- if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
2638
+ const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
2639
+ if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
2621
2640
  const consumption = this.consumePendingSyncAdmission(admission, identity);
2622
2641
  if (consumption === "invalid") {
2623
2642
  return;
2624
2643
  }
2625
- 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);
2626
2672
  continue;
2627
2673
  }
2628
- this.movePendingSyncKeyExpiryEarlier(coordinateOrHash, admission.expiresAt);
2629
- const added = this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
2630
- if (added) {
2631
- requestHashes.push(coordinateOrHash);
2674
+ const consumption = this.consumePendingSyncAdmission(admission, identity);
2675
+ if (consumption === "invalid") {
2676
+ return;
2677
+ }
2678
+ if (consumption === "settled") {
2679
+ continue;
2632
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)
2633
2684
  }
2634
2685
  }
2635
- else {
2636
- const has = options?.skipCheck !== true &&
2637
- (await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys));
2638
- if (!isCapturedLifecycleActive()) {
2639
- return;
2640
- }
2641
- if (has) {
2642
- this.clearPendingSyncAdmissionIdentity(identity);
2643
- continue;
2644
- }
2645
- const consumption = this.consumePendingSyncAdmission(admission, identity);
2646
- if (consumption === "invalid") {
2647
- return;
2648
- }
2649
- if (consumption === "settled") {
2650
- continue;
2651
- }
2652
- // Track the initial sender so we can retry if the first request is lost.
2653
- this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
2654
- 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
+ });
2655
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
+ }));
2656
2705
  }
2657
- if (profile) {
2658
- emitSyncProfileDuration(profile, loopStartedAt, {
2659
- name: "simple.queueSync.plan",
2660
- entries: keysToCheck.length,
2661
- count: requestHashes.length,
2662
- targets: 1,
2663
- });
2706
+ finally {
2707
+ this.releasePendingSyncAdmission(admission);
2664
2708
  }
2665
- // Persistent admission work is complete. Do not let an unrelated
2666
- // blocked transport send retain unused quota.
2667
- this.releasePendingSyncAdmission(admission);
2668
- const dispatchableRequestHashes = this.filterDispatchablePendingSyncClaims(requestHashes, fromHash, targetEpoch);
2669
- dispatchableRequestHashes.length > 0 &&
2670
- (await this.requestSync(dispatchableRequestHashes, [fromHash], {
2671
- ownershipLifecycleController,
2672
- targetEpochs: new Map([[fromHash, targetEpoch]]),
2673
- createTargetEpochs: false,
2674
- }));
2675
- await existingRequest;
2676
- }
2677
- finally {
2678
- 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 {
2679
2715
  if (profile) {
2680
2716
  emitSyncProfileDuration(profile, startedAt, {
2681
2717
  name: "simple.queueSync",
@@ -2689,13 +2725,23 @@ export class SimpleSyncronizer {
2689
2725
  });
2690
2726
  }
2691
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
+ }
2692
2738
  }
2693
2739
  async requestSync(hashes, to, options) {
2694
2740
  const targets = [...new Set(to)];
2695
2741
  if (hashes.length === 0 || targets.length === 0) {
2696
2742
  return;
2697
2743
  }
2698
- const lifecycle = this.captureSyncDispatchLifecycle(targets, undefined, {
2744
+ const lifecycle = this.captureSyncDispatchLifecycle(targets, options?.signal, {
2699
2745
  ownershipLifecycleController: options?.ownershipLifecycleController,
2700
2746
  targetEpochs: options?.targetEpochs,
2701
2747
  createTargetEpochs: options?.createTargetEpochs,
@@ -2755,7 +2801,7 @@ export class SimpleSyncronizer {
2755
2801
  coordinateMessages += 1;
2756
2802
  }
2757
2803
  catch (error) {
2758
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2804
+ if (isSyncDispatchCancellation(error, this.getSyncDispatchSignal(lifecycle, target), !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
2759
2805
  break;
2760
2806
  }
2761
2807
  throw error;
@@ -2788,7 +2834,7 @@ export class SimpleSyncronizer {
2788
2834
  stringMessages += 1;
2789
2835
  }
2790
2836
  catch (error) {
2791
- if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2837
+ if (isSyncDispatchCancellation(error, this.getSyncDispatchSignal(lifecycle, target), !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
2792
2838
  break;
2793
2839
  }
2794
2840
  throw error;