@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.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
 
@@ -672,6 +667,136 @@ process.once("unhandledRejection", async (arg) => {
672
667
  import { randomUUID as randomUUID2 } from "crypto";
673
668
  import EventEmitter from "events";
674
669
 
670
+ // src/internal/merge.ts
671
+ import { ZodObject } from "zod";
672
+ function baseTypeName(zodType) {
673
+ let t = zodType;
674
+ while (typeof t.unwrap === "function") {
675
+ t = t.unwrap();
676
+ }
677
+ return t.constructor.name;
678
+ }
679
+ function mergeSchemas(existing, incoming, stateName) {
680
+ if (existing instanceof ZodObject && incoming instanceof ZodObject) {
681
+ const existingShape = existing.shape;
682
+ const incomingShape = incoming.shape;
683
+ for (const key of Object.keys(incomingShape)) {
684
+ if (key in existingShape) {
685
+ const existingBase = baseTypeName(existingShape[key]);
686
+ const incomingBase = baseTypeName(incomingShape[key]);
687
+ if (existingBase !== incomingBase) {
688
+ throw new Error(
689
+ `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
690
+ );
691
+ }
692
+ }
693
+ }
694
+ return existing.extend(incomingShape);
695
+ }
696
+ return existing;
697
+ }
698
+ function mergeInits(existing, incoming) {
699
+ return () => ({ ...existing(), ...incoming() });
700
+ }
701
+ function registerState(state2, states, actions, events) {
702
+ const existing = states.get(state2.name);
703
+ if (existing) {
704
+ mergeIntoExisting(state2, existing, states, actions, events);
705
+ } else {
706
+ registerNewState(state2, states, actions, events);
707
+ }
708
+ }
709
+ function registerNewState(state2, states, actions, events) {
710
+ states.set(state2.name, state2);
711
+ for (const name of Object.keys(state2.actions)) {
712
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
713
+ actions[name] = state2;
714
+ }
715
+ for (const name of Object.keys(state2.events)) {
716
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
717
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
718
+ }
719
+ }
720
+ function mergeIntoExisting(state2, existing, states, actions, events) {
721
+ for (const name of Object.keys(state2.actions)) {
722
+ if (existing.actions[name] === state2.actions[name]) continue;
723
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
724
+ }
725
+ for (const name of Object.keys(state2.events)) {
726
+ if (existing.events[name] === state2.events[name]) continue;
727
+ if (existing.events[name]) continue;
728
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
729
+ }
730
+ const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
731
+ const merged = {
732
+ ...existing,
733
+ state: mergeSchemas(existing.state, state2.state, state2.name),
734
+ init: mergeInits(existing.init, state2.init),
735
+ events: { ...existing.events, ...state2.events },
736
+ actions: { ...existing.actions, ...state2.actions },
737
+ patch: mergedPatch,
738
+ on: { ...existing.on, ...state2.on },
739
+ given: { ...existing.given, ...state2.given },
740
+ snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
741
+ throw new Error(
742
+ `Duplicate snap strategy for state "${state2.name}"`
743
+ );
744
+ })() : state2.snap || existing.snap
745
+ };
746
+ states.set(state2.name, merged);
747
+ for (const name of Object.keys(merged.actions)) {
748
+ actions[name] = merged;
749
+ }
750
+ for (const name of Object.keys(state2.events)) {
751
+ if (events[name]) continue;
752
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
753
+ }
754
+ }
755
+ function mergePatches(existing, incoming, stateName) {
756
+ const merged = { ...existing };
757
+ for (const name of Object.keys(incoming)) {
758
+ const existingP = existing[name];
759
+ const incomingP = incoming[name];
760
+ if (!existingP) {
761
+ merged[name] = incomingP;
762
+ continue;
763
+ }
764
+ const existingIsDefault = existingP._passthrough;
765
+ const incomingIsDefault = incomingP._passthrough;
766
+ if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
767
+ throw new Error(
768
+ `Duplicate custom patch for event "${name}" in state "${stateName}"`
769
+ );
770
+ }
771
+ if (existingIsDefault && !incomingIsDefault) {
772
+ merged[name] = incomingP;
773
+ }
774
+ }
775
+ return merged;
776
+ }
777
+ function mergeProjection(proj, events) {
778
+ for (const eventName of Object.keys(proj.events)) {
779
+ const projRegister = proj.events[eventName];
780
+ const existing = events[eventName];
781
+ if (!existing) {
782
+ events[eventName] = {
783
+ schema: projRegister.schema,
784
+ reactions: new Map(projRegister.reactions)
785
+ };
786
+ } else {
787
+ for (const [name, reaction] of projRegister.reactions) {
788
+ let key = name;
789
+ while (existing.reactions.has(key)) key = `${key}_p`;
790
+ existing.reactions.set(key, reaction);
791
+ }
792
+ }
793
+ }
794
+ }
795
+ var _this_ = ({ stream }) => ({
796
+ source: stream,
797
+ target: stream
798
+ });
799
+
675
800
  // src/internal/drain.ts
676
801
  var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
677
802
  async function fetch(leased, eventLimit) {
@@ -694,7 +819,6 @@ var subscribe = (streams) => store().subscribe(streams);
694
819
  // src/internal/event-sourcing.ts
695
820
  import { patch } from "@rotorsoft/act-patch";
696
821
  import { randomUUID } from "crypto";
697
- var logger2 = log();
698
822
  async function snap(snapshot) {
699
823
  try {
700
824
  const { id, stream, name, meta, version } = snapshot.event;
@@ -709,11 +833,11 @@ async function snap(snapshot) {
709
833
  // IMPORTANT! - state events are committed right after the snapshot event
710
834
  );
711
835
  } catch (error) {
712
- logger2.error(error);
836
+ log().error(error);
713
837
  }
714
838
  }
715
839
  async function load(me, stream, callback, asOf) {
716
- const timeTravel = asOf && (asOf.before !== void 0 || asOf.created_before !== void 0 || asOf.created_after !== void 0 || asOf.limit !== void 0);
840
+ const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
717
841
  const cached = timeTravel ? void 0 : await cache().get(stream);
718
842
  let state2 = cached?.state ?? (me.init ? me.init() : {});
719
843
  let patches = cached?.patches ?? 0;
@@ -729,6 +853,10 @@ async function load(me, stream, callback, asOf) {
729
853
  } else if (me.patch[e.name]) {
730
854
  state2 = patch(state2, me.patch[e.name](event, state2));
731
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
+ );
732
860
  }
733
861
  callback && callback({ event, state: state2, patches, snaps });
734
862
  },
@@ -797,7 +925,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
797
925
  reactingTo ? void 0 : expected
798
926
  );
799
927
  } catch (error) {
800
- if (error.name === "ERR_CONCURRENCY") {
928
+ if (error instanceof ConcurrencyError) {
801
929
  await cache().invalidate(stream);
802
930
  }
803
931
  throw error;
@@ -811,241 +939,56 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
811
939
  });
812
940
  const last = snapshots.at(-1);
813
941
  const snapped = me.snap && me.snap(last);
814
- void cache().set(stream, {
942
+ cache().set(stream, {
815
943
  state: last.state,
816
944
  version: last.event.version,
817
945
  event_id: last.event.id,
818
946
  patches: snapped ? 0 : last.patches,
819
947
  snaps: snapped ? last.snaps + 1 : last.snaps
820
- });
948
+ }).catch((err) => log().error(err));
821
949
  if (snapped) void snap(last);
822
950
  return snapshots;
823
951
  }
824
952
 
825
- // src/internal/merge.ts
826
- import { ZodObject } from "zod";
827
- function baseTypeName(zodType) {
828
- let t = zodType;
829
- while (typeof t.unwrap === "function") {
830
- t = t.unwrap();
831
- }
832
- return t.constructor.name;
833
- }
834
- function mergeSchemas(existing, incoming, stateName) {
835
- if (existing instanceof ZodObject && incoming instanceof ZodObject) {
836
- const existingShape = existing.shape;
837
- const incomingShape = incoming.shape;
838
- for (const key of Object.keys(incomingShape)) {
839
- if (key in existingShape) {
840
- const existingBase = baseTypeName(existingShape[key]);
841
- const incomingBase = baseTypeName(incomingShape[key]);
842
- if (existingBase !== incomingBase) {
843
- throw new Error(
844
- `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
845
- );
846
- }
847
- }
848
- }
849
- return existing.extend(incomingShape);
850
- }
851
- return existing;
852
- }
853
- function mergeInits(existing, incoming) {
854
- return () => ({ ...existing(), ...incoming() });
855
- }
856
- function registerState(state2, states, actions, events) {
857
- if (states.has(state2.name)) {
858
- const existing = states.get(state2.name);
859
- for (const name of Object.keys(state2.actions)) {
860
- if (existing.actions[name] === state2.actions[name]) continue;
861
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
862
- }
863
- for (const name of Object.keys(state2.events)) {
864
- if (existing.events[name] === state2.events[name]) continue;
865
- if (existing.events[name]) continue;
866
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
867
- }
868
- const mergedPatch = { ...existing.patch };
869
- for (const name of Object.keys(state2.patch)) {
870
- const existingP = existing.patch[name];
871
- const incomingP = state2.patch[name];
872
- if (!existingP) {
873
- mergedPatch[name] = incomingP;
874
- } else {
875
- const existingIsDefault = existingP._passthrough;
876
- const incomingIsDefault = incomingP._passthrough;
877
- if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
878
- throw new Error(
879
- `Duplicate custom patch for event "${name}" in state "${state2.name}"`
880
- );
881
- }
882
- if (existingIsDefault && !incomingIsDefault) {
883
- mergedPatch[name] = incomingP;
884
- }
885
- }
886
- }
887
- const merged = {
888
- ...existing,
889
- state: mergeSchemas(existing.state, state2.state, state2.name),
890
- init: mergeInits(existing.init, state2.init),
891
- events: { ...existing.events, ...state2.events },
892
- actions: { ...existing.actions, ...state2.actions },
893
- patch: mergedPatch,
894
- on: { ...existing.on, ...state2.on },
895
- given: { ...existing.given, ...state2.given },
896
- snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
897
- throw new Error(
898
- `Duplicate snap strategy for state "${state2.name}"`
899
- );
900
- })() : state2.snap || existing.snap
901
- };
902
- states.set(state2.name, merged);
903
- for (const name of Object.keys(merged.actions)) {
904
- actions[name] = merged;
905
- }
906
- for (const name of Object.keys(state2.events)) {
907
- if (events[name]) continue;
908
- events[name] = {
909
- schema: state2.events[name],
910
- reactions: /* @__PURE__ */ new Map()
911
- };
912
- }
913
- } else {
914
- states.set(state2.name, state2);
915
- for (const name of Object.keys(state2.actions)) {
916
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
917
- actions[name] = state2;
918
- }
919
- for (const name of Object.keys(state2.events)) {
920
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
921
- events[name] = {
922
- schema: state2.events[name],
923
- reactions: /* @__PURE__ */ new Map()
924
- };
925
- }
926
- }
927
- }
928
- function mergeProjection(proj, events) {
929
- for (const eventName of Object.keys(proj.events)) {
930
- const projRegister = proj.events[eventName];
931
- const existing = events[eventName];
932
- if (!existing) {
933
- events[eventName] = {
934
- schema: projRegister.schema,
935
- reactions: new Map(projRegister.reactions)
936
- };
937
- } else {
938
- for (const [name, reaction] of projRegister.reactions) {
939
- let key = name;
940
- while (existing.reactions.has(key)) key = `${key}_p`;
941
- existing.reactions.set(key, reaction);
942
- }
943
- }
944
- }
945
- }
946
- var _this_ = ({ stream }) => ({
947
- source: stream,
948
- target: stream
949
- });
950
-
951
953
  // src/internal/tracing.ts
