@rotorsoft/act 1.32.27 → 1.32.29

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";
@@ -1149,12 +1153,13 @@ var CircuitBreaker = class {
1149
1153
  * if a half-open trial just failed. Returns the resulting state.
1150
1154
  *
1151
1155
  * Callers gate on the state directly — `state(now) === "open"` means skip.
1152
- * On opening, schedules the `on_retry` wake so recovery is automatic;
1153
- * always surfaces the failure via `on_error`.
1156
+ * Every failure (re)schedules the `on_retry` wake, so one failed pass on
1157
+ * an idle app retries without waiting for the breaker to open; always
1158
+ * surfaces the failure via `on_error`.
1154
1159
  */
1155
1160
  failed(now, error) {
1156
1161
  const circuit = this._advance(now);
1157
- if (circuit === "open") this._schedule_wake();
1162
+ this._schedule_wake();
1158
1163
  this._hooks.on_error?.(error, circuit);
1159
1164
  return circuit;
1160
1165
  }
@@ -2682,20 +2687,24 @@ function fetch_failure_policy(registry) {
2682
2687
  }
2683
2688
  return { blockOnError, maxRetries };
2684
2689
  }
2685
- function resolver_failed(lease, { error, options }) {
2686
- log().error(error);
2687
- const block2 = options.blockOnError && lease.retry >= options.maxRetries;
2690
+ function finalize(lease, handled, at, error, options, logger, failed_at) {
2691
+ if (!error) return { lease, handled, acked_at: at };
2692
+ logger.error(error);
2693
+ const non_retryable = error instanceof NonRetryableError;
2694
+ const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
2695
+ if (block2)
2696
+ logger.error(
2697
+ non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
2698
+ );
2699
+ const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
2688
2700
  return {
2689
2701
  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
- }
2702
+ handled,
2703
+ acked_at: at,
2704
+ error: error.message,
2705
+ block: block2,
2706
+ next_attempt_at,
2707
+ failed_at
2699
2708
  };
2700
2709
  }
