@rotorsoft/act 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -55,7 +55,7 @@ __export(index_exports, {
55
55
  ValidationError: () => ValidationError,
56
56
  ZodEmpty: () => ZodEmpty,
57
57
  act: () => act,
58
- cache: () => cache,
58
+ cache: () => cache2,
59
59
  config: () => config,
60
60
  dispose: () => dispose,
61
61
  disposeAndExit: () => disposeAndExit,
@@ -63,14 +63,18 @@ __export(index_exports, {
63
63
  log: () => log,
64
64
  port: () => port,
65
65
  projection: () => projection,
66
+ scoped: () => scoped,
66
67
  sleep: () => sleep,
67
68
  slice: () => slice,
68
69
  state: () => state,
69
- store: () => store,
70
+ store: () => store2,
70
71
  validate: () => validate
71
72
  });
72
73
  module.exports = __toCommonJS(index_exports);
73
74
 
75
+ // src/ports.ts
76
+ var import_node_async_hooks = require("async_hooks");
77
+
74
78
  // src/adapters/console-logger.ts
75
79
  var LEVEL_VALUES = {
76
80
  fatal: 60,
@@ -971,6 +975,7 @@ var InMemoryStore = class {
971
975
  };
972
976
 
973
977
  // src/ports.ts
978
+ var scoped = new import_node_async_hooks.AsyncLocalStorage();
974
979
  var ExitCodes = ["ERROR", "EXIT"];
975
980
  var adapters = /* @__PURE__ */ new Map();
976
981
  function port(injector) {
@@ -990,11 +995,17 @@ var log = port(function log2(adapter) {
990
995
  pretty: cfg.env !== "production"
991
996
  });
992
997
  });
993
- var store = port(function store2(adapter) {
994
- return adapter || new InMemoryStore();
998
+ var _store = port(function store(adapter) {
999
+ return adapter ?? new InMemoryStore();
1000
+ });
1001
+ var store2 = ((adapter) => {
1002
+ return scoped.getStore()?.store ?? _store(adapter);
995
1003
  });
996
- var cache = port(function cache2(adapter) {
997
- return adapter || new InMemoryCache();
1004
+ var _cache = port(function cache(adapter) {
1005
+ return adapter ?? new InMemoryCache();
1006
+ });
1007
+ var cache2 = ((adapter) => {
1008
+ return scoped.getStore()?.cache ?? _cache(adapter);
998
1009
  });
999
1010
  var disposers = [];
1000
1011
  async function disposeAndExit(code = "EXIT") {
@@ -1124,7 +1135,7 @@ async function scanStreamHeads(streams) {
1124
1135
  let maxId = -1;
1125
1136
  let version = -1;
1126
1137
  let lastEventName = "";
1127
- await store().query(
1138
+ await store2().query(
1128
1139
  (e) => {
1129
1140
  if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
1130
1141
  maxId = e.id;
@@ -1141,7 +1152,7 @@ async function scanStreamHeads(streams) {
1141
1152
  async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
1142
1153
  if (reactiveEventsSize === 0) return [...streamInfo.keys()];
1143
1154
  const pendingSet = /* @__PURE__ */ new Set();
1144
- await store().query_streams((position) => {
1155
+ await store2().query_streams((position) => {
1145
1156
  const sourceRe = position.source ? RegExp(position.source) : void 0;
1146
1157
  for (const [stream, info] of streamInfo) {
1147
1158
  if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
@@ -1212,13 +1223,13 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1212
1223
  }
1213
1224
  };
1214
1225
  });
1215
- const truncated = await store().truncate(truncTargets);
1226
+ const truncated = await store2().truncate(truncTargets);
1216
1227
  await Promise.all(
1217
1228
  guarded.map(async (stream) => {
1218
1229
  const entry = truncated.get(stream);
1219
1230
  const state2 = seedStates.get(stream);
1220
1231
  if (state2 && entry) {
1221
- await cache().set(stream, {
1232
+ await cache2().set(stream, {
1222
1233
  state: state2,
1223
1234
  version: entry.committed.version,
1224
1235
  event_id: entry.committed.id,
@@ -1226,7 +1237,7 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1226
1237
  snaps: 1
1227
1238
  });
1228
1239
  } else {
1229
- await cache().invalidate(stream);
1240
+ await cache2().invalidate(stream);
1230
1241
  }
1231
1242
  })
1232
1243
  );
@@ -1261,7 +1272,7 @@ var CorrelateCycle = class {
1261
1272
  async init() {
1262
1273
  if (this._initialized) return;
1263
1274
  this._initialized = true;
1264
- const { watermark } = await store().subscribe([...this.staticTargets]);
1275
+ const { watermark } = await store2().subscribe([...this.staticTargets]);
1265
1276
  this._checkpoint = watermark;
1266
1277
  this.onInit?.();
1267
1278
  for (const { stream } of this.staticTargets) {
@@ -1280,7 +1291,7 @@ var CorrelateCycle = class {
1280
1291
  const after = Math.max(this._checkpoint, query.after || -1);
1281
1292
  const correlated = /* @__PURE__ */ new Map();
1282
1293
  let last_id = after;
1283
- await store().query(
1294
+ await store2().query(
1284
1295
  (event) => {
1285
1296
  last_id = event.id;
1286
1297
  const register = this.registry.events[event.name];
@@ -1485,6 +1496,43 @@ var DrainController = class {
1485
1496
  }
1486
1497
  };
1487
1498
 
1499
+ // src/internal/event-versions.ts
1500
+ var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
1501
+ function parse(name) {
1502
+ const m = name.match(VERSION_SUFFIX);
1503
+ if (m) {
1504
+ const v = Number.parseInt(m[2], 10);
1505
+ if (v >= 2) return { base: m[1], version: v };
1506
+ }
1507
+ return { base: name, version: 1 };
1508
+ }
1509
+ function deprecatedEventNames(names) {
1510
+ const groups = /* @__PURE__ */ new Map();
1511
+ for (const name of names) {
1512
+ const { base, version } = parse(name);
1513
+ const list = groups.get(base);
1514
+ if (list) list.push({ version, name });
1515
+ else groups.set(base, [{ version, name }]);
1516
+ }
1517
+ const deprecated = /* @__PURE__ */ new Set();
1518
+ for (const list of groups.values()) {
1519
+ if (list.length < 2) continue;
1520
+ list.sort((a, b) => b.version - a.version);
1521
+ for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
1522
+ }
1523
+ return deprecated;
1524
+ }
1525
+ function currentVersionOf(deprecatedName, allNames) {
1526
+ const target = parse(deprecatedName);
1527
+ let highest;
1528
+ for (const name of allNames) {
1529
+ const { base, version } = parse(name);
1530
+ if (base !== target.base) continue;
1531
+ if (!highest || version > highest.version) highest = { version, name };
1532
+ }
1533
+ return highest && highest.version > target.version ? highest.name : void 0;
1534
+ }
1535
+
1488
1536
  // src/internal/merge.ts
1489
1537
  var import_zod4 = require("zod");
1490
1538
  function baseTypeName(zodType) {
@@ -1763,12 +1811,12 @@ var SettleLoop = class {
1763
1811
  };
1764
1812
 
1765
1813
  // src/internal/drain.ts
1766
- var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
1814
+ var claim = (lagging, leading, by, millis) => store2().claim(lagging, leading, by, millis);
1767
1815
  async function fetch(leased, eventLimit) {
1768
1816
  return Promise.all(
1769
1817
  leased.map(async ({ stream, source, at, lagging }) => {
1770
1818
  const events = [];
1771
- await store().query((e) => events.push(e), {
1819
+ await store2().query((e) => events.push(e), {
1772
1820
  stream: source,
1773
1821
  after: at,
1774
1822
  limit: eventLimit
@@ -1777,9 +1825,9 @@ async function fetch(leased, eventLimit) {
1777
1825
  })
1778
1826
  );
1779
1827
  }
1780
- var ack = (leases) => store().ack(leases);
1781
- var block = (leases) => store().block(leases);
1782
- var subscribe = (streams) => store().subscribe(streams);
1828
+ var ack = (leases) => store2().ack(leases);
1829
+ var block = (leases) => store2().block(leases);
1830
+ var subscribe = (streams) => store2().subscribe(streams);
1783
1831
 
1784
1832
  // src/internal/event-sourcing.ts
1785
1833
  var import_node_crypto3 = require("crypto");
@@ -1787,7 +1835,7 @@ var import_act_patch = require("@rotorsoft/act-patch");
1787
1835
  async function snap(snapshot) {
1788
1836
  try {
1789
1837
  const { id, stream, name, meta, version } = snapshot.event;
1790
- await store().commit(
1838
+ await store2().commit(
1791
1839
  stream,
1792
1840
  [{ name: SNAP_EVENT, data: snapshot.state }],
1793
1841
  {
@@ -1803,7 +1851,7 @@ async function snap(snapshot) {
1803
1851
  }
1804
1852
  async function tombstone(stream, expectedVersion, correlation) {
1805
1853
  try {
1806
- const [committed] = await store().commit(
1854
+ const [committed] = await store2().commit(
1807
1855
  stream,
1808
1856
  [{ name: TOMBSTONE_EVENT, data: {} }],
1809
1857
  { correlation, causation: {} },
@@ -1817,7 +1865,7 @@ async function tombstone(stream, expectedVersion, correlation) {
1817
1865
  }
1818
1866
  async function load(me, stream, callback, asOf) {
1819
1867
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1820
- const cached = timeTravel ? void 0 : await cache().get(stream);
1868
+ const cached = timeTravel ? void 0 : await cache2().get(stream);
1821
1869
  const cache_hit = !!cached;
1822
1870
  let state2 = cached?.state ?? (me.init ? me.init() : {});
1823
1871
  let patches = cached?.patches ?? 0;
@@ -1825,7 +1873,7 @@ async function load(me, stream, callback, asOf) {
1825
1873
  let version = cached?.version ?? -1;
1826
1874
  let replayed = 0;
1827
1875
  let event;
1828
- await store().query(
1876
+ await store2().query(
1829
1877
  (e) => {
1830
1878
  event = e;
1831
1879
  version = e.version;
@@ -1860,7 +1908,7 @@ async function load(me, stream, callback, asOf) {
1860
1908
  }
1861
1909
  );
1862
1910
  if (replayed > 0 && !timeTravel && event) {
1863
- await cache().set(stream, {
1911
+ await cache2().set(stream, {
1864
1912
  state: state2,
1865
1913
  version,
1866
1914
  event_id: event.id,
@@ -1897,6 +1945,20 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1897
1945
  return [snapshot];
1898
1946
  }
1899
1947
  const tuples = Array.isArray(result[0]) ? result : [result];
1948
+ const deprecated = me._deprecated;
1949
+ if (deprecated && deprecated.size > 0) {
1950
+ const me_ = me;
1951
+ const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
1952
+ for (const [name] of tuples) {
1953
+ const evt = name;
1954
+ if (deprecated.has(evt) && !warned.has(evt)) {
1955
+ warned.add(evt);
1956
+ log().warn(
1957
+ `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
1958
+ );
1959
+ }
1960
+ }
1961
+ }
1900
1962
  const emitted = tuples.map(([name, data]) => ({
1901
1963
  name,
1902
1964
  data: skipValidation ? data : validate(name, data, me.events[name])
@@ -1919,7 +1981,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1919
1981
  };
1920
1982
  let committed;
1921
1983
  try {
1922
- committed = await store().commit(
1984
+ committed = await store2().commit(
1923
1985
  stream,
1924
1986
  emitted,
1925
1987
  meta,
@@ -1931,7 +1993,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1931
1993
  );
1932
1994
  } catch (error) {
1933
1995
  if (error instanceof ConcurrencyError) {
1934
- await cache().invalidate(stream);
1996
+ await cache2().invalidate(stream);
1935
1997
  }
1936
1998
  throw error;
1937
1999
  }
@@ -1953,7 +2015,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1953
2015
  });
1954
2016
  const last = snapshots.at(-1);
1955
2017
  const snapped = me.snap?.(last);
1956
- cache().set(stream, {
2018
+ cache2().set(stream, {
1957
2019
  state: last.state,
1958
2020
  version: last.event.version,
1959
2021
  event_id: last.event.id,
@@ -2148,6 +2210,7 @@ var Act = class {
2148
2210
  this.registry = registry;
2149
2211
  this._states = _states;
2150
2212
  this._batch_handlers = batchHandlers;
2213
+ this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
2151
2214
  this._es = buildEs(this._logger);
2152
2215
  this._cd = buildDrain(this._logger);
2153
2216
  this._handle = buildHandle({
@@ -2193,7 +2256,7 @@ var Act = class {
2193
2256
  },
2194
2257
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2195
2258
  );
2196
- this._notify_disposer = this._wireNotify();
2259
+ this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
2197
2260
  dispose(async () => {
2198
2261
  this._emitter.removeAllListeners();
2199
2262
  this.stop_correlations();
@@ -2262,6 +2325,11 @@ var Act = class {
2262
2325
  _event_to_state;
2263
2326
  /** Logger resolved at construction time (after user port configuration) */
2264
2327
  _logger = log();
2328
+ /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
2329
+ * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
2330
+ * path keeps reading fresh `store()`/`cache()` per call, which matters for
2331
+ * tests that dispose and re-seed mid-suite. */
2332
+ _scoped;
2265
2333
  /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
2266
2334
  * payload (it captures the triggering event for reactingTo auto-inject). */
2267
2335
  _bound_do = this.do.bind(this);
@@ -2277,9 +2345,8 @@ var Act = class {
2277
2345
  * subscription was made). Errors during subscription are logged but
2278
2346
  * never thrown — `notify` is a hint, not a contract.
2279
2347
  */
2280
- async _wireNotify() {
2348
+ async _wireNotify(s) {
2281
2349
  if (this._reactive_events.size === 0) return void 0;
2282
- const s = store();
2283
2350
  if (!s.notify) return void 0;
2284
2351
  try {
2285
2352
  return await s.notify((notification) => {
@@ -2383,35 +2450,39 @@ var Act = class {
2383
2450
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
2384
2451
  */
2385
2452
  async do(action2, target, payload, reactingTo, skipValidation = false) {
2386
- const snapshots = await this._es.action(
2387
- this.registry.actions[action2],
2388
- action2,
2389
- target,
2390
- payload,
2391
- reactingTo,
2392
- skipValidation
2393
- );
2394
- if (this._reactive_events.size > 0) {
2395
- for (const snap2 of snapshots) {
2396
- if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2397
- this._drain.arm();
2398
- break;
2453
+ return this._scoped(async () => {
2454
+ const snapshots = await this._es.action(
2455
+ this.registry.actions[action2],
2456
+ action2,
2457
+ target,
2458
+ payload,
2459
+ reactingTo,
2460
+ skipValidation
2461
+ );
2462
+ if (this._reactive_events.size > 0) {
2463
+ for (const snap2 of snapshots) {
2464
+ if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2465
+ this._drain.arm();
2466
+ break;
2467
+ }
2399
2468
  }
2400
2469
  }
2401
- }
2402
- this.emit("committed", snapshots);
2403
- return snapshots;
2470
+ this.emit("committed", snapshots);
2471
+ return snapshots;
2472
+ });
2404
2473
  }
2405
2474
  async load(stateOrName, stream, callback, asOf) {
2406
- let merged;
2407
- if (typeof stateOrName === "string") {
2408
- const found = this._states.get(stateOrName);
2409
- if (!found) throw new Error(`State "${stateOrName}" not found`);
2410
- merged = found;
2411
- } else {
2412
- merged = this._states.get(stateOrName.name) || stateOrName;
2413
- }
2414
- return await this._es.load(merged, stream, callback, asOf);
2475
+ return this._scoped(async () => {
2476
+ let merged;
2477
+ if (typeof stateOrName === "string") {
2478
+ const found = this._states.get(stateOrName);
2479
+ if (!found) throw new Error(`State "${stateOrName}" not found`);
2480
+ merged = found;
2481
+ } else {
2482
+ merged = this._states.get(stateOrName.name) || stateOrName;
2483
+ }
2484
+ return await this._es.load(merged, stream, callback, asOf);
2485
+ });
2415
2486
  }
2416
2487
  /**
2417
2488
  * Queries the event store for events matching a filter.
@@ -2460,14 +2531,16 @@ var Act = class {
2460
2531
  * @see {@link query_array} for loading events into memory
2461
2532
  */
2462
2533
  async query(query, callback) {
2463
- let first;
2464
- let last;
2465
- const count = await store().query((e) => {
2466
- if (!first) first = e;
2467
- last = e;
2468
- callback?.(e);
2469
- }, query);
2470
- return { first, last, count };
2534
+ return this._scoped(async () => {
2535
+ let first;
2536
+ let last;
2537
+ const count = await store2().query((e) => {
2538
+ if (!first) first = e;
2539
+ last = e;
2540
+ callback?.(e);
2541
+ }, query);
2542
+ return { first, last, count };
2543
+ });
2471
2544
  }
2472
2545
  /**
2473
2546
  * Queries the event store and returns all matching events in memory.
@@ -2496,9 +2569,11 @@ var Act = class {
2496
2569
  * @see {@link query} for large result sets
2497
2570
  */
2498
2571
  async query_array(query) {
2499
- const events = [];
2500
- await store().query((e) => events.push(e), query);
2501
- return events;
2572
+ return this._scoped(async () => {
2573
+ const events = [];
2574
+ await store2().query((e) => events.push(e), query);
2575
+ return events;
2576
+ });
2502
2577
  }
2503
2578
  /**
2504
2579
  * Processes pending reactions by draining uncommitted events from the event store.
@@ -2538,7 +2613,7 @@ var Act = class {
2538
2613
  * @see {@link start_correlations} for automatic correlation
2539
2614
  */
2540
2615
  async drain(options = {}) {
2541
- return this._drain.drain(options);
2616
+ return this._scoped(() => this._drain.drain(options));
2542
2617
  }
2543
2618
  /**
2544
2619
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -2586,7 +2661,7 @@ var Act = class {
2586
2661
  * @see {@link stop_correlations} to stop automatic correlation
2587
2662
  */
2588
2663
  async correlate(query = { after: -1, limit: 10 }) {
2589
- return this._correlate.correlate(query);
2664
+ return this._scoped(() => this._correlate.correlate(query));
2590
2665
  }
2591
2666
  /**
2592
2667
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -2707,9 +2782,11 @@ var Act = class {
2707
2782
  * @see {@link settle} for the debounced full-catch-up loop
2708
2783
  */
2709
2784
  async reset(streams) {
2710
- const count = await store().reset(streams);
2711
- if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2712
- return count;
2785
+ return this._scoped(async () => {
2786
+ const count = await store2().reset(streams);
2787
+ if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2788
+ return count;
2789
+ });
2713
2790
  }
2714
2791
  /**
2715
2792
  * Bulk-update scheduling priority for streams matching `filter`.
@@ -2750,7 +2827,7 @@ var Act = class {
2750
2827
  * @see {@link claim} for how priority biases scheduling
2751
2828
  */
2752
2829
  async prioritize(filter, priority) {
2753
- return store().prioritize(filter, priority);
2830
+ return this._scoped(() => store2().prioritize(filter, priority));
2754
2831
  }
2755
2832
  /**
2756
2833
  * Close the books — guard, archive, truncate, and optionally restart streams.
@@ -2787,16 +2864,18 @@ var Act = class {
2787
2864
  */
2788
2865
  async close(targets) {
2789
2866
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
2790
- await this.correlate({ limit: 1e3 });
2791
- const result = await runCloseCycle(targets, {
2792
- reactiveEventsSize: this._reactive_events.size,
2793
- eventToState: this._event_to_state,
2794
- load: this._es.load,
2795
- tombstone: this._es.tombstone,
2796
- logger: this._logger
2867
+ return this._scoped(async () => {
2868
+ await this.correlate({ limit: 1e3 });
2869
+ const result = await runCloseCycle(targets, {
2870
+ reactiveEventsSize: this._reactive_events.size,
2871
+ eventToState: this._event_to_state,
2872
+ load: this._es.load,
2873
+ tombstone: this._es.tombstone,
2874
+ logger: this._logger
2875
+ });
2876
+ this.emit("closed", result);
2877
+ return result;
2797
2878
  });
2798
- this.emit("closed", result);
2799
- return result;
2800
2879
  }
2801
2880
  /**
2802
2881
  * Debounced, non-blocking correlate→drain cycle.
@@ -2850,6 +2929,41 @@ function act() {
2850
2929
  };
2851
2930
  const pendingProjections = [];
2852
2931
  const batchHandlers = /* @__PURE__ */ new Map();
2932
+ let _built = false;
2933
+ const finalizeDeprecations = () => {
2934
+ const deprecationSummary = [];
2935
+ for (const state2 of states.values()) {
2936
+ const eventNames = Object.keys(state2.events);
2937
+ const deprecated = deprecatedEventNames(eventNames);
2938
+ if (deprecated.size === 0) continue;
2939
+ state2._deprecated = deprecated;
2940
+ for (const name of deprecated) {
2941
+ const current = currentVersionOf(name, eventNames);
2942
+ deprecationSummary.push({
2943
+ stateName: state2.name,
2944
+ deprecated: name,
2945
+ current
2946
+ });
2947
+ }
2948
+ for (const [actionName, handler] of Object.entries(state2.on)) {
2949
+ const staticTarget = handler?._staticEmit;
2950
+ if (staticTarget && deprecated.has(staticTarget)) {
2951
+ const current = currentVersionOf(staticTarget, eventNames);
2952
+ throw new Error(
2953
+ `Action "${actionName}" in state "${state2.name}" emits deprecated event "${staticTarget}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${staticTarget}" stays as-is \u2014 historical events still need it.`
2954
+ );
2955
+ }
2956
+ }
2957
+ }
2958
+ if (deprecationSummary.length > 0) {
2959
+ const list = deprecationSummary.map(
2960
+ (d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
2961
+ ).join(", ");
2962
+ log().info(
2963
+ `Act registered ${deprecationSummary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
2964
+ );
2965
+ }
2966
+ };
2853
2967
  const builder = {
2854
2968
  withState: (state2) => {
2855
2969
  registerState(state2, states, registry.actions, registry.events);
@@ -2893,9 +3007,13 @@ function act() {
2893
3007
  }
2894
3008
  }),
2895
3009
  build: (options) => {
2896
- for (const proj of pendingProjections) {
2897
- mergeProjection(proj, registry.events);
2898
- registerBatchHandler(proj, batchHandlers);
3010
+ if (!_built) {
3011
+ for (const proj of pendingProjections) {
3012
+ mergeProjection(proj, registry.events);
3013
+ registerBatchHandler(proj, batchHandlers);
3014
+ }
3015
+ finalizeDeprecations();
3016
+ _built = true;
2899
3017
  }
2900
3018
  return new Act(
2901
3019
  registry,
@@ -3083,10 +3201,10 @@ function action_builder(state2) {
3083
3201
  function emit(handler) {
3084
3202
  if (typeof handler === "string") {
3085
3203
  const eventName = handler;
3086
- internal.on[action2] = (payload) => [
3087
- eventName,
3088
- payload
3089
- ];
3204
+ const emitFn = Object.assign((payload) => [eventName, payload], {
3205
+ _staticEmit: eventName
3206
+ });
3207
+ internal.on[action2] = emitFn;
3090
3208
  } else {
3091
3209
  internal.on[action2] = handler;
3092
3210
  }
@@ -3139,6 +3257,7 @@ function action_builder(state2) {
3139
3257
  log,
3140
3258
  port,
3141
3259
  projection,
3260
+ scoped,
3142
3261
  sleep,
3143
3262
  slice,
3144
3263
  state,