@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.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) {
@@ -671,9 +720,233 @@ process.once("unhandledRejection", async (arg) => {
671
720
  });
672
721
 
673
722
  // src/act.ts
674
- import { randomUUID as randomUUID2 } from "crypto";
675
723
  import EventEmitter from "events";
676
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
+
677
950
  // src/internal/merge.ts
678
951
  import { ZodObject } from "zod";
679
952
  function baseTypeName(zodType) {
@@ -781,6 +1054,15 @@ function mergePatches(existing, incoming, stateName) {
781
1054
  }
782
1055
  return merged;
783
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
+ }
784
1066
  function mergeProjection(proj, events) {
785
1067
  for (const eventName of Object.keys(proj.events)) {
786
1068
  const projRegister = proj.events[eventName];
@@ -825,7 +1107,7 @@ var subscribe = (streams) => store().subscribe(streams);
825
1107
 
826
1108
  // src/internal/event-sourcing.ts
827
1109
  import { patch } from "@rotorsoft/act-patch";
828
- import { randomUUID } from "crypto";
1110
+ import { randomUUID as randomUUID3 } from "crypto";
829
1111
  async function snap(snapshot) {
830
1112
  try {
831
1113
  const { id, stream, name, meta, version } = snapshot.event;
@@ -843,6 +1125,20 @@ async function snap(snapshot) {
843
1125
  log().error(error);
844
1126
  }
845
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
+ }
846
1142
  async function load(me, stream, callback, asOf) {
847
1143
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
848
1144
  const cached = timeTravel ? void 0 : await cache().get(stream);
@@ -907,7 +1203,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
907
1203
  data: skipValidation ? data : validate(name, data, me.events[name])
908
1204
  }));
909
1205
  const meta = {
910
- correlation: reactingTo?.meta.correlation || randomUUID(),
1206
+ correlation: reactingTo?.meta.correlation || randomUUID3(),
911
1207
  causation: {
912
1208
  action: {
913
1209
  name: action2,
@@ -978,7 +1274,12 @@ var traced = (inner, exit, entry) => (async (...args) => {
978
1274
  });
979
1275
  function buildEs(logger) {
980
1276
  if (logger.level !== "trace") {
981
- return { snap, load, action };
1277
+ return {
1278
+ snap,
1279
+ load,
1280
+ action,
1281
+ tombstone
1282
+ };
982
1283
  }
983
1284
  return {
984
1285
  snap: traced(snap, void 0, (snapshot) => {
@@ -1016,7 +1317,13 @@ function buildEs(logger) {
1016
1317
  es_caption("action", C_BLUE, `${target.stream}.${action2}`)
1017
1318
  );
1018
1319
  }
1019
- )
1320
+ ),
1321
+ tombstone: traced(tombstone, (committed, stream) => {
1322
+ if (committed)
1323
+ logger.trace(
1324
+ es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
1325
+ );
1326
+ })
1020
1327
  };
1021
1328
  }
1022
1329
  function buildDrain(logger) {
@@ -1079,11 +1386,15 @@ function buildDrain(logger) {
1079
1386
  }
1080
1387
 
1081
1388
  // src/act.ts
1389
+ var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
1082
1390
  var Act = class {
1083
- constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
1391
+ constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
1084
1392
  this.registry = registry;
1085
1393
  this._states = _states;
1086
1394
  this._batch_handlers = batchHandlers;
1395
+ this._subscribed_streams = new LruSet(
1396
+ options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
1397
+ );
1087
1398
  this._es = buildEs(this._logger);
1088
1399
  this._cd = buildDrain(this._logger);
1089
1400
  const statics = /* @__PURE__ */ new Map();
@@ -1121,20 +1432,42 @@ var Act = class {
1121
1432
  _settle_timer = void 0;
1122
1433
  _settling = false;
1123
1434
  _correlation_checkpoint = -1;
1124
- _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;
1125
1447
  _has_dynamic_resolvers = false;
1126
1448
  _correlation_initialized = false;
1127
1449
  /** Event names with at least one registered reaction (computed at build time) */
1128
1450
  _reactive_events = /* @__PURE__ */ new Set();
1129
1451
  /** Set in do() when a committed event has reactions — cleared by drain() */
1130
1452
  _needs_drain = false;
1453
+ /**
1454
+ * Emit a lifecycle event. The payload type is inferred from the event name
1455
+ * via {@link ActLifecycleEvents}.
1456
+ */
1131
1457
  emit(event, args) {
1132
1458
  return this._emitter.emit(event, args);
1133
1459
  }
1460
+ /**
1461
+ * Register a listener for a lifecycle event. The listener receives the
1462
+ * event-specific payload.
1463
+ */
1134
1464
  on(event, listener) {
1135
1465
  this._emitter.on(event, listener);
1136
1466
  return this;
1137
1467
  }
1468
+ /**
1469
+ * Remove a previously registered lifecycle listener.
1470
+ */
1138
1471
  off(event, listener) {
1139
1472
  this._emitter.off(event, listener);
1140
1473
  return this;
@@ -1168,6 +1501,9 @@ var Act = class {
1168
1501
  _bound_load = this.load.bind(this);
1169
1502
  _bound_query = this.query.bind(this);
1170
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);
1171
1507
  /**
1172
1508
  * Executes an action on a state instance, committing resulting events.
1173
1509
  *
@@ -1370,26 +1706,46 @@ var Act = class {
1370
1706
  return events;
1371
1707
  }
1372
1708
  /**
1373
- * Handles leased reactions.
1374
- *
1375
- * This is called by the main `drain` loop after fetching new events.
1376
- * 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.
1377
1733
  *
1378
- * Each handler receives a scoped `IAct` proxy that auto-injects the
1379
- * triggering event as `reactingTo` when `do()` is called without it,
1380
- * maintaining correlation chains by default (#587). Handlers can still
1381
- * 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.
1382
1739
  *
1383
1740
  * @internal
1384
- * @param lease The lease to handle
1385
- * @param payloads The reactions to handle
1386
- * @returns The lease with results
1387
1741
  */
1388
1742
  async handle(lease, payloads) {
1389
1743
  if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
1390
1744
  const stream = lease.stream;
1391
- let at = payloads.at(0).event.id, handled = 0;
1392
- lease.retry > 0 && this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
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}).`);
1393
1749
  const doAction = this._bound_do;
1394
1750
  const scopedApp = {
1395
1751
  do: doAction,
@@ -1398,7 +1754,7 @@ var Act = class {
1398
1754
  query_array: this._bound_query_array
1399
1755
  };
1400
1756
  for (const payload of payloads) {
1401
- const { event, handler, options } = payload;
1757
+ const { event, handler } = payload;
1402
1758
  scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
1403
1759
  action2,
1404
1760
  target,
@@ -1411,22 +1767,16 @@ var Act = class {
1411
1767
  at = event.id;
1412
1768
  handled++;
1413
1769
  } catch (error) {
1414
- this._logger.error(error);
1415
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1416
- block2 && this._logger.error(
1417
- `Blocking ${stream} after ${lease.retry} retries.`
1418
- );
1419
- return {
1770
+ return this._finalize(
1420
1771
  lease,
1421
1772
  handled,
1422
1773
  at,
1423
- // only report error when nothing was handled
1424
- error: handled === 0 ? error.message : void 0,
1425
- block: block2
1426
- };
1774
+ error,
1775
+ payload.options
1776
+ );
1427
1777
  }
1428
1778
  }
1429
- return { lease, handled, at };
1779
+ return this._finalize(lease, handled, at, void 0, payloads[0].options);
1430
1780
  }
1431
1781
  /**
1432
1782
  * Handles a batch of events for a projection with a batch handler.
@@ -1436,33 +1786,26 @@ var Act = class {
1436
1786
  * in a single call, enabling bulk DB operations.
1437
1787
  *
1438
1788
  * @internal
1439
- * @param lease The lease to handle
1440
- * @param payloads The reactions to handle
1441
- * @param batchHandler The batch handler for this projection
1442
- * @returns The lease with results
1443
1789
  */
1444
1790
  async handleBatch(lease, payloads, batchHandler) {
1445
1791
  const stream = lease.stream;
1446
1792
  const events = payloads.map((p) => p.event);
1447
- const at = events.at(-1).id;
1448
- lease.retry > 0 && this._logger.warn(
1449
- `Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
1450
- );
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
+ );
1451
1798
  try {
1452
1799
  await batchHandler(events, stream);
1453
- return { lease, handled: events.length, at };
1454
- } catch (error) {
1455
- this._logger.error(error);
1456
- const { options } = payloads[0];
1457
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1458
- block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
1459
- return {
1800
+ return this._finalize(
1460
1801
  lease,
1461
- handled: 0,
1462
- at: lease.at,
1463
- error: error.message,
1464
- block: block2
1465
- };
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);
1466
1809
  }
1467
1810
  }
1468
1811
  /**
@@ -1512,82 +1855,46 @@ var Act = class {
1512
1855
  if (!this._needs_drain) {
1513
1856
  return { fetched: [], leased: [], acked: [], blocked: [] };
1514
1857
  }
1515
- if (!this._drain_locked) {
1516
- try {
1517
- this._drain_locked = true;
1518
- const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1519
- const leading = streamLimit - lagging;
1520
- const leased = await this._cd.claim(
1521
- lagging,
1522
- leading,
1523
- randomUUID2(),
1524
- leaseMillis
1525
- );
1526
- if (!leased.length) {
1527
- this._needs_drain = false;
1528
- return { fetched: [], leased: [], acked: [], blocked: [] };
1529
- }
1530
- const fetched = await this._cd.fetch(leased, eventLimit);
1531
- const fetchMap = /* @__PURE__ */ new Map();
1532
- const fetch_window_at = fetched.reduce(
1533
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1534
- 0
1535
- );
1536
- for (const f of fetched) {
1537
- const { stream, events } = f;
1538
- const payloads = events.flatMap((event) => {
1539
- const register = this.registry.events[event.name];
1540
- if (!register) return [];
1541
- return [...register.reactions.values()].filter((reaction) => {
1542
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1543
- return resolved && resolved.target === stream;
1544
- }).map((reaction) => ({ ...reaction, event }));
1545
- });
1546
- fetchMap.set(stream, { fetch: f, payloads });
1547
- }
1548
- const handled = await Promise.all(
1549
- leased.map((lease) => {
1550
- const entry = fetchMap.get(lease.stream);
1551
- const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
1552
- const payloads = entry?.payloads ?? [];
1553
- const batchHandler = this._batch_handlers.get(lease.stream);
1554
- if (batchHandler && payloads.length > 0) {
1555
- return this.handleBatch({ ...lease, at }, payloads, batchHandler);
1556
- }
1557
- return this.handle({ ...lease, at }, payloads);
1558
- })
1559
- );
1560
- const [lagging_handled, leading_handled] = handled.reduce(
1561
- ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1562
- lagging_handled2 + (lease.lagging ? handled2 : 0),
1563
- leading_handled2 + (lease.lagging ? 0 : handled2)
1564
- ],
1565
- [0, 0]
1566
- );
1567
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1568
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1569
- const total = lagging_avg + leading_avg;
1570
- this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1571
- const acked = await this._cd.ack(
1572
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1573
- );
1574
- if (acked.length) this.emit("acked", acked);
1575
- const blocked = await this._cd.block(
1576
- handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
1577
- );
1578
- if (blocked.length) this.emit("blocked", blocked);
1579
- const result = { fetched, leased, acked, blocked };
1580
- const hasErrors = handled.some(({ error }) => error);
1581
- if (!acked.length && !blocked.length && !hasErrors)
1582
- this._needs_drain = false;
1583
- return result;
1584
- } catch (error) {
1585
- this._logger.error(error);
1586
- } finally {
1587
- 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: [] };
1588
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;
1589
1897
  }
1590
- return { fetched: [], leased: [], acked: [], blocked: [] };
1591
1898
  }
