@rotorsoft/act 1.25.1 → 1.25.3

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
@@ -2806,6 +2806,7 @@ var DrainController = class {
2806
2806
  };
2807
2807
 
2808
2808
  // src/internal/projection-fold.ts
2809
+ var FOLD_RESET = /* @__PURE__ */ Symbol("act.fold.reset");
2809
2810
  function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_fields) {
2810
2811
  const load_me = {
2811
2812
  ...me,
@@ -2829,7 +2830,27 @@ function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_
2829
2830
  await flush(rows);
2830
2831
  for (const f of flushed) f.dirty = false;
2831
2832
  };
2832
- return async (events) => {
2833
+ const first_sight = async (stream) => {
2834
+ if (cache2.size >= config2.maxCachedStates) {
2835
+ const oldest = cache2.keys().next().value;
2836
+ const evictee = cache2.get(oldest);
2837
+ if (evictee.dirty) await flush([row(evictee)]);
2838
+ cache2.delete(oldest);
2839
+ }
2840
+ const snapshot = await load(load_me, { stream }, void 0, patch_fn);
2841
+ const seated = {
2842
+ stream,
2843
+ state: snapshot.state,
2844
+ version: snapshot.version,
2845
+ event_id: snapshot.id,
2846
+ patches: snapshot.patches,
2847
+ snaps: snapshot.snaps,
2848
+ dirty: true
2849
+ };
2850
+ cache2.set(stream, seated);
2851
+ return seated;
2852
+ };
2853
+ const handler = async (events) => {
2833
2854
  let folded_since_flush = 0;
2834
2855
  for (const event of events) {
2835
2856
  const stream = event.stream;
@@ -2838,23 +2859,12 @@ function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_
2838
2859
  cache2.delete(stream);
2839
2860
  cache2.set(stream, fold);
2840
2861
  } else {
2841
- if (cache2.size >= config2.maxCachedStates) {
2842
- const oldest = cache2.keys().next().value;
2843
- const evictee = cache2.get(oldest);
2844
- if (evictee.dirty) await flush([row(evictee)]);
2845
- cache2.delete(oldest);
2846
- }
2847
- const snapshot = await load(load_me, { stream }, void 0, patch_fn);
2848
- fold = {
2849
- stream,
2850
- state: snapshot.state,
2851
- version: snapshot.version,
2852
- event_id: snapshot.id,
2853
- patches: snapshot.patches,
2854
- snaps: snapshot.snaps,
2855
- dirty: true
2856
- };
2857
- cache2.set(stream, fold);
2862
+ fold = await first_sight(stream);
2863
+ }
2864
+ const contiguous = event.version === fold.version + 1;
2865
+ if (event.id > fold.event_id && !contiguous) {
2866
+ cache2.delete(stream);
2867
+ fold = await first_sight(stream);
2858
2868
  }
2859
2869
  if (event.id > fold.event_id) {
2860
2870
  const reducer = me.patch[event.name];
@@ -2878,6 +2888,8 @@ function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_
2878
2888
  }
2879
2889
  await flush_dirty();
2880
2890
  };
2891
+ Object.defineProperty(handler, FOLD_RESET, { value: () => cache2.clear() });
2892
+ return handler;
2881
2893
  }
2882
2894
 
2883
2895
  // src/internal/reactions.ts
@@ -3010,6 +3022,19 @@ function build_handle_batch(logger) {
3010
3022
  var SettleLoop = class {
3011
3023
  _timer = void 0;
3012
3024
  _running = false;
3025
+ /**
3026
+ * Resolves when the cycle currently in flight finishes; `undefined` when
3027
+ * idle (#1468). `_running` answers "is a cycle running?" for the
3028
+ * re-arm bookkeeping; this answers "tell me when it is done" for a
3029
+ * graceful shutdown.
3030
+ *
3031
+ * `stop()` only cancels *scheduling* — a cycle already inside its
3032
+ * correlate → drain loop keeps going, and because `DrainController.drain`
3033
+ * does not consult `_stopped`, it can claim a stream after teardown
3034
+ * returned and after the store adapter was disposed. Never rejects: the
3035
+ * cycle's own `catch` contains its errors, so awaiting this is safe.
3036
+ */
3037
+ _inflight;
3013
3038
  /**
3014
3039
  * Set when a `schedule()` timer fires while a cycle is still running
3015
3040
  * (ACT-1205). The in-flight cycle's `finally` re-arms one more pass so
@@ -3049,6 +3074,10 @@ var SettleLoop = class {
3049
3074
  return;
3050
3075
  }
3051
3076
  this._running = true;
3077
+ let settle_done;
3078
+ this._inflight = new Promise((done) => {
3079
+ settle_done = done;
3080
+ });
3052
3081
  (async () => {
3053
3082
  await this._deps.init();
3054
3083
  let settled_drain;
@@ -3074,6 +3103,8 @@ var SettleLoop = class {
3074
3103
  this._deps.breaker.failed(Date.now(), err);
3075
3104
  }).finally(() => {
3076
3105
  this._running = false;
3106
+ this._inflight = void 0;
3107
+ settle_done();
3077
3108
  const pending = this._pending;
3078
3109
  if (pending !== void 0) {
3079
3110
  this._pending = void 0;
@@ -3082,6 +3113,14 @@ var SettleLoop = class {
3082
3113
  });
3083
3114
  }, debounceMs);
3084
3115
  }
3116
+ /**
3117
+ * The cycle currently in flight, or `undefined` when idle (#1468). A
3118
+ * graceful shutdown awaits this alongside the drain controllers so a
3119
+ * settle parked in `correlate` does not resume after teardown.
3120
+ */
3121
+ get inflight() {
3122
+ return this._inflight;
3123
+ }
3085
3124
  /** Cancel any pending or active settle cycle. Idempotent. */
3086
3125
  stop() {
3087
3126
  this._pending = void 0;
@@ -3624,10 +3663,12 @@ var Act = class {
3624
3663
  const running = [...this._drain_controllers.values()].filter(
3625
3664
  (c) => c.inflight !== void 0
3626
3665
  );
3627
- if (running.length === 0) return;
3666
+ const settling = this._settle.inflight;
3667
+ if (running.length === 0 && !settling) return;
3628
3668
  const grace = grace_ms ?? this._derive_grace_ms(running);
3629
3669
  if (grace <= 0) return;
3630
3670
  const inflight = running.map((c) => c.inflight);
3671
+ if (settling) inflight.push(settling);
3631
3672
  let timer;
3632
3673
  const budget = new Promise((resolve) => {
3633
3674
  timer = setTimeout(resolve, grace);
@@ -4179,6 +4220,8 @@ var Act = class {
4179
4220
  async reset(input) {
4180
4221
  return this._scoped(async () => {
4181
4222
  const count = await store().reset(input);
4223
+ for (const handler of this._batch_handlers.values())
4224
+ handler[FOLD_RESET]?.();
4182
4225
  if (count > 0 && this._reactive_events.size > 0) this._arm_all();
4183
4226
  return count;
4184
4227
  });
@@ -4577,17 +4620,30 @@ function act() {
4577
4620
  autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
4578
4621
  };
4579
4622
  const pending_projections = [];
4623
+ const projection_reactions = /* @__PURE__ */ new Set();
4624
+ const record_projection_reactions = (proj) => {
4625
+ for (const register of Object.values(
4626
+ proj.events
4627
+ ))
4628
+ for (const reaction of register.reactions.values())
4629
+ projection_reactions.add(reaction);
4630
+ };
4580
4631
  const fold_projections = [];
4581
4632
  const batch_handlers = /* @__PURE__ */ new Map();
4582
4633
  const fold_specs = [];
4583
4634
  const lanes = [];
4584
4635
  let _built = false;
4585
- const pii_wrap = (original) => async (events, stream) => {
4586
- const stripped = events.map((e) => {
4587
- const f = _sf.get(e.name);
4588
- return f ? pii_strip(e, f) : e;
4589
- });
4590
- return original(stripped, stream);
4636
+ const pii_wrap = (original) => {
4637
+ const wrapped = async (events, stream) => {
4638
+ const stripped = events.map((e) => {
4639
+ const f = _sf.get(e.name);
4640
+ return f ? pii_strip(e, f) : e;
4641
+ });
4642
+ return original(stripped, stream);
4643
+ };
4644
+ const reset = original[FOLD_RESET];
4645
+ if (reset) Object.defineProperty(wrapped, FOLD_RESET, { value: reset });
4646
+ return wrapped;
4591
4647
  };
4592
4648
  const make_batch_handlers = (patch_fn) => {
4593
4649
  const handlers = new Map(batch_handlers);
@@ -4668,6 +4724,7 @@ function act() {
4668
4724
  return builder;
4669
4725
  },
4670
4726
  withProjection: (proj) => {
4727
+ record_projection_reactions(proj);
4671
4728
  merge_projection(proj, registry.events);
4672
4729
  register_batch_handler(proj, batch_handlers);
4673
4730
  if (proj.fold)
@@ -4685,6 +4742,7 @@ function act() {
4685
4742
  const patch_fn = options?.validateFoldedState === true ? validating_patch : bare_patch;
4686
4743
  if (!_built) {
4687
4744
  for (const proj of pending_projections) {
4745
+ record_projection_reactions(proj);
4688
4746
  merge_projection(proj, registry.events);
4689
4747
  register_batch_handler(proj, batch_handlers);
4690
4748
  if (proj.fold) fold_projections.push(proj);
@@ -4703,17 +4761,31 @@ function act() {
4703
4761
  throw new Error(
4704
4762
  `State projection "${proj.target}" of "${fold.name}" is missing events ${missing.join(", ")} \u2014 pass every partial of the state to .of()`
4705
4763
  );
4706
- if (batch_handlers.has(proj.target) || fold_specs.some((s) => s.target === proj.target))
4764
+ const claimed = fold_specs.find((s) => s.target === proj.target);
4765
+ if (claimed?.projection === proj) continue;
4766
+ if (batch_handlers.has(proj.target) || claimed)
4707
4767
  throw new Error(
4708
4768
  `Duplicate projection target "${proj.target}" \u2014 a target is served by one batch handler or one state projection, never both`
4709
4769
  );
4710
4770
  fold_specs.push({
4711
4771
  target: proj.target,
4772
+ projection: proj,
4712
4773
  merged,
4713
4774
  flush: fold.flush,
4714
4775
  config: fold.config
4715
4776
  });
4716
4777
  }
4778
+ for (const register of Object.values(registry.events)) {
4779
+ for (const reaction of register.reactions.values()) {
4780
+ if (typeof reaction.resolver === "function") continue;
4781
+ const target = reaction.resolver.target;
4782
+ const claimed = batch_handlers.has(target) || fold_specs.some((f) => f.target === target);
4783
+ if (claimed && !projection_reactions.has(reaction))
4784
+ throw new Error(
4785
+ `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`
4786
+ );
4787
+ }
4788
+ }
4717
4789
  finalize_deprecations();
4718
4790
  validate_lane_references(registry, lanes);
4719
4791
  for (const [event_name, reg] of Object.entries(