@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.cjs CHANGED
@@ -635,13 +635,9 @@ var InMemoryStream = class {
635
635
  _leased_until = void 0;
636
636
  _priority = 0;
637
637
  _lane = DEFAULT_LANE;
638
- // Persisted next-visit time. When set and still in the future, the
639
- // stream is held out of `claim` entirely — so a deferred reaction is not
640
- // re-claimed (and `retry` is never bumped) until its due-time passes. Unlike
641
- // in-process backoff, this is durable store state shared across workers.
638
+ // While in the future, the stream is held out of `claim`.
642
639
  _deferred_at = void 0;
643
- // Work mark: highest event id observed to resolve to this target.
644
- // `undefined` means the row was never correlated, so `claim` skips it.
640
+ // Work mark; `undefined` = never correlated, so `claim` skips it.
645
641
  _correlated_at = void 0;
646
642
  constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
647
643
  this.stream = stream;
@@ -655,44 +651,28 @@ var InMemoryStream = class {
655
651
  get lane() {
656
652
  return this._lane;
657
653
  }
658
- /** Replace on every subscribe — current builder config wins on restart. */
654
+ /** Replaced on every subscribe, so the current config wins on restart. */
659
655
  set lane(value) {
660
656
  this._lane = value;
661
657
  }
662
- /**
663
- * Bump the priority via {@link subscribe}: keeps the maximum across
664
- * reactions so the highest-priority registrant wins.
665
- */
666
658
  bump_priority(priority) {
667
659
  if (priority > this._priority) this._priority = priority;
668
660
  }
669
661
  get correlated_at() {
670
662
  return this._correlated_at;
671
663
  }
672
- /**
673
- * Raise the work mark via {@link subscribe}: keeps the maximum, for every
674
- * value including zero and negatives, so a mark never regresses.
675
- */
664
+ /** A mark never regresses, including at zero and negatives. */
676
665
  mark(correlated_at) {
677
666
  if (this._correlated_at === void 0 || correlated_at > this._correlated_at)
678
667
  this._correlated_at = correlated_at;
679
668
  }
680
- /**
681
- * Set the priority outright via {@link prioritize}: operator
682
- * runtime override that ignores the build-time `max()` invariant.
683
- */
669
+ /** Operator override; ignores the subscribe-time `max()`. */
684
670
  set_priority(priority) {
685
671
  this._priority = priority;
686
672
  }
687
673
  get is_available() {
688
- return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date()) && // A stream deferred to a future time is not claimable until due.
689
- (!this._deferred_at || this._deferred_at <= Date.now());
674
+ return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date()) && (!this._deferred_at || this._deferred_at <= Date.now());
690
675
  }
