@rotorsoft/act 1.3.0 → 1.5.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.
Files changed (36) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +28 -3
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  5. package/dist/@types/internal/correlate-cycle.d.ts +6 -6
  6. package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
  7. package/dist/@types/internal/drain-cycle.d.ts +1 -1
  8. package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
  9. package/dist/@types/internal/event-sourcing.d.ts +11 -21
  10. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  11. package/dist/@types/internal/lru-map.d.ts +2 -2
  12. package/dist/@types/internal/lru-map.d.ts.map +1 -1
  13. package/dist/@types/internal/settle.d.ts +3 -5
  14. package/dist/@types/internal/settle.d.ts.map +1 -1
  15. package/dist/@types/types/action.d.ts +21 -5
  16. package/dist/@types/types/action.d.ts.map +1 -1
  17. package/dist/@types/types/errors.d.ts +4 -30
  18. package/dist/@types/types/errors.d.ts.map +1 -1
  19. package/dist/{chunk-XKCTGUW2.js → chunk-I4L224TZ.js} +15 -12
  20. package/dist/chunk-I4L224TZ.js.map +1 -0
  21. package/dist/{chunk-TZWDSNSN.js → chunk-PMAZTOSO.js} +31 -5
  22. package/dist/chunk-PMAZTOSO.js.map +1 -0
  23. package/dist/index.cjs +281 -246
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.js +240 -234
  26. package/dist/index.js.map +1 -1
  27. package/dist/test/index.cjs +34 -16
  28. package/dist/test/index.cjs.map +1 -1
  29. package/dist/test/index.js +7 -6
  30. package/dist/test/index.js.map +1 -1
  31. package/dist/types/index.cjs +30 -4
  32. package/dist/types/index.cjs.map +1 -1
  33. package/dist/types/index.js +1 -1
  34. package/package.json +2 -2
  35. package/dist/chunk-TZWDSNSN.js.map +0 -1
  36. package/dist/chunk-XKCTGUW2.js.map +0 -1
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  sleep,
20
20
  store,
21
21
  validate
22
- } from "./chunk-XKCTGUW2.js";
22
+ } from "./chunk-I4L224TZ.js";
23
23
  import {
24
24
  ActorSchema,
25
25
  CausationEventSchema,
@@ -36,7 +36,7 @@ import {
36
36
  TargetSchema,
37
37
  ValidationError,
38
38
  ZodEmpty
39
- } from "./chunk-TZWDSNSN.js";
39
+ } from "./chunk-PMAZTOSO.js";
40
40
  import "./chunk-5WRI5ZAA.js";
41
41
 
42
42
  // src/signals.ts
@@ -681,18 +681,23 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
681
681
 
682
682
  // src/internal/correlate-cycle.ts
683
683
  var CorrelateCycle = class {
684
- constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
685
- this.registry = registry;
686
- this.staticTargets = staticTargets;
687
- this.hasDynamicResolvers = hasDynamicResolvers;
688
- this.cd = cd;
689
- this.onInit = onInit;
690
- this._subscribed = new LruSet(maxSubscribedStreams);
691
- }
692
684
  _checkpoint = -1;
693
685
  _initialized = false;
694
686
  _timer = void 0;
695
687
  _subscribed;
688
+ _registry;
689
+ _static_targets;
690
+ _has_dynamic_resolvers;
691
+ _cd;
692
+ _on_init;
693
+ constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
694
+ this._subscribed = new LruSet(maxSubscribedStreams);
695
+ this._registry = registry;
696
+ this._static_targets = staticTargets;
697
+ this._has_dynamic_resolvers = hasDynamicResolvers;
698
+ this._cd = cd;
699
+ this._on_init = onInit;
700
+ }
696
701
  /** Last correlated event id. */
