@rotorsoft/act 0.32.2 → 0.32.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
@@ -154,7 +154,7 @@ var InMemoryCache = class {
154
154
  };
155
155
 
156
156
  // src/utils.ts
157
- import { prettifyError } from "zod";
157
+ import { ZodError, prettifyError } from "zod";
158
158
 
159
159
  // src/config.ts
160
160
  import * as fs from "fs";
@@ -181,7 +181,7 @@ var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
181
181
  var env = NODE_ENV || "development";
182
182
  var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "error" : NODE_ENV === "production" ? "info" : "trace");
183
183
  var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
184
- var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100");
184
+ var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
185
185
  var pkg = getPackage();
186
186
  var config = () => {
187
187
  return extend({ ...pkg, env, logLevel, logSingleLine, sleepMs }, BaseSchema);
@@ -192,19 +192,15 @@ var validate = (target, payload, schema) => {
192
192
  try {
193
193
  return schema ? schema.parse(payload) : payload;
194
194
  } catch (error) {
195
- if (error instanceof Error && error.name === "ZodError") {
196
- throw new ValidationError(
197
- target,
198
- payload,
199
- prettifyError(error)
200
- );
195
+ if (error instanceof ZodError) {
196
+ throw new ValidationError(target, payload, prettifyError(error));
201
197
  }
202
198
  throw new ValidationError(target, payload, error);
203
199
  }
204
200
  };
205
201
  var extend = (source, schema, target) => {
206
202
  const value = validate("config", source, schema);
207
- return Object.assign(target || {}, value);
203
+ return { ...target, ...value };
208
204
  };
209
205
  async function sleep(ms) {
210
206
  return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
@@ -632,13 +628,13 @@ var cache = port(function cache2(adapter) {
632
628
  var disposers = [];
633
629
  async function disposeAndExit(code = "EXIT") {
634
630
  if (code === "ERROR" && config().env === "production") return;
635
- await Promise.all(disposers.map((disposer) => disposer()));
636
- await Promise.all(
637
- [...adapters.values()].reverse().map(async (adapter) => {
638
- await adapter.dispose();
639
- console.log(`[act] - ${adapter.constructor.name}`);
640
- })
641
- );
631
+ for (const disposer of [...disposers].reverse()) {
632
+ await disposer();
633
+ }
634
+ for (const adapter of [...adapters.values()].reverse()) {
635
+ await adapter.dispose();
636
+ console.log(`[act] - ${adapter.constructor.name}`);
637
+ }
642
638
  adapters.clear();
643
639
  config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
644
640
  }
@@ -650,21 +646,20 @@ var SNAP_EVENT = "__snapshot__";
650
646
  var TOMBSTONE_EVENT = "__tombstone__";
651
647
 
652
648
  // src/signals.ts
653
- var logger = log();
654
649
  process.once("SIGINT", async (arg) => {
655
- logger.info(arg, "SIGINT");
650
+ log().info(arg, "SIGINT");
656
651
  await disposeAndExit("EXIT");
657
652
  });
658
653
  process.once("SIGTERM", async (arg) => {
659
- logger.info(arg, "SIGTERM");
654
+ log().info(arg, "SIGTERM");
660
655
  await disposeAndExit("EXIT");
661
656
  });
662
657
  process.once("uncaughtException", async (arg) => {
663
- logger.error(arg, "Uncaught Exception");
658
+ log().error(arg, "Uncaught Exception");
664
659
  await disposeAndExit("ERROR");
665
660
  });
666
661
  process.once("unhandledRejection", async (arg) => {
667
- logger.error(arg, "Unhandled Rejection");
662
+ log().error(arg, "Unhandled Rejection");
668
663
  await disposeAndExit("ERROR");
669
664
  });
670
665
 
@@ -858,6 +853,10 @@ async function load(me, stream, callback, asOf) {
858
853
  } else if (me.patch[e.name]) {
859
854
  state2 = patch(state2, me.patch[e.name](event, state2));
860
855
  patches++;
856
+ } else if (e.name !== TOMBSTONE_EVENT) {
857
+ log().warn(
858
+ `Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
859
+ );
861
860
  }
862
861
  callback && callback({ event, state: state2, patches, snaps });
863
862
  },
@@ -958,38 +957,38 @@ var traced = (inner, exit, entry) => (async (...args) => {
958
957
  exit?.(result, ...args);
959
958
  return result;
960
959
  });
961
- function buildEs(logger2) {
962
- if (logger2.level !== "trace") {
960
+ function buildEs(logger) {
961
+ if (logger.level !== "trace") {
963
962
  return { snap, load, action };
964
963
  }
965
964
  return {
966
965
  snap: traced(snap, void 0, (snapshot) => {
967
- logger2.trace(
966
+ logger.trace(
968
967
  `\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
969
968
  );
970
969
  }),
971
970
  load: traced(load, void 0, (_me, stream, _cb, asOf) => {
972
- logger2.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
971
+ logger.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
973
972
  }),
974
973
  action: traced(
975
974
  action,
976
975
  (snapshots, _me, _action, target) => {
977
976
  const committed = snapshots.filter((s) => s.event);
978
977
  if (committed.length) {
979
- logger2.trace(
978
+ logger.trace(
980
979
  committed.map((s) => s.event.data),
981
980
  `\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
982
981
  );
983
982
  }
984
983
  },
985
984
  (_me, action2, target, payload) => {
986
- logger2.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
985
+ logger.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
987
986
  }
988
987
  )
989
988
  };
990
989
  }
991
- function buildDrain(logger2) {
992
- if (logger2.level !== "trace") {
990
+ function buildDrain(logger) {
991
+ if (logger.level !== "trace") {
993
992
  return {
994
993
  claim,
995
994
  fetch,
@@ -1004,7 +1003,7 @@ function buildDrain(logger2) {
1004
1003
  const data = Object.fromEntries(
1005
1004
  leased.map(({ stream, at, retry }) => [stream, { at, retry }])
1006
1005
  );
1007
- logger2.trace(data, ">> lease");
1006
+ logger.trace(data, ">> lease");
1008
1007
  }
1009
1008
  }),
1010
1009
  fetch: traced(fetch, (fetched) => {
@@ -1017,14 +1016,14 @@ function buildDrain(logger2) {
1017
1016
  return [key, value];
1018
1017
  })
1019
1018
  );
1020
- logger2.trace(data, ">> fetch");
1019
+ logger.trace(data, ">> fetch");
1021
1020
  }),
1022
1021
  ack: traced(ack, (acked) => {
1023
1022
  if (acked.length) {
1024
1023
  const data = Object.fromEntries(
1025
1024
  acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1026
1025
  );
1027
- logger2.trace(data, ">> ack");
1026
+ logger.trace(data, ">> ack");
1028
1027
  }
1029
1028
  }),
1030
1029
  block: traced(block, (blocked) => {
@@ -1035,13 +1034,13 @@ function buildDrain(logger2) {
1035
1034
  { at, retry, error }
1036
1035
  ])
1037
1036
  );
1038
- logger2.trace(data, ">> block");
1037
+ logger.trace(data, ">> block");
1039
1038
  }
1040
1039
  }),
1041
1040
  subscribe: traced(subscribe, (result, streams) => {
1042
1041
  if (result.subscribed) {
1043
1042
  const data = streams.map(({ stream }) => stream).join(" ");
1044
- logger2.trace(`>> correlate ${data}`);
1043
+ logger.trace(`>> correlate ${data}`);
1045
1044
  }
1046
1045
  })
1047
1046
  };
@@ -1072,6 +1071,11 @@ var Act = class {
1072
1071
  }
1073
1072
  }
1074
1073
  this._static_targets = statics;
1074
+ for (const merged of this._states.values()) {
1075
+ for (const eventName of Object.keys(merged.events)) {
1076
+ this._event_to_state.set(eventName, merged);
1077
+ }
1078
+ }
1075
1079
  dispose(() => {
1076
1080
  this._emitter.removeAllListeners();
1077
1081
  this.stop_correlations();
@@ -1118,6 +1122,13 @@ var Act = class {
1118
1122
  _es;
1119
1123
  /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
1120
1124
  _cd;
1125
+ /**
1126
+ * Event-name → owning state, computed at build time. The duplicate-event
1127
+ * guard in merge.ts ensures one event name maps to at most one state, so
1128
+ * this lookup is unambiguous. Used by `close()` to pick the right reducer
1129
+ * set when seeding a `restart` snapshot in multi-state apps.
1130
+ */
1131
+ _event_to_state = /* @__PURE__ */ new Map();
1121
1132
  /** Logger resolved at construction time (after user port configuration) */
1122
1133
  _logger = log();
1123
1134
  /**
@@ -1834,16 +1845,24 @@ var Act = class {
1834
1845
  streams.map(async (s) => {
1835
1846
  let maxId = -1;
1836
1847
  let version = -1;
1848
+ let lastEventName;
1837
1849
  await store().query(
1838
1850
  (e) => {
1839
- if (e.name !== TOMBSTONE_EVENT) {
1851
+ if (e.name === TOMBSTONE_EVENT) return;
1852
+ if (maxId === -1) {
1840
1853
  maxId = e.id;
1841
1854
  version = e.version;
1842
1855
  }
1856
+ if (e.name !== SNAP_EVENT && lastEventName === void 0) {
1857
+ lastEventName = e.name;
1858
+ }
1843
1859
  },
1844
- { stream: s, stream_exact: true, backward: true, limit: 1 }
1860
+ // limit: 2 covers the typical snapshot-at-head case (snapshot is
1861
+ // always preceded by the domain event it captured). Streams with
1862
+ // unusual layouts fall back to no-seed via the lookup miss path.
1863
+ { stream: s, stream_exact: true, backward: true, limit: 2 }
1845
1864
  );
1846
- if (maxId >= 0) streamInfo.set(s, { maxId, version });
1865
+ if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
1847
1866
  })
1848
1867
  );
1849
1868
  const skipped = [];
@@ -1852,16 +1871,14 @@ var Act = class {
1852
1871
  safe = [...streamInfo.keys()];
1853
1872
  } else {
1854
1873
  const pendingSet = /* @__PURE__ */ new Set();
1855
- const leases = await store().claim(1e3, 1e3, randomUUID2(), 1);
1856
- if (leases.length) await store().ack(leases);
1857
- for (const lease of leases) {
1858
- const sourceRe = lease.source ? RegExp(lease.source) : void 0;
1874
+ await store().query_streams((position) => {
1875
+ const sourceRe = position.source ? RegExp(position.source) : void 0;
1859
1876
  for (const [stream, info] of streamInfo) {
1860
- if ((!sourceRe || sourceRe.test(stream)) && lease.at < info.maxId) {
1877
+ if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
1861
1878
  pendingSet.add(stream);
1862
1879
  }
1863
1880
  }
1864
- }
1881
+ });
1865
1882
  safe = [];
1866
1883
  for (const [stream] of streamInfo) {
1867
1884
  if (pendingSet.has(stream)) {
@@ -1901,16 +1918,21 @@ var Act = class {
1901
1918
  this.emit("closed", result2);
1902
1919
  return result2;
1903
1920
  }
1904
- const mergedState = [...this._states.values()][0];
1905
1921
  const seedStates = /* @__PURE__ */ new Map();
1906
- if (mergedState) {
1907
- await Promise.all(
1908
- guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
1909
- const snap2 = await this._es.load(mergedState, stream);
1910
- seedStates.set(stream, snap2.state);
1911
- })
1912
- );
1913
- }
1922
+ await Promise.all(
1923
+ guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
1924
+ const lastEventName = streamInfo.get(stream)?.lastEventName;
1925
+ const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
1926
+ if (!ownerState) {
1927
+ this._logger.error(
1928
+ `Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
1929
+ );
1930
+ return;
1931
+ }
1932
+ const snap2 = await this._es.load(ownerState, stream);
1933
+ seedStates.set(stream, snap2.state);
1934
+ })
1935
+ );
1914
1936
  for (const stream of guarded) {
1915
1937
  const archiveFn = targetMap.get(stream)?.archive;
1916
1938
  if (archiveFn) await archiveFn();
@@ -2021,6 +2043,14 @@ var Act = class {
2021
2043
  };
2022
2044
 
2023
2045
  // src/act-builder.ts
2046
+ function registerBatchHandler(proj, batchHandlers) {
2047
+ if (!proj.batchHandler || !proj.target) return;
2048
+ const existing = batchHandlers.get(proj.target);
2049
+ if (existing && existing !== proj.batchHandler) {
2050
+ throw new Error(`Duplicate batch handler for target "${proj.target}"`);
2051
+ }
2052
+ batchHandlers.set(proj.target, proj.batchHandler);
2053
+ }
2024
2054
  function act(states = /* @__PURE__ */ new Map(), registry = {
2025
2055
  actions: {},
2026
2056
  events: {}
@@ -2055,9 +2085,7 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2055
2085
  },
2056
2086
  withProjection: (proj) => {
2057
2087
  mergeProjection(proj, registry.events);
2058
- if (proj.batchHandler && proj.target) {
2059
- batchHandlers.set(proj.target, proj.batchHandler);
2060
- }
2088
+ registerBatchHandler(proj, batchHandlers);
2061
2089
  return act(
2062
2090
  states,
2063
2091
  registry,
@@ -2103,9 +2131,7 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2103
2131
  build: () => {
2104
2132
  for (const proj of pendingProjections) {
2105
2133
  mergeProjection(proj, registry.events);
2106
- if (proj.batchHandler && proj.target) {
2107
- batchHandlers.set(proj.target, proj.batchHandler);
2108
- }
2134
+ registerBatchHandler(proj, batchHandlers);
2109
2135
  }
2110
2136
  return new Act(
2111
2137
  registry,