@rotorsoft/act 0.37.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];
@@ -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,7 +2256,7 @@ var Act = class {
2244
2256
  },
2245
2257
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2246
2258
  );
2247
- this._notify_disposer = this._wireNotify();
2259
+ this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
2248
2260
  dispose(async () => {
2249
2261
  this._emitter.removeAllListeners();
2250
2262
  this.stop_correlations();
@@ -2313,6 +2325,11 @@ var Act = class {
2313
2325
  _event_to_state;
2314
2326
  /** Logger resolved at construction time (after user port configuration) */
2315
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;
2316
2333
  /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
2317
2334
  * payload (it captures the triggering event for reactingTo auto-inject). */
2318
2335
  _bound_do = this.do.bind(this);
@@ -2328,9 +2345,8 @@ var Act = class {
2328
2345
  * subscription was made). Errors during subscription are logged but
2329
2346
  * never thrown — `notify` is a hint, not a contract.
2330
2347
  */
2331
- async _wireNotify() {
2348
+ async _wireNotify(s) {
2332
2349
  if (this._reactive_events.size === 0) return void 0;
2333
- const s = store();
2334
2350
  if (!s.notify) return void 0;
2335
2351
  try {
2336
2352
  return await s.notify((notification) => {
@@ -2434,35 +2450,39 @@ var Act = class {
2434
2450
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
2435
2451
  */
2436
2452
  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;
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
+ }
2450
2468
  }
2451
2469
  }
2452
- }
2453
- this.emit("committed", snapshots);
2454
- return snapshots;
2470
+ this.emit("committed", snapshots);
2471
+ return snapshots;
2472
+ });
2455
2473
  }
2456
2474
  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);
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
+ });
2466
2486
  }
2467
2487
  /**
2468
2488
  * Queries the event store for events matching a filter.
@@ -2511,14 +2531,16 @@ var Act = class {
2511
2531
  * @see {@link query_array} for loading events into memory
2512
2532
  */
2513
2533
  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 };
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
+ });
2522
2544
  }
2523
2545
  /**
2524
2546
  * Queries the event store and returns all matching events in memory.
@@ -2547,9 +2569,11 @@ var Act = class {
2547
2569
  * @see {@link query} for large result sets
2548
2570
  */
2549
2571
  async query_array(query) {
2550
- const events = [];
2551
- await store().query((e) => events.push(e), query);
2552
- return events;
2572
+ return this._scoped(async () => {
2573
+ const events = [];
2574
+ await store2().query((e) => events.push(e), query);
2575
+ return events;
2576
+ });
2553
2577
  }
2554
2578
  /**
2555
2579
  * Processes pending reactions by draining uncommitted events from the event store.
@@ -2589,7 +2613,7 @@ var Act = class {
2589
2613
  * @see {@link start_correlations} for automatic correlation
2590
2614
  */
2591
2615
  async drain(options = {}) {
2592
- return this._drain.drain(options);
2616
+ return this._scoped(() => this._drain.drain(options));
2593
2617
  }
2594
2618
  /**
2595
2619
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -2637,7 +2661,7 @@ var Act = class {
2637
2661
  * @see {@link stop_correlations} to stop automatic correlation
2638
2662
  */
2639
2663
  async correlate(query = { after: -1, limit: 10 }) {
2640
- return this._correlate.correlate(query);
2664
+ return this._scoped(() => this._correlate.correlate(query));
2641
2665
  }
2642
2666
  /**
2643
2667
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -2758,9 +2782,11 @@ var Act = class {
2758
2782
  * @see {@link settle} for the debounced full-catch-up loop
2759
2783
  */
2760
2784
  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;
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
+ });
2764
2790
  }
2765
2791
  /**
2766
2792
  * Bulk-update scheduling priority for streams matching `filter`.
@@ -2801,7 +2827,7 @@ var Act = class {
2801
2827
  * @see {@link claim} for how priority biases scheduling
2802
2828
  */
2803
2829
  async prioritize(filter, priority) {
2804
- return store().prioritize(filter, priority);
2830
+ return this._scoped(() => store2().prioritize(filter, priority));
2805
2831
  }
2806
2832
  /**
2807
2833
  * Close the books — guard, archive, truncate, and optionally restart streams.
@@ -2838,16 +2864,18 @@ var Act = class {
2838
2864
  */
2839
2865
  async close(targets) {
2840
2866
  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
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;
2848
2878
  });
2849
- this.emit("closed", result);
2850
- return result;
2851
2879
  }
2852
2880
  /**
2853
2881
  * Debounced, non-blocking correlate→drain cycle.
@@ -2901,6 +2929,41 @@ function act() {
2901
2929
  };
2902
2930
  const pendingProjections = [];
2903
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
+ };
2904
2967
  const builder = {
2905
2968
  withState: (state2) => {
2906
2969
  registerState(state2, states, registry.actions, registry.events);
@@ -2944,41 +3007,13 @@ function act() {
2944
3007
  }
2945
3008
  }),
2946
3009
  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
- });
2964
- }
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
- }
3010
+ if (!_built) {
3011
+ for (const proj of pendingProjections) {
3012
+ mergeProjection(proj, registry.events);
3013
+ registerBatchHandler(proj, batchHandlers);
2973
3014
  }
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
- );
3015
+ finalizeDeprecations();
3016
+ _built = true;
2982
3017
  }
2983
3018
  return new Act(
2984
3019
  registry,
@@ -3222,6 +3257,7 @@ function action_builder(state2) {
3222
3257
  log,
3223
3258
  port,
3224
3259
  projection,
3260
+ scoped,
3225
3261
  sleep,
3226
3262
  slice,
3227
3263
  state,