@rotorsoft/act 1.23.18 → 1.23.20

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
@@ -1566,6 +1566,15 @@ var CorrelateCycle = class {
1566
1566
  get checkpoint() {
1567
1567
  return this._checkpoint;
1568
1568
  }
1569
+ /**
1570
+ * Whether any reaction uses a dynamic resolver — i.e. whether `correlate`
1571
+ * performs a store scan rather than the static no-op early-return. The
1572
+ * settle loop reads this to avoid recording a fictitious circuit-breaker
1573
+ * success on a static app whose correlate never touches the store (#1329).
1574
+ */
1575
+ get has_dynamic_resolvers() {
1576
+ return this._has_dynamic_resolvers;
1577
+ }
1569
1578
  /**
1570
1579
  * Initialize correlation state on first call.
1571
1580
  * - Reads max(at) from store, then floors the cold-start checkpoint at
@@ -2067,8 +2076,21 @@ async function load(me, target, callback, patch_fn = bare_patch) {
2067
2076
  // a non-time-travel cold load — a bounded load full-scans real events
2068
2077
  // under its `asOf` filter. This is the single floor-eligibility decision
2069
2078
  // for the whole system; the stores apply the floor whenever asked and
2070
- // never re-derive it (RFC 1274). The warm path resumes from `after`.
2071
- ...cached ? { after: cached.event_id } : { ...time_travel ? {} : { with_snaps: true }, ...asOf }
2079
+ // never re-derive it (RFC 1274).
2080
+ //
2081
+ // The warm path resumes from `after`, and MUST also carry `with_snaps`
2082
+ // (#1345): `after` and `with_snaps` are independent store conditions —
2083
+ // `after` bounds the scan (id > cached.event_id), `with_snaps` keeps
2084
+ // `__snapshot__` rows in the result. A stale (lagging/cross-process)
2085
+ // cache checkpoint can sit below a newer `__snapshot__` boundary, and a
2086
+ // windowed close (`app.close`/`.autocloses({keep})`) may have pruned the
2087
+ // domain events between the checkpoint and that snapshot. Without
2088
+ // `with_snaps` the rebaselining snapshot is filtered out and the fold
2089
+ // silently applies the surviving tail on top of stale state (wrong
2090
+ // count, yet `version` still reports the true head — the concurrency
2091
+ // guard passes). With it, the snapshot in the after-window rebaselines
2092
+ // the fold; when no snapshot falls in the window it is a no-op.
2093
+ ...cached ? { after: cached.event_id, with_snaps: true } : { ...time_travel ? {} : { with_snaps: true }, ...asOf }
2072
2094
  }
2073
2095
  );
2074
2096
  const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
@@ -2860,7 +2882,7 @@ var SettleLoop = class {
2860
2882
  ...correlate_query,
2861
2883
  after: after_before
2862
2884
  });
2863
- this._deps.breaker.passed();
2885
+ if (this._deps.correlate_probes_store) this._deps.breaker.passed();
2864
2886
  last_drain = await this._deps.drain(drain_options);
2865
2887
  const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0 || last_id > after_before;
2866
2888
  if (!made_progress) break;
@@ -3125,13 +3147,14 @@ var Act = class {
3125
3147
  on_error: (error, circuit) => this._emit_error(error, circuit),
3126
3148
  // Re-probe the store when the cooldown elapses, so recovery is
3127
3149
  // automatic even on the default lane (which has no periodic poller).
3128
- // Any polling loop can trip the shared breaker — but a bare `drain()`
3129
- // only touches the store when the controller is armed, so a trip
3130
- // from a correlate failure could go unprobed. `settle()` always runs
3131
- // a correlate (a store query) before draining, so it re-probes
3132
- // regardless of which loop opened the breaker: one success closes it
3133
- // and every loop resumes on its own cadence. A failed probe re-opens
3134
- // the breaker and reschedules the wake.
3150
+ // The wake fires `settle()`, which (in half-open) runs a real store
3151
+ // probe: for a dynamic-resolver app the probe is settle's correlate
3152
+ // (a store scan); for a static-reaction app correlate is a no-op that
3153
+ // records no health, so the probe is settle's DRAIN claim — either
3154
+ // way one success closes the breaker and every loop resumes, a
3155
+ // failure re-opens it and reschedules the wake. Settle does NOT close
3156
+ // the breaker off a no-op correlate (#1329) — only a real store op
3157
+ // (correlate scan or drain claim) records `passed()`.
3135
3158
  on_retry: () => {
3136
3159
  this.settle({ debounceMs: 0 });
3137
3160
  }
@@ -3308,7 +3331,11 @@ var Act = class {
3308
3331
  correlate: (q) => this.correlate(q),
3309
3332
  drain: (o) => this.drain(o),
3310
3333
  on_settled: (drain) => this.emit("settled", drain),
3311
- breaker: this._breaker
3334
+ breaker: this._breaker,
3335
+ // Static-reaction apps' correlate is a no-op (no store call), so a
3336
+ // settle pass carries no store-health signal — don't let it record a
3337
+ // breaker success (#1329).
3338
+ correlate_probes_store: this._correlate.has_dynamic_resolvers
3312
3339
  },
3313
3340
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
3314
3341
  );