@rotorsoft/act 0.32.3 → 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.
Files changed (46) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +71 -47
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/adapters/InMemoryCache.d.ts +1 -2
  5. package/dist/@types/adapters/InMemoryCache.d.ts.map +1 -1
  6. package/dist/@types/{act-builder.d.ts → builders/act-builder.d.ts} +5 -5
  7. package/dist/@types/builders/act-builder.d.ts.map +1 -0
  8. package/dist/@types/builders/index.d.ts +13 -0
  9. package/dist/@types/builders/index.d.ts.map +1 -0
  10. package/dist/@types/{projection-builder.d.ts → builders/projection-builder.d.ts} +3 -3
  11. package/dist/@types/builders/projection-builder.d.ts.map +1 -0
  12. package/dist/@types/{slice-builder.d.ts → builders/slice-builder.d.ts} +2 -2
  13. package/dist/@types/builders/slice-builder.d.ts.map +1 -0
  14. package/dist/@types/{state-builder.d.ts → builders/state-builder.d.ts} +1 -1
  15. package/dist/@types/builders/state-builder.d.ts.map +1 -0
  16. package/dist/@types/config.d.ts.map +1 -1
  17. package/dist/@types/index.d.ts +1 -4
  18. package/dist/@types/index.d.ts.map +1 -1
  19. package/dist/@types/internal/close-cycle.d.ts +38 -0
  20. package/dist/@types/internal/close-cycle.d.ts.map +1 -0
  21. package/dist/@types/internal/drain-cycle.d.ts +61 -0
  22. package/dist/@types/internal/drain-cycle.d.ts.map +1 -0
  23. package/dist/@types/internal/drain-ratio.d.ts +26 -0
  24. package/dist/@types/internal/drain-ratio.d.ts.map +1 -0
  25. package/dist/@types/internal/event-sourcing.d.ts +14 -0
  26. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  27. package/dist/@types/internal/index.d.ts +5 -1
  28. package/dist/@types/internal/index.d.ts.map +1 -1
  29. package/dist/@types/internal/lru-map.d.ts +50 -0
  30. package/dist/@types/internal/lru-map.d.ts.map +1 -0
  31. package/dist/@types/internal/merge.d.ts +13 -1
  32. package/dist/@types/internal/merge.d.ts.map +1 -1
  33. package/dist/@types/internal/tracing.d.ts +9 -2
  34. package/dist/@types/internal/tracing.d.ts.map +1 -1
  35. package/dist/@types/ports.d.ts.map +1 -1
  36. package/dist/@types/types/reaction.d.ts +7 -1
  37. package/dist/@types/types/reaction.d.ts.map +1 -1
  38. package/dist/index.cjs +581 -416
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.js +580 -416
  41. package/dist/index.js.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/@types/act-builder.d.ts.map +0 -1
  44. package/dist/@types/projection-builder.d.ts.map +0 -1
  45. package/dist/@types/slice-builder.d.ts.map +0 -1
  46. package/dist/@types/state-builder.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -117,26 +117,75 @@ var ConsoleLogger = class _ConsoleLogger {
117
117
  }
118
118
  };
119
119
 
120
+ // src/internal/lru-map.ts
121
+ var LruMap = class {
122
+ constructor(_maxSize) {
123
+ this._maxSize = _maxSize;
124
+ }
125
+ _entries = /* @__PURE__ */ new Map();
126
+ get(key) {
127
+ const v = this._entries.get(key);
128
+ if (v === void 0) return void 0;
129
+ this._entries.delete(key);
130
+ this._entries.set(key, v);
131
+ return v;
132
+ }
133
+ has(key) {
134
+ return this._entries.has(key);
135
+ }
136
+ set(key, value) {
137
+ this._entries.delete(key);
138
+ if (this._entries.size >= this._maxSize) {
139
+ const oldest = this._entries.keys().next().value;
140
+ if (oldest !== void 0) this._entries.delete(oldest);
141
+ }
142
+ this._entries.set(key, value);
143
+ }
144
+ delete(key) {
145
+ return this._entries.delete(key);
146
+ }
147
+ clear() {
148
+ this._entries.clear();
149
+ }
150
+ get size() {
151
+ return this._entries.size;
152
+ }
153
+ };
154
+ var LruSet = class {
155
+ _map;
156
+ constructor(maxSize) {
157
+ this._map = new LruMap(maxSize);
158
+ }
159
+ has(value) {
160
+ return this._map.has(value);
161
+ }
162
+ add(value) {
163
+ this._map.set(value, true);
164
+ }
165
+ delete(value) {
166
+ return this._map.delete(value);
167
+ }
168
+ clear() {
169
+ this._map.clear();
170
+ }
171
+ get size() {
172
+ return this._map.size;
173
+ }
174
+ };
175
+
120
176
  // src/adapters/InMemoryCache.ts
