@rotorsoft/act 0.32.4 → 0.32.5
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.d.ts +65 -47
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryCache.d.ts +1 -2
- package/dist/@types/adapters/InMemoryCache.d.ts.map +1 -1
- package/dist/@types/{act-builder.d.ts → builders/act-builder.d.ts} +5 -5
- package/dist/@types/builders/act-builder.d.ts.map +1 -0
- package/dist/@types/builders/index.d.ts +13 -0
- package/dist/@types/builders/index.d.ts.map +1 -0
- package/dist/@types/{projection-builder.d.ts → builders/projection-builder.d.ts} +3 -3
- package/dist/@types/builders/projection-builder.d.ts.map +1 -0
- package/dist/@types/{slice-builder.d.ts → builders/slice-builder.d.ts} +2 -2
- package/dist/@types/builders/slice-builder.d.ts.map +1 -0
- package/dist/@types/{state-builder.d.ts → builders/state-builder.d.ts} +1 -1
- package/dist/@types/builders/state-builder.d.ts.map +1 -0
- package/dist/@types/index.d.ts +1 -4
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +38 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +61 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-ratio.d.ts +26 -0
- package/dist/@types/internal/drain-ratio.d.ts.map +1 -0
- package/dist/@types/internal/event-sourcing.d.ts +14 -0
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +5 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/lru-map.d.ts +50 -0
- package/dist/@types/internal/lru-map.d.ts.map +1 -0
- package/dist/@types/internal/merge.d.ts +13 -1
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +7 -1
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/index.cjs +521 -394
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +520 -394
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/@types/act-builder.d.ts.map +0 -1
- package/dist/@types/projection-builder.d.ts.map +0 -1
- package/dist/@types/slice-builder.d.ts.map +0 -1
- package/dist/@types/state-builder.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
CommittedMetaSchema: () => CommittedMetaSchema,
|
|
37
37
|
ConcurrencyError: () => ConcurrencyError,
|
|
38
38
|
ConsoleLogger: () => ConsoleLogger,
|
|
39
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
39
40
|
Environments: () => Environments,
|
|
40
41
|
Errors: () => Errors,
|
|
41
42
|
EventMetaSchema: () => EventMetaSchema,
|
|
@@ -171,26 +172,75 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
171
172
|
}
|
|
172
173
|
};
|
|
173
174
|
|
|
175
|
+
// src/internal/lru-map.ts
|
|
176
|
+
var LruMap = class {
|
|
177
|
+
constructor(_maxSize) {
|
|
178
|
+
this._maxSize = _maxSize;
|
|
179
|
+
}
|
|
180
|
+
_entries = /* @__PURE__ */ new Map();
|
|
181
|
+
get(key) {
|
|
182
|
+
const v = this._entries.get(key);
|
|
183
|
+
if (v === void 0) return void 0;
|
|
184
|
+
this._entries.delete(key);
|
|
185
|
+
this._entries.set(key, v);
|
|
186
|
+
return v;
|
|
187
|
+
}
|
|
188
|
+
has(key) {
|
|
189
|
+
return this._entries.has(key);
|
|
190
|
+
}
|
|
191
|
+
set(key, value) {
|
|
192
|
+
this._entries.delete(key);
|
|
193
|
+
if (this._entries.size >= this._maxSize) {
|
|
194
|
+
const oldest = this._entries.keys().next().value;
|
|
195
|
+
if (oldest !== void 0) this._entries.delete(oldest);
|
|
196
|
+
}
|
|
197
|
+
this._entries.set(key, value);
|
|
198
|
+
}
|
|
199
|
+
delete(key) {
|
|
200
|
+
return this._entries.delete(key);
|
|
201
|
+
}
|
|
202
|
+
clear() {
|
|
203
|
+
this._entries.clear();
|
|
204
|
+
}
|
|
205
|
+
get size() {
|
|
206
|
+
return this._entries.size;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
var LruSet = class {
|
|
210
|
+
_map;
|
|
211
|
+
constructor(maxSize) {
|
|
212
|
+
this._map = new LruMap(maxSize);
|
|
213
|
+
}
|
|
214
|
+
has(value) {
|
|
215
|
+
return this._map.has(value);
|
|
216
|
+
}
|
|
217
|
+
add(value) {
|
|
218
|
+
this._map.set(value, true);
|
|
219
|
+
}
|
|
220
|
+
delete(value) {
|
|
221
|
+
return this._map.delete(value);
|
|
222
|
+
}
|
|
223
|
+
clear() {
|
|
224
|
+
this._map.clear();
|
|
225
|
+
}
|
|
226
|
+
get size() {
|
|
227
|
+
return this._map.size;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
174
231
|
// src/adapters/InMemoryCache.ts
|
|
175
232
|
var InMemoryCache = class {
|
|
176
|
-
|
|
177
|
-
|
|
233
|
+
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
234
|
+
// any is bidirectionally compatible with the per-call TState binding, while
|
|
235
|
+
// the public Cache interface still presents a typed surface to callers.
|
|
236
|
+
_entries;
|
|
178
237
|
constructor(options) {
|
|
179
|
-
this.
|
|
238
|
+
this._entries = new LruMap(options?.maxSize ?? 1e3);
|
|
180
239
|
}
|
|
181
240
|
async get(stream) {
|
|
182
|
-
|
|
183
|
-
if (!entry) return void 0;
|
|
184
|
-
this._entries.delete(stream);
|
|
185
|
-
this._entries.set(stream, entry);
|
|
186
|
-
return entry;
|
|
241
|
+
return this._entries.get(stream);
|
|
187
242
|
}
|
|
188
243
|
async set(stream, entry) {
|
|
189
|
-
this._entries.delete(stream);
|
|
190
|
-
if (this._entries.size >= this._maxSize) {
|
|
191
|
-
const first = this._entries.keys().next().value;
|
|
192
|
-
this._entries.delete(first);
|
|
193
|
-
}
|
|
194
244
|
this._entries.set(stream, entry);
|
|
195
245
|
}
|
|
196
246
|
async invalidate(stream) {
|
|
@@ -837,9 +887,233 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
837
887
|
});
|
|
838
888
|
|
|
839
889
|
// src/act.ts
|
|
840
|
-
var import_crypto2 = require("crypto");
|
|
841
890
|
var import_events = __toESM(require("events"), 1);
|
|
842
891
|
|
|
892
|
+
// src/internal/close-cycle.ts
|
|
893
|
+
var import_crypto = require("crypto");
|
|
894
|
+
async function runCloseCycle(targets, deps) {
|
|
895
|
+
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
896
|
+
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
897
|
+
const streams = [...targetMap.keys()];
|
|
898
|
+
const skipped = [];
|
|
899
|
+
const streamInfo = await scanStreamHeads(streams);
|
|
900
|
+
const safe = await partitionBySafety(
|
|
901
|
+
streamInfo,
|
|
902
|
+
deps.reactiveEventsSize,
|
|
903
|
+
skipped
|
|
904
|
+
);
|
|
905
|
+
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
906
|
+
const correlation = (0, import_crypto.randomUUID)();
|
|
907
|
+
const { guarded, guardEvents } = await guardWithTombstones(
|
|
908
|
+
safe,
|
|
909
|
+
streamInfo,
|
|
910
|
+
correlation,
|
|
911
|
+
deps.tombstone,
|
|
912
|
+
skipped
|
|
913
|
+
);
|
|
914
|
+
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
915
|
+
const seedStates = await loadRestartSeeds(
|
|
916
|
+
guarded,
|
|
917
|
+
targetMap,
|
|
918
|
+
streamInfo,
|
|
919
|
+
deps.eventToState,
|
|
920
|
+
deps.load,
|
|
921
|
+
deps.logger
|
|
922
|
+
);
|
|
923
|
+
await runArchiveCallbacks(guarded, targetMap);
|
|
924
|
+
const truncated = await truncateAndWarmCache(
|
|
925
|
+
guarded,
|
|
926
|
+
seedStates,
|
|
927
|
+
guardEvents,
|
|
928
|
+
correlation
|
|
929
|
+
);
|
|
930
|
+
return { truncated, skipped };
|
|
931
|
+
}
|
|
932
|
+
async function scanStreamHeads(streams) {
|
|
933
|
+
const out = /* @__PURE__ */ new Map();
|
|
934
|
+
await Promise.all(
|
|
935
|
+
streams.map(async (s) => {
|
|
936
|
+
let maxId = -1;
|
|
937
|
+
let version = -1;
|
|
938
|
+
let lastEventName;
|
|
939
|
+
await store().query(
|
|
940
|
+
(e) => {
|
|
941
|
+
if (e.name === TOMBSTONE_EVENT) return;
|
|
942
|
+
if (maxId === -1) {
|
|
943
|
+
maxId = e.id;
|
|
944
|
+
version = e.version;
|
|
945
|
+
}
|
|
946
|
+
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
947
|
+
lastEventName = e.name;
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
// limit: 2 covers the typical snapshot-at-head case (snapshot is
|
|
951
|
+
// always preceded by the domain event it captured). Streams with
|
|
952
|
+
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
953
|
+
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
954
|
+
);
|
|
955
|
+
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
956
|
+
})
|
|
957
|
+
);
|
|
958
|
+
return out;
|
|
959
|
+
}
|
|
960
|
+
async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
|
|
961
|
+
if (reactiveEventsSize === 0) return [...streamInfo.keys()];
|
|
962
|
+
const pendingSet = /* @__PURE__ */ new Set();
|
|
963
|
+
await store().query_streams((position) => {
|
|
964
|
+
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
965
|
+
for (const [stream, info] of streamInfo) {
|
|
966
|
+
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
967
|
+
pendingSet.add(stream);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
const safe = [];
|
|
972
|
+
for (const [stream] of streamInfo) {
|
|
973
|
+
if (pendingSet.has(stream)) skipped.push(stream);
|
|
974
|
+
else safe.push(stream);
|
|
975
|
+
}
|
|
976
|
+
return safe;
|
|
977
|
+
}
|
|
978
|
+
async function guardWithTombstones(safe, streamInfo, correlation, tombstone2, skipped) {
|
|
979
|
+
const guarded = [];
|
|
980
|
+
const guardEvents = /* @__PURE__ */ new Map();
|
|
981
|
+
await Promise.all(
|
|
982
|
+
safe.map(async (stream) => {
|
|
983
|
+
const info = streamInfo.get(stream);
|
|
984
|
+
const committed = await tombstone2(stream, info.version, correlation);
|
|
985
|
+
if (committed) {
|
|
986
|
+
guarded.push(stream);
|
|
987
|
+
guardEvents.set(stream, { id: committed.id, stream });
|
|
988
|
+
} else {
|
|
989
|
+
skipped.push(stream);
|
|
990
|
+
}
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
return { guarded, guardEvents };
|
|
994
|
+
}
|
|
995
|
+
async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, load2, logger) {
|
|
996
|
+
const seedStates = /* @__PURE__ */ new Map();
|
|
997
|
+
await Promise.all(
|
|
998
|
+
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
999
|
+
const lastEventName = streamInfo.get(stream)?.lastEventName;
|
|
1000
|
+
const ownerState = lastEventName ? eventToState.get(lastEventName) : void 0;
|
|
1001
|
+
if (!ownerState) {
|
|
1002
|
+
logger.error(
|
|
1003
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
|
|
1004
|
+
);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const snap2 = await load2(ownerState, stream);
|
|
1008
|
+
seedStates.set(stream, snap2.state);
|
|
1009
|
+
})
|
|
1010
|
+
);
|
|
1011
|
+
return seedStates;
|
|
1012
|
+
}
|
|
1013
|
+
async function runArchiveCallbacks(guarded, targetMap) {
|
|
1014
|
+
for (const stream of guarded) {
|
|
1015
|
+
const archiveFn = targetMap.get(stream)?.archive;
|
|
1016
|
+
if (archiveFn) await archiveFn();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlation) {
|
|
1020
|
+
const truncTargets = guarded.map((stream) => {
|
|
1021
|
+
const snapshot = seedStates.get(stream);
|
|
1022
|
+
const guard = guardEvents.get(stream);
|
|
1023
|
+
return {
|
|
1024
|
+
stream,
|
|
1025
|
+
snapshot,
|
|
1026
|
+
meta: {
|
|
1027
|
+
correlation,
|
|
1028
|
+
causation: {
|
|
1029
|
+
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
});
|
|
1034
|
+
const truncated = await store().truncate(truncTargets);
|
|
1035
|
+
await Promise.all(
|
|
1036
|
+
guarded.map(async (stream) => {
|
|
1037
|
+
const entry = truncated.get(stream);
|
|
1038
|
+
const state2 = seedStates.get(stream);
|
|
1039
|
+
if (state2 && entry) {
|
|
1040
|
+
await cache().set(stream, {
|
|
1041
|
+
state: state2,
|
|
1042
|
+
version: entry.committed.version,
|
|
1043
|
+
event_id: entry.committed.id,
|
|
1044
|
+
patches: 0,
|
|
1045
|
+
snaps: 1
|
|
1046
|
+
});
|
|
1047
|
+
} else {
|
|
1048
|
+
await cache().invalidate(stream);
|
|
1049
|
+
}
|
|
1050
|
+
})
|
|
1051
|
+
);
|
|
1052
|
+
return truncated;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/internal/drain-cycle.ts
|
|
1056
|
+
var import_crypto2 = require("crypto");
|
|
1057
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
1058
|
+
const leased = await ops.claim(lagging, leading, (0, import_crypto2.randomUUID)(), leaseMillis);
|
|
1059
|
+
if (!leased.length) return void 0;
|
|
1060
|
+
const fetched = await ops.fetch(leased, eventLimit);
|
|
1061
|
+
const fetchMap = /* @__PURE__ */ new Map();
|
|
1062
|
+
const fetch_window_at = fetched.reduce(
|
|
1063
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1064
|
+
0
|
|
1065
|
+
);
|
|
1066
|
+
for (const f of fetched) {
|
|
1067
|
+
const { stream, events } = f;
|
|
1068
|
+
const payloads = events.flatMap((event) => {
|
|
1069
|
+
const register = registry.events[event.name];
|
|
1070
|
+
if (!register) return [];
|
|
1071
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
1072
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1073
|
+
return resolved && resolved.target === stream;
|
|
1074
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
1075
|
+
});
|
|
1076
|
+
fetchMap.set(stream, { fetch: f, payloads });
|
|
1077
|
+
}
|
|
1078
|
+
const handled = await Promise.all(
|
|
1079
|
+
leased.map((lease) => {
|
|
1080
|
+
const entry = fetchMap.get(lease.stream);
|
|
1081
|
+
const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1082
|
+
const payloads = entry?.payloads ?? [];
|
|
1083
|
+
const batchHandler = batchHandlers.get(lease.stream);
|
|
1084
|
+
if (batchHandler && payloads.length > 0) {
|
|
1085
|
+
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1086
|
+
}
|
|
1087
|
+
return handle({ ...lease, at }, payloads);
|
|
1088
|
+
})
|
|
1089
|
+
);
|
|
1090
|
+
const acked = await ops.ack(
|
|
1091
|
+
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1092
|
+
);
|
|
1093
|
+
const blocked = await ops.block(
|
|
1094
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1095
|
+
);
|
|
1096
|
+
return { leased, fetched, handled, acked, blocked };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/internal/drain-ratio.ts
|
|
1100
|
+
var RATIO_MIN = 0.2;
|
|
1101
|
+
var RATIO_MAX = 0.8;
|
|
1102
|
+
var RATIO_DEFAULT = 0.5;
|
|
1103
|
+
function computeLagLeadRatio(handled, lagging, leading) {
|
|
1104
|
+
let lagging_handled = 0;
|
|
1105
|
+
let leading_handled = 0;
|
|
1106
|
+
for (const { lease, handled: count } of handled) {
|
|
1107
|
+
if (lease.lagging) lagging_handled += count;
|
|
1108
|
+
else leading_handled += count;
|
|
1109
|
+
}
|
|
1110
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1111
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1112
|
+
const total = lagging_avg + leading_avg;
|
|
1113
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
1114
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
843
1117
|
// src/internal/merge.ts
|
|
844
1118
|
var import_zod4 = require("zod");
|
|
845
1119
|
function baseTypeName(zodType) {
|
|
@@ -947,6 +1221,15 @@ function mergePatches(existing, incoming, stateName) {
|
|
|
947
1221
|
}
|
|
948
1222
|
return merged;
|
|
949
1223
|
}
|
|
1224
|
+
function mergeEventRegister(target, source) {
|
|
1225
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1226
|
+
const targetReg = target[eventName];
|
|
1227
|
+
if (!targetReg) continue;
|
|
1228
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
1229
|
+
targetReg.reactions.set(name, reaction);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
950
1233
|
function mergeProjection(proj, events) {
|
|
951
1234
|
for (const eventName of Object.keys(proj.events)) {
|
|
952
1235
|
const projRegister = proj.events[eventName];
|
|
@@ -991,7 +1274,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
991
1274
|
|
|
992
1275
|
// src/internal/event-sourcing.ts
|
|
993
1276
|
var import_act_patch = require("@rotorsoft/act-patch");
|
|
994
|
-
var
|
|
1277
|
+
var import_crypto3 = require("crypto");
|
|
995
1278
|
async function snap(snapshot) {
|
|
996
1279
|
try {
|
|
997
1280
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -1009,6 +1292,20 @@ async function snap(snapshot) {
|
|
|
1009
1292
|
log().error(error);
|
|
1010
1293
|
}
|
|
1011
1294
|
}
|
|
1295
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
1296
|
+
try {
|
|
1297
|
+
const [committed] = await store().commit(
|
|
1298
|
+
stream,
|
|
1299
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1300
|
+
{ correlation, causation: {} },
|
|
1301
|
+
expectedVersion
|
|
1302
|
+
);
|
|
1303
|
+
return committed;
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1012
1309
|
async function load(me, stream, callback, asOf) {
|
|
1013
1310
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1014
1311
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
@@ -1073,7 +1370,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1073
1370
|
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
1074
1371
|
}));
|
|
1075
1372
|
const meta = {
|
|
1076
|
-
correlation: reactingTo?.meta.correlation || (0,
|
|
1373
|
+
correlation: reactingTo?.meta.correlation || (0, import_crypto3.randomUUID)(),
|
|
1077
1374
|
causation: {
|
|
1078
1375
|
action: {
|
|
1079
1376
|
name: action2,
|
|
@@ -1144,7 +1441,12 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
1144
1441
|
});
|
|
1145
1442
|
function buildEs(logger) {
|
|
1146
1443
|
if (logger.level !== "trace") {
|
|
1147
|
-
return {
|
|
1444
|
+
return {
|
|
1445
|
+
snap,
|
|
1446
|
+
load,
|
|
1447
|
+
action,
|
|
1448
|
+
tombstone
|
|
1449
|
+
};
|
|
1148
1450
|
}
|
|
1149
1451
|
return {
|
|
1150
1452
|
snap: traced(snap, void 0, (snapshot) => {
|
|
@@ -1182,7 +1484,13 @@ function buildEs(logger) {
|
|
|
1182
1484
|
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
1183
1485
|
);
|
|
1184
1486
|
}
|
|
1185
|
-
)
|
|
1487
|
+
),
|
|
1488
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
1489
|
+
if (committed)
|
|
1490
|
+
logger.trace(
|
|
1491
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
1492
|
+
);
|
|
1493
|
+
})
|
|
1186
1494
|
};
|
|
1187
1495
|
}
|
|
1188
1496
|
function buildDrain(logger) {
|
|
@@ -1245,11 +1553,15 @@ function buildDrain(logger) {
|
|
|
1245
1553
|
}
|
|
1246
1554
|
|
|
1247
1555
|
// src/act.ts
|
|
1556
|
+
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
1248
1557
|
var Act = class {
|
|
1249
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
|
|
1558
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
1250
1559
|
this.registry = registry;
|
|
1251
1560
|
this._states = _states;
|
|
1252
1561
|
this._batch_handlers = batchHandlers;
|
|
1562
|
+
this._subscribed_streams = new LruSet(
|
|
1563
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
|
|
1564
|
+
);
|
|
1253
1565
|
this._es = buildEs(this._logger);
|
|
1254
1566
|
this._cd = buildDrain(this._logger);
|
|
1255
1567
|
const statics = /* @__PURE__ */ new Map();
|
|
@@ -1287,20 +1599,42 @@ var Act = class {
|
|
|
1287
1599
|
_settle_timer = void 0;
|
|
1288
1600
|
_settling = false;
|
|
1289
1601
|
_correlation_checkpoint = -1;
|
|
1290
|
-
|
|
1602
|
+
/**
|
|
1603
|
+
* Streams already subscribed via store.subscribe() — both the static
|
|
1604
|
+
* targets registered at init and dynamic targets discovered by
|
|
1605
|
+
* correlate(). correlate() consults this set to avoid re-subscribing
|
|
1606
|
+
* known streams.
|
|
1607
|
+
*
|
|
1608
|
+
* Bounded LRU so apps that mint millions of dynamic targets (one per
|
|
1609
|
+
* aggregate) don't grow this unbounded. Eviction costs at most one
|
|
1610
|
+
* redundant store.subscribe() call per evicted-but-still-active stream
|
|
1611
|
+
* (subscribe is idempotent). Cap configurable via {@link ActOptions}.
|
|
1612
|
+
*/
|
|
1613
|
+
_subscribed_streams;
|
|
1291
1614
|
_has_dynamic_resolvers = false;
|
|
1292
1615
|
_correlation_initialized = false;
|
|
1293
1616
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
1294
1617
|
_reactive_events = /* @__PURE__ */ new Set();
|
|
1295
1618
|
/** Set in do() when a committed event has reactions — cleared by drain() */
|
|
1296
1619
|
_needs_drain = false;
|
|
1620
|
+
/**
|
|
1621
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1622
|
+
* via {@link ActLifecycleEvents}.
|
|
1623
|
+
*/
|
|
1297
1624
|
emit(event, args) {
|
|
1298
1625
|
return this._emitter.emit(event, args);
|
|
1299
1626
|
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
1629
|
+
* event-specific payload.
|
|
1630
|
+
*/
|
|
1300
1631
|
on(event, listener) {
|
|
1301
1632
|
this._emitter.on(event, listener);
|
|
1302
1633
|
return this;
|
|
1303
1634
|
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Remove a previously registered lifecycle listener.
|
|
1637
|
+
*/
|
|
1304
1638
|
off(event, listener) {
|
|
1305
1639
|
this._emitter.off(event, listener);
|
|
1306
1640
|
return this;
|
|
@@ -1334,6 +1668,9 @@ var Act = class {
|
|
|
1334
1668
|
_bound_load = this.load.bind(this);
|
|
1335
1669
|
_bound_query = this.query.bind(this);
|
|
1336
1670
|
_bound_query_array = this.query_array.bind(this);
|
|
1671
|
+
/** Pre-bound dispatchers handed to runDrainCycle each cycle. */
|
|
1672
|
+
_bound_handle = this.handle.bind(this);
|
|
1673
|
+
_bound_handle_batch = this.handleBatch.bind(this);
|
|
1337
1674
|
/**
|
|
1338
1675
|
* Executes an action on a state instance, committing resulting events.
|
|
1339
1676
|
*
|
|
@@ -1536,26 +1873,46 @@ var Act = class {
|
|
|
1536
1873
|
return events;
|
|
1537
1874
|
}
|
|
1538
1875
|
/**
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1876
|
+
* Shared finalization for the two reaction-runner shapes (per-event
|
|
1877
|
+
* `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
|
|
1878
|
+
* block decision, and the "error reported only when nothing was handled"
|
|
1879
|
+
* rule that's true in both shapes (in batch mode, `handled` is always 0
|
|
1880
|
+
* on failure, so the rule degenerates to "always reported").
|
|
1881
|
+
*/
|
|
1882
|
+
_finalize(lease, handled, at, error, options) {
|
|
1883
|
+
if (!error) return { lease, handled, at };
|
|
1884
|
+
this._logger.error(error);
|
|
1885
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1886
|
+
if (block2)
|
|
1887
|
+
this._logger.error(
|
|
1888
|
+
`Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1889
|
+
);
|
|
1890
|
+
return {
|
|
1891
|
+
lease,
|
|
1892
|
+
handled,
|
|
1893
|
+
at,
|
|
1894
|
+
error: handled === 0 ? error.message : void 0,
|
|
1895
|
+
block: block2
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Handles leased reactions one event at a time.
|
|
1543
1900
|
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1901
|
+
* Called by the main `drain` loop after fetching new events. Each handler
|
|
1902
|
+
* receives a scoped `IAct` proxy that auto-injects the triggering event
|
|
1903
|
+
* as `reactingTo` when `do()` is called without it, maintaining
|
|
1904
|
+
* correlation chains by default (#587). Handlers can still pass an
|
|
1905
|
+
* explicit `reactingTo` to override.
|
|
1548
1906
|
*
|
|
1549
1907
|
* @internal
|
|
1550
|
-
* @param lease The lease to handle
|
|
1551
|
-
* @param payloads The reactions to handle
|
|
1552
|
-
* @returns The lease with results
|
|
1553
1908
|
*/
|
|
1554
1909
|
async handle(lease, payloads) {
|
|
1555
1910
|
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1556
1911
|
const stream = lease.stream;
|
|
1557
|
-
let at = payloads.at(0).event.id
|
|
1558
|
-
|
|
1912
|
+
let at = payloads.at(0).event.id;
|
|
1913
|
+
let handled = 0;
|
|
1914
|
+
if (lease.retry > 0)
|
|
1915
|
+
this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1559
1916
|
const doAction = this._bound_do;
|
|
1560
1917
|
const scopedApp = {
|
|
1561
1918
|
do: doAction,
|
|
@@ -1564,7 +1921,7 @@ var Act = class {
|
|
|
1564
1921
|
query_array: this._bound_query_array
|
|
1565
1922
|
};
|
|
1566
1923
|
for (const payload of payloads) {
|
|
1567
|
-
const { event, handler
|
|
1924
|
+
const { event, handler } = payload;
|
|
1568
1925
|
scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
|
|
1569
1926
|
action2,
|
|
1570
1927
|
target,
|
|
@@ -1577,22 +1934,16 @@ var Act = class {
|
|
|
1577
1934
|
at = event.id;
|
|
1578
1935
|
handled++;
|
|
1579
1936
|
} catch (error) {
|
|
1580
|
-
this.
|
|
1581
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1582
|
-
block2 && this._logger.error(
|
|
1583
|
-
`Blocking ${stream} after ${lease.retry} retries.`
|
|
1584
|
-
);
|
|
1585
|
-
return {
|
|
1937
|
+
return this._finalize(
|
|
1586
1938
|
lease,
|
|
1587
1939
|
handled,
|
|
1588
1940
|
at,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
};
|
|
1941
|
+
error,
|
|
1942
|
+
payload.options
|
|
1943
|
+
);
|
|
1593
1944
|
}
|
|
1594
1945
|
}
|
|
1595
|
-
return
|
|
1946
|
+
return this._finalize(lease, handled, at, void 0, payloads[0].options);
|
|
1596
1947
|
}
|
|
1597
1948
|
/**
|
|
1598
1949
|
* Handles a batch of events for a projection with a batch handler.
|
|
@@ -1602,33 +1953,26 @@ var Act = class {
|
|
|
1602
1953
|
* in a single call, enabling bulk DB operations.
|
|
1603
1954
|
*
|
|
1604
1955
|
* @internal
|
|
1605
|
-
* @param lease The lease to handle
|
|
1606
|
-
* @param payloads The reactions to handle
|
|
1607
|
-
* @param batchHandler The batch handler for this projection
|
|
1608
|
-
* @returns The lease with results
|
|
1609
1956
|
*/
|
|
1610
1957
|
async handleBatch(lease, payloads, batchHandler) {
|
|
1611
1958
|
const stream = lease.stream;
|
|
1612
1959
|
const events = payloads.map((p) => p.event);
|
|
1613
|
-
const
|
|
1614
|
-
lease.retry > 0
|
|
1615
|
-
|
|
1616
|
-
|
|
1960
|
+
const options = payloads[0].options;
|
|
1961
|
+
if (lease.retry > 0)
|
|
1962
|
+
this._logger.warn(
|
|
1963
|
+
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
1964
|
+
);
|
|
1617
1965
|
try {
|
|
1618
1966
|
await batchHandler(events, stream);
|
|
1619
|
-
return
|
|
1620
|
-
} catch (error) {
|
|
1621
|
-
this._logger.error(error);
|
|
1622
|
-
const { options } = payloads[0];
|
|
1623
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1624
|
-
block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
1625
|
-
return {
|
|
1967
|
+
return this._finalize(
|
|
1626
1968
|
lease,
|
|
1627
|
-
|
|
1628
|
-
at
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1969
|
+
events.length,
|
|
1970
|
+
events.at(-1).id,
|
|
1971
|
+
void 0,
|
|
1972
|
+
options
|
|
1973
|
+
);
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
return this._finalize(lease, 0, lease.at, error, options);
|
|
1632
1976
|
}
|
|
1633
1977
|
}
|
|
1634
1978
|
/**
|
|
@@ -1678,82 +2022,46 @@ var Act = class {
|
|
|
1678
2022
|
if (!this._needs_drain) {
|
|
1679
2023
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1680
2024
|
}
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
for (const f of fetched) {
|
|
1703
|
-
const { stream, events } = f;
|
|
1704
|
-
const payloads = events.flatMap((event) => {
|
|
1705
|
-
const register = this.registry.events[event.name];
|
|
1706
|
-
if (!register) return [];
|
|
1707
|
-
return [...register.reactions.values()].filter((reaction) => {
|
|
1708
|
-
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1709
|
-
return resolved && resolved.target === stream;
|
|
1710
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1711
|
-
});
|
|
1712
|
-
fetchMap.set(stream, { fetch: f, payloads });
|
|
1713
|
-
}
|
|
1714
|
-
const handled = await Promise.all(
|
|
1715
|
-
leased.map((lease) => {
|
|
1716
|
-
const entry = fetchMap.get(lease.stream);
|
|
1717
|
-
const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1718
|
-
const payloads = entry?.payloads ?? [];
|
|
1719
|
-
const batchHandler = this._batch_handlers.get(lease.stream);
|
|
1720
|
-
if (batchHandler && payloads.length > 0) {
|
|
1721
|
-
return this.handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1722
|
-
}
|
|
1723
|
-
return this.handle({ ...lease, at }, payloads);
|
|
1724
|
-
})
|
|
1725
|
-
);
|
|
1726
|
-
const [lagging_handled, leading_handled] = handled.reduce(
|
|
1727
|
-
([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
|
|
1728
|
-
lagging_handled2 + (lease.lagging ? handled2 : 0),
|
|
1729
|
-
leading_handled2 + (lease.lagging ? 0 : handled2)
|
|
1730
|
-
],
|
|
1731
|
-
[0, 0]
|
|
1732
|
-
);
|
|
1733
|
-
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1734
|
-
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1735
|
-
const total = lagging_avg + leading_avg;
|
|
1736
|
-
this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
|
|
1737
|
-
const acked = await this._cd.ack(
|
|
1738
|
-
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1739
|
-
);
|
|
1740
|
-
if (acked.length) this.emit("acked", acked);
|
|
1741
|
-
const blocked = await this._cd.block(
|
|
1742
|
-
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1743
|
-
);
|
|
1744
|
-
if (blocked.length) this.emit("blocked", blocked);
|
|
1745
|
-
const result = { fetched, leased, acked, blocked };
|
|
1746
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1747
|
-
if (!acked.length && !blocked.length && !hasErrors)
|
|
1748
|
-
this._needs_drain = false;
|
|
1749
|
-
return result;
|
|
1750
|
-
} catch (error) {
|
|
1751
|
-
this._logger.error(error);
|
|
1752
|
-
} finally {
|
|
1753
|
-
this._drain_locked = false;
|
|
2025
|
+
if (this._drain_locked) {
|
|
2026
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2027
|
+
}
|
|
2028
|
+
try {
|
|
2029
|
+
this._drain_locked = true;
|
|
2030
|
+
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
2031
|
+
const leading = streamLimit - lagging;
|
|
2032
|
+
const cycle = await runDrainCycle(
|
|
2033
|
+
this._cd,
|
|
2034
|
+
this.registry,
|
|
2035
|
+
this._batch_handlers,
|
|
2036
|
+
this._bound_handle,
|
|
2037
|
+
this._bound_handle_batch,
|
|
2038
|
+
lagging,
|
|
2039
|
+
leading,
|
|
2040
|
+
eventLimit,
|
|
2041
|
+
leaseMillis
|
|
2042
|
+
);
|
|
2043
|
+
if (!cycle) {
|
|
2044
|
+
this._needs_drain = false;
|
|
2045
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1754
2046
|
}
|
|
2047
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2048
|
+
this._drain_lag2lead_ratio = computeLagLeadRatio(
|
|
2049
|
+
handled,
|
|
2050
|
+
lagging,
|
|
2051
|
+
leading
|
|
2052
|
+
);
|
|
2053
|
+
if (acked.length) this.emit("acked", acked);
|
|
2054
|
+
if (blocked.length) this.emit("blocked", blocked);
|
|
2055
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
2056
|
+
if (!acked.length && !blocked.length && !hasErrors)
|
|
2057
|
+
this._needs_drain = false;
|
|
2058
|
+
return { fetched, leased, acked, blocked };
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
this._logger.error(error);
|
|
2061
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2062
|
+
} finally {
|
|
2063
|
+
this._drain_locked = false;
|
|
1755
2064
|
}
|
|
1756
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1757
2065
|
}
|
|
1758
2066
|
/**
|
|
1759
2067
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -1814,7 +2122,7 @@ var Act = class {
|
|
|
1814
2122
|
this._correlation_checkpoint = watermark;
|
|
1815
2123
|
if (this._reactive_events.size > 0) this._needs_drain = true;
|
|
1816
2124
|
for (const { stream } of this._static_targets) {
|
|
1817
|
-
this.
|
|
2125
|
+
this._subscribed_streams.add(stream);
|
|
1818
2126
|
}
|
|
1819
2127
|
}
|
|
1820
2128
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
@@ -1832,7 +2140,7 @@ var Act = class {
|
|
|
1832
2140
|
for (const reaction of register.reactions.values()) {
|
|
1833
2141
|
if (typeof reaction.resolver !== "function") continue;
|
|
1834
2142
|
const resolved = reaction.resolver(event);
|
|
1835
|
-
if (resolved && !this.
|
|
2143
|
+
if (resolved && !this._subscribed_streams.has(resolved.target)) {
|
|
1836
2144
|
const entry = correlated.get(resolved.target) || {
|
|
1837
2145
|
source: resolved.source,
|
|
1838
2146
|
payloads: []
|
|
@@ -1858,7 +2166,7 @@ var Act = class {
|
|
|
1858
2166
|
this._correlation_checkpoint = last_id;
|
|
1859
2167
|
if (subscribed) {
|
|
1860
2168
|
for (const { stream } of streams) {
|
|
1861
|
-
this.
|
|
2169
|
+
this._subscribed_streams.add(stream);
|
|
1862
2170
|
}
|
|
1863
2171
|
}
|
|
1864
2172
|
return { subscribed, last_id };
|
|
@@ -2041,143 +2349,14 @@ var Act = class {
|
|
|
2041
2349
|
*/
|
|
2042
2350
|
async close(targets) {
|
|
2043
2351
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2044
|
-
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
2045
|
-
const streams = [...targetMap.keys()];
|
|
2046
2352
|
await this.correlate({ limit: 1e3 });
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
await store().query(
|
|
2054
|
-
(e) => {
|
|
2055
|
-
if (e.name === TOMBSTONE_EVENT) return;
|
|
2056
|
-
if (maxId === -1) {
|
|
2057
|
-
maxId = e.id;
|
|
2058
|
-
version = e.version;
|
|
2059
|
-
}
|
|
2060
|
-
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
2061
|
-
lastEventName = e.name;
|
|
2062
|
-
}
|
|
2063
|
-
},
|
|
2064
|
-
// limit: 2 covers the typical snapshot-at-head case (snapshot is
|
|
2065
|
-
// always preceded by the domain event it captured). Streams with
|
|
2066
|
-
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
2067
|
-
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
2068
|
-
);
|
|
2069
|
-
if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
|
|
2070
|
-
})
|
|
2071
|
-
);
|
|
2072
|
-
const skipped = [];
|
|
2073
|
-
let safe;
|
|
2074
|
-
if (this._reactive_events.size === 0) {
|
|
2075
|
-
safe = [...streamInfo.keys()];
|
|
2076
|
-
} else {
|
|
2077
|
-
const pendingSet = /* @__PURE__ */ new Set();
|
|
2078
|
-
await store().query_streams((position) => {
|
|
2079
|
-
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
2080
|
-
for (const [stream, info] of streamInfo) {
|
|
2081
|
-
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
2082
|
-
pendingSet.add(stream);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
safe = [];
|
|
2087
|
-
for (const [stream] of streamInfo) {
|
|
2088
|
-
if (pendingSet.has(stream)) {
|
|
2089
|
-
skipped.push(stream);
|
|
2090
|
-
} else {
|
|
2091
|
-
safe.push(stream);
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
if (!safe.length) {
|
|
2096
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
2097
|
-
this.emit("closed", result2);
|
|
2098
|
-
return result2;
|
|
2099
|
-
}
|
|
2100
|
-
const correlation = (0, import_crypto2.randomUUID)();
|
|
2101
|
-
const guarded = [];
|
|
2102
|
-
const guardEvents = /* @__PURE__ */ new Map();
|
|
2103
|
-
await Promise.all(
|
|
2104
|
-
safe.map(async (stream) => {
|
|
2105
|
-
try {
|
|
2106
|
-
const info = streamInfo.get(stream);
|
|
2107
|
-
const [committed] = await store().commit(
|
|
2108
|
-
stream,
|
|
2109
|
-
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
2110
|
-
{ correlation, causation: {} },
|
|
2111
|
-
info.version
|
|
2112
|
-
);
|
|
2113
|
-
guarded.push(stream);
|
|
2114
|
-
guardEvents.set(stream, { id: committed.id, stream });
|
|
2115
|
-
} catch {
|
|
2116
|
-
skipped.push(stream);
|
|
2117
|
-
}
|
|
2118
|
-
})
|
|
2119
|
-
);
|
|
2120
|
-
if (!guarded.length) {
|
|
2121
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
2122
|
-
this.emit("closed", result2);
|
|
2123
|
-
return result2;
|
|
2124
|
-
}
|
|
2125
|
-
const seedStates = /* @__PURE__ */ new Map();
|
|
2126
|
-
await Promise.all(
|
|
2127
|
-
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
2128
|
-
const lastEventName = streamInfo.get(stream)?.lastEventName;
|
|
2129
|
-
const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
|
|
2130
|
-
if (!ownerState) {
|
|
2131
|
-
this._logger.error(
|
|
2132
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
|
|
2133
|
-
);
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
const snap2 = await this._es.load(ownerState, stream);
|
|
2137
|
-
seedStates.set(stream, snap2.state);
|
|
2138
|
-
})
|
|
2139
|
-
);
|
|
2140
|
-
for (const stream of guarded) {
|
|
2141
|
-
const archiveFn = targetMap.get(stream)?.archive;
|
|
2142
|
-
if (archiveFn) await archiveFn();
|
|
2143
|
-
}
|
|
2144
|
-
const truncTargets = guarded.map((stream) => {
|
|
2145
|
-
const snapshot = seedStates.get(stream);
|
|
2146
|
-
const guard = guardEvents.get(stream);
|
|
2147
|
-
return {
|
|
2148
|
-
stream,
|
|
2149
|
-
snapshot,
|
|
2150
|
-
meta: {
|
|
2151
|
-
correlation,
|
|
2152
|
-
causation: {
|
|
2153
|
-
event: {
|
|
2154
|
-
id: guard.id,
|
|
2155
|
-
name: TOMBSTONE_EVENT,
|
|
2156
|
-
stream: guard.stream
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
};
|
|
2353
|
+
const result = await runCloseCycle(targets, {
|
|
2354
|
+
reactiveEventsSize: this._reactive_events.size,
|
|
2355
|
+
eventToState: this._event_to_state,
|
|
2356
|
+
load: this._es.load,
|
|
2357
|
+
tombstone: this._es.tombstone,
|
|
2358
|
+
logger: this._logger
|
|
2161
2359
|
});
|
|
2162
|
-
const truncated = await store().truncate(truncTargets);
|
|
2163
|
-
await Promise.all(
|
|
2164
|
-
guarded.map(async (stream) => {
|
|
2165
|
-
const entry = truncated.get(stream);
|
|
2166
|
-
const state2 = seedStates.get(stream);
|
|
2167
|
-
if (state2 && entry) {
|
|
2168
|
-
await cache().set(stream, {
|
|
2169
|
-
state: state2,
|
|
2170
|
-
version: entry.committed.version,
|
|
2171
|
-
event_id: entry.committed.id,
|
|
2172
|
-
patches: 0,
|
|
2173
|
-
snaps: 1
|
|
2174
|
-
});
|
|
2175
|
-
} else {
|
|
2176
|
-
await cache().invalidate(stream);
|
|
2177
|
-
}
|
|
2178
|
-
})
|
|
2179
|
-
);
|
|
2180
|
-
const result = { truncated, skipped };
|
|
2181
2360
|
this.emit("closed", result);
|
|
2182
2361
|
return result;
|
|
2183
2362
|
}
|
|
@@ -2246,7 +2425,7 @@ var Act = class {
|
|
|
2246
2425
|
}
|
|
2247
2426
|
};
|
|
2248
2427
|
|
|
2249
|
-
// src/act-builder.ts
|
|
2428
|
+
// src/builders/act-builder.ts
|
|
2250
2429
|
function registerBatchHandler(proj, batchHandlers) {
|
|
2251
2430
|
if (!proj.batchHandler || !proj.target) return;
|
|
2252
2431
|
const existing = batchHandlers.get(proj.target);
|
|
@@ -2255,56 +2434,33 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
2255
2434
|
}
|
|
2256
2435
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
2257
2436
|
}
|
|
2258
|
-
function act(
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
},
|
|
2437
|
+
function act() {
|
|
2438
|
+
const states = /* @__PURE__ */ new Map();
|
|
2439
|
+
const registry = {
|
|
2440
|
+
actions: {},
|
|
2441
|
+
events: {}
|
|
2442
|
+
};
|
|
2443
|
+
const pendingProjections = [];
|
|
2444
|
+
const batchHandlers = /* @__PURE__ */ new Map();
|
|
2262
2445
|
const builder = {
|
|
2263
2446
|
withState: (state2) => {
|
|
2264
2447
|
registerState(state2, states, registry.actions, registry.events);
|
|
2265
|
-
return
|
|
2266
|
-
states,
|
|
2267
|
-
registry,
|
|
2268
|
-
pendingProjections,
|
|
2269
|
-
batchHandlers
|
|
2270
|
-
);
|
|
2448
|
+
return builder;
|
|
2271
2449
|
},
|
|
2272
2450
|
withSlice: (input) => {
|
|
2273
2451
|
for (const s of input.states.values()) {
|
|
2274
2452
|
registerState(s, states, registry.actions, registry.events);
|
|
2275
2453
|
}
|
|
2276
|
-
|
|
2277
|
-
const sliceRegister = input.events[eventName];
|
|
2278
|
-
for (const [name, reaction] of sliceRegister.reactions) {
|
|
2279
|
-
registry.events[eventName].reactions.set(name, reaction);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2454
|
+
mergeEventRegister(registry.events, input.events);
|
|
2282
2455
|
pendingProjections.push(...input.projections);
|
|
2283
|
-
return
|
|
2284
|
-
states,
|
|
2285
|
-
registry,
|
|
2286
|
-
pendingProjections,
|
|
2287
|
-
batchHandlers
|
|
2288
|
-
);
|
|
2456
|
+
return builder;
|
|
2289
2457
|
},
|
|
2290
2458
|
withProjection: (proj) => {
|
|
2291
2459
|
mergeProjection(proj, registry.events);
|
|
2292
2460
|
registerBatchHandler(proj, batchHandlers);
|
|
2293
|
-
return
|
|
2294
|
-
states,
|
|
2295
|
-
registry,
|
|
2296
|
-
pendingProjections,
|
|
2297
|
-
batchHandlers
|
|
2298
|
-
);
|
|
2299
|
-
},
|
|
2300
|
-
withActor: () => {
|
|
2301
|
-
return act(
|
|
2302
|
-
states,
|
|
2303
|
-
registry,
|
|
2304
|
-
pendingProjections,
|
|
2305
|
-
batchHandlers
|
|
2306
|
-
);
|
|
2461
|
+
return builder;
|
|
2307
2462
|
},
|
|
2463
|
+
withActor: () => builder,
|
|
2308
2464
|
on: (event) => ({
|
|
2309
2465
|
do: (handler, options) => {
|
|
2310
2466
|
const reaction = {
|
|
@@ -2320,19 +2476,15 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2320
2476
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2321
2477
|
);
|
|
2322
2478
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
2323
|
-
return {
|
|
2324
|
-
...builder,
|
|
2479
|
+
return Object.assign(builder, {
|
|
2325
2480
|
to(resolver) {
|
|
2326
|
-
|
|
2327
|
-
...reaction,
|
|
2328
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2329
|
-
});
|
|
2481
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2330
2482
|
return builder;
|
|
2331
2483
|
}
|
|
2332
|
-
};
|
|
2484
|
+
});
|
|
2333
2485
|
}
|
|
2334
2486
|
}),
|
|
2335
|
-
build: () => {
|
|
2487
|
+
build: (options) => {
|
|
2336
2488
|
for (const proj of pendingProjections) {
|
|
2337
2489
|
mergeProjection(proj, registry.events);
|
|
2338
2490
|
registerBatchHandler(proj, batchHandlers);
|
|
@@ -2340,7 +2492,8 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2340
2492
|
return new Act(
|
|
2341
2493
|
registry,
|
|
2342
2494
|
states,
|
|
2343
|
-
batchHandlers
|
|
2495
|
+
batchHandlers,
|
|
2496
|
+
options
|
|
2344
2497
|
);
|
|
2345
2498
|
},
|
|
2346
2499
|
events: registry.events
|
|
@@ -2348,8 +2501,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2348
2501
|
return builder;
|
|
2349
2502
|
}
|
|
2350
2503
|
|
|
2351
|
-
// src/projection-builder.ts
|
|
2352
|
-
function _projection(target
|
|
2504
|
+
// src/builders/projection-builder.ts
|
|
2505
|
+
function _projection(target) {
|
|
2506
|
+
const events = {};
|
|
2353
2507
|
const defaultResolver = typeof target === "string" ? { target } : void 0;
|
|
2354
2508
|
const base = {
|
|
2355
2509
|
on: (entry) => {
|
|
@@ -2379,17 +2533,13 @@ function _projection(target, events) {
|
|
|
2379
2533
|
`Projection handler for "${event}" must be a named function`
|
|
2380
2534
|
);
|
|
2381
2535
|
register.reactions.set(handler.name, reaction);
|
|
2382
|
-
const
|
|
2383
|
-
return {
|
|
2384
|
-
...nextBuilder,
|
|
2536
|
+
const widened = base;
|
|
2537
|
+
return Object.assign(widened, {
|
|
2385
2538
|
to(resolver) {
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2389
|
-
});
|
|
2390
|
-
return nextBuilder;
|
|
2539
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2540
|
+
return widened;
|
|
2391
2541
|
}
|
|
2392
|
-
};
|
|
2542
|
+
});
|
|
2393
2543
|
}
|
|
2394
2544
|
};
|
|
2395
2545
|
},
|
|
@@ -2401,8 +2551,7 @@ function _projection(target, events) {
|
|
|
2401
2551
|
events
|
|
2402
2552
|
};
|
|
2403
2553
|
if (typeof target === "string") {
|
|
2404
|
-
return {
|
|
2405
|
-
...base,
|
|
2554
|
+
return Object.assign(base, {
|
|
2406
2555
|
batch: (handler) => ({
|
|
2407
2556
|
build: () => ({
|
|
2408
2557
|
_tag: "Projection",
|
|
@@ -2411,34 +2560,28 @@ function _projection(target, events) {
|
|
|
2411
2560
|
batchHandler: handler
|
|
2412
2561
|
})
|
|
2413
2562
|
})
|
|
2414
|
-
};
|
|
2563
|
+
});
|
|
2415
2564
|
}
|
|
2416
2565
|
return base;
|
|
2417
2566
|
}
|
|
2418
|
-
function projection(target
|
|
2419
|
-
return _projection(target
|
|
2567
|
+
function projection(target) {
|
|
2568
|
+
return _projection(target);
|
|
2420
2569
|
}
|
|
2421
2570
|
|
|
2422
|
-
// src/slice-builder.ts
|
|
2423
|
-
function slice(
|
|
2571
|
+
// src/builders/slice-builder.ts
|
|
2572
|
+
function slice() {
|
|
2573
|
+
const states = /* @__PURE__ */ new Map();
|
|
2574
|
+
const actions = {};
|
|
2575
|
+
const events = {};
|
|
2576
|
+
const projections = [];
|
|
2424
2577
|
const builder = {
|
|
2425
2578
|
withState: (state2) => {
|
|
2426
2579
|
registerState(state2, states, actions, events);
|
|
2427
|
-
return
|
|
2428
|
-
states,
|
|
2429
|
-
actions,
|
|
2430
|
-
events,
|
|
2431
|
-
projections
|
|
2432
|
-
);
|
|
2580
|
+
return builder;
|
|
2433
2581
|
},
|
|
2434
2582
|
withProjection: (proj) => {
|
|
2435
2583
|
projections.push(proj);
|
|
2436
|
-
return
|
|
2437
|
-
states,
|
|
2438
|
-
actions,
|
|
2439
|
-
events,
|
|
2440
|
-
projections
|
|
2441
|
-
);
|
|
2584
|
+
return builder;
|
|
2442
2585
|
},
|
|
2443
2586
|
on: (event) => ({
|
|
2444
2587
|
do: (handler, options) => {
|
|
@@ -2455,16 +2598,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2455
2598
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2456
2599
|
);
|
|
2457
2600
|
events[event].reactions.set(handler.name, reaction);
|
|
2458
|
-
return {
|
|
2459
|
-
...builder,
|
|
2601
|
+
return Object.assign(builder, {
|
|
2460
2602
|
to(resolver) {
|
|
2461
|
-
|
|
2462
|
-
...reaction,
|
|
2463
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2464
|
-
});
|
|
2603
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2465
2604
|
return builder;
|
|
2466
2605
|
}
|
|
2467
|
-
};
|
|
2606
|
+
});
|
|
2468
2607
|
}
|
|
2469
2608
|
}),
|
|
2470
2609
|
build: () => ({
|
|
@@ -2478,7 +2617,7 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2478
2617
|
return builder;
|
|
2479
2618
|
}
|
|
2480
2619
|
|
|
2481
|
-
// src/state-builder.ts
|
|
2620
|
+
// src/builders/state-builder.ts
|
|
2482
2621
|
function state(entry) {
|
|
2483
2622
|
const keys = Object.keys(entry);
|
|
2484
2623
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
@@ -2496,7 +2635,7 @@ function state(entry) {
|
|
|
2496
2635
|
return [k, fn];
|
|
2497
2636
|
})
|
|
2498
2637
|
);
|
|
2499
|
-
const
|
|
2638
|
+
const internal = {
|
|
2500
2639
|
events,
|
|
2501
2640
|
actions: {},
|
|
2502
2641
|
state: stateSchema,
|
|
@@ -2504,18 +2643,12 @@ function state(entry) {
|
|
|
2504
2643
|
init,
|
|
2505
2644
|
patch: defaultPatch,
|
|
2506
2645
|
on: {}
|
|
2507
|
-
}
|
|
2646
|
+
};
|
|
2647
|
+
const builder = action_builder(internal);
|
|
2508
2648
|
return Object.assign(builder, {
|
|
2509
2649
|
patch(customPatch) {
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
actions: {},
|
|
2513
|
-
state: stateSchema,
|
|
2514
|
-
name,
|
|
2515
|
-
init,
|
|
2516
|
-
patch: { ...defaultPatch, ...customPatch },
|
|
2517
|
-
on: {}
|
|
2518
|
-
});
|
|
2650
|
+
Object.assign(internal.patch, customPatch);
|
|
2651
|
+
return builder;
|
|
2519
2652
|
}
|
|
2520
2653
|
});
|
|
2521
2654
|
}
|
|
@@ -2524,50 +2657,43 @@ function state(entry) {
|
|
|
2524
2657
|
};
|
|
2525
2658
|
}
|
|
2526
2659
|
function action_builder(state2) {
|
|
2527
|
-
|
|
2660
|
+
const internal = state2;
|
|
2661
|
+
const builder = {
|
|
2528
2662
|
on(entry) {
|
|
2529
2663
|
const keys = Object.keys(entry);
|
|
2530
2664
|
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
2531
2665
|
const action2 = keys[0];
|
|
2532
2666
|
const schema = entry[action2];
|
|
2533
|
-
if (action2 in
|
|
2667
|
+
if (action2 in internal.actions)
|
|
2534
2668
|
throw new Error(`Duplicate action "${action2}"`);
|
|
2535
|
-
|
|
2536
|
-
...state2.actions,
|
|
2537
|
-
[action2]: schema
|
|
2538
|
-
};
|
|
2539
|
-
const on = { ...state2.on };
|
|
2540
|
-
const _given = { ...state2.given };
|
|
2669
|
+
internal.actions[action2] = schema;
|
|
2541
2670
|
function given(rules) {
|
|
2542
|
-
|
|
2671
|
+
(internal.given ??= {})[action2] = rules;
|
|
2543
2672
|
return { emit };
|
|
2544
2673
|
}
|
|
2545
2674
|
function emit(handler) {
|
|
2546
2675
|
if (typeof handler === "string") {
|
|
2547
2676
|
const eventName = handler;
|
|
2548
|
-
on[action2] = (
|
|
2677
|
+
internal.on[action2] = (payload) => [
|
|
2678
|
+
eventName,
|
|
2679
|
+
payload
|
|
2680
|
+
];
|
|
2549
2681
|
} else {
|
|
2550
|
-
on[action2] = handler;
|
|
2682
|
+
internal.on[action2] = handler;
|
|
2551
2683
|
}
|
|
2552
|
-
return
|
|
2553
|
-
...state2,
|
|
2554
|
-
actions,
|
|
2555
|
-
on,
|
|
2556
|
-
given: _given
|
|
2557
|
-
});
|
|
2684
|
+
return builder;
|
|
2558
2685
|
}
|
|
2559
2686
|
return { given, emit };
|
|
2560
2687
|
},
|
|
2561
2688
|
snap(snap2) {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
snap: snap2
|
|
2565
|
-
});
|
|
2689
|
+
internal.snap = snap2;
|
|
2690
|
+
return builder;
|
|
2566
2691
|
},
|
|
2567
2692
|
build() {
|
|
2568
|
-
return
|
|
2693
|
+
return internal;
|
|
2569
2694
|
}
|
|
2570
2695
|
};
|
|
2696
|
+
return builder;
|
|
2571
2697
|
}
|
|
2572
2698
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2573
2699
|
0 && (module.exports = {
|
|
@@ -2577,6 +2703,7 @@ function action_builder(state2) {
|
|
|
2577
2703
|
CommittedMetaSchema,
|
|
2578
2704
|
ConcurrencyError,
|
|
2579
2705
|
ConsoleLogger,
|
|
2706
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2580
2707
|
Environments,
|
|
2581
2708
|
Errors,
|
|
2582
2709
|
EventMetaSchema,
|