@rotorsoft/act 0.37.0 → 0.39.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];
@@ -1800,12 +1811,12 @@ var SettleLoop = class {
1800
1811
  };
1801
1812
 
1802
1813
  // src/internal/drain.ts
1803
- 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);
1804
1815
  async function fetch(leased, eventLimit) {
1805
1816
  return Promise.all(
1806
1817
  leased.map(async ({ stream, source, at, lagging }) => {
1807
1818
  const events = [];
1808
- await store().query((e) => events.push(e), {
1819
+ await store2().query((e) => events.push(e), {
1809
1820
  stream: source,
1810
1821
  after: at,
1811
1822
  limit: eventLimit
@@ -1814,9 +1825,9 @@ async function fetch(leased, eventLimit) {
1814
1825
  })
1815
1826
  );
1816
1827
  }
1817
- var ack = (leases) => store().ack(leases);
1818
- var block = (leases) => store().block(leases);
1819
- 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);
1820
1831
 
1821
1832
  // src/internal/event-sourcing.ts
1822
1833
  var import_node_crypto3 = require("crypto");
@@ -1824,7 +1835,7 @@ var import_act_patch = require("@rotorsoft/act-patch");
1824
1835
  async function snap(snapshot) {
1825
1836
  try {
1826
1837
  const { id, stream, name, meta, version } = snapshot.event;
1827
- await store().commit(
1838
+ await store2().commit(
1828
1839
  stream,
1829
1840
  [{ name: SNAP_EVENT, data: snapshot.state }],
1830
1841
  {
@@ -1840,7 +1851,7 @@ async function snap(snapshot) {
1840
1851
  }
1841
1852
  async function tombstone(stream, expectedVersion, correlation) {
1842
1853
  try {
1843
- const [committed] = await store().commit(
1854
+ const [committed] = await store2().commit(
1844
1855
  stream,
1845
1856
  [{ name: TOMBSTONE_EVENT, data: {} }],
1846
1857
  { correlation, causation: {} },
@@ -1854,7 +1865,7 @@ async function tombstone(stream, expectedVersion, correlation) {
1854
1865
  }
1855
1866
  async function load(me, stream, callback, asOf) {
1856
1867
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1857
- const cached = timeTravel ? void 0 : await cache().get(stream);
1868
+ const cached = timeTravel ? void 0 : await cache2().get(stream);
1858
1869
  const cache_hit = !!cached;
1859
1870
  let state2 = cached?.state ?? (me.init ? me.init() : {});
1860
1871
  let patches = cached?.patches ?? 0;
@@ -1862,7 +1873,7 @@ async function load(me, stream, callback, asOf) {
1862
1873
  let version = cached?.version ?? -1;
1863
1874
  let replayed = 0;
1864
1875
  let event;
1865
- await store().query(
1876
+ await store2().query(
1866
1877
  (e) => {
1867
1878
  event = e;
1868
1879
  version = e.version;
@@ -1897,7 +1908,7 @@ async function load(me, stream, callback, asOf) {
1897
1908
  }
1898
1909
  );
1899
1910
  if (replayed > 0 && !timeTravel && event) {
1900
- await cache().set(stream, {
1911
+ await cache2().set(stream, {
1901
1912
  state: state2,
1902
1913
  version,
1903
1914
  event_id: event.id,
@@ -1970,7 +1981,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1970
1981
  };
1971
1982
  let committed;
1972
1983
  try {
1973
- committed = await store().commit(
1984
+ committed = await store2().commit(
1974
1985
  stream,
1975
1986
  emitted,
1976
1987
  meta,
@@ -1982,7 +1993,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1982
1993
  );
1983
1994
  } catch (error) {
1984
1995
  if (error instanceof ConcurrencyError) {
1985
- await cache().invalidate(stream);
1996
+ await cache2().invalidate(stream);
1986
1997
  }
1987
1998
  throw error;
1988
1999
  }
@@ -2004,7 +2015,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
2004
2015
  });
2005
2016
  const last = snapshots.at(-1);
2006
2017
  const snapped = me.snap?.(last);
2007
- cache().set(stream, {
2018
+ cache2().set(stream, {
2008
2019
  state: last.state,
2009
2020
  version: last.event.version,
2010
2021
  event_id: last.event.id,
@@ -2199,6 +2210,7 @@ var Act = class {
2199
2210
  this.registry = registry;
2200
2211
  this._states = _states;
2201
2212
  this._batch_handlers = batchHandlers;
2213
+ this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
2202
2214
  this._es = buildEs(this._logger);
2203
2215
  this._cd = buildDrain(this._logger);
2204
2216
  this._handle = buildHandle({
@@ -2244,14 +2256,8 @@ var Act = class {
2244
2256
  },
2245
2257
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2246
2258
  );
2247
- this._notify_disposer = this._wireNotify();
2248
- dispose(async () => {
2249
- this._emitter.removeAllListeners();
2250
- this.stop_correlations();
2251
- this.stop_settling();
2252
- const disposer = await this._notify_disposer;
2253
- if (disposer) await disposer();
2254
- });
2259
+ this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
2260
+ dispose(() => this.shutdown());
2255
2261
  }
2256
2262
  _emitter = new import_node_events.default();
2257
2263
  /** Event names with at least one registered reaction (computed at build time) */
@@ -2313,6 +2319,11 @@ var Act = class {
2313
2319
  _event_to_state;
2314
2320
  /** Logger resolved at construction time (after user port configuration) */
2315
2321
  _logger = log();
2322
+ /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
2323
+ * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
2324
+ * path keeps reading fresh `store()`/`cache()` per call, which matters for
2325
+ * tests that dispose and re-seed mid-suite. */
2326
+ _scoped;
2316
2327
  /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
2317
2328
  * payload (it captures the triggering event for reactingTo auto-inject). */
2318
2329
  _bound_do = this.do.bind(this);
@@ -2322,15 +2333,38 @@ var Act = class {
2322
2333
  /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
2323
2334
  _handle;
2324
2335
  _handle_batch;
2336
+ /** True after the first `shutdown()` call. Guards idempotency. */
2337
+ _shutdown_promise;
2338
+ /**
2339
+ * Per-instance teardown: remove lifecycle listeners, stop the
2340
+ * correlation worker, cancel any pending settle cycle, and tear
2341
+ * down the cross-process notify subscription.
2342
+ *
2343
+ * Idempotent — repeated calls return the same promise. Registered
2344
+ * automatically with the global `dispose()` registry at construction,
2345
+ * so process-wide `dispose()()` covers it; test helpers (or operators
2346
+ * that mint short-lived Acts) call it explicitly for prompt cleanup.
2347
+ */
2348
+ shutdown() {
2349
+ if (!this._shutdown_promise) {
2350
+ this._shutdown_promise = (async () => {
2351
+ this._emitter.removeAllListeners();
2352
+ this.stop_correlations();
2353
+ this.stop_settling();
2354
+ const disposer = await this._notify_disposer;
2355
+ if (disposer) await disposer();
2356
+ })();
2357
+ }
2358
+ return this._shutdown_promise;
2359
+ }
2325
2360
  /**
2326
2361
  * Subscribe to {@link Store.notify} when both the store and the
2327
2362
  * registry support it. Returns the disposer (or `undefined` when no
2328
2363
  * subscription was made). Errors during subscription are logged but
2329
2364
  * never thrown — `notify` is a hint, not a contract.
2330
2365
  */
2331
- async _wireNotify() {
2366
+ async _wireNotify(s) {
2332
2367
  if (this._reactive_events.size === 0) return void 0;
2333
- const s = store();
2334
2368
  if (!s.notify) return void 0;
2335
2369
  try {
2336
2370
  return await s.notify((notification) => {
@@ -2434,35 +2468,39 @@ var Act = class {
2434
2468
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
2435
2469
  */
2436
2470
  async do(action2, target, payload, reactingTo, skipValidation = false) {
2437
- const snapshots = await this._es.action(
2438
- this.registry.actions[action2],
2439
- action2,
2440
- target,
2441
- payload,
2442
- reactingTo,
2443
- skipValidation
2444
- );
2445
- if (this._reactive_events.size > 0) {
2446
- for (const snap2 of snapshots) {
2447
- if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2448
- this._drain.arm();
2449
- break;
2471
+ return this._scoped(async () => {
2472
+ const snapshots = await this._es.action(
2473
+ this.registry.actions[action2],
2474
+ action2,
2475
+ target,
2476
+ payload,
2477
+ reactingTo,
2478
+ skipValidation
2479
+ );
2480
+ if (this._reactive_events.size > 0) {
2481
+ for (const snap2 of snapshots) {
2482
+ if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2483
+ this._drain.arm();
2484
+ break;
2485
+ }
2450
2486
  }
2451
2487
  }
2452
- }
2453
- this.emit("committed", snapshots);
2454
- return snapshots;
2488
+ this.emit("committed", snapshots);
2489
+ return snapshots;
2490
+ });
2455
2491
  }
2456
2492
  async load(stateOrName, stream, callback, asOf) {
2457
- let merged;
2458
- if (typeof stateOrName === "string") {
2459
- const found = this._states.get(stateOrName);
2460
- if (!found) throw new Error(`State "${stateOrName}" not found`);
2461
- merged = found;
2462
- } else {
2463
- merged = this._states.get(stateOrName.name) || stateOrName;
2464
- }
2465
- return await this._es.load(merged, stream, callback, asOf);
2493
+ return this._scoped(async () => {
2494
+ let merged;
2495
+ if (typeof stateOrName === "string") {
2496
+ const found = this._states.get(stateOrName);
2497
+ if (!found) throw new Error(`State "${stateOrName}" not found`);
2498
+ merged = found;
2499
+ } else {
2500
+ merged = this._states.get(stateOrName.name) || stateOrName;
2501
+ }
2502
+ return await this._es.load(merged, stream, callback, asOf);
2503
+ });
2466
2504
  }
2467
2505
  /**
2468
2506
  * Queries the event store for events matching a filter.
@@ -2511,14 +2549,16 @@ var Act = class {
2511
2549
  * @see {@link query_array} for loading events into memory
2512
2550
  */
2513
2551
  async query(query, callback) {
2514
- let first;
2515
- let last;
2516
- const count = await store().query((e) => {
2517
- if (!first) first = e;
2518
- last = e;
2519
- callback?.(e);
2520
- }, query);
2521
- return { first, last, count };
2552
+ return this._scoped(async () => {
2553
+ let first;
2554
+ let last;
2555
+ const count = await store2().query((e) => {
2556
+ if (!first) first = e;
2557
+ last = e;
2558
+ callback?.(e);
2559
+ }, query);
2560
+ return { first, last, count };
2561
+ });
2522
2562
  }
2523
2563
  /**
2524
2564
  * Queries the event store and returns all matching events in memory.
@@ -2547,9 +2587,11 @@ var Act = class {
2547
2587
  * @see {@link query} for large result sets
2548
2588
  */
2549
2589
  async query_array(query) {
2550
- const events = [];
2551
- await store().query((e) => events.push(e), query);
2552
- return events;
2590
+ return this._scoped(async () => {
2591
+ const events = [];
2592
+ await store2().query((e) => events.push(e), query);
2593
+ return events;
2594
+ });
2553
2595
  }
2554
2596
  /**
2555
2597
  * Processes pending reactions by draining uncommitted events from the event store.
@@ -2589,7 +2631,7 @@ var Act = class {
2589
2631
  * @see {@link start_correlations} for automatic correlation
2590
2632
  */
2591
2633
  async drain(options = {}) {
2592
- return this._drain.drain(options);
2634
+ return this._scoped(() => this._drain.drain(options));
2593
2635
  }
2594
2636
  /**
2595
2637
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -2637,7 +2679,7 @@ var Act = class {
2637
2679
  * @see {@link stop_correlations} to stop automatic correlation
2638
2680
  */
2639
2681
  async correlate(query = { after: -1, limit: 10 }) {
2640
- return this._correlate.correlate(query);
2682
+ return this._scoped(() => this._correlate.correlate(query));
2641
2683
  }
2642
2684
  /**
2643
2685
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -2758,9 +2800,11 @@ var Act = class {
2758
2800
  * @see {@link settle} for the debounced full-catch-up loop
2759
2801
  */
2760
2802
  async reset(streams) {
2761
- const count = await store().reset(streams);
2762
- if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2763
- return count;
2803
+ return this._scoped(async () => {
2804
+ const count = await store2().reset(streams);
2805
+ if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2806
+ return count;
2807
+ });
2764
2808
  }
2765
2809
  /**
2766
2810
  * Bulk-update scheduling priority for streams matching `filter`.
@@ -2801,7 +2845,7 @@ var Act = class {
2801
2845
  * @see {@link claim} for how priority biases scheduling
2802
2846
  */
2803
2847
  async prioritize(filter, priority) {
2804
- return store().prioritize(filter, priority);
2848
+ return this._scoped(() => store2().prioritize(filter, priority));
2805
2849
  }
2806
2850
  /**
2807
2851
  * Close the books — guard, archive, truncate, and optionally restart streams.
@@ -2838,16 +2882,18 @@ var Act = class {
2838
2882
  */
2839
2883
  async close(targets) {
2840
2884
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
2841
- await this.correlate({ limit: 1e3 });
2842
- const result = await runCloseCycle(targets, {
2843
- reactiveEventsSize: this._reactive_events.size,
2844
- eventToState: this._event_to_state,
2845
- load: this._es.load,
2846
- tombstone: this._es.tombstone,
2847
- logger: this._logger
2885
+ return this._scoped(async () => {
2886
+ await this.correlate({ limit: 1e3 });
2887
+ const result = await runCloseCycle(targets, {
2888
+ reactiveEventsSize: this._reactive_events.size,
2889
+ eventToState: this._event_to_state,
2890
+ load: this._es.load,
2891
+ tombstone: this._es.tombstone,
2892
+ logger: this._logger
2893
+ });
2894
+ this.emit("closed", result);
2895
+ return result;
2848
2896
  });
2849
- this.emit("closed", result);
2850
- return result;
2851
2897
  }
2852
2898
  /**
2853
2899
  * Debounced, non-blocking correlate→drain cycle.
@@ -2901,6 +2947,41 @@ function act() {
2901
2947
  };
2902
2948
  const pendingProjections = [];
2903
2949
  const batchHandlers = /* @__PURE__ */ new Map();
2950
+ let _built = false;
2951
+ const finalizeDeprecations = () => {
2952
+ const deprecationSummary = [];
2953
+ for (const state2 of states.values()) {
2954
+ const eventNames = Object.keys(state2.events);
2955
+ const deprecated = deprecatedEventNames(eventNames);
2956
+ if (deprecated.size === 0) continue;
2957
+ state2._deprecated = deprecated;
2958
+ for (const name of deprecated) {
2959
+ const current = currentVersionOf(name, eventNames);
2960
+ deprecationSummary.push({
2961
+ stateName: state2.name,
2962
+ deprecated: name,
2963
+ current
2964
+ });
2965
+ }
2966
+ for (const [actionName, handler] of Object.entries(state2.on)) {
2967
+ const staticTarget = handler?._staticEmit;
2968
+ if (staticTarget && deprecated.has(staticTarget)) {
2969
+ const current = currentVersionOf(staticTarget, eventNames);
2970
+ throw new Error(
2971
+ `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.`
2972
+ );
2973
+ }
2974
+ }
2975
+ }
2976
+ if (deprecationSummary.length > 0) {
2977
+ const list = deprecationSummary.map(
2978
+ (d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
2979
+ ).join(", ");
2980
+ log().info(
2981
+ `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.`
2982
+ );
2983
+ }
2984
+ };
2904
2985
  const builder = {
2905
2986
  withState: (state2) => {
2906
2987
  registerState(state2, states, registry.actions, registry.events);
@@ -2944,41 +3025,13 @@ function act() {
2944
3025
  }
2945
3026
  }),
2946
3027
  build: (options) => {
2947
- for (const proj of pendingProjections) {
2948
- mergeProjection(proj, registry.events);
2949
- registerBatchHandler(proj, batchHandlers);
2950
- }
2951
- const deprecationSummary = [];
2952
- for (const state2 of states.values()) {
2953
- const eventNames = Object.keys(state2.events);
2954
- const deprecated = deprecatedEventNames(eventNames);
2955
- if (deprecated.size === 0) continue;
2956
- state2._deprecated = deprecated;
2957
- for (const name of deprecated) {
2958
- const current = currentVersionOf(name, eventNames);
2959
- deprecationSummary.push({
2960
- stateName: state2.name,
2961
- deprecated: name,
2962
- current
2963
- });
3028
+ if (!_built) {
3029
+ for (const proj of pendingProjections) {
3030
+ mergeProjection(proj, registry.events);
3031
+ registerBatchHandler(proj, batchHandlers);
2964
3032
  }
2965
- for (const [actionName, handler] of Object.entries(state2.on)) {
2966
- const staticTarget = handler?._staticEmit;
2967
- if (staticTarget && deprecated.has(staticTarget)) {
2968
- const current = currentVersionOf(staticTarget, eventNames);
2969
- throw new Error(
2970
- `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.`
2971
- );
2972
- }
2973
- }
2974
- }
2975
- if (deprecationSummary.length > 0) {
2976
- const list = deprecationSummary.map(
2977
- (d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
2978
- ).join(", ");
2979
- log().info(
2980
- `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.`
2981
- );
3033
+ finalizeDeprecations();
3034
+ _built = true;
2982
3035
  }
2983
3036
  return new Act(
2984
3037
  registry,
@@ -3222,6 +3275,7 @@ function action_builder(state2) {
3222
3275
  log,
3223
3276
  port,
3224
3277
  projection,
3278
+ scoped,
3225
3279
  sleep,
3226
3280
  slice,
3227
3281
  state,