@rotorsoft/act 1.32.23 → 1.32.25

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))
@@ -1584,71 +1565,27 @@ function report_lane_conflict(seen, handler, target, kept, dropped) {
1584
1565
  var CorrelateCycle = class {
1585
1566
  _checkpoint = -1;
1586
1567
  _initialized = false;
1587
- /**
1588
- * This worker's identity for the correlation lease. A per-instance
1589
- * UUID, matching the drain's convention, so a renewal is recognised as the
1590
- * same holder and a restarted process never inherits a stale claim.
1591
- */
1568
+ /** Lease holder id; per instance, so a restart never inherits a stale claim. */
1592
1569
  _by = randomUUID();
1593
- /**
1594
- * Which correlator this is. Computed once from the registry so identical
1595
- * workers share a lease and unrelated applications never do.
1596
- */
1597
1570
  _key;
1598
- /**
1599
- * When this worker's correlation lease runs out, as a local clock reading,
1600
- * or 0 when it holds none.
1601
- *
1602
- * Cached so the holder doesn't ask the store on every pass. A stale belief
1603
- * can only cause a duplicate scan, and marks are idempotent, so it costs
1604
- * work, never correctness.
1605
- */
1571
+ /** Local expiry of the held lease (0 = none); stale only costs a duplicate scan. */
1606
1572
  _lease_until = 0;
1607
- /**
1608
- * Whether a scan might find anything, like the drain's flag: a commit or
1609
- * notification raises it, a short page lowers it, and a disarmed scan
1610
- * returns without touching the store.
1611
- *
1612
- * Starts armed: the log may already hold events this process has never
1613
- * correlated, and only a scan can find out.
1614
- */
1573
+ /** Raised by a commit or notification, lowered by a short page. Starts armed. */
1615
1574
  _armed = true;
1616
- /** In-flight init, memoized for single-flight and cleared on failure. */
1617
1575
  _init_promise;
1618
- // Dynamically discovered targets → what each was last subscribed at,
1619
- // bounded by `maxSubscribedStreams`. It decides *what* a scan sends with a
1620
- // target: priority/lane only when the resolution beats the recorded floor,
1621
- // otherwise the row's own values. See {@link Subscription}.
1576
+ // Bounded by `maxSubscribedStreams`; static records live apart so eviction
1577
+ // never drops their +Infinity floor.
1622
1578
  _dynamic_subscriptions;
1623
- /**
1624
- * What each static target was subscribed at by `init`. A plain map, never
1625
- * evicted: the collection is the build-time `_static_targets` list, so it
1626
- * is already bounded by the registry and costs nothing the registry does
1627
- * not already hold.
1628
- *
1629
- * Kept out of the LRU so eviction can never drop a static target's
1630
- * `+Infinity` floor and let a dynamic resolution re-lane it.
1631
- */
1632
1579
  _static_subscriptions = /* @__PURE__ */ new Map();
1633
- /** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
1634
1580
  _patterns = new LruMap(PATTERN_CACHE_SIZE);
1635
1581
  _registry;
1636
1582
  _static_targets;
1637
1583
  _cd;
1638
1584
  _on_init;
1639
- /**
1640
- * Async cold-start hook, run once after `on_init` inside `init()`. The
1641
- * orchestrator re-seeds its defer timers from the persisted `deferred_at`
1642
- * here, so an idle deferred stream re-arms across a restart.
1643
- */
1585
+ /** Runs once after `on_init`; re-seeds defer timers across a restart. */
1644
1586
  _on_init_async;
1645
- /** Lanes a controller exists for. See {@link CorrelateCycleDeps}. */
1646
1587
  _declared_lanes;
1647
- /**
1648
- * Offending declarations already reported, so a resolver firing for every
1649
- * matching event reports once. Owned by the instance rather than the
1650
- * module: `internal/` holds no module-level state.
1651
- */
1588
+ /** Report-once keys; per instance because `internal/` holds no module state. */
1652
1589
  _reported = /* @__PURE__ */ new Set();
1653
1590
  constructor({
1654
1591
  registry,
@@ -1669,28 +1606,13 @@ var CorrelateCycle = class {
1669
1606
  this._on_init = on_init;
1670
1607
  this._on_init_async = on_init_async;
1671
1608
  }
1672
- /** Last correlated event id. */
1673
1609
  get checkpoint() {
1674
1610
  return this._checkpoint;
1675
1611
  }
1676
- /**
1677
- * Signal that a commit (local or remote) may have produced events this
1678
- * process has not correlated. Cheap and idempotent — the orchestrator calls
1679
- * it on every commit and every notification.
1680
- */
1612
+ /** A commit (local or remote) may have produced uncorrelated events. */
1681
1613
  arm() {
1682
1614
  this._armed = true;
1683
1615
  }
1684
- /**
1685
- * Initialize correlation state on first call.
1686
- * - Reads the durable correlate checkpoint (and max(at)) from the store,
1687
- * flooring a first boot at `watermark - back_scan` so an event
1688
- * committed-but-not-correlated before a crash is re-scanned on
1689
- * restart instead of skipped
1690
- * - Subscribes static resolver targets (idempotent upsert)
1691
- * - Populates the subscribed-streams LRU
1692
- * - Fires `on_init` once (Act uses this to flag a cold-start drain)
1693
- */
1694
1616
  async init() {
1695
1617
  if (this._initialized) return;
1696
1618
  if (!this._init_promise) {
@@ -1717,16 +1639,7 @@ var CorrelateCycle = class {
1717
1639
  }
1718
1640
  await this._on_init_async?.();
1719
1641
  }
1720
- /**
1721
- * Start correlation over after the event log was replaced wholesale
1722
- * (`restore`). The restored log is renumbered from the start and its
1723
- * subscription rows are gone, so the in-memory scan position and both
1724
- * subscription records describe a log that no longer exists. Dropping them
1725
- * and the init latch makes the next pass run cold-start again: static
1726
- * targets are re-subscribed, the checkpoint is re-read from the store
1727
- * (which `restore` reset), and dynamic targets are rediscovered by the
1728
- * scan.
1729
- */
1642
+ /** After `restore` replaced the log: forget everything and cold-start again. */
1730
1643
  restart() {
1731
1644
  this._initialized = false;
1732
1645
  this._init_promise = void 0;
@@ -1735,21 +1648,11 @@ var CorrelateCycle = class {
1735
1648
  this._static_subscriptions.clear();
1736
1649
  this._armed = true;
1737
1650
  }
1738
- /**
1739
- * Forget dynamic targets whose subscription rows no longer exist (a full
1740
- * close deletes the row), so a later scan re-subscribes them. Static
1741
- * targets keep their records.
1742
- */
1651
+ /** Forget dynamic targets whose rows a full close deleted, so they re-subscribe. */
1743
1652
  forget_subscribed(streams) {
1744
1653
  for (const stream of streams) this._dynamic_subscriptions.delete(stream);
1745
1654
  }
1746
- /**
1747
- * Would an event from `stream` be fetched for a target subscribed with
1748
- * `source`? The subscription's source is the filter `fetch` queries with
1749
- * — literal names by equality, patterns compiled as a `RegExp` — so an
1750
- * event outside it is not work for that target and must not mark it.
1751
- * No source means the target consumes every stream.
1752
- */
1655
+ /** Would the target's own `fetch` (filtered by `source`) return this event? */
1753
1656
  _in_fetch_window(source, stream) {
1754
1657
  if (source === void 0 || source === stream) return true;
1755
1658
  if (is_literal_source(source)) return false;
@@ -1760,16 +1663,7 @@ var CorrelateCycle = class {
1760
1663
  }
1761
1664
  return pattern.test(stream);
1762
1665
  }
1763
- /**
1764
- * Scan the events past the checkpoint, resolve each to its target
1765
- * streams, and record what it found through `cd.subscribe` — new dynamic
1766
- * targets get registered, and every target an event resolved to gets its
1767
- * **work mark** raised to that event's id.
1768
- *
1769
- * Both resolver kinds are walked. A static target is already subscribed
1770
- * at init, but marking it is what makes it claimable without probing the
1771
- * event log, so the scan runs for every app.
1772
- */
1666
+ /** Scan past the checkpoint, subscribe new targets and raise their work marks. */
1773
1667
  async correlate(query = { after: -1, limit: 10 }, lease = false) {
1774
1668
  await this.init();
1775
1669
  if (!this._armed)
@@ -1854,10 +1748,8 @@ var CorrelateCycle = class {
1854
1748
  }
1855
1749
  }
1856
1750
  },
1857
- // Decline the sensitive payload: the scan only needs name/stream/id,
1858
- // and one undecryptable row would otherwise stop every stream's
1859
- // reactions. Resolvers therefore see `pii: null`; correlate runs
1860
- // actor-less, so plaintext here would be un-gated disclosure.
1751
+ // Resolvers see `pii: null`: correlate runs actor-less, and one
1752
+ // undecryptable row would otherwise stop every stream's reactions.
1861
1753
  { ...query, after, with_pii: false }
1862
1754
  );
1863
1755
  if (this._declared_lanes.size > 1) await this._recall(correlated);
@@ -1900,14 +1792,9 @@ var CorrelateCycle = class {
1900
1792
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1901
1793
  }
1902
1794
  /**
1903
- * Give each target this scan had no record of the record its row holds.
1904
- *
1905
- * A target with no record is either new or forgotten. Its row, when it
1906
- * has one, says what lane and priority it was subscribed at; a resolution
1907
- * that doesn't beat that priority carries the row's values, exactly as if
1908
- * the record had never been evicted. One read per target with no record,
1909
- * and only when more than one lane is declared: with one lane there is no
1910
- * lane to lose, and the store already keeps the higher priority.
1795
+ * Read the row of each target this scan had no record of (new or evicted),
1796
+ * so a resolution that doesn't beat its priority keeps the row's lane.
1797
+ * Only called with more than one lane declared.
1911
1798
  */
1912
1799
  async _recall(correlated) {
1913
1800
  const missed = [...correlated].filter(([, entry]) => entry.missed);
@@ -1943,13 +1830,7 @@ var CorrelateCycle = class {
1943
1830
  })
1944
1831
  );
1945
1832
  }
1946
- /**
1947
- * Hand the correlation lease back early.
1948
- *
1949
- * The port has no release verb; re-acquiring as the same holder with a
1950
- * zero duration releases it. Best-effort: a failure costs the lease's
1951
- * remaining lifetime, as a crash would.
1952
- */
1833
+ /** Release the lease early by re-acquiring it for zero ms. Best-effort. */
1953
1834
  async release_correlation() {
1954
1835
  try {
1955
1836
  this._lease_until = 0;
@@ -2232,6 +2113,18 @@ var validating_patch = (me, state2, partial, event) => {
2232
2113
  );
2233
2114
  };
2234
2115
  var DEFAULT_BATCH = 500;
2116
+ var cache_put = (stream, entry) => cache().set(stream, entry).catch(
2117
+ (err) => log().warn(
2118
+ err,
2119
+ "cache set skipped; the next load re-folds from the store"
2120
+ )
2121
+ );
2122
+ var cache_drop = (stream) => cache().invalidate(stream).catch(
2123
+ (err) => log().warn(
2124
+ err,
2125
+ "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
2126
+ )
2127
+ );
2235
2128
  async function snap(snapshot) {
2236
2129
  const { id, stream, name, meta, version } = snapshot.event;
2237
2130
  try {
@@ -2426,38 +2319,28 @@ async function load(me, target, callback, patch_fn = bare_patch) {
2426
2319
  {
2427
2320
  stream,
2428
2321
  stream_exact: true,
2429
- // `with_snaps` on every non-time-travel load. Cold, it resumes from the
2430
- // latest snapshot (this is the only place that decides it; stores just
2431
- // apply it). Warm, it keeps a snapshot above a stale checkpoint in the
2432
- // result, so a windowed close that pruned the events in between can't
2433
- // make the fold apply the tail onto stale state.
2322
+ // `with_snaps` on every non-time-travel load: cold, it resumes from the
2323
+ // latest snapshot; warm, it keeps a snapshot above a stale checkpoint,
2324
+ // so a windowed close's prune can't fold the tail onto stale state.
2434
2325
  ...cached ? { after: cached.event_id, with_snaps: true } : { ...time_travel ? {} : { with_snaps: true }, ...asOf }
2435
2326
  }
2436
2327
  );
2437
2328
  const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
2438
2329
  if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
2439
- await cache().set(stream, {
2330
+ await cache_put(stream, {
2440
2331
  stream,
2441
2332
  state: state2,
2442
2333
  version: event.version,
2443
2334
  event_id: event.id,
2444
2335
  patches,
2445
2336
  snaps
2446
- }).catch(
2447
- (err) => log().warn(
2448
- err,
2449
- "cache set skipped; the next load re-folds from the store"
2450
- )
2451
- );
2337
+ });
2452
2338
  }
2453
2339
  return {
2454
2340
  event,
2455
2341
  state: state2,
2456
2342
  version: event?.version ?? cached?.version ?? -1,
2457
- // Head event id, captured atomically with `state`: the last replayed
2458
- // event's id on a cache miss, or the cached checkpoint's event_id on a
2459
- // warm hit with no new events. Consumers read this instead of a
2460
- // separate cache lookup that could race a concurrent commit.
2343
+ // Captured with `state`, so consumers never race a separate cache lookup.
2461
2344
  id: event?.id ?? cached?.event_id ?? -1,
2462
2345
  patches,
2463
2346
  snaps,
@@ -2519,8 +2402,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
2519
2402
  action: {
2520
2403
  name: action2,
2521
2404
  ...target
2522
- // payload intentionally omitted from causation metadata —
2523
- // callers correlate via the correlation id when they need it.
2405
+ // payload intentionally omitted
2524
2406
  },
2525
2407
  event: reactingTo ? {
2526
2408
  id: reactingTo.id,
@@ -2540,14 +2422,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
2540
2422
  reactingTo ? expectedVersion : expected
2541
2423
  );
2542
2424
  } catch (error) {
2543
- if (error instanceof ConcurrencyError) {
2544
- cache().invalidate(stream).catch(
2545
- (err) => log().warn(
2546
- err,
2547
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
2548
- )
2549
- );
2550
- }
2425
+ if (error instanceof ConcurrencyError) void cache_drop(stream);
2551
2426
  throw error;
2552
2427
  }
2553
2428
  let { state: state2, patches } = snapshot;
@@ -2574,26 +2449,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
2574
2449
  const snap_event = snapped ? await snap(last) : void 0;
2575
2450
  if (!me.pii_aware) {
2576
2451
  if (contiguous)
2577
- cache().set(stream, {
2452
+ void cache_put(stream, {
2578
2453
  stream,
2579
2454
  state: last.state,
2580
2455
  version: snap_event?.version ?? last.event.version,
2581
2456
  event_id: snap_event?.id ?? last.event.id,
2582
2457
  patches: snap_event ? 0 : last.patches,
2583
2458
  snaps: snap_event ? last.snaps + 1 : last.snaps
2584
- }).catch(
2585
- (err) => log().warn(
2586
- err,
2587
- "cache set skipped; the next load re-folds from the store"
2588
- )
2589
- );
2590
- else
2591
- cache().invalidate(stream).catch(
2592
- (err) => log().warn(
2593
- err,
2594
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
2595
- )
2596
- );
2459
+ });
2460
+ else void cache_drop(stream);
2597
2461
  }
2598
2462
  return snapshots;
2599
2463
  } catch (error) {
@@ -2923,61 +2787,35 @@ var DrainController = class {
2923
2787
  _locked = false;
2924
2788
  _ratio = 0.5;
2925
2789
  /**
2926
- * Local wake for streams this worker parked (backoff or defer). The
2927
- * schedule itself is persisted in the store; this runs the drain at the
2928
- * earliest pending visit. A parked stream already carries its mark, so
2929
- * the drain alone finds it, and on an idle app nothing else would.
2930
- * Every drain re-aims it when it ends, and a due entry stays until a pass
2931
- * handles it or a pass that started after it came due claims nothing, so
2932
- * the wake keeps draining until every due stream is reached.
2933
- * `stop()` cancels the timer and a stopped drain never re-schedules it,
2934
- * so a wake never runs after a stop.
2790
+ * Local wake at the earliest parked (backoff or defer) visit; the schedule
2791
+ * itself is persisted. On an idle app nothing else would drain it. A due
2792
+ * entry stays until handled or until a later pass claims nothing.
2935
2793
  */
2936
2794
  _defer = new DeferTimer(() => {
2937
2795
  this._armed = true;
2938
2796
  void this._deps.run_scoped(() => this.drain());
2939
2797
  });
2940
- /** Worker timer. Set when `start()` is active, undefined otherwise. */
2941
2798
  _worker;
2942
- /**
2943
- * Misroutings this controller has reported. A resolver returning a
2944
- * projection's target does so for every matching event; one line per event
2945
- * would bury the signal it exists to raise.
2946
- */
2947
2799
  _misrouted = /* @__PURE__ */ new Set();
2948
2800
  _stopped = false;
2949
- /**
2950
- * Resolves when the cycle in flight finishes, so a graceful shutdown can
2951
- * wait for it. Never rejects.
2952
- */
2801
+ /** Never rejects; graceful shutdown waits on it. */
2953
2802
  _inflight;
2954
2803
  _inflight_done;
2955
2804
  _deps;
2956
2805
  constructor(deps) {
2957
2806
  this._deps = deps;
2958
2807
  }
2959
- /**
2960
- * Signal that a commit (or reset / cold-start) may have produced work.
2961
- * Subsequent `drain()` calls will run the pipeline; once the pipeline
2962
- * settles to no-progress, the controller disarms itself.
2963
- */
2964
2808
  arm() {
2965
2809
  this._armed = true;
2966
2810
  }
2967
- /**
2968
- * Re-seed a persisted `deferred_at` into the local wake at cold start, so
2969
- * an idle deferred stream re-arms at its due-time with no new commit.
2970
- * Seeds collapse into one timer; the earliest wins.
2971
- */
2811
+ /** Cold start: re-seed a persisted `deferred_at` into the local wake. */
2972
2812
  seed_defer(stream, at) {
2973
2813
  this._defer.set(stream, at);
2974
2814
  this._defer.schedule();
2975
2815
  }
2976
- /** Read-only flag — true while a commit / reset is unprocessed. */
2977
2816
  get armed() {
2978
2817
  return this._armed;
2979
2818
  }
2980
- /** The cycle in flight, or `undefined` when idle. */
2981
2819
  get inflight() {
2982
2820
  return this._inflight;
2983
2821
  }
@@ -2985,19 +2823,10 @@ var DrainController = class {
2985
2823
  get lease_millis() {
2986
2824
  return this._deps.defaults?.leaseMillis;
2987
2825
  }
2988
- /** Lane this controller drains (undefined spans all lanes). */
2989
2826
  get lane() {
2990
2827
  return this._deps.lane;
2991
2828
  }
2992
- /**
2993
- * Start a per-lane worker that drains at the lane's `cycleMs`
2994
- * cadence. When armed, the worker calls `drain()` on every
2995
- * tick and re-schedules; when not armed, it still re-schedules at
2996
- * `cycleMs` so a future `arm()` is picked up on the next tick.
2997
- *
2998
- * The setTimeout chain uses `unref()` so it doesn't keep the process
2999
- * alive on its own.
3000
- */
2829
+ /** Drains every `cycleMs` while armed; the unref'd timer never holds the process. */
3001
2830
  start(cycleMs) {
3002
2831
  if (this._worker || this._stopped) return;
3003
2832
  const run = this._deps.run_scoped;
@@ -3010,7 +2839,6 @@ var DrainController = class {
3010
2839
  this._worker = setTimeout(tick, cycleMs);
3011
2840
  this._worker.unref();
3012
2841
  }
3013
- /** Stop the per-lane worker. Idempotent. */
3014
2842
  stop() {
3015
2843
  this._stopped = true;
3016
2844
  if (this._worker) {
@@ -3019,7 +2847,6 @@ var DrainController = class {
3019
2847
  }
3020
2848
  this._defer.stop();
3021
2849
  }
3022
- /** Run one drain pass. Short-circuits when not armed or already running. */
3023
2850
  async drain(options = {}) {
3024
2851
  if (!this._armed) return EMPTY_DRAIN;
3025
2852
  if (this._locked) return EMPTY_DRAIN;
@@ -3393,41 +3220,23 @@ function validate_only_lanes(options, lanes) {
3393
3220
  }
3394
3221
  var Act = class {
3395
3222
  _emitter = new EventEmitter();
3396
- /** Circuit breaker shared by all drain lanes. */
3397
3223
  _breaker;
3398
- /** Whether this instance subscribes to `Store.notify`. */
3399
3224
  _listen;
3400
- /** Whether this instance runs reactions (drain, settle, correlate). */
3401
3225
  _drain;
3402
- /** Event names with at least one registered reaction (computed at build time) */
3403
3226
  _reactive_events;
3404
- /** One DrainController per active lane, keyed by lane name. */
3405
3227
  _drain_controllers;
3406
- /** Correlation: lazy init and the scan that marks work. */
3407
3228
  _correlate;
3408
- /** Debounced correlate→drain catch-up loop. */
3409
3229
  _settle;
3410
- /** Timer behind {@link start_correlations}. */
3411
3230
  _poll;
3412
3231
  /** The deprecated `start_correlations` callback, while the poll runs. */
3413
3232
  _on_discovered;
3414
3233
  /**
3415
- * Disposer for the cross-process notify subscription, set up eagerly
3416
- * during construction. Held as a promise because the subscription
3417
- * itself may be async (the PG adapter checks out a dedicated client
3418
- * and runs `LISTEN` before resolving). Resolves to `undefined` when
3419
- * the store doesn't implement `notify` or there are no registered
3420
- * reactions.
3421
- *
3422
- * **Contract:** the configured store must be injected via
3423
- * {@link store}`(adapter)` *before* calling `act()...build()`. The
3424
- * orchestrator wires notify against whatever store is current at
3425
- * construction time — late injection after build is unsupported.
3234
+ * Wired at construction against the store current then, so a store
3235
+ * injected after `build()` is never notified.
3426
3236
  */
3427
3237
  _notify_disposer;
3428
3238
  /** Public registry — kept as-is per the no-prefix-on-public convention. */
3429
3239
  registry;
3430
- /** Map of state name → state definition; populated by the builder. */
3431
3240
  _states;
3432
3241
  /**
3433
3242
  * Emit a lifecycle event. The payload type is inferred from the event name
@@ -3453,15 +3262,9 @@ var Act = class {
3453
3262
  return listeners.length > 0;
3454
3263
  }
3455
3264
  /**
3456
- * The single store-failure handler: log it, then emit the `error`
3457
- * lifecycle event. Wired into the circuit breaker's `on_error`, so it
3458
- * runs on every `failed()` — drain / settle / autoclose just call
3459
- * `breaker.failed(now, error)` and never log or emit themselves.
3460
- *
3461
- * The emit is guarded against Node's `EventEmitter` contract that an
3462
- * unhandled `"error"` emission is rethrown (which would crash the process
3463
- * from inside the drain catch); logging is unconditional so failures are
3464
- * never silent.
3265
+ * The one place a store failure is logged and emitted (the breaker's
3266
+ * `on_error`). The emit is guarded: an unhandled `"error"` emission
3267
+ * would crash the process.
3465
3268
  */
3466
3269
  _emit_error(error, circuit) {
3467
3270
  this._logger.error(error);
@@ -3483,58 +3286,29 @@ var Act = class {
3483
3286
  this._emitter.off(event, listener);
3484
3287
  return this;
3485
3288
  }
3486
- /** Batch handlers for static-target projections (target → handler) */
3487
3289
  _batch_handlers;
3488
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
3489
3290
  _es;
3490
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
3491
3291
  _cd;
3492
- /**
3493
- * Event-name → owning state, computed at build time. The duplicate-event
3494
- * guard in merge.ts ensures one event name maps to at most one state, so
3495
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
3496
- * set when seeding a `restart` snapshot in multi-state apps.
3497
- */
3292
+ /** Unambiguous: merge rejects an event name declared by two states. */
3498
3293
  _event_to_state;
3499
- /**
3500
- * Event-name → lane fan-in for selective arming. Built by
3501
- * `classify_registry` once per build. `"all"` means at least one of
3502
- * the event's reactions is a dynamic resolver (lane opaque until
3503
- * runtime); a `Set<string>` lists the static lanes only that event's
3504
- * reactions target.
3505
- */
3294
+ /** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
3506
3295
  _event_to_lanes;
3507
- /** What `internal/audit.ts` needs, built once at construction. */
3508
3296
  _audit_deps;
3509
- /** Logger resolved at construction time (after user port configuration) */
3510
3297
  _logger = log();
3511
3298
  /** This Act's ports: its own bag, or the singleton adapters. */
3512
3299
  _ports;
3513
- /**
3514
- * Runs a body inside this Act's ports frame, so internal
3515
- * `store()`/`cache()` calls resolve to `_ports`.
3516
- */
3300
+ /** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
3517
3301
  _scoped;
3518
- /**
3519
- * Correlation-id generator for originating actions. Bound at
3520
- * construction from `options.correlator ?? default_correlator`. The
3521
- * `do()` path passes this into the `_es.action` closure; close-cycle
3522
- * uses it via {@link close_correlation}.
3523
- */
3524
3302
  _correlator;
3525
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
3526
- * payload (it captures the triggering event for reactingTo auto-inject). */
3303
+ // Bound once, reused by every reaction payload.
3527
3304
  _bound_do = this.do.bind(this);
3528
3305
  _bound_load = this.load.bind(this);
3529
3306
  _bound_query = this.query.bind(this);
3530
3307
  _bound_query_array = this.query_array.bind(this);
3531
3308
  _bound_forget = this.forget.bind(this);
3532
- /** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
3533
3309
  _handle;
3534
3310
  _handle_batch;
3535
- /** Declared drain lanes. */
3536
3311
  _lanes;
3537
- /** Serializes close critical sections per stream. */
3538
3312
  _close_lock = new CloseLock();
3539
3313
  /** Every lane a controller can exist for: `"default"` plus each declared one. */
3540
3314
  _declared_lanes;
@@ -3542,26 +3316,7 @@ var Act = class {
3542
3316
  get lanes() {
3543
3317
  return this._lanes;
3544
3318
  }
3545
- /**
3546
- * Create a new Act orchestrator. Prefer the {@link act} builder over
3547
- * direct construction — `act()...build()` wires the registry, merges
3548
- * partial states, and collects batch handlers from registered slices
3549
- * and projections in one pass.
3550
- *
3551
- * @param registry Schemas for every event and action across registered states
3552
- * @param states Merged map of state name → state definition
3553
- * @param batch_handlers Static-target projection batch handlers (target → handler)
3554
- * @param options Tuning knobs — see {@link ActOptions}
3555
- * @param lanes Declared drain lanes. The builder collects
3556
- * these from `.withLane(...)` calls; one `DrainController` runs
3557
- * per lane, plus the implicit `"default"`.
3558
- * @param patch_fn The per-event patch step selected once by the builder
3559
- * from `ActOptions.validateFoldedState` — `bare_patch` by
3560
- * default, `validating_patch` when the flag is on. The builder uses
3561
- * the same value for its projection-fold handlers, so there is a
3562
- * single selection site. Defaults to `bare_patch` for direct
3563
- * construction.
3564
- */
3319
+ /** Use the {@link act} builder rather than constructing directly. */
3565
3320
  constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
3566
3321
  this.registry = registry;
3567
3322
  this._states = states;
@@ -3612,11 +3367,7 @@ var Act = class {
3612
3367
  this._notify_disposer = this._wire_notify(this._ports.store);
3613
3368
  register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
3614
3369
  }
3615
- /**
3616
- * Circuit breaker shared by every store-polling loop (drain, the settle
3617
- * correlate, autoclose). Validates the knobs eagerly so out-of-range
3618
- * values throw at build time, not on the first cycle tick.
3619
- */
3370
+ /** Shared by drain, settle and autoclose; bad knobs throw at build. */
3620
3371
  _build_breaker(options) {
3621
3372
  return new CircuitBreaker(
3622
3373
  resolveCircuitBreakerConfig(options.circuitBreaker),
@@ -3630,11 +3381,7 @@ var Act = class {
3630
3381
  }
3631
3382
  );
3632
3383
  }
3633
- /**
3634
- * One DrainController per active lane, plus the implicit "default" unless
3635
- * `onlyLanes` excludes it. Apps that declare no lanes pass no lane filter
3636
- * to `claim`.
3637
- */
3384
+ /** Apps that declare no lanes pass no lane filter to `claim`. */
3638
3385
  _build_drain_controllers(options, lanes) {
3639
3386
  const all_lanes = this._declared_lanes;
3640
3387
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
@@ -3670,11 +3417,7 @@ var Act = class {
3670
3417
  }
3671
3418
  return controllers;
3672
3419
  }
3673
- /**
3674
- * Startup advisory naming declared lanes that `onlyLanes` leaves without a
3675
- * controller here. One process can't check that the cluster covers every
3676
- * lane, so it reports its own gaps.
3677
- */
3420
+ /** One process can't see the cluster, so it reports its own lane gaps. */
3678
3421
  _advise_orphaned_lanes(options) {
3679
3422
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3680
3423
  const active = new Set(this._drain_controllers.keys());
@@ -3696,11 +3439,6 @@ var Act = class {
3696
3439
  routed_events: new Set(this._event_to_lanes.keys())
3697
3440
  };
3698
3441
  }
3699
- /**
3700
- * Correlate cycle over the classified registry. The cold-start callback
3701
- * arms every controller — historical events may need processing —
3702
- * except on writer-only instances (`drain: false`).
3703
- */
3704
3442
  _build_correlate(options, classification) {
3705
3443
  return new CorrelateCycle({
3706
3444
  registry: this.registry,
@@ -3718,12 +3456,7 @@ var Act = class {
3718
3456
  on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
3719
3457
  });
3720
3458
  }
3721
- /**
3722
- * At cold start, re-seed each lane controller's local wake from the
3723
- * persisted future `deferred_at`, so an idle deferred stream (an
3724
- * autoclose waiting out its window) still fires on time. Streams on lanes
3725
- * this instance doesn't run are skipped.
3726
- */
3459
+ /** Cold start: an idle deferred stream still fires on time after a restart. */
3727
3460
  async _seed_persisted_defers() {
3728
3461
  const now = Date.now();
3729
3462
  await walk_streams(store(), (pos) => {
@@ -3732,7 +3465,6 @@ var Act = class {
3732
3465
  controller?.seed_defer(pos.stream, pos.deferred_at);
3733
3466
  });
3734
3467
  }
3735
- /** Settle loop driving correlate + drain to quiescence. */
3736
3468
  _build_settle(options) {
3737
3469
  return new SettleLoop(
3738
3470
  {
@@ -3746,7 +3478,6 @@ var Act = class {
3746
3478
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
3747
3479
  );
3748
3480
  }
3749
- /** True after the first `shutdown()` call. Guards idempotency. */
3750
3481
  _shutdown_promise;
3751
3482
  /**
3752
3483
  * Per-instance teardown: stop scheduling new work, give drain cycles
@@ -3786,14 +3517,8 @@ var Act = class {
3786
3517
  return this._shutdown_promise;
3787
3518
  }
3788
3519
  /**
3789
- * Wait for every in-flight drain cycle, or for the grace budget to elapse,
3790
- * whichever comes first. Cycle promises never reject (`drain()` contains
3791
- * its own errors), so this never throws.
3792
- *
3793
- * An omitted budget is the largest `leaseMillis` among lanes with a cycle
3794
- * in flight (the drain's fallback for lanes that pinned none, or pinned
3795
- * 0, which is the same as none), with an in-flight settle counting on the
3796
- * fallback, capped at the maximum. Idle lanes don't count.
3520
+ * The largest `leaseMillis` among lanes with a cycle in flight (the
3521
+ * default for none or 0, and for a running settle), capped.
3797
3522
  */
3798
3523
  _derive_grace_ms(running, settling) {
3799
3524
  let max = settling ? DEFAULT_LEASE_MILLIS : 0;
@@ -3822,12 +3547,7 @@ var Act = class {
3822
3547
  clearTimeout(timer);
3823
3548
  }
3824
3549
  }
3825
- /**
3826
- * Subscribe to {@link Store.notify} when both the store and the
3827
- * registry support it. Returns the disposer (or `undefined` when no
3828
- * subscription was made). Errors during subscription are logged but
3829
- * never thrown — `notify` is a hint, not a contract.
3830
- */
3550
+ /** Failures are logged, never thrown: `notify` is a hint. */
3831
3551
  async _wire_notify(s) {
3832
3552
  if (this._reactive_events.size === 0) return void 0;
3833
3553
  if (!s.notify) return void 0;
@@ -4139,20 +3859,11 @@ var Act = class {
4139
3859
  return { fetched: [], leased: [], acked: [], blocked: [] };
4140
3860
  return this._scoped(() => this._drain_all(options));
4141
3861
  }
4142
- /** Arm every active lane controller. */
4143
3862
  _arm_all() {
4144
3863
  this._correlate.arm();
4145
3864
  for (const c of this._drain_controllers.values()) c.arm();
4146
3865
  }
4147
- /**
4148
- * Arm only the lane controllers whose reactions match the supplied
4149
- * event names (selective arming). Events with any dynamic
4150
- * resolver fall back to `_arm_all()` via the `"all"` sentinel — the
4151
- * resolver's lane isn't known until correlate runs the function.
4152
- * Events with no reactions are skipped; `_event_to_lanes` doesn't
4153
- * carry them. Returns true when any controller was armed (used by
4154
- * the notify handler to decide whether to schedule a settle).
4155
- */
3866
+ /** Arms only the lanes these events react on; true when any was armed. */
4156
3867
  _arm_for_event_names(names) {
4157
3868
  const to_arm = /* @__PURE__ */ new Set();
4158
3869
  for (const name of names) {
@@ -4169,14 +3880,7 @@ var Act = class {
4169
3880
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
4170
3881
  return true;
4171
3882
  }
4172
- /** Drain every active lane controller in parallel and aggregate.
4173
- *
4174
- * Parallel — not sequential — so a slow lane's in-flight handler does
4175
- * not block a fast lane's claim/dispatch/ack cycle. Each controller's
4176
- * `claim()` is independent (filtered by lane); the store's
4177
- * `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
4178
- * (`acked`, `blocked`) may interleave by lane — listeners filter via
4179
- * `lease.lane`. */
3883
+ /** In parallel, so a slow lane never holds up a fast one. */
4180
3884
  async _drain_all(options) {
4181
3885
  const results = await Promise.all(
4182
3886
  [...this._drain_controllers.values()].map((c) => c.drain(options))
@@ -4249,14 +3953,7 @@ var Act = class {
4249
3953
  const { subscribed, last_id } = await this._correlate_scanned(query);
4250
3954
  return { subscribed, last_id };
4251
3955
  }
4252
- /**
4253
- * `correlate` plus whether the pass actually read the store.
4254
- *
4255
- * The settle loop needs that extra bit to decide whether the pass carries a
4256
- * circuit-breaker health signal, and a disarmed pass carries none. It stays
4257
- * internal rather than widening the public `correlate` return, which is
4258
- * charter-covered and has no use for it.
4259
- */
3956
+ /** `correlate` plus `scanned`, which settle needs for the breaker. */
4260
3957
  async _correlate_scanned(query, lease = false) {
4261
3958
  if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
4262
3959
  return this._scoped(async () => {
@@ -4597,11 +4294,7 @@ var Act = class {
4597
4294
  async prioritize(filter, priority) {
4598
4295
  return this._scoped(() => store().prioritize(filter, priority));
4599
4296
  }
4600
- /**
4601
- * Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
4602
- * reaction-requested closes. The close actor lets a custom correlator
4603
- * still tag tenant context or trace ids.
4604
- */
4297
+ /** Shared by `close()` and reaction-requested closes. */
4605
4298
  async _run_close(targets) {
4606
4299
  const close_actor = { id: "$close", name: "close" };
4607
4300
  const result = await run_close_cycle(targets, {
@@ -4682,10 +4375,7 @@ var Act = class {
4682
4375
  */
4683
4376
  async close(targets) {
4684
4377
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
4685
- return this._scoped(async () => {
4686
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4687
- return this._run_close(targets);
4688
- });
4378
+ return this._scoped(() => this._run_close(targets));
4689
4379
  }
4690
4380
  /**
4691
4381
  * Debounced, non-blocking correlate→drain cycle.