@rotorsoft/act 0.32.0 → 0.32.2
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/.tsbuildinfo +1 -1
- package/dist/@types/act-builder.d.ts.map +1 -1
- package/dist/@types/act.d.ts +10 -10
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryStore.d.ts +2 -4
- package/dist/@types/adapters/InMemoryStore.d.ts.map +1 -1
- package/dist/@types/internal/drain.d.ts +39 -0
- package/dist/@types/internal/drain.d.ts.map +1 -0
- package/dist/@types/{event-sourcing.d.ts → internal/event-sourcing.d.ts} +15 -2
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -0
- package/dist/@types/internal/index.d.ts +22 -0
- package/dist/@types/internal/index.d.ts.map +1 -0
- package/dist/@types/internal/merge.d.ts +19 -0
- package/dist/@types/internal/merge.d.ts.map +1 -0
- package/dist/@types/internal/tracing.d.ts +36 -0
- package/dist/@types/internal/tracing.d.ts.map +1 -0
- package/dist/@types/ports.d.ts +1 -25
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/projection-builder.d.ts.map +1 -1
- package/dist/@types/slice-builder.d.ts.map +1 -1
- package/dist/@types/state-builder.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +13 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +2 -6
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +9 -3
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/index.cjs +291 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +291 -251
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/@types/event-sourcing.d.ts.map +0 -1
- package/dist/@types/merge.d.ts +0 -43
- package/dist/@types/merge.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -53,7 +53,6 @@ __export(index_exports, {
|
|
|
53
53
|
ValidationError: () => ValidationError,
|
|
54
54
|
ZodEmpty: () => ZodEmpty,
|
|
55
55
|
act: () => act,
|
|
56
|
-
build_tracer: () => build_tracer,
|
|
57
56
|
cache: () => cache,
|
|
58
57
|
config: () => config,
|
|
59
58
|
dispose: () => dispose,
|
|
@@ -815,63 +814,6 @@ function dispose(disposer) {
|
|
|
815
814
|
}
|
|
816
815
|
var SNAP_EVENT = "__snapshot__";
|
|
817
816
|
var TOMBSTONE_EVENT = "__tombstone__";
|
|
818
|
-
function build_tracer(logLevel2) {
|
|
819
|
-
if (logLevel2 === "trace") {
|
|
820
|
-
const logger4 = log();
|
|
821
|
-
return {
|
|
822
|
-
fetched: (fetched) => {
|
|
823
|
-
const data = Object.fromEntries(
|
|
824
|
-
fetched.map(({ stream, source, events }) => {
|
|
825
|
-
const key = source ? `${stream}<-${source}` : stream;
|
|
826
|
-
const value = Object.fromEntries(
|
|
827
|
-
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
828
|
-
);
|
|
829
|
-
return [key, value];
|
|
830
|
-
})
|
|
831
|
-
);
|
|
832
|
-
logger4.trace(data, ">> fetch");
|
|
833
|
-
},
|
|
834
|
-
correlated: (streams) => {
|
|
835
|
-
const data = streams.map(({ stream }) => stream).join(" ");
|
|
836
|
-
logger4.trace(`>> correlate ${data}`);
|
|
837
|
-
},
|
|
838
|
-
leased: (leases) => {
|
|
839
|
-
const data = Object.fromEntries(
|
|
840
|
-
leases.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
841
|
-
);
|
|
842
|
-
logger4.trace(data, ">> lease");
|
|
843
|
-
},
|
|
844
|
-
acked: (leases) => {
|
|
845
|
-
const data = Object.fromEntries(
|
|
846
|
-
leases.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
847
|
-
);
|
|
848
|
-
logger4.trace(data, ">> ack");
|
|
849
|
-
},
|
|
850
|
-
blocked: (leases) => {
|
|
851
|
-
const data = Object.fromEntries(
|
|
852
|
-
leases.map(({ stream, at, retry, error }) => [
|
|
853
|
-
stream,
|
|
854
|
-
{ at, retry, error }
|
|
855
|
-
])
|
|
856
|
-
);
|
|
857
|
-
logger4.trace(data, ">> block");
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
} else {
|
|
861
|
-
return {
|
|
862
|
-
fetched: () => {
|
|
863
|
-
},
|
|
864
|
-
correlated: () => {
|
|
865
|
-
},
|
|
866
|
-
leased: () => {
|
|
867
|
-
},
|
|
868
|
-
acked: () => {
|
|
869
|
-
},
|
|
870
|
-
blocked: () => {
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
817
|
|
|
876
818
|
// src/signals.ts
|
|
877
819
|
var logger = log();
|
|
@@ -896,14 +838,162 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
896
838
|
var import_crypto2 = require("crypto");
|
|
897
839
|
var import_events = __toESM(require("events"), 1);
|
|
898
840
|
|
|
899
|
-
// src/
|
|
841
|
+
// src/internal/merge.ts
|
|
842
|
+
var import_zod4 = require("zod");
|
|
843
|
+
function baseTypeName(zodType) {
|
|
844
|
+
let t = zodType;
|
|
845
|
+
while (typeof t.unwrap === "function") {
|
|
846
|
+
t = t.unwrap();
|
|
847
|
+
}
|
|
848
|
+
return t.constructor.name;
|
|
849
|
+
}
|
|
850
|
+
function mergeSchemas(existing, incoming, stateName) {
|
|
851
|
+
if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
|
|
852
|
+
const existingShape = existing.shape;
|
|
853
|
+
const incomingShape = incoming.shape;
|
|
854
|
+
for (const key of Object.keys(incomingShape)) {
|
|
855
|
+
if (key in existingShape) {
|
|
856
|
+
const existingBase = baseTypeName(existingShape[key]);
|
|
857
|
+
const incomingBase = baseTypeName(incomingShape[key]);
|
|
858
|
+
if (existingBase !== incomingBase) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return existing.extend(incomingShape);
|
|
866
|
+
}
|
|
867
|
+
return existing;
|
|
868
|
+
}
|
|
869
|
+
function mergeInits(existing, incoming) {
|
|
870
|
+
return () => ({ ...existing(), ...incoming() });
|
|
871
|
+
}
|
|
872
|
+
function registerState(state2, states, actions, events) {
|
|
873
|
+
const existing = states.get(state2.name);
|
|
874
|
+
if (existing) {
|
|
875
|
+
mergeIntoExisting(state2, existing, states, actions, events);
|
|
876
|
+
} else {
|
|
877
|
+
registerNewState(state2, states, actions, events);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function registerNewState(state2, states, actions, events) {
|
|
881
|
+
states.set(state2.name, state2);
|
|
882
|
+
for (const name of Object.keys(state2.actions)) {
|
|
883
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
884
|
+
actions[name] = state2;
|
|
885
|
+
}
|
|
886
|
+
for (const name of Object.keys(state2.events)) {
|
|
887
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
888
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
892
|
+
for (const name of Object.keys(state2.actions)) {
|
|
893
|
+
if (existing.actions[name] === state2.actions[name]) continue;
|
|
894
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
895
|
+
}
|
|
896
|
+
for (const name of Object.keys(state2.events)) {
|
|
897
|
+
if (existing.events[name] === state2.events[name]) continue;
|
|
898
|
+
if (existing.events[name]) continue;
|
|
899
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
900
|
+
}
|
|
901
|
+
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
902
|
+
const merged = {
|
|
903
|
+
...existing,
|
|
904
|
+
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
905
|
+
init: mergeInits(existing.init, state2.init),
|
|
906
|
+
events: { ...existing.events, ...state2.events },
|
|
907
|
+
actions: { ...existing.actions, ...state2.actions },
|
|
908
|
+
patch: mergedPatch,
|
|
909
|
+
on: { ...existing.on, ...state2.on },
|
|
910
|
+
given: { ...existing.given, ...state2.given },
|
|
911
|
+
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`Duplicate snap strategy for state "${state2.name}"`
|
|
914
|
+
);
|
|
915
|
+
})() : state2.snap || existing.snap
|
|
916
|
+
};
|
|
917
|
+
states.set(state2.name, merged);
|
|
918
|
+
for (const name of Object.keys(merged.actions)) {
|
|
919
|
+
actions[name] = merged;
|
|
920
|
+
}
|
|
921
|
+
for (const name of Object.keys(state2.events)) {
|
|
922
|
+
if (events[name]) continue;
|
|
923
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function mergePatches(existing, incoming, stateName) {
|
|
927
|
+
const merged = { ...existing };
|
|
928
|
+
for (const name of Object.keys(incoming)) {
|
|
929
|
+
const existingP = existing[name];
|
|
930
|
+
const incomingP = incoming[name];
|
|
931
|
+
if (!existingP) {
|
|
932
|
+
merged[name] = incomingP;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const existingIsDefault = existingP._passthrough;
|
|
936
|
+
const incomingIsDefault = incomingP._passthrough;
|
|
937
|
+
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`Duplicate custom patch for event "${name}" in state "${stateName}"`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (existingIsDefault && !incomingIsDefault) {
|
|
943
|
+
merged[name] = incomingP;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return merged;
|
|
947
|
+
}
|
|
948
|
+
function mergeProjection(proj, events) {
|
|
949
|
+
for (const eventName of Object.keys(proj.events)) {
|
|
950
|
+
const projRegister = proj.events[eventName];
|
|
951
|
+
const existing = events[eventName];
|
|
952
|
+
if (!existing) {
|
|
953
|
+
events[eventName] = {
|
|
954
|
+
schema: projRegister.schema,
|
|
955
|
+
reactions: new Map(projRegister.reactions)
|
|
956
|
+
};
|
|
957
|
+
} else {
|
|
958
|
+
for (const [name, reaction] of projRegister.reactions) {
|
|
959
|
+
let key = name;
|
|
960
|
+
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
961
|
+
existing.reactions.set(key, reaction);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
var _this_ = ({ stream }) => ({
|
|
967
|
+
source: stream,
|
|
968
|
+
target: stream
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// src/internal/drain.ts
|
|
972
|
+
var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
|
|
973
|
+
async function fetch(leased, eventLimit) {
|
|
974
|
+
return Promise.all(
|
|
975
|
+
leased.map(async ({ stream, source, at, lagging }) => {
|
|
976
|
+
const events = [];
|
|
977
|
+
await store().query((e) => events.push(e), {
|
|
978
|
+
stream: source,
|
|
979
|
+
after: at,
|
|
980
|
+
limit: eventLimit
|
|
981
|
+
});
|
|
982
|
+
return { stream, source, at, lagging, events };
|
|
983
|
+
})
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
var ack = (leases) => store().ack(leases);
|
|
987
|
+
var block = (leases) => store().block(leases);
|
|
988
|
+
var subscribe = (streams) => store().subscribe(streams);
|
|
989
|
+
|
|
990
|
+
// src/internal/event-sourcing.ts
|
|
900
991
|
var import_act_patch = require("@rotorsoft/act-patch");
|
|
901
992
|
var import_crypto = require("crypto");
|
|
902
|
-
var logger2 = log();
|
|
903
993
|
async function snap(snapshot) {
|
|
904
994
|
try {
|
|
905
995
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
906
|
-
|
|
996
|
+
await store().commit(
|
|
907
997
|
stream,
|
|
908
998
|
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
909
999
|
{
|
|
@@ -913,19 +1003,18 @@ async function snap(snapshot) {
|
|
|
913
1003
|
version
|
|
914
1004
|
// IMPORTANT! - state events are committed right after the snapshot event
|
|
915
1005
|
);
|
|
916
|
-
logger2.trace(snapped, "\u{1F7E0} snap");
|
|
917
1006
|
} catch (error) {
|
|
918
|
-
|
|
1007
|
+
log().error(error);
|
|
919
1008
|
}
|
|
920
1009
|
}
|
|
921
1010
|
async function load(me, stream, callback, asOf) {
|
|
922
|
-
const timeTravel = asOf && (asOf.
|
|
1011
|
+
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
923
1012
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
924
1013
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
925
1014
|
let patches = cached?.patches ?? 0;
|
|
926
1015
|
let snaps = cached?.snaps ?? 0;
|
|
927
1016
|
let event;
|
|
928
|
-
|
|
1017
|
+
await store().query(
|
|
929
1018
|
(e) => {
|
|
930
1019
|
event = e;
|
|
931
1020
|
if (e.name === SNAP_EVENT) {
|
|
@@ -944,10 +1033,6 @@ async function load(me, stream, callback, asOf) {
|
|
|
944
1033
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
945
1034
|
}
|
|
946
1035
|
);
|
|
947
|
-
logger2.trace(
|
|
948
|
-
state2,
|
|
949
|
-
`\u{1F7E2} load ${stream}${cached && count === 0 ? " (cached)" : ""}${timeTravel ? " (as-of)" : ""}`
|
|
950
|
-
);
|
|
951
1036
|
return { event, state: state2, patches, snaps };
|
|
952
1037
|
}
|
|
953
1038
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
@@ -958,10 +1043,6 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
958
1043
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
959
1044
|
throw new StreamClosedError(stream);
|
|
960
1045
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
961
|
-
logger2.trace(
|
|
962
|
-
payload,
|
|
963
|
-
`\u{1F535} ${stream}.${action2}${typeof expected === "number" ? `.${expected}` : ""}`
|
|
964
|
-
);
|
|
965
1046
|
if (me.given) {
|
|
966
1047
|
const invariants = me.given[action2] || [];
|
|
967
1048
|
invariants.forEach(({ valid, description }) => {
|
|
@@ -1001,10 +1082,6 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1001
1082
|
} : void 0
|
|
1002
1083
|
}
|
|
1003
1084
|
};
|
|
1004
|
-
logger2.trace(
|
|
1005
|
-
emitted.map((e) => e.data),
|
|
1006
|
-
`\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
|
|
1007
|
-
);
|
|
1008
1085
|
let committed;
|
|
1009
1086
|
try {
|
|
1010
1087
|
committed = await store().commit(
|
|
@@ -1015,7 +1092,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1015
1092
|
reactingTo ? void 0 : expected
|
|
1016
1093
|
);
|
|
1017
1094
|
} catch (error) {
|
|
1018
|
-
if (error
|
|
1095
|
+
if (error instanceof ConcurrencyError) {
|
|
1019
1096
|
await cache().invalidate(stream);
|
|
1020
1097
|
}
|
|
1021
1098
|
throw error;
|
|
@@ -1029,25 +1106,121 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1029
1106
|
});
|
|
1030
1107
|
const last = snapshots.at(-1);
|
|
1031
1108
|
const snapped = me.snap && me.snap(last);
|
|
1032
|
-
|
|
1109
|
+
cache().set(stream, {
|
|
1033
1110
|
state: last.state,
|
|
1034
1111
|
version: last.event.version,
|
|
1035
1112
|
event_id: last.event.id,
|
|
1036
1113
|
patches: snapped ? 0 : last.patches,
|
|
1037
1114
|
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
1038
|
-
});
|
|
1115
|
+
}).catch((err) => log().error(err));
|
|
1039
1116
|
if (snapped) void snap(last);
|
|
1040
1117
|
return snapshots;
|
|
1041
1118
|
}
|
|
1042
1119
|
|
|
1120
|
+
// src/internal/tracing.ts
|
|
1121
|
+
var traced = (inner, exit, entry) => (async (...args) => {
|
|
1122
|
+
entry?.(...args);
|
|
1123
|
+
const result = await inner(...args);
|
|
1124
|
+
exit?.(result, ...args);
|
|
1125
|
+
return result;
|
|
1126
|
+
});
|
|
1127
|
+
function buildEs(logger2) {
|
|
1128
|
+
if (logger2.level !== "trace") {
|
|
1129
|
+
return { snap, load, action };
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
snap: traced(snap, void 0, (snapshot) => {
|
|
1133
|
+
logger2.trace(
|
|
1134
|
+
`\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
|
|
1135
|
+
);
|
|
1136
|
+
}),
|
|
1137
|
+
load: traced(load, void 0, (_me, stream, _cb, asOf) => {
|
|
1138
|
+
logger2.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
|
|
1139
|
+
}),
|
|
1140
|
+
action: traced(
|
|
1141
|
+
action,
|
|
1142
|
+
(snapshots, _me, _action, target) => {
|
|
1143
|
+
const committed = snapshots.filter((s) => s.event);
|
|
1144
|
+
if (committed.length) {
|
|
1145
|
+
logger2.trace(
|
|
1146
|
+
committed.map((s) => s.event.data),
|
|
1147
|
+
`\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
(_me, action2, target, payload) => {
|
|
1152
|
+
logger2.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
|
|
1153
|
+
}
|
|
1154
|
+
)
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function buildDrain(logger2) {
|
|
1158
|
+
if (logger2.level !== "trace") {
|
|
1159
|
+
return {
|
|
1160
|
+
claim,
|
|
1161
|
+
fetch,
|
|
1162
|
+
ack,
|
|
1163
|
+
block,
|
|
1164
|
+
subscribe
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
claim: traced(claim, (leased) => {
|
|
1169
|
+
if (leased.length) {
|
|
1170
|
+
const data = Object.fromEntries(
|
|
1171
|
+
leased.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
1172
|
+
);
|
|
1173
|
+
logger2.trace(data, ">> lease");
|
|
1174
|
+
}
|
|
1175
|
+
}),
|
|
1176
|
+
fetch: traced(fetch, (fetched) => {
|
|
1177
|
+
const data = Object.fromEntries(
|
|
1178
|
+
fetched.map(({ stream, source, events }) => {
|
|
1179
|
+
const key = source ? `${stream}<-${source}` : stream;
|
|
1180
|
+
const value = Object.fromEntries(
|
|
1181
|
+
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
1182
|
+
);
|
|
1183
|
+
return [key, value];
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
logger2.trace(data, ">> fetch");
|
|
1187
|
+
}),
|
|
1188
|
+
ack: traced(ack, (acked) => {
|
|
1189
|
+
if (acked.length) {
|
|
1190
|
+
const data = Object.fromEntries(
|
|
1191
|
+
acked.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
1192
|
+
);
|
|
1193
|
+
logger2.trace(data, ">> ack");
|
|
1194
|
+
}
|
|
1195
|
+
}),
|
|
1196
|
+
block: traced(block, (blocked) => {
|
|
1197
|
+
if (blocked.length) {
|
|
1198
|
+
const data = Object.fromEntries(
|
|
1199
|
+
blocked.map(({ stream, at, retry, error }) => [
|
|
1200
|
+
stream,
|
|
1201
|
+
{ at, retry, error }
|
|
1202
|
+
])
|
|
1203
|
+
);
|
|
1204
|
+
logger2.trace(data, ">> block");
|
|
1205
|
+
}
|
|
1206
|
+
}),
|
|
1207
|
+
subscribe: traced(subscribe, (result, streams) => {
|
|
1208
|
+
if (result.subscribed) {
|
|
1209
|
+
const data = streams.map(({ stream }) => stream).join(" ");
|
|
1210
|
+
logger2.trace(`>> correlate ${data}`);
|
|
1211
|
+
}
|
|
1212
|
+
})
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1043
1216
|
// src/act.ts
|
|
1044
|
-
var logger3 = log();
|
|
1045
|
-
var tracer = build_tracer(config().logLevel);
|
|
1046
1217
|
var Act = class {
|
|
1047
1218
|
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
|
|
1048
1219
|
this.registry = registry;
|
|
1049
1220
|
this._states = _states;
|
|
1050
1221
|
this._batch_handlers = batchHandlers;
|
|
1222
|
+
this._es = buildEs(this._logger);
|
|
1223
|
+
this._cd = buildDrain(this._logger);
|
|
1051
1224
|
const statics = [];
|
|
1052
1225
|
for (const [name, register] of Object.entries(this.registry.events)) {
|
|
1053
1226
|
if (register.reactions.size > 0) {
|
|
@@ -1107,6 +1280,12 @@ var Act = class {
|
|
|
1107
1280
|
_static_targets;
|
|
1108
1281
|
/** Batch handlers for static-target projections (target → handler) */
|
|
1109
1282
|
_batch_handlers;
|
|
1283
|
+
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
1284
|
+
_es;
|
|
1285
|
+
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
1286
|
+
_cd;
|
|
1287
|
+
/** Logger resolved at construction time (after user port configuration) */
|
|
1288
|
+
_logger = log();
|
|
1110
1289
|
/**
|
|
1111
1290
|
* Executes an action on a state instance, committing resulting events.
|
|
1112
1291
|
*
|
|
@@ -1189,7 +1368,7 @@ var Act = class {
|
|
|
1189
1368
|
* @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
|
|
1190
1369
|
*/
|
|
1191
1370
|
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
1192
|
-
const snapshots = await action(
|
|
1371
|
+
const snapshots = await this._es.action(
|
|
1193
1372
|
this.registry.actions[action2],
|
|
1194
1373
|
action2,
|
|
1195
1374
|
target,
|
|
@@ -1215,7 +1394,7 @@ var Act = class {
|
|
|
1215
1394
|
} else {
|
|
1216
1395
|
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
1217
1396
|
}
|
|
1218
|
-
return await load(merged, stream, callback, asOf);
|
|
1397
|
+
return await this._es.load(merged, stream, callback, asOf);
|
|
1219
1398
|
}
|
|
1220
1399
|
/**
|
|
1221
1400
|
* Queries the event store for events matching a filter.
|
|
@@ -1328,7 +1507,7 @@ var Act = class {
|
|
|
1328
1507
|
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1329
1508
|
const stream = lease.stream;
|
|
1330
1509
|
let at = payloads.at(0).event.id, handled = 0;
|
|
1331
|
-
lease.retry > 0 &&
|
|
1510
|
+
lease.retry > 0 && this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1332
1511
|
const doAction = this.do.bind(this);
|
|
1333
1512
|
const scopedApp = {
|
|
1334
1513
|
do: doAction,
|
|
@@ -1350,16 +1529,18 @@ var Act = class {
|
|
|
1350
1529
|
at = event.id;
|
|
1351
1530
|
handled++;
|
|
1352
1531
|
} catch (error) {
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1532
|
+
this._logger.error(error);
|
|
1533
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1534
|
+
block2 && this._logger.error(
|
|
1535
|
+
`Blocking ${stream} after ${lease.retry} retries.`
|
|
1536
|
+
);
|
|
1356
1537
|
return {
|
|
1357
1538
|
lease,
|
|
1358
1539
|
handled,
|
|
1359
1540
|
at,
|
|
1360
1541
|
// only report error when nothing was handled
|
|
1361
1542
|
error: handled === 0 ? error.message : void 0,
|
|
1362
|
-
block
|
|
1543
|
+
block: block2
|
|
1363
1544
|
};
|
|
1364
1545
|
}
|
|
1365
1546
|
}
|
|
@@ -1382,21 +1563,23 @@ var Act = class {
|
|
|
1382
1563
|
const stream = lease.stream;
|
|
1383
1564
|
const events = payloads.map((p) => p.event);
|
|
1384
1565
|
const at = events.at(-1).id;
|
|
1385
|
-
lease.retry > 0 &&
|
|
1566
|
+
lease.retry > 0 && this._logger.warn(
|
|
1567
|
+
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
1568
|
+
);
|
|
1386
1569
|
try {
|
|
1387
1570
|
await batchHandler(events, stream);
|
|
1388
1571
|
return { lease, handled: events.length, at };
|
|
1389
1572
|
} catch (error) {
|
|
1390
|
-
|
|
1573
|
+
this._logger.error(error);
|
|
1391
1574
|
const { options } = payloads[0];
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1575
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1576
|
+
block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
1394
1577
|
return {
|
|
1395
1578
|
lease,
|
|
1396
1579
|
handled: 0,
|
|
1397
1580
|
at: lease.at,
|
|
1398
1581
|
error: error.message,
|
|
1399
|
-
block
|
|
1582
|
+
block: block2
|
|
1400
1583
|
};
|
|
1401
1584
|
}
|
|
1402
1585
|
}
|
|
@@ -1452,7 +1635,7 @@ var Act = class {
|
|
|
1452
1635
|
this._drain_locked = true;
|
|
1453
1636
|
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
1454
1637
|
const leading = streamLimit - lagging;
|
|
1455
|
-
const leased = await
|
|
1638
|
+
const leased = await this._cd.claim(
|
|
1456
1639
|
lagging,
|
|
1457
1640
|
leading,
|
|
1458
1641
|
(0, import_crypto2.randomUUID)(),
|
|
@@ -1462,17 +1645,7 @@ var Act = class {
|
|
|
1462
1645
|
this._needs_drain = false;
|
|
1463
1646
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1464
1647
|
}
|
|
1465
|
-
const fetched = await
|
|
1466
|
-
leased.map(async ({ stream, source, at, lagging: lagging2 }) => {
|
|
1467
|
-
const events = await this.query_array({
|
|
1468
|
-
stream: source,
|
|
1469
|
-
after: at,
|
|
1470
|
-
limit: eventLimit
|
|
1471
|
-
});
|
|
1472
|
-
return { stream, source, at, lagging: lagging2, events };
|
|
1473
|
-
})
|
|
1474
|
-
);
|
|
1475
|
-
tracer.fetched(fetched);
|
|
1648
|
+
const fetched = await this._cd.fetch(leased, eventLimit);
|
|
1476
1649
|
const payloadsMap = /* @__PURE__ */ new Map();
|
|
1477
1650
|
const fetch_window_at = fetched.reduce(
|
|
1478
1651
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
@@ -1489,7 +1662,6 @@ var Act = class {
|
|
|
1489
1662
|
});
|
|
1490
1663
|
payloadsMap.set(stream, payloads);
|
|
1491
1664
|
});
|
|
1492
|
-
tracer.leased(leased);
|
|
1493
1665
|
const handled = await Promise.all(
|
|
1494
1666
|
leased.map((lease) => {
|
|
1495
1667
|
const streamFetch = fetched.find((f) => f.stream === lease.stream);
|
|
@@ -1513,27 +1685,21 @@ var Act = class {
|
|
|
1513
1685
|
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1514
1686
|
const total = lagging_avg + leading_avg;
|
|
1515
1687
|
this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
|
|
1516
|
-
const acked = await
|
|
1688
|
+
const acked = await this._cd.ack(
|
|
1517
1689
|
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1518
1690
|
);
|
|
1519
|
-
if (acked.length)
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
const blocked = await store().block(
|
|
1524
|
-
handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
|
|
1691
|
+
if (acked.length) this.emit("acked", acked);
|
|
1692
|
+
const blocked = await this._cd.block(
|
|
1693
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1525
1694
|
);
|
|
1526
|
-
if (blocked.length)
|
|
1527
|
-
tracer.blocked(blocked);
|
|
1528
|
-
this.emit("blocked", blocked);
|
|
1529
|
-
}
|
|
1695
|
+
if (blocked.length) this.emit("blocked", blocked);
|
|
1530
1696
|
const result = { fetched, leased, acked, blocked };
|
|
1531
1697
|
const hasErrors = handled.some(({ error }) => error);
|
|
1532
1698
|
if (!acked.length && !blocked.length && !hasErrors)
|
|
1533
1699
|
this._needs_drain = false;
|
|
1534
1700
|
return result;
|
|
1535
1701
|
} catch (error) {
|
|
1536
|
-
|
|
1702
|
+
this._logger.error(error);
|
|
1537
1703
|
} finally {
|
|
1538
1704
|
this._drain_locked = false;
|
|
1539
1705
|
}
|
|
@@ -1639,10 +1805,9 @@ var Act = class {
|
|
|
1639
1805
|
stream,
|
|
1640
1806
|
source
|
|
1641
1807
|
}));
|
|
1642
|
-
const { subscribed } = await
|
|
1808
|
+
const { subscribed } = await this._cd.subscribe(streams);
|
|
1643
1809
|
this._correlation_checkpoint = last_id;
|
|
1644
1810
|
if (subscribed) {
|
|
1645
|
-
tracer.correlated(streams);
|
|
1646
1811
|
for (const { stream } of streams) {
|
|
1647
1812
|
this._subscribed_statics.add(stream);
|
|
1648
1813
|
}
|
|
@@ -1907,7 +2072,7 @@ var Act = class {
|
|
|
1907
2072
|
if (mergedState) {
|
|
1908
2073
|
await Promise.all(
|
|
1909
2074
|
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
1910
|
-
const snap2 = await load(mergedState, stream);
|
|
2075
|
+
const snap2 = await this._es.load(mergedState, stream);
|
|
1911
2076
|
seedStates.set(stream, snap2.state);
|
|
1912
2077
|
})
|
|
1913
2078
|
);
|
|
@@ -2014,139 +2179,13 @@ var Act = class {
|
|
|
2014
2179
|
if (!made_progress) break;
|
|
2015
2180
|
}
|
|
2016
2181
|
if (lastDrain) this.emit("settled", lastDrain);
|
|
2017
|
-
})().catch((err) =>
|
|
2182
|
+
})().catch((err) => this._logger.error(err)).finally(() => {
|
|
2018
2183
|
this._settling = false;
|
|
2019
2184
|
});
|
|
2020
2185
|
}, debounceMs);
|
|
2021
2186
|
}
|
|
2022
2187
|
};
|
|
2023
2188
|
|
|
2024
|
-
// src/merge.ts
|
|
2025
|
-
var import_zod4 = require("zod");
|
|
2026
|
-
function baseTypeName(zodType) {
|
|
2027
|
-
let t = zodType;
|
|
2028
|
-
while (typeof t.unwrap === "function") {
|
|
2029
|
-
t = t.unwrap();
|
|
2030
|
-
}
|
|
2031
|
-
return t.constructor.name;
|
|
2032
|
-
}
|
|
2033
|
-
function mergeSchemas(existing, incoming, stateName) {
|
|
2034
|
-
if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
|
|
2035
|
-
const existingShape = existing.shape;
|
|
2036
|
-
const incomingShape = incoming.shape;
|
|
2037
|
-
for (const key of Object.keys(incomingShape)) {
|
|
2038
|
-
if (key in existingShape) {
|
|
2039
|
-
const existingBase = baseTypeName(existingShape[key]);
|
|
2040
|
-
const incomingBase = baseTypeName(incomingShape[key]);
|
|
2041
|
-
if (existingBase !== incomingBase) {
|
|
2042
|
-
throw new Error(
|
|
2043
|
-
`Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
return existing.extend(incomingShape);
|
|
2049
|
-
}
|
|
2050
|
-
return existing;
|
|
2051
|
-
}
|
|
2052
|
-
function mergeInits(existing, incoming) {
|
|
2053
|
-
return () => ({ ...existing(), ...incoming() });
|
|
2054
|
-
}
|
|
2055
|
-
function registerState(state2, states, actions, events) {
|
|
2056
|
-
if (states.has(state2.name)) {
|
|
2057
|
-
const existing = states.get(state2.name);
|
|
2058
|
-
for (const name of Object.keys(state2.actions)) {
|
|
2059
|
-
if (existing.actions[name] === state2.actions[name]) continue;
|
|
2060
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2061
|
-
}
|
|
2062
|
-
for (const name of Object.keys(state2.events)) {
|
|
2063
|
-
if (existing.events[name] === state2.events[name]) continue;
|
|
2064
|
-
if (existing.events[name]) continue;
|
|
2065
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2066
|
-
}
|
|
2067
|
-
const mergedPatch = { ...existing.patch };
|
|
2068
|
-
for (const name of Object.keys(state2.patch)) {
|
|
2069
|
-
const existingP = existing.patch[name];
|
|
2070
|
-
const incomingP = state2.patch[name];
|
|
2071
|
-
if (!existingP) {
|
|
2072
|
-
mergedPatch[name] = incomingP;
|
|
2073
|
-
} else {
|
|
2074
|
-
const existingIsDefault = existingP._passthrough;
|
|
2075
|
-
const incomingIsDefault = incomingP._passthrough;
|
|
2076
|
-
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
2077
|
-
throw new Error(
|
|
2078
|
-
`Duplicate custom patch for event "${name}" in state "${state2.name}"`
|
|
2079
|
-
);
|
|
2080
|
-
}
|
|
2081
|
-
if (existingIsDefault && !incomingIsDefault) {
|
|
2082
|
-
mergedPatch[name] = incomingP;
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
const merged = {
|
|
2087
|
-
...existing,
|
|
2088
|
-
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
2089
|
-
init: mergeInits(existing.init, state2.init),
|
|
2090
|
-
events: { ...existing.events, ...state2.events },
|
|
2091
|
-
actions: { ...existing.actions, ...state2.actions },
|
|
2092
|
-
patch: mergedPatch,
|
|
2093
|
-
on: { ...existing.on, ...state2.on },
|
|
2094
|
-
given: { ...existing.given, ...state2.given },
|
|
2095
|
-
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
2096
|
-
throw new Error(
|
|
2097
|
-
`Duplicate snap strategy for state "${state2.name}"`
|
|
2098
|
-
);
|
|
2099
|
-
})() : state2.snap || existing.snap
|
|
2100
|
-
};
|
|
2101
|
-
states.set(state2.name, merged);
|
|
2102
|
-
for (const name of Object.keys(merged.actions)) {
|
|
2103
|
-
actions[name] = merged;
|
|
2104
|
-
}
|
|
2105
|
-
for (const name of Object.keys(state2.events)) {
|
|
2106
|
-
if (events[name]) continue;
|
|
2107
|
-
events[name] = {
|
|
2108
|
-
schema: state2.events[name],
|
|
2109
|
-
reactions: /* @__PURE__ */ new Map()
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
} else {
|
|
2113
|
-
states.set(state2.name, state2);
|
|
2114
|
-
for (const name of Object.keys(state2.actions)) {
|
|
2115
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2116
|
-
actions[name] = state2;
|
|
2117
|
-
}
|
|
2118
|
-
for (const name of Object.keys(state2.events)) {
|
|
2119
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2120
|
-
events[name] = {
|
|
2121
|
-
schema: state2.events[name],
|
|
2122
|
-
reactions: /* @__PURE__ */ new Map()
|
|
2123
|
-
};
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
function mergeProjection(proj, events) {
|
|
2128
|
-
for (const eventName of Object.keys(proj.events)) {
|
|
2129
|
-
const projRegister = proj.events[eventName];
|
|
2130
|
-
const existing = events[eventName];
|
|
2131
|
-
if (!existing) {
|
|
2132
|
-
events[eventName] = {
|
|
2133
|
-
schema: projRegister.schema,
|
|
2134
|
-
reactions: new Map(projRegister.reactions)
|
|
2135
|
-
};
|
|
2136
|
-
} else {
|
|
2137
|
-
for (const [name, reaction] of projRegister.reactions) {
|
|
2138
|
-
let key = name;
|
|
2139
|
-
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
2140
|
-
existing.reactions.set(key, reaction);
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
var _this_ = ({ stream }) => ({
|
|
2146
|
-
source: stream,
|
|
2147
|
-
target: stream
|
|
2148
|
-
});
|
|
2149
|
-
|
|
2150
2189
|
// src/act-builder.ts
|
|
2151
2190
|
function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
2152
2191
|
actions: {},
|
|
@@ -2387,8 +2426,9 @@ function state(entry) {
|
|
|
2387
2426
|
emits(events) {
|
|
2388
2427
|
const defaultPatch = Object.fromEntries(
|
|
2389
2428
|
Object.keys(events).map((k) => {
|
|
2390
|
-
const fn = ({ data }) => data
|
|
2391
|
-
|
|
2429
|
+
const fn = Object.assign(({ data }) => data, {
|
|
2430
|
+
_passthrough: true
|
|
2431
|
+
});
|
|
2392
2432
|
return [k, fn];
|
|
2393
2433
|
})
|
|
2394
2434
|
);
|
|
@@ -2490,7 +2530,6 @@ function action_builder(state2) {
|
|
|
2490
2530
|
ValidationError,
|
|
2491
2531
|
ZodEmpty,
|
|
2492
2532
|
act,
|
|
2493
|
-
build_tracer,
|
|
2494
2533
|
cache,
|
|
2495
2534
|
config,
|
|
2496
2535
|
dispose,
|