@rotorsoft/act 1.32.24 → 1.32.26

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
@@ -26,7 +26,7 @@ import {
26
26
  sleep,
27
27
  store,
28
28
  validate
29
- } from "./chunk-OG5O36AJ.js";
29
+ } from "./chunk-QXW3JV2H.js";
30
30
  import {
31
31
  ActorSchema,
32
32
  CausationEventSchema,
@@ -1355,37 +1355,34 @@ async function windowed_prune_pending(stream, before, max_id) {
1355
1355
  }
1356
1356
  async function probe_min_watermarks(streams, page_size, checkpoint) {
1357
1357
  const min_at = /* @__PURE__ */ new Map();
1358
- const source_regex = /* @__PURE__ */ new Map();
1359
- const get_regex = (source) => {
1360
- let re = source_regex.get(source);
1358
+ const matches = source_matcher();
1359
+ await walk_streams(
1360
+ store(),
1361
+ (position) => {
1362
+ const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
1363
+ const cap = pending ? position.at : Math.max(position.at, checkpoint);
1364
+ for (const stream of streams) {
1365
+ if (matches(position.source, stream)) {
1366
+ const prev = min_at.get(stream);
1367
+ if (prev === void 0 || cap < prev) min_at.set(stream, cap);
1368
+ }
1369
+ }
1370
+ },
1371
+ { limit: page_size, source_matches: streams }
1372
+ );
1373
+ return min_at;
1374
+ }
1375
+ function source_matcher() {
1376
+ const compiled = /* @__PURE__ */ new Map();
1377
+ return (source, stream) => {
1378
+ if (!source) return true;
1379
+ let re = compiled.get(source);
1361
1380
  if (!re) {
1362
1381
  re = new RegExp(source);
1363
- source_regex.set(source, re);
1382
+ compiled.set(source, re);
1364
1383
  }
1365
- return re;
1384
+ return re.test(stream);
1366
1385
  };
1367
- let after;
1368
- for (; ; ) {
1369
- let last;
1370
- const { count } = await store().query_streams(
1371
- (position) => {
1372
- last = position.stream;
1373
- const source_re = position.source ? get_regex(position.source) : void 0;
1374
- const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
1375
- const cap = pending ? position.at : Math.max(position.at, checkpoint);
1376
- for (const stream of streams) {
1377
- if (!source_re || source_re.test(stream)) {
1378
- const prev = min_at.get(stream);
1379
- if (prev === void 0 || cap < prev) min_at.set(stream, cap);
1380
- }
1381
- }
1382
- },
1383
- { after, limit: page_size, source_matches: streams }
1384
- );
1385
- if (count < page_size) break;
1386
- after = last;
1387
- }
1388
- return min_at;
1389
1386
  }
1390
1387
  async function scan_stream_heads(streams) {
1391
1388
  const stats = await store().query_stats(streams, {
@@ -1419,36 +1416,20 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
1419
1416
  if (checkpoint < info.max_id) uncorrelated.add(stream);
1420
1417
  }
1421
1418
  const pending_set = /* @__PURE__ */ new Set();
1422
- const source_regex = /* @__PURE__ */ new Map();
1423
- const get_regex = (source) => {
1424
- let re = source_regex.get(source);
1425
- if (!re) {
1426
- re = new RegExp(source);
1427
- source_regex.set(source, re);
1428
- }
1429
- return re;
1430
- };
1419
+ const matches = source_matcher();
1431
1420
  const targets = [...stream_info.keys()];
1432
- let after;
1433
- for (; ; ) {
1434
- let last;
1435
- const { count } = await store().query_streams(
1436
- (position) => {
1437
- last = position.stream;
1438
- const source_re = position.source ? get_regex(position.source) : void 0;
1439
- const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
1440
- if (!has_work) return;
1441
- for (const [stream, info] of stream_info) {
1442
- if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
1443
- pending_set.add(stream);
1444
- }
1445
- }
1446
- },
1447
- { after, limit: page_size, source_matches: targets }
1448
- );
1449
- if (count < page_size) break;
1450
- after = last;
1451
- }
1421
+ await walk_streams(
1422
+ store(),
1423
+ (position) => {
1424
+ const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
1425
+ if (!has_work) return;
1426
+ for (const [stream, info] of stream_info) {
1427
+ if (matches(position.source, stream) && position.at < info.max_id)
1428
+ pending_set.add(stream);
1429
+ }
1430
+ },
1431
+ { limit: page_size, source_matches: targets }
1432
+ );
1452
1433
  const safe = [];
1453
1434
  for (const [stream] of stream_info) {
1454
1435
  if (pending_set.has(stream) || uncorrelated.has(stream))
@@ -1711,13 +1692,25 @@ var CorrelateCycle = class {
1711
1692
  const after = Math.max(this._checkpoint, query.after || -1);
1712
1693
  const correlated = /* @__PURE__ */ new Map();
1713
1694
  let last_id = after;
1695
+ let stopped = false;
1714
1696
  const found = await store().query(
1715
1697
  (event) => {
1716
- last_id = event.id;
1698
+ if (stopped) return;
1717
1699
  const register = this._registry.events[event.name];
1718
1700
  if (register) {
1719
1701
  for (const reaction of register.reactions.values()) {
1720
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1702
+ let resolved;
1703
+ try {
1704
+ resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1705
+ } catch (error) {
1706
+ stopped = true;
1707
+ report_once(
1708
+ this._reported,
1709
+ `resolver|${reaction.handler.name}|${event.id}`,
1710
+ `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}. Correlation stops before this event until the resolver stops throwing; work already found keeps draining.`
1711
+ );
1712
+ return;
1713
+ }
1721
1714
  if (!resolved) continue;
1722
1715
  let lane = resolved.lane;
1723
1716
  if (lane !== void 0 && !this._declared_lanes.has(lane)) {
@@ -1766,6 +1759,7 @@ var CorrelateCycle = class {
1766
1759
  correlated.set(resolved.target, entry);
1767
1760
  }
1768
1761
  }
1762
+ last_id = event.id;
1769
1763
  },
1770
1764
  // Resolvers see `pii: null`: correlate runs actor-less, and one
1771
1765
  // undecryptable row would otherwise stop every stream's reactions.
@@ -1807,7 +1801,7 @@ var CorrelateCycle = class {
1807
1801
  return { subscribed, last_id, marked, scanned: true };
1808
1802
  }
1809
1803
  this._checkpoint = last_id;
1810
- this._armed = found === query.limit;
1804
+ this._armed = stopped || found === query.limit;
1811
1805
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1812
1806
  }
1813
1807
  /**
@@ -2132,6 +2126,18 @@ var validating_patch = (me, state2, partial, event) => {
2132
2126
  );
2133
2127
  };
2134
2128
  var DEFAULT_BATCH = 500;
2129
+ var cache_put = (stream, entry) => cache().set(stream, entry).catch(
2130
+ (err) => log().warn(
2131
+ err,
2132
+ "cache set skipped; the next load re-folds from the store"
2133
+ )
2134
+ );
2135
+ var cache_drop = (stream) => cache().invalidate(stream).catch(
2136
+ (err) => log().warn(
2137
+ err,
2138
+ "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
2139
+ )
2140
+ );
2135
2141
  async function snap(snapshot) {
2136
2142
  const { id, stream, name, meta, version } = snapshot.event;
2137
2143
  try {
@@ -2334,19 +2340,14 @@ async function load(me, target, callback, patch_fn = bare_patch) {
2334
2340
  );
2335
2341
  const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
2336
2342
  if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
2337
- await cache().set(stream, {
2343
+ await cache_put(stream, {
2338
2344
  stream,
2339
2345
  state: state2,
2340
2346
  version: event.version,
2341
2347
  event_id: event.id,
2342
2348
  patches,
2343
2349
  snaps
2344
- }).catch(
2345
- (err) => log().warn(
2346
- err,
2347
- "cache set skipped; the next load re-folds from the store"
2348
- )
2349
- );
2350
+ });
2350
2351
  }
2351
2352
  return {
2352
2353
  event,
@@ -2434,14 +2435,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
2434
2435
  reactingTo ? expectedVersion : expected
2435
2436
  );
2436
2437
  } catch (error) {
2437
- if (error instanceof ConcurrencyError) {
2438
- cache().invalidate(stream).catch(
2439
- (err) => log().warn(
2440
- err,
2441
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
2442
- )
2443
- );
2444
- }
2438
+ if (error instanceof ConcurrencyError) void cache_drop(stream);
2445
2439
  throw error;
2446
2440
  }
2447
2441
  let { state: state2, patches } = snapshot;
@@ -2468,26 +2462,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
2468
2462
  const snap_event = snapped ? await snap(last) : void 0;
2469
2463
  if (!me.pii_aware) {
2470
2464
  if (contiguous)
2471
- cache().set(stream, {
2465
+ void cache_put(stream, {
2472
2466
  stream,
2473
2467
  state: last.state,
2474
2468
  version: snap_event?.version ?? last.event.version,
2475
2469
  event_id: snap_event?.id ?? last.event.id,
2476
2470
  patches: snap_event ? 0 : last.patches,
2477
2471
  snaps: snap_event ? last.snaps + 1 : last.snaps
2478
- }).catch(
2479
- (err) => log().warn(
2480
- err,
2481
- "cache set skipped; the next load re-folds from the store"
2482
- )
2483
- );
2484
- else
2485
- cache().invalidate(stream).catch(
2486
- (err) => log().warn(
2487
- err,
2488
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
2489
- )
2490
- );
2472
+ });
2473
+ else void cache_drop(stream);
2491
2474
  }
2492
2475
  return snapshots;
2493
2476
  } catch (error) {
@@ -2702,6 +2685,22 @@ function fetch_failure_policy(registry) {
2702
2685
  }
2703
2686
  return { blockOnError, maxRetries };
2704
2687
  }
2688
+ function resolver_failed(lease, { error, options }) {
2689
+ log().error(error);
2690
+ const block2 = options.blockOnError && lease.retry >= options.maxRetries;
2691
+ return {
2692
+ lease,
2693
+ handled: 0,
2694
+ acked_at: lease.at,
2695
+ error,
2696
+ ...block2 ? { block: true } : {
2697
+ next_attempt_at: Date.now() + compute_backoff_delay(
2698
+ lease.retry,
2699
+ options.backoff ?? DEFAULT_BACKOFF
2700
+ )
2701
+ }
2702
+ };
2703
+ }
2705
2704
  function warn_misrouted(seen, stream, handler, event) {
2706
2705
  report_once(
2707
2706
  seen,
@@ -2735,13 +2734,23 @@ async function run_drain_cycle(deps, cycle) {
2735
2734
  );
2736
2735
  for (const f of fetched) {
2737
2736
  const { stream, events } = f;
2737
+ let failed;
2738
2738
  const payloads = events.flatMap((event) => {
2739
2739
  const register = registry.events[event.name];
2740
2740
  if (!register) return [];
2741
2741
  return [...register.reactions.values()].filter((reaction) => {
2742
2742
  const resolver = reaction.resolver;
2743
2743
  const dynamic = typeof resolver === "function";
2744
- const resolved = dynamic ? resolver(event) : resolver;
2744
+ let resolved;
2745
+ try {
2746
+ resolved = dynamic ? resolver(event) : resolver;
2747
+ } catch (error) {
2748
+ failed ??= {
2749
+ error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
2750
+ options: reaction.options
2751
+ };
2752
+ return false;
2753
+ }
2745
2754
  if (!resolved || resolved.target !== stream) return false;
2746
2755
  if (!dynamic || !batch_handlers.has(stream)) return true;
2747
2756
  warn_misrouted(
@@ -2753,7 +2762,7 @@ async function run_drain_cycle(deps, cycle) {
2753
2762
  return false;
2754
2763
  }).map((reaction) => ({ ...reaction, event }));
2755
2764
  });
2756
- fetch_map.set(stream, { fetch: f, payloads });
2765
+ fetch_map.set(stream, { fetch: f, payloads, failed });
2757
2766
  }
2758
2767
  const handled = await Promise.all(
2759
2768
  leased.map((lease) => {
@@ -2774,6 +2783,8 @@ async function run_drain_cycle(deps, cycle) {
2774
2783
  };
2775
2784
  return Promise.resolve(failed);
2776
2785
  }
2786
+ if (entry.failed)
2787
+ return Promise.resolve(resolver_failed(lease, entry.failed));
2777
2788
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
2778
2789
  const { payloads } = entry;
2779
2790
  const exhausted = budget_exhausted(
@@ -3250,41 +3261,23 @@ function validate_only_lanes(options, lanes) {
3250
3261
  }
3251
3262
  var Act = class {
3252
3263
  _emitter = new EventEmitter();
3253
- /** Circuit breaker shared by all drain lanes. */
3254
3264
  _breaker;
3255
- /** Whether this instance subscribes to `Store.notify`. */
3256
3265
  _listen;
3257
- /** Whether this instance runs reactions (drain, settle, correlate). */
3258
3266
  _drain;
3259
- /** Event names with at least one registered reaction (computed at build time) */
3260
3267
  _reactive_events;
3261
- /** One DrainController per active lane, keyed by lane name. */
3262
3268
  _drain_controllers;
3263
- /** Correlation: lazy init and the scan that marks work. */
3264
3269
  _correlate;
3265
- /** Debounced correlate→drain catch-up loop. */
3266
3270
  _settle;
3267
- /** Timer behind {@link start_correlations}. */
3268
3271
  _poll;
3269
3272
  /** The deprecated `start_correlations` callback, while the poll runs. */
3270
3273
  _on_discovered;
3271
3274
  /**
3272
- * Disposer for the cross-process notify subscription, set up eagerly
3273
- * during construction. Held as a promise because the subscription
3274
- * itself may be async (the PG adapter checks out a dedicated client
3275
- * and runs `LISTEN` before resolving). Resolves to `undefined` when
3276
- * the store doesn't implement `notify` or there are no registered
3277
- * reactions.
3278
- *
3279
- * **Contract:** the configured store must be injected via
3280
- * {@link store}`(adapter)` *before* calling `act()...build()`. The
3281
- * orchestrator wires notify against whatever store is current at
3282
- * construction time — late injection after build is unsupported.
3275
+ * Wired at construction against the store current then, so a store
3276
+ * injected after `build()` is never notified.
3283
3277
  */
3284
3278
  _notify_disposer;
3285
3279
  /** Public registry — kept as-is per the no-prefix-on-public convention. */
3286
3280
  registry;
3287
- /** Map of state name → state definition; populated by the builder. */
3288
3281
  _states;
3289
3282
  /**
3290
3283
  * Emit a lifecycle event. The payload type is inferred from the event name
@@ -3310,15 +3303,9 @@ var Act = class {
3310
3303
  return listeners.length > 0;
3311
3304
  }
3312
3305
  /**
3313
- * The single store-failure handler: log it, then emit the `error`
3314
- * lifecycle event. Wired into the circuit breaker's `on_error`, so it
3315
- * runs on every `failed()` — drain / settle / autoclose just call
3316
- * `breaker.failed(now, error)` and never log or emit themselves.
3317
- *
3318
- * The emit is guarded against Node's `EventEmitter` contract that an
3319
- * unhandled `"error"` emission is rethrown (which would crash the process
3320
- * from inside the drain catch); logging is unconditional so failures are
3321
- * never silent.
3306
+ * The one place a store failure is logged and emitted (the breaker's
3307
+ * `on_error`). The emit is guarded: an unhandled `"error"` emission
3308
+ * would crash the process.
3322
3309
  */
3323
3310
  _emit_error(error, circuit) {
3324
3311
  this._logger.error(error);
@@ -3340,58 +3327,29 @@ var Act = class {
3340
3327
  this._emitter.off(event, listener);
3341
3328
  return this;
3342
3329
  }
3343
- /** Batch handlers for static-target projections (target → handler) */
3344
3330
  _batch_handlers;
3345
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
3346
3331
  _es;
3347
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
3348
3332
  _cd;
3349
- /**
3350
- * Event-name → owning state, computed at build time. The duplicate-event
3351
- * guard in merge.ts ensures one event name maps to at most one state, so
3352
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
3353
- * set when seeding a `restart` snapshot in multi-state apps.
3354
- */
3333
+ /** Unambiguous: merge rejects an event name declared by two states. */
3355
3334
  _event_to_state;
3356
- /**
3357
- * Event-name → lane fan-in for selective arming. Built by
3358
- * `classify_registry` once per build. `"all"` means at least one of
3359
- * the event's reactions is a dynamic resolver (lane opaque until
3360
- * runtime); a `Set<string>` lists the static lanes only that event's
3361
- * reactions target.
3362
- */
3335
+ /** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
3363
3336
  _event_to_lanes;
3364
- /** What `internal/audit.ts` needs, built once at construction. */
3365
3337
  _audit_deps;
3366
- /** Logger resolved at construction time (after user port configuration) */
3367
3338
  _logger = log();
3368
3339
  /** This Act's ports: its own bag, or the singleton adapters. */
3369
3340
  _ports;
3370
- /**
3371
- * Runs a body inside this Act's ports frame, so internal
3372
- * `store()`/`cache()` calls resolve to `_ports`.
3373
- */
3341
+ /** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
3374
3342
  _scoped;
3375
- /**
3376
- * Correlation-id generator for originating actions. Bound at
3377
- * construction from `options.correlator ?? default_correlator`. The
3378
- * `do()` path passes this into the `_es.action` closure; close-cycle
3379
- * uses it via {@link close_correlation}.
3380
- */
3381
3343
  _correlator;
3382
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
3383
- * payload (it captures the triggering event for reactingTo auto-inject). */
3344
+ // Bound once, reused by every reaction payload.
3384
3345
  _bound_do = this.do.bind(this);
3385
3346
  _bound_load = this.load.bind(this);
3386
3347
  _bound_query = this.query.bind(this);
3387
3348
  _bound_query_array = this.query_array.bind(this);
3388
3349
  _bound_forget = this.forget.bind(this);
3389
- /** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
3390
3350
  _handle;
3391
3351
  _handle_batch;
3392
- /** Declared drain lanes. */
3393
3352
  _lanes;
3394
- /** Serializes close critical sections per stream. */
3395
3353
  _close_lock = new CloseLock();
3396
3354
  /** Every lane a controller can exist for: `"default"` plus each declared one. */
3397
3355
  _declared_lanes;
@@ -3399,26 +3357,7 @@ var Act = class {
3399
3357
  get lanes() {
3400
3358
  return this._lanes;
3401
3359
  }
3402
- /**
3403
- * Create a new Act orchestrator. Prefer the {@link act} builder over
3404
- * direct construction — `act()...build()` wires the registry, merges
3405
- * partial states, and collects batch handlers from registered slices
3406
- * and projections in one pass.
3407
- *
3408
- * @param registry Schemas for every event and action across registered states
3409
- * @param states Merged map of state name → state definition
3410
- * @param batch_handlers Static-target projection batch handlers (target → handler)
3411
- * @param options Tuning knobs — see {@link ActOptions}
3412
- * @param lanes Declared drain lanes. The builder collects
3413
- * these from `.withLane(...)` calls; one `DrainController` runs
3414
- * per lane, plus the implicit `"default"`.
3415
- * @param patch_fn The per-event patch step selected once by the builder
3416
- * from `ActOptions.validateFoldedState` — `bare_patch` by
3417
- * default, `validating_patch` when the flag is on. The builder uses
3418
- * the same value for its projection-fold handlers, so there is a
3419
- * single selection site. Defaults to `bare_patch` for direct
3420
- * construction.
3421
- */
3360
+ /** Use the {@link act} builder rather than constructing directly. */
3422
3361
  constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
3423
3362
  this.registry = registry;
3424
3363
  this._states = states;
@@ -3469,11 +3408,7 @@ var Act = class {
3469
3408
  this._notify_disposer = this._wire_notify(this._ports.store);
3470
3409
  register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
3471
3410
  }
3472
- /**
3473
- * Circuit breaker shared by every store-polling loop (drain, the settle
3474
- * correlate, autoclose). Validates the knobs eagerly so out-of-range
3475
- * values throw at build time, not on the first cycle tick.
3476
- */
3411
+ /** Shared by drain, settle and autoclose; bad knobs throw at build. */
3477
3412
  _build_breaker(options) {
3478
3413
  return new CircuitBreaker(
3479
3414
  resolveCircuitBreakerConfig(options.circuitBreaker),
@@ -3487,11 +3422,7 @@ var Act = class {
3487
3422
  }
3488
3423
  );
3489
3424
  }
3490
- /**
3491
- * One DrainController per active lane, plus the implicit "default" unless
3492
- * `onlyLanes` excludes it. Apps that declare no lanes pass no lane filter
3493
- * to `claim`.
3494
- */
3425
+ /** Apps that declare no lanes pass no lane filter to `claim`. */
3495
3426
  _build_drain_controllers(options, lanes) {
3496
3427
  const all_lanes = this._declared_lanes;
3497
3428
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
@@ -3510,8 +3441,18 @@ var Act = class {
3510
3441
  on_acked: (acked) => this.emit("acked", acked),
3511
3442
  on_blocked: (blocked) => this.emit("blocked", blocked),
3512
3443
  // Reaction-requested close: the same machinery as `app.close`.
3444
+ // A store failure reaches the breaker; anything else (the archive
3445
+ // callback is app code) is logged here, never counted.
3513
3446
  on_close: async (targets) => {
3514
- await this._run_close(targets);
3447
+ try {
3448
+ await this._run_close(targets);
3449
+ } catch (error) {
3450
+ if (error instanceof StoreError) throw error;
3451
+ this._logger.error(
3452
+ error,
3453
+ `Reaction-requested close of ${targets.map((t) => `"${t.stream}"`).join(", ")} failed and was not retried; close it with app.close(...) once the cause is fixed.`
3454
+ );
3455
+ }
3515
3456
  },
3516
3457
  breaker: this._breaker,
3517
3458
  run_scoped: this._scoped,
@@ -3527,11 +3468,7 @@ var Act = class {
3527
3468
  }
3528
3469
  return controllers;
3529
3470
  }
3530
- /**
3531
- * Startup advisory naming declared lanes that `onlyLanes` leaves without a
3532
- * controller here. One process can't check that the cluster covers every
3533
- * lane, so it reports its own gaps.
3534
- */
3471
+ /** One process can't see the cluster, so it reports its own lane gaps. */
3535
3472
  _advise_orphaned_lanes(options) {
3536
3473
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3537
3474
  const active = new Set(this._drain_controllers.keys());
@@ -3553,11 +3490,6 @@ var Act = class {
3553
3490
  routed_events: new Set(this._event_to_lanes.keys())
3554
3491
  };
3555
3492
  }
3556
- /**
3557
- * Correlate cycle over the classified registry. The cold-start callback
3558
- * arms every controller — historical events may need processing —
3559
- * except on writer-only instances (`drain: false`).
3560
- */
3561
3493
  _build_correlate(options, classification) {
3562
3494
  return new CorrelateCycle({
3563
3495
  registry: this.registry,
@@ -3575,12 +3507,7 @@ var Act = class {
3575
3507
  on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
3576
3508
  });
3577
3509
  }
3578
- /**
3579
- * At cold start, re-seed each lane controller's local wake from the
3580
- * persisted future `deferred_at`, so an idle deferred stream (an
3581
- * autoclose waiting out its window) still fires on time. Streams on lanes
3582
- * this instance doesn't run are skipped.
3583
- */
3510
+ /** Cold start: an idle deferred stream still fires on time after a restart. */
3584
3511
  async _seed_persisted_defers() {
3585
3512
  const now = Date.now();
3586
3513
  await walk_streams(store(), (pos) => {
@@ -3589,7 +3516,6 @@ var Act = class {
3589
3516
  controller?.seed_defer(pos.stream, pos.deferred_at);
3590
3517
  });
3591
3518
  }
3592
- /** Settle loop driving correlate + drain to quiescence. */
3593
3519
  _build_settle(options) {
3594
3520
  return new SettleLoop(
3595
3521
  {
@@ -3603,7 +3529,6 @@ var Act = class {
3603
3529
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
3604
3530
  );
3605
3531
  }
3606
- /** True after the first `shutdown()` call. Guards idempotency. */
3607
3532
  _shutdown_promise;
3608
3533
  /**
3609
3534
  * Per-instance teardown: stop scheduling new work, give drain cycles
@@ -3643,14 +3568,8 @@ var Act = class {
3643
3568
  return this._shutdown_promise;
3644
3569
  }
3645
3570
  /**
3646
- * Wait for every in-flight drain cycle, or for the grace budget to elapse,
3647
- * whichever comes first. Cycle promises never reject (`drain()` contains
3648
- * its own errors), so this never throws.
3649
- *
3650
- * An omitted budget is the largest `leaseMillis` among lanes with a cycle
3651
- * in flight (the drain's fallback for lanes that pinned none, or pinned
3652
- * 0, which is the same as none), with an in-flight settle counting on the
3653
- * fallback, capped at the maximum. Idle lanes don't count.
3571
+ * The largest `leaseMillis` among lanes with a cycle in flight (the
3572
+ * default for none or 0, and for a running settle), capped.
3654
3573
  */
3655
3574
  _derive_grace_ms(running, settling) {
3656
3575
  let max = settling ? DEFAULT_LEASE_MILLIS : 0;
@@ -3679,12 +3598,7 @@ var Act = class {
3679
3598
  clearTimeout(timer);
3680
3599
  }
3681
3600
  }
3682
- /**
3683
- * Subscribe to {@link Store.notify} when both the store and the
3684
- * registry support it. Returns the disposer (or `undefined` when no
3685
- * subscription was made). Errors during subscription are logged but
3686
- * never thrown — `notify` is a hint, not a contract.
3687
- */
3601
+ /** Failures are logged, never thrown: `notify` is a hint. */
3688
3602
  async _wire_notify(s) {
3689
3603
  if (this._reactive_events.size === 0) return void 0;
3690
3604
  if (!s.notify) return void 0;
@@ -3996,20 +3910,11 @@ var Act = class {
3996
3910
  return { fetched: [], leased: [], acked: [], blocked: [] };
3997
3911
  return this._scoped(() => this._drain_all(options));
3998
3912
  }
3999
- /** Arm every active lane controller. */
4000
3913
  _arm_all() {
4001
3914
  this._correlate.arm();
4002
3915
  for (const c of this._drain_controllers.values()) c.arm();
4003
3916
  }
4004
- /**
4005
- * Arm only the lane controllers whose reactions match the supplied
4006
- * event names (selective arming). Events with any dynamic
4007
- * resolver fall back to `_arm_all()` via the `"all"` sentinel — the
4008
- * resolver's lane isn't known until correlate runs the function.
4009
- * Events with no reactions are skipped; `_event_to_lanes` doesn't
4010
- * carry them. Returns true when any controller was armed (used by
4011
- * the notify handler to decide whether to schedule a settle).
4012
- */
3917
+ /** Arms only the lanes these events react on; true when any was armed. */
4013
3918
  _arm_for_event_names(names) {
4014
3919
  const to_arm = /* @__PURE__ */ new Set();
4015
3920
  for (const name of names) {
@@ -4026,14 +3931,7 @@ var Act = class {
4026
3931
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
4027
3932
  return true;
4028
3933
  }
4029
- /** Drain every active lane controller in parallel and aggregate.
4030
- *
4031
- * Parallel — not sequential — so a slow lane's in-flight handler does
4032
- * not block a fast lane's claim/dispatch/ack cycle. Each controller's
4033
- * `claim()` is independent (filtered by lane); the store's
4034
- * `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
4035
- * (`acked`, `blocked`) may interleave by lane — listeners filter via
4036
- * `lease.lane`. */
3934
+ /** In parallel, so a slow lane never holds up a fast one. */
4037
3935
  async _drain_all(options) {
4038
3936
  const results = await Promise.all(
4039
3937
  [...this._drain_controllers.values()].map((c) => c.drain(options))
@@ -4106,14 +4004,7 @@ var Act = class {
4106
4004
  const { subscribed, last_id } = await this._correlate_scanned(query);
4107
4005
  return { subscribed, last_id };
4108
4006
  }
4109
- /**
4110
- * `correlate` plus whether the pass actually read the store.
4111
- *
4112
- * The settle loop needs that extra bit to decide whether the pass carries a
4113
- * circuit-breaker health signal, and a disarmed pass carries none. It stays
4114
- * internal rather than widening the public `correlate` return, which is
4115
- * charter-covered and has no use for it.
4116
- */
4007
+ /** `correlate` plus `scanned`, which settle needs for the breaker. */
4117
4008
  async _correlate_scanned(query, lease = false) {
4118
4009
  if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
4119
4010
  return this._scoped(async () => {
@@ -4454,11 +4345,7 @@ var Act = class {
4454
4345
  async prioritize(filter, priority) {
4455
4346
  return this._scoped(() => store().prioritize(filter, priority));
4456
4347
  }
4457
- /**
4458
- * Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
4459
- * reaction-requested closes. The close actor lets a custom correlator
4460
- * still tag tenant context or trace ids.
4461
- */
4348
+ /** Shared by `close()` and reaction-requested closes. */
4462
4349
  async _run_close(targets) {
4463
4350
  const close_actor = { id: "$close", name: "close" };
4464
4351
  const result = await run_close_cycle(targets, {
@@ -4539,10 +4426,7 @@ var Act = class {
4539
4426
  */
4540
4427
  async close(targets) {
4541
4428
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
4542
- return this._scoped(async () => {
4543
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4544
- return this._run_close(targets);
4545
- });
4429
+ return this._scoped(() => this._run_close(targets));
4546
4430
  }
4547
4431
  /**
4548
4432
  * Debounced, non-blocking correlate→drain cycle.