@rotorsoft/act 0.32.1 → 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.cjs CHANGED
@@ -347,7 +347,7 @@ var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
347
347
  var env = NODE_ENV || "development";
348
348
  var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "error" : NODE_ENV === "production" ? "info" : "trace");
349
349
  var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
350
- var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100");
350
+ var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
351
351
  var pkg = getPackage();
352
352
  var config = () => {
353
353
  return extend({ ...pkg, env, logLevel, logSingleLine, sleepMs }, BaseSchema);
@@ -358,19 +358,15 @@ var validate = (target, payload, schema) => {
358
358
  try {
359
359
  return schema ? schema.parse(payload) : payload;
360
360
  } catch (error) {
361
- if (error instanceof Error && error.name === "ZodError") {
362
- throw new ValidationError(
363
- target,
364
- payload,
365
- (0, import_zod3.prettifyError)(error)
366
- );
361
+ if (error instanceof import_zod3.ZodError) {
362
+ throw new ValidationError(target, payload, (0, import_zod3.prettifyError)(error));
367
363
  }
368
364
  throw new ValidationError(target, payload, error);
369
365
  }
370
366
  };
371
367
  var extend = (source, schema, target) => {
372
368
  const value = validate("config", source, schema);
373
- return Object.assign(target || {}, value);
369
+ return { ...target, ...value };
374
370
  };
375
371
  async function sleep(ms) {
376
372
  return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
@@ -798,13 +794,13 @@ var cache = port(function cache2(adapter) {
798
794
  var disposers = [];
799
795
  async function disposeAndExit(code = "EXIT") {
800
796
  if (code === "ERROR" && config().env === "production") return;
801
- await Promise.all(disposers.map((disposer) => disposer()));
802
- await Promise.all(
803
- [...adapters.values()].reverse().map(async (adapter) => {
804
- await adapter.dispose();
805
- console.log(`[act] - ${adapter.constructor.name}`);
806
- })
807
- );
797
+ for (const disposer of [...disposers].reverse()) {
798
+ await disposer();
799
+ }
800
+ for (const adapter of [...adapters.values()].reverse()) {
801
+ await adapter.dispose();
802
+ console.log(`[act] - ${adapter.constructor.name}`);
803
+ }
808
804
  adapters.clear();
809
805
  config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
810
806
  }
@@ -816,21 +812,20 @@ var SNAP_EVENT = "__snapshot__";
816
812
  var TOMBSTONE_EVENT = "__tombstone__";
817
813
 
818
814
  // src/signals.ts
819
- var logger = log();
820
815
  process.once("SIGINT", async (arg) => {
821
- logger.info(arg, "SIGINT");
816
+ log().info(arg, "SIGINT");
822
817
  await disposeAndExit("EXIT");
823
818
  });
824
819
  process.once("SIGTERM", async (arg) => {
825
- logger.info(arg, "SIGTERM");
820
+ log().info(arg, "SIGTERM");
826
821
  await disposeAndExit("EXIT");
827
822
  });
828
823
  process.once("uncaughtException", async (arg) => {
829
- logger.error(arg, "Uncaught Exception");
824
+ log().error(arg, "Uncaught Exception");
830
825
  await disposeAndExit("ERROR");
831
826
  });
832
827
  process.once("unhandledRejection", async (arg) => {
833
- logger.error(arg, "Unhandled Rejection");
828
+ log().error(arg, "Unhandled Rejection");
834
829
  await disposeAndExit("ERROR");
835
830
  });
836
831
 
@@ -838,6 +833,136 @@ process.once("unhandledRejection", async (arg) => {
838
833
  var import_crypto2 = require("crypto");
839
834
  var import_events = __toESM(require("events"), 1);
840
835
 
836
+ // src/internal/merge.ts
837
+ var import_zod4 = require("zod");
838
+ function baseTypeName(zodType) {
839
+ let t = zodType;
840
+ while (typeof t.unwrap === "function") {
841
+ t = t.unwrap();
842
+ }
843
+ return t.constructor.name;
844
+ }
845
+ function mergeSchemas(existing, incoming, stateName) {
846
+ if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
847
+ const existingShape = existing.shape;
848
+ const incomingShape = incoming.shape;
849
+ for (const key of Object.keys(incomingShape)) {
850
+ if (key in existingShape) {
851
+ const existingBase = baseTypeName(existingShape[key]);
852
+ const incomingBase = baseTypeName(incomingShape[key]);
853
+ if (existingBase !== incomingBase) {
854
+ throw new Error(
855
+ `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
856
+ );
857
+ }
858
+ }
859
+ }
860
+ return existing.extend(incomingShape);
861
+ }
862
+ return existing;
863
+ }
864
+ function mergeInits(existing, incoming) {
865
+ return () => ({ ...existing(), ...incoming() });
866
+ }
867
+ function registerState(state2, states, actions, events) {
868
+ const existing = states.get(state2.name);
869
+ if (existing) {
870
+ mergeIntoExisting(state2, existing, states, actions, events);
871
+ } else {
872
+ registerNewState(state2, states, actions, events);
873
+ }
874
+ }
875
+ function registerNewState(state2, states, actions, events) {
876
+ states.set(state2.name, state2);
877
+ for (const name of Object.keys(state2.actions)) {
878
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
879
+ actions[name] = state2;
880
+ }
881
+ for (const name of Object.keys(state2.events)) {
882
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
883
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
884
+ }
885
+ }
886
+ function mergeIntoExisting(state2, existing, states, actions, events) {
887
+ for (const name of Object.keys(state2.actions)) {
888
+ if (existing.actions[name] === state2.actions[name]) continue;
889
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
890
+ }
891
+ for (const name of Object.keys(state2.events)) {
892
+ if (existing.events[name] === state2.events[name]) continue;
893
+ if (existing.events[name]) continue;
894
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
895
+ }
896
+ const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
897
+ const merged = {
898
+ ...existing,
899
+ state: mergeSchemas(existing.state, state2.state, state2.name),
900
+ init: mergeInits(existing.init, state2.init),
901
+ events: { ...existing.events, ...state2.events },
902
+ actions: { ...existing.actions, ...state2.actions },
903
+ patch: mergedPatch,
904
+ on: { ...existing.on, ...state2.on },
905
+ given: { ...existing.given, ...state2.given },
906
+ snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
907
+ throw new Error(
908
+ `Duplicate snap strategy for state "${state2.name}"`
909
+ );
910
+ })() : state2.snap || existing.snap
911
+ };
912
+ states.set(state2.name, merged);
913
+ for (const name of Object.keys(merged.actions)) {
914
+ actions[name] = merged;
915
+ }
916
+ for (const name of Object.keys(state2.events)) {
917
+ if (events[name]) continue;
918
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
919
+ }
920
+ }
921
+ function mergePatches(existing, incoming, stateName) {
922
+ const merged = { ...existing };
923
+ for (const name of Object.keys(incoming)) {
924
+ const existingP = existing[name];
925
+ const incomingP = incoming[name];
926
+ if (!existingP) {
927
+ merged[name] = incomingP;
928
+ continue;
929
+ }
930
+ const existingIsDefault = existingP._passthrough;
931
+ const incomingIsDefault = incomingP._passthrough;
932
+ if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
933
+ throw new Error(
934
+ `Duplicate custom patch for event "${name}" in state "${stateName}"`
935
+ );
936
+ }
937
+ if (existingIsDefault && !incomingIsDefault) {
938
+ merged[name] = incomingP;
939
+ }
940
+ }
941
+ return merged;
942
+ }
943
+ function mergeProjection(proj, events) {
944
+ for (const eventName of Object.keys(proj.events)) {
945
+ const projRegister = proj.events[eventName];
946
+ const existing = events[eventName];
947
+ if (!existing) {
948
+ events[eventName] = {
949
+ schema: projRegister.schema,
950
+ reactions: new Map(projRegister.reactions)
951
+ };
952
+ } else {
953
+ for (const [name, reaction] of projRegister.reactions) {
954
+ let key = name;
955
+ while (existing.reactions.has(key)) key = `${key}_p`;
956
+ existing.reactions.set(key, reaction);
957
+ }
958
+ }
959
+ }
960
+ }
961
+ var _this_ = ({ stream }) => ({
962
+ source: stream,
963
+ target: stream
964
+ });
965
+
841
966
  // src/internal/drain.ts
842
967
  var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
843
968
  async function fetch(leased, eventLimit) {
@@ -860,7 +985,6 @@ var subscribe = (streams) => store().subscribe(streams);
860
985
  // src/internal/event-sourcing.ts
861
986
  var import_act_patch = require("@rotorsoft/act-patch");
862
987
  var import_crypto = require("crypto");
863
- var logger2 = log();
864
988
  async function snap(snapshot) {
865
989
  try {
866
990
  const { id, stream, name, meta, version } = snapshot.event;
@@ -875,11 +999,11 @@ async function snap(snapshot) {
875
999
  // IMPORTANT! - state events are committed right after the snapshot event
876
1000
  );
877
1001
  } catch (error) {
878
- logger2.error(error);
1002
+ log().error(error);
879
1003
  }
880
1004
  }
881
1005
  async function load(me, stream, callback, asOf) {
882
- const timeTravel = asOf && (asOf.before !== void 0 || asOf.created_before !== void 0 || asOf.created_after !== void 0 || asOf.limit !== void 0);
1006
+ const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
883
1007
  const cached = timeTravel ? void 0 : await cache().get(stream);
884
1008
  let state2 = cached?.state ?? (me.init ? me.init() : {});
885
1009
  let patches = cached?.patches ?? 0;
@@ -895,6 +1019,10 @@ async function load(me, stream, callback, asOf) {
895
1019
  } else if (me.patch[e.name]) {
896
1020
  state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
897
1021
  patches++;
1022
+ } else if (e.name !== TOMBSTONE_EVENT) {
1023
+ log().warn(
1024
+ `Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
1025
+ );
898
1026
  }
899
1027
  callback && callback({ event, state: state2, patches, snaps });
900
1028
  },
@@ -963,7 +1091,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
963
1091
  reactingTo ? void 0 : expected
964
1092
  );
965
1093
  } catch (error) {
966
- if (error.name === "ERR_CONCURRENCY") {
1094
+ if (error instanceof ConcurrencyError) {
967
1095
  await cache().invalidate(stream);
968
1096
  }
969
1097
  throw error;
@@ -977,241 +1105,56 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
977
1105
  });
978
1106
  const last = snapshots.at(-1);
979
1107
  const snapped = me.snap && me.snap(last);
980
- void cache().set(stream, {
1108
+ cache().set(stream, {
981
1109
  state: last.state,
982
1110
  version: last.event.version,
983
1111
  event_id: last.event.id,
984
1112
  patches: snapped ? 0 : last.patches,
985
1113
  snaps: snapped ? last.snaps + 1 : last.snaps
986
- });
1114
+ }).catch((err) => log().error(err));
987
1115
  if (snapped) void snap(last);
988
1116
  return snapshots;
989
1117
  }
990
1118
 
991
- // src/internal/merge.ts
992
- var import_zod4 = require("zod");
993
- function baseTypeName(zodType) {
994
- let t = zodType;
995
- while (typeof t.unwrap === "function") {
996
- t = t.unwrap();
997
- }
998
- return t.constructor.name;
999
- }
1000
- function mergeSchemas(existing, incoming, stateName) {
1001
- if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
1002
- const existingShape = existing.shape;
1003
- const incomingShape = incoming.shape;
1004
- for (const key of Object.keys(incomingShape)) {
1005
- if (key in existingShape) {
1006
- const existingBase = baseTypeName(existingShape[key]);
1007
- const incomingBase = baseTypeName(incomingShape[key]);
1008
- if (existingBase !== incomingBase) {
1009
- throw new Error(
1010
- `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
1011
- );
1012
- }
1013
- }
1014
- }
1015
- return existing.extend(incomingShape);
1016
- }
1017
- return existing;
1018
- }
1019
- function mergeInits(existing, incoming) {
1020
- return () => ({ ...existing(), ...incoming() });
1021
- }
1022
- function registerState(state2, states, actions, events) {
1023
- if (states.has(state2.name)) {
1024
- const existing = states.get(state2.name);
1025
- for (const name of Object.keys(state2.actions)) {
1026
- if (existing.actions[name] === state2.actions[name]) continue;
1027
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1028
- }
1029
- for (const name of Object.keys(state2.events)) {
1030
- if (existing.events[name] === state2.events[name]) continue;
1031
- if (existing.events[name]) continue;
1032
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
1033
- }
1034
- const mergedPatch = { ...existing.patch };
1035
- for (const name of Object.keys(state2.patch)) {
1036
- const existingP = existing.patch[name];
1037
- const incomingP = state2.patch[name];
1038
- if (!existingP) {
1039
- mergedPatch[name] = incomingP;
1040
- } else {
1041
- const existingIsDefault = existingP._passthrough;
1042
- const incomingIsDefault = incomingP._passthrough;
1043
- if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
1044
- throw new Error(
1045
- `Duplicate custom patch for event "${name}" in state "${state2.name}"`
1046
- );
1047
- }
1048
- if (existingIsDefault && !incomingIsDefault) {
1049
- mergedPatch[name] = incomingP;
1050
- }
1051
- }
1052
- }
1053
- const merged = {
1054
- ...existing,
1055
- state: mergeSchemas(existing.state, state2.state, state2.name),
1056
- init: mergeInits(existing.init, state2.init),
1057
- events: { ...existing.events, ...state2.events },
1058
- actions: { ...existing.actions, ...state2.actions },
1059
- patch: mergedPatch,
1060
- on: { ...existing.on, ...state2.on },
1061
- given: { ...existing.given, ...state2.given },
1062
- snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
1063
- throw new Error(
1064
- `Duplicate snap strategy for state "${state2.name}"`
1065
- );
1066
- })() : state2.snap || existing.snap
1067
- };
1068
- states.set(state2.name, merged);
1069
- for (const name of Object.keys(merged.actions)) {
1070
- actions[name] = merged;
1071
- }
1072
- for (const name of Object.keys(state2.events)) {
1073
- if (events[name]) continue;
1074
- events[name] = {
1075
- schema: state2.events[name],
1076
- reactions: /* @__PURE__ */ new Map()
1077
- };
1078
- }
1079
- } else {
1080
- states.set(state2.name, state2);
1081
- for (const name of Object.keys(state2.actions)) {
1082
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1083
- actions[name] = state2;
1084
- }
1085
- for (const name of Object.keys(state2.events)) {
1086
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
1087
- events[name] = {
1088
- schema: state2.events[name],
1089
- reactions: /* @__PURE__ */ new Map()
1090
- };
1091
- }
1092
- }
1093
- }
1094
- function mergeProjection(proj, events) {
1095
- for (const eventName of Object.keys(proj.events)) {
1096
- const projRegister = proj.events[eventName];
1097
- const existing = events[eventName];
1098
- if (!existing) {
1099
- events[eventName] = {
1100
- schema: projRegister.schema,
1101
- reactions: new Map(projRegister.reactions)
1102
- };
1103
- } else {
1104
- for (const [name, reaction] of projRegister.reactions) {
1105
- let key = name;
1106
- while (existing.reactions.has(key)) key = `${key}_p`;
1107
- existing.reactions.set(key, reaction);
1108
- }
1109
- }
1110
- }
1111
- }
1112
- var _this_ = ({ stream }) => ({
1113
- source: stream,
1114
- target: stream
1115
- });
1116
-
1117
1119
  // src/internal/tracing.ts
1118
- var logger3 = log();
1119
- var withSnapTrace = (inner) => async (snapshot) => {
1120
- logger3.trace(
1121
- `\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
1122
- );
1123
- return inner(snapshot);
1124
- };
1125
- var withLoadTrace = (inner) => async (me, stream, callback, asOf) => {
1126
- logger3.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
1127
- return inner(me, stream, callback, asOf);
1128
- };
1129
- var withActionTrace = (inner) => async (me, action2, target, payload, reactingTo, skipValidation) => {
1130
- logger3.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
1131
- const snapshots = await inner(
1132
- me,
1133
- action2,
1134
- target,
1135
- payload,
1136
- reactingTo,
1137
- skipValidation
1138
- );
1139
- const committed = snapshots.filter((s) => s.event);
1140
- if (committed.length) {
1141
- logger3.trace(
1142
- committed.map((s) => s.event.data),
1143
- `\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
1144
- );
1145
- }
1146
- return snapshots;
1147
- };
1148
- function buildEs(level) {
1149
- if (level !== "trace") {
1120
+ var traced = (inner, exit, entry) => (async (...args) => {
1121
+ entry?.(...args);
1122
+ const result = await inner(...args);
1123
+ exit?.(result, ...args);
1124
+ return result;
1125
+ });
1126
+ function buildEs(logger) {
1127
+ if (logger.level !== "trace") {
1150
1128
  return { snap, load, action };
1151
1129
  }
1152
1130
  return {
1153
- snap: withSnapTrace(snap),
1154
- load: withLoadTrace(load),
1155
- action: withActionTrace(action)
1131
+ snap: traced(snap, void 0, (snapshot) => {
1132
+ logger.trace(
1133
+ `\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
1134
+ );
1135
+ }),
1136
+ load: traced(load, void 0, (_me, stream, _cb, asOf) => {
1137
+ logger.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
1138
+ }),
1139
+ action: traced(
1140
+ action,
1141
+ (snapshots, _me, _action, target) => {
1142
+ const committed = snapshots.filter((s) => s.event);
1143
+ if (committed.length) {
1144
+ logger.trace(
1145
+ committed.map((s) => s.event.data),
1146
+ `\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
1147
+ );
1148
+ }
1149
+ },
1150
+ (_me, action2, target, payload) => {
1151
+ logger.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
1152
+ }
1153
+ )
1156
1154
  };
1157
1155
  }
1158
- var withClaimTrace = (inner) => async (lagging, leading, by, millis) => {
1159
- const leased = await inner(lagging, leading, by, millis);
1160
- if (leased.length) {
1161
- const data = Object.fromEntries(
1162
- leased.map(({ stream, at, retry }) => [stream, { at, retry }])
1163
- );
1164
- logger3.trace(data, ">> lease");
1165
- }
1166
- return leased;
1167
- };
1168
- var withFetchTrace = (inner) => async (leased, eventLimit) => {
1169
- const fetched = await inner(leased, eventLimit);
1170
- const data = Object.fromEntries(
1171
- fetched.map(({ stream, source, events }) => {
1172
- const key = source ? `${stream}<-${source}` : stream;
1173
- const value = Object.fromEntries(
1174
- events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
1175
- );
1176
- return [key, value];
1177
- })
1178
- );
1179
- logger3.trace(data, ">> fetch");
1180
- return fetched;
1181
- };
1182
- var withAckTrace = (inner) => async (leases) => {
1183
- const acked = await inner(leases);
1184
- if (acked.length) {
1185
- const data = Object.fromEntries(
1186
- acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1187
- );
1188
- logger3.trace(data, ">> ack");
1189
- }
1190
- return acked;
1191
- };
1192
- var withBlockTrace = (inner) => async (leases) => {
1193
- const blocked = await inner(leases);
1194
- if (blocked.length) {
1195
- const data = Object.fromEntries(
1196
- blocked.map(({ stream, at, retry, error }) => [
1197
- stream,
1198
- { at, retry, error }
1199
- ])
1200
- );
1201
- logger3.trace(data, ">> block");
1202
- }
1203
- return blocked;
1204
- };
1205
- var withSubscribeTrace = (inner) => async (streams) => {
1206
- const result = await inner(streams);
1207
- if (result.subscribed) {
1208
- const data = streams.map(({ stream }) => stream).join(" ");
1209
- logger3.trace(`>> correlate ${data}`);
1210
- }
1211
- return result;
1212
- };
1213
- function buildDrain(level) {
1214
- if (level !== "trace") {
1156
+ function buildDrain(logger) {
1157
+ if (logger.level !== "trace") {
1215
1158
  return {
1216
1159
  claim,
1217
1160
  fetch,
@@ -1221,24 +1164,62 @@ function buildDrain(level) {
1221
1164
  };
1222
1165
  }
1223
1166
  return {
1224
- claim: withClaimTrace(claim),
1225
- fetch: withFetchTrace(fetch),
1226
- ack: withAckTrace(ack),
1227
- block: withBlockTrace(block),
1228
- subscribe: withSubscribeTrace(subscribe)
1167
+ claim: traced(claim, (leased) => {
1168
+ if (leased.length) {
1169
+ const data = Object.fromEntries(
1170
+ leased.map(({ stream, at, retry }) => [stream, { at, retry }])
1171
+ );
1172
+ logger.trace(data, ">> lease");
1173
+ }
1174
+ }),
1175
+ fetch: traced(fetch, (fetched) => {
1176
+ const data = Object.fromEntries(
1177
+ fetched.map(({ stream, source, events }) => {
1178
+ const key = source ? `${stream}<-${source}` : stream;
1179
+ const value = Object.fromEntries(
1180
+ events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
1181
+ );
1182
+ return [key, value];
1183
+ })
1184
+ );
1185
+ logger.trace(data, ">> fetch");
1186
+ }),
1187
+ ack: traced(ack, (acked) => {
1188
+ if (acked.length) {
1189
+ const data = Object.fromEntries(
1190
+ acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1191
+ );
1192
+ logger.trace(data, ">> ack");
1193
+ }
1194
+ }),
1195
+ block: traced(block, (blocked) => {
1196
+ if (blocked.length) {
1197
+ const data = Object.fromEntries(
1198
+ blocked.map(({ stream, at, retry, error }) => [
1199
+ stream,
1200
+ { at, retry, error }
1201
+ ])
1202
+ );
1203
+ logger.trace(data, ">> block");
1204
+ }
1205
+ }),
1206
+ subscribe: traced(subscribe, (result, streams) => {
1207
+ if (result.subscribed) {
1208
+ const data = streams.map(({ stream }) => stream).join(" ");
1209
+ logger.trace(`>> correlate ${data}`);
1210
+ }
1211
+ })
1229
1212
  };
1230
1213
  }
1231
1214
 
1232
1215
  // src/act.ts
1233
- var logger4 = log();
1234
1216
  var Act = class {
1235
1217
  constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
1236
1218
  this.registry = registry;
1237
1219
  this._states = _states;
1238
1220
  this._batch_handlers = batchHandlers;
1239
- const level = log().level;
1240
- this._es = buildEs(level);
1241
- this._cd = buildDrain(level);
1221
+ this._es = buildEs(this._logger);
1222
+ this._cd = buildDrain(this._logger);
1242
1223
  const statics = [];
1243
1224
  for (const [name, register] of Object.entries(this.registry.events)) {
1244
1225
  if (register.reactions.size > 0) {
@@ -1256,6 +1237,11 @@ var Act = class {
1256
1237
  }
1257
1238
  }
1258
1239
  this._static_targets = statics;
1240
+ for (const merged of this._states.values()) {
1241
+ for (const eventName of Object.keys(merged.events)) {
1242
+ this._event_to_state.set(eventName, merged);
1243
+ }
1244
+ }
1259
1245
  dispose(() => {
1260
1246
  this._emitter.removeAllListeners();
1261
1247
  this.stop_correlations();
@@ -1302,6 +1288,15 @@ var Act = class {
1302
1288
  _es;
1303
1289
  /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
1304
1290
  _cd;
1291
+ /**
1292
+ * Event-name → owning state, computed at build time. The duplicate-event
1293
+ * guard in merge.ts ensures one event name maps to at most one state, so
1294
+ * this lookup is unambiguous. Used by `close()` to pick the right reducer
1295
+ * set when seeding a `restart` snapshot in multi-state apps.
1296
+ */
1297
+ _event_to_state = /* @__PURE__ */ new Map();
1298
+ /** Logger resolved at construction time (after user port configuration) */
1299
+ _logger = log();
1305
1300
  /**
1306
1301
  * Executes an action on a state instance, committing resulting events.
1307
1302
  *
@@ -1523,7 +1518,7 @@ var Act = class {
1523
1518
  if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
1524
1519
  const stream = lease.stream;
1525
1520
  let at = payloads.at(0).event.id, handled = 0;
1526
- lease.retry > 0 && logger4.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1521
+ lease.retry > 0 && this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1527
1522
  const doAction = this.do.bind(this);
1528
1523
  const scopedApp = {
1529
1524
  do: doAction,
@@ -1545,9 +1540,11 @@ var Act = class {
1545
1540
  at = event.id;
1546
1541
  handled++;
1547
1542
  } catch (error) {
1548
- logger4.error(error);
1543
+ this._logger.error(error);
1549
1544
  const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1550
- block2 && logger4.error(`Blocking ${stream} after ${lease.retry} retries.`);
1545
+ block2 && this._logger.error(
1546
+ `Blocking ${stream} after ${lease.retry} retries.`
1547
+ );
1551
1548
  return {
1552
1549
  lease,
1553
1550
  handled,
@@ -1577,15 +1574,17 @@ var Act = class {
1577
1574
  const stream = lease.stream;
1578
1575
  const events = payloads.map((p) => p.event);
1579
1576
  const at = events.at(-1).id;
1580
- lease.retry > 0 && logger4.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
1577
+ lease.retry > 0 && this._logger.warn(
1578
+ `Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
1579
+ );
1581
1580
  try {
1582
1581
  await batchHandler(events, stream);
1583
1582
  return { lease, handled: events.length, at };
1584
1583
  } catch (error) {
1585
- logger4.error(error);
1584
+ this._logger.error(error);
1586
1585
  const { options } = payloads[0];
1587
1586
  const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1588
- block2 && logger4.error(`Blocking ${stream} after ${lease.retry} retries.`);
1587
+ block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
1589
1588
  return {
1590
1589
  lease,
1591
1590
  handled: 0,
@@ -1711,7 +1710,7 @@ var Act = class {
1711
1710
  this._needs_drain = false;
1712
1711
  return result;
1713
1712
  } catch (error) {
1714
- logger4.error(error);
1713
+ this._logger.error(error);
1715
1714
  } finally {
1716
1715
  this._drain_locked = false;
1717
1716
  }
@@ -2012,16 +2011,24 @@ var Act = class {
2012
2011
  streams.map(async (s) => {
2013
2012
  let maxId = -1;
2014
2013
  let version = -1;
2014
+ let lastEventName;
2015
2015
  await store().query(
2016
2016
  (e) => {
2017
- if (e.name !== TOMBSTONE_EVENT) {
2017
+ if (e.name === TOMBSTONE_EVENT) return;
2018
+ if (maxId === -1) {
2018
2019
  maxId = e.id;
2019
2020
  version = e.version;
2020
2021
  }
2022
+ if (e.name !== SNAP_EVENT && lastEventName === void 0) {
2023
+ lastEventName = e.name;
2024
+ }
2021
2025
  },
2022
- { stream: s, stream_exact: true, backward: true, limit: 1 }
2026
+ // limit: 2 covers the typical snapshot-at-head case (snapshot is
2027
+ // always preceded by the domain event it captured). Streams with
2028
+ // unusual layouts fall back to no-seed via the lookup miss path.
2029
+ { stream: s, stream_exact: true, backward: true, limit: 2 }
2023
2030
  );
2024
- if (maxId >= 0) streamInfo.set(s, { maxId, version });
2031
+ if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
2025
2032
  })
2026
2033
  );
2027
2034
  const skipped = [];
@@ -2030,16 +2037,14 @@ var Act = class {
2030
2037
  safe = [...streamInfo.keys()];
2031
2038
  } else {
2032
2039
  const pendingSet = /* @__PURE__ */ new Set();
2033
- const leases = await store().claim(1e3, 1e3, (0, import_crypto2.randomUUID)(), 1);
2034
- if (leases.length) await store().ack(leases);
2035
- for (const lease of leases) {
2036
- const sourceRe = lease.source ? RegExp(lease.source) : void 0;
2040
+ await store().query_streams((position) => {
2041
+ const sourceRe = position.source ? RegExp(position.source) : void 0;
2037
2042
  for (const [stream, info] of streamInfo) {
2038
- if ((!sourceRe || sourceRe.test(stream)) && lease.at < info.maxId) {
2043
+ if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
2039
2044
  pendingSet.add(stream);
2040
2045
  }
2041
2046
  }
2042
- }
2047
+ });
2043
2048
  safe = [];
2044
2049
  for (const [stream] of streamInfo) {
2045
2050
  if (pendingSet.has(stream)) {
@@ -2079,16 +2084,21 @@ var Act = class {
2079
2084
  this.emit("closed", result2);
2080
2085
  return result2;
2081
2086
  }
2082
- const mergedState = [...this._states.values()][0];
2083
2087
  const seedStates = /* @__PURE__ */ new Map();
2084
- if (mergedState) {
2085
- await Promise.all(
2086
- guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
2087
- const snap2 = await this._es.load(mergedState, stream);
2088
- seedStates.set(stream, snap2.state);
2089
- })
2090
- );
2091
- }
2088
+ await Promise.all(
2089
+ guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
2090
+ const lastEventName = streamInfo.get(stream)?.lastEventName;
2091
+ const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
2092
+ if (!ownerState) {
2093
+ this._logger.error(
2094
+ `Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
2095
+ );
2096
+ return;
2097
+ }
2098
+ const snap2 = await this._es.load(ownerState, stream);
2099
+ seedStates.set(stream, snap2.state);
2100
+ })
2101
+ );
2092
2102
  for (const stream of guarded) {
2093
2103
  const archiveFn = targetMap.get(stream)?.archive;
2094
2104
  if (archiveFn) await archiveFn();
@@ -2191,7 +2201,7 @@ var Act = class {
2191
2201
  if (!made_progress) break;
2192
2202
  }
2193
2203
  if (lastDrain) this.emit("settled", lastDrain);
2194
- })().catch((err) => logger4.error(err)).finally(() => {
2204
+ })().catch((err) => this._logger.error(err)).finally(() => {
2195
2205
  this._settling = false;
2196
2206
  });
2197
2207
  }, debounceMs);
@@ -2199,6 +2209,14 @@ var Act = class {
2199
2209
  };
2200
2210
 
2201
2211
  // src/act-builder.ts
2212
+ function registerBatchHandler(proj, batchHandlers) {
2213
+ if (!proj.batchHandler || !proj.target) return;
2214
+ const existing = batchHandlers.get(proj.target);
2215
+ if (existing && existing !== proj.batchHandler) {
2216
+ throw new Error(`Duplicate batch handler for target "${proj.target}"`);
2217
+ }
2218
+ batchHandlers.set(proj.target, proj.batchHandler);
2219
+ }
2202
2220
  function act(states = /* @__PURE__ */ new Map(), registry = {
2203
2221
  actions: {},
2204
2222
  events: {}
@@ -2233,9 +2251,7 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2233
2251
  },
2234
2252
  withProjection: (proj) => {
2235
2253
  mergeProjection(proj, registry.events);
2236
- if (proj.batchHandler && proj.target) {
2237
- batchHandlers.set(proj.target, proj.batchHandler);
2238
- }
2254
+ registerBatchHandler(proj, batchHandlers);
2239
2255
  return act(
2240
2256
  states,
2241
2257
  registry,
@@ -2281,9 +2297,7 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2281
2297
  build: () => {
2282
2298
  for (const proj of pendingProjections) {
2283
2299
  mergeProjection(proj, registry.events);
2284
- if (proj.batchHandler && proj.target) {
2285
- batchHandlers.set(proj.target, proj.batchHandler);
2286
- }
2300
+ registerBatchHandler(proj, batchHandlers);
2287
2301
  }
2288
2302
  return new Act(
2289
2303
  registry,
@@ -2438,8 +2452,9 @@ function state(entry) {
2438
2452
  emits(events) {
2439
2453
  const defaultPatch = Object.fromEntries(
2440
2454
  Object.keys(events).map((k) => {
2441
- const fn = ({ data }) => data;
2442
- fn._passthrough = true;
2455
+ const fn = Object.assign(({ data }) => data, {
2456
+ _passthrough: true
2457
+ });
2443
2458
  return [k, fn];
2444
2459
  })
2445
2460
  );