@rotorsoft/act 1.32.23 → 1.32.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.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/audit.d.ts +5 -45
- package/dist/@types/internal/audit.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 +23 -163
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +19 -129
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +21 -158
- 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 +125 -654
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +107 -417
- 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))
|
|
@@ -3128,71 +2890,27 @@ function report_lane_conflict(seen, handler, target, kept, dropped) {
|
|
|
3128
2890
|
var CorrelateCycle = class {
|
|
3129
2891
|
_checkpoint = -1;
|
|
3130
2892
|
_initialized = false;
|
|
3131
|
-
/**
|
|
3132
|
-
* This worker's identity for the correlation lease. A per-instance
|
|
3133
|
-
* UUID, matching the drain's convention, so a renewal is recognised as the
|
|
3134
|
-
* same holder and a restarted process never inherits a stale claim.
|
|
3135
|
-
*/
|
|
2893
|
+
/** Lease holder id; per instance, so a restart never inherits a stale claim. */
|
|
3136
2894
|
_by = (0, import_node_crypto.randomUUID)();
|
|
3137
|
-
/**
|
|
3138
|
-
* Which correlator this is. Computed once from the registry so identical
|
|
3139
|
-
* workers share a lease and unrelated applications never do.
|
|
3140
|
-
*/
|
|
3141
2895
|
_key;
|
|
3142
|
-
/**
|
|
3143
|
-
* When this worker's correlation lease runs out, as a local clock reading,
|
|
3144
|
-
* or 0 when it holds none.
|
|
3145
|
-
*
|
|
3146
|
-
* Cached so the holder doesn't ask the store on every pass. A stale belief
|
|
3147
|
-
* can only cause a duplicate scan, and marks are idempotent, so it costs
|
|
3148
|
-
* work, never correctness.
|
|
3149
|
-
*/
|
|
2896
|
+
/** Local expiry of the held lease (0 = none); stale only costs a duplicate scan. */
|
|
3150
2897
|
_lease_until = 0;
|
|
3151
|
-
/**
|
|
3152
|
-
* Whether a scan might find anything, like the drain's flag: a commit or
|
|
3153
|
-
* notification raises it, a short page lowers it, and a disarmed scan
|
|
3154
|
-
* returns without touching the store.
|
|
3155
|
-
*
|
|
3156
|
-
* Starts armed: the log may already hold events this process has never
|
|
3157
|
-
* correlated, and only a scan can find out.
|
|
3158
|
-
*/
|
|
2898
|
+
/** Raised by a commit or notification, lowered by a short page. Starts armed. */
|
|
3159
2899
|
_armed = true;
|
|
3160
|
-
/** In-flight init, memoized for single-flight and cleared on failure. */
|
|
3161
2900
|
_init_promise;
|
|
3162
|
-
//
|
|
3163
|
-
//
|
|
3164
|
-
// target: priority/lane only when the resolution beats the recorded floor,
|
|
3165
|
-
// otherwise the row's own values. See {@link Subscription}.
|
|
2901
|
+
// Bounded by `maxSubscribedStreams`; static records live apart so eviction
|
|
2902
|
+
// never drops their +Infinity floor.
|
|
3166
2903
|
_dynamic_subscriptions;
|
|
3167
|
-
/**
|
|
3168
|
-
* What each static target was subscribed at by `init`. A plain map, never
|
|
3169
|
-
* evicted: the collection is the build-time `_static_targets` list, so it
|
|
3170
|
-
* is already bounded by the registry and costs nothing the registry does
|
|
3171
|
-
* not already hold.
|
|
3172
|
-
*
|
|
3173
|
-
* Kept out of the LRU so eviction can never drop a static target's
|
|
3174
|
-
* `+Infinity` floor and let a dynamic resolution re-lane it.
|
|
3175
|
-
*/
|
|
3176
2904
|
_static_subscriptions = /* @__PURE__ */ new Map();
|
|
3177
|
-
/** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
|
|
3178
2905
|
_patterns = new LruMap(PATTERN_CACHE_SIZE);
|
|
3179
2906
|
_registry;
|
|
3180
2907
|
_static_targets;
|
|
3181
2908
|
_cd;
|
|
3182
2909
|
_on_init;
|
|
3183
|
-
/**
|
|
3184
|
-
* Async cold-start hook, run once after `on_init` inside `init()`. The
|
|
3185
|
-
* orchestrator re-seeds its defer timers from the persisted `deferred_at`
|
|
3186
|
-
* here, so an idle deferred stream re-arms across a restart.
|
|
3187
|
-
*/
|
|
2910
|
+
/** Runs once after `on_init`; re-seeds defer timers across a restart. */
|
|
3188
2911
|
_on_init_async;
|
|
3189
|
-
/** Lanes a controller exists for. See {@link CorrelateCycleDeps}. */
|
|
3190
2912
|
_declared_lanes;
|
|
3191
|
-
/**
|
|
3192
|
-
* Offending declarations already reported, so a resolver firing for every
|
|
3193
|
-
* matching event reports once. Owned by the instance rather than the
|
|
3194
|
-
* module: `internal/` holds no module-level state.
|
|
3195
|
-
*/
|
|
2913
|
+
/** Report-once keys; per instance because `internal/` holds no module state. */
|
|
3196
2914
|
_reported = /* @__PURE__ */ new Set();
|
|
3197
2915
|
constructor({
|
|
3198
2916
|
registry,
|
|
@@ -3213,28 +2931,13 @@ var CorrelateCycle = class {
|
|
|
3213
2931
|
this._on_init = on_init;
|
|
3214
2932
|
this._on_init_async = on_init_async;
|
|
3215
2933
|
}
|
|
3216
|
-
/** Last correlated event id. */
|
|
3217
2934
|
get checkpoint() {
|
|
3218
2935
|
return this._checkpoint;
|
|
3219
2936
|
}
|
|
3220
|
-
/**
|
|
3221
|
-
* Signal that a commit (local or remote) may have produced events this
|
|
3222
|
-
* process has not correlated. Cheap and idempotent — the orchestrator calls
|
|
3223
|
-
* it on every commit and every notification.
|
|
3224
|
-
*/
|
|
2937
|
+
/** A commit (local or remote) may have produced uncorrelated events. */
|
|
3225
2938
|
arm() {
|
|
3226
2939
|
this._armed = true;
|
|
3227
2940
|
}
|
|
3228
|
-
/**
|
|
3229
|
-
* Initialize correlation state on first call.
|
|
3230
|
-
* - Reads the durable correlate checkpoint (and max(at)) from the store,
|
|
3231
|
-
* flooring a first boot at `watermark - back_scan` so an event
|
|
3232
|
-
* committed-but-not-correlated before a crash is re-scanned on
|
|
3233
|
-
* restart instead of skipped
|
|
3234
|
-
* - Subscribes static resolver targets (idempotent upsert)
|
|
3235
|
-
* - Populates the subscribed-streams LRU
|
|
3236
|
-
* - Fires `on_init` once (Act uses this to flag a cold-start drain)
|
|
3237
|
-
*/
|
|
3238
2941
|
async init() {
|
|
3239
2942
|
if (this._initialized) return;
|
|
3240
2943
|
if (!this._init_promise) {
|
|
@@ -3261,16 +2964,7 @@ var CorrelateCycle = class {
|
|
|
3261
2964
|
}
|
|
3262
2965
|
await this._on_init_async?.();
|
|
3263
2966
|
}
|
|
3264
|
-
/**
|
|
3265
|
-
* Start correlation over after the event log was replaced wholesale
|
|
3266
|
-
* (`restore`). The restored log is renumbered from the start and its
|
|
3267
|
-
* subscription rows are gone, so the in-memory scan position and both
|
|
3268
|
-
* subscription records describe a log that no longer exists. Dropping them
|
|
3269
|
-
* and the init latch makes the next pass run cold-start again: static
|
|
3270
|
-
* targets are re-subscribed, the checkpoint is re-read from the store
|
|
3271
|
-
* (which `restore` reset), and dynamic targets are rediscovered by the
|
|
3272
|
-
* scan.
|
|
3273
|
-
*/
|
|
2967
|
+
/** After `restore` replaced the log: forget everything and cold-start again. */
|
|
3274
2968
|
restart() {
|
|
3275
2969
|
this._initialized = false;
|
|
3276
2970
|
this._init_promise = void 0;
|
|
@@ -3279,21 +2973,11 @@ var CorrelateCycle = class {
|
|
|
3279
2973
|
this._static_subscriptions.clear();
|
|
3280
2974
|
this._armed = true;
|
|
3281
2975
|
}
|
|
3282
|
-
/**
|
|
3283
|
-
* Forget dynamic targets whose subscription rows no longer exist (a full
|
|
3284
|
-
* close deletes the row), so a later scan re-subscribes them. Static
|
|
3285
|
-
* targets keep their records.
|
|
3286
|
-
*/
|
|
2976
|
+
/** Forget dynamic targets whose rows a full close deleted, so they re-subscribe. */
|
|
3287
2977
|
forget_subscribed(streams) {
|
|
3288
2978
|
for (const stream of streams) this._dynamic_subscriptions.delete(stream);
|
|
3289
2979
|
}
|
|
3290
|
-
/**
|
|
3291
|
-
* Would an event from `stream` be fetched for a target subscribed with
|
|
3292
|
-
* `source`? The subscription's source is the filter `fetch` queries with
|
|
3293
|
-
* — literal names by equality, patterns compiled as a `RegExp` — so an
|
|
3294
|
-
* event outside it is not work for that target and must not mark it.
|
|
3295
|
-
* No source means the target consumes every stream.
|
|
3296
|
-
*/
|
|
2980
|
+
/** Would the target's own `fetch` (filtered by `source`) return this event? */
|
|
3297
2981
|
_in_fetch_window(source, stream) {
|
|
3298
2982
|
if (source === void 0 || source === stream) return true;
|
|
3299
2983
|
if (is_literal_source(source)) return false;
|
|
@@ -3304,16 +2988,7 @@ var CorrelateCycle = class {
|
|
|
3304
2988
|
}
|
|
3305
2989
|
return pattern.test(stream);
|
|
3306
2990
|
}
|
|
3307
|
-
/**
|
|
3308
|
-
* Scan the events past the checkpoint, resolve each to its target
|
|
3309
|
-
* streams, and record what it found through `cd.subscribe` — new dynamic
|
|
3310
|
-
* targets get registered, and every target an event resolved to gets its
|
|
3311
|
-
* **work mark** raised to that event's id.
|
|
3312
|
-
*
|
|
3313
|
-
* Both resolver kinds are walked. A static target is already subscribed
|
|
3314
|
-
* at init, but marking it is what makes it claimable without probing the
|
|
3315
|
-
* event log, so the scan runs for every app.
|
|
3316
|
-
*/
|
|
2991
|
+
/** Scan past the checkpoint, subscribe new targets and raise their work marks. */
|
|
3317
2992
|
async correlate(query = { after: -1, limit: 10 }, lease = false) {
|
|
3318
2993
|
await this.init();
|
|
3319
2994
|
if (!this._armed)
|
|
@@ -3398,10 +3073,8 @@ var CorrelateCycle = class {
|
|
|
3398
3073
|
}
|
|
3399
3074
|
}
|
|
3400
3075
|
},
|
|
3401
|
-
//
|
|
3402
|
-
//
|
|
3403
|
-
// reactions. Resolvers therefore see `pii: null`; correlate runs
|
|
3404
|
-
// actor-less, so plaintext here would be un-gated disclosure.
|
|
3076
|
+
// Resolvers see `pii: null`: correlate runs actor-less, and one
|
|
3077
|
+
// undecryptable row would otherwise stop every stream's reactions.
|
|
3405
3078
|
{ ...query, after, with_pii: false }
|
|
3406
3079
|
);
|
|
3407
3080
|
if (this._declared_lanes.size > 1) await this._recall(correlated);
|
|
@@ -3444,14 +3117,9 @@ var CorrelateCycle = class {
|
|
|
3444
3117
|
return { subscribed: 0, last_id, marked: 0, scanned: true };
|
|
3445
3118
|
}
|
|
3446
3119
|
/**
|
|
3447
|
-
*
|
|
3448
|
-
*
|
|
3449
|
-
*
|
|
3450
|
-
* has one, says what lane and priority it was subscribed at; a resolution
|
|
3451
|
-
* that doesn't beat that priority carries the row's values, exactly as if
|
|
3452
|
-
* the record had never been evicted. One read per target with no record,
|
|
3453
|
-
* and only when more than one lane is declared: with one lane there is no
|
|
3454
|
-
* lane to lose, and the store already keeps the higher priority.
|
|
3120
|
+
* Read the row of each target this scan had no record of (new or evicted),
|
|
3121
|
+
* so a resolution that doesn't beat its priority keeps the row's lane.
|
|
3122
|
+
* Only called with more than one lane declared.
|
|
3455
3123
|
*/
|
|
3456
3124
|
async _recall(correlated) {
|
|
3457
3125
|
const missed = [...correlated].filter(([, entry]) => entry.missed);
|
|
@@ -3487,13 +3155,7 @@ var CorrelateCycle = class {
|
|
|
3487
3155
|
})
|
|
3488
3156
|
);
|
|
3489
3157
|
}
|
|
3490
|
-
/**
|
|
3491
|
-
* Hand the correlation lease back early.
|
|
3492
|
-
*
|
|
3493
|
-
* The port has no release verb; re-acquiring as the same holder with a
|
|
3494
|
-
* zero duration releases it. Best-effort: a failure costs the lease's
|
|
3495
|
-
* remaining lifetime, as a crash would.
|
|
3496
|
-
*/
|
|
3158
|
+
/** Release the lease early by re-acquiring it for zero ms. Best-effort. */
|
|
3497
3159
|
async release_correlation() {
|
|
3498
3160
|
try {
|
|
3499
3161
|
this._lease_until = 0;
|
|
@@ -3776,6 +3438,18 @@ var validating_patch = (me, state2, partial, event) => {
|
|
|
3776
3438
|
);
|
|
3777
3439
|
};
|
|
3778
3440
|
var DEFAULT_BATCH = 500;
|
|
3441
|
+
var cache_put = (stream, entry) => cache2().set(stream, entry).catch(
|
|
3442
|
+
(err) => log().warn(
|
|
3443
|
+
err,
|
|
3444
|
+
"cache set skipped; the next load re-folds from the store"
|
|
3445
|
+
)
|
|
3446
|
+
);
|
|
3447
|
+
var cache_drop = (stream) => cache2().invalidate(stream).catch(
|
|
3448
|
+
(err) => log().warn(
|
|
3449
|
+
err,
|
|
3450
|
+
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
3451
|
+
)
|
|
3452
|
+
);
|
|
3779
3453
|
async function snap(snapshot) {
|
|
3780
3454
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
3781
3455
|
try {
|
|
@@ -3970,38 +3644,28 @@ async function load(me, target, callback, patch_fn = bare_patch) {
|
|
|
3970
3644
|
{
|
|
3971
3645
|
stream,
|
|
3972
3646
|
stream_exact: true,
|
|
3973
|
-
// `with_snaps` on every non-time-travel load
|
|
3974
|
-
// latest snapshot
|
|
3975
|
-
//
|
|
3976
|
-
// result, so a windowed close that pruned the events in between can't
|
|
3977
|
-
// make the fold apply the tail onto stale state.
|
|
3647
|
+
// `with_snaps` on every non-time-travel load: cold, it resumes from the
|
|
3648
|
+
// latest snapshot; warm, it keeps a snapshot above a stale checkpoint,
|
|
3649
|
+
// so a windowed close's prune can't fold the tail onto stale state.
|
|
3978
3650
|
...cached ? { after: cached.event_id, with_snaps: true } : { ...time_travel ? {} : { with_snaps: true }, ...asOf }
|
|
3979
3651
|
}
|
|
3980
3652
|
);
|
|
3981
3653
|
const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
|
|
3982
3654
|
if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
|
|
3983
|
-
await
|
|
3655
|
+
await cache_put(stream, {
|
|
3984
3656
|
stream,
|
|
3985
3657
|
state: state2,
|
|
3986
3658
|
version: event.version,
|
|
3987
3659
|
event_id: event.id,
|
|
3988
3660
|
patches,
|
|
3989
3661
|
snaps
|
|
3990
|
-
})
|
|
3991
|
-
(err) => log().warn(
|
|
3992
|
-
err,
|
|
3993
|
-
"cache set skipped; the next load re-folds from the store"
|
|
3994
|
-
)
|
|
3995
|
-
);
|
|
3662
|
+
});
|
|
3996
3663
|
}
|
|
3997
3664
|
return {
|
|
3998
3665
|
event,
|
|
3999
3666
|
state: state2,
|
|
4000
3667
|
version: event?.version ?? cached?.version ?? -1,
|
|
4001
|
-
//
|
|
4002
|
-
// event's id on a cache miss, or the cached checkpoint's event_id on a
|
|
4003
|
-
// warm hit with no new events. Consumers read this instead of a
|
|
4004
|
-
// separate cache lookup that could race a concurrent commit.
|
|
3668
|
+
// Captured with `state`, so consumers never race a separate cache lookup.
|
|
4005
3669
|
id: event?.id ?? cached?.event_id ?? -1,
|
|
4006
3670
|
patches,
|
|
4007
3671
|
snaps,
|
|
@@ -4063,8 +3727,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
4063
3727
|
action: {
|
|
4064
3728
|
name: action2,
|
|
4065
3729
|
...target
|
|
4066
|
-
// payload intentionally omitted
|
|
4067
|
-
// callers correlate via the correlation id when they need it.
|
|
3730
|
+
// payload intentionally omitted
|
|
4068
3731
|
},
|
|
4069
3732
|
event: reactingTo ? {
|
|
4070
3733
|
id: reactingTo.id,
|
|
@@ -4084,14 +3747,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
4084
3747
|
reactingTo ? expectedVersion : expected
|
|
4085
3748
|
);
|
|
4086
3749
|
} catch (error) {
|
|
4087
|
-
if (error instanceof ConcurrencyError)
|
|
4088
|
-
cache2().invalidate(stream).catch(
|
|
4089
|
-
(err) => log().warn(
|
|
4090
|
-
err,
|
|
4091
|
-
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
4092
|
-
)
|
|
4093
|
-
);
|
|
4094
|
-
}
|
|
3750
|
+
if (error instanceof ConcurrencyError) void cache_drop(stream);
|
|
4095
3751
|
throw error;
|
|
4096
3752
|
}
|
|
4097
3753
|
let { state: state2, patches } = snapshot;
|
|
@@ -4118,26 +3774,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
4118
3774
|
const snap_event = snapped ? await snap(last) : void 0;
|
|
4119
3775
|
if (!me.pii_aware) {
|
|
4120
3776
|
if (contiguous)
|
|
4121
|
-
|
|
3777
|
+
void cache_put(stream, {
|
|
4122
3778
|
stream,
|
|
4123
3779
|
state: last.state,
|
|
4124
3780
|
version: snap_event?.version ?? last.event.version,
|
|
4125
3781
|
event_id: snap_event?.id ?? last.event.id,
|
|
4126
3782
|
patches: snap_event ? 0 : last.patches,
|
|
4127
3783
|
snaps: snap_event ? last.snaps + 1 : last.snaps
|
|
4128
|
-
})
|
|
4129
|
-
|
|
4130
|
-
err,
|
|
4131
|
-
"cache set skipped; the next load re-folds from the store"
|
|
4132
|
-
)
|
|
4133
|
-
);
|
|
4134
|
-
else
|
|
4135
|
-
cache2().invalidate(stream).catch(
|
|
4136
|
-
(err) => log().warn(
|
|
4137
|
-
err,
|
|
4138
|
-
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
4139
|
-
)
|
|
4140
|
-
);
|
|
3784
|
+
});
|
|
3785
|
+
else void cache_drop(stream);
|
|
4141
3786
|
}
|
|
4142
3787
|
return snapshots;
|
|
4143
3788
|
} catch (error) {
|
|
@@ -4467,61 +4112,35 @@ var DrainController = class {
|
|
|
4467
4112
|
_locked = false;
|
|
4468
4113
|
_ratio = 0.5;
|
|
4469
4114
|
/**
|
|
4470
|
-
* Local wake
|
|
4471
|
-
*
|
|
4472
|
-
*
|
|
4473
|
-
* the drain alone finds it, and on an idle app nothing else would.
|
|
4474
|
-
* Every drain re-aims it when it ends, and a due entry stays until a pass
|
|
4475
|
-
* handles it or a pass that started after it came due claims nothing, so
|
|
4476
|
-
* the wake keeps draining until every due stream is reached.
|
|
4477
|
-
* `stop()` cancels the timer and a stopped drain never re-schedules it,
|
|
4478
|
-
* so a wake never runs after a stop.
|
|
4115
|
+
* Local wake at the earliest parked (backoff or defer) visit; the schedule
|
|
4116
|
+
* itself is persisted. On an idle app nothing else would drain it. A due
|
|
4117
|
+
* entry stays until handled or until a later pass claims nothing.
|
|
4479
4118
|
*/
|
|
4480
4119
|
_defer = new DeferTimer(() => {
|
|
4481
4120
|
this._armed = true;
|
|
4482
4121
|
void this._deps.run_scoped(() => this.drain());
|
|
4483
4122
|
});
|
|
4484
|
-
/** Worker timer. Set when `start()` is active, undefined otherwise. */
|
|
4485
4123
|
_worker;
|
|
4486
|
-
/**
|
|
4487
|
-
* Misroutings this controller has reported. A resolver returning a
|
|
4488
|
-
* projection's target does so for every matching event; one line per event
|
|
4489
|
-
* would bury the signal it exists to raise.
|
|
4490
|
-
*/
|
|
4491
4124
|
_misrouted = /* @__PURE__ */ new Set();
|
|
4492
4125
|
_stopped = false;
|
|
4493
|
-
/**
|
|
4494
|
-
* Resolves when the cycle in flight finishes, so a graceful shutdown can
|
|
4495
|
-
* wait for it. Never rejects.
|
|
4496
|
-
*/
|
|
4126
|
+
/** Never rejects; graceful shutdown waits on it. */
|
|
4497
4127
|
_inflight;
|
|
4498
4128
|
_inflight_done;
|
|
4499
4129
|
_deps;
|
|
4500
4130
|
constructor(deps) {
|
|
4501
4131
|
this._deps = deps;
|
|
4502
4132
|
}
|
|
4503
|
-
/**
|
|
4504
|
-
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
4505
|
-
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
4506
|
-
* settles to no-progress, the controller disarms itself.
|
|
4507
|
-
*/
|
|
4508
4133
|
arm() {
|
|
4509
4134
|
this._armed = true;
|
|
4510
4135
|
}
|
|
4511
|
-
/**
|
|
4512
|
-
* Re-seed a persisted `deferred_at` into the local wake at cold start, so
|
|
4513
|
-
* an idle deferred stream re-arms at its due-time with no new commit.
|
|
4514
|
-
* Seeds collapse into one timer; the earliest wins.
|
|
4515
|
-
*/
|
|
4136
|
+
/** Cold start: re-seed a persisted `deferred_at` into the local wake. */
|
|
4516
4137
|
seed_defer(stream, at) {
|
|
4517
4138
|
this._defer.set(stream, at);
|
|
4518
4139
|
this._defer.schedule();
|
|
4519
4140
|
}
|
|
4520
|
-
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
4521
4141
|
get armed() {
|
|
4522
4142
|
return this._armed;
|
|
4523
4143
|
}
|
|
4524
|
-
/** The cycle in flight, or `undefined` when idle. */
|
|
4525
4144
|
get inflight() {
|
|
4526
4145
|
return this._inflight;
|
|
4527
4146
|
}
|
|
@@ -4529,19 +4148,10 @@ var DrainController = class {
|
|
|
4529
4148
|
get lease_millis() {
|
|
4530
4149
|
return this._deps.defaults?.leaseMillis;
|
|
4531
4150
|
}
|
|
4532
|
-
/** Lane this controller drains (undefined spans all lanes). */
|
|
4533
4151
|
get lane() {
|
|
4534
4152
|
return this._deps.lane;
|
|
4535
4153
|
}
|
|
4536
|
-
/**
|
|
4537
|
-
* Start a per-lane worker that drains at the lane's `cycleMs`
|
|
4538
|
-
* cadence. When armed, the worker calls `drain()` on every
|
|
4539
|
-
* tick and re-schedules; when not armed, it still re-schedules at
|
|
4540
|
-
* `cycleMs` so a future `arm()` is picked up on the next tick.
|
|
4541
|
-
*
|
|
4542
|
-
* The setTimeout chain uses `unref()` so it doesn't keep the process
|
|
4543
|
-
* alive on its own.
|
|
4544
|
-
*/
|
|
4154
|
+
/** Drains every `cycleMs` while armed; the unref'd timer never holds the process. */
|
|
4545
4155
|
start(cycleMs) {
|
|
4546
4156
|
if (this._worker || this._stopped) return;
|
|
4547
4157
|
const run = this._deps.run_scoped;
|
|
@@ -4554,7 +4164,6 @@ var DrainController = class {
|
|
|
4554
4164
|
this._worker = setTimeout(tick, cycleMs);
|
|
4555
4165
|
this._worker.unref();
|
|
4556
4166
|
}
|
|
4557
|
-
/** Stop the per-lane worker. Idempotent. */
|
|
4558
4167
|
stop() {
|
|
4559
4168
|
this._stopped = true;
|
|
4560
4169
|
if (this._worker) {
|
|
@@ -4563,7 +4172,6 @@ var DrainController = class {
|
|
|
4563
4172
|
}
|
|
4564
4173
|
this._defer.stop();
|
|
4565
4174
|
}
|
|
4566
|
-
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
4567
4175
|
async drain(options = {}) {
|
|
4568
4176
|
if (!this._armed) return EMPTY_DRAIN;
|
|
4569
4177
|
if (this._locked) return EMPTY_DRAIN;
|
|
@@ -4937,41 +4545,23 @@ function validate_only_lanes(options, lanes) {
|
|
|
4937
4545
|
}
|
|
4938
4546
|
var Act = class {
|
|
4939
4547
|
_emitter = new import_node_events.default();
|
|
4940
|
-
/** Circuit breaker shared by all drain lanes. */
|
|
4941
4548
|
_breaker;
|
|
4942
|
-
/** Whether this instance subscribes to `Store.notify`. */
|
|
4943
4549
|
_listen;
|
|
4944
|
-
/** Whether this instance runs reactions (drain, settle, correlate). */
|
|
4945
4550
|
_drain;
|
|
4946
|
-
/** Event names with at least one registered reaction (computed at build time) */
|
|
4947
4551
|
_reactive_events;
|
|
4948
|
-
/** One DrainController per active lane, keyed by lane name. */
|
|
4949
4552
|
_drain_controllers;
|
|
4950
|
-
/** Correlation: lazy init and the scan that marks work. */
|
|
4951
4553
|
_correlate;
|
|
4952
|
-
/** Debounced correlate→drain catch-up loop. */
|
|
4953
4554
|
_settle;
|
|
4954
|
-
/** Timer behind {@link start_correlations}. */
|
|
4955
4555
|
_poll;
|
|
4956
4556
|
/** The deprecated `start_correlations` callback, while the poll runs. */
|
|
4957
4557
|
_on_discovered;
|
|
4958
4558
|
/**
|
|
4959
|
-
*
|
|
4960
|
-
*
|
|
4961
|
-
* itself may be async (the PG adapter checks out a dedicated client
|
|
4962
|
-
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
4963
|
-
* the store doesn't implement `notify` or there are no registered
|
|
4964
|
-
* reactions.
|
|
4965
|
-
*
|
|
4966
|
-
* **Contract:** the configured store must be injected via
|
|
4967
|
-
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
4968
|
-
* orchestrator wires notify against whatever store is current at
|
|
4969
|
-
* construction time — late injection after build is unsupported.
|
|
4559
|
+
* Wired at construction against the store current then, so a store
|
|
4560
|
+
* injected after `build()` is never notified.
|
|
4970
4561
|
*/
|
|
4971
4562
|
_notify_disposer;
|
|
4972
4563
|
/** Public registry — kept as-is per the no-prefix-on-public convention. */
|
|
4973
4564
|
registry;
|
|
4974
|
-
/** Map of state name → state definition; populated by the builder. */
|
|
4975
4565
|
_states;
|
|
4976
4566
|
/**
|
|
4977
4567
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
@@ -4997,15 +4587,9 @@ var Act = class {
|
|
|
4997
4587
|
return listeners.length > 0;
|
|
4998
4588
|
}
|
|
4999
4589
|
/**
|
|
5000
|
-
* The
|
|
5001
|
-
*
|
|
5002
|
-
*
|
|
5003
|
-
* `breaker.failed(now, error)` and never log or emit themselves.
|
|
5004
|
-
*
|
|
5005
|
-
* The emit is guarded against Node's `EventEmitter` contract that an
|
|
5006
|
-
* unhandled `"error"` emission is rethrown (which would crash the process
|
|
5007
|
-
* from inside the drain catch); logging is unconditional so failures are
|
|
5008
|
-
* never silent.
|
|
4590
|
+
* The one place a store failure is logged and emitted (the breaker's
|
|
4591
|
+
* `on_error`). The emit is guarded: an unhandled `"error"` emission
|
|
4592
|
+
* would crash the process.
|
|
5009
4593
|
*/
|
|
5010
4594
|
_emit_error(error, circuit) {
|
|
5011
4595
|
this._logger.error(error);
|
|
@@ -5027,58 +4611,29 @@ var Act = class {
|
|
|
5027
4611
|
this._emitter.off(event, listener);
|
|
5028
4612
|
return this;
|
|
5029
4613
|
}
|
|
5030
|
-
/** Batch handlers for static-target projections (target → handler) */
|
|
5031
4614
|
_batch_handlers;
|
|
5032
|
-
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
5033
4615
|
_es;
|
|
5034
|
-
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
5035
4616
|
_cd;
|
|
5036
|
-
/**
|
|
5037
|
-
* Event-name → owning state, computed at build time. The duplicate-event
|
|
5038
|
-
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
5039
|
-
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
5040
|
-
* set when seeding a `restart` snapshot in multi-state apps.
|
|
5041
|
-
*/
|
|
4617
|
+
/** Unambiguous: merge rejects an event name declared by two states. */
|
|
5042
4618
|
_event_to_state;
|
|
5043
|
-
/**
|
|
5044
|
-
* Event-name → lane fan-in for selective arming. Built by
|
|
5045
|
-
* `classify_registry` once per build. `"all"` means at least one of
|
|
5046
|
-
* the event's reactions is a dynamic resolver (lane opaque until
|
|
5047
|
-
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
5048
|
-
* reactions target.
|
|
5049
|
-
*/
|
|
4619
|
+
/** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
|
|
5050
4620
|
_event_to_lanes;
|
|
5051
|
-
/** What `internal/audit.ts` needs, built once at construction. */
|
|
5052
4621
|
_audit_deps;
|
|
5053
|
-
/** Logger resolved at construction time (after user port configuration) */
|
|
5054
4622
|
_logger = log();
|
|
5055
4623
|
/** This Act's ports: its own bag, or the singleton adapters. */
|
|
5056
4624
|
_ports;
|
|
5057
|
-
/**
|
|
5058
|
-
* Runs a body inside this Act's ports frame, so internal
|
|
5059
|
-
* `store()`/`cache()` calls resolve to `_ports`.
|
|
5060
|
-
*/
|
|
4625
|
+
/** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
|
|
5061
4626
|
_scoped;
|
|
5062
|
-
/**
|
|
5063
|
-
* Correlation-id generator for originating actions. Bound at
|
|
5064
|
-
* construction from `options.correlator ?? default_correlator`. The
|
|
5065
|
-
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
5066
|
-
* uses it via {@link close_correlation}.
|
|
5067
|
-
*/
|
|
5068
4627
|
_correlator;
|
|
5069
|
-
|
|
5070
|
-
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
4628
|
+
// Bound once, reused by every reaction payload.
|
|
5071
4629
|
_bound_do = this.do.bind(this);
|
|
5072
4630
|
_bound_load = this.load.bind(this);
|
|
5073
4631
|
_bound_query = this.query.bind(this);
|
|
5074
4632
|
_bound_query_array = this.query_array.bind(this);
|
|
5075
4633
|
_bound_forget = this.forget.bind(this);
|
|
5076
|
-
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
5077
4634
|
_handle;
|
|
5078
4635
|
_handle_batch;
|
|
5079
|
-
/** Declared drain lanes. */
|
|
5080
4636
|
_lanes;
|
|
5081
|
-
/** Serializes close critical sections per stream. */
|
|
5082
4637
|
_close_lock = new CloseLock();
|
|
5083
4638
|
/** Every lane a controller can exist for: `"default"` plus each declared one. */
|
|
5084
4639
|
_declared_lanes;
|
|
@@ -5086,26 +4641,7 @@ var Act = class {
|
|
|
5086
4641
|
get lanes() {
|
|
5087
4642
|
return this._lanes;
|
|
5088
4643
|
}
|
|
5089
|
-
/**
|
|
5090
|
-
* Create a new Act orchestrator. Prefer the {@link act} builder over
|
|
5091
|
-
* direct construction — `act()...build()` wires the registry, merges
|
|
5092
|
-
* partial states, and collects batch handlers from registered slices
|
|
5093
|
-
* and projections in one pass.
|
|
5094
|
-
*
|
|
5095
|
-
* @param registry Schemas for every event and action across registered states
|
|
5096
|
-
* @param states Merged map of state name → state definition
|
|
5097
|
-
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
5098
|
-
* @param options Tuning knobs — see {@link ActOptions}
|
|
5099
|
-
* @param lanes Declared drain lanes. The builder collects
|
|
5100
|
-
* these from `.withLane(...)` calls; one `DrainController` runs
|
|
5101
|
-
* per lane, plus the implicit `"default"`.
|
|
5102
|
-
* @param patch_fn The per-event patch step selected once by the builder
|
|
5103
|
-
* from `ActOptions.validateFoldedState` — `bare_patch` by
|
|
5104
|
-
* default, `validating_patch` when the flag is on. The builder uses
|
|
5105
|
-
* the same value for its projection-fold handlers, so there is a
|
|
5106
|
-
* single selection site. Defaults to `bare_patch` for direct
|
|
5107
|
-
* construction.
|
|
5108
|
-
*/
|
|
4644
|
+
/** Use the {@link act} builder rather than constructing directly. */
|
|
5109
4645
|
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
|
|
5110
4646
|
this.registry = registry;
|
|
5111
4647
|
this._states = states;
|
|
@@ -5156,11 +4692,7 @@ var Act = class {
|
|
|
5156
4692
|
this._notify_disposer = this._wire_notify(this._ports.store);
|
|
5157
4693
|
register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
|
|
5158
4694
|
}
|
|
5159
|
-
/**
|
|
5160
|
-
* Circuit breaker shared by every store-polling loop (drain, the settle
|
|
5161
|
-
* correlate, autoclose). Validates the knobs eagerly so out-of-range
|
|
5162
|
-
* values throw at build time, not on the first cycle tick.
|
|
5163
|
-
*/
|
|
4695
|
+
/** Shared by drain, settle and autoclose; bad knobs throw at build. */
|
|
5164
4696
|
_build_breaker(options) {
|
|
5165
4697
|
return new CircuitBreaker(
|
|
5166
4698
|
resolveCircuitBreakerConfig(options.circuitBreaker),
|
|
@@ -5174,11 +4706,7 @@ var Act = class {
|
|
|
5174
4706
|
}
|
|
5175
4707
|
);
|
|
5176
4708
|
}
|
|
5177
|
-
/**
|
|
5178
|
-
* One DrainController per active lane, plus the implicit "default" unless
|
|
5179
|
-
* `onlyLanes` excludes it. Apps that declare no lanes pass no lane filter
|
|
5180
|
-
* to `claim`.
|
|
5181
|
-
*/
|
|
4709
|
+
/** Apps that declare no lanes pass no lane filter to `claim`. */
|
|
5182
4710
|
_build_drain_controllers(options, lanes) {
|
|
5183
4711
|
const all_lanes = this._declared_lanes;
|
|
5184
4712
|
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
@@ -5214,11 +4742,7 @@ var Act = class {
|
|
|
5214
4742
|
}
|
|
5215
4743
|
return controllers;
|
|
5216
4744
|
}
|
|
5217
|
-
/**
|
|
5218
|
-
* Startup advisory naming declared lanes that `onlyLanes` leaves without a
|
|
5219
|
-
* controller here. One process can't check that the cluster covers every
|
|
5220
|
-
* lane, so it reports its own gaps.
|
|
5221
|
-
*/
|
|
4745
|
+
/** One process can't see the cluster, so it reports its own lane gaps. */
|
|
5222
4746
|
_advise_orphaned_lanes(options) {
|
|
5223
4747
|
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
5224
4748
|
const active = new Set(this._drain_controllers.keys());
|
|
@@ -5240,11 +4764,6 @@ var Act = class {
|
|
|
5240
4764
|
routed_events: new Set(this._event_to_lanes.keys())
|
|
5241
4765
|
};
|
|
5242
4766
|
}
|
|
5243
|
-
/**
|
|
5244
|
-
* Correlate cycle over the classified registry. The cold-start callback
|
|
5245
|
-
* arms every controller — historical events may need processing —
|
|
5246
|
-
* except on writer-only instances (`drain: false`).
|
|
5247
|
-
*/
|
|
5248
4767
|
_build_correlate(options, classification) {
|
|
5249
4768
|
return new CorrelateCycle({
|
|
5250
4769
|
registry: this.registry,
|
|
@@ -5262,12 +4781,7 @@ var Act = class {
|
|
|
5262
4781
|
on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
|
|
5263
4782
|
});
|
|
5264
4783
|
}
|
|
5265
|
-
/**
|
|
5266
|
-
* At cold start, re-seed each lane controller's local wake from the
|
|
5267
|
-
* persisted future `deferred_at`, so an idle deferred stream (an
|
|
5268
|
-
* autoclose waiting out its window) still fires on time. Streams on lanes
|
|
5269
|
-
* this instance doesn't run are skipped.
|
|
5270
|
-
*/
|
|
4784
|
+
/** Cold start: an idle deferred stream still fires on time after a restart. */
|
|
5271
4785
|
async _seed_persisted_defers() {
|
|
5272
4786
|
const now = Date.now();
|
|
5273
4787
|
await walk_streams(store2(), (pos) => {
|
|
@@ -5276,7 +4790,6 @@ var Act = class {
|
|
|
5276
4790
|
controller?.seed_defer(pos.stream, pos.deferred_at);
|
|
5277
4791
|
});
|
|
5278
4792
|
}
|
|
5279
|
-
/** Settle loop driving correlate + drain to quiescence. */
|
|
5280
4793
|
_build_settle(options) {
|
|
5281
4794
|
return new SettleLoop(
|
|
5282
4795
|
{
|
|
@@ -5290,7 +4803,6 @@ var Act = class {
|
|
|
5290
4803
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
5291
4804
|
);
|
|
5292
4805
|
}
|
|
5293
|
-
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
5294
4806
|
_shutdown_promise;
|
|
5295
4807
|
/**
|
|
5296
4808
|
* Per-instance teardown: stop scheduling new work, give drain cycles
|
|
@@ -5330,14 +4842,8 @@ var Act = class {
|
|
|
5330
4842
|
return this._shutdown_promise;
|
|
5331
4843
|
}
|
|
5332
4844
|
/**
|
|
5333
|
-
*
|
|
5334
|
-
*
|
|
5335
|
-
* its own errors), so this never throws.
|
|
5336
|
-
*
|
|
5337
|
-
* An omitted budget is the largest `leaseMillis` among lanes with a cycle
|
|
5338
|
-
* in flight (the drain's fallback for lanes that pinned none, or pinned
|
|
5339
|
-
* 0, which is the same as none), with an in-flight settle counting on the
|
|
5340
|
-
* fallback, capped at the maximum. Idle lanes don't count.
|
|
4845
|
+
* The largest `leaseMillis` among lanes with a cycle in flight (the
|
|
4846
|
+
* default for none or 0, and for a running settle), capped.
|
|
5341
4847
|
*/
|
|
5342
4848
|
_derive_grace_ms(running, settling) {
|
|
5343
4849
|
let max = settling ? DEFAULT_LEASE_MILLIS : 0;
|
|
@@ -5366,12 +4872,7 @@ var Act = class {
|
|
|
5366
4872
|
clearTimeout(timer);
|
|
5367
4873
|
}
|
|
5368
4874
|
}
|
|
5369
|
-
/**
|
|
5370
|
-
* Subscribe to {@link Store.notify} when both the store and the
|
|
5371
|
-
* registry support it. Returns the disposer (or `undefined` when no
|
|
5372
|
-
* subscription was made). Errors during subscription are logged but
|
|
5373
|
-
* never thrown — `notify` is a hint, not a contract.
|
|
5374
|
-
*/
|
|
4875
|
+
/** Failures are logged, never thrown: `notify` is a hint. */
|
|
5375
4876
|
async _wire_notify(s) {
|
|
5376
4877
|
if (this._reactive_events.size === 0) return void 0;
|
|
5377
4878
|
if (!s.notify) return void 0;
|
|
@@ -5683,20 +5184,11 @@ var Act = class {
|
|
|
5683
5184
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
5684
5185
|
return this._scoped(() => this._drain_all(options));
|
|
5685
5186
|
}
|
|
5686
|
-
/** Arm every active lane controller. */
|
|
5687
5187
|
_arm_all() {
|
|
5688
5188
|
this._correlate.arm();
|
|
5689
5189
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
5690
5190
|
}
|
|
5691
|
-
/**
|
|
5692
|
-
* Arm only the lane controllers whose reactions match the supplied
|
|
5693
|
-
* event names (selective arming). Events with any dynamic
|
|
5694
|
-
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
5695
|
-
* resolver's lane isn't known until correlate runs the function.
|
|
5696
|
-
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
5697
|
-
* carry them. Returns true when any controller was armed (used by
|
|
5698
|
-
* the notify handler to decide whether to schedule a settle).
|
|
5699
|
-
*/
|
|
5191
|
+
/** Arms only the lanes these events react on; true when any was armed. */
|
|
5700
5192
|
_arm_for_event_names(names) {
|
|
5701
5193
|
const to_arm = /* @__PURE__ */ new Set();
|
|
5702
5194
|
for (const name of names) {
|
|
@@ -5713,14 +5205,7 @@ var Act = class {
|
|
|
5713
5205
|
for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
|
|
5714
5206
|
return true;
|
|
5715
5207
|
}
|
|
5716
|
-
/**
|
|
5717
|
-
*
|
|
5718
|
-
* Parallel — not sequential — so a slow lane's in-flight handler does
|
|
5719
|
-
* not block a fast lane's claim/dispatch/ack cycle. Each controller's
|
|
5720
|
-
* `claim()` is independent (filtered by lane); the store's
|
|
5721
|
-
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
5722
|
-
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
5723
|
-
* `lease.lane`. */
|
|
5208
|
+
/** In parallel, so a slow lane never holds up a fast one. */
|
|
5724
5209
|
async _drain_all(options) {
|
|
5725
5210
|
const results = await Promise.all(
|
|
5726
5211
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
@@ -5793,14 +5278,7 @@ var Act = class {
|
|
|
5793
5278
|
const { subscribed, last_id } = await this._correlate_scanned(query);
|
|
5794
5279
|
return { subscribed, last_id };
|
|
5795
5280
|
}
|
|
5796
|
-
/**
|
|
5797
|
-
* `correlate` plus whether the pass actually read the store.
|
|
5798
|
-
*
|
|
5799
|
-
* The settle loop needs that extra bit to decide whether the pass carries a
|
|
5800
|
-
* circuit-breaker health signal, and a disarmed pass carries none. It stays
|
|
5801
|
-
* internal rather than widening the public `correlate` return, which is
|
|
5802
|
-
* charter-covered and has no use for it.
|
|
5803
|
-
*/
|
|
5281
|
+
/** `correlate` plus `scanned`, which settle needs for the breaker. */
|
|
5804
5282
|
async _correlate_scanned(query, lease = false) {
|
|
5805
5283
|
if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
|
|
5806
5284
|
return this._scoped(async () => {
|
|
@@ -6141,11 +5619,7 @@ var Act = class {
|
|
|
6141
5619
|
async prioritize(filter, priority) {
|
|
6142
5620
|
return this._scoped(() => store2().prioritize(filter, priority));
|
|
6143
5621
|
}
|
|
6144
|
-
/**
|
|
6145
|
-
* Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
|
|
6146
|
-
* reaction-requested closes. The close actor lets a custom correlator
|
|
6147
|
-
* still tag tenant context or trace ids.
|
|
6148
|
-
*/
|
|
5622
|
+
/** Shared by `close()` and reaction-requested closes. */
|
|
6149
5623
|
async _run_close(targets) {
|
|
6150
5624
|
const close_actor = { id: "$close", name: "close" };
|
|
6151
5625
|
const result = await run_close_cycle(targets, {
|
|
@@ -6226,10 +5700,7 @@ var Act = class {
|
|
|
6226
5700
|
*/
|
|
6227
5701
|
async close(targets) {
|
|
6228
5702
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
6229
|
-
return this._scoped(
|
|
6230
|
-
await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
|
|
6231
|
-
return this._run_close(targets);
|
|
6232
|
-
});
|
|
5703
|
+
return this._scoped(() => this._run_close(targets));
|
|
6233
5704
|
}
|
|
6234
5705
|
/**
|
|
6235
5706
|
* Debounced, non-blocking correlate→drain cycle.
|