952
- var logger3 = log();
953
- var withSnapTrace = (inner) => async (snapshot) => {
954
- logger3.trace(
955
- `\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
956
- );
957
- return inner(snapshot);
958
- };
959
- var withLoadTrace = (inner) => async (me, stream, callback, asOf) => {
960
- logger3.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
961
- return inner(me, stream, callback, asOf);
962
- };
963
- var withActionTrace = (inner) => async (me, action2, target, payload, reactingTo, skipValidation) => {
964
- logger3.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
965
- const snapshots = await inner(
966
- me,
967
- action2,
968
- target,
969
- payload,
970
- reactingTo,
971
- skipValidation
972
- );
973
- const committed = snapshots.filter((s) => s.event);
974
- if (committed.length) {
975
- logger3.trace(
976
- committed.map((s) => s.event.data),
977
- `\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
978
- );
979
- }
980
- return snapshots;
981
- };
982
- function buildEs(level) {
983
- if (level !== "trace") {
954
+ var traced = (inner, exit, entry) => (async (...args) => {
955
+ entry?.(...args);
956
+ const result = await inner(...args);
957
+ exit?.(result, ...args);
958
+ return result;
959
+ });
960
+ function buildEs(logger) {
961
+ if (logger.level !== "trace") {
984
962
  return { snap, load, action };
985
963
  }
986
964
  return {
987
- snap: withSnapTrace(snap),
988
- load: withLoadTrace(load),
989
- action: withActionTrace(action)
965
+ snap: traced(snap, void 0, (snapshot) => {
966
+ logger.trace(
967
+ `\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
968
+ );
969
+ }),
970
+ load: traced(load, void 0, (_me, stream, _cb, asOf) => {
971
+ logger.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
972
+ }),
973
+ action: traced(
974
+ action,
975
+ (snapshots, _me, _action, target) => {
976
+ const committed = snapshots.filter((s) => s.event);
977
+ if (committed.length) {
978
+ logger.trace(
979
+ committed.map((s) => s.event.data),
980
+ `\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
981
+ );
982
+ }
983
+ },
984
+ (_me, action2, target, payload) => {
985
+ logger.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
986
+ }
987
+ )
990
988
  };
991
989
  }
992
- var withClaimTrace = (inner) => async (lagging, leading, by, millis) => {
993
- const leased = await inner(lagging, leading, by, millis);
994
- if (leased.length) {
995
- const data = Object.fromEntries(
996
- leased.map(({ stream, at, retry }) => [stream, { at, retry }])
997
- );
998
- logger3.trace(data, ">> lease");
999
- }
1000
- return leased;
1001
- };
1002
- var withFetchTrace = (inner) => async (leased, eventLimit) => {
1003
- const fetched = await inner(leased, eventLimit);
1004
- const data = Object.fromEntries(
1005
- fetched.map(({ stream, source, events }) => {
1006
- const key = source ? `${stream}<-${source}` : stream;
1007
- const value = Object.fromEntries(
1008
- events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
1009
- );
1010
- return [key, value];
1011
- })
1012
- );
1013
- logger3.trace(data, ">> fetch");
1014
- return fetched;
1015
- };
1016
- var withAckTrace = (inner) => async (leases) => {
1017
- const acked = await inner(leases);
1018
- if (acked.length) {
1019
- const data = Object.fromEntries(
1020
- acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1021
- );
1022
- logger3.trace(data, ">> ack");
1023
- }
1024
- return acked;
1025
- };
1026
- var withBlockTrace = (inner) => async (leases) => {
1027
- const blocked = await inner(leases);
1028
- if (blocked.length) {
1029
- const data = Object.fromEntries(
1030
- blocked.map(({ stream, at, retry, error }) => [
1031
- stream,
1032
- { at, retry, error }
1033
- ])
1034
- );
1035
- logger3.trace(data, ">> block");
1036
- }
1037
- return blocked;
1038
- };
1039
- var withSubscribeTrace = (inner) => async (streams) => {
1040
- const result = await inner(streams);
1041
- if (result.subscribed) {
1042
- const data = streams.map(({ stream }) => stream).join(" ");
1043
- logger3.trace(`>> correlate ${data}`);
1044
- }
1045
- return result;
1046
- };
1047
- function buildDrain(level) {
1048
- if (level !== "trace") {
990
+ function buildDrain(logger) {
991
+ if (logger.level !== "trace") {
1049
992
  return {
1050
993
  claim,
1051
994
  fetch,
@@ -1055,24 +998,62 @@ function buildDrain(level) {
1055
998
  };
1056
999
  }
1057
1000
  return {
1058
- claim: withClaimTrace(claim),
1059
- fetch: withFetchTrace(fetch),
1060
- ack: withAckTrace(ack),
1061
- block: withBlockTrace(block),
1062
- subscribe: withSubscribeTrace(subscribe)
1001
+ claim: traced(claim, (leased) => {
1002
+ if (leased.length) {
1003
+ const data = Object.fromEntries(
1004
+ leased.map(({ stream, at, retry }) => [stream, { at, retry }])
1005
+ );
1006
+ logger.trace(data, ">> lease");
1007
+ }
1008
+ }),
1009
+ fetch: traced(fetch, (fetched) => {
1010
+ const data = Object.fromEntries(
1011
+ fetched.map(({ stream, source, events }) => {
1012
+ const key = source ? `${stream}<-${source}` : stream;
1013
+ const value = Object.fromEntries(
1014
+ events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
1015
+ );
1016
+ return [key, value];
1017
+ })
1018
+ );
1019
+ logger.trace(data, ">> fetch");
1020
+ }),
1021
+ ack: traced(ack, (acked) => {
1022
+ if (acked.length) {
1023
+ const data = Object.fromEntries(
1024
+ acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1025
+ );
1026
+ logger.trace(data, ">> ack");
1027
+ }
1028
+ }),
1029
+ block: traced(block, (blocked) => {
1030
+ if (blocked.length) {
1031
+ const data = Object.fromEntries(
1032
+ blocked.map(({ stream, at, retry, error }) => [
1033
+ stream,
1034
+ { at, retry, error }
1035
+ ])
1036
+ );
1037
+ logger.trace(data, ">> block");
1038
+ }
1039
+ }),
1040
+ subscribe: traced(subscribe, (result, streams) => {
1041
+ if (result.subscribed) {
1042
+ const data = streams.map(({ stream }) => stream).join(" ");
1043
+ logger.trace(`>> correlate ${data}`);
1044
+ }
1045
+ })
1063
1046
  };
1064
1047
  }
1065
1048
 
1066
1049
  // src/act.ts
1067
- var logger4 = log();
1068
1050
  var Act = class {
1069
1051
  constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
1070
1052
  this.registry = registry;
1071
1053
  this._states = _states;
1072
1054
  this._batch_handlers = batchHandlers;
1073
- const level = log().level;
1074
- this._es = buildEs(level);
1075
- this._cd = buildDrain(level);
1055
+ this._es = buildEs(this._logger);
1056
+ this._cd = buildDrain(this._logger);
1076
1057
  const statics = [];
1077
1058
  for (const [name, register] of Object.entries(this.registry.events)) {
1078
1059
  if (register.reactions.size > 0) {
@@ -1090,6 +1071,11 @@ var Act = class {
1090
1071
  }
1091
1072
  }
1092
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
+ }
1093
1079
  dispose(() => {
1094
1080
  this._emitter.removeAllListeners();
1095
1081
  this.stop_correlations();
@@ -1136,6 +1122,15 @@ var Act = class {
1136
1122
  _es;
1137
1123
  /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
1138
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();
1132
+ /** Logger resolved at construction time (after user port configuration) */
1133
+ _logger = log();
1139
1134
  /**
1140
1135
  * Executes an action on a state instance, committing resulting events.
1141
1136
  *
@@ -1357,7 +1352,7 @@ var Act = class {
1357
1352
  if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
1358
1353
  const stream = lease.stream;
1359
1354
  let at = payloads.at(0).event.id, handled = 0;
1360
- lease.retry > 0 && logger4.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1355
+ lease.retry > 0 && this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1361
1356
  const doAction = this.do.bind(this);
1362
1357
  const scopedApp = {
1363
1358
  do: doAction,
@@ -1379,9 +1374,11 @@ var Act = class {
1379
1374
  at = event.id;
1380
1375
  handled++;
1381
1376
  } catch (error) {
1382
- logger4.error(error);
1377
+ this._logger.error(error);
1383
1378
  const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1384
- block2 && logger4.error(`Blocking ${stream} after ${lease.retry} retries.`);
1379
+ block2 && this._logger.error(
1380
+ `Blocking ${stream} after ${lease.retry} retries.`
1381
+ );
1385
1382
  return {
1386
1383
  lease,
1387
1384
  handled,
@@ -1411,15 +1408,17 @@ var Act = class {
1411
1408
  const stream = lease.stream;
1412
1409
  const events = payloads.map((p) => p.event);
1413
1410
  const at = events.at(-1).id;
1414
- lease.retry > 0 && logger4.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
1411
+ lease.retry > 0 && this._logger.warn(
1412
+ `Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
1413
+ );
1415
1414
  try {
1416
1415
  await batchHandler(events, stream);
1417
1416
  return { lease, handled: events.length, at };
1418
1417
  } catch (error) {
1419
- logger4.error(error);
1418
+ this._logger.error(error);
1420
1419
  const { options } = payloads[0];
1421
1420
  const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1422
- block2 && logger4.error(`Blocking ${stream} after ${lease.retry} retries.`);
1421
+ block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
1423
1422
  return {
1424
1423
  lease,
1425
1424
  handled: 0,
@@ -1545,7 +1544,7 @@ var Act = class {
1545
1544
  this._needs_drain = false;
1546
1545
  return result;
1547
1546
  } catch (error) {
1548
- logger4.error(error);
1547
+ this._logger.error(error);
1549
1548
  } finally {
1550
1549
  this._drain_locked = false;
1551
1550
  }
@@ -1846,16 +1845,24 @@ var Act = class {
1846
1845
  streams.map(async (s) => {
1847
1846
  let maxId = -1;
1848
1847
  let version = -1;
1848
+ let lastEventName;
1849
1849
  await store().query(
1850
1850
  (e) => {
1851
- if (e.name !== TOMBSTONE_EVENT) {
1851
+ if (e.name === TOMBSTONE_EVENT) return;
1852
+ if (maxId === -1) {
1852
1853
  maxId = e.id;
1853
1854
  version = e.version;
1854
1855
  }
1856
+ if (e.name !== SNAP_EVENT && lastEventName === void 0) {
1857
+ lastEventName = e.name;
1858
+ }
1855
1859
  },
1856
- { 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 }
1857
1864
  );
1858
- if (maxId >= 0) streamInfo.set(s, { maxId, version });
1865
+ if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
1859
1866
  })
1860
1867
  );
1861
1868
  const skipped = [];
@@ -1864,16 +1871,14 @@ var Act = class {
1864
1871
  safe = [...streamInfo.keys()];
1865
1872
  } else {
1866
1873
  const pendingSet = /* @__PURE__ */ new Set();
1867
- const leases = await store().claim(1e3, 1e3, randomUUID2(), 1);
1868
- if (leases.length) await store().ack(leases);
1869
- for (const lease of leases) {
1870
- 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;
1871
1876
  for (const [stream, info] of streamInfo) {
1872
- if ((!sourceRe || sourceRe.test(stream)) && lease.at < info.maxId) {
1877
+ if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
1873
1878
  pendingSet.add(stream);
1874
1879
  }
1875
1880
  }
1876
- }
1881
+ });
1877
1882
  safe = [];
1878
1883
  for (const [stream] of streamInfo) {
1879
1884
  if (pendingSet.has(stream)) {
@@ -1913,16 +1918,21 @@ var Act = class {
1913
1918
  this.emit("closed", result2);
1914
1919
  return result2;
1915
1920
  }
1916
- const mergedState = [...this._states.values()][0];
1917
1921
  const seedStates = /* @__PURE__ */ new Map();
1918
- if (mergedState) {
1919
- await Promise.all(
1920
- guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
1921
- const snap2 = await this._es.load(mergedState, stream);
1922
- seedStates.set(stream, snap2.state);
1923
- })
1924
- );
1925
- }
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
+ );
1926
1936
  for (const stream of guarded) {
1927
1937
  const archiveFn = targetMap.get(stream)?.archive;
1928
1938
  if (archiveFn) await archiveFn();
@@ -2025,7 +2035,7 @@ var Act = class {
2025
2035
  if (!made_progress) break;
2026
2036
  }
2027
2037
  if (lastDrain) this.emit("settled", lastDrain);
2028
- })().catch((err) => logger4.error(err)).finally(() => {
2038
+ })().catch((err) => this._logger.error(err)).finally(() => {
2029
2039
  this._settling = false;
2030
2040
  });
2031
2041
  }, debounceMs);
@@ -2033,6 +2043,14 @@ var Act = class {
2033
2043
  };
2034
2044
 
2035
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
+ }
2036
2054
  function act(states = /* @__PURE__ */ new Map(), registry = {
2037
2055
  actions: {},
2038
2056
  events: {}
@@ -2067,9 +2085,7 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2067
2085
  },
2068
2086
  withProjection: (proj) => {
2069
2087
  mergeProjection(proj, registry.events);
2070
- if (proj.batchHandler && proj.target) {
2071
- batchHandlers.set(proj.target, proj.batchHandler);
2072
- }
2088
+ registerBatchHandler(proj, batchHandlers);
2073
2089
  return act(
2074
2090
  states,
2075
2091
  registry,
@@ -2115,9 +2131,7 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2115
2131
  build: () => {
2116
2132
  for (const proj of pendingProjections) {
2117
2133
  mergeProjection(proj, registry.events);
2118
- if (proj.batchHandler && proj.target) {
2119
- batchHandlers.set(proj.target, proj.batchHandler);
2120
- }
2134
+ registerBatchHandler(proj, batchHandlers);
2121
2135
  }
2122
2136
  return new Act(
2123
2137
  registry,
@@ -2272,8 +2286,9 @@ function state(entry) {
2272
2286
  emits(events) {
2273
2287
  const defaultPatch = Object.fromEntries(
2274
2288
  Object.keys(events).map((k) => {
2275
- const fn = ({ data }) => data;
2276
- fn._passthrough = true;
2289
+ const fn = Object.assign(({ data }) => data, {
2290
+ _passthrough: true
2291
+ });
2277
2292
  return [k, fn];
2278
2293
  })
2279
2294
  );