@rotorsoft/act 1.34.3 → 1.35.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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/act.d.ts +11 -8
  4. package/dist/@types/act.d.ts.map +1 -1
  5. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  6. package/dist/@types/builders/state-builder.d.ts +1 -1
  7. package/dist/@types/internal/correlate-cycle.d.ts +4 -2
  8. package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
  9. package/dist/@types/internal/event-sourcing.d.ts +1 -6
  10. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  11. package/dist/@types/internal/projection-fold.d.ts +1 -1
  12. package/dist/@types/internal/projection-fold.d.ts.map +1 -1
  13. package/dist/@types/internal/tracing.d.ts.map +1 -1
  14. package/dist/@types/types/act.d.ts +5 -21
  15. package/dist/@types/types/act.d.ts.map +1 -1
  16. package/dist/@types/types/action.d.ts +34 -16
  17. package/dist/@types/types/action.d.ts.map +1 -1
  18. package/dist/@types/types/builders.d.ts +26 -30
  19. package/dist/@types/types/builders.d.ts.map +1 -1
  20. package/dist/@types/types/errors.d.ts +56 -29
  21. package/dist/@types/types/errors.d.ts.map +1 -1
  22. package/dist/{chunk-GBAMBUDN.cjs → chunk-BNTVE7RQ.cjs} +13 -13
  23. package/dist/{chunk-GBAMBUDN.cjs.map → chunk-BNTVE7RQ.cjs.map} +1 -1
  24. package/dist/{chunk-BKCNU6FF.js → chunk-DH2HW5XJ.js} +13 -4
  25. package/dist/chunk-DH2HW5XJ.js.map +1 -0
  26. package/dist/{chunk-RF5MV72K.cjs → chunk-L66OOP2F.cjs} +14 -5
  27. package/dist/chunk-L66OOP2F.cjs.map +1 -0
  28. package/dist/{chunk-IEM2NS6J.js → chunk-MHPUVTMX.js} +2 -2
  29. package/dist/index.cjs +233 -231
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +76 -74
  32. package/dist/index.js.map +1 -1
  33. package/dist/test/index.cjs +4 -4
  34. package/dist/test/index.js +2 -2
  35. package/dist/types/index.cjs +4 -2
  36. package/dist/types/index.cjs.map +1 -1
  37. package/dist/types/index.js +3 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-BKCNU6FF.js.map +0 -1
  40. package/dist/chunk-RF5MV72K.cjs.map +0 -1
  41. /package/dist/{chunk-IEM2NS6J.js.map → chunk-MHPUVTMX.js.map} +0 -0
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  sleep,
41
41
  store,
42
42
  validate
