@rotorsoft/act 1.32.24 → 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))
@@ -3676,6 +3438,18 @@ var validating_patch = (me, state2, partial, event) => {
3676
3438
  );
3677
3439
  };
3678
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
+ );
3679
3453
  async function snap(snapshot) {
3680
3454
  const { id, stream, name, meta, version } = snapshot.event;
3681
3455
  try {
@@ -3878,19 +3652,14 @@ async function load(me, target, callback, patch_fn = bare_patch) {
3878
3652
  );
3879
3653
  const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
3880
3654
  if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
3881
- await cache2().set(stream, {
3655
+ await cache_put(stream, {
3882
3656
  stream,
3883
3657
  state: state2,
3884
3658
  version: event.version,
3885
3659
  event_id: event.id,
3886
3660
  patches,
3887
3661
  snaps
3888
- }).catch(
3889
- (err) => log().warn(
3890
- err,
3891
- "cache set skipped; the next load re-folds from the store"
3892
- )
3893
- );
3662
+ });
3894
3663
  }
3895
3664
  return {
3896
3665
  event,
@@ -3978,14 +3747,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
3978
3747
  reactingTo ? expectedVersion : expected
3979
3748
  );
3980
3749
  } catch (error) {
3981
- if (error instanceof ConcurrencyError) {
3982
- cache2().invalidate(stream).catch(
3983
- (err) => log().warn(
3984
- err,
3985
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
3986
- )
3987
- );
3988
- }
3750
+ if (error instanceof ConcurrencyError) void cache_drop(stream);
3989
3751
  throw error;
3990
3752
  }
3991
3753
  let { state: state2, patches } = snapshot;
@@ -4012,26 +3774,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
4012
3774
  const snap_event = snapped ? await snap(last) : void 0;
4013
3775
  if (!me.pii_aware) {
4014
3776
  if (contiguous)
4015
- cache2().set(stream, {
3777
+ void cache_put(stream, {
4016
3778
  stream,
4017
3779
  state: last.state,
4018
3780
  version: snap_event?.version ?? last.event.version,
4019
3781
  event_id: snap_event?.id ?? last.event.id,
4020
3782
  patches: snap_event ? 0 : last.patches,
4021
3783
  snaps: snap_event ? last.snaps + 1 : last.snaps
4022
- }).catch(
4023
- (err) => log().warn(
4024
- err,
4025
- "cache set skipped; the next load re-folds from the store"
4026
- )
4027
- );
4028
- else
4029
- cache2().invalidate(stream).catch(
4030
- (err) => log().warn(
4031
- err,
4032
- "cache invalidate skipped; a stale checkpoint re-folds from its event_id"
4033
- )
4034
- );
3784
+ });
3785
+ else void cache_drop(stream);
4035
3786
  }
4036
3787
  return snapshots;
