@rotorsoft/act 1.32.25 → 1.32.26

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
@@ -1692,13 +1692,25 @@ var CorrelateCycle = class {
1692
1692
  const after = Math.max(this._checkpoint, query.after || -1);
1693
1693
  const correlated = /* @__PURE__ */ new Map();
1694
1694
  let last_id = after;
1695
+ let stopped = false;
1695
1696
  const found = await store().query(
1696
1697
  (event) => {
1697
- last_id = event.id;
1698
+ if (stopped) return;
1698
1699
  const register = this._registry.events[event.name];
1699
1700
  if (register) {
1700
1701
  for (const reaction of register.reactions.values()) {
1701
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1702
+ let resolved;
1703
+ try {
1704
+ resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1705
+ } catch (error) {
1706
+ stopped = true;
1707
+ report_once(
1708
+ this._reported,
1709
+ `resolver|${reaction.handler.name}|${event.id}`,
1710
+ `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}. Correlation stops before this event until the resolver stops throwing; work already found keeps draining.`
1711
+ );
1712
+ return;
1713
+ }
1702
1714
  if (!resolved) continue;
1703
1715
  let lane = resolved.lane;
1704
1716
  if (lane !== void 0 && !this._declared_lanes.has(lane)) {
@@ -1747,6 +1759,7 @@ var CorrelateCycle = class {
1747
1759
  correlated.set(resolved.target, entry);
1748
1760
  }
1749
1761
  }
1762
+ last_id = event.id;
1750
1763
  },
1751
1764
  // Resolvers see `pii: null`: correlate runs actor-less, and one
1752
1765
  // undecryptable row would otherwise stop every stream's reactions.
@@ -1788,7 +1801,7 @@ var CorrelateCycle = class {
1788
1801
  return { subscribed, last_id, marked, scanned: true };
1789
1802
  }
1790
1803
  this._checkpoint = last_id;
1791
- this._armed = found === query.limit;
1804
+ this._armed = stopped || found === query.limit;
1792
1805
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1793
1806
  }
1794
1807
  /**
@@ -2672,6 +2685,22 @@ function fetch_failure_policy(registry) {
2672
2685
  }
2673
2686
  return { blockOnError, maxRetries };
2674
2687
  }
2688
+ function resolver_failed(lease, { error, options }) {
2689
+ log().error(error);
2690
+ const block2 = options.blockOnError && lease.retry >= options.maxRetries;
2691
+ return {
2692
+ lease,
2693
+ handled: 0,
2694
+ acked_at: lease.at,
2695
+ error,
2696
+ ...block2 ? { block: true } : {
2697
+ next_attempt_at: Date.now() + compute_backoff_delay(
2698
+ lease.retry,
2699
+ options.backoff ?? DEFAULT_BACKOFF
2700
+ )
2701
+ }
2702
+ };
2703
+ }
2675
2704
  function warn_misrouted(seen, stream, handler, event) {
2676
2705
  report_once(
2677
2706
  seen,
@@ -2705,13 +2734,23 @@ async function run_drain_cycle(deps, cycle) {
2705
2734
  );
2706
2735
  for (const f of fetched) {
2707
2736
  const { stream, events } = f;
2737
+ let failed;
2708
2738
  const payloads = events.flatMap((event) => {
2709
2739
  const register = registry.events[event.name];
2710
2740
  if (!register) return [];
2711
2741
  return [...register.reactions.values()].filter((reaction) => {
2712
2742
  const resolver = reaction.resolver;
2713
2743
  const dynamic = typeof resolver === "function";
2714
- const resolved = dynamic ? resolver(event) : resolver;
2744
+ let resolved;
2745
+ try {
2746
+ resolved = dynamic ? resolver(event) : resolver;
2747
+ } catch (error) {
2748
+ failed ??= {
2749
+ error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
2750
+ options: reaction.options
2751
+ };
2752
+ return false;
2753
+ }
2715
2754
  if (!resolved || resolved.target !== stream) return false;
2716
2755
  if (!dynamic || !batch_handlers.has(stream)) return true;
2717
2756
  warn_misrouted(
@@ -2723,7 +2762,7 @@ async function run_drain_cycle(deps, cycle) {
2723
2762
  return false;
2724
2763
  }).map((reaction) => ({ ...reaction, event }));
2725
2764
  });
2726
- fetch_map.set(stream, { fetch: f, payloads });
2765
+ fetch_map.set(stream, { fetch: f, payloads, failed });
2727
2766
  }
2728
2767
  const handled = await Promise.all(
2729
2768
  leased.map((lease) => {
@@ -2744,6 +2783,8 @@ async function run_drain_cycle(deps, cycle) {
2744
2783
  };
2745
2784
  return Promise.resolve(failed);
2746
2785
  }
2786
+ if (entry.failed)
2787
+ return Promise.resolve(resolver_failed(lease, entry.failed));
2747
2788
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
2748
2789
  const { payloads } = entry;
2749
2790
  const exhausted = budget_exhausted(
@@ -3400,8 +3441,18 @@ var Act = class {
3400
3441
  on_acked: (acked) => this.emit("acked", acked),
3401
3442
  on_blocked: (blocked) => this.emit("blocked", blocked),
3402
3443
  // Reaction-requested close: the same machinery as `app.close`.
3444
+ // A store failure reaches the breaker; anything else (the archive
3445
+ // callback is app code) is logged here, never counted.
3403
3446
  on_close: async (targets) => {
3404
- await this._run_close(targets);
3447
+ try {
3448
+ await this._run_close(targets);
3449
+ } catch (error) {
3450
+ if (error instanceof StoreError) throw error;
3451
+ this._logger.error(
3452
+ error,
3453
+ `Reaction-requested close of ${targets.map((t) => `"${t.stream}"`).join(", ")} failed and was not retried; close it with app.close(...) once the cause is fixed.`
3454
+ );
3455
+ }
3405
3456
  },
3406
3457
  breaker: this._breaker,
3407
3458
  run_scoped: this._scoped,