@rotorsoft/act 1.19.0 → 1.20.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
@@ -1556,7 +1556,7 @@ var DEFAULT_BATCH = 500;
1556
1556
  async function snap(snapshot) {
1557
1557
  const { id, stream, name, meta, version } = snapshot.event;
1558
1558
  try {
1559
- await store().commit(
1559
+ const [committed] = await store().commit(
1560
1560
  stream,
1561
1561
  [{ name: SNAP_EVENT, data: snapshot.state }],
1562
1562
  {
@@ -1566,6 +1566,7 @@ async function snap(snapshot) {
1566
1566
  version
1567
1567
  // IMPORTANT! - state events are committed right after the snapshot event
1568
1568
  );
1569
+ return committed;
1569
1570
  } catch (error) {
1570
1571
  const reason = error instanceof Error ? error.message : String(error);
1571
1572
  log().warn(
@@ -1778,7 +1779,7 @@ async function action(me, action2, target, payload, options) {
1778
1779
  const snapshot = await load(me, { stream, actor: target.actor });
1779
1780
  if (snapshot.event?.name === TOMBSTONE_EVENT)
1780
1781
  throw new StreamClosedError(stream);
1781
- const expected = expectedVersion ?? snapshot.event?.version;
1782
+ const expected = expectedVersion ?? (snapshot.version >= 0 ? snapshot.version : void 0);
1782
1783
  if (me.given) {
1783
1784
  const invariants = me.given[action2] || [];
1784
1785
  invariants.forEach(({ valid: valid2, description }) => {
@@ -1860,18 +1861,22 @@ async function action(me, action2, target, payload, options) {
1860
1861
  };
1861
1862
  });
1862
1863
  const last = snapshots.at(-1);
1863
- const snapped = me.snap?.(last);
1864
+ const contiguous = committed[0].version === snapshot.version + 1;
1865
+ const snapped = contiguous && me.snap?.(last);
1866
+ const snap_event = snapped ? await snap(last) : void 0;
1864
1867
  if (!me.pii_aware) {
1865
- cache().set(stream, {
1866
- stream,
1867
- state: last.state,
1868
- version: last.event.version,
1869
- event_id: last.event.id,
1870
- patches: snapped ? 0 : last.patches,
1871
- snaps: snapped ? last.snaps + 1 : last.snaps
1872
- }).catch((err) => log().error(err));
1868
+ if (contiguous)
1869
+ cache().set(stream, {
1870
+ stream,
1871
+ state: last.state,
1872
+ version: snap_event?.version ?? last.event.version,
1873
+ event_id: snap_event?.id ?? last.event.id,
1874
+ patches: snap_event ? 0 : last.patches,
1875
+ snaps: snap_event ? last.snaps + 1 : last.snaps
1876
+ }).catch((err) => log().error(err));
1877
+ else
1878
+ cache().invalidate(stream).catch((err) => log().error(err));
1873
1879
  }
1874
- if (snapped) void snap(last);
1875
1880
  return snapshots;
1876
1881
  } catch (error) {
1877
1882
  if (!(error instanceof ConcurrencyError)) throw error;
@@ -3740,6 +3745,7 @@ function act() {
3740
3745
  autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
3741
3746
  };
3742
3747
  const pending_projections = [];
3748
+ const fold_projections = [];
3743
3749
  const batch_handlers = /* @__PURE__ */ new Map();
3744
3750
  const lanes = [];
3745
3751
  let _built = false;
@@ -3805,6 +3811,8 @@ function act() {
3805
3811
  withProjection: (proj) => {
3806
3812
  merge_projection(proj, registry.events);
3807
3813
  register_batch_handler(proj, batch_handlers);
3814
+ if (proj.fold)
3815
+ fold_projections.push(proj);
3808
3816
  return builder;
3809
3817
  },
3810
3818
  withActor: () => builder,
@@ -3818,6 +3826,26 @@ function act() {
3818
3826
  for (const proj of pending_projections) {
3819
3827
  merge_projection(proj, registry.events);
3820
3828
  register_batch_handler(proj, batch_handlers);
3829
+ if (proj.fold) fold_projections.push(proj);
3830
+ }
3831
+ for (const proj of fold_projections) {
3832
+ const fold = proj.fold;
3833
+ const merged = states.get(fold.name);
3834
+ if (!merged)
3835
+ throw new Error(
3836
+ `State projection "${proj.target}" folds "${fold.name}", which is not registered \u2014 add it via withState/withSlice before build()`
3837
+ );
3838
+ const missing = Object.keys(merged.events).filter(
3839
+ (event_name) => !(event_name in proj.events)
3840
+ );
3841
+ if (missing.length > 0)
3842
+ throw new Error(
3843
+ `State projection "${proj.target}" of "${fold.name}" is missing events ${missing.join(", ")} \u2014 pass every partial of the state to .of()`
3844
+ );
3845
+ batch_handlers.set(
3846
+ proj.target,
3847
+ make_fold_handler(merged, fold.flush, fold.config)
3848
+ );
3821
3849
  }
3822
3850
  finalize_deprecations();
3823
3851
  validate_lane_references(registry, lanes);
@@ -3960,39 +3988,52 @@ function _projection(target) {
3960
3988
  batchHandler: handler
3961
3989
  })
3962
3990
  }),
3963
- of: (state2, options = {}) => {
3991
+ of: (...args) => {
3964
3992
  if (Object.keys(events).length > 0)
3965
3993
  throw new Error(
3966
3994
  `Projection "${target}" mixes .of() with .on() handlers \u2014 a projection either folds a state or declares handlers, never both`
3967
3995
  );
3996
+ const is_state = (a) => !!a && typeof a.init === "function";
3997
+ const partials = args.filter(is_state);
3998
+ const options = args.find((a) => !is_state(a)) ?? {};
3999
+ if (partials.length === 0)
4000
+ throw new Error(`Projection "${target}" .of() requires a state`);
4001
+ const name = partials[0].name;
4002
+ for (const partial of partials)
4003
+ if (partial.name !== name)
4004
+ throw new Error(
4005
+ `Projection "${target}" .of() partials must share one state name \u2014 got "${name}" and "${partial.name}"`
4006
+ );
3968
4007
  const config2 = resolveFoldConfig(options);
3969
4008
  const fold_events = {};
3970
- for (const [name, schema] of Object.entries(state2.events)) {
3971
- const noop = {
3972
- [`${target}_fold`]: async () => {
3973
- }
3974
- }[`${target}_fold`];
3975
- fold_events[name] = {
3976
- schema,
3977
- reactions: /* @__PURE__ */ new Map([
3978
- [
3979
- `${target}_fold`,
3980
- {
3981
- handler: noop,
3982
- resolver: { target },
3983
- options: { blockOnError: true, maxRetries: 3 }
3984
- }
3985
- ]
3986
- ])
3987
- };
3988
- }
4009
+ for (const partial of partials)
4010
+ for (const [event_name, schema] of Object.entries(partial.events)) {
4011
+ if (event_name in fold_events) continue;
4012
+ const noop = {
4013
+ [`${target}_fold`]: async () => {
4014
+ }
4015
+ }[`${target}_fold`];
4016
+ fold_events[event_name] = {
4017
+ schema,
4018
+ reactions: /* @__PURE__ */ new Map([
4019
+ [
4020
+ `${target}_fold`,
4021
+ {
4022
+ handler: noop,
4023
+ resolver: { target },
4024
+ options: { blockOnError: true, maxRetries: 3 }
4025
+ }
4026
+ ]
4027
+ ])
4028
+ };
4029
+ }
3989
4030
  return {
3990
4031
  flush: (handler) => ({
3991
4032
  build: () => ({
3992
4033
  _tag: "Projection",
3993
4034
  events: fold_events,
3994
4035
  target,
3995
- batchHandler: make_fold_handler(state2, handler, config2)
4036
+ fold: { name, flush: handler, config: config2 }
3996
4037
  })
3997
4038
  })
3998
4039
  };