697
702
  get checkpoint() {
698
703
  return this._checkpoint;
@@ -707,10 +712,10 @@ var CorrelateCycle = class {
707
712
  async init() {
708
713
  if (this._initialized) return;
709
714
  this._initialized = true;
710
- const { watermark } = await store().subscribe([...this.staticTargets]);
715
+ const { watermark } = await store().subscribe([...this._static_targets]);
711
716
  this._checkpoint = watermark;
712
- this.onInit?.();
713
- for (const { stream } of this.staticTargets) {
717
+ this._on_init?.();
718
+ for (const { stream } of this._static_targets) {
714
719
  this._subscribed.add(stream);
715
720
  }
716
721
  }
@@ -721,7 +726,7 @@ var CorrelateCycle = class {
721
726
  */
722
727
  async correlate(query = { after: -1, limit: 10 }) {
723
728
  await this.init();
724
- if (!this.hasDynamicResolvers)
729
+ if (!this._has_dynamic_resolvers)
725
730
  return { subscribed: 0, last_id: this._checkpoint };
726
731
  const after = Math.max(this._checkpoint, query.after || -1);
727
732
  const correlated = /* @__PURE__ */ new Map();
@@ -729,7 +734,7 @@ var CorrelateCycle = class {
729
734
  await store().query(
730
735
  (event) => {
731
736
  last_id = event.id;
732
- const register = this.registry.events[event.name];
737
+ const register = this._registry.events[event.name];
733
738
  if (register) {
734
739
  for (const reaction of register.reactions.values()) {
735
740
  if (typeof reaction.resolver !== "function") continue;
@@ -765,7 +770,7 @@ var CorrelateCycle = class {
765
770
  lane
766
771
  })
767
772
  );
768
- const { subscribed } = await this.cd.subscribe(streams);
773
+ const { subscribed } = await this._cd.subscribe(streams);
769
774
  this._checkpoint = last_id;
770
775
  if (subscribed) {
771
776
  for (const { stream } of streams) {
@@ -868,51 +873,7 @@ var subscribe = (streams) => store().subscribe(streams);
868
873
 
869
874
  // src/internal/event-sourcing.ts
870
875
  import { patch } from "@rotorsoft/act-patch";
871
- async function* iterate(source, filter) {
872
- const state2 = {
873
- slot: null,
874
- onProduce: null,
875
- onConsume: null,
876
- done: false,
877
- error: void 0
878
- };
879
- const wakeProduce = () => {
880
- const fn = state2.onProduce;
881
- state2.onProduce = null;
882
- if (fn) fn();
883
- };
884
- void source.query((event) => {
885
- state2.slot = event;
886
- wakeProduce();
887
- return new Promise((resolve) => {
888
- state2.onConsume = () => resolve();
889
- });
890
- }, filter).then(
891
- () => {
892
- state2.done = true;
893
- wakeProduce();
894
- },
895
- (err) => {
896
- state2.error = err;
897
- state2.done = true;
898
- wakeProduce();
899
- }
900
- );
901
- while (true) {
902
- if (state2.slot === null && !state2.done)
903
- await new Promise((resolve) => {
904
- state2.onProduce = resolve;
905
- });
906
- if (state2.error) throw state2.error;
907
- if (state2.slot === null) return;
908
- const event = state2.slot;
909
- state2.slot = null;
910
- const fn = state2.onConsume;
911
- state2.onConsume = null;
912
- fn();
913
- yield event;
914
- }
915
- }
876
+ var DEFAULT_BATCH = 500;
916
877
  async function snap(snapshot) {
917
878
  try {
918
879
  const { id, stream, name, meta, version } = snapshot.event;
@@ -944,7 +905,7 @@ async function tombstone(stream, expectedVersion, correlation) {
944
905
  throw error;
945
906
  }
946
907
  }
947
- function isValid(event) {
908
+ function is_valid(event) {
948
909
  if (event.version < 0) return false;
949
910
  if (!(event.created instanceof Date) || Number.isNaN(event.created.getTime()))
950
911
  return false;
@@ -952,48 +913,70 @@ function isValid(event) {
952
913
  }
953
914
  async function scan(source, opts = {}, callback) {
954
915
  const { drop_snapshots = false, on_progress } = opts;
955
- const idMap = /* @__PURE__ */ new Map();
916
+ const limit = opts.batch_size ?? DEFAULT_BATCH;
917
+ const id_map = /* @__PURE__ */ new Map();
956
918
  let kept = 0;
957
- let droppedSnapshots = 0;
919
+ let dropped_snaps = 0;
958
920
  let processed = 0;
959
- for await (const event of iterate(source)) {
960
- processed++;
961
- if (!isValid(event)) throw new Error(`Invalid event at index ${processed}`);
962
- if (on_progress) on_progress({ processed });
963
- if (drop_snapshots && event.name === SNAP_EVENT) {
964
- droppedSnapshots++;
965
- continue;
966
- }
967
- if (!callback) {
968
- kept++;
969
- continue;
970
- }
971
- let remapped = event;
972
- const causedBy = event.meta.causation.event?.id;
973
- if (causedBy !== void 0) {
974
- const newCausedBy = idMap.get(causedBy);
975
- if (newCausedBy !== void 0 && newCausedBy !== causedBy) {
976
- remapped = {
977
- ...event,
978
- meta: {
979
- ...event.meta,
980
- causation: {
981
- ...event.meta.causation,
982
- event: { ...event.meta.causation.event, id: newCausedBy }
983
- }
921
+ let at;
922
+ let max_id;
923
+ const probed = await source.query(
924
+ (e) => {
925
+ max_id = e.id;
926
+ },
927
+ { backward: true, limit: 1 }
928
+ );
929
+ if (probed !== 1) max_id = void 0;
930
+ while (true) {
931
+ let got = 0;
932
+ let id;
933
+ await source.query(
934
+ async (event) => {
935
+ got++;
936
+ id = event.id;
937
+ processed++;
938
+ if (!is_valid(event))
939
+ throw new Error(`Invalid event at index ${processed}`);
940
+ if (on_progress) on_progress({ processed, id: event.id, max_id });
941
+ if (drop_snapshots && event.name === SNAP_EVENT) {
942
+ dropped_snaps++;
943
+ return;
944
+ }
945
+ if (!callback) {
946
+ kept++;
947
+ return;
948
+ }
949
+ let remapped = event;
950
+ const caused_by = event.meta.causation.event?.id;
951
+ if (caused_by !== void 0) {
952
+ const new_caused_by = id_map.get(caused_by);
953
+ if (new_caused_by !== void 0 && new_caused_by !== caused_by) {
954
+ remapped = {
955
+ ...event,
956
+ meta: {
957
+ ...event.meta,
958
+ causation: {
959
+ ...event.meta.causation,
960
+ event: { ...event.meta.causation.event, id: new_caused_by }
961
+ }
962
+ }
963
+ };
984
964
  }
985
- };
986
- }
987
- }
988
- const newId = await callback(remapped);
989
- idMap.set(event.id, newId);
990
- kept++;
965
+ }
966
+ const new_id = await callback(remapped);
967
+ id_map.set(event.id, new_id);
968
+ kept++;
969
+ },
970
+ { after: at, limit }
971
+ );
972
+ if (got !== limit) break;
973
+ at = id;
991
974
  }
992
975
  return {
993
976
  kept,
994
977
  dropped: {
995
978
  closed_streams: 0,
996
- snapshots: droppedSnapshots,
979
+ snapshots: dropped_snaps,
997
980
  empty_streams: 0
998
981
  }
999
982
  };
@@ -1411,9 +1394,6 @@ var EMPTY_DRAIN = {
1411
1394
  blocked: []
1412
1395
  };
1413
1396
  var DrainController = class {
1414
- constructor(deps) {
1415
- this.deps = deps;
1416
- }
1417
1397
  _armed = false;
1418
1398
  _locked = false;
1419
1399
  _ratio = 0.5;
@@ -1429,6 +1409,10 @@ var DrainController = class {
1429
1409
  /** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
1430
1410
  _worker;
1431
1411
  _stopped = false;
1412
+ _deps;
1413
+ constructor(deps) {
1414
+ this._deps = deps;
1415
+ }
1432
1416
  /**
1433
1417
  * Signal that a commit (or reset / cold-start) may have produced work.
1434
1418
  * Subsequent `drain()` calls will run the pipeline; once the pipeline
@@ -1469,7 +1453,7 @@ var DrainController = class {
1469
1453
  }
1470
1454
  /** Lane this controller drains (undefined = legacy single-lane span). */
1471
1455
  get lane() {
1472
- return this.deps.lane;
1456
+ return this._deps.lane;
1473
1457
  }
1474
1458
  /**
1475
1459
  * Start a per-lane worker that drains at the lane's `cycleMs`
@@ -1503,7 +1487,7 @@ var DrainController = class {
1503
1487
  async drain(options = {}) {
1504
1488
  if (!this._armed) return EMPTY_DRAIN;
1505
1489
  if (this._locked) return EMPTY_DRAIN;
1506
- const d = this.deps.defaults ?? {};
1490
+ const d = this._deps.defaults ?? {};
1507
1491
  const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
1508
1492
  const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
1509
1493
  const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
@@ -1512,24 +1496,24 @@ var DrainController = class {
1512
1496
  const lagging = Math.ceil(streamLimit * this._ratio);
1513
1497
  const leading = streamLimit - lagging;
1514
1498
  const cycle = await runDrainCycle(
1515
- this.deps.ops,
1516
- this.deps.registry,
1517
- this.deps.batchHandlers,
1518
- this.deps.handle,
1519
- this.deps.handleBatch,
1499
+ this._deps.ops,
1500
+ this._deps.registry,
1501
+ this._deps.batchHandlers,
1502
+ this._deps.handle,
1503
+ this._deps.handleBatch,
1520
1504
  lagging,
1521
1505
  leading,
1522
1506
  eventLimit,
1523
1507
  leaseMillis,
1524
1508
  this._backoff.size > 0 ? this.isDeferred : void 0,
1525
- this.deps.lane
1509
+ this._deps.lane
1526
1510
  );
1527
1511
  if (!cycle) {
1528
1512
  this._armed = false;
1529
1513
  return EMPTY_DRAIN;
1530
1514
  }
1531
1515
  const { leased, fetched, handled, acked, blocked } = cycle;
1532
- traceCycle(this.deps.logger, leased, fetched, handled, acked, blocked);
1516
+ traceCycle(this._deps.logger, leased, fetched, handled, acked, blocked);
1533
1517
  this._ratio = computeLagLeadRatio(handled, lagging, leading);
1534
1518
  for (const lease of acked) this._backoff.delete(lease.stream);
1535
1519
  for (const lease of blocked) this._backoff.delete(lease.stream);
@@ -1539,13 +1523,13 @@ var DrainController = class {
1539
1523
  }
1540
1524
  }
1541
1525
  if (this._backoff.size > 0) this.scheduleBackoffWake();
1542
- if (acked.length) this.deps.onAcked(acked);
1543
- if (blocked.length) this.deps.onBlocked(blocked);
1526
+ if (acked.length) this._deps.onAcked(acked);
1527
+ if (blocked.length) this._deps.onBlocked(blocked);
1544
1528
  const hasErrors = handled.some(({ error }) => error);
1545
1529
  if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
1546
1530
  return { fetched, leased, acked, blocked };
1547
1531
  } catch (error) {
1548
- this.deps.logger.error(error);
1532
+ this._deps.logger.error(error);
1549
1533
  return EMPTY_DRAIN;
1550
1534
  } finally {
1551
1535
  this._locked = false;
@@ -1806,12 +1790,15 @@ function buildHandleBatch(logger) {
1806
1790
 
1807
1791
  // src/internal/settle.ts
1808
1792
  var SettleLoop = class {
1809
- constructor(deps, defaultDebounceMs) {
1810
- this.deps = deps;
1811
- this.defaultDebounceMs = defaultDebounceMs;
1812
- }
1813
1793
  _timer = void 0;
1814
1794
  _running = false;
1795
+ _deps;
1796
+ /** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
1797
+ _default_debounce_ms;
1798
+ constructor(deps, defaultDebounceMs) {
1799
+ this._deps = deps;
1800
+ this._default_debounce_ms = defaultDebounceMs;
1801
+ }
1815
1802
  /**
1816
1803
  * Schedule a settle pass. Multiple calls inside the debounce window
1817
1804
  * coalesce into one cycle. The cycle runs correlate→drain in a loop
@@ -1821,7 +1808,7 @@ var SettleLoop = class {
1821
1808
  */
1822
1809
  schedule(options = {}) {
1823
1810
  const {
1824
- debounceMs = this.defaultDebounceMs,
1811
+ debounceMs = this._default_debounce_ms,
1825
1812
  correlate: correlateQuery = { after: -1, limit: 100 },
1826
1813
  maxPasses = Infinity,
1827
1814
  ...drainOptions
@@ -1832,19 +1819,19 @@ var SettleLoop = class {
1832
1819
  if (this._running) return;
1833
1820
  this._running = true;
1834
1821
  (async () => {
1835
- await this.deps.init();
1822
+ await this._deps.init();
1836
1823
  let lastDrain;
1837
1824
  for (let i = 0; i < maxPasses; i++) {
1838
- const { subscribed } = await this.deps.correlate({
1825
+ const { subscribed } = await this._deps.correlate({
1839
1826
  ...correlateQuery,
1840
- after: this.deps.checkpoint()
1827
+ after: this._deps.checkpoint()
1841
1828
  });
1842
- lastDrain = await this.deps.drain(drainOptions);
1829
+ lastDrain = await this._deps.drain(drainOptions);
1843
1830
  const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
1844
1831
  if (!made_progress) break;
1845
1832
  }
1846
- if (lastDrain) this.deps.onSettled(lastDrain);
1847
- })().catch((err) => this.deps.logger.error(err)).finally(() => {
1833
+ if (lastDrain) this._deps.onSettled(lastDrain);
1834
+ })().catch((err) => this._deps.logger.error(err)).finally(() => {
1848
1835
  this._running = false;
1849
1836
  });
1850
1837
  }, debounceMs);
@@ -1862,6 +1849,117 @@ var SettleLoop = class {
1862
1849
  var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
1863
1850
  var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
1864
1851
  var Act = class {
1852
+ _emitter = new EventEmitter();
1853
+ /** #803: gate the `Store.notify` subscription side. */
1854
+ _listen;
1855
+ /** #803: gate the local reaction pipeline (drain controllers, settle, correlate). */
1856
+ _drain;
1857
+ /** Event names with at least one registered reaction (computed at build time) */
1858
+ _reactive_events;
1859
+ /** One DrainController per active lane, keyed by lane name. */
1860
+ _drain_controllers;
1861
+ /** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
1862
+ _correlate;
1863
+ /** Debounced correlate→drain catch-up loop. */
1864
+ _settle;
1865
+ /**
1866
+ * Disposer for the cross-process notify subscription, set up eagerly
1867
+ * during construction. Held as a promise because the subscription
1868
+ * itself may be async (the PG adapter checks out a dedicated client
1869
+ * and runs `LISTEN` before resolving). Resolves to `undefined` when
1870
+ * the store doesn't implement `notify` or there are no registered
1871
+ * reactions.
1872
+ *
1873
+ * **Contract:** the configured store must be injected via
1874
+ * {@link store}`(adapter)` *before* calling `act()...build()`. The
1875
+ * orchestrator wires notify against whatever store is current at
1876
+ * construction time — late injection after build is unsupported.
1877
+ */
1878
+ _notify_disposer;
1879
+ /** Public registry — kept as-is per the no-prefix-on-public convention. */
1880
+ registry;
1881
+ /** Map of state name → state definition; populated by the builder. */
1882
+ _states;
1883
+ /**
1884
+ * Emit a lifecycle event. The payload type is inferred from the event name
1885
+ * via {@link ActLifecycleEvents}.
1886
+ */
1887
+ emit(event, args) {
1888
+ return this._emitter.emit(event, args);
1889
+ }
1890
+ /**
1891
+ * Register a listener for a lifecycle event. The listener receives the
1892
+ * event-specific payload.
1893
+ */
1894
+ on(event, listener) {
1895
+ this._emitter.on(event, listener);
1896
+ return this;
1897
+ }
1898
+ /**
1899
+ * Remove a previously registered lifecycle listener.
1900
+ */
1901
+ off(event, listener) {
1902
+ this._emitter.off(event, listener);
1903
+ return this;
1904
+ }
1905
+ /** Batch handlers for static-target projections (target → handler) */
1906
+ _batch_handlers;
1907
+ /** Event-sourcing handlers, optionally wrapped with trace decorators */
1908
+ _es;
1909
+ /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
1910
+ _cd;
1911
+ /**
1912
+ * Event-name → owning state, computed at build time. The duplicate-event
1913
+ * guard in merge.ts ensures one event name maps to at most one state, so
1914
+ * this lookup is unambiguous. Used by `close()` to pick the right reducer
1915
+ * set when seeding a `restart` snapshot in multi-state apps.
1916
+ */
1917
+ _event_to_state;
1918
+ /**
1919
+ * Event-name → lane fan-in for selective arming (ACT-1103). Built by
1920
+ * `classifyRegistry` once per build. `"all"` means at least one of
1921
+ * the event's reactions is a dynamic resolver (lane opaque until
1922
+ * runtime); a `Set<string>` lists the static lanes only that event's
1923
+ * reactions target.
1924
+ */
1925
+ _event_to_lanes;
1926
+ /**
1927
+ * Audit dependency bag (#723). Built once at construction; held as
1928
+ * an immutable snapshot of the registry state the audit module
1929
+ * needs. Lives in `internal/audit.ts` — this orchestrator never
1930
+ * carries audit logic, only the deps + a one-liner that hands them
1931
+ * over.
1932
+ */
1933
+ _audit_deps;
1934
+ /** Logger resolved at construction time (after user port configuration) */
1935
+ _logger = log();
1936
+ /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
1937
+ * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
1938
+ * path keeps reading fresh `store()`/`cache()` per call, which matters for
1939
+ * tests that dispose and re-seed mid-suite. */
1940
+ _scoped;
1941
+ /**
1942
+ * Correlation-id generator for originating actions. Bound at
1943
+ * construction from `options.correlator ?? defaultCorrelator`. The
1944
+ * `do()` path passes this into the `_es.action` closure; close-cycle
1945
+ * uses it via {@link closeCorrelation}.
1946
+ */
1947
+ _correlator;
1948
+ /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
1949
+ * payload (it captures the triggering event for reactingTo auto-inject). */
1950
+ _bound_do = this.do.bind(this);
1951
+ _bound_load = this.load.bind(this);
1952
+ _bound_query = this.query.bind(this);
1953
+ _bound_query_array = this.query_array.bind(this);
1954
+ /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
1955
+ _handle;
1956
+ _handle_batch;
1957
+ /** Declared drain lanes (ACT-1103). */
1958
+ _lanes;
1959
+ /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
1960
+ get lanes() {
1961
+ return this._lanes;
1962
+ }
1865
1963
  /**
1866
1964
  * Create a new Act orchestrator. Prefer the {@link act} builder over
1867
1965
  * direct construction — `act()...build()` wires the registry, merges
@@ -1876,9 +1974,9 @@ var Act = class {
1876
1974
  * these from `.withLane(...)` calls. Slice 1 records them on the
1877
1975
  * instance; later slices fan out one `DrainController` per lane.
1878
1976
  */
1879
- constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
1977
+ constructor(registry, states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
1880
1978
  this.registry = registry;
1881
- this._states = _states;
1979
+ this._states = states;
1882
1980
  this._batch_handlers = batchHandlers;
1883
1981
  this._lanes = lanes;
1884
1982
  if (options.onlyLanes && options.onlyLanes.length > 0) {
@@ -1912,6 +2010,8 @@ var Act = class {
1912
2010
  eventToLanes
1913
2011
  } = classifyRegistry(this.registry, this._states);
1914
2012
  this._reactive_events = reactiveEvents;
2013
+ this._listen = options.listen !== false;
2014
+ this._drain = options.drain !== false;
1915
2015
  this._event_to_state = eventToState;
1916
2016
  this._event_to_lanes = eventToLanes;
1917
2017
  const allLanes = ["default", ...lanes.map((l) => l.name)];
@@ -1939,7 +2039,8 @@ var Act = class {
1939
2039
  leaseMillis: cfg.leaseMillis
1940
2040
  }
1941
2041
  });
1942
- if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
2042
+ if (cfg?.cycleMs !== void 0 && options.drain !== false)
2043
+ controller.start(cfg.cycleMs);
1943
2044
  this._drain_controllers.set(name, controller);
1944
2045
  }
1945
2046
  this._audit_deps = {
@@ -1957,9 +2058,10 @@ var Act = class {
1957
2058
  hasDynamicResolvers,
1958
2059
  this._cd,
1959
2060
  options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
1960
- // Cold start: assume drain is needed (historical events may need processing)
2061
+ // Cold start: assume drain is needed (historical events may need processing).
2062
+ // #803: writer-only instances skip the cold-start arm.
1961
2063
  () => {
1962
- if (this._reactive_events.size > 0) this._armAll();
2064
+ if (this._drain && this._reactive_events.size > 0) this._armAll();
1963
2065
  }
1964
2066
  );
1965
2067
  this._settle = new SettleLoop(
@@ -1976,109 +2078,6 @@ var Act = class {
1976
2078
  this._notify_disposer = this._wireNotify(options.scoped?.store ?? store());
1977
2079
  dispose(() => this.shutdown());
1978
2080
  }
1979
- _emitter = new EventEmitter();
1980
- /** Event names with at least one registered reaction (computed at build time) */
1981
- _reactive_events;
1982
- /** One DrainController per active lane, keyed by lane name. */
1983
- _drain_controllers;
1984
- /** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
1985
- _correlate;
1986
- /** Debounced correlate→drain catch-up loop. */
1987
- _settle;
1988
- /**
1989
- * Disposer for the cross-process notify subscription, set up eagerly
1990
- * during construction. Held as a promise because the subscription
1991
- * itself may be async (the PG adapter checks out a dedicated client
1992
- * and runs `LISTEN` before resolving). Resolves to `undefined` when
1993
- * the store doesn't implement `notify` or there are no registered
1994
- * reactions.
1995
- *
1996
- * **Contract:** the configured store must be injected via
1997
- * {@link store}`(adapter)` *before* calling `act()...build()`. The
1998
- * orchestrator wires notify against whatever store is current at
1999
- * construction time — late injection after build is unsupported.
2000
- */
2001
- _notify_disposer;
2002
- /**
2003
- * Emit a lifecycle event. The payload type is inferred from the event name
2004
- * via {@link ActLifecycleEvents}.
2005
- */
2006
- emit(event, args) {
2007
- return this._emitter.emit(event, args);
2008
- }
2009
- /**
2010
- * Register a listener for a lifecycle event. The listener receives the
2011
- * event-specific payload.
2012
- */
2013
- on(event, listener) {
2014
- this._emitter.on(event, listener);
2015
- return this;
2016
- }
2017
- /**
2018
- * Remove a previously registered lifecycle listener.
2019
- */
2020
- off(event, listener) {
2021
- this._emitter.off(event, listener);
2022
- return this;
2023
- }
2024
- /** Batch handlers for static-target projections (target → handler) */
2025
- _batch_handlers;
2026
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
2027
- _es;
2028
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
2029
- _cd;
2030
- /**
2031
- * Event-name → owning state, computed at build time. The duplicate-event
2032
- * guard in merge.ts ensures one event name maps to at most one state, so
2033
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
2034
- * set when seeding a `restart` snapshot in multi-state apps.
2035
- */
2036
- _event_to_state;
2037
- /**
2038
- * Event-name → lane fan-in for selective arming (ACT-1103). Built by
2039
- * `classifyRegistry` once per build. `"all"` means at least one of
2040
- * the event's reactions is a dynamic resolver (lane opaque until
2041
- * runtime); a `Set<string>` lists the static lanes only that event's
2042
- * reactions target.
2043
- */
2044
- _event_to_lanes;
2045
- /**
2046
- * Audit dependency bag (#723). Built once at construction; held as
2047
- * an immutable snapshot of the registry state the audit module
2048
- * needs. Lives in `internal/audit.ts` — this orchestrator never
2049
- * carries audit logic, only the deps + a one-liner that hands them
2050
- * over.
2051
- */
2052
- _audit_deps;
2053
- /** Logger resolved at construction time (after user port configuration) */
2054
- _logger = log();
2055
- /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
2056
- * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
2057
- * path keeps reading fresh `store()`/`cache()` per call, which matters for
2058
- * tests that dispose and re-seed mid-suite. */
2059
- _scoped;
2060
- /**
2061
- * Correlation-id generator for originating actions. Bound at
2062
- * construction from `options.correlator ?? defaultCorrelator`. The
2063
- * `do()` path passes this into the `_es.action` closure; close-cycle
2064
- * uses it via {@link closeCorrelation}.
2065
- */
2066
- _correlator;
2067
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
2068
- * payload (it captures the triggering event for reactingTo auto-inject). */
2069
- _bound_do = this.do.bind(this);
2070
- _bound_load = this.load.bind(this);
2071
- _bound_query = this.query.bind(this);
2072
- _bound_query_array = this.query_array.bind(this);
2073
- /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
2074
- _handle;
2075
- _handle_batch;
2076
- /** Declared drain lanes (ACT-1103). */
2077
- _lanes;
2078
- /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
2079
- get lanes() {
2080
- return this._lanes;
2081
- }
2082
2081
  /** True after the first `shutdown()` call. Guards idempotency. */
2083
2082
  _shutdown_promise;
2084
2083
  /**
@@ -2113,14 +2112,17 @@ var Act = class {
2113
2112
  async _wireNotify(s) {
2114
2113
  if (this._reactive_events.size === 0) return void 0;
2115
2114
  if (!s.notify) return void 0;
2115
+ if (!this._listen) return void 0;
2116
2116
  try {
2117
2117
  return await s.notify((notification) => {
2118
2118
  try {
2119
2119
  this.emit("notified", notification);
2120
- const armed = this._armForEventNames(
2121
- notification.events.map((e) => e.name)
2122
- );
2123
- if (armed) this._settle.schedule({ debounceMs: 0 });
2120
+ if (this._drain) {
2121
+ const armed = this._armForEventNames(
2122
+ notification.events.map((e) => e.name)
2123
+ );
2124
+ if (armed) this._settle.schedule({ debounceMs: 0 });
2125
+ }
2124
2126
  } catch (err) {
2125
2127
  this._logger.error(err, "notified handler threw");
2126
2128
  }
@@ -2371,6 +2373,8 @@ var Act = class {
2371
2373
  * @see {@link start_correlations} for automatic correlation
2372
2374
  */
2373
2375
  async drain(options = {}) {
2376
+ if (!this._drain)
2377
+ return { fetched: [], leased: [], acked: [], blocked: [] };
2374
2378
  return this._scoped(() => this._drainAll(options));
2375
2379
  }
2376
2380
  /** Arm every active lane controller (ACT-1103). */
@@ -2471,6 +2475,7 @@ var Act = class {
2471
2475
  * @see {@link stop_correlations} to stop automatic correlation
2472
2476
  */
2473
2477
  async correlate(query = { after: -1, limit: 10 }) {
2478
+ if (!this._drain) return { subscribed: 0, last_id: -1 };
2474
2479
  return this._scoped(() => this._correlate.correlate(query));
2475
2480
  }
2476
2481
  /**
@@ -2896,6 +2901,7 @@ var Act = class {
2896
2901
  * @see {@link correlate} for manual correlation
2897
2902
  */
2898
2903
  settle(options = {}) {
2904
+ if (!this._drain) return;
2899
2905
  this._settle.schedule(options);
2900
2906
  }
2901
2907
  };