@rotorsoft/act 0.35.2 → 0.37.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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @module event-versions
3
+ * @category Internal
4
+ *
5
+ * Auto-deprecation of legacy event versions via the `_v<digits>` naming
6
+ * convention (ACT-403).
7
+ *
8
+ * Act's schema-evolution pattern keeps the old and new event names alive
9
+ * forever — the old name on the read path (reducers), the new on the write
10
+ * path (emissions). This module reads the convention to identify legacy
11
+ * versions automatically; the framework then enforces "emit only the
12
+ * current version" at build time and warns at runtime for dynamic emits.
13
+ *
14
+ * Convention pin: only `_v<digits>` with digits ≥ 2 counts as a version
15
+ * suffix. `Foo_v1` is just a literal event name (the base `Foo` is
16
+ * implicitly v1). Pinning here keeps the contract surface small.
17
+ *
18
+ * @internal
19
+ */
20
+ /**
21
+ * Returns the set of event names that are deprecated by virtue of having
22
+ * a higher-numbered sibling in the registry. The highest version in each
23
+ * group is the current version; every lower version is deprecated.
24
+ *
25
+ * Gaps are allowed: `{Foo, Foo_v3}` → `Foo` is deprecated, `Foo_v3` is
26
+ * current. The framework picks the max regardless of contiguity.
27
+ *
28
+ * Single-version groups (no siblings) yield no deprecations.
29
+ *
30
+ * @internal
31
+ */
32
+ export declare function deprecatedEventNames(names: Iterable<string>): Set<string>;
33
+ /**
34
+ * Given a deprecated event name and the full set of event names in its
35
+ * registry, returns the current (highest-version) sibling. Used to build
36
+ * actionable error messages — "use `Foo_v3` instead."
37
+ *
38
+ * Returns `undefined` if the event has no higher-versioned sibling (which
39
+ * means the caller's classification is stale or wrong).
40
+ *
41
+ * @internal
42
+ */
43
+ export declare function currentVersionOf(deprecatedName: string, allNames: Iterable<string>): string | undefined;
44
+ //# sourceMappingURL=event-versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-versions.d.ts","sourceRoot":"","sources":["../../../src/internal/event-versions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAmBH;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAgBzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GACzB,MAAM,GAAG,SAAS,CASpB"}
@@ -22,6 +22,7 @@ export { CorrelateCycle } from "./correlate-cycle.js";
22
22
  export type { DrainOps } from "./drain.js";
23
23
  export { DrainController, type Handle, type HandleBatch, } from "./drain-cycle.js";
24
24
  export type { EsOps } from "./event-sourcing.js";
25
+ export { currentVersionOf, deprecatedEventNames, } from "./event-versions.js";
25
26
  export { _this_, mergeEventRegister, mergeProjection, registerState, } from "./merge.js";
26
27
  export { buildHandle, buildHandleBatch } from "./reactions.js";
27
28
  export { SettleLoop } from "./settle.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/internal/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EACL,eAAe,EACf,KAAK,MAAM,EACX,KAAK,WAAW,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EACL,MAAM,EACN,kBAAkB,EAClB,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/internal/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EACL,eAAe,EACf,KAAK,MAAM,EACX,KAAK,WAAW,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,MAAM,EACN,kBAAkB,EAClB,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/internal/merge.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,KAAK,EAAU,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAuDvD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,EACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAON;AAwHD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC1D,IAAI,CAQN;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,EACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAiBN;AAGD,eAAO,MAAM,MAAM,GAAI,YAAY;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE;;;CAGnD,CAAC"}
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/internal/merge.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,KAAK,EAAU,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAuDvD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,EACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAON;AAmID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC1D,IAAI,CAQN;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,EACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAiBN;AAGD,eAAO,MAAM,MAAM,GAAI,YAAY;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE;;;CAGnD,CAAC"}
package/dist/index.cjs CHANGED
@@ -1485,6 +1485,43 @@ var DrainController = class {
1485
1485
  }
1486
1486
  };
1487
1487
 