1592
1899
  /**
1593
1900
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -1648,7 +1955,7 @@ var Act = class {
1648
1955
  this._correlation_checkpoint = watermark;
1649
1956
  if (this._reactive_events.size > 0) this._needs_drain = true;
1650
1957
  for (const { stream } of this._static_targets) {
1651
- this._subscribed_statics.add(stream);
1958
+ this._subscribed_streams.add(stream);
1652
1959
  }
1653
1960
  }
1654
1961
  async correlate(query = { after: -1, limit: 10 }) {
@@ -1666,7 +1973,7 @@ var Act = class {
1666
1973
  for (const reaction of register.reactions.values()) {
1667
1974
  if (typeof reaction.resolver !== "function") continue;
1668
1975
  const resolved = reaction.resolver(event);
1669
- if (resolved && !this._subscribed_statics.has(resolved.target)) {
1976
+ if (resolved && !this._subscribed_streams.has(resolved.target)) {
1670
1977
  const entry = correlated.get(resolved.target) || {
1671
1978
  source: resolved.source,
1672
1979
  payloads: []
@@ -1692,7 +1999,7 @@ var Act = class {
1692
1999
  this._correlation_checkpoint = last_id;
1693
2000
  if (subscribed) {
1694
2001
  for (const { stream } of streams) {
1695
- this._subscribed_statics.add(stream);
2002
+ this._subscribed_streams.add(stream);
1696
2003
  }
1697
2004
  }
1698
2005
  return { subscribed, last_id };
@@ -1875,143 +2182,14 @@ var Act = class {
1875
2182
  */