121
177
  var InMemoryCache = class {
122
- _entries = /* @__PURE__ */ new Map();
123
- _maxSize;
178
+ // CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
179
+ // any is bidirectionally compatible with the per-call TState binding, while
180
+ // the public Cache interface still presents a typed surface to callers.
181
+ _entries;
124
182
  constructor(options) {
125
- this._maxSize = options?.maxSize ?? 1e3;
183
+ this._entries = new LruMap(options?.maxSize ?? 1e3);
126
184
  }
127
185
  async get(stream) {
128
- const entry = this._entries.get(stream);
129
- if (!entry) return void 0;
130
- this._entries.delete(stream);
131
- this._entries.set(stream, entry);
132
- return entry;
186
+ return this._entries.get(stream);
133
187
  }
134
188
  async set(stream, entry) {
135
- this._entries.delete(stream);
136
- if (this._entries.size >= this._maxSize) {
137
- const first = this._entries.keys().next().value;
138
- this._entries.delete(first);
139
- }
140
189
  this._entries.set(stream, entry);
141
190
  }
142
191
  async invalidate(stream) {
@@ -179,12 +228,19 @@ var BaseSchema = PackageSchema.extend({
179
228
  });
180
229
  var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
181
230
  var env = NODE_ENV || "development";
182
- var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "error" : NODE_ENV === "production" ? "info" : "trace");
231
+ var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
183
232
  var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
184
233
  var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
185
234
  var pkg = getPackage();
235
+ var _validated;
186
236
  var config = () => {
187
- return extend({ ...pkg, env, logLevel, logSingleLine, sleepMs }, BaseSchema);
237
+ if (!_validated) {
238
+ _validated = extend(
239
+ { ...pkg, env, logLevel, logSingleLine, sleepMs },
240
+ BaseSchema
241
+ );
242
+ }
243
+ return _validated;
188
244
  };
189
245
 
190
246
  // src/utils.ts
@@ -607,7 +663,7 @@ function port(injector) {
607
663
  if (!adapters.has(injector.name)) {
608
664
  const injected = injector(adapter);
609
665
  adapters.set(injector.name, injected);
610
- console.log(`[act] + ${injector.name}:${injected.constructor.name}`);
666
+ log().info(`[act] + ${injector.name}:${injected.constructor.name}`);
611
667
  }
612
668
  return adapters.get(injector.name);
613
669
  };
@@ -633,7 +689,7 @@ async function disposeAndExit(code = "EXIT") {
633
689
  }
634
690
  for (const adapter of [...adapters.values()].reverse()) {
635
691
  await adapter.dispose();
636
- console.log(`[act] - ${adapter.constructor.name}`);
692
+ log().info(`[act] - ${adapter.constructor.name}`);
637
693
  }
638
694
  adapters.clear();
639
695
  config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
@@ -664,9 +720,233 @@ process.once("unhandledRejection", async (arg) => {
664
720
  });
665
721
 
666
722
  // src/act.ts
667
- import { randomUUID as randomUUID2 } from "crypto";
668
723
  import EventEmitter from "events";
669
724
 
725
+ // src/internal/close-cycle.ts
726
+ import { randomUUID } from "crypto";
727
+ async function runCloseCycle(targets, deps) {
728
+ if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
729
+ const targetMap = new Map(targets.map((t) => [t.stream, t]));
730
+ const streams = [...targetMap.keys()];
731
+ const skipped = [];
732
+ const streamInfo = await scanStreamHeads(streams);
733
+ const safe = await partitionBySafety(
734
+ streamInfo,
735
+ deps.reactiveEventsSize,
736
+ skipped
737
+ );
738
+ if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
739
+ const correlation = randomUUID();
740
+ const { guarded, guardEvents } = await guardWithTombstones(
741
+ safe,
742
+ streamInfo,
743
+ correlation,
744
+ deps.tombstone,
745
+ skipped
746
+ );
747
+ if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
748
+ const seedStates = await loadRestartSeeds(
749
+ guarded,
750
+ targetMap,
751
+ streamInfo,
752
+ deps.eventToState,
753
+ deps.load,
754
+ deps.logger
755
+ );
756
+ await runArchiveCallbacks(guarded, targetMap);
757
+ const truncated = await truncateAndWarmCache(
758
+ guarded,
759
+ seedStates,
760
+ guardEvents,
761
+ correlation
762
+ );
763
+ return { truncated, skipped };
764
+ }
765
+ async function scanStreamHeads(streams) {
766
+ const out = /* @__PURE__ */ new Map();
767
+ await Promise.all(
768
+ streams.map(async (s) => {
769
+ let maxId = -1;
770
+ let version = -1;
771
+ let lastEventName;
772
+ await store().query(
773
+ (e) => {
774
+ if (e.name === TOMBSTONE_EVENT) return;
775
+ if (maxId === -1) {
776
+ maxId = e.id;
777
+ version = e.version;
778
+ }
779
+ if (e.name !== SNAP_EVENT && lastEventName === void 0) {
780
+ lastEventName = e.name;
781
+ }
782
+ },
783
+ // limit: 2 covers the typical snapshot-at-head case (snapshot is
784
+ // always preceded by the domain event it captured). Streams with
785
+ // unusual layouts fall back to no-seed via the lookup miss path.
786
+ { stream: s, stream_exact: true, backward: true, limit: 2 }
787
+ );
788
+ if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
789
+ })
790
+ );
791
+ return out;
792
+ }
793
+ async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
794
+ if (reactiveEventsSize === 0) return [...streamInfo.keys()];
795
+ const pendingSet = /* @__PURE__ */ new Set();
796
+ await store().query_streams((position) => {
797
+ const sourceRe = position.source ? RegExp(position.source) : void 0;
798
+ for (const [stream, info] of streamInfo) {
799
+ if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
800
+ pendingSet.add(stream);
801
+ }
802
+ }
803
+ });
804
+ const safe = [];
805
+ for (const [stream] of streamInfo) {
806
+ if (pendingSet.has(stream)) skipped.push(stream);
807
+ else safe.push(stream);
808
+ }
809
+ return safe;
810
+ }
811
+ async function guardWithTombstones(safe, streamInfo, correlation, tombstone2, skipped) {
812
+ const guarded = [];
813
+ const guardEvents = /* @__PURE__ */ new Map();
814
+ await Promise.all(
815
+ safe.map(async (stream) => {
816
+ const info = streamInfo.get(stream);
817
+ const committed = await tombstone2(stream, info.version, correlation);
818
+ if (committed) {
819
+ guarded.push(stream);
820
+ guardEvents.set(stream, { id: committed.id, stream });
821
+ } else {
822
+ skipped.push(stream);
823
+ }
824
+ })
825
+ );
826
+ return { guarded, guardEvents };
827
+ }
828
+ async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, load2, logger) {
829
+ const seedStates = /* @__PURE__ */ new Map();
830
+ await Promise.all(
831
+ guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
832
+ const lastEventName = streamInfo.get(stream)?.lastEventName;
833
+ const ownerState = lastEventName ? eventToState.get(lastEventName) : void 0;
834
+ if (!ownerState) {
835
+ logger.error(
836
+ `Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
837
+ );
838
+ return;
839
+ }
840
+ const snap2 = await load2(ownerState, stream);
841
+ seedStates.set(stream, snap2.state);
842
+ })
843
+ );
844
+ return seedStates;
845
+ }
846
+ async function runArchiveCallbacks(guarded, targetMap) {
847
+ for (const stream of guarded) {
848
+ const archiveFn = targetMap.get(stream)?.archive;
849
+ if (archiveFn) await archiveFn();
850
+ }
851
+ }
852
+ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlation) {
853
+ const truncTargets = guarded.map((stream) => {
854
+ const snapshot = seedStates.get(stream);
855
+ const guard = guardEvents.get(stream);
856
+ return {
857
+ stream,
858
+ snapshot,
859
+ meta: {
860
+ correlation,
861
+ causation: {
862
+ event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
863
+ }
864
+ }
865
+ };
866
+ });
867
+ const truncated = await store().truncate(truncTargets);
868
+ await Promise.all(
869
+ guarded.map(async (stream) => {
870
+ const entry = truncated.get(stream);
871
+ const state2 = seedStates.get(stream);
872
+ if (state2 && entry) {
873
+ await cache().set(stream, {
874
+ state: state2,
875
+ version: entry.committed.version,
876
+ event_id: entry.committed.id,
877
+ patches: 0,
878
+ snaps: 1
879
+ });
880
+ } else {
881
+ await cache().invalidate(stream);
882
+ }
883
+ })
884
+ );
885
+ return truncated;
886
+ }
887
+
888
+ // src/internal/drain-cycle.ts
889
+ import { randomUUID as randomUUID2 } from "crypto";
890
+ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
891
+ const leased = await ops.claim(lagging, leading, randomUUID2(), leaseMillis);
892
+ if (!leased.length) return void 0;
893
+ const fetched = await ops.fetch(leased, eventLimit);
894
+ const fetchMap = /* @__PURE__ */ new Map();
895
+ const fetch_window_at = fetched.reduce(
896
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
897
+ 0
898
+ );
899
+ for (const f of fetched) {
900
+ const { stream, events } = f;
901
+ const payloads = events.flatMap((event) => {
902
+ const register = registry.events[event.name];
903
+ if (!register) return [];
904
+ return [...register.reactions.values()].filter((reaction) => {
905
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
906
+ return resolved && resolved.target === stream;
907
+ }).map((reaction) => ({ ...reaction, event }));
908
+ });
909
+ fetchMap.set(stream, { fetch: f, payloads });
910
+ }
911
+ const handled = await Promise.all(
912
+ leased.map((lease) => {
913
+ const entry = fetchMap.get(lease.stream);
914
+ const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
915
+ const payloads = entry?.payloads ?? [];
916
+ const batchHandler = batchHandlers.get(lease.stream);
917
+ if (batchHandler && payloads.length > 0) {
918
+ return handleBatch({ ...lease, at }, payloads, batchHandler);
919
+ }
920
+ return handle({ ...lease, at }, payloads);
921
+ })
922
+ );
923
+ const acked = await ops.ack(
924
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
925
+ );
926
+ const blocked = await ops.block(
927
+ handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
928
+ );
929
+ return { leased, fetched, handled, acked, blocked };
930
+ }
931
+
932
+ // src/internal/drain-ratio.ts
933
+ var RATIO_MIN = 0.2;
934
+ var RATIO_MAX = 0.8;
935
+ var RATIO_DEFAULT = 0.5;
936
+ function computeLagLeadRatio(handled, lagging, leading) {
937
+ let lagging_handled = 0;
938
+ let leading_handled = 0;
939
+ for (const { lease, handled: count } of handled) {
940
+ if (lease.lagging) lagging_handled += count;
941
+ else leading_handled += count;
942
+ }
943
+ const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
944
+ const leading_avg = leading > 0 ? leading_handled / leading : 0;
945
+ const total = lagging_avg + leading_avg;
946
+ if (total === 0) return RATIO_DEFAULT;
947
+ return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
948
+ }
949
+
670
950
  // src/internal/merge.ts
671
951
  import { ZodObject } from "zod";
672
952
  function baseTypeName(zodType) {
@@ -774,6 +1054,15 @@ function mergePatches(existing, incoming, stateName) {
774
1054
  }
775
1055
  return merged;
776
1056
  }
1057
+ function mergeEventRegister(target, source) {
1058
+ for (const [eventName, sourceReg] of Object.entries(source)) {
1059
+ const targetReg = target[eventName];
1060
+ if (!targetReg) continue;
1061
+ for (const [name, reaction] of sourceReg.reactions) {
1062
+ targetReg.reactions.set(name, reaction);
1063
+ }
1064
+ }
1065
+ }
777
1066
  function mergeProjection(proj, events) {
778
1067
  for (const eventName of Object.keys(proj.events)) {
779
1068
  const projRegister = proj.events[eventName];
@@ -818,7 +1107,7 @@ var subscribe = (streams) => store().subscribe(streams);
818
1107
 
819
1108
  // src/internal/event-sourcing.ts
820
1109
  import { patch } from "@rotorsoft/act-patch";
821
- import { randomUUID } from "crypto";
1110
+ import { randomUUID as randomUUID3 } from "crypto";
822
1111
  async function snap(snapshot) {
823
1112
  try {
824
1113
  const { id, stream, name, meta, version } = snapshot.event;
@@ -836,6 +1125,20 @@ async function snap(snapshot) {
836
1125
  log().error(error);
837
1126
  }
838
1127
  }
1128
+ async function tombstone(stream, expectedVersion, correlation) {
1129
+ try {
1130
+ const [committed] = await store().commit(
1131
+ stream,
1132
+ [{ name: TOMBSTONE_EVENT, data: {} }],
1133
+ { correlation, causation: {} },
1134
+ expectedVersion
1135
+ );
1136
+ return committed;
1137
+ } catch (error) {
1138
+ if (error instanceof ConcurrencyError) return void 0;
1139
+ throw error;
1140
+ }
1141
+ }
839
1142
  async function load(me, stream, callback, asOf) {
840
1143
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
841
1144
  const cached = timeTravel ? void 0 : await cache().get(stream);
@@ -900,7 +1203,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
900
1203
  data: skipValidation ? data : validate(name, data, me.events[name])
901
1204
  }));
902
1205
  const meta = {
903
- correlation: reactingTo?.meta.correlation || randomUUID(),
1206
+ correlation: reactingTo?.meta.correlation || randomUUID3(),
904
1207
  causation: {
905
1208
  action: {
906
1209
  name: action2,
@@ -951,6 +1254,18 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
951
1254
  }
952
1255
 
953
1256
  // src/internal/tracing.ts
1257
+ var PRETTY = config().env !== "production";
1258
+ var C_BLUE = "\x1B[38;5;39m";
1259
+ var C_ORANGE = "\x1B[38;5;208m";
1260
+ var C_GREEN = "\x1B[38;5;42m";
1261
+ var C_MAGENTA = "\x1B[38;5;165m";
1262
+ var C_DRAIN = "\x1B[38;5;244m";
1263
+ var C_RESET = "\x1B[0m";
1264
+ var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
1265
+ var drain_caption = (caption) => {
1266
+ const tag = `>> ${caption}`;
1267
+ return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
1268
+ };
954
1269
  var traced = (inner, exit, entry) => (async (...args) => {
955
1270
  entry?.(...args);
956
1271
  const result = await inner(...args);
@@ -959,16 +1274,27 @@ var traced = (inner, exit, entry) => (async (...args) => {
959
1274
  });
960
1275
  function buildEs(logger) {
961
1276
  if (logger.level !== "trace") {
962
- return { snap, load, action };
1277
+ return {
1278
+ snap,
1279
+ load,
1280
+ action,
1281
+ tombstone
1282
+ };
963
1283
  }
964
1284
  return {
965
1285
  snap: traced(snap, void 0, (snapshot) => {
966
1286
  logger.trace(
967
- `\u{1F7E0} snap ${snapshot.event.stream}@${snapshot.event.version}`
1287
+ es_caption(
1288
+ "snap",
1289
+ C_MAGENTA,
1290
+ `${snapshot.event.stream}@${snapshot.event.version}`
1291
+ )
968
1292
  );
969
1293
  }),
970
1294
  load: traced(load, void 0, (_me, stream, _cb, asOf) => {
971
- logger.trace(`\u{1F7E2} load ${stream}${asOf ? " (as-of)" : ""}`);
1295
+ logger.trace(
1296
+ es_caption("load", C_GREEN, `${stream}${asOf ? " (as-of)" : ""}`)
1297
+ );
972
1298
  }),
973
1299
  action: traced(
974
1300
  action,
@@ -977,14 +1303,27 @@ function buildEs(logger) {
977
1303
  if (committed.length) {
978
1304
  logger.trace(
979
1305
  committed.map((s) => s.event.data),
980
- `\u{1F534} commit ${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
1306
+ es_caption(
1307
+ "committed",
1308
+ C_ORANGE,
1309
+ `${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
1310
+ )
981
1311
  );
982
1312
  }
983
1313
  },
984
1314
  (_me, action2, target, payload) => {
985
- logger.trace(payload, `\u{1F535} ${target.stream}.${action2}`);
1315
+ logger.trace(
1316
+ payload,
1317
+ es_caption("action", C_BLUE, `${target.stream}.${action2}`)
1318
+ );
986
1319
  }
987
- )
1320
+ ),
1321
+ tombstone: traced(tombstone, (committed, stream) => {
1322
+ if (committed)
1323
+ logger.trace(
1324
+ es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
1325
+ );
1326
+ })
988
1327
  };
989
1328
  }
990
1329
  function buildDrain(logger) {
@@ -1003,7 +1342,7 @@ function buildDrain(logger) {
1003
1342
  const data = Object.fromEntries(
1004
1343
  leased.map(({ stream, at, retry }) => [stream, { at, retry }])
1005
1344
  );
1006
- logger.trace(data, ">> lease");
1345
+ logger.trace(data, drain_caption("claimed"));
1007
1346
  }
1008
1347
  }),
1009
1348
  fetch: traced(fetch, (fetched) => {
@@ -1016,14 +1355,14 @@ function buildDrain(logger) {
1016
1355
  return [key, value];
1017
1356
  })
1018
1357
  );
1019
- logger.trace(data, ">> fetch");
1358
+ logger.trace(data, drain_caption("fetched"));
1020
1359
  }),
1021
1360
  ack: traced(ack, (acked) => {
1022
1361
  if (acked.length) {
1023
1362
  const data = Object.fromEntries(
1024
1363
  acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1025
1364
  );
1026
- logger.trace(data, ">> ack");
1365
+ logger.trace(data, drain_caption("acked"));
1027
1366
  }
1028
1367
  }),
1029
1368
  block: traced(block, (blocked) => {
@@ -1034,27 +1373,31 @@ function buildDrain(logger) {
1034
1373
  { at, retry, error }
1035
1374
  ])
1036
1375
  );
1037
- logger.trace(data, ">> block");
1376
+ logger.trace(data, drain_caption("blocked"));
1038
1377
  }
1039
1378
  }),
1040
1379
  subscribe: traced(subscribe, (result, streams) => {
1041
1380
  if (result.subscribed) {
1042
1381
  const data = streams.map(({ stream }) => stream).join(" ");
1043
- logger.trace(`>> correlate ${data}`);
1382
+ logger.trace(`${drain_caption("correlated")} ${data}`);
1044
1383
  }
1045
1384
  })
1046
1385
  };
1047
1386
  }
1048
1387
 
1049
1388
  // src/act.ts
1389
+ var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
1050
1390
  var Act = class {
1051
- constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
1391
+ constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
1052
1392
  this.registry = registry;
1053
1393
  this._states = _states;
1054
1394
  this._batch_handlers = batchHandlers;
1395
+ this._subscribed_streams = new LruSet(
1396
+ options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
1397
+ );
1055
1398
  this._es = buildEs(this._logger);
1056
1399
  this._cd = buildDrain(this._logger);
1057
- const statics = [];
1400
+ const statics = /* @__PURE__ */ new Map();
1058
1401
  for (const [name, register] of Object.entries(this.registry.events)) {
1059
1402
  if (register.reactions.size > 0) {
1060
1403
  this._reactive_events.add(name);
@@ -1063,14 +1406,13 @@ var Act = class {
1063
1406
  if (typeof reaction.resolver === "function") {
1064
1407
  this._has_dynamic_resolvers = true;
1065
1408
  } else {
1066
- statics.push({
1067
- stream: reaction.resolver.target,
1068
- source: reaction.resolver.source
1069
- });
1409
+ const { target, source } = reaction.resolver;
1410
+ const key = `${target}|${source ?? ""}`;
1411
+ if (!statics.has(key)) statics.set(key, { stream: target, source });
1070
1412
  }
1071
1413
  }
1072
1414
  }
1073
- this._static_targets = statics;
1415
+ this._static_targets = [...statics.values()];
1074
1416
  for (const merged of this._states.values()) {
1075
1417
  for (const eventName of Object.keys(merged.events)) {
1076
1418
  this._event_to_state.set(eventName, merged);
@@ -1090,20 +1432,42 @@ var Act = class {
1090
1432
  _settle_timer = void 0;
1091
1433
  _settling = false;
1092
1434
  _correlation_checkpoint = -1;
1093
- _subscribed_statics = /* @__PURE__ */ new Set();
1435
+ /**
1436
+ * Streams already subscribed via store.subscribe() — both the static
1437
+ * targets registered at init and dynamic targets discovered by
1438
+ * correlate(). correlate() consults this set to avoid re-subscribing
1439
+ * known streams.
1440
+ *
1441
+ * Bounded LRU so apps that mint millions of dynamic targets (one per
1442
+ * aggregate) don't grow this unbounded. Eviction costs at most one
1443
+ * redundant store.subscribe() call per evicted-but-still-active stream
1444
+ * (subscribe is idempotent). Cap configurable via {@link ActOptions}.
1445
+ */
1446
+ _subscribed_streams;
1094
1447
  _has_dynamic_resolvers = false;
1095
1448
  _correlation_initialized = false;
1096
1449
  /** Event names with at least one registered reaction (computed at build time) */
1097
1450
  _reactive_events = /* @__PURE__ */ new Set();
1098
1451
  /** Set in do() when a committed event has reactions — cleared by drain() */
1099
1452
  _needs_drain = false;
1453
+ /**
1454
+ * Emit a lifecycle event. The payload type is inferred from the event name
1455
+ * via {@link ActLifecycleEvents}.
1456
+ */
1100
1457
  emit(event, args) {
1101
1458
  return this._emitter.emit(event, args);
1102
1459
  }
1460
+ /**
1461
+ * Register a listener for a lifecycle event. The listener receives the
1462
+ * event-specific payload.
1463
+ */
1103
1464
  on(event, listener) {
1104
1465
  this._emitter.on(event, listener);
1105
1466
  return this;
1106
1467
  }
1468
+ /**
1469
+ * Remove a previously registered lifecycle listener.
1470
+ */
1107
1471
  off(event, listener) {
1108
1472
  this._emitter.off(event, listener);
1109
1473
  return this;
@@ -1131,6 +1495,15 @@ var Act = class {
1131
1495
  _event_to_state = /* @__PURE__ */ new Map();
1132
1496
  /** Logger resolved at construction time (after user port configuration) */
1133
1497
  _logger = log();
1498
+ /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
1499
+ * payload (it captures the triggering event for reactingTo auto-inject). */
1500
+ _bound_do = this.do.bind(this);
1501
+ _bound_load = this.load.bind(this);
1502
+ _bound_query = this.query.bind(this);
1503
+ _bound_query_array = this.query_array.bind(this);
1504
+ /** Pre-bound dispatchers handed to runDrainCycle each cycle. */
1505
+ _bound_handle = this.handle.bind(this);
1506
+ _bound_handle_batch = this.handleBatch.bind(this);
1134
1507
  /**
1135
1508
  * Executes an action on a state instance, committing resulting events.
1136
1509
  *
@@ -1333,35 +1706,55 @@ var Act = class {
1333
1706
  return events;
1334
1707
  }
1335
1708
  /**
1336
- * Handles leased reactions.
1337
- *
1338
- * This is called by the main `drain` loop after fetching new events.
1339
- * It handles reactions, supporting retries, blocking, and error handling.
1709
+ * Shared finalization for the two reaction-runner shapes (per-event
1710
+ * `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
1711
+ * block decision, and the "error reported only when nothing was handled"
1712
+ * rule that's true in both shapes (in batch mode, `handled` is always 0
1713
+ * on failure, so the rule degenerates to "always reported").
1714
+ */
1715
+ _finalize(lease, handled, at, error, options) {
1716
+ if (!error) return { lease, handled, at };
1717
+ this._logger.error(error);
1718
+ const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1719
+ if (block2)
1720
+ this._logger.error(
1721
+ `Blocking ${lease.stream} after ${lease.retry} retries.`
1722
+ );
1723
+ return {
1724
+ lease,
1725
+ handled,
1726
+ at,
1727
+ error: handled === 0 ? error.message : void 0,
1728
+ block: block2
1729
+ };
1730
+ }
1731
+ /**
1732
+ * Handles leased reactions one event at a time.
1340
1733
  *
1341
- * Each handler receives a scoped `IAct` proxy that auto-injects the
1342
- * triggering event as `reactingTo` when `do()` is called without it,
1343
- * maintaining correlation chains by default (#587). Handlers can still
1344
- * pass an explicit `reactingTo` to override this behavior.
1734
+ * Called by the main `drain` loop after fetching new events. Each handler
1735
+ * receives a scoped `IAct` proxy that auto-injects the triggering event
1736
+ * as `reactingTo` when `do()` is called without it, maintaining
1737
+ * correlation chains by default (#587). Handlers can still pass an
1738
+ * explicit `reactingTo` to override.
1345
1739
  *
1346
1740
  * @internal
1347
- * @param lease The lease to handle
1348
- * @param payloads The reactions to handle
1349
- * @returns The lease with results
1350
1741
  */
1351
1742
  async handle(lease, payloads) {
1352
1743
  if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
1353
1744
  const stream = lease.stream;
1354
- let at = payloads.at(0).event.id, handled = 0;
1355
- lease.retry > 0 && this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1356
- const doAction = this.do.bind(this);
1745
+ let at = payloads.at(0).event.id;
1746
+ let handled = 0;
1747
+ if (lease.retry > 0)
1748
+ this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1749
+ const doAction = this._bound_do;
1357
1750
  const scopedApp = {
1358
1751
  do: doAction,
1359
- load: this.load.bind(this),
1360
- query: this.query.bind(this),
1361
- query_array: this.query_array.bind(this)
1752
+ load: this._bound_load,
1753
+ query: this._bound_query,
1754
+ query_array: this._bound_query_array
1362
1755
  };
1363
1756
  for (const payload of payloads) {
1364
- const { event, handler, options } = payload;
1757
+ const { event, handler } = payload;
1365
1758
  scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
1366
1759
  action2,
1367
1760
  target,
@@ -1374,22 +1767,16 @@ var Act = class {
1374
1767
  at = event.id;
1375
1768
  handled++;
1376
1769
  } catch (error) {
1377
- this._logger.error(error);
1378
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1379
- block2 && this._logger.error(
1380
- `Blocking ${stream} after ${lease.retry} retries.`
1381
- );
1382
- return {
1770
+ return this._finalize(
1383
1771
  lease,
1384
1772
  handled,
1385
1773
  at,
1386
- // only report error when nothing was handled
1387
- error: handled === 0 ? error.message : void 0,
1388
- block: block2
1389
- };
1774
+ error,
1775
+ payload.options
1776
+ );
1390
1777
  }
1391
1778
  }
1392
- return { lease, handled, at };
1779
+ return this._finalize(lease, handled, at, void 0, payloads[0].options);
1393
1780
  }
1394
1781
  /**
1395
1782
  * Handles a batch of events for a projection with a batch handler.
@@ -1399,33 +1786,26 @@ var Act = class {
1399
1786
  * in a single call, enabling bulk DB operations.
1400
1787
  *
1401
1788
  * @internal
1402
- * @param lease The lease to handle
1403
- * @param payloads The reactions to handle
1404
- * @param batchHandler The batch handler for this projection
1405
- * @returns The lease with results
1406
1789
  */
1407
1790
  async handleBatch(lease, payloads, batchHandler) {
1408
1791
  const stream = lease.stream;
1409
1792
  const events = payloads.map((p) => p.event);
1410
- const at = events.at(-1).id;
1411
- lease.retry > 0 && this._logger.warn(
1412
- `Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
1413
- );
1793
+ const options = payloads[0].options;
1794
+ if (lease.retry > 0)
1795
+ this._logger.warn(
1796
+ `Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
1797
+ );
1414
1798
  try {
1415
1799
  await batchHandler(events, stream);
1416
- return { lease, handled: events.length, at };
1417
- } catch (error) {
1418
- this._logger.error(error);
1419
- const { options } = payloads[0];
1420
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1421
- block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
1422
- return {
1800
+ return this._finalize(
1423
1801
  lease,
1424
- handled: 0,
1425
- at: lease.at,
1426
- error: error.message,
1427
- block: block2
1428
- };
1802
+ events.length,
1803
+ events.at(-1).id,
1804
+ void 0,
1805
+ options
1806
+ );
1807
+ } catch (error) {
1808
+ return this._finalize(lease, 0, lease.at, error, options);
1429
1809
  }
1430
1810
  }
1431
1811
  /**
@@ -1475,81 +1855,46 @@ var Act = class {
1475
1855
  if (!this._needs_drain) {
1476
1856
  return { fetched: [], leased: [], acked: [], blocked: [] };
1477
1857
  }
1478
- if (!this._drain_locked) {
1479
- try {
1480
- this._drain_locked = true;
1481
- const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1482
- const leading = streamLimit - lagging;
1483
- const leased = await this._cd.claim(
1484
- lagging,
1485
- leading,
1486
- randomUUID2(),
1487
- leaseMillis
1488
- );
1489
- if (!leased.length) {
1490
- this._needs_drain = false;
1491
- return { fetched: [], leased: [], acked: [], blocked: [] };
1492
- }
1493
- const fetched = await this._cd.fetch(leased, eventLimit);
1494
- const payloadsMap = /* @__PURE__ */ new Map();
1495
- const fetch_window_at = fetched.reduce(
1496
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1497
- 0
1498
- );
1499
- fetched.forEach(({ stream, events }) => {
1500
- const payloads = events.flatMap((event) => {
1501
- const register = this.registry.events[event.name];
1502
- if (!register) return [];
1503
- return [...register.reactions.values()].filter((reaction) => {
1504
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1505
- return resolved && resolved.target === stream;
1506
- }).map((reaction) => ({ ...reaction, event }));
1507
- });
1508
- payloadsMap.set(stream, payloads);
1509
- });
1510
- const handled = await Promise.all(
1511
- leased.map((lease) => {
1512
- const streamFetch = fetched.find((f) => f.stream === lease.stream);
1513
- const at = streamFetch?.events.at(-1)?.id || fetch_window_at;
1514
- const payloads = payloadsMap.get(lease.stream);
1515
- const batchHandler = this._batch_handlers.get(lease.stream);
1516
- if (batchHandler && payloads.length > 0) {
1517
- return this.handleBatch({ ...lease, at }, payloads, batchHandler);
1518
- }
1519
- return this.handle({ ...lease, at }, payloads);
1520
- })
1521
- );
1522
- const [lagging_handled, leading_handled] = handled.reduce(
1523
- ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1524
- lagging_handled2 + (lease.lagging ? handled2 : 0),
1525
- leading_handled2 + (lease.lagging ? 0 : handled2)
1526
- ],
1527
- [0, 0]
1528
- );
1529
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1530
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1531
- const total = lagging_avg + leading_avg;
1532
- this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1533
- const acked = await this._cd.ack(
1534
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1535
- );
1536
- if (acked.length) this.emit("acked", acked);
1537
- const blocked = await this._cd.block(
1538
- handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
1539
- );
1540
- if (blocked.length) this.emit("blocked", blocked);
1541
- const result = { fetched, leased, acked, blocked };
1542
- const hasErrors = handled.some(({ error }) => error);
1543
- if (!acked.length && !blocked.length && !hasErrors)
1544
- this._needs_drain = false;
1545
- return result;
1546
- } catch (error) {
1547
- this._logger.error(error);
1548
- } finally {
1549
- this._drain_locked = false;
1858
+ if (this._drain_locked) {
1859
+ return { fetched: [], leased: [], acked: [], blocked: [] };
1860
+ }
1861
+ try {
1862
+ this._drain_locked = true;
1863
+ const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1864
+ const leading = streamLimit - lagging;
1865
+ const cycle = await runDrainCycle(
1866
+ this._cd,
1867
+ this.registry,
1868
+ this._batch_handlers,
1869
+ this._bound_handle,
1870
+ this._bound_handle_batch,
1871
+ lagging,
1872
+ leading,
1873
+ eventLimit,
1874
+ leaseMillis
1875
+ );
1876
+ if (!cycle) {
1877
+ this._needs_drain = false;
1878
+ return { fetched: [], leased: [], acked: [], blocked: [] };
1550
1879
  }
1880
+ const { leased, fetched, handled, acked, blocked } = cycle;
1881
+ this._drain_lag2lead_ratio = computeLagLeadRatio(
1882
+ handled,
1883
+ lagging,
1884
+ leading
1885
+ );
1886
+ if (acked.length) this.emit("acked", acked);
1887
+ if (blocked.length) this.emit("blocked", blocked);
1888
+ const hasErrors = handled.some(({ error }) => error);
1889
+ if (!acked.length && !blocked.length && !hasErrors)
1890
+ this._needs_drain = false;
1891
+ return { fetched, leased, acked, blocked };
1892
+ } catch (error) {
1893
+ this._logger.error(error);
1894
+ return { fetched: [], leased: [], acked: [], blocked: [] };
1895
+ } finally {
1896
+ this._drain_locked = false;
1551
1897
  }
1552
- return { fetched: [], leased: [], acked: [], blocked: [] };
1553
1898
  }
1554
1899
  /**
1555
1900
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -1610,7 +1955,7 @@ var Act = class {
1610
1955
  this._correlation_checkpoint = watermark;
1611
1956
  if (this._reactive_events.size > 0) this._needs_drain = true;
1612
1957
  for (const { stream } of this._static_targets) {
1613
- this._subscribed_statics.add(stream);
1958
+ this._subscribed_streams.add(stream);
1614
1959
  }
1615
1960
  }
1616
1961
  async correlate(query = { after: -1, limit: 10 }) {
@@ -1628,7 +1973,7 @@ var Act = class {
1628
1973
  for (const reaction of register.reactions.values()) {
1629
1974
  if (typeof reaction.resolver !== "function") continue;
1630
1975
  const resolved = reaction.resolver(event);
1631
- if (resolved && !this._subscribed_statics.has(resolved.target)) {
1976
+ if (resolved && !this._subscribed_streams.has(resolved.target)) {
1632
1977
  const entry = correlated.get(resolved.target) || {
1633
1978
  source: resolved.source,
1634
1979
  payloads: []
@@ -1654,7 +1999,7 @@ var Act = class {
1654
1999
  this._correlation_checkpoint = last_id;
1655
2000
  if (subscribed) {
1656
2001
  for (const { stream } of streams) {
1657
- this._subscribed_statics.add(stream);
2002
+ this._subscribed_streams.add(stream);
1658
2003
  }
1659
2004
  }
1660
2005
  return { subscribed, last_id };
@@ -1837,143 +2182,14 @@ var Act = class {
1837
2182
  */
1838
2183
  async close(targets) {
1839
2184
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
1840
- const targetMap = new Map(targets.map((t) => [t.stream, t]));
1841
- const streams = [...targetMap.keys()];
1842
2185
  await this.correlate({ limit: 1e3 });
1843
- const streamInfo = /* @__PURE__ */ new Map();
1844
- await Promise.all(
1845
- streams.map(async (s) => {
1846
- let maxId = -1;
1847
- let version = -1;
1848
- let lastEventName;
1849
- await store().query(
1850
- (e) => {
1851
- if (e.name === TOMBSTONE_EVENT) return;
1852
- if (maxId === -1) {
1853
- maxId = e.id;
1854
- version = e.version;
1855
- }
1856
- if (e.name !== SNAP_EVENT && lastEventName === void 0) {
1857
- lastEventName = e.name;
1858
- }
1859
- },
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 }
1864
- );
1865
- if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
1866
- })
1867
- );
1868
- const skipped = [];
1869
- let safe;
1870
- if (this._reactive_events.size === 0) {
1871
- safe = [...streamInfo.keys()];
1872
- } else {
1873
- const pendingSet = /* @__PURE__ */ new Set();
1874
- await store().query_streams((position) => {
1875
- const sourceRe = position.source ? RegExp(position.source) : void 0;
1876
- for (const [stream, info] of streamInfo) {
1877
- if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
1878
- pendingSet.add(stream);
1879
- }
1880
- }
1881
- });
1882
- safe = [];
1883
- for (const [stream] of streamInfo) {
1884
- if (pendingSet.has(stream)) {
1885
- skipped.push(stream);
1886
- } else {
1887
- safe.push(stream);
1888
- }
1889
- }
1890
- }
1891
- if (!safe.length) {
1892
- const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
1893
- this.emit("closed", result2);
1894
- return result2;
1895
- }
1896
- const correlation = randomUUID2();
1897
- const guarded = [];
1898
- const guardEvents = /* @__PURE__ */ new Map();
1899
- await Promise.all(
1900
- safe.map(async (stream) => {
1901
- try {
1902
- const info = streamInfo.get(stream);
1903
- const [committed] = await store().commit(
1904
- stream,
1905
- [{ name: TOMBSTONE_EVENT, data: {} }],
1906
- { correlation, causation: {} },
1907
- info.version
1908
- );
1909
- guarded.push(stream);
1910
- guardEvents.set(stream, { id: committed.id, stream });
1911
- } catch {
1912
- skipped.push(stream);
1913
- }
1914
- })
1915
- );
1916
- if (!guarded.length) {
1917
- const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
1918
- this.emit("closed", result2);
1919
- return result2;
1920
- }
1921
- const seedStates = /* @__PURE__ */ new Map();
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
- );
1936
- for (const stream of guarded) {
1937
- const archiveFn = targetMap.get(stream)?.archive;
1938
- if (archiveFn) await archiveFn();
1939
- }
1940
- const truncTargets = guarded.map((stream) => {
1941
- const snapshot = seedStates.get(stream);
1942
- const guard = guardEvents.get(stream);
1943
- return {
1944
- stream,
1945
- snapshot,
1946
- meta: {
1947
- correlation,
1948
- causation: {
1949
- event: {
1950
- id: guard.id,
1951
- name: TOMBSTONE_EVENT,
1952
- stream: guard.stream
1953
- }
1954
- }
1955
- }
1956
- };
2186
+ const result = await runCloseCycle(targets, {
2187
+ reactiveEventsSize: this._reactive_events.size,
2188
+ eventToState: this._event_to_state,
2189
+ load: this._es.load,
2190
+ tombstone: this._es.tombstone,
2191
+ logger: this._logger
1957
2192
  });
1958
- const truncated = await store().truncate(truncTargets);
1959
- await Promise.all(
1960
- guarded.map(async (stream) => {
1961
- const entry = truncated.get(stream);
1962
- const state2 = seedStates.get(stream);
1963
- if (state2 && entry) {
1964
- await cache().set(stream, {
1965
- state: state2,
1966
- version: entry.committed.version,
1967
- event_id: entry.committed.id,
1968
- patches: 0,
1969
- snaps: 1
1970
- });
1971
- } else {
1972
- await cache().invalidate(stream);
1973
- }
1974
- })
1975
- );
1976
- const result = { truncated, skipped };
1977
2193
  this.emit("closed", result);
1978
2194
  return result;
1979
2195
  }
@@ -2042,7 +2258,7 @@ var Act = class {
2042
2258
  }
2043
2259
  };
2044
2260
 
2045
- // src/act-builder.ts
2261
+ // src/builders/act-builder.ts
2046
2262
  function registerBatchHandler(proj, batchHandlers) {
2047
2263
  if (!proj.batchHandler || !proj.target) return;
2048
2264
  const existing = batchHandlers.get(proj.target);
@@ -2051,56 +2267,33 @@ function registerBatchHandler(proj, batchHandlers) {
2051
2267
  }
2052
2268
  batchHandlers.set(proj.target, proj.batchHandler);
2053
2269
  }
2054
- function act(states = /* @__PURE__ */ new Map(), registry = {
2055
- actions: {},
2056
- events: {}
2057
- }, pendingProjections = [], batchHandlers = /* @__PURE__ */ new Map()) {
2270
+ function act() {
2271
+ const states = /* @__PURE__ */ new Map();
2272
+ const registry = {
2273
+ actions: {},
2274
+ events: {}
2275
+ };
2276
+ const pendingProjections = [];
2277
+ const batchHandlers = /* @__PURE__ */ new Map();
2058
2278
  const builder = {
2059
2279
  withState: (state2) => {
2060
2280
  registerState(state2, states, registry.actions, registry.events);
2061
- return act(
2062
- states,
2063
- registry,
2064
- pendingProjections,
2065
- batchHandlers
2066
- );
2281
+ return builder;
2067
2282
  },
2068
2283
  withSlice: (input) => {
2069
2284
  for (const s of input.states.values()) {
2070
2285
  registerState(s, states, registry.actions, registry.events);
2071
2286
  }
2072
- for (const eventName of Object.keys(input.events)) {
2073
- const sliceRegister = input.events[eventName];
2074
- for (const [name, reaction] of sliceRegister.reactions) {
2075
- registry.events[eventName].reactions.set(name, reaction);
2076
- }
2077
- }
2287
+ mergeEventRegister(registry.events, input.events);
2078
2288
  pendingProjections.push(...input.projections);
2079
- return act(
2080
- states,
2081
- registry,
2082
- pendingProjections,
2083
- batchHandlers
2084
- );
2289
+ return builder;
2085
2290
  },
2086
2291
  withProjection: (proj) => {
2087
2292
  mergeProjection(proj, registry.events);
2088
2293
  registerBatchHandler(proj, batchHandlers);
2089
- return act(
2090
- states,
2091
- registry,
2092
- pendingProjections,
2093
- batchHandlers
2094
- );
2095
- },
2096
- withActor: () => {
2097
- return act(
2098
- states,
2099
- registry,
2100
- pendingProjections,
2101
- batchHandlers
2102
- );
2294
+ return builder;
2103
2295
  },
2296
+ withActor: () => builder,
2104
2297
  on: (event) => ({
2105
2298
  do: (handler, options) => {
2106
2299
  const reaction = {
@@ -2116,19 +2309,15 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2116
2309
  `Reaction handler for "${String(event)}" must be a named function`
2117
2310
  );
2118
2311
  registry.events[event].reactions.set(handler.name, reaction);
2119
- return {
2120
- ...builder,
2312
+ return Object.assign(builder, {
2121
2313
  to(resolver) {
2122
- registry.events[event].reactions.set(handler.name, {
2123
- ...reaction,
2124
- resolver: typeof resolver === "string" ? { target: resolver } : resolver
2125
- });
2314
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2126
2315
  return builder;
2127
2316
  }
2128
- };
2317
+ });
2129
2318
  }
2130
2319
  }),
2131
- build: () => {
2320
+ build: (options) => {
2132
2321
  for (const proj of pendingProjections) {
2133
2322
  mergeProjection(proj, registry.events);
2134
2323
  registerBatchHandler(proj, batchHandlers);
@@ -2136,7 +2325,8 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2136
2325
  return new Act(
2137
2326
  registry,
2138
2327
  states,
2139
- batchHandlers
2328
+ batchHandlers,
2329
+ options
2140
2330
  );
2141
2331
  },
2142
2332
  events: registry.events
@@ -2144,8 +2334,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2144
2334
  return builder;
2145
2335
  }
2146
2336
 
2147
- // src/projection-builder.ts
2148
- function _projection(target, events) {
2337
+ // src/builders/projection-builder.ts
2338
+ function _projection(target) {
2339
+ const events = {};
2149
2340
  const defaultResolver = typeof target === "string" ? { target } : void 0;
2150
2341
  const base = {
2151
2342
  on: (entry) => {
@@ -2175,17 +2366,13 @@ function _projection(target, events) {
2175
2366
  `Projection handler for "${event}" must be a named function`
2176
2367
  );
2177
2368
  register.reactions.set(handler.name, reaction);
2178
- const nextBuilder = _projection(target, events);
2179
- return {
2180
- ...nextBuilder,
2369
+ const widened = base;
2370
+ return Object.assign(widened, {
2181
2371
  to(resolver) {
2182
- register.reactions.set(handler.name, {
2183
- ...reaction,
2184
- resolver: typeof resolver === "string" ? { target: resolver } : resolver
2185
- });
2186
- return nextBuilder;
2372
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2373
+ return widened;
2187
2374
  }
2188
- };
2375
+ });
2189
2376
  }
2190
2377
  };
2191
2378
  },
@@ -2197,8 +2384,7 @@ function _projection(target, events) {
2197
2384
  events
2198
2385
  };
2199
2386
  if (typeof target === "string") {
2200
- return {
2201
- ...base,
2387
+ return Object.assign(base, {
2202
2388
  batch: (handler) => ({
2203
2389
  build: () => ({
2204
2390
  _tag: "Projection",
@@ -2207,34 +2393,28 @@ function _projection(target, events) {
2207
2393
  batchHandler: handler
2208
2394
  })
2209
2395
  })
2210
- };
2396
+ });
2211
2397
  }
2212
2398
  return base;
2213
2399
  }
2214
- function projection(target, events = {}) {
2215
- return _projection(target, events);
2400
+ function projection(target) {
2401
+ return _projection(target);
2216
2402
  }
2217
2403
 
2218
- // src/slice-builder.ts
2219
- function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, projections = []) {
2404
+ // src/builders/slice-builder.ts
2405
+ function slice() {
2406
+ const states = /* @__PURE__ */ new Map();
2407
+ const actions = {};
2408
+ const events = {};
2409
+ const projections = [];
2220
2410
  const builder = {
2221
2411
  withState: (state2) => {
2222
2412
  registerState(state2, states, actions, events);
2223
- return slice(
2224
- states,
2225
- actions,
2226
- events,
2227
- projections
2228
- );
2413
+ return builder;
2229
2414
  },
2230
2415
  withProjection: (proj) => {
2231
2416
  projections.push(proj);
2232
- return slice(
2233
- states,
2234
- actions,
2235
- events,
2236
- projections
2237
- );
2417
+ return builder;
2238
2418
  },
2239
2419
  on: (event) => ({
2240
2420
  do: (handler, options) => {
@@ -2251,16 +2431,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
2251
2431
  `Reaction handler for "${String(event)}" must be a named function`
2252
2432
  );
2253
2433
  events[event].reactions.set(handler.name, reaction);
2254
- return {
2255
- ...builder,
2434
+ return Object.assign(builder, {
2256
2435
  to(resolver) {
2257
- events[event].reactions.set(handler.name, {
2258
- ...reaction,
2259
- resolver: typeof resolver === "string" ? { target: resolver } : resolver
2260
- });
2436
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2261
2437
  return builder;
2262
2438
  }
2263
- };
2439
+ });
2264
2440
  }
2265
2441
  }),
2266
2442
  build: () => ({
@@ -2274,7 +2450,7 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
2274
2450
  return builder;
2275
2451
  }
2276
2452
 
2277
- // src/state-builder.ts
2453
+ // src/builders/state-builder.ts
2278
2454
  function state(entry) {
2279
2455
  const keys = Object.keys(entry);
2280
2456
  if (keys.length !== 1) throw new Error("state() requires exactly one key");
@@ -2292,7 +2468,7 @@ function state(entry) {
2292
2468
  return [k, fn];
2293
2469
  })
2294
2470
  );
2295
- const builder = action_builder({
2471
+ const internal = {
2296
2472
  events,
2297
2473
  actions: {},
2298
2474
  state: stateSchema,
@@ -2300,18 +2476,12 @@ function state(entry) {
2300
2476
  init,
2301
2477
  patch: defaultPatch,
2302
2478
  on: {}
2303
- });
2479
+ };
2480
+ const builder = action_builder(internal);
2304
2481
  return Object.assign(builder, {
2305
2482
  patch(customPatch) {
2306
- return action_builder({
2307
- events,
2308
- actions: {},
2309
- state: stateSchema,
2310
- name,
2311
- init,
2312
- patch: { ...defaultPatch, ...customPatch },
2313
- on: {}
2314
- });
2483
+ Object.assign(internal.patch, customPatch);
2484
+ return builder;
2315
2485
  }
2316
2486
  });
2317
2487
  }
@@ -2320,50 +2490,43 @@ function state(entry) {
2320
2490
  };
2321
2491
  }
2322
2492
  function action_builder(state2) {
2323
- return {
2493
+ const internal = state2;
2494
+ const builder = {
2324
2495
  on(entry) {
2325
2496
  const keys = Object.keys(entry);
2326
2497
  if (keys.length !== 1) throw new Error(".on() requires exactly one key");
2327
2498
  const action2 = keys[0];
2328
2499
  const schema = entry[action2];
2329
- if (action2 in state2.actions)
2500
+ if (action2 in internal.actions)
2330
2501
  throw new Error(`Duplicate action "${action2}"`);
2331
- const actions = {
2332
- ...state2.actions,
2333
- [action2]: schema
2334
- };
2335
- const on = { ...state2.on };
2336
- const _given = { ...state2.given };
2502
+ internal.actions[action2] = schema;
2337
2503
  function given(rules) {
2338
- _given[action2] = rules;
2504
+ (internal.given ??= {})[action2] = rules;
2339
2505
  return { emit };
2340
2506
  }
2341
2507
  function emit(handler) {
2342
2508
  if (typeof handler === "string") {
2343
2509
  const eventName = handler;
2344
- on[action2] = ((payload) => [eventName, payload]);
2510
+ internal.on[action2] = (payload) => [
2511
+ eventName,
2512
+ payload
2513
+ ];
2345
2514
  } else {
2346
- on[action2] = handler;
2515
+ internal.on[action2] = handler;
2347
2516
  }
2348
- return action_builder({
2349
- ...state2,
2350
- actions,
2351
- on,
2352
- given: _given
2353
- });
2517
+ return builder;
2354
2518
  }
2355
2519
  return { given, emit };
2356
2520
  },
2357
2521
  snap(snap2) {
2358
- return action_builder({
2359
- ...state2,
2360
- snap: snap2
2361
- });
2522
+ internal.snap = snap2;
2523
+ return builder;
2362
2524
  },
2363
2525
  build() {
2364
- return state2;
2526
+ return internal;
2365
2527
  }
2366
2528
  };
2529
+ return builder;
2367
2530
  }
2368
2531
  export {
2369
2532
  Act,
@@ -2372,6 +2535,7 @@ export {
2372
2535
  CommittedMetaSchema,
2373
2536
  ConcurrencyError,
2374
2537
  ConsoleLogger,
2538
+ DEFAULT_MAX_SUBSCRIBED_STREAMS,
2375
2539
  Environments,
2376
2540
  Errors,
2377
2541
  EventMetaSchema,