2701
2710
  function warn_misrouted(seen, stream, handler, event) {
@@ -2743,7 +2752,9 @@ async function run_drain_cycle(deps, cycle) {
2743
2752
  resolved = dynamic ? resolver(event) : resolver;
2744
2753
  } catch (error) {
2745
2754
  failed ??= {
2746
- error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
2755
+ error: new Error(
2756
+ `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`
2757
+ ),
2747
2758
  options: reaction.options
2748
2759
  };
2749
2760
  return false;
@@ -2765,23 +2776,29 @@ async function run_drain_cycle(deps, cycle) {
2765
2776
  leased.map((lease) => {
2766
2777
  const entry = fetch_map.get(lease.stream);
2767
2778
  if (entry.fetch.error !== void 0) {
2768
- const error = `Fetch failed for ${lease.stream}: ${entry.fetch.error}`;
2769
- log().error(error);
2770
2779
  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);
2780
+ return Promise.resolve(
2781
+ finalize(
2782
+ lease,
2783
+ 0,
2784
+ lease.at,
2785
+ new Error(`Fetch failed for ${lease.stream}: ${entry.fetch.error}`),
2786
+ store_failing ? { ...policy, blockOnError: false } : policy,
2787
+ deps.logger
2788
+ )
2789
+ );
2782
2790
  }
2783
2791
  if (entry.failed)
2784
- return Promise.resolve(resolver_failed(lease, entry.failed));
2792
+ return Promise.resolve(
2793
+ finalize(
2794
+ lease,
2795
+ 0,
2796
+ lease.at,
2797
+ entry.failed.error,
2798
+ entry.failed.options,
2799
+ deps.logger
2800
+ )
2801
+ );
2785
2802
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
2786
2803
  const { payloads } = entry;
2787
2804
  const exhausted = budget_exhausted(
@@ -2820,18 +2837,23 @@ var EMPTY_DRAIN = {
2820
2837
  acked: [],
2821
2838
  blocked: []
2822
2839
  };
2840
+ var RETRY = "";
2823
2841
  var DrainController = class {
2824
2842
  _armed = false;
2825
2843
  _locked = false;
2826
2844
  _ratio = 0.5;
2827
2845
  /**
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.
2846
+ * Local wake at the earliest parked (backoff or defer) visit, or at the
2847
+ * lease expiry after a failed pass (the {@link RETRY} key); the schedule
2848
+ * itself is persisted. On an idle app nothing else would drain it. It
2849
+ * drains until a pass leases nothing or the controller stops, and an entry
2850
+ * stays until handled or until a later pass claims nothing.
2831
2851
  */
2832
2852
  _defer = new DeferTimer(() => {
2833
2853
  this._armed = true;
2834
- void this._deps.run_scoped(() => this.drain());
2854
+ void this._deps.run_scoped(async () => {
2855
+ while (!this._stopped && (await this.drain()).leased.length > 0) ;
2856
+ });
2835
2857
  });
2836
2858
  _worker;
2837
2859
  _misrouted = /* @__PURE__ */ new Set();
@@ -2928,10 +2950,12 @@ var DrainController = class {
2928
2950
  if (acked.length) this._deps.on_acked(acked);
2929
2951
  if (closeable.length) await this._deps.on_close(closeable);
2930
2952
  this._deps.breaker.passed();
2931
- const has_errors = handled.some(({ error }) => error);
2932
- if (!acked.length && !blocked.length && !has_errors) this._armed = false;
2933
2953
  return { fetched, leased, acked, blocked };
2934
2954
  } catch (error) {
2955
+ this._defer.set(
2956
+ RETRY,
2957
+ Date.now() + Math.max(leaseMillis, this._deps.breaker.cooldown_ms)
2958
+ );
2935
2959
  this._deps.breaker.failed(Date.now(), error);
2936
2960
  return EMPTY_DRAIN;
2937
2961
  } finally {
@@ -3055,7 +3079,8 @@ var SettleLoop = class {
3055
3079
  /**
3056
3080
  * Schedule a settle pass. Multiple calls inside the debounce window
3057
3081
  * 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)
3082
+ * until no progress is made (no new subscriptions, no events read, no
3083
+ * streams leased)
3059
3084
  * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
3060
3085
  * via {@link SettleDeps.on_settled}.
3061
3086
  */
@@ -3095,7 +3120,7 @@ var SettleLoop = class {
3095
3120
  acked: [...settled_drain.acked, ...drain.acked],
3096
3121
  blocked: [...settled_drain.blocked, ...drain.blocked]
3097
3122
  } : drain;
3098
- const made_progress = subscribed > 0 || drain.acked.length > 0 || drain.blocked.length > 0 || last_id > after_before;
3123
+ const made_progress = subscribed > 0 || drain.leased.length > 0 || last_id > after_before;
3099
3124
  if (!made_progress) break;
3100
3125
  }
3101
3126
  if (settled_drain) this._deps.on_settled(settled_drain);
@@ -3132,26 +3157,6 @@ var SettleLoop = class {
3132
3157
  };
3133
3158
 
3134
3159
  // 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
3160
  function build_handle(deps) {
3156
3161
  const { logger, reaction_scope } = deps;
3157
3162
  return async (lease, payloads) => {
@@ -4297,7 +4302,8 @@ var Act = class {
4297
4302
  * Call this after `app.do()` (or `app.reset()`) to schedule a background
4298
4303
  * drain. Multiple rapid calls within the debounce window are coalesced
4299
4304
  * 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
4305
+ * no progress — no new subscriptions, no events read, no streams leased —
4306
+ * then emits
4301
4307
  * the `"settled"` lifecycle event. This means a single `settle()` call
4302
4308
  * fully catches up paginated streams (e.g. after `reset()` on a long
4303
4309
  * projection) without forcing callers to loop.