@rotorsoft/act 1.28.1 → 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
@@ -22,7 +22,7 @@ import {
22
22
  sleep,
23
23
  store,
24
24
  validate
25
- } from "./chunk-7WFKIQKY.js";
25
+ } from "./chunk-PX3IBVLW.js";
26
26
  import {
27
27
  ActorSchema,
28
28
  CausationEventSchema,
@@ -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
  },
@@ -1499,7 +1502,7 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
1499
1502
  (position) => {
1500
1503
  last = position.stream;
1501
1504
  const source_re = position.source ? get_regex(position.source) : void 0;
1502
- const has_work = position.correlated_at === void 0 || position.at < position.correlated_at;
1505
+ const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
1503
1506
  if (!has_work) return;
1504
1507
  for (const [stream, info] of stream_info) {
1505
1508
  if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
@@ -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;
@@ -1805,6 +1834,9 @@ var CorrelateCycle = class {
1805
1834
  }
1806
1835
  if (streams.length) {
1807
1836
  const { subscribed } = await this._cd.subscribe(streams, last_id);
1837
+ const marked = streams.filter(
1838
+ (entry) => entry.correlated_at !== void 0
1839
+ ).length;
1808
1840
  this._checkpoint = last_id;
1809
1841
  for (const { stream, priority, lane } of streams) {
1810
1842
  if (correlated.get(stream)?.upgraded)
@@ -1814,10 +1846,11 @@ var CorrelateCycle = class {
1814
1846
  lane
1815
1847
  });
1816
1848
  }
1817
- return { subscribed, last_id };
1849
+ return { subscribed, last_id, marked, scanned: true };
1818
1850
  }
1819
1851
  this._checkpoint = last_id;
1820
- return { subscribed: 0, last_id };
1852
+ this._armed = false;
1853
+ return { subscribed: 0, last_id, marked: 0, scanned: true };
1821
1854
  }
1822
1855
  /**
1823
1856
  * Start a periodic correlation worker. Returns false if one is already
@@ -1828,9 +1861,14 @@ var CorrelateCycle = class {
1828
1861
  if (this._timer) return false;
1829
1862
  const limit = query.limit || 100;
1830
1863
  this._timer = setInterval(
1831
- () => this._run_scoped(
1832
- () => this.correlate({ ...query, after: this._checkpoint, limit })
1833
- ).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) => {
1834
1872
  if (callback && result.subscribed) callback(result.subscribed);
1835
1873
  }).catch((err) => log().error(err)),
1836
1874
  frequency
@@ -3116,11 +3154,11 @@ var SettleLoop = class {
3116
3154
  let settled_drain;
3117
3155
  for (let i = 0; i < maxPasses; i++) {
3118
3156
  const after_before = this._deps.checkpoint();
3119
- const { subscribed, last_id } = await this._deps.correlate({
3157
+ const { subscribed, last_id, scanned } = await this._deps.correlate({
3120
3158
  ...correlate_query,
3121
3159
  after: after_before
3122
3160
  });
3123
- this._deps.breaker.passed();
3161
+ if (scanned) this._deps.breaker.passed();
3124
3162
  const drain = await this._deps.drain(drain_options);
3125
3163
  settled_drain = settled_drain ? {
3126
3164
  fetched: [...settled_drain.fetched, ...drain.fetched],
@@ -3617,7 +3655,7 @@ var Act = class {
3617
3655
  // the scoped store and `_initialized` then blocks a retry (#1191).
3618
3656
  init: () => this._scoped(() => this._correlate.init()),
3619
3657
  checkpoint: () => this._correlate.checkpoint,
3620
- correlate: (q) => this.correlate(q),
3658
+ correlate: (q) => this._correlate_scanned(q),
3621
3659
  drain: (o) => this.drain(o),
3622
3660
  on_settled: (drain) => this.emit("settled", drain),
3623
3661
  breaker: this._breaker
@@ -4030,6 +4068,7 @@ var Act = class {
4030
4068
  }
4031
4069
  /** Arm every active lane controller (ACT-1103). */
4032
4070
  _arm_all() {
4071
+ this._correlate.arm();
4033
4072
  for (const c of this._drain_controllers.values()) c.arm();
4034
4073
  }
4035
4074
  /**
@@ -4053,6 +4092,7 @@ var Act = class {
4053
4092
  for (const lane of set) to_arm.add(lane);
4054
4093
  }
4055
4094
  if (to_arm.size === 0) return false;
4095
+ this._correlate.arm();
4056
4096
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
4057
4097
  return true;
4058
4098
  }
@@ -4126,12 +4166,24 @@ var Act = class {
4126
4166
  * @see {@link stop_correlations} to stop automatic correlation
4127
4167
  */
4128
4168
  async correlate(query = { after: -1, limit: 10 }) {
4129
- 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 };
4130
4182
  return this._scoped(async () => {
4131
- const result = await this._correlate.correlate(query);
4132
- if (result.subscribed > 0 && this._reactive_events.size > 0)
4183
+ const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query);
4184
+ if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
4133
4185
  this._arm_all();
4134
- return result;
4186
+ return { subscribed, last_id, scanned };
4135
4187
  });
4136
4188
  }
4137
4189
  /**
@@ -4564,6 +4616,7 @@ var Act = class {
4564
4616
  async _catch_up_correlation(until) {
4565
4617
  for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
4566
4618
  const before = this._correlate.checkpoint;
4619
+ this._correlate.arm();
4567
4620
  await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4568
4621
  if (this._correlate.checkpoint <= before) break;
4569
4622
  }