@rotorsoft/act 1.32.27 → 1.32.28

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.
package/dist/index.js CHANGED
@@ -1132,6 +1132,10 @@ var CircuitBreaker = class {
1132
1132
  get failing() {
1133
1133
  return this._failures > 0;
1134
1134
  }
1135
+ /** How long the store is left alone after a failure before a retry. */
1136
+ get cooldown_ms() {
1137
+ return this._cooldown_ms;
1138
+ }
1135
1139
  /** Current state given the wall-clock `now`. */
1136
1140
  state(now) {
1137
1141
  if (this._opened_at === void 0) return "closed";
@@ -2682,20 +2686,24 @@ function fetch_failure_policy(registry) {
2682
2686
  }
2683
2687
  return { blockOnError, maxRetries };
2684
2688
  }
2685
- function resolver_failed(lease, { error, options }) {
2686
- log().error(error);
2687
- const block2 = options.blockOnError && lease.retry >= options.maxRetries;
2689
+ function finalize(lease, handled, at, error, options, logger, failed_at) {
2690
+ if (!error) return { lease, handled, acked_at: at };
2691
+ logger.error(error);
2692
+ const non_retryable = error instanceof NonRetryableError;
2693
+ const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
2694
+ if (block2)
2695
+ logger.error(
2696
+ non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
2697
+ );
2698
+ const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
2688
2699
  return {
2689
2700
  lease,
2690
- handled: 0,
2691
- acked_at: lease.at,
2692
- error,
2693
- ...block2 ? { block: true } : {
2694
- next_attempt_at: Date.now() + compute_backoff_delay(
2695
- lease.retry,
2696
- options.backoff ?? DEFAULT_BACKOFF
2697
- )
2698
- }
2701
+ handled,
2702
+ acked_at: at,
2703
+ error: error.message,
2704
+ block: block2,
2705
+ next_attempt_at,
2706
+ failed_at
2699
2707
  };
2700
2708
  }
2701
2709
  function warn_misrouted(seen, stream, handler, event) {
@@ -2743,7 +2751,9 @@ async function run_drain_cycle(deps, cycle) {
2743
2751
  resolved = dynamic ? resolver(event) : resolver;
2744
2752
  } catch (error) {
2745
2753
  failed ??= {
2746
- error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
2754
+ error: new Error(
2755
+ `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`
2756
+ ),
2747
2757
  options: reaction.options
2748
2758
  };
2749
2759
  return false;
@@ -2765,23 +2775,29 @@ async function run_drain_cycle(deps, cycle) {
2765
2775
  leased.map((lease) => {
2766
2776
  const entry = fetch_map.get(lease.stream);
2767
2777
  if (entry.fetch.error !== void 0) {
2768
- const error = `Fetch failed for ${lease.stream}: ${entry.fetch.error}`;
2769
- log().error(error);
2770
2778
  const policy = fetch_failure_policy(registry);
2771
- const block2 = !store_failing && policy.blockOnError && lease.retry >= policy.maxRetries;
2772
- const failed = {
2773
- lease,
2774
- handled: 0,
2775
- acked_at: lease.at,
2776
- error,
2777
- ...block2 ? { block: true } : {
2778
- next_attempt_at: Date.now() + compute_backoff_delay(lease.retry, DEFAULT_BACKOFF)
2779
- }
2780
- };
2781
- return Promise.resolve(failed);
2779
+ return Promise.resolve(
2780
+ finalize(
2781
+ lease,
2782
+ 0,
2783
+ lease.at,
2784
+ new Error(`Fetch failed for ${lease.stream}: ${entry.fetch.error}`),
2785
+ store_failing ? { ...policy, blockOnError: false } : policy,
2786
+ deps.logger
2787
+ )
2788
+ );
2782
2789
  }
2783
2790
  if (entry.failed)
2784
- return Promise.resolve(resolver_failed(lease, entry.failed));
2791
+ return Promise.resolve(
2792
+ finalize(
2793
+ lease,
2794
+ 0,
2795
+ lease.at,
2796
+ entry.failed.error,
2797
+ entry.failed.options,
2798
+ deps.logger
2799
+ )
2800
+ );
2785
2801
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
2786
2802
  const { payloads } = entry;
2787
2803
  const exhausted = budget_exhausted(
@@ -2820,18 +2836,23 @@ var EMPTY_DRAIN = {
2820
2836
  acked: [],
2821
2837
  blocked: []
2822
2838
  };
2839
+ var RETRY = "";
2823
2840
  var DrainController = class {
2824
2841
  _armed = false;
2825
2842
  _locked = false;
2826
2843
  _ratio = 0.5;
2827
2844
  /**
2828
- * Local wake at the earliest parked (backoff or defer) visit; the schedule
2829
- * itself is persisted. On an idle app nothing else would drain it. A due
2830
- * entry stays until handled or until a later pass claims nothing.
2845
+ * Local wake at the earliest parked (backoff or defer) visit, or at the
2846
+ * lease expiry after a failed pass (the {@link RETRY} key); the schedule
2847
+ * itself is persisted. On an idle app nothing else would drain it. It
2848
+ * drains until a pass leases nothing, and an entry stays until handled or
2849
+ * until a later pass claims nothing.
2831
2850
  */
2832
2851
  _defer = new DeferTimer(() => {
2833
2852
  this._armed = true;
2834
- void this._deps.run_scoped(() => this.drain());
2853
+ void this._deps.run_scoped(async () => {
2854
+ while ((await this.drain()).leased.length > 0) ;
2855
+ });
2835
2856
  });
2836
2857
  _worker;
2837
2858
  _misrouted = /* @__PURE__ */ new Set();
@@ -2928,10 +2949,12 @@ var DrainController = class {
2928
2949
  if (acked.length) this._deps.on_acked(acked);
2929
2950
  if (closeable.length) await this._deps.on_close(closeable);
2930
2951
  this._deps.breaker.passed();
2931
- const has_errors = handled.some(({ error }) => error);
2932
- if (!acked.length && !blocked.length && !has_errors) this._armed = false;
2933
2952
  return { fetched, leased, acked, blocked };
2934
2953
  } catch (error) {
2954
+ this._defer.set(
2955
+ RETRY,
2956
+ Date.now() + Math.max(leaseMillis, this._deps.breaker.cooldown_ms)
2957
+ );
2935
2958
  this._deps.breaker.failed(Date.now(), error);
2936
2959
  return EMPTY_DRAIN;
2937
2960
  } finally {
@@ -3055,7 +3078,8 @@ var SettleLoop = class {
3055
3078
  /**
3056
3079
  * Schedule a settle pass. Multiple calls inside the debounce window
3057
3080
  * coalesce into one cycle. The cycle runs correlate→drain in a loop
3058
- * until no progress is made (no new subscriptions, no acks, no blocks)
3081
+ * until no progress is made (no new subscriptions, no events read, no
3082
+ * streams leased)
3059
3083
  * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
3060
3084
  * via {@link SettleDeps.on_settled}.
3061
3085
  */
@@ -3095,7 +3119,7 @@ var SettleLoop = class {
3095
3119
  acked: [...settled_drain.acked, ...drain.acked],
3096
3120
  blocked: [...settled_drain.blocked, ...drain.blocked]
3097
3121
  } : drain;
3098
- const made_progress = subscribed > 0 || drain.acked.length > 0 || drain.blocked.length > 0 || last_id > after_before;
3122
+ const made_progress = subscribed > 0 || drain.leased.length > 0 || last_id > after_before;
3099
3123
  if (!made_progress) break;
3100
3124
  }
3101
3125
  if (settled_drain) this._deps.on_settled(settled_drain);
@@ -3132,26 +3156,6 @@ var SettleLoop = class {
3132
3156
  };
3133
3157
 
3134
3158
  // src/builders/reaction-builder.ts
3135
- function finalize(lease, handled, at, error, options, logger, failed_at) {
3136
- if (!error) return { lease, handled, acked_at: at };
3137
- logger.error(error);
3138
- const non_retryable = error instanceof NonRetryableError;
3139
- const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
3140
- if (block2)
3141
- logger.error(
3142
- non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
3143
- );
3144
- const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
3145
- return {
3146
- lease,
3147
- handled,
3148
- acked_at: at,
3149
- error: error.message,
3150
- block: block2,
3151
- next_attempt_at,
3152
- failed_at
3153
- };
3154
- }
3155
3159
  function build_handle(deps) {
3156
3160
  const { logger, reaction_scope } = deps;
3157
3161
  return async (lease, payloads) => {
@@ -4297,7 +4301,8 @@ var Act = class {
4297
4301
  * Call this after `app.do()` (or `app.reset()`) to schedule a background
4298
4302
  * drain. Multiple rapid calls within the debounce window are coalesced
4299
4303
  * into a single cycle. Runs correlate→drain in a loop until a pass makes
4300
- * no progress — no new subscriptions, no acks, no blocks — then emits
4304
+ * no progress — no new subscriptions, no events read, no streams leased —
4305
+ * then emits
4301
4306
  * the `"settled"` lifecycle event. This means a single `settle()` call
4302
4307
  * fully catches up paginated streams (e.g. after `reset()` on a long
4303
4308
  * projection) without forcing callers to loop.