691
- /**
692
- * Hold this stream out of `claim` until `deferred_at` (ms since epoch).
693
- * Set by a deliberate `defer` outcome — not a failure, so retry/blocked
694
- * state is untouched. Cleared by ack/block/reset/unblock.
695
- */
696
676
  defer(deferred_at) {
697
677
  this._deferred_at = deferred_at;
698
678
  this._retry = -1;
@@ -715,16 +695,9 @@ var InMemoryStream = class {
715
695
  get leased_until() {
716
696
  return this._leased_until;
717
697
  }
718
- /** Persisted next-visit time, or undefined when no active defer. */
719
698
  get deferred_at() {
720
699
  return this._deferred_at;
721
700
  }
722
- /**
723
- * Attempt to lease this stream for processing.
724
- * @param lease - The lease request.
725
- * @param millis - Lease duration in milliseconds.
726
- * @returns The granted lease or undefined if blocked.
727
- */
728
701
  lease(lease, millis) {
729
702
  this._leased_by = lease.by;
730
703
  this._leased_until = new Date(Date.now() + millis);
@@ -739,12 +712,7 @@ var InMemoryStream = class {
739
712
  lane: this._lane
740
713
  };
741
714
  }
742
- /**
743
- * Finalize this stream's lease: ack (advance the watermark) or, when the
744
- * lease carries a `due` marker, defer (persist the schedule, hold the
745
- * watermark) — see {@link Store.ack}.
746
- * @param lease - The lease request.
747
- */
715
+ /** Ack, or with `due`, also persist the schedule. See {@link Store.ack}. */
748
716
  ack(lease) {
749
717
  if (this._leased_by === lease.by) {
750
718
  this._leased_by = void 0;
@@ -769,11 +737,6 @@ var InMemoryStream = class {
769
737
  };
770
738
  }
771
739
  }
772
- /**
773
- * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
774
- * @param lease - The lease request.
775
- * @param error Blocked error message.
776
- */
777
740
  block(lease, error) {
778
741
  if (this._leased_by === lease.by && !this._blocked) {
779
742
  this._blocked = true;
@@ -791,11 +754,7 @@ var InMemoryStream = class {
791
754
  };
792
755
  }
793
756
  }
794
- /**
795
- * Reset this stream's watermark and state for replay. The retry counter
796
- * resets to -1 to match the constructor + ack() invariant ("released
797
- * stream"); the next claim() bumps it to 0 (first attempt).
798
- */
757
+ /** Retry resets to -1 (released); the next claim makes it 0. */
799
758
  reset() {
800
759
  this._at = -1;
801
760
  this._retry = -1;
@@ -805,11 +764,7 @@ var InMemoryStream = class {
805
764
  this._leased_until = void 0;
806
765
  this._deferred_at = void 0;
807
766
  }
808
- /**
809
- * Clear the blocked flag and lease bookkeeping without touching the
810
- * watermark. Returns true if the stream was actually blocked (and is
811
- * now flipped); false otherwise.
812
- */
767
+ /** True only when the stream was blocked. */
813
768
  unblock() {
814
769
  if (!this._blocked) return false;
815
770
  this._blocked = false;
@@ -822,33 +777,17 @@ var InMemoryStream = class {
822
777
  }
823
778
  };
824
779
  var InMemoryStore = class {
825
- // stored events
826
780
  _events = [];
827
- // next event id — monotonic, never reused. Deletions (truncate, windowed
828
- // prune) punch holes in the id sequence, so ids are NOT array indexes and
829
- // NOT `_events.length`; they only stay sorted ascending in `_events`.
781
+ // Never reused. Deletions punch holes, so ids are not array indexes.
830
782
  _next_id = 0;
831
- // stored stream positions and other metadata
832
783
  _streams = /* @__PURE__ */ new Map();
833
784
  /** Correlate checkpoint: how far the log has been READ. */
834
785
  _correlated_at = -1;
835
- /**
836
- * Per-correlator checkpoint and lease, keyed by correlator.
837
- *
838
- * Keyed rather than singular because correlators that look for different
839
- * things read the log for different reasons: sharing one position lets a
840
- * partial-behaviour worker inherit another's, and sharing one lease lets
841
- * one starve the other. Callers that supply no correlator use
842
- * `_correlated_at` instead, the shared checkpoint.
843
- */
786
+ /** Per-correlator checkpoint and lease; keyless callers use `_correlated_at`. */
844
787
  _correlators = /* @__PURE__ */ new Map();
845
- // last committed version per stream — O(1) replacement for filter-on-commit
846
788
  _stream_versions = /* @__PURE__ */ new Map();
847
- // stream → (event_id → cloned sensitive payload). Two-level so `forget_pii`
848
- // is O(1) — drop the inner Map for the stream and the wipe is done — mirroring
849
- // the `DELETE WHERE stream = ?` scope that durable adapters get from their
850
- // stream index. Entries exist only for events committed with a non-null
851
- // `pii` field; absence means "no PII" (returned as `null` on load).
789
+ // stream → (event_id → cloned pii). Two levels so `forget_pii` drops one
790
+ // inner Map. Absence means no PII.
852
791
  _pii = /* @__PURE__ */ new Map();
853
792
  _reset_indexes() {
854
793
  this._events.length = 0;
@@ -866,10 +805,7 @@ var InMemoryStore = class {
866
805
  }
867
806
  per_stream.set(id, structuredClone(pii));
868
807
  }
869
- // First index whose event id is greater than `after`. `_events` stays
870
- // sorted ascending by id (append-only commits; deletions preserve
871
- // order), so id-bounded scans binary-search their start instead of
872
- // assuming id === index — an invariant that truncation breaks.
808
+ // First index whose id is greater than `after` (ids are sorted, not dense).
873
809
  _first_index_after(after) {
874
810
  let lo = 0;
875
811
  let hi = this._events.length;
@@ -880,32 +816,19 @@ var InMemoryStore = class {
880
816
  }
881
817
  return lo;
882
818
  }
883
- // Attach the isolated PII payload (or null) to an event before handing it to
884
- // a caller. Allocation-free for events without PII — by far the common case.
819
+ // Allocation-free for events without PII, the common case.
885
820
  _with_pii(e, wanted = true) {
886
821
  if (!wanted) return e;
887
822
  const pii = this._pii.get(e.stream)?.get(e.id);
888
823
  return pii ? { ...e, pii } : e;
889
824
  }
890
- /**
891
- * Dispose of the store and clear all events.
892
- * @returns Promise that resolves when disposal is complete.
893
- */
894
825
  async dispose() {
895
826
  await sleep();
896
827
  this._reset_indexes();
897
828
  }
898
- /**
899
- * Seed the store with initial data (no-op for in-memory).
900
- * @returns Promise that resolves when seeding is complete.
901
- */
902
829
  async seed() {
903
830
  await sleep();
904
831
  }
905
- /**
906
- * Drop all data from the store.
907
- * @returns Promise that resolves when the store is cleared.
908
- */
909
832
  async drop() {
910
833
  await sleep();
911
834
  this._reset_indexes();
@@ -925,12 +848,7 @@ var InMemoryStore = class {
925
848
  _stream_matches(pattern, query, stream) {
926
849
  return query.stream_exact ? stream === pattern : RegExp(pattern).test(stream);
927
850
  }
928
- /**
929
- * Index of the latest snapshot of the queried stream, or -1. `with_snaps`
930
- * on an exact single stream resumes there, so earlier events aren't read.
931
- * The orchestrator asks only for unbounded current-state loads, so the
932
- * floor applies whenever asked; an explicit `after` wins over it.
933
- */
851
+ /** Index of the stream's latest snapshot, or -1; an explicit `after` wins. */
934
852
  _snap_floor(query) {
935
853
  if (!query?.with_snaps || !query.stream_exact || query.stream === void 0 || query.after !== void 0)
936
854
  return -1;
@@ -940,12 +858,6 @@ var InMemoryStore = class {
940
858
  }
941
859
  return -1;
942
860
  }
943
- /**
944
- * Query events in the store, optionally filtered by query options.
945
- * @param callback - Function to call for each event.
946
- * @param query - Optional query options.
947
- * @returns The number of events processed.
948
- */
949
861
  async query(callback, query) {
950
862
  await sleep();
951
863
  const floor = this._snap_floor(query);
@@ -964,15 +876,6 @@ var InMemoryStore = class {
964
876
  }
965
877
  return count;
966
878
  }
967
- /**
968
- * Commit one or more events to a stream.
969
- * @param stream - The stream name.
970
- * @param msgs - The events/messages to commit.
971
- * @param meta - Event metadata.
972
- * @param expectedVersion - Optional optimistic concurrency check.
973
- * @returns The committed events with metadata.
974
- * @throws ConcurrencyError if expectedVersion does not match.
975
- */
976
879
  async commit(stream, msgs, meta, expectedVersion) {
977
880
  await sleep();
978
881
  const current_version = this._stream_versions.get(stream) ?? -1;
@@ -1003,15 +906,6 @@ var InMemoryStore = class {
1003
906
  this._stream_versions.set(stream, version - 1);
1004
907
  return committed;
1005
908
  }
1006
- /**
1007
- * Atomically discovers and leases streams for processing.
1008
- * Fuses poll + lease into a single operation.
1009
- * @param lagging - Max streams from lagging frontier.
1010
- * @param leading - Max streams from leading frontier.
1011
- * @param by - Lease holder identifier.
1012
- * @param millis - Lease duration in milliseconds.
1013
- * @returns Granted leases.
1014
- */
1015
909
  async claim(lagging, leading, by, millis, lane) {
1016
910
  await sleep();
1017
911
  const has_work = (s) => s.correlated_at !== void 0 && s.at < s.correlated_at;
@@ -1047,15 +941,6 @@ var InMemoryStore = class {
1047
941
  (p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
1048
942
  ).filter((l) => !!l);
1049
943
  }
1050
- /**
1051
- * Registers streams for event processing. When the same stream is
1052
- * resubscribed with a different priority, the **maximum** wins — so
1053
- * the highest-priority registered reaction sets the scheduling lane.
1054
- * Use {@link prioritize} for operator runtime overrides.
1055
- *
1056
- * @param streams - Streams to register with optional source + priority.
1057
- * @returns subscribed count and current max watermark.
1058
- */
1059
944
  async subscribe(streams, correlated_at, correlator) {
1060
945
  await sleep();
1061
946
  let correlating;
@@ -1070,9 +955,7 @@ var InMemoryStore = class {
1070
955
  this._correlators.set(correlator.key, {
1071
956
  at,
1072
957
  by: correlator.by,
1073
- // A non-positive `millis` releases outright rather than setting an
1074
- // expiry a hair in the future: a successor asking inside the same
1075
- // millisecond would otherwise still be refused.
958
+ // Release outright, or a successor in the same ms is still refused.
1076
959
  until: correlator.millis > 0 ? now + correlator.millis : 0
1077
960
  });
1078
961
  } else {
@@ -1114,53 +997,26 @@ var InMemoryStore = class {
1114
997
  ...correlating === void 0 ? {} : { correlating }
1115
998
  };
1116
999
  }
1117
- /**
1118
- * Acknowledge completion of processing for leased streams.
1119
- * @param leases - Leases to acknowledge, including last processed watermark and lease holder.
1120
- */
1121
1000
  async ack(leases) {
1122
1001
  await sleep();
1123
1002
  return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
1124
1003
  }
1125
- /**
1126
- * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
1127
- * @param leases - Leases to block, including lease holder and last error message.
1128
- * @returns Blocked leases.
1129
- */
1130
1004
  async block(leases) {
1131
1005
  await sleep();
1132
1006
  return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
1133
1007
  }
1134
- /**
1135
- * Hold the matched streams out of {@link claim} until `deferred_at`
1136
- * (ms since epoch) — see {@link Store.defer}. Accepts an explicit list
1137
- * of names or a {@link StreamFilter}, mirroring {@link reset}/{@link unblock}.
1138
- * Persisted store state (unlike in-process backoff), so the skip is honored
1139
- * by every competing worker. Unknown names are silently skipped.
1140
- *
1141
- * @returns Count of streams whose `deferred_at` was set.
1142
- */
1143
1008
  async defer(input, deferred_at) {
1144
1009
  await sleep();
1145
1010
  const selected = this._select(input);
1146
1011
  for (const s of selected) s.defer(deferred_at);
1147
1012
  return selected.length;
1148
1013
  }
1149
- /**
1150
- * The streams named by `input`: an explicit list (de-duplicated, so a
1151
- * repeated name counts once, matching the SQL adapters' set-based
1152
- * `stream = ANY(...)`; unknown names are skipped) or a {@link StreamFilter}.
1153
- */
1014
+ /** De-duplicated like the SQL `stream = ANY(...)`; unknown names skipped. */
1154
1015
  _select(input) {
1155
1016
  if (Array.isArray(input))
1156
1017
  return [...new Set(input)].map((name) => this._streams.get(name)).filter((s) => s !== void 0);
1157
1018
  return [...this._streams.values()].filter(this._filter_predicate(input));
1158
1019
  }
1159
- /**
1160
- * Build a predicate from a {@link StreamFilter}. Compiled regexes are
1161
- * cached in the closure so callers can apply it across the streams
1162
- * map without re-compiling per iteration.
1163
- */
1164
1020
  _filter_predicate(filter) {
1165
1021
  const stream_re = filter.stream !== void 0 && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
1166
1022
  const source_re = filter.source !== void 0 && !filter.source_exact ? new RegExp(filter.source) : void 0;
@@ -1180,46 +1036,18 @@ var InMemoryStore = class {
1180
1036
  return true;
1181
1037
  };
1182
1038
  }
1183
- /**
1184
- * Reset watermarks to -1, clearing retry, blocked, error, and lease
1185
- * state so the matched streams can be replayed from the beginning.
1186
- * Accepts either an explicit list of names or a {@link StreamFilter}.
1187
- *
1188
- * @param input - Stream names or a filter selecting the streams to reset.
1189
- * @returns Count of streams that were actually reset.
1190
- */
1191
1039
  async reset(input) {
1192
1040
  await sleep();
1193
1041
  const selected = this._select(input);
1194
1042
  for (const s of selected) s.reset();
1195
1043
  return selected.length;
1196
1044
  }
1197
- /**
1198
- * Wipe the sensitive-data payload for every event on the stream — see
1199
- * {@link Store.forget_pii}. O(1) drop of the stream's inner Map; the size of
1200
- * that Map is the count of events that had PII. Idempotent: a second call
1201
- * finds no inner Map and returns `0`.
1202
- *
1203
- * @param stream - Target stream.
1204
- * @returns Count of events whose isolated PII payload was deleted.
1205
- */
1206
1045
  async forget_pii(stream) {
1207
1046
  await sleep();
1208
1047
  const count = this._pii.get(stream)?.size ?? 0;
1209
1048
  this._pii.delete(stream);
1210
1049
  return count;
1211
1050
  }
1212
- /**
1213
- * Clear the blocked flag (and retry / error / lease) on the matched
1214
- * streams without touching the watermark. Streams that aren't blocked
1215
- * at call time are silently skipped. Accepts either an explicit list
1216
- * of names or a {@link StreamFilter}. The filter form always restricts
1217
- * to blocked streams — passing `blocked: false` matches nothing.
1218
- * See {@link Store.unblock}.
1219
- *
1220
- * @param input - Stream names or a filter selecting the streams to unblock.
1221
- * @returns Count of streams that were actually flipped (were blocked).
1222
- */
1223
1051
  async unblock(input) {
1224
1052
  await sleep();
1225
1053
  const selected = this._select(
@@ -1227,26 +1055,12 @@ var InMemoryStore = class {
1227
1055
  );
1228
1056
  return selected.filter((s) => s.unblock()).length;
1229
1057
  }
1230
- /**
1231
- * Bulk-update priority of streams matching `filter`. Mirrors
1232
- * {@link query_streams}'s filter semantics — see {@link Store.prioritize}.
1233
- * Unlike {@link subscribe} (which keeps `max()` of registered
1234
- * priorities), this sets the priority outright — operator override
1235
- * for the build-time scheduling policy.
1236
- *
1237
- * @returns Count of streams whose priority changed.
1238
- */
1239
1058
  async prioritize(filter, priority) {
1240
1059
  await sleep();
1241
1060
  const changed = this._select(filter).filter((s) => s.priority !== priority);
1242
1061
  for (const s of changed) s.set_priority(priority);
1243
1062
  return changed.length;
1244
1063
  }
1245
- /**
1246
- * Streams registered subscription positions to the callback, ordered by
1247
- * stream name. Returns the highest event id in the store and the count
1248
- * of positions emitted.
1249
- */
1250
1064
  async query_streams(callback, query) {
1251
1065
  await sleep();
1252
1066
  const limit = query?.limit ?? 100;
@@ -1289,19 +1103,6 @@ var InMemoryStore = class {
1289
1103
  }
1290
1104
  return { maxEventId: this._events.at(-1)?.id ?? -1, count };
1291
1105
  }
1292
- /**
1293
- * Per-stream aggregated stats — see {@link Store.query_stats}.
1294
- *
1295
- * Single forward scan over the in-memory event list, accumulating per
1296
- * stream. The "cheap heads" cost tier from durable adapters doesn't
1297
- * apply here (InMemory has no indexes); correctness is the goal, perf
1298
- * is a non-issue.
1299
- *
1300
- * Scope rules:
1301
- * - Array `input` — explicit stream names, regardless of subscription.
1302
- * - Filter `input` — `stream`/`stream_exact` match against event-bearing
1303
- * stream names. Empty filter `{}` matches every event-bearing stream.
1304
- */
1305
1106
  async query_stats(input, options) {
1306
1107
  await sleep();
1307
1108
  const exclude = new Set(options?.exclude ?? []);
@@ -1363,15 +1164,6 @@ var InMemoryStore = class {
1363
1164
  }
1364
1165
  return out;
1365
1166
  }
1366
- /**
1367
- * Atomically truncates streams and seeds each with a snapshot or tombstone.
1368
- * Windowed targets (`before` set) prune the prefix below the closest safe
1369
- * `__snapshot__` instead — no seed, subscriptions untouched, no-op when no
1370
- * snapshot qualifies.
1371
- * @param targets - Streams to truncate with optional snapshot state and meta,
1372
- * or a `before`/`max_id` boundary for a windowed prefix delete.
1373
- * @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
1374
- */
1375
1167
  async truncate(targets) {
1376
1168
  await sleep();
1377
1169
  const result = /* @__PURE__ */ new Map();
@@ -1422,18 +1214,7 @@ var InMemoryStore = class {
1422
1214
  }
1423
1215
  return result;
1424
1216
  }
1425
- /**
1426
- * Atomically wipe-and-rebuild the store under an in-process snapshot.
1427
- *
1428
- * Captures every index state up front, clears it, then hands the
1429
- * orchestrator a per-event insert `callback` via the driver. Any
1430
- * throw inside the driver restores the snapshot, leaving the store
1431
- * byte-for-byte unchanged from the operator's perspective.
1432
- *
1433
- * `id`s are reassigned `0..N-1` as events arrive (dense — the
1434
- * monotonic id counter restarts at 0 for the rebuild). `created` is
1435
- * preserved verbatim from the source.
1436
- */
1217
+ /** A throw inside the driver restores the captured state. Ids restart at 0. */
1437
1218
  async restore(driver) {
1438
1219
  await sleep();
1439
1220
  const prev_events = this._events;
@@ -2899,37 +2680,34 @@ async function windowed_prune_pending(stream, before, max_id) {
2899
2680
  }
2900
2681
  async function probe_min_watermarks(streams, page_size, checkpoint) {
2901
2682
  const min_at = /* @__PURE__ */ new Map();
2902
- const source_regex = /* @__PURE__ */ new Map();
2903
- const get_regex = (source) => {
2904
- let re = source_regex.get(source);
2683
+ const matches = source_matcher();
2684
+ await walk_streams(
2685
+ store2(),
2686
+ (position) => {
2687
+ const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
2688
+ const cap = pending ? position.at : Math.max(position.at, checkpoint);
2689
+ for (const stream of streams) {
2690
+ if (matches(position.source, stream)) {
2691
+ const prev = min_at.get(stream);
2692
+ if (prev === void 0 || cap < prev) min_at.set(stream, cap);
2693
+ }
2694
+ }
2695
+ },
2696
+ { limit: page_size, source_matches: streams }
2697
+ );
2698
+ return min_at;
2699
+ }
2700
+ function source_matcher() {
2701
+ const compiled = /* @__PURE__ */ new Map();
2702
+ return (source, stream) => {
2703
+ if (!source) return true;
2704
+ let re = compiled.get(source);
2905
2705
  if (!re) {
2906
2706
  re = new RegExp(source);
2907
- source_regex.set(source, re);
2707
+ compiled.set(source, re);
2908
2708
  }
2909
- return re;
2709
+ return re.test(stream);
2910
2710
  };
2911
- let after;
2912
- for (; ; ) {
2913
- let last;
2914
- const { count } = await store2().query_streams(
2915
- (position) => {
2916
- last = position.stream;
2917
- const source_re = position.source ? get_regex(position.source) : void 0;
2918
- const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
2919
- const cap = pending ? position.at : Math.max(position.at, checkpoint);
2920
- for (const stream of streams) {
2921
- if (!source_re || source_re.test(stream)) {
2922
- const prev = min_at.get(stream);
2923
- if (prev === void 0 || cap < prev) min_at.set(stream, cap);
2924
- }
2925
- }
2926
- },
2927
- { after, limit: page_size, source_matches: streams }
2928
- );
2929
- if (count < page_size) break;
2930
- after = last;
2931
- }
2932
- return min_at;
2933
2711
  }
2934
2712
  async function scan_stream_heads(streams) {
2935
2713
  const stats = await store2().query_stats(streams, {
@@ -2963,36 +2741,20 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
2963
2741
  if (checkpoint < info.max_id) uncorrelated.add(stream);
2964
2742
  }
2965
2743
  const pending_set = /* @__PURE__ */ new Set();
2966
- const source_regex = /* @__PURE__ */ new Map();
2967
- const get_regex = (source) => {
2968
- let re = source_regex.get(source);
2969
- if (!re) {
2970
- re = new RegExp(source);
2971
- source_regex.set(source, re);
2972
- }
2973
- return re;
2974
- };
2744
+ const matches = source_matcher();
2975
2745
  const targets = [...stream_info.keys()];
2976
- let after;
2977
- for (; ; ) {
2978
- let last;
2979
- const { count } = await store2().query_streams(
2980
- (position) => {
2981
- last = position.stream;
2982
- const source_re = position.source ? get_regex(position.source) : void 0;
2983
- const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
2984
- if (!has_work) return;
2985
- for (const [stream, info] of stream_info) {
2986
- if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
2987
- pending_set.add(stream);
2988
- }
2989
- }
2990
- },
2991
- { after, limit: page_size, source_matches: targets }
2992
- );
2993
- if (count < page_size) break;
2994
- after = last;
2995
- }
2746
+ await walk_streams(
2747
+ store2(),
2748
+ (position) => {
2749
+ const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
2750
+ if (!has_work) return;
2751
+ for (const [stream, info] of stream_info) {
2752
+ if (matches(position.source, stream) && position.at < info.max_id)
2753
+ pending_set.add(stream);
2754
+ }
2755
+ },
2756
+ { limit: page_size, source_matches: targets }
2757
+ );
2996
2758
  const safe = [];
2997
2759
  for (const [stream] of stream_info) {
2998
2760
  if (pending_set.has(stream) || uncorrelated.has(stream))
@@ -3128,71 +2890,27 @@ function report_lane_conflict(seen, handler, target, kept, dropped) {
3128
2890
  var CorrelateCycle = class {
3129
2891
  _checkpoint = -1;
3130
2892
  _initialized = false;
3131
- /**
3132
- * This worker's identity for the correlation lease. A per-instance
3133
- * UUID, matching the drain's convention, so a renewal is recognised as the
3134
- * same holder and a restarted process never inherits a stale claim.
3135
- */
2893
+ /** Lease holder id; per instance, so a restart never inherits a stale claim. */
3136
2894
  _by = (0, import_node_crypto.randomUUID)();
3137
- /**
3138
- * Which correlator this is. Computed once from the registry so identical
3139
- * workers share a lease and unrelated applications never do.
3140
- */
3141
2895
  _key;
3142
- /**
3143
- * When this worker's correlation lease runs out, as a local clock reading,
3144
- * or 0 when it holds none.
3145
- *
3146
- * Cached so the holder doesn't ask the store on every pass. A stale belief
3147
- * can only cause a duplicate scan, and marks are idempotent, so it costs
3148
- * work, never correctness.
3149
- */
2896
+ /** Local expiry of the held lease (0 = none); stale only costs a duplicate scan. */
3150
2897
  _lease_until = 0;
3151
- /**
3152
- * Whether a scan might find anything, like the drain's flag: a commit or
3153
- * notification raises it, a short page lowers it, and a disarmed scan
3154
- * returns without touching the store.
3155
- *
3156
- * Starts armed: the log may already hold events this process has never
3157
- * correlated, and only a scan can find out.
3158
- */
2898
+ /** Raised by a commit or notification, lowered by a short page. Starts armed. */
3159
2899
  _armed = true;
3160
- /** In-flight init, memoized for single-flight and cleared on failure. */
3161
2900
  _init_promise;
3162
- // Dynamically discovered targets → what each was last subscribed at,
3163
- // bounded by `maxSubscribedStreams`. It decides *what* a scan sends with a
3164
- // target: priority/lane only when the resolution beats the recorded floor,
3165
- // otherwise the row's own values. See {@link Subscription}.
2901
+ // Bounded by `maxSubscribedStreams`; static records live apart so eviction
2902
+ // never drops their +Infinity floor.
3166
2903
  _dynamic_subscriptions;
3167
- /**
3168
- * What each static target was subscribed at by `init`. A plain map, never
3169
- * evicted: the collection is the build-time `_static_targets` list, so it
3170
- * is already bounded by the registry and costs nothing the registry does
3171
- * not already hold.
3172
- *
3173
- * Kept out of the LRU so eviction can never drop a static target's
3174
- * `+Infinity` floor and let a dynamic resolution re-lane it.
3175
- */
3176
2904
  _static_subscriptions = /* @__PURE__ */ new Map();
3177
- /** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
3178
2905
  _patterns = new LruMap(PATTERN_CACHE_SIZE);
3179
2906
  _registry;
3180
2907
  _static_targets;
3181
2908
  _cd;
3182
2909
  _on_init;
3183
- /**
3184
- * Async cold-start hook, run once after `on_init` inside `init()`. The
3185
- * orchestrator re-seeds its defer timers from the persisted `deferred_at`
3186
- * here, so an idle deferred stream re-arms across a restart.
3187
- */
2910
+ /** Runs once after `on_init`; re-seeds defer timers across a restart. */
3188
2911
  _on_init_async;
3189
- /** Lanes a controller exists for. See {@link CorrelateCycleDeps}. */
3190
2912
  _declared_lanes;
3191
- /**
3192
- * Offending declarations already reported, so a resolver firing for every
3193
- * matching event reports once. Owned by the instance rather than the
3194
- * module: `internal/` holds no module-level state.
3195
- */
2913
+ /** Report-once keys; per instance because `internal/` holds no module state. */
3196
2914
  _reported = /* @__PURE__ */ new Set();
3197
2915
  constructor({
3198
2916
  registry,
@@ -3213,28 +2931,13 @@ var CorrelateCycle = class {
3213
2931
  this._on_init = on_init;
3214
2932
  this._on_init_async = on_init_async;
3215
2933
  }
3216
- /** Last correlated event id. */
3217
2934
  get checkpoint() {
3218
2935
  return this._checkpoint;
3219
2936
  }
3220
- /**
3221
- * Signal that a commit (local or remote) may have produced events this
3222
- * process has not correlated. Cheap and idempotent — the orchestrator calls
3223
- * it on every commit and every notification.
3224
- */
2937
+ /** A commit (local or remote) may have produced uncorrelated events. */
3225
2938
  arm() {
3226
2939
  this._armed = true;
3227
2940
  }
3228
- /**
3229
- * Initialize correlation state on first call.
3230
- * - Reads the durable correlate checkpoint (and max(at)) from the store,
3231
- * flooring a first boot at `watermark - back_scan` so an event
3232
- * committed-but-not-correlated before a crash is re-scanned on
3233
- * restart instead of skipped
3234
- * - Subscribes static resolver targets (idempotent upsert)
3235
- * - Populates the subscribed-streams LRU
3236
- * - Fires `on_init` once (Act uses this to flag a cold-start drain)
3237
- */
3238
2941
  async init() {
3239
2942
  if (this._initialized) return;
3240
2943
  if (!this._init_promise) {
@@ -3261,16 +2964,7 @@ var CorrelateCycle = class {
3261
2964
  }
3262
2965
  await this._on_init_async?.();
3263
2966
  }
3264
- /**
3265
- * Start correlation over after the event log was replaced wholesale
3266
- * (`restore`). The restored log is renumbered from the start and its
3267
- * subscription rows are gone, so the in-memory scan position and both
3268
- * subscription records describe a log that no longer exists. Dropping them
3269
- * and the init latch makes the next pass run cold-start again: static
3270
- * targets are re-subscribed, the checkpoint is re-read from the store
3271
- * (which `restore` reset), and dynamic targets are rediscovered by the
3272
- * scan.
3273
- */
2967
+ /** After `restore` replaced the log: forget everything and cold-start again. */
3274
2968
  restart() {
3275
2969
  this._initialized = false;
3276
2970
  this._init_promise = void 0;
@@ -3279,21 +2973,11 @@ var CorrelateCycle = class {
3279
2973
  this._static_subscriptions.clear();
3280
2974
  this._armed = true;
3281
2975
  }
3282
- /**
3283
- * Forget dynamic targets whose subscription rows no longer exist (a full
3284
- * close deletes the row), so a later scan re-subscribes them. Static
3285
- * targets keep their records.
3286
- */
2976
+ /** Forget dynamic targets whose rows a full close deleted, so they re-subscribe. */
3287
2977
  forget_subscribed(streams) {
3288
2978
  for (const stream of streams) this._dynamic_subscriptions.delete(stream);
3289
2979
  }
3290
- /**
3291
- * Would an event from `stream` be fetched for a target subscribed with
3292
- * `source`? The subscription's source is the filter `fetch` queries with
3293
- * — literal names by equality, patterns compiled as a `RegExp` — so an
3294
- * event outside it is not work for that target and must not mark it.
3295
- * No source means the target consumes every stream.
3296
- */
2980
+ /** Would the target's own `fetch` (filtered by `source`) return this event? */
3297
2981
  _in_fetch_window(source, stream) {
3298
2982
  if (source === void 0 || source === stream) return true;
3299
2983
  if (is_literal_source(source)) return false;
@@ -3304,16 +2988,7 @@ var CorrelateCycle = class {
3304
2988
  }
3305
2989
  return pattern.test(stream);
3306
2990
  }
3307
- /**
3308
- * Scan the events past the checkpoint, resolve each to its target
3309
- * streams, and record what it found through `cd.subscribe` — new dynamic
3310
- * targets get registered, and every target an event resolved to gets its
3311
- * **work mark** raised to that event's id.
3312
- *
3313
- * Both resolver kinds are walked. A static target is already subscribed
3314
- * at init, but marking it is what makes it claimable without probing the
3315
- * event log, so the scan runs for every app.
3316
- */
2991
+ /** Scan past the checkpoint, subscribe new targets and raise their work marks. */
3317
2992
  async correlate(query = { after: -1, limit: 10 }, lease = false) {
3318
2993
  await this.init();
3319
2994
  if (!this._armed)
@@ -3398,10 +3073,8 @@ var CorrelateCycle = class {
3398
3073
  }
3399
3074
  }
3400
3075
  },
3401
- // Decline the sensitive payload: the scan only needs name/stream/id,
3402
- // and one undecryptable row would otherwise stop every stream's
3403
- // reactions. Resolvers therefore see `pii: null`; correlate runs
3404
- // actor-less, so plaintext here would be un-gated disclosure.
3076
+ // Resolvers see `pii: null`: correlate runs actor-less, and one
3077
+ // undecryptable row would otherwise stop every stream's reactions.
3405
3078
  { ...query, after, with_pii: false }
3406
3079
  );
3407
3080
  if (this._declared_lanes.size > 1) await this._recall(correlated);
@@ -3444,14 +3117,9 @@ var CorrelateCycle = class {
3444
3117
  return { subscribed: 0, last_id, marked: 0, scanned: true };
3445
3118
  }
3446
3119
  /**
3447
- * Give each target this scan had no record of the record its row holds.
3448
- *
3449
- * A target with no record is either new or forgotten. Its row, when it
3450
- * has one, says what lane and priority it was subscribed at; a resolution
3451
- * that doesn't beat that priority carries the row's values, exactly as if
3452
- * the record had never been evicted. One read per target with no record,
3453
- * and only when more than one lane is declared: with one lane there is no
3454
- * lane to lose, and the store already keeps the higher priority.
3120
+ * Read the row of each target this scan had no record of (new or evicted),
3121
+ * so a resolution that doesn't beat its priority keeps the row's lane.
3122
+ * Only called with more than one lane declared.
3455
3123
  */
3456
3124
  async _recall(correlated) {
3457
3125
  const missed = [...correlated].filter(([, entry]) => entry.missed);
@@ -3487,13 +3155,7 @@ var CorrelateCycle = class {
3487
3155
  })
3488
3156
  );
3489
3157
  }
3490
- /**
3491
- * Hand the correlation lease back early.
3492
- *
3493
- * The port has no release verb; re-acquiring as the same holder with a
3494
- * zero duration releases it. Best-effort: a failure costs the lease's
3495
- * remaining lifetime, as a crash would.
3496
- */
3158
+ /** Release the lease early by re-acquiring it for zero ms. Best-effort. */
3497
3159
  async release_correlation() {
3498
3160
  try {
3499
3161
  this._lease_until = 0;
@@ -3776,6 +3438,18 @@ var validating_patch = (me, state2, partial, event) => {
3776
3438
  );
3777
3439
  };
3778
3440
  var DEFAULT_BATCH = 500;
3441
+ var cache_put = (stream, entry) => cache2().set(stream, entry).catch(
3442
+ (err) => log().warn(
3443
+ err,
3444
+ "cache set skipped; the next load re-folds from the store"
3445
+ )
3446
+ );
3447
+ var cache_drop = (stream) => cache2().invalidate(stream).catch(
3448
+ (err) => log().warn(
3449
+ err,
3450
+ "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
3451
+ )
3452
+ );
3779
3453
  async function snap(snapshot) {
3780
3454
  const { id, stream, name, meta, version } = snapshot.event;
3781
3455
  try {
@@ -3970,38 +3644,28 @@ async function load(me, target, callback, patch_fn = bare_patch) {
3970
3644
  {
3971
3645
  stream,
3972
3646
  stream_exact: true,
3973
- // `with_snaps` on every non-time-travel load. Cold, it resumes from the
3974
- // latest snapshot (this is the only place that decides it; stores just
3975
- // apply it). Warm, it keeps a snapshot above a stale checkpoint in the
3976
- // result, so a windowed close that pruned the events in between can't
3977
- // make the fold apply the tail onto stale state.
3647
+ // `with_snaps` on every non-time-travel load: cold, it resumes from the
3648
+ // latest snapshot; warm, it keeps a snapshot above a stale checkpoint,
3649
+ // so a windowed close's prune can't fold the tail onto stale state.
3978
3650
  ...cached ? { after: cached.event_id, with_snaps: true } : { ...time_travel ? {} : { with_snaps: true }, ...asOf }
3979
3651
  }
3980
3652
  );
3981
3653
  const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
3982
3654
  if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
3983
- await cache2().set(stream, {
3655
+ await cache_put(stream, {
3984
3656
  stream,
3985
3657
  state: state2,
3986
3658
  version: event.version,
3987
3659
  event_id: event.id,
3988
3660
  patches,
3989
3661
  snaps
3990
- }).catch(
3991
- (err) => log().warn(
3992
- err,
3993
- "cache set skipped; the next load re-folds from the store"
3994
- )
3995
- );
3662
+ });
3996
3663
  }
3997
3664
  return {
3998
3665
  event,
3999
3666
  state: state2,
4000
3667
  version: event?.version ?? cached?.version ?? -1,
4001
- // Head event id, captured atomically with `state`: the last replayed
4002
- // event's id on a cache miss, or the cached checkpoint's event_id on a
4003
- // warm hit with no new events. Consumers read this instead of a
4004
- // separate cache lookup that could race a concurrent commit.
3668
+ // Captured with `state`, so consumers never race a separate cache lookup.
4005
3669
  id: event?.id ?? cached?.event_id ?? -1,
4006
3670
  patches,
4007
3671
  snaps,
@@ -4063,8 +3727,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
4063
3727
  action: {
4064
3728
  name: action2,
4065
3729
  ...target
4066
- // payload intentionally omitted from causation metadata —
4067
- // callers correlate via the correlation id when they need it.
3730
+ // payload intentionally omitted
4068
3731
  },
4069
3732
  event: reactingTo ? {
4070
3733
  id: reactingTo.id,
@@ -4084,14 +3747,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
4084
3747
  reactingTo ? expectedVersion : expected
4085
3748
  );
4086
3749
  } catch (error) {
4087
- if (error instanceof ConcurrencyError) {
4088
- cache2().invalidate(stream).catch(
4089
- (err) => log().warn(
4090
- err,
4091
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
4092
- )
4093
- );
4094
- }
3750
+ if (error instanceof ConcurrencyError) void cache_drop(stream);
4095
3751
  throw error;
4096
3752
  }
4097
3753
  let { state: state2, patches } = snapshot;
@@ -4118,26 +3774,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
4118
3774
  const snap_event = snapped ? await snap(last) : void 0;
4119
3775
  if (!me.pii_aware) {
4120
3776
  if (contiguous)
4121
- cache2().set(stream, {
3777
+ void cache_put(stream, {
4122
3778
  stream,
4123
3779
  state: last.state,
4124
3780
  version: snap_event?.version ?? last.event.version,
4125
3781
  event_id: snap_event?.id ?? last.event.id,
4126
3782
  patches: snap_event ? 0 : last.patches,
4127
3783
  snaps: snap_event ? last.snaps + 1 : last.snaps
4128
- }).catch(
4129
- (err) => log().warn(
4130
- err,
4131
- "cache set skipped; the next load re-folds from the store"
4132
- )
4133
- );
4134
- else
4135
- cache2().invalidate(stream).catch(
4136
- (err) => log().warn(
4137
- err,
4138
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
4139
- )
4140
- );
3784
+ });
3785
+ else void cache_drop(stream);
4141
3786
  }
4142
3787
  return snapshots;
4143
3788
  } catch (error) {
@@ -4467,61 +4112,35 @@ var DrainController = class {
4467
4112
  _locked = false;
4468
4113
  _ratio = 0.5;
4469
4114
  /**
4470
- * Local wake for streams this worker parked (backoff or defer). The
4471
- * schedule itself is persisted in the store; this runs the drain at the
4472
- * earliest pending visit. A parked stream already carries its mark, so
4473
- * the drain alone finds it, and on an idle app nothing else would.
4474
- * Every drain re-aims it when it ends, and a due entry stays until a pass
4475
- * handles it or a pass that started after it came due claims nothing, so
4476
- * the wake keeps draining until every due stream is reached.
4477
- * `stop()` cancels the timer and a stopped drain never re-schedules it,
4478
- * so a wake never runs after a stop.
4115
+ * Local wake at the earliest parked (backoff or defer) visit; the schedule
4116
+ * itself is persisted. On an idle app nothing else would drain it. A due
4117
+ * entry stays until handled or until a later pass claims nothing.
4479
4118
  */
4480
4119
  _defer = new DeferTimer(() => {
4481
4120
  this._armed = true;
4482
4121
  void this._deps.run_scoped(() => this.drain());
4483
4122
  });
4484
- /** Worker timer. Set when `start()` is active, undefined otherwise. */
4485
4123
  _worker;
4486
- /**
4487
- * Misroutings this controller has reported. A resolver returning a
4488
- * projection's target does so for every matching event; one line per event
4489
- * would bury the signal it exists to raise.
4490
- */
4491
4124
  _misrouted = /* @__PURE__ */ new Set();
4492
4125
  _stopped = false;
4493
- /**
4494
- * Resolves when the cycle in flight finishes, so a graceful shutdown can
4495
- * wait for it. Never rejects.
4496
- */
4126
+ /** Never rejects; graceful shutdown waits on it. */
4497
4127
  _inflight;
4498
4128
  _inflight_done;
4499
4129
  _deps;
4500
4130
  constructor(deps) {
4501
4131
  this._deps = deps;
4502
4132
  }
4503
- /**
4504
- * Signal that a commit (or reset / cold-start) may have produced work.
4505
- * Subsequent `drain()` calls will run the pipeline; once the pipeline
4506
- * settles to no-progress, the controller disarms itself.
4507
- */
4508
4133
  arm() {
4509
4134
  this._armed = true;
4510
4135
  }
4511
- /**
4512
- * Re-seed a persisted `deferred_at` into the local wake at cold start, so
4513
- * an idle deferred stream re-arms at its due-time with no new commit.
4514
- * Seeds collapse into one timer; the earliest wins.
4515
- */
4136
+ /** Cold start: re-seed a persisted `deferred_at` into the local wake. */
4516
4137
  seed_defer(stream, at) {
4517
4138
  this._defer.set(stream, at);
4518
4139
  this._defer.schedule();
4519
4140
  }
4520
- /** Read-only flag — true while a commit / reset is unprocessed. */
4521
4141
  get armed() {
4522
4142
  return this._armed;
4523
4143
  }
4524
- /** The cycle in flight, or `undefined` when idle. */
4525
4144
  get inflight() {
4526
4145
  return this._inflight;
4527
4146
  }
@@ -4529,19 +4148,10 @@ var DrainController = class {
4529
4148
  get lease_millis() {
4530
4149
  return this._deps.defaults?.leaseMillis;
4531
4150
  }
4532
- /** Lane this controller drains (undefined spans all lanes). */
4533
4151
  get lane() {
4534
4152
  return this._deps.lane;
4535
4153
  }
4536
- /**
4537
- * Start a per-lane worker that drains at the lane's `cycleMs`
4538
- * cadence. When armed, the worker calls `drain()` on every
4539
- * tick and re-schedules; when not armed, it still re-schedules at
4540
- * `cycleMs` so a future `arm()` is picked up on the next tick.
4541
- *
4542
- * The setTimeout chain uses `unref()` so it doesn't keep the process
4543
- * alive on its own.
4544
- */
4154
+ /** Drains every `cycleMs` while armed; the unref'd timer never holds the process. */
4545
4155
  start(cycleMs) {
4546
4156
  if (this._worker || this._stopped) return;
4547
4157
  const run = this._deps.run_scoped;
@@ -4554,7 +4164,6 @@ var DrainController = class {
4554
4164
  this._worker = setTimeout(tick, cycleMs);
4555
4165
  this._worker.unref();
4556
4166
  }
4557
- /** Stop the per-lane worker. Idempotent. */
4558
4167
  stop() {
4559
4168
  this._stopped = true;
4560
4169
  if (this._worker) {
@@ -4563,7 +4172,6 @@ var DrainController = class {
4563
4172
  }
4564
4173
  this._defer.stop();
4565
4174
  }
4566
- /** Run one drain pass. Short-circuits when not armed or already running. */
4567
4175
  async drain(options = {}) {
4568
4176
  if (!this._armed) return EMPTY_DRAIN;
4569
4177
  if (this._locked) return EMPTY_DRAIN;
@@ -4937,41 +4545,23 @@ function validate_only_lanes(options, lanes) {
4937
4545
  }
4938
4546
  var Act = class {
4939
4547
  _emitter = new import_node_events.default();
4940
- /** Circuit breaker shared by all drain lanes. */
4941
4548
  _breaker;
4942
- /** Whether this instance subscribes to `Store.notify`. */
4943
4549
  _listen;
4944
- /** Whether this instance runs reactions (drain, settle, correlate). */
4945
4550
  _drain;
4946
- /** Event names with at least one registered reaction (computed at build time) */
4947
4551
  _reactive_events;
4948
- /** One DrainController per active lane, keyed by lane name. */
4949
4552
  _drain_controllers;
4950
- /** Correlation: lazy init and the scan that marks work. */
4951
4553
  _correlate;
4952
- /** Debounced correlate→drain catch-up loop. */
4953
4554
  _settle;
4954
- /** Timer behind {@link start_correlations}. */
4955
4555
  _poll;
4956
4556
  /** The deprecated `start_correlations` callback, while the poll runs. */
4957
4557
  _on_discovered;
4958
4558
  /**
4959
- * Disposer for the cross-process notify subscription, set up eagerly
4960
- * during construction. Held as a promise because the subscription
4961
- * itself may be async (the PG adapter checks out a dedicated client
4962
- * and runs `LISTEN` before resolving). Resolves to `undefined` when
4963
- * the store doesn't implement `notify` or there are no registered
4964
- * reactions.
4965
- *
4966
- * **Contract:** the configured store must be injected via
4967
- * {@link store}`(adapter)` *before* calling `act()...build()`. The
4968
- * orchestrator wires notify against whatever store is current at
4969
- * construction time — late injection after build is unsupported.
4559
+ * Wired at construction against the store current then, so a store
4560
+ * injected after `build()` is never notified.
4970
4561
  */
4971
4562
  _notify_disposer;
4972
4563
  /** Public registry — kept as-is per the no-prefix-on-public convention. */
4973
4564
  registry;
4974
- /** Map of state name → state definition; populated by the builder. */
4975
4565
  _states;
4976
4566
  /**
4977
4567
  * Emit a lifecycle event. The payload type is inferred from the event name
@@ -4997,15 +4587,9 @@ var Act = class {
4997
4587
  return listeners.length > 0;
4998
4588
  }
4999
4589
  /**
5000
- * The single store-failure handler: log it, then emit the `error`
5001
- * lifecycle event. Wired into the circuit breaker's `on_error`, so it
5002
- * runs on every `failed()` — drain / settle / autoclose just call
5003
- * `breaker.failed(now, error)` and never log or emit themselves.
5004
- *
5005
- * The emit is guarded against Node's `EventEmitter` contract that an
5006
- * unhandled `"error"` emission is rethrown (which would crash the process
5007
- * from inside the drain catch); logging is unconditional so failures are
5008
- * never silent.
4590
+ * The one place a store failure is logged and emitted (the breaker's
4591
+ * `on_error`). The emit is guarded: an unhandled `"error"` emission
4592
+ * would crash the process.
5009
4593
  */
5010
4594
  _emit_error(error, circuit) {
5011
4595
  this._logger.error(error);
@@ -5027,58 +4611,29 @@ var Act = class {
5027
4611
  this._emitter.off(event, listener);
5028
4612
  return this;
5029
4613
  }
5030
- /** Batch handlers for static-target projections (target → handler) */
5031
4614
  _batch_handlers;
5032
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
5033
4615
  _es;
5034
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
5035
4616
  _cd;
5036
- /**
5037
- * Event-name → owning state, computed at build time. The duplicate-event
5038
- * guard in merge.ts ensures one event name maps to at most one state, so
5039
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
5040
- * set when seeding a `restart` snapshot in multi-state apps.
5041
- */
4617
+ /** Unambiguous: merge rejects an event name declared by two states. */
5042
4618
  _event_to_state;
5043
- /**
5044
- * Event-name → lane fan-in for selective arming. Built by
5045
- * `classify_registry` once per build. `"all"` means at least one of
5046
- * the event's reactions is a dynamic resolver (lane opaque until
5047
- * runtime); a `Set<string>` lists the static lanes only that event's
5048
- * reactions target.
5049
- */
4619
+ /** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
5050
4620
  _event_to_lanes;
5051
- /** What `internal/audit.ts` needs, built once at construction. */
5052
4621
  _audit_deps;
5053
- /** Logger resolved at construction time (after user port configuration) */
5054
4622
  _logger = log();
5055
4623
  /** This Act's ports: its own bag, or the singleton adapters. */
5056
4624
  _ports;
5057
- /**
5058
- * Runs a body inside this Act's ports frame, so internal
5059
- * `store()`/`cache()` calls resolve to `_ports`.
5060
- */
4625
+ /** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
5061
4626
  _scoped;
5062
- /**
5063
- * Correlation-id generator for originating actions. Bound at
5064
- * construction from `options.correlator ?? default_correlator`. The
5065
- * `do()` path passes this into the `_es.action` closure; close-cycle
5066
- * uses it via {@link close_correlation}.
5067
- */
5068
4627
  _correlator;
5069
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
5070
- * payload (it captures the triggering event for reactingTo auto-inject). */
4628
+ // Bound once, reused by every reaction payload.
5071
4629
  _bound_do = this.do.bind(this);
5072
4630
  _bound_load = this.load.bind(this);
5073
4631
  _bound_query = this.query.bind(this);
5074
4632
  _bound_query_array = this.query_array.bind(this);
5075
4633
  _bound_forget = this.forget.bind(this);
5076
- /** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
5077
4634
  _handle;
5078
4635
  _handle_batch;
5079
- /** Declared drain lanes. */
5080
4636
  _lanes;
5081
- /** Serializes close critical sections per stream. */
5082
4637
  _close_lock = new CloseLock();
5083
4638
  /** Every lane a controller can exist for: `"default"` plus each declared one. */
5084
4639
  _declared_lanes;
@@ -5086,26 +4641,7 @@ var Act = class {
5086
4641
  get lanes() {
5087
4642
  return this._lanes;
5088
4643
  }
5089
- /**
5090
- * Create a new Act orchestrator. Prefer the {@link act} builder over
5091
- * direct construction — `act()...build()` wires the registry, merges
5092
- * partial states, and collects batch handlers from registered slices
5093
- * and projections in one pass.
5094
- *
5095
- * @param registry Schemas for every event and action across registered states
5096
- * @param states Merged map of state name → state definition
5097
- * @param batch_handlers Static-target projection batch handlers (target → handler)
5098
- * @param options Tuning knobs — see {@link ActOptions}
5099
- * @param lanes Declared drain lanes. The builder collects
5100
- * these from `.withLane(...)` calls; one `DrainController` runs
5101
- * per lane, plus the implicit `"default"`.
5102
- * @param patch_fn The per-event patch step selected once by the builder
5103
- * from `ActOptions.validateFoldedState` — `bare_patch` by
5104
- * default, `validating_patch` when the flag is on. The builder uses
5105
- * the same value for its projection-fold handlers, so there is a
5106
- * single selection site. Defaults to `bare_patch` for direct
5107
- * construction.
5108
- */
4644
+ /** Use the {@link act} builder rather than constructing directly. */
5109
4645
  constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
5110
4646
  this.registry = registry;
5111
4647
  this._states = states;
@@ -5156,11 +4692,7 @@ var Act = class {
5156
4692
  this._notify_disposer = this._wire_notify(this._ports.store);
5157
4693
  register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
5158
4694
  }
5159
- /**
5160
- * Circuit breaker shared by every store-polling loop (drain, the settle
5161
- * correlate, autoclose). Validates the knobs eagerly so out-of-range
5162
- * values throw at build time, not on the first cycle tick.
5163
- */
4695
+ /** Shared by drain, settle and autoclose; bad knobs throw at build. */
5164
4696
  _build_breaker(options) {
5165
4697
  return new CircuitBreaker(
5166
4698
  resolveCircuitBreakerConfig(options.circuitBreaker),
@@ -5174,11 +4706,7 @@ var Act = class {
5174
4706
  }
5175
4707
  );
5176
4708
  }
5177
- /**
5178
- * One DrainController per active lane, plus the implicit "default" unless
5179
- * `onlyLanes` excludes it. Apps that declare no lanes pass no lane filter
5180
- * to `claim`.
5181
- */
4709
+ /** Apps that declare no lanes pass no lane filter to `claim`. */
5182
4710
  _build_drain_controllers(options, lanes) {
5183
4711
  const all_lanes = this._declared_lanes;
5184
4712
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
@@ -5214,11 +4742,7 @@ var Act = class {
5214
4742
  }
5215
4743
  return controllers;
5216
4744
  }
5217
- /**
5218
- * Startup advisory naming declared lanes that `onlyLanes` leaves without a
5219
- * controller here. One process can't check that the cluster covers every
5220
- * lane, so it reports its own gaps.
5221
- */
4745
+ /** One process can't see the cluster, so it reports its own lane gaps. */
5222
4746
  _advise_orphaned_lanes(options) {
5223
4747
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
5224
4748
  const active = new Set(this._drain_controllers.keys());
@@ -5240,11 +4764,6 @@ var Act = class {
5240
4764
  routed_events: new Set(this._event_to_lanes.keys())
5241
4765
  };
5242
4766
  }
5243
- /**
5244
- * Correlate cycle over the classified registry. The cold-start callback
5245
- * arms every controller — historical events may need processing —
5246
- * except on writer-only instances (`drain: false`).
5247
- */
5248
4767
  _build_correlate(options, classification) {
5249
4768
  return new CorrelateCycle({
5250
4769
  registry: this.registry,
@@ -5262,12 +4781,7 @@ var Act = class {
5262
4781
  on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
5263
4782
  });
5264
4783
  }
5265
- /**
5266
- * At cold start, re-seed each lane controller's local wake from the
5267
- * persisted future `deferred_at`, so an idle deferred stream (an
5268
- * autoclose waiting out its window) still fires on time. Streams on lanes
5269
- * this instance doesn't run are skipped.
5270
- */
4784
+ /** Cold start: an idle deferred stream still fires on time after a restart. */
5271
4785
  async _seed_persisted_defers() {
5272
4786
  const now = Date.now();
5273
4787
  await walk_streams(store2(), (pos) => {
@@ -5276,7 +4790,6 @@ var Act = class {
5276
4790
  controller?.seed_defer(pos.stream, pos.deferred_at);
5277
4791
  });
5278
4792
  }
5279
- /** Settle loop driving correlate + drain to quiescence. */
5280
4793
  _build_settle(options) {
5281
4794
  return new SettleLoop(
5282
4795
  {
@@ -5290,7 +4803,6 @@ var Act = class {
5290
4803
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
5291
4804
  );
5292
4805
  }
5293
- /** True after the first `shutdown()` call. Guards idempotency. */
5294
4806
  _shutdown_promise;
5295
4807
  /**
5296
4808
  * Per-instance teardown: stop scheduling new work, give drain cycles
@@ -5330,14 +4842,8 @@ var Act = class {
5330
4842
  return this._shutdown_promise;
5331
4843
  }
5332
4844
  /**
5333
- * Wait for every in-flight drain cycle, or for the grace budget to elapse,
5334
- * whichever comes first. Cycle promises never reject (`drain()` contains
5335
- * its own errors), so this never throws.
5336
- *
5337
- * An omitted budget is the largest `leaseMillis` among lanes with a cycle
5338
- * in flight (the drain's fallback for lanes that pinned none, or pinned
5339
- * 0, which is the same as none), with an in-flight settle counting on the
5340
- * fallback, capped at the maximum. Idle lanes don't count.
4845
+ * The largest `leaseMillis` among lanes with a cycle in flight (the
4846
+ * default for none or 0, and for a running settle), capped.
5341
4847
  */
5342
4848
  _derive_grace_ms(running, settling) {
5343
4849
  let max = settling ? DEFAULT_LEASE_MILLIS : 0;
@@ -5366,12 +4872,7 @@ var Act = class {
5366
4872
  clearTimeout(timer);
5367
4873
  }
5368
4874
  }
5369
- /**
5370
- * Subscribe to {@link Store.notify} when both the store and the
5371
- * registry support it. Returns the disposer (or `undefined` when no
5372
- * subscription was made). Errors during subscription are logged but
5373
- * never thrown — `notify` is a hint, not a contract.
5374
- */
4875
+ /** Failures are logged, never thrown: `notify` is a hint. */
5375
4876
  async _wire_notify(s) {
5376
4877
  if (this._reactive_events.size === 0) return void 0;
5377
4878
  if (!s.notify) return void 0;
@@ -5683,20 +5184,11 @@ var Act = class {
5683
5184
  return { fetched: [], leased: [], acked: [], blocked: [] };
5684
5185
  return this._scoped(() => this._drain_all(options));
5685
5186
  }
5686
- /** Arm every active lane controller. */
5687
5187
  _arm_all() {
5688
5188
  this._correlate.arm();
5689
5189
  for (const c of this._drain_controllers.values()) c.arm();
5690
5190
  }
5691
- /**
5692
- * Arm only the lane controllers whose reactions match the supplied
5693
- * event names (selective arming). Events with any dynamic
5694
- * resolver fall back to `_arm_all()` via the `"all"` sentinel — the
5695
- * resolver's lane isn't known until correlate runs the function.
5696
- * Events with no reactions are skipped; `_event_to_lanes` doesn't
5697
- * carry them. Returns true when any controller was armed (used by
5698
- * the notify handler to decide whether to schedule a settle).
5699
- */
5191
+ /** Arms only the lanes these events react on; true when any was armed. */
5700
5192
  _arm_for_event_names(names) {
5701
5193
  const to_arm = /* @__PURE__ */ new Set();
5702
5194
  for (const name of names) {
@@ -5713,14 +5205,7 @@ var Act = class {
5713
5205
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
5714
5206
  return true;
5715
5207
  }
5716
- /** Drain every active lane controller in parallel and aggregate.
5717
- *
5718
- * Parallel — not sequential — so a slow lane's in-flight handler does
5719
- * not block a fast lane's claim/dispatch/ack cycle. Each controller's
5720
- * `claim()` is independent (filtered by lane); the store's
5721
- * `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
5722
- * (`acked`, `blocked`) may interleave by lane — listeners filter via
5723
- * `lease.lane`. */
5208
+ /** In parallel, so a slow lane never holds up a fast one. */
5724
5209
  async _drain_all(options) {
5725
5210
  const results = await Promise.all(
5726
5211
  [...this._drain_controllers.values()].map((c) => c.drain(options))
@@ -5793,14 +5278,7 @@ var Act = class {
5793
5278
  const { subscribed, last_id } = await this._correlate_scanned(query);
5794
5279
  return { subscribed, last_id };
5795
5280
  }
5796
- /**
5797
- * `correlate` plus whether the pass actually read the store.
5798
- *
5799
- * The settle loop needs that extra bit to decide whether the pass carries a
5800
- * circuit-breaker health signal, and a disarmed pass carries none. It stays
5801
- * internal rather than widening the public `correlate` return, which is
5802
- * charter-covered and has no use for it.
5803
- */
5281
+ /** `correlate` plus `scanned`, which settle needs for the breaker. */
5804
5282
  async _correlate_scanned(query, lease = false) {
5805
5283
  if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
5806
5284
  return this._scoped(async () => {
@@ -6141,11 +5619,7 @@ var Act = class {
6141
5619
  async prioritize(filter, priority) {
6142
5620
  return this._scoped(() => store2().prioritize(filter, priority));
6143
5621
  }
6144
- /**
6145
- * Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
6146
- * reaction-requested closes. The close actor lets a custom correlator
6147
- * still tag tenant context or trace ids.
6148
- */
5622
+ /** Shared by `close()` and reaction-requested closes. */
6149
5623
  async _run_close(targets) {
6150
5624
  const close_actor = { id: "$close", name: "close" };
6151
5625
  const result = await run_close_cycle(targets, {
@@ -6226,10 +5700,7 @@ var Act = class {
6226
5700
  */
6227
5701
  async close(targets) {
6228
5702
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
6229
- return this._scoped(async () => {
6230
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6231
- return this._run_close(targets);
6232
- });
5703
+ return this._scoped(() => this._run_close(targets));
6233
5704
  }
6234
5705
  /**
6235
5706
  * Debounced, non-blocking correlate→drain cycle.