43
- } from "./chunk-IEM2NS6J.js";
43
+ } from "./chunk-MHPUVTMX.js";
44
44
  import {
45
45
  ActorSchema,
46
46
  CausationEventSchema,
@@ -57,6 +57,7 @@ import {
57
57
  InvariantError,
58
58
  LogLevels,
59
59
  NonRetryableError,
60
+ PreconditionError,
60
61
  QuerySchema,
61
62
  REDACTED,
62
63
  SHREDDED,
@@ -70,7 +71,7 @@ import {
70
71
  pii_fields,
71
72
  pii_schemas,
72
73
  sensitive
73
- } from "./chunk-BKCNU6FF.js";
74
+ } from "./chunk-DH2HW5XJ.js";
74
75
 
75
76
  // src/signals.ts
76
77
  process.once("SIGINT", async (arg) => {
@@ -1265,9 +1266,9 @@ var CorrelateCycle = class {
1265
1266
  return pattern.test(stream);
1266
1267
  }
1267
1268
  /** Scan past the checkpoint, subscribe new targets and raise their work marks. */
1268
- async correlate(query, lease = false) {
1269
+ async correlate(limit, lease = false) {
1269
1270
  if (this._restoring) return { kind: "idle", last_id: this._checkpoint };
1270
- const pass = this._scan(query, lease);
1271
+ const pass = this._scan(limit, lease);
1271
1272
  this._passes.add(pass);
1272
1273
  try {
1273
1274
  return await pass;
@@ -1275,7 +1276,7 @@ var CorrelateCycle = class {
1275
1276
  this._passes.delete(pass);
1276
1277
  }
1277
1278
  }
1278
- async _scan(query, lease) {
1279
+ async _scan(limit, lease) {
1279
1280
  await this._init();
1280
1281
  const epoch = this._epoch;
1281
1282
  const idle = () => ({
@@ -1370,7 +1371,7 @@ var CorrelateCycle = class {
1370
1371
  },
1371
1372
  // Resolvers see `pii: null`: correlate runs actor-less, and one
1372
1373
  // undecryptable row would otherwise stop every stream's reactions.
1373
- { after, limit: query.limit, with_pii: false }
1374
+ { after, limit, with_pii: false }
1374
1375
  );
1375
1376
  if (this._declared_lanes.size > 1) await this._recall(correlated);
1376
1377
  const read = last_id > after;
@@ -1409,7 +1410,7 @@ var CorrelateCycle = class {
1409
1410
  return { kind: "scanned", last_id, subscribed, marked, read };
1410
1411
  }
1411
1412
  this._checkpoint = last_id;
1412
- if (!stopped && found !== query.limit) this._lowered = raised;
1413
+ if (!stopped && found !== limit) this._lowered = raised;
1413
1414
  return { kind: "scanned", last_id, subscribed: 0, marked: 0, read };
1414
1415
  }
1415
1416
  /**
@@ -1647,14 +1648,12 @@ var subscribe = (streams, correlated_at, correlator) => store().subscribe(stream
1647
1648
 
1648
1649
  // src/internal/event-sourcing.ts
1649
1650
  import { patch } from "@rotorsoft/act-patch";
1651
+ import { prettifyError } from "zod";
1650
1652
  var bare_patch = (_me, state2, partial) => patch(state2, partial);
1651
- var validating_patch = (me, state2, partial, event) => {
1652
- const next = patch(state2, partial);
1653
- return validate(
1654
- `${me.name}.${String(event.name)}#${event.id}`,
1655
- next,
1656
- me.state
1657
- );
1653
+ var check_invariants = (target, state2, schema) => {
1654
+ const result = schema.safeParse(state2);
1655
+ if (!result.success)
1656
+ throw new ValidationError(target, state2, prettifyError(result.error));
1658
1657
  };
1659
1658
  var DEFAULT_BATCH = 500;
1660
1659
  var cache_put = (stream, entry) => cache().set(stream, entry).catch(
@@ -1911,8 +1910,8 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
1911
1910
  if (snapshot.event?.name === TOMBSTONE_EVENT)
1912
1911
  throw new StreamClosedError(stream);
1913
1912
  const expected = expectedVersion ?? (snapshot.version >= 0 ? snapshot.version : void 0);
1914
- for (const { valid: valid2, description } of me.given?.[action2] ?? [])
1915
- if (!valid2(snapshot.state, actor))
1913
+ for (const { valid, description } of me.given?.[action2] ?? [])
1914
+ if (!valid(snapshot.state, actor))
1916
1915
  throw new InvariantError(
1917
1916
  action2,
1918
1917
  validated,
@@ -1922,25 +1921,22 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
1922
1921
  );
1923
1922
  for (const { holds, description } of me.requires?.[action2] ?? [])
1924
1923
  if (!holds(validated, snapshot.state, actor))
1925
- throw new InvariantError(
1924
+ throw new PreconditionError(
1926
1925
  action2,
1927
1926
  validated,
1928
1927
  target,
1929
1928
  snapshot,
1930
- description,
1931
- "precondition"
1929
+ description
1932
1930
  );
1933
1931
  const result = me.on[action2](validated, snapshot, target);
1934
- if (!result) return [snapshot];
1935
- if (Array.isArray(result) && result.length === 0) {
1932
+ if (!result || Array.isArray(result) && !result.length)
1936
1933
  return [snapshot];
1937
- }
1938
1934
  const tuples = Array.isArray(result[0]) ? result : [result];
1939
- const valid = tuples.map(([name, data]) => ({
1935
+ const events = tuples.map(([name, data]) => ({
1940
1936
  name,
1941
1937
  data: validate(name, data, me.events[name])
1942
1938
  }));
1943
- const emitted = valid.map((e) => me.message(e));
1939
+ const emitted = events.map((e) => me.message(e));
1944
1940
  const meta = {
1945
1941
  correlation: reactingTo?.meta.correlation || correlator({
1946
1942
  action: action2,
@@ -1961,6 +1957,23 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
1961
1957
  } : void 0
1962
1958
  }
1963
1959
  };
1960
+ let { state: state2, patches } = snapshot;
1961
+ const created = /* @__PURE__ */ new Date();
1962
+ const folded = events.map((e, i) => {
1963
+ const event = {
1964
+ ...e,
1965
+ id: -1,
1966
+ stream,
1967
+ version: snapshot.version + i + 1,
1968
+ created,
1969
+ meta
1970
+ };
1971
+ const patch2 = me.patch[event.name](event, state2);
1972
+ state2 = patch_fn(me, state2, patch2, event);
1973
+ patches++;
1974
+ return { state: state2, patch: patch2, patches };
1975
+ });
1976
+ check_invariants(`${me.name}.${action2}`, state2, me.state);
1964
1977
  let committed;
1965
1978
  try {
1966
1979
  committed = await store().commit(
@@ -1975,24 +1988,17 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
1975
1988
  if (error instanceof ConcurrencyError) void cache_drop(stream);
1976
1989
  throw error;
1977
1990
  }
1978
- let { state: state2, patches } = snapshot;
1979
- const snapshots = committed.map((row, i) => {
1980
- const event = { ...row, data: valid[i].data };
1981
- const p = me.patch[event.name](event, state2);
1982
- state2 = patch_fn(me, state2, p, event);
1983
- patches++;
1984
- return {
1985
- event,
1986
- state: state2,
1987
- version: event.version,
1988
- id: event.id,
1989
- patches,
1990
- snaps: snapshot.snaps,
1991
- patch: p,
1992
- cache_hit: snapshot.cache_hit,
1993
- replayed: snapshot.replayed
1994
- };
1995
- });
1991
+ const snapshots = committed.map((row, i) => ({
1992
+ event: { ...row, data: events[i].data },
1993
+ state: folded[i].state,
1994
+ version: row.version,
1995
+ id: row.id,
1996
+ patches: folded[i].patches,
1997
+ snaps: snapshot.snaps,
1998
+ patch: folded[i].patch,
1999
+ cache_hit: snapshot.cache_hit,
2000
+ replayed: snapshot.replayed
2001
+ }));
1996
2002
  const last = snapshots.at(-1);
1997
2003
  const contiguous = committed[0].version === snapshot.version + 1;
1998
2004
  const snapped = contiguous && me.snap?.(last);
@@ -2569,7 +2575,7 @@ function pii_strip(event, fields) {
2569
2575
 
2570
2576
  // src/internal/projection-fold.ts
2571
2577
  var FOLD_RESET = /* @__PURE__ */ Symbol("act.fold.reset");
2572
- function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_fields) {
2578
+ function make_fold_handler(me, flush, config2, patch_fn, sensitive_fields) {
2573
2579
  const load_me = {
2574
2580
  ...me,
2575
2581
  view: (raw, actor) => {
@@ -2933,10 +2939,8 @@ var SettleLoop = class {
2933
2939
  };
2934
2940
 
2935
2941
  // src/act.ts
2936
- function validate_only_lanes(options, lanes) {
2937
- if (!options.onlyLanes || options.onlyLanes.length === 0) return;
2938
- const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
2939
- const unknown = options.onlyLanes.filter((l) => !declared.has(l));
2942
+ function validate_only_lanes(only, declared) {
2943
+ const unknown = (only ?? []).filter((l) => !declared.includes(l));
2940
2944
  if (unknown.length > 0)
2941
2945
  throw new Error(
2942
2946
  `ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
@@ -3040,7 +3044,7 @@ var Act = class {
3040
3044
  this._batch_handlers = batch_handlers;
3041
3045
  this._lanes = lanes;
3042
3046
  this._declared_lanes = ["default", ...lanes.map((l) => l.name)];
3043
- validate_only_lanes(options, lanes);
3047
+ validate_only_lanes(options.onlyLanes, this._declared_lanes);
3044
3048
  this._ports = options.scoped ?? default_scope();
3045
3049
  const ports = this._ports;
3046
3050
  this._scoped = make_run_scoped({
@@ -3082,7 +3086,7 @@ var Act = class {
3082
3086
  handle,
3083
3087
  handle_batch
3084
3088
  );
3085
- this._advise_orphaned_lanes(options);
3089
+ this._advise_orphaned_lanes();
3086
3090
  this._audit_deps = this._build_audit_deps();
3087
3091
  this._correlate = this._build_correlate(options, classification);
3088
3092
  this._settle = this._build_settle(options);
@@ -3107,7 +3111,7 @@ var Act = class {
3107
3111
  /** Apps that declare no lanes pass no lane filter to `claim`. */
3108
3112
  _build_drain_controllers(options, lanes, handle, handle_batch) {
3109
3113
  const all_lanes = this._declared_lanes;
3110
- const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
3114
+ const only_set = options.onlyLanes?.length ? new Set(options.onlyLanes) : void 0;
3111
3115
  const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
3112
3116
  const single_default_lane = lanes.length === 0;
3113
3117
  const controllers = /* @__PURE__ */ new Map();
@@ -3144,15 +3148,14 @@ var Act = class {
3144
3148
  leaseMillis: cfg.leaseMillis
3145
3149
  }
3146
3150
  });
3147
- if (cfg?.cycleMs !== void 0 && options.drain !== false)
3151
+ if (cfg?.cycleMs !== void 0 && this._drain)
3148
3152
  controller.start(cfg.cycleMs);
3149
3153
  controllers.set(name, controller);
3150
3154
  }
3151
3155
  return controllers;
3152
3156
  }
3153
3157
  /** One process can't see the cluster, so it reports its own lane gaps. */
3154
- _advise_orphaned_lanes(options) {
3155
- if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3158
+ _advise_orphaned_lanes() {
3156
3159
  const active = new Set(this._drain_controllers.keys());
3157
3160
  const orphaned = this._declared_lanes.filter((name) => !active.has(name));
3158
3161
  if (orphaned.length === 0) return;
@@ -3202,10 +3205,7 @@ var Act = class {
3202
3205
  _build_settle(options) {
3203
3206
  return new SettleLoop(
3204
3207
  {
3205
- correlate: (q) => this._correlate_pass(
3206
- { ...q, limit: q.limit ?? this._poll_limit ?? 100 },
3207
- true
3208
- ),
3208
+ correlate: (q) => this._correlate_pass(q.limit ?? this._poll_limit ?? 100, true),
3209
3209
  // `settle()` validated the options, and `_schedule` skips writer-only
3210
3210
  // instances, so the loop drains through the internal path.
3211
3211
  drain: (o) => this._scoped(() => this._drain_all(o)),
@@ -3300,9 +3300,10 @@ var Act = class {
3300
3300
  }
3301
3301
  /**
3302
3302
  * Executes an action: validates the payload, loads the state, checks its
3303
- * invariants, and commits the events its handler emits, with optimistic
3304
- * concurrency on the stream. The reactions to those events settle on
3305
- * their own (unless `autoSettle: false`).
3303
+ * preconditions, runs the handler, folds the events it emits, checks the
3304
+ * state's invariants (its schema) on the result, and commits them with
3305
+ * optimistic concurrency on the stream. The reactions to those events
3306
+ * settle on their own (unless `autoSettle: false`).
3306
3307
  *
3307
3308
  * @template TKey - Action name from registered actions
3308
3309
  * @param action - The name of the action to execute
@@ -3313,8 +3314,9 @@ var Act = class {
3313
3314
  * framework or orchestrator-level correlator for this call only.
3314
3315
  * @returns Array of snapshots for all affected states (usually one)
3315
3316
  *
3316
- * @throws {ValidationError} If payload doesn't match action schema
3317
- * @throws {InvariantError} If business rules are violated
3317
+ * @throws {ValidationError} If the payload doesn't match the action schema,
3318
+ * or the state the action leaves breaks the state schema
3319
+ * @throws {PreconditionError} If business rules are violated
3318
3320
  * @throws {ConcurrencyError} If another process modified the stream
3319
3321
  *
3320
3322
  * @example Basic action execution
@@ -3333,7 +3335,7 @@ var Act = class {
3333
3335
  *
3334
3336
  * @see {@link Target} for target structure
3335
3337
  * @see {@link Snapshot} for return value structure
3336
- * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
3338
+ * @see {@link ValidationError}, {@link PreconditionError}, {@link ConcurrencyError}
3337
3339
  */
3338
3340
  async do(action2, target, payload, options) {
3339
3341
  const reacting_to = options?.reactingTo ?? current_reacting();
@@ -3567,17 +3569,17 @@ var Act = class {
3567
3569
  * @see {@link start_correlations} for automatic periodic correlation
3568
3570
  */
3569
3571
  async correlate(query = {}) {
3570
- const pass = await this._correlate_pass({ limit: query.limit ?? 10 });
3572
+ const pass = await this._correlate_pass(query.limit ?? 10);
3571
3573
  return {
3572
3574
  subscribed: pass.kind === "scanned" ? pass.subscribed : 0,
3573
3575
  last_id: pass.last_id
3574
3576
  };
3575
3577
  }
3576
3578
  /** One correlate pass; arms every lane when it found work. */
3577
- async _correlate_pass(query, lease = false) {
3579
+ async _correlate_pass(limit, lease = false) {
3578
3580
  if (!this._drain) return { kind: "idle", last_id: -1 };
3579
3581
  return this._scoped(async () => {
3580
- const pass = await this._correlate.correlate(query, lease);
3582
+ const pass = await this._correlate.correlate(limit, lease);
3581
3583
  if (pass.kind === "scanned" && pass.marked > 0) this._arm_all();
3582
3584
  return pass;
3583
3585
  });
@@ -3595,8 +3597,9 @@ var Act = class {
3595
3597
  * (`drain: false`) each tick is a no-op, like `settle()`.
3596
3598
  *
3597
3599
  * @param query - Only `limit` is used: the most events each correlate pass
3598
- * reads (default 100). The scan always starts at the correlation
3599
- * checkpoint, so `after` is ignored.
3600
+ * reads. With no limit, or `0`, the passes use the background default
3601
+ * (100): a poll never reads the whole log in one tick. The scan always
3602
+ * starts at the correlation checkpoint, so `after` is ignored.
3600
3603
  * @param frequency - Milliseconds between ticks (default: 10000)
3601
3604
  * @returns `true` if the poll started, `false` if one is already running
3602
3605
  *
@@ -4639,7 +4642,7 @@ function act() {
4639
4642
  if (reset) Object.defineProperty(wrapped, FOLD_RESET, { value: reset });
4640
4643
  return wrapped;
4641
4644
  };
4642
- const make_batch_handlers = (patch_fn) => {
4645
+ const make_batch_handlers = () => {
4643
4646
  const handlers = new Map(batch_handlers);
4644
4647
  for (const spec of fold_specs) {
4645
4648
  handlers.set(
@@ -4649,7 +4652,7 @@ function act() {
4649
4652
  spec.merged,
4650
4653
  spec.flush,
4651
4654
  spec.config,
4652
- patch_fn,
4655
+ bare_patch,
4653
4656
  // Head loads strip sensitive keys like the warm path.
4654
4657
  registry.sensitive_fields
4655
4658
  )
@@ -4730,7 +4733,6 @@ function act() {
4730
4733
  },
4731
4734
  build: (options) => {
4732
4735
  resolveActConfig(options);
4733
- const patch_fn = options?.validateFoldedState === true ? validating_patch : bare_patch;
4734
4736
  if (!_built) {
4735
4737
  for (const proj of pending_projections) add_projection(proj);
4736
4738
  for (const proj of fold_projections) {
@@ -4791,10 +4793,9 @@ function act() {
4791
4793
  return new Act(
4792
4794
  registry,
4793
4795
  states,
4794
- make_batch_handlers(patch_fn),
4796
+ make_batch_handlers(),
4795
4797
  options,
4796
- lanes,
4797
- patch_fn
4798
+ lanes
4798
4799
  );
4799
4800
  },
4800
4801
  events: registry.events
@@ -5269,6 +5270,7 @@ export {
5269
5270
  LogLevels,
5270
5271
  NonRetryableError,
5271
5272
  PackageSchema,
5273
+ PreconditionError,
5272
5274
  QuerySchema,
5273
5275
  REDACTED,
5274
5276
  SHREDDED,