@rotorsoft/act 1.4.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.
package/dist/index.cjs CHANGED
@@ -204,10 +204,11 @@ var ConsoleLogger = class _ConsoleLogger {
204
204
 
205
205
  // src/internal/lru-map.ts
206
206
  var LruMap = class {
207
- constructor(_maxSize) {
208
- this._maxSize = _maxSize;
209
- }
210
207
  _entries = /* @__PURE__ */ new Map();
208
+ _max_size;
209
+ constructor(maxSize) {
210
+ this._max_size = maxSize;
211
+ }
211
212
  get(key) {
212
213
  const v = this._entries.get(key);
213
214
  if (v === void 0) return void 0;
@@ -220,7 +221,7 @@ var LruMap = class {
220
221
  }
221
222
  set(key, value) {
222
223
  this._entries.delete(key);
223
- if (this._entries.size >= this._maxSize) {
224
+ if (this._entries.size >= this._max_size) {
224
225
  const oldest = this._entries.keys().next().value;
225
226
  this._entries.delete(oldest);
226
227
  }
@@ -302,44 +303,70 @@ var Errors = {
302
303
  NonRetryableError: "ERR_NON_RETRYABLE"
303
304
  };
304
305
  var ValidationError = class extends Error {
306
+ /** The type of target being validated (e.g., "action", "event") */
307
+ target;
308
+ /** The invalid payload that failed validation */
309
+ payload;
310
+ /** Zod validation error details */
311
+ details;
305
312
  constructor(target, payload, details) {
306
313
  super(`Invalid ${target} payload`);
314
+ this.name = Errors.ValidationError;
307
315
  this.target = target;
308
316
  this.payload = payload;
309
317
  this.details = details;
310
- this.name = Errors.ValidationError;
311
318
  }
312
319
  };
313
320
  var InvariantError = class extends Error {
321
+ /** The action that was attempted */
322
+ action;
323
+ /** The action payload that was provided */
324
+ payload;
325
+ /** The target stream and actor context */
326
+ target;
327
+ /** The current state snapshot when invariant was checked */
328
+ snapshot;
329
+ /** Human-readable description of why the invariant failed */
330
+ description;
314
331
  constructor(action2, payload, target, snapshot, description) {
315
332
  super(`${action2} failed invariant: ${description}`);
333
+ this.name = Errors.InvariantError;
316
334
  this.action = action2;
317
335
  this.payload = payload;
318
336
  this.target = target;
319
337
  this.snapshot = snapshot;
320
338
  this.description = description;
321
- this.name = Errors.InvariantError;
322
339
  }
323
340
  };
324
341
  var ConcurrencyError = class extends Error {
342
+ /** The stream that had the concurrent modification */
343
+ stream;
344
+ /** The actual current version in the store */
345
+ lastVersion;
346
+ /** The events that were being committed */
347
+ events;
348
+ /** The version number that was expected */
349
+ expectedVersion;
325
350
  constructor(stream, lastVersion, events, expectedVersion) {
326
351
  super(
327
352
  `Concurrency error committing "${events.map((e) => `${stream}.${e.name}`).join(
328
353
  ", "
329
354
  )}". Expected version ${expectedVersion} but found version ${lastVersion}.`
330
355
  );
356
+ this.name = Errors.ConcurrencyError;
331
357
  this.stream = stream;
332
358
  this.lastVersion = lastVersion;
333
359
  this.events = events;
334
360
  this.expectedVersion = expectedVersion;
335
- this.name = Errors.ConcurrencyError;
336
361
  }
337
362
  };
338
363
  var StreamClosedError = class extends Error {
364
+ /** The stream that is closed */
365
+ stream;
339
366
  constructor(stream) {
340
367
  super(`Stream "${stream}" is closed (tombstoned)`);
341
- this.stream = stream;
342
368
  this.name = Errors.StreamClosedError;
369
+ this.stream = stream;
343
370
  }
344
371
  };
345
372
  var NonRetryableError = class extends Error {
@@ -495,12 +522,8 @@ async function sleep(ms) {
495
522
 
496
523
  // src/adapters/in-memory-store.ts
497
524
  var InMemoryStream = class {
498
- constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
499
- this.stream = stream;
500
- this.source = source;
501
- this._priority = priority;
502
- this._lane = lane;
503
- }
525
+ stream;
526
+ source;
504
527
  _at = -1;
505
528
  _retry = -1;
506
529
  _blocked = false;
@@ -509,6 +532,12 @@ var InMemoryStream = class {
509
532
  _leased_until = void 0;
510
533
  _priority = 0;
511
534
  _lane = DEFAULT_LANE;
535
+ constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
536
+ this.stream = stream;
537
+ this.source = source;
538
+ this._priority = priority;
539
+ this._lane = lane;
540
+ }
512
541
  get priority() {
513
542
  return this._priority;
514
543
  }
@@ -1902,18 +1931,23 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1902
1931
 
1903
1932
  // src/internal/correlate-cycle.ts
1904
1933
  var CorrelateCycle = class {
1905
- constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
1906
- this.registry = registry;
1907
- this.staticTargets = staticTargets;
1908
- this.hasDynamicResolvers = hasDynamicResolvers;
1909
- this.cd = cd;
1910
- this.onInit = onInit;
1911
- this._subscribed = new LruSet(maxSubscribedStreams);
1912
- }
1913
1934
  _checkpoint = -1;
1914
1935
  _initialized = false;
1915
1936
  _timer = void 0;
1916
1937
  _subscribed;
1938
+ _registry;
1939
+ _static_targets;
1940
+ _has_dynamic_resolvers;
1941
+ _cd;
1942
+ _on_init;
1943
+ constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
1944
+ this._subscribed = new LruSet(maxSubscribedStreams);
1945
+ this._registry = registry;
1946
+ this._static_targets = staticTargets;
1947
+ this._has_dynamic_resolvers = hasDynamicResolvers;
1948
+ this._cd = cd;
1949
+ this._on_init = onInit;
1950
+ }
1917
1951
  /** Last correlated event id. */
1918
1952
  get checkpoint() {
1919
1953
  return this._checkpoint;
@@ -1928,10 +1962,10 @@ var CorrelateCycle = class {
1928
1962
  async init() {
1929
1963
  if (this._initialized) return;
1930
1964
  this._initialized = true;
1931
- const { watermark } = await store2().subscribe([...this.staticTargets]);
1965
+ const { watermark } = await store2().subscribe([...this._static_targets]);
1932
1966
  this._checkpoint = watermark;
1933
- this.onInit?.();
1934
- for (const { stream } of this.staticTargets) {
1967
+ this._on_init?.();
1968
+ for (const { stream } of this._static_targets) {
1935
1969
  this._subscribed.add(stream);
1936
1970
  }
1937
1971
  }
@@ -1942,7 +1976,7 @@ var CorrelateCycle = class {
1942
1976
  */
1943
1977
  async correlate(query = { after: -1, limit: 10 }) {
1944
1978
  await this.init();
1945
- if (!this.hasDynamicResolvers)
1979
+ if (!this._has_dynamic_resolvers)
1946
1980
  return { subscribed: 0, last_id: this._checkpoint };
1947
1981
  const after = Math.max(this._checkpoint, query.after || -1);
1948
1982
  const correlated = /* @__PURE__ */ new Map();
@@ -1950,7 +1984,7 @@ var CorrelateCycle = class {
1950
1984
  await store2().query(
1951
1985
  (event) => {
1952
1986
  last_id = event.id;
1953
- const register = this.registry.events[event.name];
1987
+ const register = this._registry.events[event.name];
1954
1988
  if (register) {
1955
1989
  for (const reaction of register.reactions.values()) {
1956
1990
  if (typeof reaction.resolver !== "function") continue;
@@ -1986,7 +2020,7 @@ var CorrelateCycle = class {
1986
2020
  lane
1987
2021
  })
1988
2022
  );
1989
- const { subscribed } = await this.cd.subscribe(streams);
2023
+ const { subscribed } = await this._cd.subscribe(streams);
1990
2024
  this._checkpoint = last_id;
1991
2025
  if (subscribed) {
1992
2026
  for (const { stream } of streams) {
@@ -2610,9 +2644,6 @@ var EMPTY_DRAIN = {
2610
2644
  blocked: []
2611
2645
  };
2612
2646
  var DrainController = class {
2613
- constructor(deps) {
2614
- this.deps = deps;
2615
- }
2616
2647
  _armed = false;
2617
2648
  _locked = false;
2618
2649
  _ratio = 0.5;
@@ -2628,6 +2659,10 @@ var DrainController = class {
2628
2659
  /** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
2629
2660
  _worker;
2630
2661
  _stopped = false;
2662
+ _deps;
2663
+ constructor(deps) {
2664
+ this._deps = deps;
2665
+ }
2631
2666
  /**
2632
2667
  * Signal that a commit (or reset / cold-start) may have produced work.
2633
2668
  * Subsequent `drain()` calls will run the pipeline; once the pipeline
@@ -2668,7 +2703,7 @@ var DrainController = class {
2668
2703
  }
2669
2704
  /** Lane this controller drains (undefined = legacy single-lane span). */
2670
2705
  get lane() {
2671
- return this.deps.lane;
2706
+ return this._deps.lane;
2672
2707
  }
2673
2708
  /**
2674
2709
  * Start a per-lane worker that drains at the lane's `cycleMs`
@@ -2702,7 +2737,7 @@ var DrainController = class {
2702
2737
  async drain(options = {}) {
2703
2738
  if (!this._armed) return EMPTY_DRAIN;
2704
2739
  if (this._locked) return EMPTY_DRAIN;
2705
- const d = this.deps.defaults ?? {};
2740
+ const d = this._deps.defaults ?? {};
2706
2741
  const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
2707
2742
  const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
2708
2743
  const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
@@ -2711,24 +2746,24 @@ var DrainController = class {
2711
2746
  const lagging = Math.ceil(streamLimit * this._ratio);
2712
2747
  const leading = streamLimit - lagging;
2713
2748
  const cycle = await runDrainCycle(
2714
- this.deps.ops,
2715
- this.deps.registry,
2716
- this.deps.batchHandlers,
2717
- this.deps.handle,
2718
- this.deps.handleBatch,
2749
+ this._deps.ops,
2750
+ this._deps.registry,
2751
+ this._deps.batchHandlers,
2752
+ this._deps.handle,
2753
+ this._deps.handleBatch,
2719
2754
  lagging,
2720
2755
  leading,
2721
2756
  eventLimit,
2722
2757
  leaseMillis,
2723
2758
  this._backoff.size > 0 ? this.isDeferred : void 0,
2724
- this.deps.lane
2759
+ this._deps.lane
2725
2760
  );
2726
2761
  if (!cycle) {
2727
2762
  this._armed = false;
2728
2763
  return EMPTY_DRAIN;
2729
2764
  }
2730
2765
  const { leased, fetched, handled, acked, blocked } = cycle;
2731
- traceCycle(this.deps.logger, leased, fetched, handled, acked, blocked);
2766
+ traceCycle(this._deps.logger, leased, fetched, handled, acked, blocked);
2732
2767
  this._ratio = computeLagLeadRatio(handled, lagging, leading);
2733
2768
  for (const lease of acked) this._backoff.delete(lease.stream);
2734
2769
  for (const lease of blocked) this._backoff.delete(lease.stream);
@@ -2738,13 +2773,13 @@ var DrainController = class {
2738
2773
  }
2739
2774
  }
2740
2775
  if (this._backoff.size > 0) this.scheduleBackoffWake();
2741
- if (acked.length) this.deps.onAcked(acked);
2742
- if (blocked.length) this.deps.onBlocked(blocked);
2776
+ if (acked.length) this._deps.onAcked(acked);
2777
+ if (blocked.length) this._deps.onBlocked(blocked);
2743
2778
  const hasErrors = handled.some(({ error }) => error);
2744
2779
  if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
2745
2780
  return { fetched, leased, acked, blocked };
2746
2781
  } catch (error) {
2747
- this.deps.logger.error(error);
2782
+ this._deps.logger.error(error);
2748
2783
  return EMPTY_DRAIN;
2749
2784
  } finally {
2750
2785
  this._locked = false;
@@ -3005,12 +3040,15 @@ function buildHandleBatch(logger) {
3005
3040
 
3006
3041
  // src/internal/settle.ts
3007
3042
  var SettleLoop = class {
3008
- constructor(deps, defaultDebounceMs) {
3009
- this.deps = deps;
3010
- this.defaultDebounceMs = defaultDebounceMs;
3011
- }
3012
3043
  _timer = void 0;
3013
3044
  _running = false;
3045
+ _deps;
3046
+ /** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
3047
+ _default_debounce_ms;
3048
+ constructor(deps, defaultDebounceMs) {
3049
+ this._deps = deps;
3050
+ this._default_debounce_ms = defaultDebounceMs;
3051
+ }
3014
3052
  /**
3015
3053
  * Schedule a settle pass. Multiple calls inside the debounce window
3016
3054
  * coalesce into one cycle. The cycle runs correlate→drain in a loop
@@ -3020,7 +3058,7 @@ var SettleLoop = class {
3020
3058
  */
3021
3059
  schedule(options = {}) {
3022
3060
  const {
3023
- debounceMs = this.defaultDebounceMs,
3061
+ debounceMs = this._default_debounce_ms,
3024
3062
  correlate: correlateQuery = { after: -1, limit: 100 },
3025
3063
  maxPasses = Infinity,
3026
3064
  ...drainOptions
@@ -3031,19 +3069,19 @@ var SettleLoop = class {
3031
3069
  if (this._running) return;
3032
3070
  this._running = true;
3033
3071
  (async () => {
3034
- await this.deps.init();
3072
+ await this._deps.init();
3035
3073
  let lastDrain;
3036
3074
  for (let i = 0; i < maxPasses; i++) {
3037
- const { subscribed } = await this.deps.correlate({
3075
+ const { subscribed } = await this._deps.correlate({
3038
3076
  ...correlateQuery,
3039
- after: this.deps.checkpoint()
3077
+ after: this._deps.checkpoint()
3040
3078
  });
3041
- lastDrain = await this.deps.drain(drainOptions);
3079
+ lastDrain = await this._deps.drain(drainOptions);
3042
3080
  const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
3043
3081
  if (!made_progress) break;
3044
3082
  }
3045
- if (lastDrain) this.deps.onSettled(lastDrain);
3046
- })().catch((err) => this.deps.logger.error(err)).finally(() => {
3083
+ if (lastDrain) this._deps.onSettled(lastDrain);
3084
+ })().catch((err) => this._deps.logger.error(err)).finally(() => {
3047
3085
  this._running = false;
3048
3086
  });
3049
3087
  }, debounceMs);
@@ -3061,6 +3099,117 @@ var SettleLoop = class {
3061
3099
  var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
3062
3100
  var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
3063
3101
  var Act = class {
3102
+ _emitter = new import_node_events.default();
3103
+ /** #803: gate the `Store.notify` subscription side. */
3104
+ _listen;
3105
+ /** #803: gate the local reaction pipeline (drain controllers, settle, correlate). */
3106
+ _drain;
3107
+ /** Event names with at least one registered reaction (computed at build time) */
3108
+ _reactive_events;
3109
+ /** One DrainController per active lane, keyed by lane name. */
3110
+ _drain_controllers;
3111
+ /** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
3112
+ _correlate;
3113
+ /** Debounced correlate→drain catch-up loop. */
3114
+ _settle;
3115
+ /**
3116
+ * Disposer for the cross-process notify subscription, set up eagerly
3117
+ * during construction. Held as a promise because the subscription
3118
+ * itself may be async (the PG adapter checks out a dedicated client
3119
+ * and runs `LISTEN` before resolving). Resolves to `undefined` when
3120
+ * the store doesn't implement `notify` or there are no registered
3121
+ * reactions.
3122
+ *
3123
+ * **Contract:** the configured store must be injected via
3124
+ * {@link store}`(adapter)` *before* calling `act()...build()`. The
3125
+ * orchestrator wires notify against whatever store is current at
3126
+ * construction time — late injection after build is unsupported.
3127
+ */
3128
+ _notify_disposer;
3129
+ /** Public registry — kept as-is per the no-prefix-on-public convention. */
3130
+ registry;
3131
+ /** Map of state name → state definition; populated by the builder. */
3132
+ _states;
3133
+ /**
3134
+ * Emit a lifecycle event. The payload type is inferred from the event name
3135
+ * via {@link ActLifecycleEvents}.
3136
+ */
3137
+ emit(event, args) {
3138
+ return this._emitter.emit(event, args);
3139
+ }
3140
+ /**
3141
+ * Register a listener for a lifecycle event. The listener receives the
3142
+ * event-specific payload.
3143
+ */
3144
+ on(event, listener) {
3145
+ this._emitter.on(event, listener);
3146
+ return this;
3147
+ }
3148
+ /**
3149
+ * Remove a previously registered lifecycle listener.
3150
+ */
3151
+ off(event, listener) {
3152
+ this._emitter.off(event, listener);
3153
+ return this;
3154
+ }
3155
+ /** Batch handlers for static-target projections (target → handler) */
3156
+ _batch_handlers;
3157
+ /** Event-sourcing handlers, optionally wrapped with trace decorators */
3158
+ _es;
3159
+ /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
3160
+ _cd;
3161
+ /**
3162
+ * Event-name → owning state, computed at build time. The duplicate-event
3163
+ * guard in merge.ts ensures one event name maps to at most one state, so
3164
+ * this lookup is unambiguous. Used by `close()` to pick the right reducer
3165
+ * set when seeding a `restart` snapshot in multi-state apps.
3166
+ */
3167
+ _event_to_state;
3168
+ /**
3169
+ * Event-name → lane fan-in for selective arming (ACT-1103). Built by
3170
+ * `classifyRegistry` once per build. `"all"` means at least one of
3171
+ * the event's reactions is a dynamic resolver (lane opaque until
3172
+ * runtime); a `Set<string>` lists the static lanes only that event's
3173
+ * reactions target.
3174
+ */
3175
+ _event_to_lanes;
3176
+ /**
3177
+ * Audit dependency bag (#723). Built once at construction; held as
3178
+ * an immutable snapshot of the registry state the audit module
3179
+ * needs. Lives in `internal/audit.ts` — this orchestrator never
3180
+ * carries audit logic, only the deps + a one-liner that hands them
3181
+ * over.
3182
+ */
3183
+ _audit_deps;
3184
+ /** Logger resolved at construction time (after user port configuration) */
3185
+ _logger = log();
3186
+ /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
3187
+ * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
3188
+ * path keeps reading fresh `store()`/`cache()` per call, which matters for
3189
+ * tests that dispose and re-seed mid-suite. */
3190
+ _scoped;
3191
+ /**
3192
+ * Correlation-id generator for originating actions. Bound at
3193
+ * construction from `options.correlator ?? defaultCorrelator`. The
3194
+ * `do()` path passes this into the `_es.action` closure; close-cycle
3195
+ * uses it via {@link closeCorrelation}.
3196
+ */
3197
+ _correlator;
3198
+ /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
3199
+ * payload (it captures the triggering event for reactingTo auto-inject). */
3200
+ _bound_do = this.do.bind(this);
3201
+ _bound_load = this.load.bind(this);
3202
+ _bound_query = this.query.bind(this);
3203
+ _bound_query_array = this.query_array.bind(this);
3204
+ /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
3205
+ _handle;
3206
+ _handle_batch;
3207
+ /** Declared drain lanes (ACT-1103). */
3208
+ _lanes;
3209
+ /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
3210
+ get lanes() {
3211
+ return this._lanes;
3212
+ }
3064
3213
  /**
3065
3214
  * Create a new Act orchestrator. Prefer the {@link act} builder over
3066
3215
  * direct construction — `act()...build()` wires the registry, merges
@@ -3075,9 +3224,9 @@ var Act = class {
3075
3224
  * these from `.withLane(...)` calls. Slice 1 records them on the
3076
3225
  * instance; later slices fan out one `DrainController` per lane.
3077
3226
  */
3078
- constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
3227
+ constructor(registry, states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
3079
3228
  this.registry = registry;
3080
- this._states = _states;
3229
+ this._states = states;
3081
3230
  this._batch_handlers = batchHandlers;
3082
3231
  this._lanes = lanes;
3083
3232
  if (options.onlyLanes && options.onlyLanes.length > 0) {
@@ -3111,6 +3260,8 @@ var Act = class {
3111
3260
  eventToLanes
3112
3261
  } = classifyRegistry(this.registry, this._states);
3113
3262
  this._reactive_events = reactiveEvents;
3263
+ this._listen = options.listen !== false;
3264
+ this._drain = options.drain !== false;
3114
3265
  this._event_to_state = eventToState;
3115
3266
  this._event_to_lanes = eventToLanes;
3116
3267
  const allLanes = ["default", ...lanes.map((l) => l.name)];
@@ -3138,7 +3289,8 @@ var Act = class {
3138
3289
  leaseMillis: cfg.leaseMillis
3139
3290
  }
3140
3291
  });
3141
- if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
3292
+ if (cfg?.cycleMs !== void 0 && options.drain !== false)
3293
+ controller.start(cfg.cycleMs);
3142
3294
  this._drain_controllers.set(name, controller);
3143
3295
  }
3144
3296
  this._audit_deps = {
@@ -3156,9 +3308,10 @@ var Act = class {
3156
3308
  hasDynamicResolvers,
3157
3309
  this._cd,
3158
3310
  options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
3159
- // Cold start: assume drain is needed (historical events may need processing)
3311
+ // Cold start: assume drain is needed (historical events may need processing).
3312
+ // #803: writer-only instances skip the cold-start arm.
3160
3313
  () => {
3161
- if (this._reactive_events.size > 0) this._armAll();
3314
+ if (this._drain && this._reactive_events.size > 0) this._armAll();
3162
3315
  }
3163
3316
  );
3164
3317
  this._settle = new SettleLoop(
@@ -3175,109 +3328,6 @@ var Act = class {
3175
3328
  this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
3176
3329
  dispose(() => this.shutdown());
3177
3330
  }
3178
- _emitter = new import_node_events.default();
3179
- /** Event names with at least one registered reaction (computed at build time) */
3180
- _reactive_events;
3181
- /** One DrainController per active lane, keyed by lane name. */
3182
- _drain_controllers;
3183
- /** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
3184
- _correlate;
3185
- /** Debounced correlate→drain catch-up loop. */
3186
- _settle;
3187
- /**
3188
- * Disposer for the cross-process notify subscription, set up eagerly
3189
- * during construction. Held as a promise because the subscription
3190
- * itself may be async (the PG adapter checks out a dedicated client
3191
- * and runs `LISTEN` before resolving). Resolves to `undefined` when
3192
- * the store doesn't implement `notify` or there are no registered
3193
- * reactions.
3194
- *
3195
- * **Contract:** the configured store must be injected via
3196
- * {@link store}`(adapter)` *before* calling `act()...build()`. The
3197
- * orchestrator wires notify against whatever store is current at
3198
- * construction time — late injection after build is unsupported.
3199
- */
3200
- _notify_disposer;
3201
- /**
3202
- * Emit a lifecycle event. The payload type is inferred from the event name
3203
- * via {@link ActLifecycleEvents}.
3204
- */
3205
- emit(event, args) {
3206
- return this._emitter.emit(event, args);
3207
- }
3208
- /**
3209
- * Register a listener for a lifecycle event. The listener receives the
3210
- * event-specific payload.
3211
- */
3212
- on(event, listener) {
3213
- this._emitter.on(event, listener);
3214
- return this;
3215
- }
3216
- /**
3217
- * Remove a previously registered lifecycle listener.
3218
- */
3219
- off(event, listener) {
3220
- this._emitter.off(event, listener);
3221
- return this;
3222
- }
3223
- /** Batch handlers for static-target projections (target → handler) */
3224
- _batch_handlers;
3225
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
3226
- _es;
3227
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
3228
- _cd;
3229
- /**
3230
- * Event-name → owning state, computed at build time. The duplicate-event
3231
- * guard in merge.ts ensures one event name maps to at most one state, so
3232
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
3233
- * set when seeding a `restart` snapshot in multi-state apps.
3234
- */
3235
- _event_to_state;
3236
- /**
3237
- * Event-name → lane fan-in for selective arming (ACT-1103). Built by
3238
- * `classifyRegistry` once per build. `"all"` means at least one of
3239
- * the event's reactions is a dynamic resolver (lane opaque until
3240
- * runtime); a `Set<string>` lists the static lanes only that event's
3241
- * reactions target.
3242
- */
3243
- _event_to_lanes;
3244
- /**
3245
- * Audit dependency bag (#723). Built once at construction; held as
3246
- * an immutable snapshot of the registry state the audit module
3247
- * needs. Lives in `internal/audit.ts` — this orchestrator never
3248
- * carries audit logic, only the deps + a one-liner that hands them
3249
- * over.
3250
- */
3251
- _audit_deps;
3252
- /** Logger resolved at construction time (after user port configuration) */
3253
- _logger = log();
3254
- /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
3255
- * per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
3256
- * path keeps reading fresh `store()`/`cache()` per call, which matters for
3257
- * tests that dispose and re-seed mid-suite. */
3258
- _scoped;
3259
- /**
3260
- * Correlation-id generator for originating actions. Bound at
3261
- * construction from `options.correlator ?? defaultCorrelator`. The
3262
- * `do()` path passes this into the `_es.action` closure; close-cycle
3263
- * uses it via {@link closeCorrelation}.
3264
- */
3265
- _correlator;
3266
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
3267
- * payload (it captures the triggering event for reactingTo auto-inject). */
3268
- _bound_do = this.do.bind(this);
3269
- _bound_load = this.load.bind(this);
3270
- _bound_query = this.query.bind(this);
3271
- _bound_query_array = this.query_array.bind(this);
3272
- /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
3273
- _handle;
3274
- _handle_batch;
3275
- /** Declared drain lanes (ACT-1103). */
3276
- _lanes;
3277
- /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
3278
- get lanes() {
3279
- return this._lanes;
3280
- }
3281
3331
  /** True after the first `shutdown()` call. Guards idempotency. */
3282
3332
  _shutdown_promise;
3283
3333
  /**
@@ -3312,14 +3362,17 @@ var Act = class {
3312
3362
  async _wireNotify(s) {
3313
3363
  if (this._reactive_events.size === 0) return void 0;
3314
3364
  if (!s.notify) return void 0;
3365
+ if (!this._listen) return void 0;
3315
3366
  try {
3316
3367
  return await s.notify((notification) => {
3317
3368
  try {
3318
3369
  this.emit("notified", notification);
3319
- const armed = this._armForEventNames(
3320
- notification.events.map((e) => e.name)
3321
- );
3322
- if (armed) this._settle.schedule({ debounceMs: 0 });
3370
+ if (this._drain) {
3371
+ const armed = this._armForEventNames(
3372
+ notification.events.map((e) => e.name)
3373
+ );
3374
+ if (armed) this._settle.schedule({ debounceMs: 0 });
3375
+ }
3323
3376
  } catch (err) {
3324
3377
  this._logger.error(err, "notified handler threw");
3325
3378
  }
@@ -3570,6 +3623,8 @@ var Act = class {
3570
3623
  * @see {@link start_correlations} for automatic correlation
3571
3624
  */
3572
3625
  async drain(options = {}) {
3626
+ if (!this._drain)
3627
+ return { fetched: [], leased: [], acked: [], blocked: [] };
3573
3628
  return this._scoped(() => this._drainAll(options));
3574
3629
  }
3575
3630
  /** Arm every active lane controller (ACT-1103). */
@@ -3670,6 +3725,7 @@ var Act = class {
3670
3725
  * @see {@link stop_correlations} to stop automatic correlation
3671
3726
  */
3672
3727
  async correlate(query = { after: -1, limit: 10 }) {
3728
+ if (!this._drain) return { subscribed: 0, last_id: -1 };
3673
3729
  return this._scoped(() => this._correlate.correlate(query));
3674
3730
  }
3675
3731
  /**
@@ -4095,6 +4151,7 @@ var Act = class {
4095
4151
  * @see {@link correlate} for manual correlation
4096
4152
  */
4097
4153
  settle(options = {}) {
4154
+ if (!this._drain) return;
4098
4155
  this._settle.schedule(options);
4099
4156
  }
4100
4157
  };