@rotorsoft/act 1.32.24 → 1.32.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.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/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-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 +153 -488
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -251
- 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))
|
|
@@ -3255,13 +3017,25 @@ var CorrelateCycle = class {
|
|
|
3255
3017
|
const after = Math.max(this._checkpoint, query.after || -1);
|
|
3256
3018
|
const correlated = /* @__PURE__ */ new Map();
|
|
3257
3019
|
let last_id = after;
|
|
3020
|
+
let stopped = false;
|
|
3258
3021
|
const found = await store2().query(
|
|
3259
3022
|
(event) => {
|
|
3260
|
-
|
|
3023
|
+
if (stopped) return;
|
|
3261
3024
|
const register = this._registry.events[event.name];
|
|
3262
3025
|
if (register) {
|
|
3263
3026
|
for (const reaction of register.reactions.values()) {
|
|
3264
|
-
|
|
3027
|
+
let resolved;
|
|
3028
|
+
try {
|
|
3029
|
+
resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
3030
|
+
} catch (error) {
|
|
3031
|
+
stopped = true;
|
|
3032
|
+
report_once(
|
|
3033
|
+
this._reported,
|
|
3034
|
+
`resolver|${reaction.handler.name}|${event.id}`,
|
|
3035
|
+
`Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}. Correlation stops before this event until the resolver stops throwing; work already found keeps draining.`
|
|
3036
|
+
);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3265
3039
|
if (!resolved) continue;
|
|
3266
3040
|
let lane = resolved.lane;
|
|
3267
3041
|
if (lane !== void 0 && !this._declared_lanes.has(lane)) {
|
|
@@ -3310,6 +3084,7 @@ var CorrelateCycle = class {
|
|
|
3310
3084
|
correlated.set(resolved.target, entry);
|
|
3311
3085
|
}
|
|
3312
3086
|
}
|
|
3087
|
+
last_id = event.id;
|
|
3313
3088
|
},
|
|
3314
3089
|
// Resolvers see `pii: null`: correlate runs actor-less, and one
|
|
3315
3090
|
// undecryptable row would otherwise stop every stream's reactions.
|
|
@@ -3351,7 +3126,7 @@ var CorrelateCycle = class {
|
|
|
3351
3126
|
return { subscribed, last_id, marked, scanned: true };
|
|
3352
3127
|
}
|
|
3353
3128
|
this._checkpoint = last_id;
|
|
3354
|
-
this._armed = found === query.limit;
|
|
3129
|
+
this._armed = stopped || found === query.limit;
|
|
3355
3130
|
return { subscribed: 0, last_id, marked: 0, scanned: true };
|
|
3356
3131
|
}
|
|
3357
3132
|
/**
|
|
@@ -3676,6 +3451,18 @@ var validating_patch = (me, state2, partial, event) => {
|
|
|
3676
3451
|
);
|
|
3677
3452
|
};
|
|
3678
3453
|
var DEFAULT_BATCH = 500;
|
|
3454
|
+
var cache_put = (stream, entry) => cache2().set(stream, entry).catch(
|
|
3455
|
+
(err) => log().warn(
|
|
3456
|
+
err,
|
|
3457
|
+
"cache set skipped; the next load re-folds from the store"
|
|
3458
|
+
)
|
|
3459
|
+
);
|
|
3460
|
+
var cache_drop = (stream) => cache2().invalidate(stream).catch(
|
|
3461
|
+
(err) => log().warn(
|
|
3462
|
+
err,
|
|
3463
|
+
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
3464
|
+
)
|
|
3465
|
+
);
|
|
3679
3466
|
async function snap(snapshot) {
|
|
3680
3467
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
3681
3468
|
try {
|
|
@@ -3878,19 +3665,14 @@ async function load(me, target, callback, patch_fn = bare_patch) {
|
|
|
3878
3665
|
);
|
|
3879
3666
|
const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
|
|
3880
3667
|
if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
|
|
3881
|
-
await
|
|
3668
|
+
await cache_put(stream, {
|
|
3882
3669
|
stream,
|
|
3883
3670
|
state: state2,
|
|
3884
3671
|
version: event.version,
|
|
3885
3672
|
event_id: event.id,
|
|
3886
3673
|
patches,
|
|
3887
3674
|
snaps
|
|
3888
|
-
})
|
|
3889
|
-
(err) => log().warn(
|
|
3890
|
-
err,
|
|
3891
|
-
"cache set skipped; the next load re-folds from the store"
|
|
3892
|
-
)
|
|
3893
|
-
);
|
|
3675
|
+
});
|
|
3894
3676
|
}
|
|
3895
3677
|
return {
|
|
3896
3678
|
event,
|
|
@@ -3978,14 +3760,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
3978
3760
|
reactingTo ? expectedVersion : expected
|
|
3979
3761
|
);
|
|
3980
3762
|
} 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
|
-
}
|
|
3763
|
+
if (error instanceof ConcurrencyError) void cache_drop(stream);
|
|
3989
3764
|
throw error;
|
|
3990
3765
|
}
|
|
3991
3766
|
let { state: state2, patches } = snapshot;
|
|
@@ -4012,26 +3787,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
4012
3787
|
const snap_event = snapped ? await snap(last) : void 0;
|
|
4013
3788
|
if (!me.pii_aware) {
|
|
4014
3789
|
if (contiguous)
|
|
4015
|
-
|
|
3790
|
+
void cache_put(stream, {
|
|
4016
3791
|
stream,
|
|
4017
3792
|
state: last.state,
|
|
4018
3793
|
version: snap_event?.version ?? last.event.version,
|
|
4019
3794
|
event_id: snap_event?.id ?? last.event.id,
|
|
4020
3795
|
patches: snap_event ? 0 : last.patches,
|
|
4021
3796
|
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
|
-
);
|
|
3797
|
+
});
|
|
3798
|
+
else void cache_drop(stream);
|
|
4035
3799
|
}
|
|
4036
3800
|
return snapshots;
|
|
4037
3801
|
} catch (error) {
|
|
@@ -4246,6 +4010,22 @@ function fetch_failure_policy(registry) {
|
|
|
4246
4010
|
}
|
|
4247
4011
|
return { blockOnError, maxRetries };
|
|
4248
4012
|
}
|
|
4013
|
+
function resolver_failed(lease, { error, options }) {
|
|
4014
|
+
log().error(error);
|
|
4015
|
+
const block2 = options.blockOnError && lease.retry >= options.maxRetries;
|
|
4016
|
+
return {
|
|
4017
|
+
lease,
|
|
4018
|
+
handled: 0,
|
|
4019
|
+
acked_at: lease.at,
|
|
4020
|
+
error,
|
|
4021
|
+
...block2 ? { block: true } : {
|
|
4022
|
+
next_attempt_at: Date.now() + compute_backoff_delay(
|
|
4023
|
+
lease.retry,
|
|
4024
|
+
options.backoff ?? DEFAULT_BACKOFF
|
|
4025
|
+
)
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4249
4029
|
function warn_misrouted(seen, stream, handler, event) {
|
|
4250
4030
|
report_once(
|
|
4251
4031
|
seen,
|
|
@@ -4279,13 +4059,23 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
4279
4059
|
);
|
|
4280
4060
|
for (const f of fetched) {
|
|
4281
4061
|
const { stream, events } = f;
|
|
4062
|
+
let failed;
|
|
4282
4063
|
const payloads = events.flatMap((event) => {
|
|
4283
4064
|
const register = registry.events[event.name];
|
|
4284
4065
|
if (!register) return [];
|
|
4285
4066
|
return [...register.reactions.values()].filter((reaction) => {
|
|
4286
4067
|
const resolver = reaction.resolver;
|
|
4287
4068
|
const dynamic = typeof resolver === "function";
|
|
4288
|
-
|
|
4069
|
+
let resolved;
|
|
4070
|
+
try {
|
|
4071
|
+
resolved = dynamic ? resolver(event) : resolver;
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
failed ??= {
|
|
4074
|
+
error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
|
|
4075
|
+
options: reaction.options
|
|
4076
|
+
};
|
|
4077
|
+
return false;
|
|
4078
|
+
}
|
|
4289
4079
|
if (!resolved || resolved.target !== stream) return false;
|
|
4290
4080
|
if (!dynamic || !batch_handlers.has(stream)) return true;
|
|
4291
4081
|
warn_misrouted(
|
|
@@ -4297,7 +4087,7 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
4297
4087
|
return false;
|
|
4298
4088
|
}).map((reaction) => ({ ...reaction, event }));
|
|
4299
4089
|
});
|
|
4300
|
-
fetch_map.set(stream, { fetch: f, payloads });
|
|
4090
|
+
fetch_map.set(stream, { fetch: f, payloads, failed });
|
|
4301
4091
|
}
|
|
4302
4092
|
const handled = await Promise.all(
|
|
4303
4093
|
leased.map((lease) => {
|
|
@@ -4318,6 +4108,8 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
4318
4108
|
};
|
|
4319
4109
|
return Promise.resolve(failed);
|
|
4320
4110
|
}
|
|
4111
|
+
if (entry.failed)
|
|
4112
|
+
return Promise.resolve(resolver_failed(lease, entry.failed));
|
|
4321
4113
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
4322
4114
|
const { payloads } = entry;
|
|
4323
4115
|
const exhausted = budget_exhausted(
|
|
@@ -4794,41 +4586,23 @@ function validate_only_lanes(options, lanes) {
|
|
|
4794
4586
|
}
|
|
4795
4587
|
var Act = class {
|
|
4796
4588
|
_emitter = new import_node_events.default();
|
|
4797
|
-
/** Circuit breaker shared by all drain lanes. */
|
|
4798
4589
|
_breaker;
|
|
4799
|
-
/** Whether this instance subscribes to `Store.notify`. */
|
|
4800
4590
|
_listen;
|
|
4801
|
-
/** Whether this instance runs reactions (drain, settle, correlate). */
|
|
4802
4591
|
_drain;
|
|
4803
|
-
/** Event names with at least one registered reaction (computed at build time) */
|
|
4804
4592
|
_reactive_events;
|
|
4805
|
-
/** One DrainController per active lane, keyed by lane name. */
|
|
4806
4593
|
_drain_controllers;
|
|
4807
|
-
/** Correlation: lazy init and the scan that marks work. */
|
|
4808
4594
|
_correlate;
|
|
4809
|
-
/** Debounced correlate→drain catch-up loop. */
|
|
4810
4595
|
_settle;
|
|
4811
|
-
/** Timer behind {@link start_correlations}. */
|
|
4812
4596
|
_poll;
|
|
4813
4597
|
/** The deprecated `start_correlations` callback, while the poll runs. */
|
|
4814
4598
|
_on_discovered;
|
|
4815
4599
|
/**
|
|
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.
|
|
4600
|
+
* Wired at construction against the store current then, so a store
|
|
4601
|
+
* injected after `build()` is never notified.
|
|
4827
4602
|
*/
|
|
4828
4603
|
_notify_disposer;
|
|
4829
4604
|
/** Public registry — kept as-is per the no-prefix-on-public convention. */
|
|
4830
4605
|
registry;
|
|
4831
|
-
/** Map of state name → state definition; populated by the builder. */
|
|
4832
4606
|
_states;
|
|
4833
4607
|
/**
|
|
4834
4608
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
@@ -4854,15 +4628,9 @@ var Act = class {
|
|
|
4854
4628
|
return listeners.length > 0;
|
|
4855
4629
|
}
|
|
4856
4630
|
/**
|
|
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.
|
|
4631
|
+
* The one place a store failure is logged and emitted (the breaker's
|
|
4632
|
+
* `on_error`). The emit is guarded: an unhandled `"error"` emission
|
|
4633
|
+
* would crash the process.
|
|
4866
4634
|
*/
|
|
4867
4635
|
_emit_error(error, circuit) {
|
|
4868
4636
|
this._logger.error(error);
|
|
@@ -4884,58 +4652,29 @@ var Act = class {
|
|
|
4884
4652
|
this._emitter.off(event, listener);
|
|
4885
4653
|
return this;
|
|
4886
4654
|
}
|
|
4887
|
-
/** Batch handlers for static-target projections (target → handler) */
|
|
4888
4655
|
_batch_handlers;
|
|
4889
|
-
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
4890
4656
|
_es;
|
|
4891
|
-
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
4892
4657
|
_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
|
-
*/
|
|
4658
|
+
/** Unambiguous: merge rejects an event name declared by two states. */
|
|
4899
4659
|
_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
|
-
*/
|
|
4660
|
+
/** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
|
|
4907
4661
|
_event_to_lanes;
|
|
4908
|
-
/** What `internal/audit.ts` needs, built once at construction. */
|
|
4909
4662
|
_audit_deps;
|
|
4910
|
-
/** Logger resolved at construction time (after user port configuration) */
|
|
4911
4663
|
_logger = log();
|
|
4912
4664
|
/** This Act's ports: its own bag, or the singleton adapters. */
|
|
4913
4665
|
_ports;
|
|
4914
|
-
/**
|
|
4915
|
-
* Runs a body inside this Act's ports frame, so internal
|
|
4916
|
-
* `store()`/`cache()` calls resolve to `_ports`.
|
|
4917
|
-
*/
|
|
4666
|
+
/** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
|
|
4918
4667
|
_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
4668
|
_correlator;
|
|
4926
|
-
|
|
4927
|
-
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
4669
|
+
// Bound once, reused by every reaction payload.
|
|
4928
4670
|
_bound_do = this.do.bind(this);
|
|
4929
4671
|
_bound_load = this.load.bind(this);
|
|
4930
4672
|
_bound_query = this.query.bind(this);
|
|
4931
4673
|
_bound_query_array = this.query_array.bind(this);
|
|
4932
4674
|
_bound_forget = this.forget.bind(this);
|
|
4933
|
-
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
4934
4675
|
_handle;
|
|
4935
4676
|
_handle_batch;
|
|
4936
|
-
/** Declared drain lanes. */
|
|
4937
4677
|
_lanes;
|
|
4938
|
-
/** Serializes close critical sections per stream. */
|
|
4939
4678
|
_close_lock = new CloseLock();
|
|
4940
4679
|
/** Every lane a controller can exist for: `"default"` plus each declared one. */
|
|
4941
4680
|
_declared_lanes;
|
|
@@ -4943,26 +4682,7 @@ var Act = class {
|
|
|
4943
4682
|
get lanes() {
|
|
4944
4683
|
return this._lanes;
|
|
4945
4684
|
}
|
|
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
|
-
*/
|
|
4685
|
+
/** Use the {@link act} builder rather than constructing directly. */
|
|
4966
4686
|
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
|
|
4967
4687
|
this.registry = registry;
|
|
4968
4688
|
this._states = states;
|
|
@@ -5013,11 +4733,7 @@ var Act = class {
|
|
|
5013
4733
|
this._notify_disposer = this._wire_notify(this._ports.store);
|
|
5014
4734
|
register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
|
|
5015
4735
|
}
|
|
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
|
-
*/
|
|
4736
|
+
/** Shared by drain, settle and autoclose; bad knobs throw at build. */
|
|
5021
4737
|
_build_breaker(options) {
|
|
5022
4738
|
return new CircuitBreaker(
|
|
5023
4739
|
resolveCircuitBreakerConfig(options.circuitBreaker),
|
|
@@ -5031,11 +4747,7 @@ var Act = class {
|
|
|
5031
4747
|
}
|
|
5032
4748
|
);
|
|
5033
4749
|
}
|
|
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
|
-
*/
|
|
4750
|
+
/** Apps that declare no lanes pass no lane filter to `claim`. */
|
|
5039
4751
|
_build_drain_controllers(options, lanes) {
|
|
5040
4752
|
const all_lanes = this._declared_lanes;
|
|
5041
4753
|
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
@@ -5054,8 +4766,18 @@ var Act = class {
|
|
|
5054
4766
|
on_acked: (acked) => this.emit("acked", acked),
|
|
5055
4767
|
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
5056
4768
|
// Reaction-requested close: the same machinery as `app.close`.
|
|
4769
|
+
// A store failure reaches the breaker; anything else (the archive
|
|
4770
|
+
// callback is app code) is logged here, never counted.
|
|
5057
4771
|
on_close: async (targets) => {
|
|
5058
|
-
|
|
4772
|
+
try {
|
|
4773
|
+
await this._run_close(targets);
|
|
4774
|
+
} catch (error) {
|
|
4775
|
+
if (error instanceof StoreError) throw error;
|
|
4776
|
+
this._logger.error(
|
|
4777
|
+
error,
|
|
4778
|
+
`Reaction-requested close of ${targets.map((t) => `"${t.stream}"`).join(", ")} failed and was not retried; close it with app.close(...) once the cause is fixed.`
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
5059
4781
|
},
|
|
5060
4782
|
breaker: this._breaker,
|
|
5061
4783
|
run_scoped: this._scoped,
|
|
@@ -5071,11 +4793,7 @@ var Act = class {
|
|
|
5071
4793
|
}
|
|
5072
4794
|
return controllers;
|
|
5073
4795
|
}
|
|
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
|
-
*/
|
|
4796
|
+
/** One process can't see the cluster, so it reports its own lane gaps. */
|
|
5079
4797
|
_advise_orphaned_lanes(options) {
|
|
5080
4798
|
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
5081
4799
|
const active = new Set(this._drain_controllers.keys());
|
|
@@ -5097,11 +4815,6 @@ var Act = class {
|
|
|
5097
4815
|
routed_events: new Set(this._event_to_lanes.keys())
|
|
5098
4816
|
};
|
|
5099
4817
|
}
|
|
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
4818
|
_build_correlate(options, classification) {
|
|
5106
4819
|
return new CorrelateCycle({
|
|
5107
4820
|
registry: this.registry,
|
|
@@ -5119,12 +4832,7 @@ var Act = class {
|
|
|
5119
4832
|
on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
|
|
5120
4833
|
});
|
|
5121
4834
|
}
|
|
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
|
-
*/
|
|
4835
|
+
/** Cold start: an idle deferred stream still fires on time after a restart. */
|
|
5128
4836
|
async _seed_persisted_defers() {
|
|
5129
4837
|
const now = Date.now();
|
|
5130
4838
|
await walk_streams(store2(), (pos) => {
|
|
@@ -5133,7 +4841,6 @@ var Act = class {
|
|
|
5133
4841
|
controller?.seed_defer(pos.stream, pos.deferred_at);
|
|
5134
4842
|
});
|
|
5135
4843
|
}
|
|
5136
|
-
/** Settle loop driving correlate + drain to quiescence. */
|
|
5137
4844
|
_build_settle(options) {
|
|
5138
4845
|
return new SettleLoop(
|
|
5139
4846
|
{
|
|
@@ -5147,7 +4854,6 @@ var Act = class {
|
|
|
5147
4854
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
5148
4855
|
);
|
|
5149
4856
|
}
|
|
5150
|
-
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
5151
4857
|
_shutdown_promise;
|
|
5152
4858
|
/**
|
|
5153
4859
|
* Per-instance teardown: stop scheduling new work, give drain cycles
|
|
@@ -5187,14 +4893,8 @@ var Act = class {
|
|
|
5187
4893
|
return this._shutdown_promise;
|
|
5188
4894
|
}
|
|
5189
4895
|
/**
|
|
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.
|
|
4896
|
+
* The largest `leaseMillis` among lanes with a cycle in flight (the
|
|
4897
|
+
* default for none or 0, and for a running settle), capped.
|
|
5198
4898
|
*/
|
|
5199
4899
|
_derive_grace_ms(running, settling) {
|
|
5200
4900
|
let max = settling ? DEFAULT_LEASE_MILLIS : 0;
|
|
@@ -5223,12 +4923,7 @@ var Act = class {
|
|
|
5223
4923
|
clearTimeout(timer);
|
|
5224
4924
|
}
|
|
5225
4925
|
}
|
|
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
|
-
*/
|
|
4926
|
+
/** Failures are logged, never thrown: `notify` is a hint. */
|
|
5232
4927
|
async _wire_notify(s) {
|
|
5233
4928
|
if (this._reactive_events.size === 0) return void 0;
|
|
5234
4929
|
if (!s.notify) return void 0;
|
|
@@ -5540,20 +5235,11 @@ var Act = class {
|
|
|
5540
5235
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
5541
5236
|
return this._scoped(() => this._drain_all(options));
|
|
5542
5237
|
}
|
|
5543
|
-
/** Arm every active lane controller. */
|
|
5544
5238
|
_arm_all() {
|
|
5545
5239
|
this._correlate.arm();
|
|
5546
5240
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
5547
5241
|
}
|
|
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
|
-
*/
|
|
5242
|
+
/** Arms only the lanes these events react on; true when any was armed. */
|
|
5557
5243
|
_arm_for_event_names(names) {
|
|
5558
5244
|
const to_arm = /* @__PURE__ */ new Set();
|
|
5559
5245
|
for (const name of names) {
|
|
@@ -5570,14 +5256,7 @@ var Act = class {
|
|
|
5570
5256
|
for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
|
|
5571
5257
|
return true;
|
|
5572
5258
|
}
|
|
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`. */
|
|
5259
|
+
/** In parallel, so a slow lane never holds up a fast one. */
|
|
5581
5260
|
async _drain_all(options) {
|
|
5582
5261
|
const results = await Promise.all(
|
|
5583
5262
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
@@ -5650,14 +5329,7 @@ var Act = class {
|
|
|
5650
5329
|
const { subscribed, last_id } = await this._correlate_scanned(query);
|
|
5651
5330
|
return { subscribed, last_id };
|
|
5652
5331
|
}
|
|
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
|
-
*/
|
|
5332
|
+
/** `correlate` plus `scanned`, which settle needs for the breaker. */
|
|
5661
5333
|
async _correlate_scanned(query, lease = false) {
|
|
5662
5334
|
if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
|
|
5663
5335
|
return this._scoped(async () => {
|
|
@@ -5998,11 +5670,7 @@ var Act = class {
|
|
|
5998
5670
|
async prioritize(filter, priority) {
|
|
5999
5671
|
return this._scoped(() => store2().prioritize(filter, priority));
|
|
6000
5672
|
}
|
|
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
|
-
*/
|
|
5673
|
+
/** Shared by `close()` and reaction-requested closes. */
|
|
6006
5674
|
async _run_close(targets) {
|
|
6007
5675
|
const close_actor = { id: "$close", name: "close" };
|
|
6008
5676
|
const result = await run_close_cycle(targets, {
|
|
@@ -6083,10 +5751,7 @@ var Act = class {
|
|
|
6083
5751
|
*/
|
|
6084
5752
|
async close(targets) {
|
|
6085
5753
|
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
|
-
});
|
|
5754
|
+
return this._scoped(() => this._run_close(targets));
|
|
6090
5755
|
}
|
|
6091
5756
|
/**
|
|
6092
5757
|
* Debounced, non-blocking correlate→drain cycle.
|