@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.cjs CHANGED
@@ -1028,19 +1028,7 @@ var InMemoryStore = class {
1028
1028
  */
1029
1029
  async claim(lagging, leading, by, millis, lane) {
1030
1030
  await sleep();
1031
- const has_work = (s) => {
1032
- if (s.correlated_at !== void 0) return s.at < s.correlated_at;
1033
- if (!s.source) return s.at < this._max_non_snap_event_id;
1034
- if (is_literal_source(s.source)) {
1035
- const max_id = this._max_event_id_by_stream.get(s.source);
1036
- return max_id !== void 0 && max_id > s.at;
1037
- }
1038
- const re = new RegExp(s.source);
1039
- for (const [stream_name, max_id] of this._max_event_id_by_stream) {
1040
- if (max_id > s.at && re.test(stream_name)) return true;
1041
- }
1042
- return false;
1043
- };
1031
+ const has_work = (s) => s.correlated_at !== void 0 && s.at < s.correlated_at;
1044
1032
  const available = [...this._streams.values()].filter(
1045
1033
  (s) => s.is_available && has_work(s) && (lane === void 0 || s.lane === lane)
1046
1034
  );
@@ -2972,9 +2960,11 @@ async function run_close_cycle(targets, deps) {
2972
2960
  return { truncated, skipped };
2973
2961
  }
2974
2962
  async function run_windowed_closes(windowed, deps, skipped) {
2963
+ const checkpoint = deps.reactive_events_size > 0 ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
2975
2964
  const min_at = deps.reactive_events_size > 0 ? await probe_min_watermarks(
2976
2965
  windowed.map((t) => t.stream),
2977
- deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
2966
+ deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
2967
+ checkpoint
2978
2968
  ) : /* @__PURE__ */ new Map();
2979
2969
  const with_lock = deps.with_stream_lock ?? ((_stream, work) => work());
2980
2970
  const truncated = /* @__PURE__ */ new Map();
@@ -3007,7 +2997,7 @@ async function windowed_prune_pending(stream, before, max_id) {
3007
2997
  );
3008
2998
  return boundary_id !== void 0 && min_id < boundary_id;
3009
2999
  }
3010
- async function probe_min_watermarks(streams, page_size) {
3000
+ async function probe_min_watermarks(streams, page_size, checkpoint) {
3011
3001
  const min_at = /* @__PURE__ */ new Map();
3012
3002
  const source_regex = /* @__PURE__ */ new Map();
3013
3003
  const get_regex = (source) => {
@@ -3025,11 +3015,12 @@ async function probe_min_watermarks(streams, page_size) {
3025
3015
  (position) => {
3026
3016
  last = position.stream;
3027
3017
  const source_re = position.source ? get_regex(position.source) : void 0;
3018
+ const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
3019
+ const cap = pending ? position.at : Math.max(position.at, checkpoint);
3028
3020
  for (const stream of streams) {
3029
3021
  if (!source_re || source_re.test(stream)) {
3030
3022
  const prev = min_at.get(stream);
3031
- if (prev === void 0 || position.at < prev)
3032
- min_at.set(stream, position.at);
3023
+ if (prev === void 0 || cap < prev) min_at.set(stream, cap);
3033
3024
  }
3034
3025
  }
3035
3026
  },
@@ -3089,7 +3080,7 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
3089
3080
  (position) => {
3090
3081
  last = position.stream;
3091
3082
  const source_re = position.source ? get_regex(position.source) : void 0;
3092
- const has_work = position.correlated_at === void 0 || position.at < position.correlated_at;
3083
+ const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
3093
3084
  if (!has_work) return;
3094
3085
  for (const [stream, info] of stream_info) {
3095
3086
  if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
@@ -3199,6 +3190,17 @@ var PATTERN_CACHE_SIZE = 32;
3199
3190
  var CorrelateCycle = class {
3200
3191
  _checkpoint = -1;
3201
3192
  _initialized = false;
3193
+ /**
3194
+ * Whether a scan might find anything. The drain has carried the same flag
3195
+ * since it was written — a commit raises it, an empty claim lowers it, and
3196
+ * a disarmed drain returns without touching the store. Correlate had no
3197
+ * equivalent, so a settle pass always scanned, including the final pass
3198
+ * whose only job is to confirm nothing changed (#1510).
3199
+ *
3200
+ * Starts armed: the log may already hold events this process has never
3201
+ * correlated, and only a scan can find out.
3202
+ */
3203
+ _armed = true;
3202
3204
  /** In-flight init, memoized for single-flight and cleared on failure. */
3203
3205
  _init_promise;
3204
3206
  _timer = void 0;
@@ -3262,6 +3264,14 @@ var CorrelateCycle = class {
3262
3264
  get checkpoint() {
3263
3265
  return this._checkpoint;
3264
3266
  }
3267
+ /**
3268
+ * Signal that a commit (local or remote) may have produced events this
3269
+ * process has not correlated. Cheap and idempotent — the orchestrator calls
3270
+ * it on every commit and every notification.
3271
+ */
3272
+ arm() {
3273
+ this._armed = true;
3274
+ }
3265
3275
  /**
3266
3276
  * Initialize correlation state on first call.
3267
3277
  * - Reads the durable correlate checkpoint (and max(at)) from the store,
@@ -3347,6 +3357,13 @@ var CorrelateCycle = class {
3347
3357
  */
3348
3358
  async correlate(query = { after: -1, limit: 10 }) {
3349
3359
  await this.init();
3360
+ if (!this._armed)
3361
+ return {
3362
+ subscribed: 0,
3363
+ last_id: this._checkpoint,
3364
+ marked: 0,
3365
+ scanned: false
3366
+ };
3350
3367
  const after = Math.max(this._checkpoint, query.after || -1);
3351
3368
  const correlated = /* @__PURE__ */ new Map();
3352
3369
  let last_id = after;
@@ -3395,6 +3412,9 @@ var CorrelateCycle = class {
3395
3412
  }
3396
3413
  if (streams.length) {
3397
3414
  const { subscribed } = await this._cd.subscribe(streams, last_id);
3415
+ const marked = streams.filter(
3416
+ (entry) => entry.correlated_at !== void 0
3417
+ ).length;
3398
3418
  this._checkpoint = last_id;
3399
3419
  for (const { stream, priority, lane } of streams) {
3400
3420
  if (correlated.get(stream)?.upgraded)
@@ -3404,10 +3424,11 @@ var CorrelateCycle = class {
3404
3424
  lane
3405
3425
  });
3406
3426
  }
3407
- return { subscribed, last_id };
3427
+ return { subscribed, last_id, marked, scanned: true };
3408
3428
  }
3409
3429
  this._checkpoint = last_id;
3410
- return { subscribed: 0, last_id };
3430
+ this._armed = false;
3431
+ return { subscribed: 0, last_id, marked: 0, scanned: true };
3411
3432
  }
3412
3433
  /**
3413
3434
  * Start a periodic correlation worker. Returns false if one is already
@@ -3418,9 +3439,14 @@ var CorrelateCycle = class {
3418
3439
  if (this._timer) return false;
3419
3440
  const limit = query.limit || 100;
3420
3441
  this._timer = setInterval(
3421
- () => this._run_scoped(
3422
- () => this.correlate({ ...query, after: this._checkpoint, limit })
3423
- ).then((result) => {
3442
+ () => this._run_scoped(() => {
3443
+ this.arm();
3444
+ return this.correlate({
3445
+ ...query,
3446
+ after: this._checkpoint,
3447
+ limit
3448
+ });
3449
+ }).then((result) => {
3424
3450
  if (callback && result.subscribed) callback(result.subscribed);
3425
3451
  }).catch((err) => log().error(err)),
3426
3452
  frequency
@@ -4706,11 +4732,11 @@ var SettleLoop = class {
4706
4732
  let settled_drain;
4707
4733
  for (let i = 0; i < maxPasses; i++) {
4708
4734
  const after_before = this._deps.checkpoint();
4709
- const { subscribed, last_id } = await this._deps.correlate({
4735
+ const { subscribed, last_id, scanned } = await this._deps.correlate({
4710
4736
  ...correlate_query,
4711
4737
  after: after_before
4712
4738
  });
4713
- this._deps.breaker.passed();
4739
+ if (scanned) this._deps.breaker.passed();
4714
4740
  const drain = await this._deps.drain(drain_options);
4715
4741
  settled_drain = settled_drain ? {
4716
4742
  fetched: [...settled_drain.fetched, ...drain.fetched],
@@ -5207,7 +5233,7 @@ var Act = class {
5207
5233
  // the scoped store and `_initialized` then blocks a retry (#1191).
5208
5234
  init: () => this._scoped(() => this._correlate.init()),
5209
5235
  checkpoint: () => this._correlate.checkpoint,
5210
- correlate: (q) => this.correlate(q),
5236
+ correlate: (q) => this._correlate_scanned(q),
5211
5237
  drain: (o) => this.drain(o),
5212
5238
  on_settled: (drain) => this.emit("settled", drain),
5213
5239
  breaker: this._breaker
@@ -5620,6 +5646,7 @@ var Act = class {
5620
5646
  }
5621
5647
  /** Arm every active lane controller (ACT-1103). */
5622
5648
  _arm_all() {
5649
+ this._correlate.arm();
5623
5650
  for (const c of this._drain_controllers.values()) c.arm();
5624
5651
  }
5625
5652
  /**
@@ -5643,6 +5670,7 @@ var Act = class {
5643
5670
  for (const lane of set) to_arm.add(lane);
5644
5671
  }
5645
5672
  if (to_arm.size === 0) return false;
5673
+ this._correlate.arm();
5646
5674
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
5647
5675
  return true;
5648
5676
  }
@@ -5716,12 +5744,24 @@ var Act = class {
5716
5744
  * @see {@link stop_correlations} to stop automatic correlation
5717
5745
  */
5718
5746
  async correlate(query = { after: -1, limit: 10 }) {
5719
- if (!this._drain) return { subscribed: 0, last_id: -1 };
5747
+ const { subscribed, last_id } = await this._correlate_scanned(query);
5748
+ return { subscribed, last_id };
5749
+ }
5750
+ /**
5751
+ * `correlate` plus whether the pass actually read the store (#1510).
5752
+ *
5753
+ * The settle loop needs that extra bit to decide whether the pass carries a
5754
+ * circuit-breaker health signal, and a disarmed pass carries none. It stays
5755
+ * internal rather than widening the public `correlate` return, which is
5756
+ * charter-covered and has no use for it.
5757
+ */
5758
+ async _correlate_scanned(query) {
5759
+ if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
5720
5760
  return this._scoped(async () => {
5721
- const result = await this._correlate.correlate(query);
5722
- if (result.subscribed > 0 && this._reactive_events.size > 0)
5761
+ const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query);
5762
+ if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
5723
5763
  this._arm_all();
5724
- return result;
5764
+ return { subscribed, last_id, scanned };
5725
5765
  });
5726
5766
  }
5727
5767
  /**
@@ -6154,6 +6194,7 @@ var Act = class {
6154
6194
  async _catch_up_correlation(until) {
6155
6195
  for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
6156
6196
  const before = this._correlate.checkpoint;
6197
+ this._correlate.arm();
6157
6198
  await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6158
6199
  if (this._correlate.checkpoint <= before) break;
6159
6200
  }