@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/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +20 -155
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +10 -190
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +0 -8
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/{chunk-OG5O36AJ.js → chunk-QXW3JV2H.js} +21 -240
- package/dist/chunk-QXW3JV2H.js.map +1 -0
- package/dist/index.cjs +96 -482
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +78 -245
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +20 -239
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-OG5O36AJ.js.map +0 -1
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
|
-
//
|
|
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
|
|
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
|
-
/**
|
|
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()) &&
|
|
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
|
-
//
|
|
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
|
|
848
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
2903
|
-
|
|
2904
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
(
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
4023
|
-
|
|
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
|
-
*
|
|
4817
|
-
*
|
|
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
|
|
4858
|
-
*
|
|
4859
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
5191
|
-
*
|
|
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
|
-
/**
|
|
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(
|
|
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.
|