@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.
Files changed (43) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +65 -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/index.d.ts +1 -4
  17. package/dist/@types/index.d.ts.map +1 -1
  18. package/dist/@types/internal/close-cycle.d.ts +38 -0
  19. package/dist/@types/internal/close-cycle.d.ts.map +1 -0
  20. package/dist/@types/internal/drain-cycle.d.ts +61 -0
  21. package/dist/@types/internal/drain-cycle.d.ts.map +1 -0
  22. package/dist/@types/internal/drain-ratio.d.ts +26 -0
  23. package/dist/@types/internal/drain-ratio.d.ts.map +1 -0
  24. package/dist/@types/internal/event-sourcing.d.ts +14 -0
  25. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  26. package/dist/@types/internal/index.d.ts +5 -1
  27. package/dist/@types/internal/index.d.ts.map +1 -1
  28. package/dist/@types/internal/lru-map.d.ts +50 -0
  29. package/dist/@types/internal/lru-map.d.ts.map +1 -0
  30. package/dist/@types/internal/merge.d.ts +13 -1
  31. package/dist/@types/internal/merge.d.ts.map +1 -1
  32. package/dist/@types/internal/tracing.d.ts.map +1 -1
  33. package/dist/@types/types/reaction.d.ts +7 -1
  34. package/dist/@types/types/reaction.d.ts.map +1 -1
  35. package/dist/index.cjs +521 -394
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +520 -394
  38. package/dist/index.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/@types/act-builder.d.ts.map +0 -1
  41. package/dist/@types/projection-builder.d.ts.map +0 -1
  42. package/dist/@types/slice-builder.d.ts.map +0 -1
  43. 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
- _entries = /* @__PURE__ */ new Map();
177
- _maxSize;
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._maxSize = options?.maxSize ?? 1e3;
238
+ this._entries = new LruMap(options?.maxSize ?? 1e3);
180
239
  }
181
240
  async get(stream) {
182
- const entry = this._entries.get(stream);
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 import_crypto = require("crypto");
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, import_crypto.randomUUID)(),
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 { snap, load, action };
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
- _subscribed_statics = /* @__PURE__ */ new Set();
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
- * Handles leased reactions.
1540
- *
1541
- * This is called by the main `drain` loop after fetching new events.
1542
- * It handles reactions, supporting retries, blocking, and error handling.
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
- * Each handler receives a scoped `IAct` proxy that auto-injects the
1545
- * triggering event as `reactingTo` when `do()` is called without it,
1546
- * maintaining correlation chains by default (#587). Handlers can still
1547
- * pass an explicit `reactingTo` to override this behavior.
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, handled = 0;
1558
- lease.retry > 0 && this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
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, options } = payload;
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._logger.error(error);
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
- // only report error when nothing was handled
1590
- error: handled === 0 ? error.message : void 0,
1591
- block: block2
1592
- };
1941
+ error,
1942
+ payload.options
1943
+ );
1593
1944
  }
1594
1945
  }
1595
- return { lease, handled, at };
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 at = events.at(-1).id;
1614
- lease.retry > 0 && this._logger.warn(
1615
- `Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
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 { lease, handled: events.length, at };
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
- handled: 0,
1628
- at: lease.at,
1629
- error: error.message,
1630
- block: block2
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 (!this._drain_locked) {
1682
- try {
1683
- this._drain_locked = true;
1684
- const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1685
- const leading = streamLimit - lagging;
1686
- const leased = await this._cd.claim(
1687
- lagging,
1688
- leading,
1689
- (0, import_crypto2.randomUUID)(),
1690
- leaseMillis
1691
- );
1692
- if (!leased.length) {
1693
- this._needs_drain = false;
1694
- return { fetched: [], leased: [], acked: [], blocked: [] };
1695
- }
1696
- const fetched = await this._cd.fetch(leased, eventLimit);
1697
- const fetchMap = /* @__PURE__ */ new Map();
1698
- const fetch_window_at = fetched.reduce(
1699
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1700
- 0
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._subscribed_statics.add(stream);
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._subscribed_statics.has(resolved.target)) {
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._subscribed_statics.add(stream);
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 streamInfo = /* @__PURE__ */ new Map();
2048
- await Promise.all(
2049
- streams.map(async (s) => {
2050
- let maxId = -1;
2051
- let version = -1;
2052
- let lastEventName;
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(states = /* @__PURE__ */ new Map(), registry = {
2259
- actions: {},
2260
- events: {}
2261
- }, pendingProjections = [], batchHandlers = /* @__PURE__ */ new Map()) {
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 act(
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
- for (const eventName of Object.keys(input.events)) {
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 act(
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 act(
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
- registry.events[event].reactions.set(handler.name, {
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, events) {
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 nextBuilder = _projection(target, events);
2383
- return {
2384
- ...nextBuilder,
2536
+ const widened = base;
2537
+ return Object.assign(widened, {
2385
2538
  to(resolver) {
2386
- register.reactions.set(handler.name, {
2387
- ...reaction,
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, events = {}) {
2419
- return _projection(target, events);
2567
+ function projection(target) {
2568
+ return _projection(target);
2420
2569
  }
2421
2570
 
2422
- // src/slice-builder.ts
2423
- function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, projections = []) {
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 slice(
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 slice(
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
- events[event].reactions.set(handler.name, {
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 builder = action_builder({
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
- return action_builder({
2511
- events,
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
- return {
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 state2.actions)
2667
+ if (action2 in internal.actions)
2534
2668
  throw new Error(`Duplicate action "${action2}"`);
2535
- const actions = {
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
- _given[action2] = rules;
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] = ((payload) => [eventName, payload]);
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 action_builder({
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
- return action_builder({
2563
- ...state2,
2564
- snap: snap2
2565
- });
2689
+ internal.snap = snap2;
2690
+ return builder;
2566
2691
  },
2567
2692
  build() {
2568
- return state2;
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,