1876
2183
  async close(targets) {
1877
2184
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
1878
- const targetMap = new Map(targets.map((t) => [t.stream, t]));
1879
- const streams = [...targetMap.keys()];
1880
2185
  await this.correlate({ limit: 1e3 });
1881
- const streamInfo = /* @__PURE__ */ new Map();
1882
- await Promise.all(
1883
- streams.map(async (s) => {
1884
- let maxId = -1;
1885
- let version = -1;
1886
- let lastEventName;
1887
- await store().query(
1888
- (e) => {
1889
- if (e.name === TOMBSTONE_EVENT) return;
1890
- if (maxId === -1) {
1891
- maxId = e.id;
1892
- version = e.version;
1893
- }
1894
- if (e.name !== SNAP_EVENT && lastEventName === void 0) {
1895
- lastEventName = e.name;
1896
- }
1897
- },
1898
- // limit: 2 covers the typical snapshot-at-head case (snapshot is
1899
- // always preceded by the domain event it captured). Streams with
1900
- // unusual layouts fall back to no-seed via the lookup miss path.
1901
- { stream: s, stream_exact: true, backward: true, limit: 2 }
1902
- );
1903
- if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
1904
- })
1905
- );
1906
- const skipped = [];
1907
- let safe;
1908
- if (this._reactive_events.size === 0) {
1909
- safe = [...streamInfo.keys()];
1910
- } else {
1911
- const pendingSet = /* @__PURE__ */ new Set();
1912
- await store().query_streams((position) => {
1913
- const sourceRe = position.source ? RegExp(position.source) : void 0;
1914
- for (const [stream, info] of streamInfo) {
1915
- if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
1916
- pendingSet.add(stream);
1917
- }
1918
- }
1919
- });
1920
- safe = [];
1921
- for (const [stream] of streamInfo) {
1922
- if (pendingSet.has(stream)) {
1923
- skipped.push(stream);
1924
- } else {
1925
- safe.push(stream);
1926
- }
1927
- }
1928
- }
1929
- if (!safe.length) {
1930
- const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
1931
- this.emit("closed", result2);
1932
- return result2;
1933
- }
1934
- const correlation = randomUUID2();
1935
- const guarded = [];
1936
- const guardEvents = /* @__PURE__ */ new Map();
1937
- await Promise.all(
1938
- safe.map(async (stream) => {
1939
- try {
1940
- const info = streamInfo.get(stream);
1941
- const [committed] = await store().commit(
1942
- stream,
1943
- [{ name: TOMBSTONE_EVENT, data: {} }],
1944
- { correlation, causation: {} },
1945
- info.version
1946
- );
1947
- guarded.push(stream);
1948
- guardEvents.set(stream, { id: committed.id, stream });
1949
- } catch {
1950
- skipped.push(stream);
1951
- }
1952
- })
1953
- );
1954
- if (!guarded.length) {
1955
- const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
1956
- this.emit("closed", result2);
1957
- return result2;
1958
- }
1959
- const seedStates = /* @__PURE__ */ new Map();
1960
- await Promise.all(
1961
- guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
1962
- const lastEventName = streamInfo.get(stream)?.lastEventName;
1963
- const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
1964
- if (!ownerState) {
1965
- this._logger.error(
1966
- `Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
1967
- );
1968
- return;
1969
- }
1970
- const snap2 = await this._es.load(ownerState, stream);
1971
- seedStates.set(stream, snap2.state);
1972
- })
1973
- );
1974
- for (const stream of guarded) {
1975
- const archiveFn = targetMap.get(stream)?.archive;
1976
- if (archiveFn) await archiveFn();
1977
- }
1978
- const truncTargets = guarded.map((stream) => {
1979
- const snapshot = seedStates.get(stream);
1980
- const guard = guardEvents.get(stream);
1981
- return {
1982
- stream,
1983
- snapshot,
1984
- meta: {
1985
- correlation,
1986
- causation: {
1987
- event: {
1988
- id: guard.id,
1989
- name: TOMBSTONE_EVENT,
1990
- stream: guard.stream
1991
- }
1992
- }
1993
- }
1994
- };
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
1995
2192
  });
1996
- const truncated = await store().truncate(truncTargets);
1997
- await Promise.all(
1998
- guarded.map(async (stream) => {
1999
- const entry = truncated.get(stream);
2000
- const state2 = seedStates.get(stream);
2001
- if (state2 && entry) {
2002
- await cache().set(stream, {
2003
- state: state2,
2004
- version: entry.committed.version,
2005
- event_id: entry.committed.id,
2006
- patches: 0,
2007
- snaps: 1
2008
- });
2009
- } else {
2010
- await cache().invalidate(stream);
2011
- }
2012
- })
2013
- );
2014
- const result = { truncated, skipped };
2015
2193
  this.emit("closed", result);
2016
2194
  return result;
2017
2195
  }
@@ -2080,7 +2258,7 @@ var Act = class {
2080
2258
  }
2081
2259
  };
2082
2260
 
2083
- // src/act-builder.ts
2261
+ // src/builders/act-builder.ts
2084
2262
  function registerBatchHandler(proj, batchHandlers) {
2085
2263
  if (!proj.batchHandler || !proj.target) return;
2086
2264
  const existing = batchHandlers.get(proj.target);
@@ -2089,56 +2267,33 @@ function registerBatchHandler(proj, batchHandlers) {
2089
2267
  }
2090
2268
  batchHandlers.set(proj.target, proj.batchHandler);
2091
2269
  }
2092
- function act(states = /* @__PURE__ */ new Map(), registry = {
2093
- actions: {},
2094
- events: {}
2095
- }, 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();
2096
2278
  const builder = {
2097
2279
  withState: (state2) => {
2098
2280
  registerState(state2, states, registry.actions, registry.events);
2099
- return act(
2100
- states,
2101
- registry,
2102
- pendingProjections,
2103
- batchHandlers
2104
- );
2281
+ return builder;
2105
2282
  },
2106
2283
  withSlice: (input) => {
2107
2284
  for (const s of input.states.values()) {
2108
2285
  registerState(s, states, registry.actions, registry.events);
2109
2286
  }
2110
- for (const eventName of Object.keys(input.events)) {
2111
- const sliceRegister = input.events[eventName];
2112
- for (const [name, reaction] of sliceRegister.reactions) {
2113
- registry.events[eventName].reactions.set(name, reaction);
2114
- }
2115
- }
2287
+ mergeEventRegister(registry.events, input.events);
2116
2288
  pendingProjections.push(...input.projections);
2117
- return act(
2118
- states,
2119
- registry,
2120
- pendingProjections,
2121
- batchHandlers
2122
- );
2289
+ return builder;
2123
2290
  },
2124
2291
  withProjection: (proj) => {
2125
2292
  mergeProjection(proj, registry.events);
2126
2293
  registerBatchHandler(proj, batchHandlers);
2127
- return act(
2128
- states,
2129
- registry,
2130
- pendingProjections,
2131
- batchHandlers
2132
- );
2133
- },
2134
- withActor: () => {
2135
- return act(
2136
- states,
2137
- registry,
2138
- pendingProjections,
2139
- batchHandlers
2140
- );
2294
+ return builder;
2141
2295
  },
2296
+ withActor: () => builder,
2142
2297
  on: (event) => ({
2143
2298
  do: (handler, options) => {
2144
2299
  const reaction = {
@@ -2154,19 +2309,15 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2154
2309
  `Reaction handler for "${String(event)}" must be a named function`
2155
2310
  );
2156
2311
  registry.events[event].reactions.set(handler.name, reaction);
2157
- return {
2158
- ...builder,
2312
+ return Object.assign(builder, {
2159
2313
  to(resolver) {
2160
- registry.events[event].reactions.set(handler.name, {
2161
- ...reaction,
2162
- resolver: typeof resolver === "string" ? { target: resolver } : resolver
2163
- });
2314
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2164
2315
  return builder;
2165
2316
  }
2166
- };
2317
+ });
2167
2318
  }
2168
2319
  }),
2169
- build: () => {
2320
+ build: (options) => {
2170
2321
  for (const proj of pendingProjections) {
2171
2322
  mergeProjection(proj, registry.events);
2172
2323
  registerBatchHandler(proj, batchHandlers);
@@ -2174,7 +2325,8 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2174
2325
  return new Act(
2175
2326
  registry,
2176
2327
  states,
2177
- batchHandlers
2328
+ batchHandlers,
2329
+ options
2178
2330
  );
2179
2331
  },
2180
2332
  events: registry.events
@@ -2182,8 +2334,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
2182
2334
  return builder;
2183
2335
  }
2184
2336
 
2185
- // src/projection-builder.ts
2186
- function _projection(target, events) {
2337
+ // src/builders/projection-builder.ts
2338
+ function _projection(target) {
2339
+ const events = {};
2187
2340
  const defaultResolver = typeof target === "string" ? { target } : void 0;
2188
2341
  const base = {
2189
2342
  on: (entry) => {
@@ -2213,17 +2366,13 @@ function _projection(target, events) {
2213
2366
  `Projection handler for "${event}" must be a named function`
2214
2367
  );
2215
2368
  register.reactions.set(handler.name, reaction);
2216
- const nextBuilder = _projection(target, events);
2217
- return {
2218
- ...nextBuilder,
2369
+ const widened = base;
2370
+ return Object.assign(widened, {
2219
2371
  to(resolver) {
2220
- register.reactions.set(handler.name, {
2221
- ...reaction,
2222
- resolver: typeof resolver === "string" ? { target: resolver } : resolver
2223
- });
2224
- return nextBuilder;
2372
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2373
+ return widened;
2225
2374
  }
2226
- };
2375
+ });
2227
2376
  }
2228
2377
  };
2229
2378
  },
@@ -2235,8 +2384,7 @@ function _projection(target, events) {
2235
2384
  events
2236
2385
  };
2237
2386
  if (typeof target === "string") {
2238
- return {
2239
- ...base,
2387
+ return Object.assign(base, {
2240
2388
  batch: (handler) => ({
2241
2389
  build: () => ({
2242
2390
  _tag: "Projection",
@@ -2245,34 +2393,28 @@ function _projection(target, events) {
2245
2393
  batchHandler: handler
2246
2394
  })
2247
2395
  })
2248
- };
2396
+ });
2249
2397
  }
2250
2398
  return base;
2251
2399
  }
2252
- function projection(target, events = {}) {
2253
- return _projection(target, events);
2400
+ function projection(target) {
2401
+ return _projection(target);
2254
2402
  }
2255
2403
 
2256
- // src/slice-builder.ts
2257
- 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 = [];
2258
2410
  const builder = {
2259
2411
  withState: (state2) => {
2260
2412
  registerState(state2, states, actions, events);
2261
- return slice(
2262
- states,
2263
- actions,
2264
- events,
2265
- projections
2266
- );
2413
+ return builder;
2267
2414
  },
2268
2415
  withProjection: (proj) => {
2269
2416
  projections.push(proj);
2270
- return slice(
2271
- states,
2272
- actions,
2273
- events,
2274
- projections
2275
- );
2417
+ return builder;
2276
2418
  },
2277
2419
  on: (event) => ({
2278
2420
  do: (handler, options) => {
@@ -2289,16 +2431,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
2289
2431
  `Reaction handler for "${String(event)}" must be a named function`
2290
2432
  );
2291
2433
  events[event].reactions.set(handler.name, reaction);
2292
- return {
2293
- ...builder,
2434
+ return Object.assign(builder, {
2294
2435
  to(resolver) {
2295
- events[event].reactions.set(handler.name, {
2296
- ...reaction,
2297
- resolver: typeof resolver === "string" ? { target: resolver } : resolver
2298
- });
2436
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2299
2437
  return builder;
2300
2438
  }
2301
- };
2439
+ });
2302
2440
  }
2303
2441
  }),
2304
2442
  build: () => ({
@@ -2312,7 +2450,7 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
2312
2450
  return builder;
2313
2451
  }
2314
2452
 
2315
- // src/state-builder.ts
2453
+ // src/builders/state-builder.ts
2316
2454
  function state(entry) {
2317
2455
  const keys = Object.keys(entry);
2318
2456
  if (keys.length !== 1) throw new Error("state() requires exactly one key");
@@ -2330,7 +2468,7 @@ function state(entry) {
2330
2468
  return [k, fn];
2331
2469
  })
2332
2470
  );
2333
- const builder = action_builder({
2471
+ const internal = {
2334
2472
  events,
2335
2473
  actions: {},
2336
2474
  state: stateSchema,
@@ -2338,18 +2476,12 @@ function state(entry) {
2338
2476
  init,
2339
2477
  patch: defaultPatch,
2340
2478
  on: {}
2341
- });
2479
+ };
2480
+ const builder = action_builder(internal);
2342
2481
  return Object.assign(builder, {
2343
2482
  patch(customPatch) {
2344
- return action_builder({
2345
- events,
2346
- actions: {},
2347
- state: stateSchema,
2348
- name,
2349
- init,
2350
- patch: { ...defaultPatch, ...customPatch },
2351
- on: {}
2352
- });
2483
+ Object.assign(internal.patch, customPatch);
2484
+ return builder;
2353
2485
  }
2354
2486
  });
2355
2487
  }
@@ -2358,50 +2490,43 @@ function state(entry) {
2358
2490
  };
2359
2491
  }
2360
2492
  function action_builder(state2) {
2361
- return {
2493
+ const internal = state2;
2494
+ const builder = {
2362
2495
  on(entry) {
2363
2496
  const keys = Object.keys(entry);
2364
2497
  if (keys.length !== 1) throw new Error(".on() requires exactly one key");
2365
2498
  const action2 = keys[0];
2366
2499
  const schema = entry[action2];
2367
- if (action2 in state2.actions)
2500
+ if (action2 in internal.actions)
2368
2501
  throw new Error(`Duplicate action "${action2}"`);
2369
- const actions = {
2370
- ...state2.actions,
2371
- [action2]: schema
2372
- };
2373
- const on = { ...state2.on };
2374
- const _given = { ...state2.given };
2502
+ internal.actions[action2] = schema;
2375
2503
  function given(rules) {
2376
- _given[action2] = rules;
2504
+ (internal.given ??= {})[action2] = rules;
2377
2505
  return { emit };
2378
2506
  }
2379
2507
  function emit(handler) {
2380
2508
  if (typeof handler === "string") {
2381
2509
  const eventName = handler;
2382
- on[action2] = ((payload) => [eventName, payload]);
2510
+ internal.on[action2] = (payload) => [
2511
+ eventName,
2512
+ payload
2513
+ ];
2383
2514
  } else {
2384
- on[action2] = handler;
2515
+ internal.on[action2] = handler;
2385
2516
  }
2386
- return action_builder({
2387
- ...state2,
2388
- actions,
2389
- on,
2390
- given: _given
2391
- });
2517
+ return builder;
2392
2518
  }
2393
2519
  return { given, emit };
2394
2520
  },
2395
2521
  snap(snap2) {
2396
- return action_builder({
2397
- ...state2,
2398
- snap: snap2
2399
- });
2522
+ internal.snap = snap2;
2523
+ return builder;
2400
2524
  },
2401
2525
  build() {
2402
- return state2;
2526
+ return internal;
2403
2527
  }
2404
2528
  };
2529
+ return builder;
2405
2530
  }
2406
2531
  export {
2407
2532
  Act,
@@ -2410,6 +2535,7 @@ export {
2410
2535
  CommittedMetaSchema,
2411
2536
  ConcurrencyError,
2412
2537
  ConsoleLogger,
2538
+ DEFAULT_MAX_SUBSCRIBED_STREAMS,
2413
2539
  Environments,
2414
2540
  Errors,
2415
2541
  EventMetaSchema,