@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.js CHANGED
@@ -15,6 +15,9 @@ import {
15
15
  ZodEmpty
16
16
  } from "./chunk-AGWZY6YT.js";
17
17
 
18
+ // src/ports.ts
19
+ import { AsyncLocalStorage } from "async_hooks";
20
+
18
21
  // src/adapters/console-logger.ts
19
22
  var LEVEL_VALUES = {
20
23
  fatal: 60,
@@ -803,6 +806,7 @@ var InMemoryStore = class {
803
806
  };
804
807
 
805
808
  // src/ports.ts
809
+ var scoped = new AsyncLocalStorage();
806
810
  var ExitCodes = ["ERROR", "EXIT"];
807
811
  var adapters = /* @__PURE__ */ new Map();
808
812
  function port(injector) {
@@ -822,11 +826,17 @@ var log = port(function log2(adapter) {
822
826
  pretty: cfg.env !== "production"
823
827
  });
824
828
  });
825
- var store = port(function store2(adapter) {
826
- return adapter || new InMemoryStore();
829
+ var _store = port(function store(adapter) {
830
+ return adapter ?? new InMemoryStore();
831
+ });
832
+ var store2 = ((adapter) => {
833
+ return scoped.getStore()?.store ?? _store(adapter);
827
834
  });
828
- var cache = port(function cache2(adapter) {
829
- return adapter || new InMemoryCache();
835
+ var _cache = port(function cache(adapter) {
836
+ return adapter ?? new InMemoryCache();
837
+ });
838
+ var cache2 = ((adapter) => {
839
+ return scoped.getStore()?.cache ?? _cache(adapter);
830
840
  });
831
841
  var disposers = [];
832
842
  async function disposeAndExit(code = "EXIT") {
@@ -956,7 +966,7 @@ async function scanStreamHeads(streams) {
956
966
  let maxId = -1;
957
967
  let version = -1;
958
968
  let lastEventName = "";
959
- await store().query(
969
+ await store2().query(
960
970
  (e) => {
961
971
  if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
962
972
  maxId = e.id;
@@ -973,7 +983,7 @@ async function scanStreamHeads(streams) {
973
983
  async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
974
984
  if (reactiveEventsSize === 0) return [...streamInfo.keys()];
975
985
  const pendingSet = /* @__PURE__ */ new Set();
976
- await store().query_streams((position) => {
986
+ await store2().query_streams((position) => {
977
987
  const sourceRe = position.source ? RegExp(position.source) : void 0;
978
988
  for (const [stream, info] of streamInfo) {
979
989
  if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
@@ -1044,13 +1054,13 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1044
1054
  }
1045
1055
  };
1046
1056
  });
1047
- const truncated = await store().truncate(truncTargets);
1057
+ const truncated = await store2().truncate(truncTargets);
1048
1058
  await Promise.all(
1049
1059
  guarded.map(async (stream) => {
1050
1060
  const entry = truncated.get(stream);
1051
1061
  const state2 = seedStates.get(stream);
1052
1062
  if (state2 && entry) {
1053
- await cache().set(stream, {
1063
+ await cache2().set(stream, {
1054
1064
  state: state2,
1055
1065
  version: entry.committed.version,
1056
1066
  event_id: entry.committed.id,
@@ -1058,7 +1068,7 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1058
1068
  snaps: 1
1059
1069
  });
1060
1070
  } else {
1061
- await cache().invalidate(stream);
1071
+ await cache2().invalidate(stream);
1062
1072
  }
1063
1073
  })
1064
1074
  );
@@ -1093,7 +1103,7 @@ var CorrelateCycle = class {
1093
1103
  async init() {
1094
1104
  if (this._initialized) return;
1095
1105
  this._initialized = true;
1096
- const { watermark } = await store().subscribe([...this.staticTargets]);
1106
+ const { watermark } = await store2().subscribe([...this.staticTargets]);
1097
1107
  this._checkpoint = watermark;
1098
1108
  this.onInit?.();
1099
1109
  for (const { stream } of this.staticTargets) {
@@ -1112,7 +1122,7 @@ var CorrelateCycle = class {
1112
1122
  const after = Math.max(this._checkpoint, query.after || -1);
1113
1123
  const correlated = /* @__PURE__ */ new Map();
1114
1124
  let last_id = after;
1115
- await store().query(
1125
+ await store2().query(
1116
1126
  (event) => {
1117
1127
  last_id = event.id;
1118
1128
  const register = this.registry.events[event.name];
@@ -1632,12 +1642,12 @@ var SettleLoop = class {
1632
1642
  };
1633
1643
 
1634
1644
  // src/internal/drain.ts
1635
- var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
1645
+ var claim = (lagging, leading, by, millis) => store2().claim(lagging, leading, by, millis);
1636
1646
  async function fetch(leased, eventLimit) {
1637
1647
  return Promise.all(
1638
1648
  leased.map(async ({ stream, source, at, lagging }) => {
1639
1649
  const events = [];
1640
- await store().query((e) => events.push(e), {
1650
+ await store2().query((e) => events.push(e), {
1641
1651
  stream: source,
1642
1652
  after: at,
1643
1653
  limit: eventLimit
@@ -1646,9 +1656,9 @@ async function fetch(leased, eventLimit) {
1646
1656
  })
1647
1657
  );
1648
1658
  }
1649
- var ack = (leases) => store().ack(leases);
1650
- var block = (leases) => store().block(leases);
1651
- var subscribe = (streams) => store().subscribe(streams);
1659
+ var ack = (leases) => store2().ack(leases);
1660
+ var block = (leases) => store2().block(leases);
1661
+ var subscribe = (streams) => store2().subscribe(streams);
1652
1662
 
1653
1663
  // src/internal/event-sourcing.ts
1654
1664
  import { randomUUID as randomUUID3 } from "crypto";
@@ -1656,7 +1666,7 @@ import { patch } from "@rotorsoft/act-patch";
1656
1666
  async function snap(snapshot) {
1657
1667
  try {
1658
1668
  const { id, stream, name, meta, version } = snapshot.event;
1659
- await store().commit(
1669
+ await store2().commit(
1660
1670
  stream,
1661
1671
  [{ name: SNAP_EVENT, data: snapshot.state }],
1662
1672
  {
@@ -1672,7 +1682,7 @@ async function snap(snapshot) {
1672
1682
  }
1673
1683
  async function tombstone(stream, expectedVersion, correlation) {
1674
1684
  try {
1675
- const [committed] = await store().commit(
1685
+ const [committed] = await store2().commit(
1676
1686
  stream,
1677
1687
  [{ name: TOMBSTONE_EVENT, data: {} }],
1678
1688
  { correlation, causation: {} },
@@ -1686,7 +1696,7 @@ async function tombstone(stream, expectedVersion, correlation) {
1686
1696
  }
1687
1697
  async function load(me, stream, callback, asOf) {
1688
1698
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1689
- const cached = timeTravel ? void 0 : await cache().get(stream);
1699
+ const cached = timeTravel ? void 0 : await cache2().get(stream);
1690
1700
  const cache_hit = !!cached;
1691
1701
  let state2 = cached?.state ?? (me.init ? me.init() : {});
1692
1702
  let patches = cached?.patches ?? 0;
@@ -1694,7 +1704,7 @@ async function load(me, stream, callback, asOf) {
1694
1704
  let version = cached?.version ?? -1;
1695
1705
  let replayed = 0;
1696
1706
  let event;
1697
- await store().query(
1707
+ await store2().query(
1698
1708
  (e) => {
1699
1709
  event = e;
1700
1710
  version = e.version;
@@ -1729,7 +1739,7 @@ async function load(me, stream, callback, asOf) {
1729
1739
  }
1730
1740
  );
1731
1741
  if (replayed > 0 && !timeTravel && event) {
1732
- await cache().set(stream, {
1742
+ await cache2().set(stream, {
1733
1743
  state: state2,
1734
1744
  version,
1735
1745
  event_id: event.id,
@@ -1802,7 +1812,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1802
1812
  };
1803
1813
  let committed;
1804
1814
  try {
1805
- committed = await store().commit(
1815
+ committed = await store2().commit(
1806
1816
  stream,
1807
1817
  emitted,
1808
1818
  meta,
@@ -1814,7 +1824,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1814
1824
  );
1815
1825
  } catch (error) {
1816
1826
  if (error instanceof ConcurrencyError) {
1817
- await cache().invalidate(stream);
1827
+ await cache2().invalidate(stream);
1818
1828
  }
1819
1829
  throw error;
1820
1830
  }
@@ -1836,7 +1846,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1836
1846
  });
1837
1847
  const last = snapshots.at(-1);
1838
1848
  const snapped = me.snap?.(last);
1839
- cache().set(stream, {
1849
+ cache2().set(stream, {
1840
1850
  state: last.state,
1841
1851
  version: last.event.version,
1842
1852
  event_id: last.event.id,
@@ -2031,6 +2041,7 @@ var Act = class {
2031
2041
  this.registry = registry;
2032
2042
  this._states = _states;
2033
2043
  this._batch_handlers = batchHandlers;
2044
+ this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
2034
2045
  this._es = buildEs(this._logger);
2035
2046
  this._cd = buildDrain(this._logger);
2036
2047
  this._handle = buildHandle({
@@ -2076,7 +2087,7 @@ var Act = class {
2076
2087
  },
2077
2088
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2078
2089
  );
2079
- this._notify_disposer = this._wireNotify();
2090
+ this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
2080
2091
  dispose(async () => {
2081
2092
  this._emitter.removeAllListeners();
2082
2093
  this.stop_correlations();
@@ -2145,6 +2156,11 @@ var Act = class {
2145
2156
  _event_to_state;
2146
2157
  /** Logger resolved at construction time (after user port configuration) */
2147
2158
  _logger = log();
2159
+ /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
2160
+ * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
2161
+ * path keeps reading fresh `store()`/`cache()` per call, which matters for
2162
+ * tests that dispose and re-seed mid-suite. */
2163
+ _scoped;
2148
2164
  /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
2149
2165
  * payload (it captures the triggering event for reactingTo auto-inject). */
2150
2166
  _bound_do = this.do.bind(this);
@@ -2160,9 +2176,8 @@ var Act = class {
2160
2176
  * subscription was made). Errors during subscription are logged but
2161
2177
  * never thrown — `notify` is a hint, not a contract.
2162
2178
  */
2163
- async _wireNotify() {
2179
+ async _wireNotify(s) {
2164
2180
  if (this._reactive_events.size === 0) return void 0;
2165
- const s = store();
2166
2181
  if (!s.notify) return void 0;
2167
2182
  try {
2168
2183
  return await s.notify((notification) => {
@@ -2266,35 +2281,39 @@ var Act = class {
2266
2281
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
2267
2282
  */
2268
2283
  async do(action2, target, payload, reactingTo, skipValidation = false) {
2269
- const snapshots = await this._es.action(
2270
- this.registry.actions[action2],
2271
- action2,
2272
- target,
2273
- payload,
2274
- reactingTo,
2275
- skipValidation
2276
- );
2277
- if (this._reactive_events.size > 0) {
2278
- for (const snap2 of snapshots) {
2279
- if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2280
- this._drain.arm();
2281
- break;
2284
+ return this._scoped(async () => {
2285
+ const snapshots = await this._es.action(
2286
+ this.registry.actions[action2],
2287
+ action2,
2288
+ target,
2289
+ payload,
2290
+ reactingTo,
2291
+ skipValidation
2292
+ );
2293
+ if (this._reactive_events.size > 0) {
2294
+ for (const snap2 of snapshots) {
2295
+ if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2296
+ this._drain.arm();
2297
+ break;
2298
+ }
2282
2299
  }
2283
2300
  }
2284
- }
2285
- this.emit("committed", snapshots);
2286
- return snapshots;
2301
+ this.emit("committed", snapshots);
2302
+ return snapshots;
2303
+ });
2287
2304
  }
2288
2305
  async load(stateOrName, stream, callback, asOf) {
2289
- let merged;
2290
- if (typeof stateOrName === "string") {
2291
- const found = this._states.get(stateOrName);
2292
- if (!found) throw new Error(`State "${stateOrName}" not found`);
2293
- merged = found;
2294
- } else {
2295
- merged = this._states.get(stateOrName.name) || stateOrName;
2296
- }
2297
- return await this._es.load(merged, stream, callback, asOf);
2306
+ return this._scoped(async () => {
2307
+ let merged;
2308
+ if (typeof stateOrName === "string") {
2309
+ const found = this._states.get(stateOrName);
2310
+ if (!found) throw new Error(`State "${stateOrName}" not found`);
2311
+ merged = found;
2312
+ } else {
2313
+ merged = this._states.get(stateOrName.name) || stateOrName;
2314
+ }
2315
+ return await this._es.load(merged, stream, callback, asOf);
2316
+ });
2298
2317
  }
2299
2318
  /**
2300
2319
  * Queries the event store for events matching a filter.
@@ -2343,14 +2362,16 @@ var Act = class {
2343
2362
  * @see {@link query_array} for loading events into memory
2344
2363
  */
2345
2364
  async query(query, callback) {
2346
- let first;
2347
- let last;
2348
- const count = await store().query((e) => {
2349
- if (!first) first = e;
2350
- last = e;
2351
- callback?.(e);
2352
- }, query);
2353
- return { first, last, count };
2365
+ return this._scoped(async () => {
2366
+ let first;
2367
+ let last;
2368
+ const count = await store2().query((e) => {
2369
+ if (!first) first = e;
2370
+ last = e;
2371
+ callback?.(e);
2372
+ }, query);
2373
+ return { first, last, count };
2374
+ });
2354
2375
  }
2355
2376
  /**
2356
2377
  * Queries the event store and returns all matching events in memory.
@@ -2379,9 +2400,11 @@ var Act = class {
2379
2400
  * @see {@link query} for large result sets
2380
2401
  */
2381
2402
  async query_array(query) {
2382
- const events = [];
2383
- await store().query((e) => events.push(e), query);
2384
- return events;
2403
+ return this._scoped(async () => {
2404
+ const events = [];
2405
+ await store2().query((e) => events.push(e), query);
2406
+ return events;
2407
+ });
2385
2408
  }
2386
2409
  /**
2387
2410
  * Processes pending reactions by draining uncommitted events from the event store.
@@ -2421,7 +2444,7 @@ var Act = class {
2421
2444
  * @see {@link start_correlations} for automatic correlation
2422
2445
  */
2423
2446
  async drain(options = {}) {
2424
- return this._drain.drain(options);
2447
+ return this._scoped(() => this._drain.drain(options));
2425
2448
  }
2426
2449
  /**
2427
2450
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -2469,7 +2492,7 @@ var Act = class {
2469
2492
  * @see {@link stop_correlations} to stop automatic correlation
2470
2493
  */
2471
2494
  async correlate(query = { after: -1, limit: 10 }) {
2472
- return this._correlate.correlate(query);
2495
+ return this._scoped(() => this._correlate.correlate(query));
2473
2496
  }
2474
2497
  /**
2475
2498
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -2590,9 +2613,11 @@ var Act = class {
2590
2613
  * @see {@link settle} for the debounced full-catch-up loop
2591
2614
  */
2592
2615
  async reset(streams) {
2593
- const count = await store().reset(streams);
2594
- if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2595
- return count;
2616
+ return this._scoped(async () => {
2617
+ const count = await store2().reset(streams);
2618
+ if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2619
+ return count;
2620
+ });
2596
2621
  }
2597
2622
  /**
2598
2623
  * Bulk-update scheduling priority for streams matching `filter`.
@@ -2633,7 +2658,7 @@ var Act = class {
2633
2658
  * @see {@link claim} for how priority biases scheduling
2634
2659
  */
2635
2660
  async prioritize(filter, priority) {
2636
- return store().prioritize(filter, priority);
2661
+ return this._scoped(() => store2().prioritize(filter, priority));
2637
2662
  }
2638
2663
  /**
2639
2664
  * Close the books — guard, archive, truncate, and optionally restart streams.
@@ -2670,16 +2695,18 @@ var Act = class {
2670
2695
  */
2671
2696
  async close(targets) {
2672
2697
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
2673
- await this.correlate({ limit: 1e3 });
2674
- const result = await runCloseCycle(targets, {
2675
- reactiveEventsSize: this._reactive_events.size,
2676
- eventToState: this._event_to_state,
2677
- load: this._es.load,
2678
- tombstone: this._es.tombstone,
2679
- logger: this._logger
2698
+ return this._scoped(async () => {
2699
+ await this.correlate({ limit: 1e3 });
2700
+ const result = await runCloseCycle(targets, {
2701
+ reactiveEventsSize: this._reactive_events.size,
2702
+ eventToState: this._event_to_state,
2703
+ load: this._es.load,
2704
+ tombstone: this._es.tombstone,
2705
+ logger: this._logger
2706
+ });
2707
+ this.emit("closed", result);
2708
+ return result;
2680
2709
  });
2681
- this.emit("closed", result);
2682
- return result;
2683
2710
  }
2684
2711
  /**
2685
2712
  * Debounced, non-blocking correlate→drain cycle.
@@ -2733,6 +2760,41 @@ function act() {
2733
2760
  };
2734
2761
  const pendingProjections = [];
2735
2762
  const batchHandlers = /* @__PURE__ */ new Map();
2763
+ let _built = false;
2764
+ const finalizeDeprecations = () => {
2765
+ const deprecationSummary = [];
2766
+ for (const state2 of states.values()) {
2767
+ const eventNames = Object.keys(state2.events);
2768
+ const deprecated = deprecatedEventNames(eventNames);
2769
+ if (deprecated.size === 0) continue;
2770
+ state2._deprecated = deprecated;
2771
+ for (const name of deprecated) {
2772
+ const current = currentVersionOf(name, eventNames);
2773
+ deprecationSummary.push({
2774
+ stateName: state2.name,
2775
+ deprecated: name,
2776
+ current
2777
+ });
2778
+ }
2779
+ for (const [actionName, handler] of Object.entries(state2.on)) {
2780
+ const staticTarget = handler?._staticEmit;
2781
+ if (staticTarget && deprecated.has(staticTarget)) {
2782
+ const current = currentVersionOf(staticTarget, eventNames);
2783
+ throw new Error(
2784
+ `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.`
2785
+ );
2786
+ }
2787
+ }
2788
+ }
2789
+ if (deprecationSummary.length > 0) {
2790
+ const list = deprecationSummary.map(
2791
+ (d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
2792
+ ).join(", ");
2793
+ log().info(
2794
+ `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.`
2795
+ );
2796
+ }
2797
+ };
2736
2798
  const builder = {
2737
2799
  withState: (state2) => {
2738
2800
  registerState(state2, states, registry.actions, registry.events);
@@ -2776,41 +2838,13 @@ function act() {
2776
2838
  }
2777
2839
  }),
2778
2840
  build: (options) => {
2779
- for (const proj of pendingProjections) {
2780
- mergeProjection(proj, registry.events);
2781
- registerBatchHandler(proj, batchHandlers);
2782
- }
2783
- const deprecationSummary = [];
2784
- for (const state2 of states.values()) {
2785
- const eventNames = Object.keys(state2.events);
2786
- const deprecated = deprecatedEventNames(eventNames);
2787
- if (deprecated.size === 0) continue;
2788
- state2._deprecated = deprecated;
2789
- for (const name of deprecated) {
2790
- const current = currentVersionOf(name, eventNames);
2791
- deprecationSummary.push({
2792
- stateName: state2.name,
2793
- deprecated: name,
2794
- current
2795
- });
2796
- }
2797
- for (const [actionName, handler] of Object.entries(state2.on)) {
2798
- const staticTarget = handler?._staticEmit;
2799
- if (staticTarget && deprecated.has(staticTarget)) {
2800
- const current = currentVersionOf(staticTarget, eventNames);
2801
- throw new Error(
2802
- `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.`
2803
- );
2804
- }
2841
+ if (!_built) {
2842
+ for (const proj of pendingProjections) {
2843
+ mergeProjection(proj, registry.events);
2844
+ registerBatchHandler(proj, batchHandlers);
2805
2845
  }
2806
- }
2807
- if (deprecationSummary.length > 0) {
2808
- const list = deprecationSummary.map(
2809
- (d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
2810
- ).join(", ");
2811
- log().info(
2812
- `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.`
2813
- );
2846
+ finalizeDeprecations();
2847
+ _built = true;
2814
2848
  }
2815
2849
  return new Act(
2816
2850
  registry,
@@ -3045,7 +3079,7 @@ export {
3045
3079
  ValidationError,
3046
3080
  ZodEmpty,
3047
3081
  act,
3048
- cache,
3082
+ cache2 as cache,
3049
3083
  config,
3050
3084
  dispose,
3051
3085
  disposeAndExit,
@@ -3053,10 +3087,11 @@ export {
3053
3087
  log,
3054
3088
  port,
3055
3089
  projection,
3090
+ scoped,
3056
3091
  sleep,
3057
3092
  slice,
3058
3093
  state,
3059
- store,
3094
+ store2 as store,
3060
3095
  validate
3061
3096
  };
3062
3097
  //# sourceMappingURL=index.js.map