@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.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  sleep,
23
23
  store,
24
24
  validate
25
- } from "./chunk-NHJQCCBS.js";
25
+ } from "./chunk-BAIMGDI4.js";
26
26
  import {
27
27
  ActorSchema,
28
28
  CausationEventSchema,
@@ -1693,8 +1693,10 @@ var CorrelateCycle = class {
1693
1693
  this._initialized = true;
1694
1694
  }
1695
1695
  async _run_init() {
1696
- const { watermark } = await store().subscribe([...this._static_targets]);
1697
- this._checkpoint = this._has_dynamic_resolvers ? Math.max(-1, watermark - this._cold_start_back_scan) : watermark;
1696
+ const { watermark, correlated_at } = await store().subscribe([
1697
+ ...this._static_targets
1698
+ ]);
1699
+ this._checkpoint = !this._has_dynamic_resolvers ? watermark : correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
1698
1700
  this._on_init?.();
1699
1701
  for (const { stream } of this._static_targets) {
1700
1702
  this._subscribed.set(stream, Number.POSITIVE_INFINITY);
@@ -1774,7 +1776,7 @@ var CorrelateCycle = class {
1774
1776
  lane
1775
1777
  })
1776
1778
  );
1777
- const { subscribed } = await this._cd.subscribe(streams);
1779
+ const { subscribed } = await this._cd.subscribe(streams, last_id);
1778
1780
  this._checkpoint = last_id;
1779
1781
  if (subscribed) {
1780
1782
  for (const { stream, priority } of streams) {
@@ -1960,7 +1962,7 @@ async function fetch(leased, eventLimit) {
1960
1962
  }
1961
1963
  var ack = (leases) => store().ack(leases);
1962
1964
  var block = (leases) => store().block(leases);
1963
- var subscribe = (streams) => store().subscribe(streams);
1965
+ var subscribe = (streams, correlated_at) => store().subscribe(streams, correlated_at);
1964
1966
 
1965
1967
  // src/internal/event-sourcing.ts
1966
1968
  import { patch } from "@rotorsoft/act-patch";
@@ -3022,6 +3024,19 @@ function build_handle_batch(logger) {
3022
3024
  var SettleLoop = class {
3023
3025
  _timer = void 0;
3024
3026
  _running = false;
3027
+ /**
3028
+ * Resolves when the cycle currently in flight finishes; `undefined` when
3029
+ * idle (#1468). `_running` answers "is a cycle running?" for the
3030
+ * re-arm bookkeeping; this answers "tell me when it is done" for a
3031
+ * graceful shutdown.
3032
+ *
3033
+ * `stop()` only cancels *scheduling* — a cycle already inside its
3034
+ * correlate → drain loop keeps going, and because `DrainController.drain`
3035
+ * does not consult `_stopped`, it can claim a stream after teardown
3036
+ * returned and after the store adapter was disposed. Never rejects: the
3037
+ * cycle's own `catch` contains its errors, so awaiting this is safe.
3038
+ */
3039
+ _inflight;
3025
3040
  /**
3026
3041
  * Set when a `schedule()` timer fires while a cycle is still running
3027
3042
  * (ACT-1205). The in-flight cycle's `finally` re-arms one more pass so
@@ -3061,6 +3076,10 @@ var SettleLoop = class {
3061
3076
  return;
3062
3077
  }
3063
3078
  this._running = true;
3079
+ let settle_done;
3080
+ this._inflight = new Promise((done) => {
3081
+ settle_done = done;
3082
+ });
3064
3083
  (async () => {
3065
3084
  await this._deps.init();
3066
3085
  let settled_drain;
@@ -3086,6 +3105,8 @@ var SettleLoop = class {
3086
3105
  this._deps.breaker.failed(Date.now(), err);
3087
3106
  }).finally(() => {
3088
3107
  this._running = false;
3108
+ this._inflight = void 0;
3109
+ settle_done();
3089
3110
  const pending = this._pending;
3090
3111
  if (pending !== void 0) {
3091
3112
  this._pending = void 0;
@@ -3094,6 +3115,14 @@ var SettleLoop = class {
3094
3115
  });
3095
3116
  }, debounceMs);
3096
3117
  }
3118
+ /**
3119
+ * The cycle currently in flight, or `undefined` when idle (#1468). A
3120
+ * graceful shutdown awaits this alongside the drain controllers so a
3121
+ * settle parked in `correlate` does not resume after teardown.
3122
+ */
3123
+ get inflight() {
3124
+ return this._inflight;
3125
+ }
3097
3126
  /** Cancel any pending or active settle cycle. Idempotent. */
3098
3127
  stop() {
3099
3128
  this._pending = void 0;
@@ -3636,10 +3665,12 @@ var Act = class {
3636
3665
  const running = [...this._drain_controllers.values()].filter(
3637
3666
  (c) => c.inflight !== void 0
3638
3667
  );
3639
- if (running.length === 0) return;
3668
+ const settling = this._settle.inflight;
3669
+ if (running.length === 0 && !settling) return;
3640
3670
  const grace = grace_ms ?? this._derive_grace_ms(running);
3641
3671
  if (grace <= 0) return;
3642
3672
  const inflight = running.map((c) => c.inflight);
3673
+ if (settling) inflight.push(settling);
3643
3674
  let timer;
3644
3675
  const budget = new Promise((resolve) => {
3645
3676
  timer = setTimeout(resolve, grace);
@@ -4591,6 +4622,14 @@ function act() {
4591
4622
  autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
4592
4623
  };
4593
4624
  const pending_projections = [];
4625
+ const projection_reactions = /* @__PURE__ */ new Set();
4626
+ const record_projection_reactions = (proj) => {
4627
+ for (const register of Object.values(
4628
+ proj.events
4629
+ ))
4630
+ for (const reaction of register.reactions.values())
4631
+ projection_reactions.add(reaction);
4632
+ };
4594
4633
  const fold_projections = [];
4595
4634
  const batch_handlers = /* @__PURE__ */ new Map();
4596
4635
  const fold_specs = [];
@@ -4687,6 +4726,7 @@ function act() {
4687
4726
  return builder;
4688
4727
  },
4689
4728
  withProjection: (proj) => {
4729
+ record_projection_reactions(proj);
4690
4730
  merge_projection(proj, registry.events);
4691
4731
  register_batch_handler(proj, batch_handlers);
4692
4732
  if (proj.fold)
@@ -4704,6 +4744,7 @@ function act() {
4704
4744
  const patch_fn = options?.validateFoldedState === true ? validating_patch : bare_patch;
4705
4745
  if (!_built) {
4706
4746
  for (const proj of pending_projections) {
4747
+ record_projection_reactions(proj);
4707
4748
  merge_projection(proj, registry.events);
4708
4749
  register_batch_handler(proj, batch_handlers);
4709
4750
  if (proj.fold) fold_projections.push(proj);
@@ -4722,17 +4763,31 @@ function act() {
4722
4763
  throw new Error(
4723
4764
  `State projection "${proj.target}" of "${fold.name}" is missing events ${missing.join(", ")} \u2014 pass every partial of the state to .of()`
4724
4765
  );
4725
- if (batch_handlers.has(proj.target) || fold_specs.some((s) => s.target === proj.target))
4766
+ const claimed = fold_specs.find((s) => s.target === proj.target);
4767
+ if (claimed?.projection === proj) continue;
4768
+ if (batch_handlers.has(proj.target) || claimed)
4726
4769
  throw new Error(
4727
4770
  `Duplicate projection target "${proj.target}" \u2014 a target is served by one batch handler or one state projection, never both`
4728
4771
  );
4729
4772
  fold_specs.push({
4730
4773
  target: proj.target,
4774
+ projection: proj,
4731
4775
  merged,
4732
4776
  flush: fold.flush,
4733
4777
  config: fold.config
4734
4778
  });
4735
4779
  }
4780
+ for (const register of Object.values(registry.events)) {
4781
+ for (const reaction of register.reactions.values()) {
4782
+ if (typeof reaction.resolver === "function") continue;
4783
+ const target = reaction.resolver.target;
4784
+ const claimed = batch_handlers.has(target) || fold_specs.some((f) => f.target === target);
4785
+ if (claimed && !projection_reactions.has(reaction))
4786
+ throw new Error(
4787
+ `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`
4788
+ );
4789
+ }
4790
+ }
4736
4791
  finalize_deprecations();
4737
4792
  validate_lane_references(registry, lanes);
4738
4793
  for (const [event_name, reg] of Object.entries(