1488
+ // src/internal/event-versions.ts
1489
+ var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
1490
+ function parse(name) {
1491
+ const m = name.match(VERSION_SUFFIX);
1492
+ if (m) {
1493
+ const v = Number.parseInt(m[2], 10);
1494
+ if (v >= 2) return { base: m[1], version: v };
1495
+ }
1496
+ return { base: name, version: 1 };
1497
+ }
1498
+ function deprecatedEventNames(names) {
1499
+ const groups = /* @__PURE__ */ new Map();
1500
+ for (const name of names) {
1501
+ const { base, version } = parse(name);
1502
+ const list = groups.get(base);
1503
+ if (list) list.push({ version, name });
1504
+ else groups.set(base, [{ version, name }]);
1505
+ }
1506
+ const deprecated = /* @__PURE__ */ new Set();
1507
+ for (const list of groups.values()) {
1508
+ if (list.length < 2) continue;
1509
+ list.sort((a, b) => b.version - a.version);
1510
+ for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
1511
+ }
1512
+ return deprecated;
1513
+ }
1514
+ function currentVersionOf(deprecatedName, allNames) {
1515
+ const target = parse(deprecatedName);
1516
+ let highest;
1517
+ for (const name of allNames) {
1518
+ const { base, version } = parse(name);
1519
+ if (base !== target.base) continue;
1520
+ if (!highest || version > highest.version) highest = { version, name };
1521
+ }
1522
+ return highest && highest.version > target.version ? highest.name : void 0;
1523
+ }
1524
+
1488
1525
  // src/internal/merge.ts
1489
1526
  var import_zod4 = require("zod");
1490
1527
  function baseTypeName(zodType) {
@@ -1542,7 +1579,11 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
1542
1579
  }
1543
1580
  for (const name of Object.keys(state2.events)) {
1544
1581
  if (existing.events[name] === state2.events[name]) continue;
1545
- if (existing.events[name]) continue;
1582
+ if (existing.events[name]) {
1583
+ throw new Error(
1584
+ `Event "${name}" in state "${state2.name}" is declared with different Zod schemas across slices. Cross-slice event schemas must reference the same instance \u2014 extract a shared schema (e.g. \`export const ${name} = z.object({ ... })\` in a shared module) and import it in every slice that declares it.`
1585
+ );
1586
+ }
1546
1587
  if (events[name]) throw new Error(`Duplicate event "${name}"`);
1547
1588
  }
1548
1589
  const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
@@ -1893,6 +1934,20 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1893
1934
  return [snapshot];
1894
1935
  }
1895
1936
  const tuples = Array.isArray(result[0]) ? result : [result];
1937
+ const deprecated = me._deprecated;
1938
+ if (deprecated && deprecated.size > 0) {
1939
+ const me_ = me;
1940
+ const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
1941
+ for (const [name] of tuples) {
1942
+ const evt = name;
1943
+ if (deprecated.has(evt) && !warned.has(evt)) {
1944
+ warned.add(evt);
1945
+ log().warn(
1946
+ `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
1947
+ );
1948
+ }
1949
+ }
1950
+ }
1896
1951
  const emitted = tuples.map(([name, data]) => ({
1897
1952
  name,
1898
1953
  data: skipValidation ? data : validate(name, data, me.events[name])
@@ -2893,6 +2948,38 @@ function act() {
2893
2948
  mergeProjection(proj, registry.events);
2894
2949
  registerBatchHandler(proj, batchHandlers);
2895
2950
  }
2951
+ const deprecationSummary = [];
2952
+ for (const state2 of states.values()) {
2953
+ const eventNames = Object.keys(state2.events);
2954
+ const deprecated = deprecatedEventNames(eventNames);
2955
+ if (deprecated.size === 0) continue;
2956
+ state2._deprecated = deprecated;
2957
+ for (const name of deprecated) {
2958
+ const current = currentVersionOf(name, eventNames);
2959
+ deprecationSummary.push({
2960
+ stateName: state2.name,
2961
+ deprecated: name,
2962
+ current
2963
+ });
2964
+ }
2965
+ for (const [actionName, handler] of Object.entries(state2.on)) {
2966
+ const staticTarget = handler?._staticEmit;
2967
+ if (staticTarget && deprecated.has(staticTarget)) {
2968
+ const current = currentVersionOf(staticTarget, eventNames);
2969
+ throw new Error(
2970
+ `Action "${actionName}" in state "${state2.name}" emits deprecated event "${staticTarget}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${staticTarget}" stays as-is \u2014 historical events still need it.`
2971
+ );
2972
+ }
2973
+ }
2974
+ }
2975
+ if (deprecationSummary.length > 0) {
2976
+ const list = deprecationSummary.map(
2977
+ (d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
2978
+ ).join(", ");
2979
+ log().info(
2980
+ `Act registered ${deprecationSummary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
2981
+ );
2982
+ }
2896
2983
  return new Act(
2897
2984
  registry,
2898
2985
  states,
@@ -3079,10 +3166,10 @@ function action_builder(state2) {
3079
3166
  function emit(handler) {
3080
3167
  if (typeof handler === "string") {
3081
3168
  const eventName = handler;
3082
- internal.on[action2] = (payload) => [
3083
- eventName,
3084
- payload
3085
- ];
3169
+ const emitFn = Object.assign((payload) => [eventName, payload], {
3170
+ _staticEmit: eventName
3171
+ });
3172
+ internal.on[action2] = emitFn;
3086
3173
  } else {
3087
3174
  internal.on[action2] = handler;
3088
3175
  }