@rotorsoft/act 0.36.0 → 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"}
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) {
@@ -1897,6 +1934,20 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1897
1934
  return [snapshot];
1898
1935
  }
1899
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
+ }
1900
1951
  const emitted = tuples.map(([name, data]) => ({
1901
1952
  name,
1902
1953
  data: skipValidation ? data : validate(name, data, me.events[name])
@@ -2897,6 +2948,38 @@ function act() {
2897
2948
  mergeProjection(proj, registry.events);
2898
2949
  registerBatchHandler(proj, batchHandlers);
2899
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
+ }
2900
2983
  return new Act(
2901
2984
  registry,
2902
2985
  states,
@@ -3083,10 +3166,10 @@ function action_builder(state2) {
3083
3166
  function emit(handler) {
3084
3167
  if (typeof handler === "string") {
3085
3168
  const eventName = handler;
3086
- internal.on[action2] = (payload) => [
3087
- eventName,
3088
- payload
3089
- ];
3169
+ const emitFn = Object.assign((payload) => [eventName, payload], {
3170
+ _staticEmit: eventName
3171
+ });
3172
+ internal.on[action2] = emitFn;
3090
3173
  } else {
3091
3174
  internal.on[action2] = handler;
3092
3175
  }