@rotorsoft/act 1.25.2 → 1.26.0

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
@@ -815,6 +815,8 @@ var InMemoryStore = class {
815
815
  _next_id = 0;
816
816
  // stored stream positions and other metadata
817
817
  _streams = /* @__PURE__ */ new Map();
818
+ /** Correlate checkpoint (#1484): how far the log has been READ. */
819
+ _correlated_at = -1;
818
820
  // last committed version per stream — O(1) replacement for filter-on-commit
819
821
  _stream_versions = /* @__PURE__ */ new Map();
820
822
  // max non-snapshot event id per stream — drives the has-work probe in
@@ -832,6 +834,7 @@ var InMemoryStore = class {
832
834
  _reset_indexes() {
833
835
  this._events.length = 0;
834
836
  this._next_id = 0;
837
+ this._correlated_at = -1;
835
838
  this._stream_versions.clear();
836
839
  this._max_event_id_by_stream.clear();
837
840
  this._max_non_snap_event_id = -1;
@@ -1063,7 +1066,9 @@ var InMemoryStore = class {
1063
1066
  * @param streams - Streams to register with optional source + priority.
1064
1067
  * @returns subscribed count and current max watermark.
1065
1068
  */
1066
- async subscribe(streams) {
1069
+ async subscribe(streams, correlated_at) {
1070
+ if (correlated_at !== void 0 && correlated_at > this._correlated_at)
1071
+ this._correlated_at = correlated_at;
1067
1072
  await sleep();
1068
1073
  let subscribed = 0;
1069
1074
  for (const {
@@ -1088,7 +1093,7 @@ var InMemoryStore = class {
1088
1093
  for (const s of this._streams.values()) {
1089
1094
  if (s.at > watermark) watermark = s.at;
1090
1095
  }
1091
- return { subscribed, watermark };
1096
+ return { subscribed, watermark, correlated_at: this._correlated_at };
1092
1097
  }
1093
1098
  /**
1094
1099
  * Acknowledge completion of processing for leased streams.
@@ -3260,8 +3265,10 @@ var CorrelateCycle = class {
3260
3265
  this._initialized = true;
3261
3266
  }
3262
3267
  async _run_init() {
3263
- const { watermark } = await store2().subscribe([...this._static_targets]);
3264
- this._checkpoint = this._has_dynamic_resolvers ? Math.max(-1, watermark - this._cold_start_back_scan) : watermark;
3268
+ const { watermark, correlated_at } = await store2().subscribe([
3269
+ ...this._static_targets
3270
+ ]);
3271
+ this._checkpoint = !this._has_dynamic_resolvers ? watermark : correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
3265
3272
  this._on_init?.();
3266
3273
  for (const { stream } of this._static_targets) {
3267
3274
  this._subscribed.set(stream, Number.POSITIVE_INFINITY);
@@ -3341,7 +3348,7 @@ var CorrelateCycle = class {
3341
3348
  lane
3342
3349
  })
3343
3350
  );
3344
- const { subscribed } = await this._cd.subscribe(streams);
3351
+ const { subscribed } = await this._cd.subscribe(streams, last_id);
3345
3352
  this._checkpoint = last_id;
3346
3353
  if (subscribed) {
3347
3354
  for (const { stream, priority } of streams) {
@@ -3527,7 +3534,7 @@ async function fetch(leased, eventLimit) {
3527
3534
  }
3528
3535
  var ack = (leases) => store2().ack(leases);
3529
3536
  var block = (leases) => store2().block(leases);
3530
- var subscribe = (streams) => store2().subscribe(streams);
3537
+ var subscribe = (streams, correlated_at) => store2().subscribe(streams, correlated_at);
3531
3538
 
3532
3539
  // src/internal/event-sourcing.ts
3533
3540
  var import_act_patch = require("@rotorsoft/act-patch");
@@ -4589,6 +4596,19 @@ function build_handle_batch(logger) {
4589
4596
  var SettleLoop = class {
4590
4597
  _timer = void 0;
4591
4598
  _running = false;
4599
+ /**
4600
+ * Resolves when the cycle currently in flight finishes; `undefined` when
4601
+ * idle (#1468). `_running` answers "is a cycle running?" for the
4602
+ * re-arm bookkeeping; this answers "tell me when it is done" for a
4603
+ * graceful shutdown.
4604
+ *
4605
+ * `stop()` only cancels *scheduling* — a cycle already inside its
4606
+ * correlate → drain loop keeps going, and because `DrainController.drain`
4607
+ * does not consult `_stopped`, it can claim a stream after teardown
4608
+ * returned and after the store adapter was disposed. Never rejects: the
4609
+ * cycle's own `catch` contains its errors, so awaiting this is safe.
4610
+ */
4611
+ _inflight;
4592
4612
  /**
4593
4613
  * Set when a `schedule()` timer fires while a cycle is still running
4594
4614
  * (ACT-1205). The in-flight cycle's `finally` re-arms one more pass so
@@ -4628,6 +4648,10 @@ var SettleLoop = class {
4628
4648
  return;
4629
4649
  }
4630
4650
  this._running = true;
4651
+ let settle_done;
4652
+ this._inflight = new Promise((done) => {
4653
+ settle_done = done;
4654
+ });
4631
4655
  (async () => {
4632
4656
  await this._deps.init();
4633
4657
  let settled_drain;
@@ -4653,6 +4677,8 @@ var SettleLoop = class {
4653
4677
  this._deps.breaker.failed(Date.now(), err);
4654
4678
  }).finally(() => {
4655
4679
  this._running = false;
4680
+ this._inflight = void 0;
4681
+ settle_done();
4656
4682
  const pending = this._pending;
4657
4683
  if (pending !== void 0) {
4658
4684
  this._pending = void 0;
@@ -4661,6 +4687,14 @@ var SettleLoop = class {
4661
4687
  });
4662
4688
  }, debounceMs);
4663
4689
  }
4690
+ /**
4691
+ * The cycle currently in flight, or `undefined` when idle (#1468). A
4692
+ * graceful shutdown awaits this alongside the drain controllers so a
4693
+ * settle parked in `correlate` does not resume after teardown.
4694
+ */
4695
+ get inflight() {
4696
+ return this._inflight;
4697
+ }
4664
4698
  /** Cancel any pending or active settle cycle. Idempotent. */
4665
4699
  stop() {
4666
4700
  this._pending = void 0;
@@ -5203,10 +5237,12 @@ var Act = class {
5203
5237
  const running = [...this._drain_controllers.values()].filter(
5204
5238
  (c) => c.inflight !== void 0
5205
5239
  );
5206
- if (running.length === 0) return;
5240
+ const settling = this._settle.inflight;
5241
+ if (running.length === 0 && !settling) return;
5207
5242
  const grace = grace_ms ?? this._derive_grace_ms(running);
5208
5243
  if (grace <= 0) return;
5209
5244
  const inflight = running.map((c) => c.inflight);
5245
+ if (settling) inflight.push(settling);
5210
5246
  let timer;
5211
5247
  const budget = new Promise((resolve) => {
5212
5248
  timer = setTimeout(resolve, grace);
@@ -6158,6 +6194,14 @@ function act() {
6158
6194
  autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
6159
6195
  };
6160
6196
  const pending_projections = [];
6197
+ const projection_reactions = /* @__PURE__ */ new Set();
6198
+ const record_projection_reactions = (proj) => {
6199
+ for (const register of Object.values(
6200
+ proj.events
6201
+ ))
6202
+ for (const reaction of register.reactions.values())
6203
+ projection_reactions.add(reaction);
6204
+ };
6161
6205
  const fold_projections = [];
6162
6206
  const batch_handlers = /* @__PURE__ */ new Map();
6163
6207
  const fold_specs = [];
@@ -6254,6 +6298,7 @@ function act() {
6254
6298
  return builder;
6255
6299
  },
6256
6300
  withProjection: (proj) => {
6301
+ record_projection_reactions(proj);
6257
6302
  merge_projection(proj, registry.events);
6258
6303
  register_batch_handler(proj, batch_handlers);
6259
6304
  if (proj.fold)
@@ -6271,6 +6316,7 @@ function act() {
6271
6316
  const patch_fn = options?.validateFoldedState === true ? validating_patch : bare_patch;
6272
6317
  if (!_built) {
6273
6318
  for (const proj of pending_projections) {
6319
+ record_projection_reactions(proj);
6274
6320
  merge_projection(proj, registry.events);
6275
6321
  register_batch_handler(proj, batch_handlers);
6276
6322
  if (proj.fold) fold_projections.push(proj);
@@ -6289,17 +6335,31 @@ function act() {
6289
6335
  throw new Error(
6290
6336
  `State projection "${proj.target}" of "${fold.name}" is missing events ${missing.join(", ")} \u2014 pass every partial of the state to .of()`
6291
6337
  );
6292
- if (batch_handlers.has(proj.target) || fold_specs.some((s) => s.target === proj.target))
6338
+ const claimed = fold_specs.find((s) => s.target === proj.target);
6339
+ if (claimed?.projection === proj) continue;
6340
+ if (batch_handlers.has(proj.target) || claimed)
6293
6341
  throw new Error(
6294
6342
  `Duplicate projection target "${proj.target}" \u2014 a target is served by one batch handler or one state projection, never both`
6295
6343
  );
6296
6344
  fold_specs.push({
6297
6345
  target: proj.target,
6346
+ projection: proj,
6298
6347
  merged,
6299
6348
  flush: fold.flush,
6300
6349
  config: fold.config
6301
6350
  });
6302
6351
  }
6352
+ for (const register of Object.values(registry.events)) {
6353
+ for (const reaction of register.reactions.values()) {
6354
+ if (typeof reaction.resolver === "function") continue;
6355
+ const target = reaction.resolver.target;
6356
+ const claimed = batch_handlers.has(target) || fold_specs.some((f) => f.target === target);
6357
+ if (claimed && !projection_reactions.has(reaction))
6358
+ throw new Error(
6359
+ `Reaction on target "${target}" conflicts with the projection that already serves it \u2014 a target is served by one batch handler or one state projection, and a reaction to it would never run`
6360
+ );
6361
+ }
6362
+ }
6303
6363
  finalize_deprecations();
6304
6364
  validate_lane_references(registry, lanes);
6305
6365
  for (const [event_name, reg] of Object.entries(