@rotorsoft/act 1.31.12 → 1.31.14

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
@@ -916,6 +916,15 @@ var CircuitBreaker = class {
916
916
  this._cooldown_ms = config2.cooldownMs;
917
917
  this._hooks = hooks;
918
918
  }
919
+ /**
920
+ * True when the store has failed at least once since the last pass that
921
+ * went all the way through. The breaker can still read "closed" here: it
922
+ * only opens after several failures in a row, so this catches the first
923
+ * one, which `state()` cannot.
924
+ */
925
+ get failing() {
926
+ return this._failures > 0;
927
+ }
919
928
  /** Current state given the wall-clock `now`. */
920
929
  state(now) {
921
930
  if (this._opened_at === void 0) return "closed";
@@ -1480,13 +1489,33 @@ var CorrelateCycle = class {
1480
1489
  /** In-flight init, memoized for single-flight and cleared on failure. */
1481
1490
  _init_promise;
1482
1491
  _timer = void 0;
1483
- // target → what it was last subscribed at. Every scan re-subscribes the
1492
+ // Dynamically discovered targets → what each was last subscribed at,
1493
+ // bounded by `maxSubscribedStreams`. The static half lives in
1494
+ // `_static_subscriptions` below, which is deliberately not evictable.
1495
+ //
1496
+ // Every scan re-subscribes the
1484
1497
  // targets it resolved (that is how the work mark lands), so this no longer
1485
1498
  // decides *whether* a target is sent — it decides *what* is sent with it:
1486
1499
  // a resolution raises priority/lane only when it beats the recorded floor,
1487
1500
  // and otherwise re-sends the row's own values so the mark changes nothing
1488
1501
  // else. See {@link Subscription}.
1489
- _subscribed;
1502
+ _dynamic_subscriptions;
1503
+ /**
1504
+ * What each static target was subscribed at by `init`. A plain map, never
1505
+ * evicted: the collection is the build-time `_static_targets` list, so it
1506
+ * is already bounded by the registry and costs nothing the registry does
1507
+ * not already hold.
1508
+ *
1509
+ * Keeping these out of the LRU is what makes the `+Infinity` floor an
1510
+ * invariant rather than a race (#1582). Sharing the bounded map meant a
1511
+ * churn of dynamic targets could evict a static record, and the next
1512
+ * dynamic resolution to that target found no record, took the
1513
+ * first-discovery branch, and re-subscribed the target with its own
1514
+ * priority and lane — silently re-laning a stream whose lane the
1515
+ * build-time subscribe owns, and starving it wherever `onlyLanes` had
1516
+ * provisioned a worker for the declared lane.
1517
+ */
1518
+ _static_subscriptions = /* @__PURE__ */ new Map();
1490
1519
  /** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
1491
1520
  _patterns = new LruMap(PATTERN_CACHE_SIZE);
1492
1521
  _registry;
@@ -1539,7 +1568,7 @@ var CorrelateCycle = class {
1539
1568
  }) {
1540
1569
  this._lease_millis = lease_millis;
1541
1570
  this._key = registry_key(registry.events);
1542
- this._subscribed = new LruMap(max_subscribed_streams);
1571
+ this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
1543
1572
  this._registry = registry;
1544
1573
  this._declared_lanes = declared_lanes;
1545
1574
  this._static_targets = static_targets;
@@ -1589,7 +1618,7 @@ var CorrelateCycle = class {
1589
1618
  this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
1590
1619
  this._on_init?.();
1591
1620
  for (const { stream, priority = 0, lane } of this._static_targets) {
1592
- this._subscribed.set(stream, {
1621
+ this._static_subscriptions.set(stream, {
1593
1622
  floor: Number.POSITIVE_INFINITY,
1594
1623
  priority,
1595
1624
  lane
@@ -1608,14 +1637,12 @@ var CorrelateCycle = class {
1608
1637
  * the documented per-aggregate shape `.to(e => ({target: e.stream}))`
1609
1638
  * makes those two namespaces collide by construction (#1398).
1610
1639
  *
1611
- * Static targets are left alone: they are subscribed once at init and
1612
- * recorded at +Infinity so the dynamic path never re-opens them.
1640
+ * Static targets are left alone: their record lives in
1641
+ * `_static_subscriptions`, which this never touches, so they stay pinned
1642
+ * at +Infinity and the dynamic path never re-opens them.
1613
1643
  */
1614
1644
  forget_subscribed(streams) {
1615
- const statics = new Set(this._static_targets.map((t) => t.stream));
1616
- for (const stream of streams) {
1617
- if (!statics.has(stream)) this._subscribed.delete(stream);
1618
- }
1645
+ for (const stream of streams) this._dynamic_subscriptions.delete(stream);
1619
1646
  }
1620
1647
  /**
1621
1648
  * Would an event from `stream` be fetched for a target subscribed with
@@ -1691,7 +1718,7 @@ var CorrelateCycle = class {
1691
1718
  );
1692
1719
  lane = void 0;
1693
1720
  }
1694
- const recorded = this._subscribed.get(resolved.target);
1721
+ const recorded = this._static_subscriptions.get(resolved.target) ?? this._dynamic_subscriptions.get(resolved.target);
1695
1722
  const priority = resolved.priority ?? 0;
1696
1723
  const upgraded = !recorded || priority > recorded.floor;
1697
1724
  const carried = upgraded ? { priority, lane } : { priority: recorded.priority, lane: recorded.lane };
@@ -1750,7 +1777,7 @@ var CorrelateCycle = class {
1750
1777
  this._checkpoint = last_id;
1751
1778
  for (const { stream, priority, lane } of streams) {
1752
1779
  if (correlated.get(stream)?.upgraded)
1753
- this._subscribed.set(stream, {
1780
+ this._dynamic_subscriptions.set(stream, {
1754
1781
  floor: priority,
1755
1782
  priority,
1756
1783
  lane
@@ -2605,10 +2632,10 @@ function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
2605
2632
  }
2606
2633
 
2607
2634
  // src/internal/drain-cycle.ts
2608
- function budget_exhausted(lease, options) {
2609
- if (!options?.blockOnError || lease.retry <= options.maxRetries)
2635
+ function budget_exhausted(lease, options, store_failing) {
2636
+ if (store_failing || !options?.blockOnError || lease.retry <= options.maxRetries)
2610
2637
  return void 0;
2611
- const error = `Blocking ${lease.stream} after ${lease.retry} claims with no acknowledged progress \u2014 the lease was lost on every attempt, so the retry budget (${options.maxRetries}) was spent without the handler ever reporting an error. Raise leaseMillis for this handler, then unblock the stream.`;
2638
+ const error = `Blocking ${lease.stream} after ${lease.retry} claims with no acknowledged progress \u2014 the retry budget (${options.maxRetries}) was spent without the handler ever reporting an error. That means every attempt lost its lease before it could ack: raise leaseMillis for this handler, then unblock the stream.`;
2612
2639
  log().error(error);
2613
2640
  return { lease, handled: 0, acked_at: lease.at, error, block: true };
2614
2641
  }
@@ -2619,7 +2646,7 @@ function warn_misrouted(seen, stream, handler, event) {
2619
2646
  `Reaction "${handler}" on "${event}" resolved to target "${stream}", which a projection already serves. A target is served by one batch handler or one state projection, so this reaction can never run \u2014 and delivering to it would hand the projection an aggregate from another stream. Skipping it. The equivalent static \`.to({ target })\` is rejected at build; a dynamic resolver's target is only knowable here.`
2620
2647
  );
2621
2648
  }
2622
- async function run_drain_cycle(ops, registry, batch_handlers, misrouted, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, on_blocked, lane) {
2649
+ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_failing, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, on_blocked, lane) {
2623
2650
  const leased = await ops.claim(
2624
2651
  lagging,
2625
2652
  leading,
@@ -2661,7 +2688,11 @@ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, handle,
2661
2688
  const entry = fetch_map.get(lease.stream);
2662
2689
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
2663
2690
  const { payloads } = entry;
2664
- const exhausted = budget_exhausted(lease, payloads[0]?.options);
2691
+ const exhausted = budget_exhausted(
2692
+ lease,
2693
+ payloads[0]?.options,
2694
+ store_failing
2695
+ );
2665
2696
  if (exhausted) return Promise.resolve(exhausted);
2666
2697
  const batchHandler = batch_handlers.get(lease.stream);
2667
2698
  if (batchHandler && payloads.length > 0) {
@@ -2833,6 +2864,7 @@ var DrainController = class {
2833
2864
  this._deps.registry,
2834
2865
  this._deps.batch_handlers,
2835
2866
  this._misrouted,
2867
+ this._deps.breaker.failing,
2836
2868
  this._deps.handle,
2837
2869
  this._deps.handle_batch,
2838
2870
  lagging,