@rotorsoft/act 1.29.0 → 1.29.1

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
@@ -1382,9 +1382,11 @@ async function run_close_cycle(targets, deps) {
1382
1382
  return { truncated, skipped };
1383
1383
  }
1384
1384
  async function run_windowed_closes(windowed, deps, skipped) {
1385
+ const checkpoint = deps.reactive_events_size > 0 ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
1385
1386
  const min_at = deps.reactive_events_size > 0 ? await probe_min_watermarks(
1386
1387
  windowed.map((t) => t.stream),
1387
- deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
1388
+ deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
1389
+ checkpoint
1388
1390
  ) : /* @__PURE__ */ new Map();
1389
1391
  const with_lock = deps.with_stream_lock ?? ((_stream, work) => work());
1390
1392
  const truncated = /* @__PURE__ */ new Map();
@@ -1417,7 +1419,7 @@ async function windowed_prune_pending(stream, before, max_id) {
1417
1419
  );
1418
1420
  return boundary_id !== void 0 && min_id < boundary_id;
1419
1421
  }
1420
- async function probe_min_watermarks(streams, page_size) {
1422
+ async function probe_min_watermarks(streams, page_size, checkpoint) {
1421
1423
  const min_at = /* @__PURE__ */ new Map();
1422
1424
  const source_regex = /* @__PURE__ */ new Map();
1423
1425
  const get_regex = (source) => {
@@ -1435,11 +1437,12 @@ async function probe_min_watermarks(streams, page_size) {
1435
1437
  (position) => {
1436
1438
  last = position.stream;
1437
1439
  const source_re = position.source ? get_regex(position.source) : void 0;
1440
+ const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
1441
+ const cap = pending ? position.at : Math.max(position.at, checkpoint);
1438
1442
  for (const stream of streams) {
1439
1443
  if (!source_re || source_re.test(stream)) {
1440
1444
  const prev = min_at.get(stream);
1441
- if (prev === void 0 || position.at < prev)
1442
- min_at.set(stream, position.at);
1445
+ if (prev === void 0 || cap < prev) min_at.set(stream, cap);
1443
1446
  }
1444
1447
  }
1445
1448
  },
@@ -1609,6 +1612,17 @@ var PATTERN_CACHE_SIZE = 32;
1609
1612
  var CorrelateCycle = class {
1610
1613
  _checkpoint = -1;
1611
1614
  _initialized = false;
1615
+ /**
1616
+ * Whether a scan might find anything. The drain has carried the same flag
1617
+ * since it was written — a commit raises it, an empty claim lowers it, and
1618
+ * a disarmed drain returns without touching the store. Correlate had no
1619
+ * equivalent, so a settle pass always scanned, including the final pass
1620
+ * whose only job is to confirm nothing changed (#1510).
1621
+ *
1622
+ * Starts armed: the log may already hold events this process has never
1623
+ * correlated, and only a scan can find out.
1624
+ */
1625
+ _armed = true;
1612
1626
  /** In-flight init, memoized for single-flight and cleared on failure. */
1613
1627
  _init_promise;
1614
1628
  _timer = void 0;
@@ -1672,6 +1686,14 @@ var CorrelateCycle = class {
1672
1686
  get checkpoint() {
1673
1687
  return this._checkpoint;
1674
1688
  }
1689
+ /**
1690
+ * Signal that a commit (local or remote) may have produced events this
1691
+ * process has not correlated. Cheap and idempotent — the orchestrator calls
1692
+ * it on every commit and every notification.
1693
+ */
1694
+ arm() {
1695
+ this._armed = true;
1696
+ }
1675
1697
  /**
1676
1698
  * Initialize correlation state on first call.
1677
1699
  * - Reads the durable correlate checkpoint (and max(at)) from the store,
@@ -1757,6 +1779,13 @@ var CorrelateCycle = class {
1757
1779
  */
1758
1780
  async correlate(query = { after: -1, limit: 10 }) {
1759
1781
  await this.init();
1782
+ if (!this._armed)
1783
+ return {
1784
+ subscribed: 0,
1785
+ last_id: this._checkpoint,
1786
+ marked: 0,
1787
+ scanned: false
1788
+ };
1760
1789
  const after = Math.max(this._checkpoint, query.after || -1);
1761
1790
  const correlated = /* @__PURE__ */ new Map();
1762
1791
  let last_id = after;
@@ -1817,10 +1846,11 @@ var CorrelateCycle = class {
1817
1846
  lane
1818
1847
  });
1819
1848
  }
1820
- return { subscribed, last_id, marked };
1849
+ return { subscribed, last_id, marked, scanned: true };
1821
1850
  }
1822
1851
  this._checkpoint = last_id;
1823
- return { subscribed: 0, last_id, marked: 0 };
1852
+ this._armed = false;
1853
+ return { subscribed: 0, last_id, marked: 0, scanned: true };
1824
1854
  }
1825
1855
  /**
1826
1856
  * Start a periodic correlation worker. Returns false if one is already
@@ -1831,9 +1861,14 @@ var CorrelateCycle = class {
1831
1861
  if (this._timer) return false;
1832
1862
  const limit = query.limit || 100;
1833
1863
  this._timer = setInterval(
1834
- () => this._run_scoped(
1835
- () => this.correlate({ ...query, after: this._checkpoint, limit })
1836
- ).then((result) => {
1864
+ () => this._run_scoped(() => {
1865
+ this.arm();
1866
+ return this.correlate({
1867
+ ...query,
1868
+ after: this._checkpoint,
1869
+ limit
1870
+ });
1871
+ }).then((result) => {
1837
1872
  if (callback && result.subscribed) callback(result.subscribed);
1838
1873
  }).catch((err) => log().error(err)),
1839
1874
  frequency
@@ -3119,11 +3154,11 @@ var SettleLoop = class {
3119
3154
  let settled_drain;
3120
3155
  for (let i = 0; i < maxPasses; i++) {
3121
3156
  const after_before = this._deps.checkpoint();
3122
- const { subscribed, last_id } = await this._deps.correlate({
3157
+ const { subscribed, last_id, scanned } = await this._deps.correlate({
3123
3158
  ...correlate_query,
3124
3159
  after: after_before
3125
3160
  });
3126
- this._deps.breaker.passed();
3161
+ if (scanned) this._deps.breaker.passed();
3127
3162
  const drain = await this._deps.drain(drain_options);
3128
3163
  settled_drain = settled_drain ? {
3129
3164
  fetched: [...settled_drain.fetched, ...drain.fetched],
@@ -3620,7 +3655,7 @@ var Act = class {
3620
3655
  // the scoped store and `_initialized` then blocks a retry (#1191).
3621
3656
  init: () => this._scoped(() => this._correlate.init()),
3622
3657
  checkpoint: () => this._correlate.checkpoint,
3623
- correlate: (q) => this.correlate(q),
3658
+ correlate: (q) => this._correlate_scanned(q),
3624
3659
  drain: (o) => this.drain(o),
3625
3660
  on_settled: (drain) => this.emit("settled", drain),
3626
3661
  breaker: this._breaker
@@ -4033,6 +4068,7 @@ var Act = class {
4033
4068
  }
4034
4069
  /** Arm every active lane controller (ACT-1103). */
4035
4070
  _arm_all() {
4071
+ this._correlate.arm();
4036
4072
  for (const c of this._drain_controllers.values()) c.arm();
4037
4073
  }
4038
4074
  /**
@@ -4056,6 +4092,7 @@ var Act = class {
4056
4092
  for (const lane of set) to_arm.add(lane);
4057
4093
  }
4058
4094
  if (to_arm.size === 0) return false;
4095
+ this._correlate.arm();
4059
4096
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
4060
4097
  return true;
4061
4098
  }
@@ -4129,12 +4166,24 @@ var Act = class {
4129
4166
  * @see {@link stop_correlations} to stop automatic correlation
4130
4167
  */
4131
4168
  async correlate(query = { after: -1, limit: 10 }) {
4132
- if (!this._drain) return { subscribed: 0, last_id: -1 };
4169
+ const { subscribed, last_id } = await this._correlate_scanned(query);
4170
+ return { subscribed, last_id };
4171
+ }
4172
+ /**
4173
+ * `correlate` plus whether the pass actually read the store (#1510).
4174
+ *
4175
+ * The settle loop needs that extra bit to decide whether the pass carries a
4176
+ * circuit-breaker health signal, and a disarmed pass carries none. It stays
4177
+ * internal rather than widening the public `correlate` return, which is
4178
+ * charter-covered and has no use for it.
4179
+ */
4180
+ async _correlate_scanned(query) {
4181
+ if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
4133
4182
  return this._scoped(async () => {
4134
- const { subscribed, last_id, marked } = await this._correlate.correlate(query);
4183
+ const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query);
4135
4184
  if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
4136
4185
  this._arm_all();
4137
- return { subscribed, last_id };
4186
+ return { subscribed, last_id, scanned };
4138
4187
  });
4139
4188
  }
4140
4189
  /**
@@ -4567,6 +4616,7 @@ var Act = class {
4567
4616
  async _catch_up_correlation(until) {
4568
4617
  for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
4569
4618
  const before = this._correlate.checkpoint;
4619
+ this._correlate.arm();
4570
4620
  await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4571
4621
  if (this._correlate.checkpoint <= before) break;
4572
4622
  }