4037
3788
  } catch (error) {
@@ -4794,41 +4545,23 @@ function validate_only_lanes(options, lanes) {
4794
4545
  }
4795
4546
  var Act = class {
4796
4547
  _emitter = new import_node_events.default();
4797
- /** Circuit breaker shared by all drain lanes. */
4798
4548
  _breaker;
4799
- /** Whether this instance subscribes to `Store.notify`. */
4800
4549
  _listen;
4801
- /** Whether this instance runs reactions (drain, settle, correlate). */
4802
4550
  _drain;
4803
- /** Event names with at least one registered reaction (computed at build time) */
4804
4551
  _reactive_events;
4805
- /** One DrainController per active lane, keyed by lane name. */
4806
4552
  _drain_controllers;
4807
- /** Correlation: lazy init and the scan that marks work. */
4808
4553
  _correlate;
4809
- /** Debounced correlate→drain catch-up loop. */
4810
4554
  _settle;
4811
- /** Timer behind {@link start_correlations}. */
4812
4555
  _poll;
4813
4556
  /** The deprecated `start_correlations` callback, while the poll runs. */
4814
4557
  _on_discovered;
4815
4558
  /**
4816
- * Disposer for the cross-process notify subscription, set up eagerly
4817
- * during construction. Held as a promise because the subscription
4818
- * itself may be async (the PG adapter checks out a dedicated client
4819
- * and runs `LISTEN` before resolving). Resolves to `undefined` when
4820
- * the store doesn't implement `notify` or there are no registered
4821
- * reactions.
4822
- *
4823
- * **Contract:** the configured store must be injected via
4824
- * {@link store}`(adapter)` *before* calling `act()...build()`. The
4825
- * orchestrator wires notify against whatever store is current at
4826
- * 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.
4827
4561
  */
4828
4562
  _notify_disposer;
4829
4563
  /** Public registry — kept as-is per the no-prefix-on-public convention. */
4830
4564
  registry;
4831
- /** Map of state name → state definition; populated by the builder. */
4832
4565
  _states;
4833
4566
  /**
4834
4567
  * Emit a lifecycle event. The payload type is inferred from the event name
@@ -4854,15 +4587,9 @@ var Act = class {
4854
4587
  return listeners.length > 0;
4855
4588
  }
4856
4589
  /**
4857
- * The single store-failure handler: log it, then emit the `error`
4858
- * lifecycle event. Wired into the circuit breaker's `on_error`, so it
4859
- * runs on every `failed()` — drain / settle / autoclose just call
4860
- * `breaker.failed(now, error)` and never log or emit themselves.
4861
- *
4862
- * The emit is guarded against Node's `EventEmitter` contract that an
4863
- * unhandled `"error"` emission is rethrown (which would crash the process
4864
- * from inside the drain catch); logging is unconditional so failures are
4865
- * 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.
4866
4593
  */
4867
4594
  _emit_error(error, circuit) {
4868
4595
  this._logger.error(error);
@@ -4884,58 +4611,29 @@ var Act = class {
4884
4611
  this._emitter.off(event, listener);
4885
4612
  return this;
4886
4613
  }
4887
- /** Batch handlers for static-target projections (target → handler) */
4888
4614
  _batch_handlers;
4889
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
4890
4615
  _es;
4891
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
4892
4616
  _cd;
4893
- /**
4894
- * Event-name → owning state, computed at build time. The duplicate-event
4895
- * guard in merge.ts ensures one event name maps to at most one state, so
4896
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
4897
- * set when seeding a `restart` snapshot in multi-state apps.
4898
- */
4617
+ /** Unambiguous: merge rejects an event name declared by two states. */
4899
4618
  _event_to_state;
4900
- /**
4901
- * Event-name → lane fan-in for selective arming. Built by
4902
- * `classify_registry` once per build. `"all"` means at least one of
4903
- * the event's reactions is a dynamic resolver (lane opaque until
4904
- * runtime); a `Set<string>` lists the static lanes only that event's
4905
- * reactions target.
4906
- */
4619
+ /** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
4907
4620
  _event_to_lanes;
4908
- /** What `internal/audit.ts` needs, built once at construction. */
4909
4621
  _audit_deps;
4910
- /** Logger resolved at construction time (after user port configuration) */
4911
4622
  _logger = log();
4912
4623
  /** This Act's ports: its own bag, or the singleton adapters. */
4913
4624
  _ports;
4914
- /**
4915
- * Runs a body inside this Act's ports frame, so internal
4916
- * `store()`/`cache()` calls resolve to `_ports`.
4917
- */
4625
+ /** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
4918
4626
  _scoped;
4919
- /**
4920
- * Correlation-id generator for originating actions. Bound at
4921
- * construction from `options.correlator ?? default_correlator`. The
4922
- * `do()` path passes this into the `_es.action` closure; close-cycle
4923
- * uses it via {@link close_correlation}.
4924
- */
4925
4627
  _correlator;
4926
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
4927
- * payload (it captures the triggering event for reactingTo auto-inject). */
4628
+ // Bound once, reused by every reaction payload.
4928
4629
  _bound_do = this.do.bind(this);
4929
4630
  _bound_load = this.load.bind(this);
4930
4631
  _bound_query = this.query.bind(this);
4931
4632
  _bound_query_array = this.query_array.bind(this);
4932
4633
  _bound_forget = this.forget.bind(this);
4933
- /** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
4934
4634
  _handle;
4935
4635
  _handle_batch;
4936
- /** Declared drain lanes. */
4937
4636
  _lanes;
4938
- /** Serializes close critical sections per stream. */
4939
4637
  _close_lock = new CloseLock();
4940
4638
  /** Every lane a controller can exist for: `"default"` plus each declared one. */
4941
4639
  _declared_lanes;
@@ -4943,26 +4641,7 @@ var Act = class {
4943
4641
  get lanes() {
4944
4642
  return this._lanes;
4945
4643
  }
4946
- /**
4947
- * Create a new Act orchestrator. Prefer the {@link act} builder over
4948
- * direct construction — `act()...build()` wires the registry, merges
4949
- * partial states, and collects batch handlers from registered slices
4950
- * and projections in one pass.
4951
- *
4952
- * @param registry Schemas for every event and action across registered states
4953
- * @param states Merged map of state name → state definition
4954
- * @param batch_handlers Static-target projection batch handlers (target → handler)
4955
- * @param options Tuning knobs — see {@link ActOptions}
4956
- * @param lanes Declared drain lanes. The builder collects
4957
- * these from `.withLane(...)` calls; one `DrainController` runs
4958
- * per lane, plus the implicit `"default"`.
4959
- * @param patch_fn The per-event patch step selected once by the builder
4960
- * from `ActOptions.validateFoldedState` — `bare_patch` by
4961
- * default, `validating_patch` when the flag is on. The builder uses
4962
- * the same value for its projection-fold handlers, so there is a
4963
- * single selection site. Defaults to `bare_patch` for direct
4964
- * construction.
4965
- */
4644
+ /** Use the {@link act} builder rather than constructing directly. */
4966
4645
  constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
4967
4646
  this.registry = registry;
4968
4647
  this._states = states;
@@ -5013,11 +4692,7 @@ var Act = class {
5013
4692
  this._notify_disposer = this._wire_notify(this._ports.store);
5014
4693
  register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
5015
4694
  }
5016
- /**
5017
- * Circuit breaker shared by every store-polling loop (drain, the settle
5018
- * correlate, autoclose). Validates the knobs eagerly so out-of-range
5019
- * values throw at build time, not on the first cycle tick.
5020
- */
4695
+ /** Shared by drain, settle and autoclose; bad knobs throw at build. */
5021
4696
  _build_breaker(options) {
5022
4697
  return new CircuitBreaker(
5023
4698
  resolveCircuitBreakerConfig(options.circuitBreaker),
@@ -5031,11 +4706,7 @@ var Act = class {
5031
4706
  }
5032
4707
  );
5033
4708
  }
5034
- /**
5035
- * One DrainController per active lane, plus the implicit "default" unless
5036
- * `onlyLanes` excludes it. Apps that declare no lanes pass no lane filter
5037
- * to `claim`.
5038
- */
4709
+ /** Apps that declare no lanes pass no lane filter to `claim`. */
5039
4710
  _build_drain_controllers(options, lanes) {
5040
4711
  const all_lanes = this._declared_lanes;
5041
4712
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
@@ -5071,11 +4742,7 @@ var Act = class {
5071
4742
  }
5072
4743
  return controllers;
5073
4744
  }
5074
- /**
5075
- * Startup advisory naming declared lanes that `onlyLanes` leaves without a
5076
- * controller here. One process can't check that the cluster covers every
5077
- * lane, so it reports its own gaps.
5078
- */
4745
+ /** One process can't see the cluster, so it reports its own lane gaps. */
5079
4746
  _advise_orphaned_lanes(options) {
5080
4747
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
5081
4748
  const active = new Set(this._drain_controllers.keys());
@@ -5097,11 +4764,6 @@ var Act = class {
5097
4764
  routed_events: new Set(this._event_to_lanes.keys())
5098
4765
  };
5099
4766
  }
5100
- /**
5101
- * Correlate cycle over the classified registry. The cold-start callback
5102
- * arms every controller — historical events may need processing —
5103
- * except on writer-only instances (`drain: false`).
5104
- */
5105
4767
  _build_correlate(options, classification) {
5106
4768
  return new CorrelateCycle({
5107
4769
  registry: this.registry,
@@ -5119,12 +4781,7 @@ var Act = class {
5119
4781
  on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
5120
4782
  });
5121
4783
  }
5122
- /**
5123
- * At cold start, re-seed each lane controller's local wake from the
5124
- * persisted future `deferred_at`, so an idle deferred stream (an
5125
- * autoclose waiting out its window) still fires on time. Streams on lanes
5126
- * this instance doesn't run are skipped.
5127
- */
4784
+ /** Cold start: an idle deferred stream still fires on time after a restart. */
5128
4785
  async _seed_persisted_defers() {
5129
4786
  const now = Date.now();
5130
4787
  await walk_streams(store2(), (pos) => {
@@ -5133,7 +4790,6 @@ var Act = class {
5133
4790
  controller?.seed_defer(pos.stream, pos.deferred_at);
5134
4791
  });
5135
4792
  }
5136
- /** Settle loop driving correlate + drain to quiescence. */
5137
4793
  _build_settle(options) {
5138
4794
  return new SettleLoop(
5139
4795
  {
@@ -5147,7 +4803,6 @@ var Act = class {
5147
4803
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
5148
4804
  );
5149
4805
  }
5150
- /** True after the first `shutdown()` call. Guards idempotency. */
5151
4806
  _shutdown_promise;
5152
4807
  /**
5153
4808
  * Per-instance teardown: stop scheduling new work, give drain cycles
@@ -5187,14 +4842,8 @@ var Act = class {
5187
4842
  return this._shutdown_promise;
5188
4843
  }
5189
4844
  /**
5190
- * Wait for every in-flight drain cycle, or for the grace budget to elapse,
5191
- * whichever comes first. Cycle promises never reject (`drain()` contains
5192
- * its own errors), so this never throws.
5193
- *
5194
- * An omitted budget is the largest `leaseMillis` among lanes with a cycle
5195
- * in flight (the drain's fallback for lanes that pinned none, or pinned
5196
- * 0, which is the same as none), with an in-flight settle counting on the
5197
- * 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.
5198
4847
  */
5199
4848
  _derive_grace_ms(running, settling) {
5200
4849
  let max = settling ? DEFAULT_LEASE_MILLIS : 0;
@@ -5223,12 +4872,7 @@ var Act = class {
5223
4872
  clearTimeout(timer);
5224
4873
  }
5225
4874
  }
5226
- /**
5227
- * Subscribe to {@link Store.notify} when both the store and the
5228
- * registry support it. Returns the disposer (or `undefined` when no
5229
- * subscription was made). Errors during subscription are logged but
5230
- * never thrown — `notify` is a hint, not a contract.
5231
- */
4875
+ /** Failures are logged, never thrown: `notify` is a hint. */
5232
4876
  async _wire_notify(s) {
5233
4877
  if (this._reactive_events.size === 0) return void 0;
5234
4878
  if (!s.notify) return void 0;
@@ -5540,20 +5184,11 @@ var Act = class {
5540
5184
  return { fetched: [], leased: [], acked: [], blocked: [] };
5541
5185
  return this._scoped(() => this._drain_all(options));
5542
5186
  }
5543
- /** Arm every active lane controller. */
5544
5187
  _arm_all() {
5545
5188
  this._correlate.arm();
5546
5189
  for (const c of this._drain_controllers.values()) c.arm();
5547
5190
  }
5548
- /**
5549
- * Arm only the lane controllers whose reactions match the supplied
5550
- * event names (selective arming). Events with any dynamic
5551
- * resolver fall back to `_arm_all()` via the `"all"` sentinel — the
5552
- * resolver's lane isn't known until correlate runs the function.
5553
- * Events with no reactions are skipped; `_event_to_lanes` doesn't
5554
- * carry them. Returns true when any controller was armed (used by
5555
- * the notify handler to decide whether to schedule a settle).
5556
- */
5191
+ /** Arms only the lanes these events react on; true when any was armed. */
5557
5192
  _arm_for_event_names(names) {
5558
5193
  const to_arm = /* @__PURE__ */ new Set();
5559
5194
  for (const name of names) {
@@ -5570,14 +5205,7 @@ var Act = class {
5570
5205
  for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
5571
5206
  return true;
5572
5207
  }
5573
- /** Drain every active lane controller in parallel and aggregate.
5574
- *
5575
- * Parallel — not sequential — so a slow lane's in-flight handler does
5576
- * not block a fast lane's claim/dispatch/ack cycle. Each controller's
5577
- * `claim()` is independent (filtered by lane); the store's
5578
- * `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
5579
- * (`acked`, `blocked`) may interleave by lane — listeners filter via
5580
- * `lease.lane`. */
5208
+ /** In parallel, so a slow lane never holds up a fast one. */
5581
5209
  async _drain_all(options) {
5582
5210
  const results = await Promise.all(
5583
5211
  [...this._drain_controllers.values()].map((c) => c.drain(options))
@@ -5650,14 +5278,7 @@ var Act = class {
5650
5278
  const { subscribed, last_id } = await this._correlate_scanned(query);
5651
5279
  return { subscribed, last_id };
5652
5280
  }
5653
- /**
5654
- * `correlate` plus whether the pass actually read the store.
5655
- *
5656
- * The settle loop needs that extra bit to decide whether the pass carries a
5657
- * circuit-breaker health signal, and a disarmed pass carries none. It stays
5658
- * internal rather than widening the public `correlate` return, which is
5659
- * charter-covered and has no use for it.
5660
- */
5281
+ /** `correlate` plus `scanned`, which settle needs for the breaker. */
5661
5282
  async _correlate_scanned(query, lease = false) {
5662
5283
  if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
5663
5284
  return this._scoped(async () => {
@@ -5998,11 +5619,7 @@ var Act = class {
5998
5619
  async prioritize(filter, priority) {
5999
5620
  return this._scoped(() => store2().prioritize(filter, priority));
6000
5621
  }
6001
- /**
6002
- * Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
6003
- * reaction-requested closes. The close actor lets a custom correlator
6004
- * still tag tenant context or trace ids.
6005
- */
5622
+ /** Shared by `close()` and reaction-requested closes. */
6006
5623
  async _run_close(targets) {
6007
5624
  const close_actor = { id: "$close", name: "close" };
6008
5625
  const result = await run_close_cycle(targets, {
@@ -6083,10 +5700,7 @@ var Act = class {
6083
5700
  */
6084
5701
  async close(targets) {
6085
5702
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
6086
- return this._scoped(async () => {
6087
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6088
- return this._run_close(targets);
6089
- });
5703
+ return this._scoped(() => this._run_close(targets));
6090
5704
  }
6091
5705
  /**
6092
5706
  * Debounced, non-blocking correlate→drain cycle.