@rotorsoft/act 1.10.0 → 1.10.1
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 +12 -12
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/console-logger.d.ts +2 -2
- package/dist/@types/adapters/console-logger.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +6 -6
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +2 -2
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/audit.d.ts +4 -4
- package/dist/@types/internal/audit.d.ts.map +1 -1
- package/dist/@types/internal/backoff.d.ts +1 -1
- package/dist/@types/internal/backoff.d.ts.map +1 -1
- package/dist/@types/internal/build-classify.d.ts +11 -11
- package/dist/@types/internal/build-classify.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +3 -3
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +4 -4
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlator.d.ts +2 -2
- package/dist/@types/internal/correlator.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +20 -20
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-ratio.d.ts +1 -1
- package/dist/@types/internal/drain-ratio.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +2 -2
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/event-versions.d.ts +2 -2
- package/dist/@types/internal/event-versions.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +7 -7
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/merge.d.ts +3 -3
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts +6 -6
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/sensitive.d.ts +2 -2
- package/dist/@types/internal/settle.d.ts +3 -3
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts +5 -5
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/{chunk-3ZTFNAY7.js → chunk-3Z2HU726.js} +134 -133
- package/dist/chunk-3Z2HU726.js.map +1 -0
- package/dist/{chunk-XSBT63QX.js → chunk-BY5JPOZR.js} +1 -1
- package/dist/{chunk-XSBT63QX.js.map → chunk-BY5JPOZR.js.map} +1 -1
- package/dist/index.cjs +558 -552
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +428 -423
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +132 -131
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +2 -2
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-3ZTFNAY7.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -125,7 +125,7 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
125
125
|
this._pretty = pretty;
|
|
126
126
|
this.level = level;
|
|
127
127
|
const threshold = LEVEL_VALUES[level] ?? 30;
|
|
128
|
-
const write = pretty ? this.
|
|
128
|
+
const write = pretty ? this._pretty_write.bind(this, bindings) : this._json_write.bind(this, bindings);
|
|
129
129
|
this.fatal = write.bind(this, "fatal", 60);
|
|
130
130
|
this.error = threshold <= 50 ? write.bind(this, "error", 50) : noop;
|
|
131
131
|
this.warn = threshold <= 40 ? write.bind(this, "warn", 40) : noop;
|
|
@@ -144,24 +144,24 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
144
144
|
bindings
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
_json_write(bindings, level, _num, obj_or_msg, msg) {
|
|
148
148
|
let obj;
|
|
149
149
|
let message;
|
|
150
|
-
if (typeof
|
|
151
|
-
message =
|
|
150
|
+
if (typeof obj_or_msg === "string") {
|
|
151
|
+
message = obj_or_msg;
|
|
152
152
|
obj = {};
|
|
153
|
-
} else if (
|
|
154
|
-
message = msg ??
|
|
153
|
+
} else if (obj_or_msg instanceof Error) {
|
|
154
|
+
message = msg ?? obj_or_msg.message;
|
|
155
155
|
obj = {
|
|
156
|
-
error: { message:
|
|
157
|
-
stack:
|
|
156
|
+
error: { message: obj_or_msg.message, name: obj_or_msg.name },
|
|
157
|
+
stack: obj_or_msg.stack
|
|
158
158
|
};
|
|
159
|
-
} else if (
|
|
159
|
+
} else if (obj_or_msg !== null && typeof obj_or_msg === "object") {
|
|
160
160
|
message = msg;
|
|
161
|
-
obj = { ...
|
|
161
|
+
obj = { ...obj_or_msg };
|
|
162
162
|
} else {
|
|
163
163
|
message = msg;
|
|
164
|
-
obj = { value:
|
|
164
|
+
obj = { value: obj_or_msg };
|
|
165
165
|
}
|
|
166
166
|
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
167
167
|
if (message) entry.msg = message;
|
|
@@ -178,29 +178,29 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
178
178
|
}
|
|
179
179
|
process.stdout.write(line + "\n");
|
|
180
180
|
}
|
|
181
|
-
|
|
181
|
+
_pretty_write(bindings, level, _num, obj_or_msg, msg) {
|
|
182
182
|
const color = LEVEL_COLORS[level];
|
|
183
183
|
const tag = `${color}${level.toUpperCase().padEnd(5)}${RESET}`;
|
|
184
184
|
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
185
185
|
let message;
|
|
186
186
|
let data;
|
|
187
|
-
if (typeof
|
|
188
|
-
message =
|
|
189
|
-
} else if (
|
|
190
|
-
message = msg ??
|
|
191
|
-
data =
|
|
187
|
+
if (typeof obj_or_msg === "string") {
|
|
188
|
+
message = obj_or_msg;
|
|
189
|
+
} else if (obj_or_msg instanceof Error) {
|
|
190
|
+
message = msg ?? obj_or_msg.message;
|
|
191
|
+
data = obj_or_msg.stack;
|
|
192
192
|
} else {
|
|
193
193
|
message = msg ?? "";
|
|
194
|
-
if (
|
|
194
|
+
if (obj_or_msg !== void 0 && obj_or_msg !== null) {
|
|
195
195
|
try {
|
|
196
|
-
data = JSON.stringify(
|
|
196
|
+
data = JSON.stringify(obj_or_msg);
|
|
197
197
|
} catch {
|
|
198
198
|
data = "[unserializable]";
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
-
const
|
|
203
|
-
const parts = [ts, tag, message, data,
|
|
202
|
+
const bind_str = bindings && Object.keys(bindings).length ? ` ${JSON.stringify(bindings)}` : "";
|
|
203
|
+
const parts = [ts, tag, message, data, bind_str].filter(Boolean);
|
|
204
204
|
process.stdout.write(parts.join(" ") + "\n");
|
|
205
205
|
}
|
|
206
206
|
};
|
|
@@ -559,16 +559,16 @@ var FALLBACK_PACKAGE = {
|
|
|
559
559
|
version: "0.0.0-fallback",
|
|
560
560
|
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
561
561
|
};
|
|
562
|
-
var
|
|
562
|
+
var get_package = () => {
|
|
563
563
|
try {
|
|
564
564
|
const raw = fs.readFileSync("package.json");
|
|
565
565
|
return JSON.parse(raw.toString());
|
|
566
566
|
} catch (err) {
|
|
567
|
-
|
|
567
|
+
pkg_load_error = err;
|
|
568
568
|
return FALLBACK_PACKAGE;
|
|
569
569
|
}
|
|
570
570
|
};
|
|
571
|
-
var
|
|
571
|
+
var pkg_load_error;
|
|
572
572
|
var BaseSchema = PackageSchema.extend({
|
|
573
573
|
env: import_zod3.z.enum(Environments),
|
|
574
574
|
logLevel: import_zod3.z.enum(LogLevels),
|
|
@@ -580,7 +580,7 @@ var env = NODE_ENV || "development";
|
|
|
580
580
|
var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
|
|
581
581
|
var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
|
|
582
582
|
var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
|
|
583
|
-
var pkg =
|
|
583
|
+
var pkg = get_package();
|
|
584
584
|
var _validated;
|
|
585
585
|
var config = () => {
|
|
586
586
|
if (!_validated) {
|
|
@@ -588,12 +588,12 @@ var config = () => {
|
|
|
588
588
|
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
589
589
|
BaseSchema
|
|
590
590
|
);
|
|
591
|
-
if (
|
|
592
|
-
const msg =
|
|
591
|
+
if (pkg_load_error) {
|
|
592
|
+
const msg = pkg_load_error instanceof Error ? pkg_load_error.message : typeof pkg_load_error === "string" ? pkg_load_error : "unknown error";
|
|
593
593
|
log().warn(
|
|
594
594
|
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
595
595
|
);
|
|
596
|
-
|
|
596
|
+
pkg_load_error = void 0;
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
599
|
return _validated;
|
|
@@ -650,14 +650,14 @@ var InMemoryStream = class {
|
|
|
650
650
|
* Bump the priority via {@link subscribe}: keeps the maximum across
|
|
651
651
|
* reactions so the highest-priority registrant wins.
|
|
652
652
|
*/
|
|
653
|
-
|
|
653
|
+
bump_priority(priority) {
|
|
654
654
|
if (priority > this._priority) this._priority = priority;
|
|
655
655
|
}
|
|
656
656
|
/**
|
|
657
657
|
* Set the priority outright via {@link prioritize}: operator
|
|
658
658
|
* runtime override that ignores the build-time `max()` invariant.
|
|
659
659
|
*/
|
|
660
|
-
|
|
660
|
+
set_priority(priority) {
|
|
661
661
|
this._priority = priority;
|
|
662
662
|
}
|
|
663
663
|
get is_available() {
|
|
@@ -779,28 +779,28 @@ var InMemoryStore = class {
|
|
|
779
779
|
// stored stream positions and other metadata
|
|
780
780
|
_streams = /* @__PURE__ */ new Map();
|
|
781
781
|
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
782
|
-
|
|
782
|
+
_stream_versions = /* @__PURE__ */ new Map();
|
|
783
783
|
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
784
784
|
// without scanning the full event log.
|
|
785
|
-
|
|
785
|
+
_max_event_id_by_stream = /* @__PURE__ */ new Map();
|
|
786
786
|
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
787
|
-
|
|
787
|
+
_max_non_snap_event_id = -1;
|
|
788
788
|
// stream → (event_id → cloned sensitive payload). Two-level so `forget_pii`
|
|
789
789
|
// is O(1) — drop the inner Map for the stream and the wipe is done — mirroring
|
|
790
790
|
// the `DELETE WHERE stream = ?` scope that durable adapters get from their
|
|
791
791
|
// stream index. Entries exist only for events committed with a non-null
|
|
792
792
|
// `pii` field; absence means "no PII" (returned as `null` on load).
|
|
793
793
|
_pii = /* @__PURE__ */ new Map();
|
|
794
|
-
|
|
794
|
+
_reset_indexes() {
|
|
795
795
|
this._events.length = 0;
|
|
796
|
-
this.
|
|
797
|
-
this.
|
|
798
|
-
this.
|
|
796
|
+
this._stream_versions.clear();
|
|
797
|
+
this._max_event_id_by_stream.clear();
|
|
798
|
+
this._max_non_snap_event_id = -1;
|
|
799
799
|
this._pii.clear();
|
|
800
800
|
}
|
|
801
801
|
// Attach the isolated PII payload (or null) to an event before handing it to
|
|
802
802
|
// a caller. Allocation-free for events without PII — by far the common case.
|
|
803
|
-
|
|
803
|
+
_with_pii(e) {
|
|
804
804
|
const pii = this._pii.get(e.stream)?.get(e.id);
|
|
805
805
|
return pii ? { ...e, pii } : e;
|
|
806
806
|
}
|
|
@@ -810,7 +810,7 @@ var InMemoryStore = class {
|
|
|
810
810
|
*/
|
|
811
811
|
async dispose() {
|
|
812
812
|
await sleep();
|
|
813
|
-
this.
|
|
813
|
+
this._reset_indexes();
|
|
814
814
|
}
|
|
815
815
|
/**
|
|
816
816
|
* Seed the store with initial data (no-op for in-memory).
|
|
@@ -825,7 +825,7 @@ var InMemoryStore = class {
|
|
|
825
825
|
*/
|
|
826
826
|
async drop() {
|
|
827
827
|
await sleep();
|
|
828
|
-
this.
|
|
828
|
+
this._reset_indexes();
|
|
829
829
|
this._streams = /* @__PURE__ */ new Map();
|
|
830
830
|
}
|
|
831
831
|
in_query(query, e) {
|
|
@@ -859,7 +859,7 @@ var InMemoryStore = class {
|
|
|
859
859
|
if (query.after && e.id <= query.after) break;
|
|
860
860
|
if (query.created_after && e.created <= query.created_after) break;
|
|
861
861
|
await Promise.resolve(
|
|
862
|
-
callback(this.
|
|
862
|
+
callback(this._with_pii(e))
|
|
863
863
|
);
|
|
864
864
|
count++;
|
|
865
865
|
if (query?.limit && count >= query.limit) break;
|
|
@@ -873,7 +873,7 @@ var InMemoryStore = class {
|
|
|
873
873
|
if (query?.before && e.id >= query.before) break;
|
|
874
874
|
if (query?.created_before && e.created >= query.created_before) break;
|
|
875
875
|
await Promise.resolve(
|
|
876
|
-
callback(this.
|
|
876
|
+
callback(this._with_pii(e))
|
|
877
877
|
);
|
|
878
878
|
count++;
|
|
879
879
|
if (query?.limit && count >= query.limit) break;
|
|
@@ -892,17 +892,17 @@ var InMemoryStore = class {
|
|
|
892
892
|
*/
|
|
893
893
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
894
894
|
await sleep();
|
|
895
|
-
const
|
|
896
|
-
if (typeof expectedVersion === "number" &&
|
|
895
|
+
const current_version = this._stream_versions.get(stream) ?? -1;
|
|
896
|
+
if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
|
|
897
897
|
throw new ConcurrencyError(
|
|
898
898
|
stream,
|
|
899
|
-
|
|
899
|
+
current_version,
|
|
900
900
|
msgs,
|
|
901
901
|
expectedVersion
|
|
902
902
|
);
|
|
903
903
|
}
|
|
904
|
-
let version =
|
|
905
|
-
let
|
|
904
|
+
let version = current_version + 1;
|
|
905
|
+
let last_non_snap_id = -1;
|
|
906
906
|
const committed = msgs.map(({ name, data, pii }) => {
|
|
907
907
|
const c = {
|
|
908
908
|
id: this._events.length,
|
|
@@ -915,21 +915,21 @@ var InMemoryStore = class {
|
|
|
915
915
|
};
|
|
916
916
|
this._events.push(c);
|
|
917
917
|
if (pii != null) {
|
|
918
|
-
let
|
|
919
|
-
if (!
|
|
920
|
-
|
|
921
|
-
this._pii.set(stream,
|
|
918
|
+
let per_stream = this._pii.get(stream);
|
|
919
|
+
if (!per_stream) {
|
|
920
|
+
per_stream = /* @__PURE__ */ new Map();
|
|
921
|
+
this._pii.set(stream, per_stream);
|
|
922
922
|
}
|
|
923
|
-
|
|
923
|
+
per_stream.set(c.id, structuredClone(pii));
|
|
924
924
|
}
|
|
925
|
-
if (name !== SNAP_EVENT)
|
|
925
|
+
if (name !== SNAP_EVENT) last_non_snap_id = c.id;
|
|
926
926
|
version++;
|
|
927
|
-
return this.
|
|
927
|
+
return this._with_pii(c);
|
|
928
928
|
});
|
|
929
|
-
this.
|
|
930
|
-
if (
|
|
931
|
-
this.
|
|
932
|
-
this.
|
|
929
|
+
this._stream_versions.set(stream, version - 1);
|
|
930
|
+
if (last_non_snap_id >= 0) {
|
|
931
|
+
this._max_event_id_by_stream.set(stream, last_non_snap_id);
|
|
932
|
+
this._max_non_snap_event_id = last_non_snap_id;
|
|
933
933
|
}
|
|
934
934
|
return committed;
|
|
935
935
|
}
|
|
@@ -944,26 +944,26 @@ var InMemoryStore = class {
|
|
|
944
944
|
*/
|
|
945
945
|
async claim(lagging, leading, by, millis, lane) {
|
|
946
946
|
await sleep();
|
|
947
|
-
const
|
|
948
|
-
const
|
|
949
|
-
let re =
|
|
947
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
948
|
+
const get_regex = (source) => {
|
|
949
|
+
let re = source_regex.get(source);
|
|
950
950
|
if (!re) {
|
|
951
951
|
re = new RegExp(source);
|
|
952
|
-
|
|
952
|
+
source_regex.set(source, re);
|
|
953
953
|
}
|
|
954
954
|
return re;
|
|
955
955
|
};
|
|
956
|
-
const
|
|
956
|
+
const has_work = (s) => {
|
|
957
957
|
if (s.at < 0) return true;
|
|
958
|
-
if (!s.source) return s.at < this.
|
|
959
|
-
const re =
|
|
960
|
-
for (const [
|
|
961
|
-
if (maxId > s.at && re.test(
|
|
958
|
+
if (!s.source) return s.at < this._max_non_snap_event_id;
|
|
959
|
+
const re = get_regex(s.source);
|
|
960
|
+
for (const [stream_name, maxId] of this._max_event_id_by_stream) {
|
|
961
|
+
if (maxId > s.at && re.test(stream_name)) return true;
|
|
962
962
|
}
|
|
963
963
|
return false;
|
|
964
964
|
};
|
|
965
965
|
const available = [...this._streams.values()].filter(
|
|
966
|
-
(s) => s.is_available &&
|
|
966
|
+
(s) => s.is_available && has_work(s) && (lane === void 0 || s.lane === lane)
|
|
967
967
|
);
|
|
968
968
|
const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
|
|
969
969
|
stream: s.stream,
|
|
@@ -1007,7 +1007,7 @@ var InMemoryStore = class {
|
|
|
1007
1007
|
} of streams) {
|
|
1008
1008
|
const existing = this._streams.get(stream);
|
|
1009
1009
|
if (existing) {
|
|
1010
|
-
existing.
|
|
1010
|
+
existing.bump_priority(priority);
|
|
1011
1011
|
existing.lane = lane;
|
|
1012
1012
|
} else {
|
|
1013
1013
|
this._streams.set(
|
|
@@ -1045,17 +1045,17 @@ var InMemoryStore = class {
|
|
|
1045
1045
|
* cached in the closure so callers can apply it across the streams
|
|
1046
1046
|
* map without re-compiling per iteration.
|
|
1047
1047
|
*/
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1048
|
+
_filter_predicate(filter) {
|
|
1049
|
+
const stream_re = filter.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
1050
|
+
const source_re = filter.source && !filter.source_exact ? new RegExp(filter.source) : void 0;
|
|
1051
1051
|
return (s) => {
|
|
1052
1052
|
if (filter.stream !== void 0) {
|
|
1053
|
-
if (filter.stream_exact ? s.stream !== filter.stream : !
|
|
1053
|
+
if (filter.stream_exact ? s.stream !== filter.stream : !stream_re.test(s.stream))
|
|
1054
1054
|
return false;
|
|
1055
1055
|
}
|
|
1056
1056
|
if (filter.source !== void 0) {
|
|
1057
1057
|
if (s.source === void 0) return false;
|
|
1058
|
-
if (filter.source_exact ? s.source !== filter.source : !
|
|
1058
|
+
if (filter.source_exact ? s.source !== filter.source : !source_re.test(s.source))
|
|
1059
1059
|
return false;
|
|
1060
1060
|
}
|
|
1061
1061
|
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
@@ -1084,7 +1084,7 @@ var InMemoryStore = class {
|
|
|
1084
1084
|
}
|
|
1085
1085
|
}
|
|
1086
1086
|
} else {
|
|
1087
|
-
const matches = this.
|
|
1087
|
+
const matches = this._filter_predicate(input);
|
|
1088
1088
|
for (const s of this._streams.values()) {
|
|
1089
1089
|
if (matches(s)) {
|
|
1090
1090
|
s.reset();
|
|
@@ -1129,7 +1129,7 @@ var InMemoryStore = class {
|
|
|
1129
1129
|
if (s?.unblock()) count++;
|
|
1130
1130
|
}
|
|
1131
1131
|
} else {
|
|
1132
|
-
const matches = this.
|
|
1132
|
+
const matches = this._filter_predicate({ ...input, blocked: true });
|
|
1133
1133
|
for (const s of this._streams.values()) {
|
|
1134
1134
|
if (matches(s) && s.unblock()) count++;
|
|
1135
1135
|
}
|
|
@@ -1147,12 +1147,12 @@ var InMemoryStore = class {
|
|
|
1147
1147
|
*/
|
|
1148
1148
|
async prioritize(filter, priority) {
|
|
1149
1149
|
await sleep();
|
|
1150
|
-
const matches = this.
|
|
1150
|
+
const matches = this._filter_predicate(filter);
|
|
1151
1151
|
let count = 0;
|
|
1152
1152
|
for (const s of this._streams.values()) {
|
|
1153
1153
|
if (!matches(s)) continue;
|
|
1154
1154
|
if (s.priority !== priority) {
|
|
1155
|
-
s.
|
|
1155
|
+
s.set_priority(priority);
|
|
1156
1156
|
count++;
|
|
1157
1157
|
}
|
|
1158
1158
|
}
|
|
@@ -1168,8 +1168,8 @@ var InMemoryStore = class {
|
|
|
1168
1168
|
const limit = query?.limit ?? 100;
|
|
1169
1169
|
const after = query?.after;
|
|
1170
1170
|
const blocked = query?.blocked;
|
|
1171
|
-
const
|
|
1172
|
-
const
|
|
1171
|
+
const stream_re = query?.stream && !query.stream_exact ? new RegExp(query.stream) : void 0;
|
|
1172
|
+
const source_re = query?.source && !query.source_exact ? new RegExp(query.source) : void 0;
|
|
1173
1173
|
const sorted = [...this._streams.values()].sort(
|
|
1174
1174
|
(a, b) => a.stream.localeCompare(b.stream)
|
|
1175
1175
|
);
|
|
@@ -1177,12 +1177,12 @@ var InMemoryStore = class {
|
|
|
1177
1177
|
for (const s of sorted) {
|
|
1178
1178
|
if (after !== void 0 && s.stream <= after) continue;
|
|
1179
1179
|
if (query?.stream !== void 0) {
|
|
1180
|
-
if (query.stream_exact ? s.stream !== query.stream : !
|
|
1180
|
+
if (query.stream_exact ? s.stream !== query.stream : !stream_re.test(s.stream))
|
|
1181
1181
|
continue;
|
|
1182
1182
|
}
|
|
1183
1183
|
if (query?.source !== void 0) {
|
|
1184
1184
|
if (s.source === void 0) continue;
|
|
1185
|
-
if (query.source_exact ? s.source !== query.source : !
|
|
1185
|
+
if (query.source_exact ? s.source !== query.source : !source_re.test(s.source))
|
|
1186
1186
|
continue;
|
|
1187
1187
|
}
|
|
1188
1188
|
if (blocked !== void 0 && s.blocked !== blocked) continue;
|
|
@@ -1223,44 +1223,44 @@ var InMemoryStore = class {
|
|
|
1223
1223
|
async query_stats(input, options) {
|
|
1224
1224
|
await sleep();
|
|
1225
1225
|
const exclude = new Set(options?.exclude ?? []);
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1226
|
+
const want_tail = options?.tail ?? false;
|
|
1227
|
+
const want_count = options?.count ?? false;
|
|
1228
|
+
const want_names = options?.names ?? false;
|
|
1229
1229
|
const before = options?.before;
|
|
1230
|
-
const
|
|
1230
|
+
const array_targets = Array.isArray(input) ? new Set(input) : null;
|
|
1231
1231
|
const filter = Array.isArray(input) ? null : input;
|
|
1232
|
-
const
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1235
|
-
const cached =
|
|
1232
|
+
const stream_re = filter?.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
1233
|
+
const scope_cache = /* @__PURE__ */ new Map();
|
|
1234
|
+
const in_scope = (stream) => {
|
|
1235
|
+
const cached = scope_cache.get(stream);
|
|
1236
1236
|
if (cached !== void 0) return cached;
|
|
1237
1237
|
let ok = true;
|
|
1238
|
-
if (
|
|
1239
|
-
ok =
|
|
1238
|
+
if (array_targets) {
|
|
1239
|
+
ok = array_targets.has(stream);
|
|
1240
1240
|
} else if (filter?.stream !== void 0) {
|
|
1241
1241
|
ok = filter.stream_exact ? stream === filter.stream : (
|
|
1242
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
1243
|
-
|
|
1242
|
+
// biome-ignore lint/style/noNonNullAssertion: stream_re set when stream is regex
|
|
1243
|
+
stream_re.test(stream)
|
|
1244
1244
|
);
|
|
1245
1245
|
}
|
|
1246
|
-
|
|
1246
|
+
scope_cache.set(stream, ok);
|
|
1247
1247
|
return ok;
|
|
1248
1248
|
};
|
|
1249
1249
|
const acc = /* @__PURE__ */ new Map();
|
|
1250
1250
|
for (const e of this._events) {
|
|
1251
1251
|
if (before !== void 0 && e.id >= before) continue;
|
|
1252
|
-
if (!
|
|
1252
|
+
if (!in_scope(e.stream)) continue;
|
|
1253
1253
|
if (exclude.has(e.name)) continue;
|
|
1254
1254
|
let a = acc.get(e.stream);
|
|
1255
1255
|
if (!a) {
|
|
1256
1256
|
a = { head: e, count: 0 };
|
|
1257
|
-
if (
|
|
1258
|
-
if (
|
|
1257
|
+
if (want_tail) a.tail = e;
|
|
1258
|
+
if (want_names) a.names = {};
|
|
1259
1259
|
acc.set(e.stream, a);
|
|
1260
1260
|
}
|
|
1261
1261
|
a.head = e;
|
|
1262
1262
|
a.count++;
|
|
1263
|
-
if (
|
|
1263
|
+
if (want_names) {
|
|
1264
1264
|
const n = String(e.name);
|
|
1265
1265
|
a.names[n] = (a.names[n] ?? 0) + 1;
|
|
1266
1266
|
}
|
|
@@ -1268,9 +1268,9 @@ var InMemoryStore = class {
|
|
|
1268
1268
|
const out = /* @__PURE__ */ new Map();
|
|
1269
1269
|
for (const [stream, a] of acc) {
|
|
1270
1270
|
const stats = { head: a.head };
|
|
1271
|
-
if (
|
|
1272
|
-
if (
|
|
1273
|
-
if (
|
|
1271
|
+
if (want_tail) stats.tail = a.tail;
|
|
1272
|
+
if (want_count) stats.count = a.count;
|
|
1273
|
+
if (want_names) stats.names = a.names;
|
|
1274
1274
|
out.set(stream, stats);
|
|
1275
1275
|
}
|
|
1276
1276
|
return out;
|
|
@@ -1282,18 +1282,18 @@ var InMemoryStore = class {
|
|
|
1282
1282
|
*/
|
|
1283
1283
|
async truncate(targets) {
|
|
1284
1284
|
await sleep();
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1285
|
+
const deleted_counts = /* @__PURE__ */ new Map();
|
|
1286
|
+
const stream_set = new Set(targets.map((t) => t.stream));
|
|
1287
1287
|
for (const e of this._events) {
|
|
1288
|
-
if (
|
|
1289
|
-
|
|
1288
|
+
if (stream_set.has(e.stream)) {
|
|
1289
|
+
deleted_counts.set(e.stream, (deleted_counts.get(e.stream) ?? 0) + 1);
|
|
1290
1290
|
}
|
|
1291
1291
|
}
|
|
1292
|
-
this._events = this._events.filter((e) => !
|
|
1293
|
-
for (const stream of
|
|
1292
|
+
this._events = this._events.filter((e) => !stream_set.has(e.stream));
|
|
1293
|
+
for (const stream of stream_set) {
|
|
1294
1294
|
this._streams.delete(stream);
|
|
1295
|
-
this.
|
|
1296
|
-
this.
|
|
1295
|
+
this._stream_versions.delete(stream);
|
|
1296
|
+
this._max_event_id_by_stream.delete(stream);
|
|
1297
1297
|
}
|
|
1298
1298
|
const result = /* @__PURE__ */ new Map();
|
|
1299
1299
|
for (const { stream, snapshot, meta } of targets) {
|
|
@@ -1307,18 +1307,19 @@ var InMemoryStore = class {
|
|
|
1307
1307
|
meta: meta ?? { correlation: "", causation: {} }
|
|
1308
1308
|
};
|
|
1309
1309
|
this._events.push(event);
|
|
1310
|
-
this.
|
|
1310
|
+
this._stream_versions.set(stream, 0);
|
|
1311
1311
|
if (event.name !== SNAP_EVENT) {
|
|
1312
|
-
this.
|
|
1312
|
+
this._max_event_id_by_stream.set(stream, event.id);
|
|
1313
1313
|
}
|
|
1314
1314
|
result.set(stream, {
|
|
1315
|
-
deleted:
|
|
1315
|
+
deleted: deleted_counts.get(stream) ?? 0,
|
|
1316
1316
|
committed: event
|
|
1317
1317
|
});
|
|
1318
1318
|
}
|
|
1319
1319
|
let max = -1;
|
|
1320
|
-
for (const id of this.
|
|
1321
|
-
|
|
1320
|
+
for (const id of this._max_event_id_by_stream.values())
|
|
1321
|
+
if (id > max) max = id;
|
|
1322
|
+
this._max_non_snap_event_id = max;
|
|
1322
1323
|
return result;
|
|
1323
1324
|
}
|
|
1324
1325
|
/**
|
|
@@ -1335,34 +1336,34 @@ var InMemoryStore = class {
|
|
|
1335
1336
|
*/
|
|
1336
1337
|
async restore(driver) {
|
|
1337
1338
|
await sleep();
|
|
1338
|
-
const
|
|
1339
|
-
const
|
|
1340
|
-
const
|
|
1341
|
-
const
|
|
1342
|
-
const
|
|
1339
|
+
const prev_events = this._events;
|
|
1340
|
+
const prev_streams = this._streams;
|
|
1341
|
+
const prev_stream_versions = this._stream_versions;
|
|
1342
|
+
const prev_max_event_id_by_stream = this._max_event_id_by_stream;
|
|
1343
|
+
const prev_max_non_snap_event_id = this._max_non_snap_event_id;
|
|
1343
1344
|
this._events = [];
|
|
1344
1345
|
this._streams = /* @__PURE__ */ new Map();
|
|
1345
|
-
this.
|
|
1346
|
-
this.
|
|
1347
|
-
this.
|
|
1346
|
+
this._stream_versions = /* @__PURE__ */ new Map();
|
|
1347
|
+
this._max_event_id_by_stream = /* @__PURE__ */ new Map();
|
|
1348
|
+
this._max_non_snap_event_id = -1;
|
|
1348
1349
|
try {
|
|
1349
1350
|
await driver(async (event) => {
|
|
1350
1351
|
const id = this._events.length;
|
|
1351
1352
|
const committed = { ...event, id };
|
|
1352
1353
|
this._events.push(committed);
|
|
1353
|
-
this.
|
|
1354
|
+
this._stream_versions.set(event.stream, event.version);
|
|
1354
1355
|
if (event.name !== SNAP_EVENT) {
|
|
1355
|
-
this.
|
|
1356
|
-
this.
|
|
1356
|
+
this._max_event_id_by_stream.set(event.stream, id);
|
|
1357
|
+
this._max_non_snap_event_id = id;
|
|
1357
1358
|
}
|
|
1358
1359
|
return id;
|
|
1359
1360
|
});
|
|
1360
1361
|
} catch (err) {
|
|
1361
|
-
this._events =
|
|
1362
|
-
this._streams =
|
|
1363
|
-
this.
|
|
1364
|
-
this.
|
|
1365
|
-
this.
|
|
1362
|
+
this._events = prev_events;
|
|
1363
|
+
this._streams = prev_streams;
|
|
1364
|
+
this._stream_versions = prev_stream_versions;
|
|
1365
|
+
this._max_event_id_by_stream = prev_max_event_id_by_stream;
|
|
1366
|
+
this._max_non_snap_event_id = prev_max_non_snap_event_id;
|
|
1366
1367
|
throw err;
|
|
1367
1368
|
}
|
|
1368
1369
|
}
|
|
@@ -1458,7 +1459,7 @@ function parse(name) {
|
|
|
1458
1459
|
}
|
|
1459
1460
|
return { base: name, version: 1 };
|
|
1460
1461
|
}
|
|
1461
|
-
function
|
|
1462
|
+
function deprecated_event_names(names) {
|
|
1462
1463
|
const groups = /* @__PURE__ */ new Map();
|
|
1463
1464
|
for (const name of names) {
|
|
1464
1465
|
const { base, version } = parse(name);
|
|
@@ -1474,10 +1475,10 @@ function deprecatedEventNames(names) {
|
|
|
1474
1475
|
}
|
|
1475
1476
|
return deprecated;
|
|
1476
1477
|
}
|
|
1477
|
-
function
|
|
1478
|
-
const target = parse(
|
|
1478
|
+
function current_version_of(deprecated_name, all_names) {
|
|
1479
|
+
const target = parse(deprecated_name);
|
|
1479
1480
|
let highest;
|
|
1480
|
-
for (const name of
|
|
1481
|
+
for (const name of all_names) {
|
|
1481
1482
|
const { base, version } = parse(name);
|
|
1482
1483
|
if (base !== target.base) continue;
|
|
1483
1484
|
if (!highest || version > highest.version) highest = { version, name };
|
|
@@ -1507,27 +1508,27 @@ var ALL_CATEGORIES = [
|
|
|
1507
1508
|
];
|
|
1508
1509
|
async function* audit(deps, categories, options = {}) {
|
|
1509
1510
|
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
1510
|
-
const
|
|
1511
|
-
const passes =
|
|
1511
|
+
const ordered_categories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
1512
|
+
const passes = ordered_categories.map(
|
|
1512
1513
|
(c) => PASS_FACTORIES[c](deps, options)
|
|
1513
1514
|
);
|
|
1514
|
-
const
|
|
1515
|
-
const
|
|
1516
|
-
const
|
|
1517
|
-
if (
|
|
1515
|
+
const need_stats = passes.some((p) => p.on_stat !== void 0);
|
|
1516
|
+
const need_streams = passes.some((p) => p.on_stream !== void 0);
|
|
1517
|
+
const need_events = passes.some((p) => p.on_event !== void 0);
|
|
1518
|
+
if (need_stats) {
|
|
1518
1519
|
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
1519
1520
|
for (const [stream, s] of stats) {
|
|
1520
|
-
for (const p of passes) p.
|
|
1521
|
+
for (const p of passes) p.on_stat?.(stream, s);
|
|
1521
1522
|
}
|
|
1522
1523
|
}
|
|
1523
|
-
if (
|
|
1524
|
+
if (need_streams) {
|
|
1524
1525
|
await deps.store().query_streams((pos) => {
|
|
1525
|
-
for (const p of passes) p.
|
|
1526
|
+
for (const p of passes) p.on_stream?.(pos);
|
|
1526
1527
|
});
|
|
1527
1528
|
}
|
|
1528
|
-
if (
|
|
1529
|
+
if (need_events) {
|
|
1529
1530
|
await deps.store().query((event) => {
|
|
1530
|
-
for (const p of passes) p.
|
|
1531
|
+
for (const p of passes) p.on_event?.(event);
|
|
1531
1532
|
}, options.query);
|
|
1532
1533
|
}
|
|
1533
1534
|
for (const p of passes) await p.finalize?.(deps);
|
|
@@ -1535,11 +1536,11 @@ async function* audit(deps, categories, options = {}) {
|
|
|
1535
1536
|
for (const f of p.drain()) yield f;
|
|
1536
1537
|
}
|
|
1537
1538
|
}
|
|
1538
|
-
var
|
|
1539
|
+
var make_schema_pass = (deps) => {
|
|
1539
1540
|
const findings = [];
|
|
1540
1541
|
return {
|
|
1541
1542
|
category: "schema",
|
|
1542
|
-
|
|
1543
|
+
on_event(event) {
|
|
1543
1544
|
const name = String(event.name);
|
|
1544
1545
|
const state2 = deps.event_to_state.get(name);
|
|
1545
1546
|
if (!state2) {
|
|
@@ -1569,19 +1570,19 @@ var makeSchemaPass = (deps) => {
|
|
|
1569
1570
|
drain: () => findings
|
|
1570
1571
|
};
|
|
1571
1572
|
};
|
|
1572
|
-
var
|
|
1573
|
+
var make_deprecated_load_pass = (deps, options) => {
|
|
1573
1574
|
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
1574
1575
|
const totals = /* @__PURE__ */ new Map();
|
|
1575
|
-
const
|
|
1576
|
+
const per_stream = /* @__PURE__ */ new Map();
|
|
1576
1577
|
return {
|
|
1577
1578
|
category: "deprecated-load",
|
|
1578
|
-
|
|
1579
|
+
on_stat(stream, { names }) {
|
|
1579
1580
|
for (const [name, count] of Object.entries(names)) {
|
|
1580
1581
|
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
1581
|
-
let m =
|
|
1582
|
+
let m = per_stream.get(name);
|
|
1582
1583
|
if (!m) {
|
|
1583
1584
|
m = /* @__PURE__ */ new Map();
|
|
1584
|
-
|
|
1585
|
+
per_stream.set(name, m);
|
|
1585
1586
|
}
|
|
1586
1587
|
m.set(stream, count);
|
|
1587
1588
|
}
|
|
@@ -1590,33 +1591,33 @@ var makeDeprecatedLoadPass = (deps, options) => {
|
|
|
1590
1591
|
const findings = [];
|
|
1591
1592
|
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
1592
1593
|
if (grand === 0) return findings;
|
|
1593
|
-
const deprecated =
|
|
1594
|
+
const deprecated = deprecated_event_names(deps.known_events);
|
|
1594
1595
|
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
1595
1596
|
for (const { name, count } of sorted) {
|
|
1596
1597
|
if (count === 0) continue;
|
|
1597
1598
|
if (count / grand < share_min) continue;
|
|
1598
|
-
const
|
|
1599
|
-
const
|
|
1599
|
+
const current_version = current_version_of(name, deps.known_events);
|
|
1600
|
+
const top_streams = [...per_stream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1600
1601
|
findings.push({
|
|
1601
1602
|
category: "deprecated-load",
|
|
1602
1603
|
name,
|
|
1603
|
-
current_version
|
|
1604
|
+
current_version,
|
|
1604
1605
|
total: count,
|
|
1605
|
-
top_streams
|
|
1606
|
+
top_streams
|
|
1606
1607
|
});
|
|
1607
1608
|
}
|
|
1608
1609
|
return findings;
|
|
1609
1610
|
}
|
|
1610
1611
|
};
|
|
1611
1612
|
};
|
|
1612
|
-
var
|
|
1613
|
+
var make_close_candidate_pass = (deps, options) => {
|
|
1613
1614
|
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
1614
1615
|
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
1615
1616
|
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
1616
1617
|
const findings = [];
|
|
1617
1618
|
return {
|
|
1618
1619
|
category: "close-candidate",
|
|
1619
|
-
|
|
1620
|
+
on_stat(stream, { head }) {
|
|
1620
1621
|
const head_name = String(head.name);
|
|
1621
1622
|
if (head_name.startsWith("__")) return;
|
|
1622
1623
|
const head_time = head.created.getTime();
|
|
@@ -1629,22 +1630,22 @@ var makeCloseCandidatePass = (deps, options) => {
|
|
|
1629
1630
|
last_event_at: head.created.toISOString(),
|
|
1630
1631
|
reason: is_terminal ? "terminal" : "idle",
|
|
1631
1632
|
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
1632
|
-
restart_supported:
|
|
1633
|
+
restart_supported: restart_is_supported(deps, head_name)
|
|
1633
1634
|
});
|
|
1634
1635
|
},
|
|
1635
1636
|
drain: () => findings
|
|
1636
1637
|
};
|
|
1637
1638
|
};
|
|
1638
|
-
var
|
|
1639
|
+
var make_restart_candidate_pass = (deps, options) => {
|
|
1639
1640
|
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
1640
1641
|
const findings = [];
|
|
1641
1642
|
return {
|
|
1642
1643
|
category: "restart-candidate",
|
|
1643
|
-
|
|
1644
|
+
on_stat(stream, { head, count, names }) {
|
|
1644
1645
|
if (count < threshold) return;
|
|
1645
1646
|
const head_name = String(head.name);
|
|
1646
1647
|
if (head_name.startsWith("__")) return;
|
|
1647
|
-
if (!
|
|
1648
|
+
if (!restart_is_supported(deps, head_name)) return;
|
|
1648
1649
|
findings.push({
|
|
1649
1650
|
category: "restart-candidate",
|
|
1650
1651
|
stream,
|
|
@@ -1658,14 +1659,14 @@ var makeRestartCandidatePass = (deps, options) => {
|
|
|
1658
1659
|
drain: () => findings
|
|
1659
1660
|
};
|
|
1660
1661
|
};
|
|
1661
|
-
var
|
|
1662
|
+
var make_reaction_health_pass = (_deps, options) => {
|
|
1662
1663
|
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
1663
1664
|
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
1664
1665
|
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
1665
1666
|
const findings = [];
|
|
1666
1667
|
return {
|
|
1667
1668
|
category: "reaction-health",
|
|
1668
|
-
|
|
1669
|
+
on_stream(p) {
|
|
1669
1670
|
if (p.blocked) {
|
|
1670
1671
|
findings.push({
|
|
1671
1672
|
category: "reaction-health",
|
|
@@ -1702,14 +1703,14 @@ var makeReactionHealthPass = (_deps, options) => {
|
|
|
1702
1703
|
drain: () => findings
|
|
1703
1704
|
};
|
|
1704
1705
|
};
|
|
1705
|
-
var
|
|
1706
|
+
var make_snapshot_drift_pass = (deps, options) => {
|
|
1706
1707
|
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
1707
1708
|
const candidates = [];
|
|
1708
1709
|
const findings = [];
|
|
1709
1710
|
return {
|
|
1710
1711
|
category: "snapshot-drift",
|
|
1711
|
-
|
|
1712
|
-
if (!
|
|
1712
|
+
on_stat(stream, { head, count, names }) {
|
|
1713
|
+
if (!restart_is_supported(deps, String(head.name))) return;
|
|
1713
1714
|
if (count < drift_min) return;
|
|
1714
1715
|
candidates.push({
|
|
1715
1716
|
stream,
|
|
@@ -1758,12 +1759,12 @@ var makeSnapshotDriftPass = (deps, options) => {
|
|
|
1758
1759
|
drain: () => findings
|
|
1759
1760
|
};
|
|
1760
1761
|
};
|
|
1761
|
-
var
|
|
1762
|
+
var make_routing_health_pass = (deps) => {
|
|
1762
1763
|
const findings = [];
|
|
1763
|
-
const
|
|
1764
|
+
const seen_event_names = /* @__PURE__ */ new Set();
|
|
1764
1765
|
return {
|
|
1765
1766
|
category: "routing-health",
|
|
1766
|
-
|
|
1767
|
+
on_stream(p) {
|
|
1767
1768
|
if (!p.lane) return;
|
|
1768
1769
|
if (deps.declared_lanes.has(p.lane)) return;
|
|
1769
1770
|
findings.push({
|
|
@@ -1773,13 +1774,13 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
1773
1774
|
lane: p.lane
|
|
1774
1775
|
});
|
|
1775
1776
|
},
|
|
1776
|
-
|
|
1777
|
+
on_stat(_stream, { names }) {
|
|
1777
1778
|
for (const name of Object.keys(names)) {
|
|
1778
|
-
|
|
1779
|
+
seen_event_names.add(name);
|
|
1779
1780
|
}
|
|
1780
1781
|
},
|
|
1781
1782
|
finalize() {
|
|
1782
|
-
for (const name of
|
|
1783
|
+
for (const name of seen_event_names) {
|
|
1783
1784
|
if (name.startsWith("__")) continue;
|
|
1784
1785
|
if (deps.routed_events.has(name)) continue;
|
|
1785
1786
|
findings.push({
|
|
@@ -1793,23 +1794,23 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
1793
1794
|
drain: () => findings
|
|
1794
1795
|
};
|
|
1795
1796
|
};
|
|
1796
|
-
var
|
|
1797
|
-
const
|
|
1797
|
+
var make_correlation_gaps_pass = () => {
|
|
1798
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
1798
1799
|
const checks = [];
|
|
1799
1800
|
return {
|
|
1800
1801
|
category: "correlation-gaps",
|
|
1801
|
-
|
|
1802
|
-
|
|
1802
|
+
on_event(e) {
|
|
1803
|
+
seen_ids.add(e.id);
|
|
1803
1804
|
const causation = e.meta?.causation;
|
|
1804
|
-
const
|
|
1805
|
-
if (
|
|
1806
|
-
checks.push({ stream: e.stream, id: e.id,
|
|
1805
|
+
const parent_id = causation?.event?.id;
|
|
1806
|
+
if (parent_id !== void 0) {
|
|
1807
|
+
checks.push({ stream: e.stream, id: e.id, parent_id });
|
|
1807
1808
|
}
|
|
1808
1809
|
},
|
|
1809
1810
|
drain() {
|
|
1810
1811
|
const findings = [];
|
|
1811
|
-
for (const { stream, id,
|
|
1812
|
-
if (!
|
|
1812
|
+
for (const { stream, id, parent_id } of checks) {
|
|
1813
|
+
if (!seen_ids.has(parent_id)) {
|
|
1813
1814
|
findings.push({
|
|
1814
1815
|
category: "correlation-gaps",
|
|
1815
1816
|
stream,
|
|
@@ -1822,12 +1823,12 @@ var makeCorrelationGapsPass = () => {
|
|
|
1822
1823
|
}
|
|
1823
1824
|
};
|
|
1824
1825
|
};
|
|
1825
|
-
var
|
|
1826
|
+
var make_clock_anomalies_pass = () => {
|
|
1826
1827
|
const findings = [];
|
|
1827
|
-
const
|
|
1828
|
+
const last_per_stream = /* @__PURE__ */ new Map();
|
|
1828
1829
|
return {
|
|
1829
1830
|
category: "clock-anomalies",
|
|
1830
|
-
|
|
1831
|
+
on_event(e) {
|
|
1831
1832
|
const created = e.created.getTime();
|
|
1832
1833
|
if (created > Date.now()) {
|
|
1833
1834
|
findings.push({
|
|
@@ -1837,7 +1838,7 @@ var makeClockAnomaliesPass = () => {
|
|
|
1837
1838
|
reason: "future-created"
|
|
1838
1839
|
});
|
|
1839
1840
|
}
|
|
1840
|
-
const prev =
|
|
1841
|
+
const prev = last_per_stream.get(e.stream);
|
|
1841
1842
|
if (prev !== void 0 && created < prev) {
|
|
1842
1843
|
findings.push({
|
|
1843
1844
|
category: "clock-anomalies",
|
|
@@ -1846,48 +1847,48 @@ var makeClockAnomaliesPass = () => {
|
|
|
1846
1847
|
reason: "out-of-order"
|
|
1847
1848
|
});
|
|
1848
1849
|
}
|
|
1849
|
-
|
|
1850
|
+
last_per_stream.set(e.stream, created);
|
|
1850
1851
|
},
|
|
1851
1852
|
drain: () => findings
|
|
1852
1853
|
};
|
|
1853
1854
|
};
|
|
1854
|
-
function
|
|
1855
|
-
const state2 = deps.event_to_state.get(
|
|
1855
|
+
function restart_is_supported(deps, head_event_name) {
|
|
1856
|
+
const state2 = deps.event_to_state.get(head_event_name);
|
|
1856
1857
|
return state2?.snap !== void 0;
|
|
1857
1858
|
}
|
|
1858
1859
|
var PASS_FACTORIES = {
|
|
1859
|
-
schema:
|
|
1860
|
-
"deprecated-load":
|
|
1861
|
-
"close-candidate":
|
|
1862
|
-
"restart-candidate":
|
|
1863
|
-
"reaction-health":
|
|
1864
|
-
"snapshot-drift":
|
|
1865
|
-
"routing-health":
|
|
1866
|
-
"correlation-gaps":
|
|
1867
|
-
"clock-anomalies":
|
|
1860
|
+
schema: make_schema_pass,
|
|
1861
|
+
"deprecated-load": make_deprecated_load_pass,
|
|
1862
|
+
"close-candidate": make_close_candidate_pass,
|
|
1863
|
+
"restart-candidate": make_restart_candidate_pass,
|
|
1864
|
+
"reaction-health": make_reaction_health_pass,
|
|
1865
|
+
"snapshot-drift": make_snapshot_drift_pass,
|
|
1866
|
+
"routing-health": make_routing_health_pass,
|
|
1867
|
+
"correlation-gaps": make_correlation_gaps_pass,
|
|
1868
|
+
"clock-anomalies": make_clock_anomalies_pass
|
|
1868
1869
|
};
|
|
1869
1870
|
|
|
1870
1871
|
// src/internal/build-classify.ts
|
|
1871
1872
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
1872
|
-
function
|
|
1873
|
+
function classify_registry(registry, states) {
|
|
1873
1874
|
const statics = /* @__PURE__ */ new Map();
|
|
1874
|
-
const
|
|
1875
|
-
const
|
|
1876
|
-
let
|
|
1875
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
1876
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
1877
|
+
let has_dynamic_resolvers = false;
|
|
1877
1878
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
1878
|
-
if (register.reactions.size > 0)
|
|
1879
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
1879
1880
|
for (const reaction of register.reactions.values()) {
|
|
1880
1881
|
if (typeof reaction.resolver === "function") {
|
|
1881
|
-
|
|
1882
|
-
|
|
1882
|
+
has_dynamic_resolvers = true;
|
|
1883
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
1883
1884
|
} else {
|
|
1884
1885
|
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
1885
1886
|
const lane_name = lane ?? "default";
|
|
1886
|
-
const existing_lanes =
|
|
1887
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
1887
1888
|
if (existing_lanes !== ALL_LANES) {
|
|
1888
1889
|
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
1889
1890
|
set.add(lane_name);
|
|
1890
|
-
|
|
1891
|
+
event_to_lanes.set(name, set);
|
|
1891
1892
|
}
|
|
1892
1893
|
const key = `${target}|${source ?? ""}`;
|
|
1893
1894
|
const existing = statics.get(key);
|
|
@@ -1905,59 +1906,59 @@ function classifyRegistry(registry, states) {
|
|
|
1905
1906
|
}
|
|
1906
1907
|
}
|
|
1907
1908
|
}
|
|
1908
|
-
const
|
|
1909
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
1909
1910
|
for (const merged of states.values()) {
|
|
1910
|
-
for (const
|
|
1911
|
-
|
|
1911
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
1912
|
+
event_to_state.set(event_name, merged);
|
|
1912
1913
|
}
|
|
1913
1914
|
}
|
|
1914
1915
|
return {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1916
|
+
static_targets: [...statics.values()],
|
|
1917
|
+
has_dynamic_resolvers,
|
|
1918
|
+
reactive_events,
|
|
1919
|
+
event_to_state,
|
|
1920
|
+
event_to_lanes
|
|
1920
1921
|
};
|
|
1921
1922
|
}
|
|
1922
1923
|
|
|
1923
1924
|
// src/internal/close-cycle.ts
|
|
1924
|
-
async function
|
|
1925
|
-
const
|
|
1926
|
-
const streams = [...
|
|
1925
|
+
async function run_close_cycle(targets, deps) {
|
|
1926
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
1927
|
+
const streams = [...target_map.keys()];
|
|
1927
1928
|
const skipped = [];
|
|
1928
|
-
const
|
|
1929
|
-
const safe = await
|
|
1930
|
-
|
|
1931
|
-
deps.
|
|
1929
|
+
const stream_info = await scan_stream_heads(streams);
|
|
1930
|
+
const safe = await partition_by_safety(
|
|
1931
|
+
stream_info,
|
|
1932
|
+
deps.reactive_events_size,
|
|
1932
1933
|
skipped
|
|
1933
1934
|
);
|
|
1934
1935
|
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1935
|
-
const { guarded,
|
|
1936
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
1936
1937
|
safe,
|
|
1937
|
-
|
|
1938
|
+
stream_info,
|
|
1938
1939
|
deps.correlation,
|
|
1939
1940
|
deps.tombstone,
|
|
1940
1941
|
skipped
|
|
1941
1942
|
);
|
|
1942
1943
|
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1943
|
-
const
|
|
1944
|
+
const seed_states = await load_restart_seeds(
|
|
1944
1945
|
guarded,
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
deps.
|
|
1946
|
+
target_map,
|
|
1947
|
+
stream_info,
|
|
1948
|
+
deps.event_to_state,
|
|
1948
1949
|
deps.load,
|
|
1949
1950
|
deps.logger
|
|
1950
1951
|
);
|
|
1951
|
-
await
|
|
1952
|
-
const truncated = await
|
|
1952
|
+
await run_archive_callbacks(guarded, target_map);
|
|
1953
|
+
const truncated = await truncate_and_warm_cache(
|
|
1953
1954
|
guarded,
|
|
1954
|
-
|
|
1955
|
-
|
|
1955
|
+
seed_states,
|
|
1956
|
+
guard_events,
|
|
1956
1957
|
deps.correlation
|
|
1957
1958
|
);
|
|
1958
1959
|
return { truncated, skipped };
|
|
1959
1960
|
}
|
|
1960
|
-
async function
|
|
1961
|
+
async function scan_stream_heads(streams) {
|
|
1961
1962
|
const stats = await store2().query_stats(streams, {
|
|
1962
1963
|
exclude: [SNAP_EVENT]
|
|
1963
1964
|
});
|
|
@@ -1965,76 +1966,76 @@ async function scanStreamHeads(streams) {
|
|
|
1965
1966
|
for (const [stream, { head }] of stats) {
|
|
1966
1967
|
if (head.name === TOMBSTONE_EVENT) continue;
|
|
1967
1968
|
out.set(stream, {
|
|
1968
|
-
|
|
1969
|
+
max_id: head.id,
|
|
1969
1970
|
version: head.version,
|
|
1970
|
-
|
|
1971
|
+
last_event_name: head.name
|
|
1971
1972
|
});
|
|
1972
1973
|
}
|
|
1973
1974
|
return out;
|
|
1974
1975
|
}
|
|
1975
|
-
async function
|
|
1976
|
-
if (
|
|
1977
|
-
const
|
|
1976
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
1977
|
+
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
1978
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
1978
1979
|
await store2().query_streams((position) => {
|
|
1979
|
-
const
|
|
1980
|
-
for (const [stream, info] of
|
|
1981
|
-
if ((!
|
|
1982
|
-
|
|
1980
|
+
const source_re = position.source ? RegExp(position.source) : void 0;
|
|
1981
|
+
for (const [stream, info] of stream_info) {
|
|
1982
|
+
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
1983
|
+
pending_set.add(stream);
|
|
1983
1984
|
}
|
|
1984
1985
|
}
|
|
1985
1986
|
});
|
|
1986
1987
|
const safe = [];
|
|
1987
|
-
for (const [stream] of
|
|
1988
|
-
if (
|
|
1988
|
+
for (const [stream] of stream_info) {
|
|
1989
|
+
if (pending_set.has(stream)) skipped.push(stream);
|
|
1989
1990
|
else safe.push(stream);
|
|
1990
1991
|
}
|
|
1991
1992
|
return safe;
|
|
1992
1993
|
}
|
|
1993
|
-
async function
|
|
1994
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
1994
1995
|
const guarded = [];
|
|
1995
|
-
const
|
|
1996
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
1996
1997
|
await Promise.all(
|
|
1997
1998
|
safe.map(async (stream) => {
|
|
1998
|
-
const info =
|
|
1999
|
+
const info = stream_info.get(stream);
|
|
1999
2000
|
const committed = await tombstone2(stream, info.version, correlation);
|
|
2000
2001
|
if (committed) {
|
|
2001
2002
|
guarded.push(stream);
|
|
2002
|
-
|
|
2003
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
2003
2004
|
} else {
|
|
2004
2005
|
skipped.push(stream);
|
|
2005
2006
|
}
|
|
2006
2007
|
})
|
|
2007
2008
|
);
|
|
2008
|
-
return { guarded,
|
|
2009
|
+
return { guarded, guard_events };
|
|
2009
2010
|
}
|
|
2010
|
-
async function
|
|
2011
|
-
const
|
|
2011
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
2012
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
2012
2013
|
await Promise.all(
|
|
2013
|
-
guarded.filter((s) =>
|
|
2014
|
-
const
|
|
2015
|
-
const
|
|
2016
|
-
if (!
|
|
2014
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
2015
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
2016
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
2017
|
+
if (!owner_state) {
|
|
2017
2018
|
logger.error(
|
|
2018
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${
|
|
2019
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
2019
2020
|
);
|
|
2020
2021
|
return;
|
|
2021
2022
|
}
|
|
2022
|
-
const snap2 = await load2(
|
|
2023
|
-
|
|
2023
|
+
const snap2 = await load2(owner_state, stream);
|
|
2024
|
+
seed_states.set(stream, snap2.state);
|
|
2024
2025
|
})
|
|
2025
2026
|
);
|
|
2026
|
-
return
|
|
2027
|
+
return seed_states;
|
|
2027
2028
|
}
|
|
2028
|
-
async function
|
|
2029
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
2029
2030
|
for (const stream of guarded) {
|
|
2030
|
-
const
|
|
2031
|
-
if (
|
|
2031
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
2032
|
+
if (archive_fn) await archive_fn();
|
|
2032
2033
|
}
|
|
2033
2034
|
}
|
|
2034
|
-
async function
|
|
2035
|
-
const
|
|
2036
|
-
const snapshot =
|
|
2037
|
-
const guard =
|
|
2035
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
2036
|
+
const trunc_targets = guarded.map((stream) => {
|
|
2037
|
+
const snapshot = seed_states.get(stream);
|
|
2038
|
+
const guard = guard_events.get(stream);
|
|
2038
2039
|
return {
|
|
2039
2040
|
stream,
|
|
2040
2041
|
snapshot,
|
|
@@ -2046,11 +2047,11 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
2046
2047
|
}
|
|
2047
2048
|
};
|
|
2048
2049
|
});
|
|
2049
|
-
const truncated = await store2().truncate(
|
|
2050
|
+
const truncated = await store2().truncate(trunc_targets);
|
|
2050
2051
|
await Promise.all(
|
|
2051
2052
|
guarded.map(async (stream) => {
|
|
2052
2053
|
const entry = truncated.get(stream);
|
|
2053
|
-
const state2 =
|
|
2054
|
+
const state2 = seed_states.get(stream);
|
|
2054
2055
|
if (state2 && entry) {
|
|
2055
2056
|
await cache2().set(stream, {
|
|
2056
2057
|
state: state2,
|
|
@@ -2078,13 +2079,13 @@ var CorrelateCycle = class {
|
|
|
2078
2079
|
_has_dynamic_resolvers;
|
|
2079
2080
|
_cd;
|
|
2080
2081
|
_on_init;
|
|
2081
|
-
constructor(registry,
|
|
2082
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init) {
|
|
2082
2083
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
2083
2084
|
this._registry = registry;
|
|
2084
|
-
this._static_targets =
|
|
2085
|
-
this._has_dynamic_resolvers =
|
|
2085
|
+
this._static_targets = static_targets;
|
|
2086
|
+
this._has_dynamic_resolvers = has_dynamic_resolvers;
|
|
2086
2087
|
this._cd = cd;
|
|
2087
|
-
this._on_init =
|
|
2088
|
+
this._on_init = on_init;
|
|
2088
2089
|
}
|
|
2089
2090
|
/** Last correlated event id. */
|
|
2090
2091
|
get checkpoint() {
|
|
@@ -2095,7 +2096,7 @@ var CorrelateCycle = class {
|
|
|
2095
2096
|
* - Reads max(at) from store as cold-start checkpoint
|
|
2096
2097
|
* - Subscribes static resolver targets (idempotent upsert)
|
|
2097
2098
|
* - Populates the subscribed-streams LRU
|
|
2098
|
-
* - Fires `
|
|
2099
|
+
* - Fires `on_init` once (Act uses this to flag a cold-start drain)
|
|
2099
2100
|
*/
|
|
2100
2101
|
async init() {
|
|
2101
2102
|
if (this._initialized) return;
|
|
@@ -2128,15 +2129,15 @@ var CorrelateCycle = class {
|
|
|
2128
2129
|
if (typeof reaction.resolver !== "function") continue;
|
|
2129
2130
|
const resolved = reaction.resolver(event);
|
|
2130
2131
|
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
2131
|
-
const
|
|
2132
|
+
const incoming_priority = resolved.priority ?? 0;
|
|
2132
2133
|
const entry = correlated.get(resolved.target) || {
|
|
2133
2134
|
source: resolved.source,
|
|
2134
|
-
priority:
|
|
2135
|
+
priority: incoming_priority,
|
|
2135
2136
|
lane: resolved.lane,
|
|
2136
2137
|
payloads: []
|
|
2137
2138
|
};
|
|
2138
|
-
if (
|
|
2139
|
-
entry.priority =
|
|
2139
|
+
if (incoming_priority > entry.priority)
|
|
2140
|
+
entry.priority = incoming_priority;
|
|
2140
2141
|
entry.payloads.push({
|
|
2141
2142
|
...reaction,
|
|
2142
2143
|
source: resolved.source,
|
|
@@ -2175,7 +2176,7 @@ var CorrelateCycle = class {
|
|
|
2175
2176
|
* running. Errors from `correlate()` are routed through `log()` so they
|
|
2176
2177
|
* land in the configured logger (the timer keeps running on failure).
|
|
2177
2178
|
*/
|
|
2178
|
-
|
|
2179
|
+
start_polling(query = {}, frequency = 1e4, callback) {
|
|
2179
2180
|
if (this._timer) return false;
|
|
2180
2181
|
const limit = query.limit || 100;
|
|
2181
2182
|
this._timer = setInterval(
|
|
@@ -2187,7 +2188,7 @@ var CorrelateCycle = class {
|
|
|
2187
2188
|
return true;
|
|
2188
2189
|
}
|
|
2189
2190
|
/** Stop the periodic correlation worker. Idempotent. */
|
|
2190
|
-
|
|
2191
|
+
stop_polling() {
|
|
2191
2192
|
if (this._timer) {
|
|
2192
2193
|
clearInterval(this._timer);
|
|
2193
2194
|
this._timer = void 0;
|
|
@@ -2203,14 +2204,14 @@ var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
|
2203
2204
|
function seg(n) {
|
|
2204
2205
|
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
2205
2206
|
}
|
|
2206
|
-
var
|
|
2207
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
2207
2208
|
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2208
2209
|
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2209
2210
|
const ts = seg(Date.now() % SEG_SPACE);
|
|
2210
2211
|
const rnd = seg((0, import_node_crypto.randomInt)(SEG_SPACE));
|
|
2211
2212
|
return `${s}-${a}-${ts}${rnd}`;
|
|
2212
2213
|
};
|
|
2213
|
-
function
|
|
2214
|
+
function close_correlation(correlator, actor) {
|
|
2214
2215
|
return correlator({
|
|
2215
2216
|
state: "$close",
|
|
2216
2217
|
action: "close",
|
|
@@ -2226,7 +2227,7 @@ var import_node_crypto2 = require("crypto");
|
|
|
2226
2227
|
var RATIO_MIN = 0.2;
|
|
2227
2228
|
var RATIO_MAX = 0.8;
|
|
2228
2229
|
var RATIO_DEFAULT = 0.5;
|
|
2229
|
-
function
|
|
2230
|
+
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
2230
2231
|
let lagging_handled = 0;
|
|
2231
2232
|
let leading_handled = 0;
|
|
2232
2233
|
for (const { lease, handled: count } of handled) {
|
|
@@ -2263,7 +2264,7 @@ var subscribe = (streams) => store2().subscribe(streams);
|
|
|
2263
2264
|
var import_act_patch = require("@rotorsoft/act-patch");
|
|
2264
2265
|
|
|
2265
2266
|
// src/internal/backoff.ts
|
|
2266
|
-
function
|
|
2267
|
+
function compute_backoff_delay(retry, opts) {
|
|
2267
2268
|
if (!opts || opts.baseMs <= 0) return 0;
|
|
2268
2269
|
const r = Math.max(0, retry);
|
|
2269
2270
|
let delay;
|
|
@@ -2433,8 +2434,8 @@ async function scan(source, opts = {}, callback) {
|
|
|
2433
2434
|
};
|
|
2434
2435
|
}
|
|
2435
2436
|
async function load(me, stream, callback, asOf, actor) {
|
|
2436
|
-
const
|
|
2437
|
-
const cached =
|
|
2437
|
+
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
2438
|
+
const cached = time_travel ? void 0 : await cache2().get(stream);
|
|
2438
2439
|
const cache_hit = !!cached;
|
|
2439
2440
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
2440
2441
|
let patches = cached?.patches ?? 0;
|
|
@@ -2477,7 +2478,7 @@ async function load(me, stream, callback, asOf, actor) {
|
|
|
2477
2478
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
2478
2479
|
}
|
|
2479
2480
|
);
|
|
2480
|
-
if (replayed > 0 && !
|
|
2481
|
+
if (replayed > 0 && !time_travel && event) {
|
|
2481
2482
|
await cache2().set(stream, {
|
|
2482
2483
|
state: state2,
|
|
2483
2484
|
version,
|
|
@@ -2488,12 +2489,12 @@ async function load(me, stream, callback, asOf, actor) {
|
|
|
2488
2489
|
}
|
|
2489
2490
|
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
2490
2491
|
}
|
|
2491
|
-
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator =
|
|
2492
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = default_correlator) {
|
|
2492
2493
|
const { stream, expectedVersion, actor } = target;
|
|
2493
2494
|
if (!stream) throw new Error("Missing target stream");
|
|
2494
2495
|
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
2495
2496
|
const opts = me.options?.[action2];
|
|
2496
|
-
const
|
|
2497
|
+
const max_retries = opts?.maxRetries ?? 0;
|
|
2497
2498
|
for (let attempt = 0; ; attempt++) {
|
|
2498
2499
|
try {
|
|
2499
2500
|
const snapshot = await load(
|
|
@@ -2611,10 +2612,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2611
2612
|
return snapshots;
|
|
2612
2613
|
} catch (error) {
|
|
2613
2614
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
2614
|
-
if (attempt >=
|
|
2615
|
+
if (attempt >= max_retries) throw error;
|
|
2615
2616
|
if (opts?.backoff) {
|
|
2616
|
-
const
|
|
2617
|
-
if (
|
|
2617
|
+
const delay_ms = compute_backoff_delay(attempt, opts.backoff);
|
|
2618
|
+
if (delay_ms > 0) await sleep(delay_ms);
|
|
2618
2619
|
}
|
|
2619
2620
|
}
|
|
2620
2621
|
}
|
|
@@ -2638,12 +2639,12 @@ var C_STREAM = "\x1B[38;5;226m";
|
|
|
2638
2639
|
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
2639
2640
|
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
2640
2641
|
var drain_caption = (caption, lane) => {
|
|
2641
|
-
const
|
|
2642
|
+
const show_lane = lane && lane !== "default";
|
|
2642
2643
|
if (PRETTY) {
|
|
2643
2644
|
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
2644
|
-
return
|
|
2645
|
+
return show_lane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
2645
2646
|
}
|
|
2646
|
-
return
|
|
2647
|
+
return show_lane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
2647
2648
|
};
|
|
2648
2649
|
var cache_marker = (hit) => {
|
|
2649
2650
|
const word = hit ? "hit" : "miss";
|
|
@@ -2672,10 +2673,10 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
2672
2673
|
exit?.(result, ...args);
|
|
2673
2674
|
return result;
|
|
2674
2675
|
});
|
|
2675
|
-
function
|
|
2676
|
-
const bound_action = (me,
|
|
2676
|
+
function build_es(logger, correlator = default_correlator) {
|
|
2677
|
+
const bound_action = (me, action_name, target, payload, reactingTo, skipValidation = false) => action(
|
|
2677
2678
|
me,
|
|
2678
|
-
|
|
2679
|
+
action_name,
|
|
2679
2680
|
target,
|
|
2680
2681
|
payload,
|
|
2681
2682
|
reactingTo,
|
|
@@ -2745,7 +2746,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
2745
2746
|
})
|
|
2746
2747
|
};
|
|
2747
2748
|
}
|
|
2748
|
-
function
|
|
2749
|
+
function build_drain(logger) {
|
|
2749
2750
|
return {
|
|
2750
2751
|
claim,
|
|
2751
2752
|
fetch,
|
|
@@ -2754,46 +2755,48 @@ function buildDrain(logger) {
|
|
|
2754
2755
|
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
2755
2756
|
if (!result.subscribed) return;
|
|
2756
2757
|
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
2757
|
-
const
|
|
2758
|
+
const uniform_lane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
2758
2759
|
const data = streams.map(
|
|
2759
|
-
({ stream, lane }) =>
|
|
2760
|
+
({ stream, lane }) => uniform_lane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
2760
2761
|
).join(" ");
|
|
2761
|
-
logger.trace(
|
|
2762
|
+
logger.trace(
|
|
2763
|
+
`${drain_caption("correlated", uniform_lane)} ${data}`
|
|
2764
|
+
);
|
|
2762
2765
|
})
|
|
2763
2766
|
};
|
|
2764
2767
|
}
|
|
2765
|
-
function
|
|
2768
|
+
function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
|
|
2766
2769
|
if (logger.level !== "trace" || !leased.length) return;
|
|
2767
2770
|
const lane = leased[0]?.lane;
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2770
|
-
const
|
|
2771
|
-
const
|
|
2771
|
+
const fetch_by_stream = new Map(fetched.map((f) => [f.stream, f]));
|
|
2772
|
+
const acked_by_stream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
2773
|
+
const blocked_by_stream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
2774
|
+
const failed_by_stream = new Map(
|
|
2772
2775
|
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
2773
2776
|
);
|
|
2774
2777
|
const detail = leased.map(({ stream, at, retry }) => {
|
|
2775
|
-
const f =
|
|
2778
|
+
const f = fetch_by_stream.get(stream);
|
|
2776
2779
|
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
2777
2780
|
const events = f && f.events.length ? ` ${dim(
|
|
2778
2781
|
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
2779
2782
|
)}` : "";
|
|
2780
|
-
const
|
|
2781
|
-
const
|
|
2782
|
-
const failure =
|
|
2783
|
-
let
|
|
2783
|
+
const acked_at = acked_by_stream.get(stream);
|
|
2784
|
+
const ack_part = acked_at !== void 0 ? hue(C_HIT, `\u2713 @${acked_at}`) : "";
|
|
2785
|
+
const failure = failed_by_stream.get(stream);
|
|
2786
|
+
let fail_part = "";
|
|
2784
2787
|
if (failure) {
|
|
2785
|
-
const
|
|
2786
|
-
const
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2788
|
+
const failed_at = failure.failed_at ?? at;
|
|
2789
|
+
const blocked_error = blocked_by_stream.get(stream);
|
|
2790
|
+
if (blocked_error !== void 0) {
|
|
2791
|
+
fail_part = `${hue(C_ERR, `\u2717 @${failed_at}/${retry}`)} ${dim(`(${blocked_error})`)}`;
|
|
2789
2792
|
} else {
|
|
2790
|
-
|
|
2793
|
+
fail_part = `${hue(C_MISS, `\u26A0 @${failed_at}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
2791
2794
|
}
|
|
2792
2795
|
}
|
|
2793
2796
|
let tail;
|
|
2794
|
-
if (
|
|
2795
|
-
else if (
|
|
2796
|
-
else if (
|
|
2797
|
+
if (ack_part && fail_part) tail = ` ${ack_part} ${fail_part}`;
|
|
2798
|
+
else if (ack_part) tail = ` ${ack_part}`;
|
|
2799
|
+
else if (fail_part) tail = ` ${fail_part}`;
|
|
2797
2800
|
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
2798
2801
|
return `${key}${events}${tail}`;
|
|
2799
2802
|
}).join(", ");
|
|
@@ -2801,7 +2804,7 @@ function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
|
2801
2804
|
}
|
|
2802
2805
|
|
|
2803
2806
|
// src/internal/drain-cycle.ts
|
|
2804
|
-
async function
|
|
2807
|
+
async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, is_deferred, lane) {
|
|
2805
2808
|
const leased = await ops.claim(
|
|
2806
2809
|
lagging,
|
|
2807
2810
|
leading,
|
|
@@ -2810,7 +2813,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
2810
2813
|
lane
|
|
2811
2814
|
);
|
|
2812
2815
|
if (!leased.length) return void 0;
|
|
2813
|
-
const active =
|
|
2816
|
+
const active = is_deferred ? leased.filter((l) => !is_deferred(l.stream)) : leased;
|
|
2814
2817
|
if (!active.length) {
|
|
2815
2818
|
return {
|
|
2816
2819
|
leased,
|
|
@@ -2821,7 +2824,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
2821
2824
|
};
|
|
2822
2825
|
}
|
|
2823
2826
|
const fetched = await ops.fetch(active, eventLimit);
|
|
2824
|
-
const
|
|
2827
|
+
const fetch_map = /* @__PURE__ */ new Map();
|
|
2825
2828
|
const fetch_window_at = fetched.reduce(
|
|
2826
2829
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
2827
2830
|
0
|
|
@@ -2836,16 +2839,16 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
2836
2839
|
return resolved && resolved.target === stream;
|
|
2837
2840
|
}).map((reaction) => ({ ...reaction, event }));
|
|
2838
2841
|
});
|
|
2839
|
-
|
|
2842
|
+
fetch_map.set(stream, { fetch: f, payloads });
|
|
2840
2843
|
}
|
|
2841
2844
|
const handled = await Promise.all(
|
|
2842
2845
|
active.map((lease) => {
|
|
2843
|
-
const entry =
|
|
2846
|
+
const entry = fetch_map.get(lease.stream);
|
|
2844
2847
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2845
2848
|
const { payloads } = entry;
|
|
2846
|
-
const batchHandler =
|
|
2849
|
+
const batchHandler = batch_handlers.get(lease.stream);
|
|
2847
2850
|
if (batchHandler && payloads.length > 0) {
|
|
2848
|
-
return
|
|
2851
|
+
return handle_batch({ ...lease, at }, payloads, batchHandler);
|
|
2849
2852
|
}
|
|
2850
2853
|
return handle({ ...lease, at }, payloads);
|
|
2851
2854
|
})
|
|
@@ -2869,14 +2872,14 @@ var DrainController = class {
|
|
|
2869
2872
|
_locked = false;
|
|
2870
2873
|
_ratio = 0.5;
|
|
2871
2874
|
/**
|
|
2872
|
-
* Per-stream backoff: `stream →
|
|
2873
|
-
* `_finalize` via `HandleResult.
|
|
2875
|
+
* Per-stream backoff: `stream → next_attempt_at` (ms since epoch). Set by
|
|
2876
|
+
* `_finalize` via `HandleResult.next_attempt_at`; cleared on successful
|
|
2874
2877
|
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
2875
2878
|
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
2876
2879
|
*/
|
|
2877
2880
|
_backoff = /* @__PURE__ */ new Map();
|
|
2878
|
-
/** Timer re-arming drain at the earliest pending `
|
|
2879
|
-
|
|
2881
|
+
/** Timer re-arming drain at the earliest pending `next_attempt_at`. */
|
|
2882
|
+
_backoff_timer;
|
|
2880
2883
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
2881
2884
|
_worker;
|
|
2882
2885
|
_stopped = false;
|
|
@@ -2897,7 +2900,7 @@ var DrainController = class {
|
|
|
2897
2900
|
return this._armed;
|
|
2898
2901
|
}
|
|
2899
2902
|
/** Returns true when `stream` is currently within a backoff window. */
|
|
2900
|
-
|
|
2903
|
+
is_deferred = (stream) => {
|
|
2901
2904
|
const next = this._backoff.get(stream);
|
|
2902
2905
|
return next !== void 0 && next > Date.now();
|
|
2903
2906
|
};
|
|
@@ -2907,20 +2910,20 @@ var DrainController = class {
|
|
|
2907
2910
|
* Idempotent — collapses many simultaneously deferred streams into a
|
|
2908
2911
|
* single timer.
|
|
2909
2912
|
*/
|
|
2910
|
-
|
|
2911
|
-
if (this.
|
|
2913
|
+
schedule_backoff_wake() {
|
|
2914
|
+
if (this._backoff_timer) clearTimeout(this._backoff_timer);
|
|
2912
2915
|
let earliest = Number.POSITIVE_INFINITY;
|
|
2913
2916
|
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
2914
2917
|
const delay = Math.max(0, earliest - Date.now());
|
|
2915
|
-
this.
|
|
2916
|
-
this.
|
|
2918
|
+
this._backoff_timer = setTimeout(() => {
|
|
2919
|
+
this._backoff_timer = void 0;
|
|
2917
2920
|
const now = Date.now();
|
|
2918
2921
|
for (const [stream, at] of this._backoff) {
|
|
2919
2922
|
if (at <= now) this._backoff.delete(stream);
|
|
2920
2923
|
}
|
|
2921
2924
|
this._armed = true;
|
|
2922
2925
|
}, delay);
|
|
2923
|
-
this.
|
|
2926
|
+
this._backoff_timer.unref();
|
|
2924
2927
|
}
|
|
2925
2928
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
2926
2929
|
get lane() {
|
|
@@ -2966,17 +2969,17 @@ var DrainController = class {
|
|
|
2966
2969
|
this._locked = true;
|
|
2967
2970
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2968
2971
|
const leading = streamLimit - lagging;
|
|
2969
|
-
const cycle = await
|
|
2972
|
+
const cycle = await run_drain_cycle(
|
|
2970
2973
|
this._deps.ops,
|
|
2971
2974
|
this._deps.registry,
|
|
2972
|
-
this._deps.
|
|
2975
|
+
this._deps.batch_handlers,
|
|
2973
2976
|
this._deps.handle,
|
|
2974
|
-
this._deps.
|
|
2977
|
+
this._deps.handle_batch,
|
|
2975
2978
|
lagging,
|
|
2976
2979
|
leading,
|
|
2977
2980
|
eventLimit,
|
|
2978
2981
|
leaseMillis,
|
|
2979
|
-
this._backoff.size > 0 ? this.
|
|
2982
|
+
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
2980
2983
|
this._deps.lane
|
|
2981
2984
|
);
|
|
2982
2985
|
if (!cycle) {
|
|
@@ -2984,20 +2987,20 @@ var DrainController = class {
|
|
|
2984
2987
|
return EMPTY_DRAIN;
|
|
2985
2988
|
}
|
|
2986
2989
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2987
|
-
|
|
2988
|
-
this._ratio =
|
|
2990
|
+
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
2991
|
+
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
2989
2992
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
2990
2993
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
2991
2994
|
for (const h of handled) {
|
|
2992
|
-
if (h.
|
|
2993
|
-
this._backoff.set(h.lease.stream, h.
|
|
2995
|
+
if (h.next_attempt_at !== void 0 && !h.block) {
|
|
2996
|
+
this._backoff.set(h.lease.stream, h.next_attempt_at);
|
|
2994
2997
|
}
|
|
2995
2998
|
}
|
|
2996
|
-
if (this._backoff.size > 0) this.
|
|
2997
|
-
if (acked.length) this._deps.
|
|
2998
|
-
if (blocked.length) this._deps.
|
|
2999
|
-
const
|
|
3000
|
-
if (!acked.length && !blocked.length && !
|
|
2999
|
+
if (this._backoff.size > 0) this.schedule_backoff_wake();
|
|
3000
|
+
if (acked.length) this._deps.on_acked(acked);
|
|
3001
|
+
if (blocked.length) this._deps.on_blocked(blocked);
|
|
3002
|
+
const has_errors = handled.some(({ error }) => error);
|
|
3003
|
+
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
3001
3004
|
return { fetched, leased, acked, blocked };
|
|
3002
3005
|
} catch (error) {
|
|
3003
3006
|
this._deps.logger.error(error);
|
|
@@ -3010,44 +3013,44 @@ var DrainController = class {
|
|
|
3010
3013
|
|
|
3011
3014
|
// src/internal/merge.ts
|
|
3012
3015
|
var import_zod5 = require("zod");
|
|
3013
|
-
function
|
|
3016
|
+
function base_type_name(zodType) {
|
|
3014
3017
|
let t = zodType;
|
|
3015
3018
|
while (typeof t.unwrap === "function") {
|
|
3016
3019
|
t = t.unwrap();
|
|
3017
3020
|
}
|
|
3018
3021
|
return t.constructor.name;
|
|
3019
3022
|
}
|
|
3020
|
-
function
|
|
3023
|
+
function merge_schemas(existing, incoming, state_name) {
|
|
3021
3024
|
if (existing instanceof import_zod5.ZodObject && incoming instanceof import_zod5.ZodObject) {
|
|
3022
|
-
const
|
|
3023
|
-
const
|
|
3024
|
-
for (const key of Object.keys(
|
|
3025
|
-
if (key in
|
|
3026
|
-
const
|
|
3027
|
-
const
|
|
3028
|
-
if (
|
|
3025
|
+
const existing_shape = existing.shape;
|
|
3026
|
+
const incoming_shape = incoming.shape;
|
|
3027
|
+
for (const key of Object.keys(incoming_shape)) {
|
|
3028
|
+
if (key in existing_shape) {
|
|
3029
|
+
const existing_base = base_type_name(existing_shape[key]);
|
|
3030
|
+
const incoming_base = base_type_name(incoming_shape[key]);
|
|
3031
|
+
if (existing_base !== incoming_base) {
|
|
3029
3032
|
throw new Error(
|
|
3030
|
-
`Schema conflict in "${
|
|
3033
|
+
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
3031
3034
|
);
|
|
3032
3035
|
}
|
|
3033
3036
|
}
|
|
3034
3037
|
}
|
|
3035
|
-
return existing.extend(
|
|
3038
|
+
return existing.extend(incoming_shape);
|
|
3036
3039
|
}
|
|
3037
3040
|
return existing;
|
|
3038
3041
|
}
|
|
3039
|
-
function
|
|
3042
|
+
function merge_inits(existing, incoming) {
|
|
3040
3043
|
return () => ({ ...existing(), ...incoming() });
|
|
3041
3044
|
}
|
|
3042
|
-
function
|
|
3045
|
+
function register_state(state2, states, actions, events) {
|
|
3043
3046
|
const existing = states.get(state2.name);
|
|
3044
3047
|
if (existing) {
|
|
3045
|
-
|
|
3048
|
+
merge_into_existing(state2, existing, states, actions, events);
|
|
3046
3049
|
} else {
|
|
3047
|
-
|
|
3050
|
+
register_new_state(state2, states, actions, events);
|
|
3048
3051
|
}
|
|
3049
3052
|
}
|
|
3050
|
-
function
|
|
3053
|
+
function register_new_state(state2, states, actions, events) {
|
|
3051
3054
|
states.set(state2.name, state2);
|
|
3052
3055
|
for (const name of Object.keys(state2.actions)) {
|
|
3053
3056
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -3058,7 +3061,7 @@ function registerNewState(state2, states, actions, events) {
|
|
|
3058
3061
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
3059
3062
|
}
|
|
3060
3063
|
}
|
|
3061
|
-
function
|
|
3064
|
+
function merge_into_existing(state2, existing, states, actions, events) {
|
|
3062
3065
|
for (const name of Object.keys(state2.actions)) {
|
|
3063
3066
|
if (existing.actions[name] === state2.actions[name]) continue;
|
|
3064
3067
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -3072,14 +3075,14 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
3072
3075
|
}
|
|
3073
3076
|
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
3074
3077
|
}
|
|
3075
|
-
const
|
|
3078
|
+
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
3076
3079
|
const merged = {
|
|
3077
3080
|
...existing,
|
|
3078
|
-
state:
|
|
3079
|
-
init:
|
|
3081
|
+
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
3082
|
+
init: merge_inits(existing.init, state2.init),
|
|
3080
3083
|
events: { ...existing.events, ...state2.events },
|
|
3081
3084
|
actions: { ...existing.actions, ...state2.actions },
|
|
3082
|
-
patch:
|
|
3085
|
+
patch: merged_patch,
|
|
3083
3086
|
on: { ...existing.on, ...state2.on },
|
|
3084
3087
|
given: { ...existing.given, ...state2.given },
|
|
3085
3088
|
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
@@ -3097,48 +3100,48 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
3097
3100
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
3098
3101
|
}
|
|
3099
3102
|
}
|
|
3100
|
-
function
|
|
3103
|
+
function merge_patches(existing, incoming, state_name) {
|
|
3101
3104
|
const merged = { ...existing };
|
|
3102
3105
|
for (const name of Object.keys(incoming)) {
|
|
3103
|
-
const
|
|
3104
|
-
const
|
|
3105
|
-
if (!
|
|
3106
|
-
merged[name] =
|
|
3106
|
+
const existing_p = existing[name];
|
|
3107
|
+
const incoming_p = incoming[name];
|
|
3108
|
+
if (!existing_p) {
|
|
3109
|
+
merged[name] = incoming_p;
|
|
3107
3110
|
continue;
|
|
3108
3111
|
}
|
|
3109
|
-
const
|
|
3110
|
-
const
|
|
3111
|
-
if (!
|
|
3112
|
+
const existing_is_default = existing_p._passthrough;
|
|
3113
|
+
const incoming_is_default = incoming_p._passthrough;
|
|
3114
|
+
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
3112
3115
|
throw new Error(
|
|
3113
|
-
`Duplicate custom patch for event "${name}" in state "${
|
|
3116
|
+
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
3114
3117
|
);
|
|
3115
3118
|
}
|
|
3116
|
-
if (
|
|
3117
|
-
merged[name] =
|
|
3119
|
+
if (existing_is_default && !incoming_is_default) {
|
|
3120
|
+
merged[name] = incoming_p;
|
|
3118
3121
|
}
|
|
3119
3122
|
}
|
|
3120
3123
|
return merged;
|
|
3121
3124
|
}
|
|
3122
|
-
function
|
|
3123
|
-
for (const [
|
|
3124
|
-
const
|
|
3125
|
-
if (!
|
|
3126
|
-
for (const [name, reaction] of
|
|
3127
|
-
|
|
3125
|
+
function merge_event_register(target, source) {
|
|
3126
|
+
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
3127
|
+
const target_reg = target[event_name];
|
|
3128
|
+
if (!target_reg) continue;
|
|
3129
|
+
for (const [name, reaction] of source_reg.reactions) {
|
|
3130
|
+
target_reg.reactions.set(name, reaction);
|
|
3128
3131
|
}
|
|
3129
3132
|
}
|
|
3130
3133
|
}
|
|
3131
|
-
function
|
|
3132
|
-
for (const
|
|
3133
|
-
const
|
|
3134
|
-
const existing = events[
|
|
3134
|
+
function merge_projection(proj, events) {
|
|
3135
|
+
for (const event_name of Object.keys(proj.events)) {
|
|
3136
|
+
const proj_register = proj.events[event_name];
|
|
3137
|
+
const existing = events[event_name];
|
|
3135
3138
|
if (!existing) {
|
|
3136
|
-
events[
|
|
3137
|
-
schema:
|
|
3138
|
-
reactions: new Map(
|
|
3139
|
+
events[event_name] = {
|
|
3140
|
+
schema: proj_register.schema,
|
|
3141
|
+
reactions: new Map(proj_register.reactions)
|
|
3139
3142
|
};
|
|
3140
3143
|
} else {
|
|
3141
|
-
for (const [name, reaction] of
|
|
3144
|
+
for (const [name, reaction] of proj_register.reactions) {
|
|
3142
3145
|
let key = name;
|
|
3143
3146
|
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
3144
3147
|
existing.reactions.set(key, reaction);
|
|
@@ -3155,24 +3158,24 @@ var _this_ = ({ stream }) => ({
|
|
|
3155
3158
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
3156
3159
|
if (!error) return { lease, handled, acked_at: at };
|
|
3157
3160
|
logger.error(error);
|
|
3158
|
-
const
|
|
3159
|
-
const block2 = options.blockOnError && (
|
|
3161
|
+
const non_retryable = error instanceof NonRetryableError;
|
|
3162
|
+
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
3160
3163
|
if (block2)
|
|
3161
3164
|
logger.error(
|
|
3162
|
-
|
|
3165
|
+
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
3163
3166
|
);
|
|
3164
|
-
const
|
|
3167
|
+
const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
|
|
3165
3168
|
return {
|
|
3166
3169
|
lease,
|
|
3167
3170
|
handled,
|
|
3168
3171
|
acked_at: at,
|
|
3169
3172
|
error: error.message,
|
|
3170
3173
|
block: block2,
|
|
3171
|
-
|
|
3174
|
+
next_attempt_at,
|
|
3172
3175
|
failed_at
|
|
3173
3176
|
};
|
|
3174
3177
|
}
|
|
3175
|
-
function
|
|
3178
|
+
function build_handle(deps) {
|
|
3176
3179
|
const {
|
|
3177
3180
|
logger,
|
|
3178
3181
|
bound_do,
|
|
@@ -3188,7 +3191,7 @@ function buildHandle(deps) {
|
|
|
3188
3191
|
let handled = 0;
|
|
3189
3192
|
if (lease.retry > 0)
|
|
3190
3193
|
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
3191
|
-
const
|
|
3194
|
+
const scoped_app = {
|
|
3192
3195
|
do: bound_do,
|
|
3193
3196
|
load: bound_load,
|
|
3194
3197
|
query: bound_query,
|
|
@@ -3197,15 +3200,15 @@ function buildHandle(deps) {
|
|
|
3197
3200
|
};
|
|
3198
3201
|
for (const payload of payloads) {
|
|
3199
3202
|
const { event, handler } = payload;
|
|
3200
|
-
|
|
3203
|
+
scoped_app.do = (action2, target, action_payload, reactingTo, skipValidation) => bound_do(
|
|
3201
3204
|
action2,
|
|
3202
3205
|
target,
|
|
3203
|
-
|
|
3206
|
+
action_payload,
|
|
3204
3207
|
reactingTo ?? event,
|
|
3205
3208
|
skipValidation
|
|
3206
3209
|
);
|
|
3207
3210
|
try {
|
|
3208
|
-
await handler(event, stream,
|
|
3211
|
+
await handler(event, stream, scoped_app);
|
|
3209
3212
|
at = event.id;
|
|
3210
3213
|
handled++;
|
|
3211
3214
|
} catch (error) {
|
|
@@ -3223,7 +3226,7 @@ function buildHandle(deps) {
|
|
|
3223
3226
|
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
3224
3227
|
};
|
|
3225
3228
|
}
|
|
3226
|
-
function
|
|
3229
|
+
function build_handle_batch(logger) {
|
|
3227
3230
|
return async (lease, payloads, batchHandler) => {
|
|
3228
3231
|
const stream = lease.stream;
|
|
3229
3232
|
const events = payloads.map(
|
|
@@ -3255,23 +3258,23 @@ var SettleLoop = class {
|
|
|
3255
3258
|
_deps;
|
|
3256
3259
|
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
3257
3260
|
_default_debounce_ms;
|
|
3258
|
-
constructor(deps,
|
|
3261
|
+
constructor(deps, default_debounce_ms) {
|
|
3259
3262
|
this._deps = deps;
|
|
3260
|
-
this._default_debounce_ms =
|
|
3263
|
+
this._default_debounce_ms = default_debounce_ms;
|
|
3261
3264
|
}
|
|
3262
3265
|
/**
|
|
3263
3266
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
3264
3267
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
3265
3268
|
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
3266
3269
|
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
3267
|
-
* via {@link SettleDeps.
|
|
3270
|
+
* via {@link SettleDeps.on_settled}.
|
|
3268
3271
|
*/
|
|
3269
3272
|
schedule(options = {}) {
|
|
3270
3273
|
const {
|
|
3271
3274
|
debounceMs = this._default_debounce_ms,
|
|
3272
|
-
correlate:
|
|
3275
|
+
correlate: correlate_query = { after: -1, limit: 100 },
|
|
3273
3276
|
maxPasses = Infinity,
|
|
3274
|
-
...
|
|
3277
|
+
...drain_options
|
|
3275
3278
|
} = options;
|
|
3276
3279
|
if (this._timer) clearTimeout(this._timer);
|
|
3277
3280
|
this._timer = setTimeout(() => {
|
|
@@ -3280,17 +3283,17 @@ var SettleLoop = class {
|
|
|
3280
3283
|
this._running = true;
|
|
3281
3284
|
(async () => {
|
|
3282
3285
|
await this._deps.init();
|
|
3283
|
-
let
|
|
3286
|
+
let last_drain;
|
|
3284
3287
|
for (let i = 0; i < maxPasses; i++) {
|
|
3285
3288
|
const { subscribed } = await this._deps.correlate({
|
|
3286
|
-
...
|
|
3289
|
+
...correlate_query,
|
|
3287
3290
|
after: this._deps.checkpoint()
|
|
3288
3291
|
});
|
|
3289
|
-
|
|
3290
|
-
const made_progress = subscribed > 0 ||
|
|
3292
|
+
last_drain = await this._deps.drain(drain_options);
|
|
3293
|
+
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
3291
3294
|
if (!made_progress) break;
|
|
3292
3295
|
}
|
|
3293
|
-
if (
|
|
3296
|
+
if (last_drain) this._deps.on_settled(last_drain);
|
|
3294
3297
|
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
3295
3298
|
this._running = false;
|
|
3296
3299
|
});
|
|
@@ -3377,7 +3380,7 @@ var Act = class {
|
|
|
3377
3380
|
_event_to_state;
|
|
3378
3381
|
/**
|
|
3379
3382
|
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
3380
|
-
* `
|
|
3383
|
+
* `classify_registry` once per build. `"all"` means at least one of
|
|
3381
3384
|
* the event's reactions is a dynamic resolver (lane opaque until
|
|
3382
3385
|
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
3383
3386
|
* reactions target.
|
|
@@ -3400,9 +3403,9 @@ var Act = class {
|
|
|
3400
3403
|
_scoped;
|
|
3401
3404
|
/**
|
|
3402
3405
|
* Correlation-id generator for originating actions. Bound at
|
|
3403
|
-
* construction from `options.correlator ??
|
|
3406
|
+
* construction from `options.correlator ?? default_correlator`. The
|
|
3404
3407
|
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
3405
|
-
* uses it via {@link
|
|
3408
|
+
* uses it via {@link close_correlation}.
|
|
3406
3409
|
*/
|
|
3407
3410
|
_correlator;
|
|
3408
3411
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -3412,7 +3415,7 @@ var Act = class {
|
|
|
3412
3415
|
_bound_query = this.query.bind(this);
|
|
3413
3416
|
_bound_query_array = this.query_array.bind(this);
|
|
3414
3417
|
_bound_forget = this.forget.bind(this);
|
|
3415
|
-
/** Reaction dispatchers built once and handed to
|
|
3418
|
+
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
3416
3419
|
_handle;
|
|
3417
3420
|
_handle_batch;
|
|
3418
3421
|
/** Declared drain lanes (ACT-1103). */
|
|
@@ -3429,16 +3432,16 @@ var Act = class {
|
|
|
3429
3432
|
*
|
|
3430
3433
|
* @param registry Schemas for every event and action across registered states
|
|
3431
3434
|
* @param states Merged map of state name → state definition
|
|
3432
|
-
* @param
|
|
3435
|
+
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
3433
3436
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
3434
3437
|
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
3435
3438
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
3436
3439
|
* instance; later slices fan out one `DrainController` per lane.
|
|
3437
3440
|
*/
|
|
3438
|
-
constructor(registry, states = /* @__PURE__ */ new Map(),
|
|
3441
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
3439
3442
|
this.registry = registry;
|
|
3440
3443
|
this._states = states;
|
|
3441
|
-
this._batch_handlers =
|
|
3444
|
+
this._batch_handlers = batch_handlers;
|
|
3442
3445
|
this._lanes = lanes;
|
|
3443
3446
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
3444
3447
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -3452,10 +3455,10 @@ var Act = class {
|
|
|
3452
3455
|
);
|
|
3453
3456
|
}
|
|
3454
3457
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
3455
|
-
this._correlator = options.correlator ??
|
|
3456
|
-
this._es =
|
|
3457
|
-
this._cd =
|
|
3458
|
-
this._handle =
|
|
3458
|
+
this._correlator = options.correlator ?? default_correlator;
|
|
3459
|
+
this._es = build_es(this._logger, this._correlator);
|
|
3460
|
+
this._cd = build_drain(this._logger);
|
|
3461
|
+
this._handle = build_handle({
|
|
3459
3462
|
logger: this._logger,
|
|
3460
3463
|
bound_do: this._bound_do,
|
|
3461
3464
|
bound_load: this._bound_load,
|
|
@@ -3463,39 +3466,39 @@ var Act = class {
|
|
|
3463
3466
|
bound_query_array: this._bound_query_array,
|
|
3464
3467
|
bound_forget: this._bound_forget
|
|
3465
3468
|
});
|
|
3466
|
-
this._handle_batch =
|
|
3469
|
+
this._handle_batch = build_handle_batch(this._logger);
|
|
3467
3470
|
const {
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
} =
|
|
3474
|
-
this._reactive_events =
|
|
3471
|
+
static_targets,
|
|
3472
|
+
has_dynamic_resolvers,
|
|
3473
|
+
reactive_events,
|
|
3474
|
+
event_to_state,
|
|
3475
|
+
event_to_lanes
|
|
3476
|
+
} = classify_registry(this.registry, this._states);
|
|
3477
|
+
this._reactive_events = reactive_events;
|
|
3475
3478
|
this._listen = options.listen !== false;
|
|
3476
3479
|
this._drain = options.drain !== false;
|
|
3477
|
-
this._event_to_state =
|
|
3478
|
-
this._event_to_lanes =
|
|
3479
|
-
const
|
|
3480
|
-
const
|
|
3481
|
-
const
|
|
3482
|
-
const
|
|
3480
|
+
this._event_to_state = event_to_state;
|
|
3481
|
+
this._event_to_lanes = event_to_lanes;
|
|
3482
|
+
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
3483
|
+
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
3484
|
+
const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
|
|
3485
|
+
const single_default_lane = active_lanes.length === 1 && active_lanes[0] === "default";
|
|
3483
3486
|
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
3484
|
-
for (const name of
|
|
3487
|
+
for (const name of active_lanes) {
|
|
3485
3488
|
const cfg = lanes.find((l) => l.name === name);
|
|
3486
3489
|
const controller = new DrainController({
|
|
3487
3490
|
logger: this._logger,
|
|
3488
3491
|
ops: this._cd,
|
|
3489
3492
|
registry: this.registry,
|
|
3490
|
-
|
|
3493
|
+
batch_handlers: this._batch_handlers,
|
|
3491
3494
|
handle: this._handle,
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
+
handle_batch: this._handle_batch,
|
|
3496
|
+
on_acked: (acked) => this.emit("acked", acked),
|
|
3497
|
+
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
3495
3498
|
// Pass lane only when a true per-lane controller is active.
|
|
3496
3499
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
3497
3500
|
// adapter SQL collapses to the pre-1103 shape.
|
|
3498
|
-
lane:
|
|
3501
|
+
lane: single_default_lane ? void 0 : name,
|
|
3499
3502
|
defaults: cfg && {
|
|
3500
3503
|
streamLimit: cfg.streamLimit,
|
|
3501
3504
|
leaseMillis: cfg.leaseMillis
|
|
@@ -3508,22 +3511,22 @@ var Act = class {
|
|
|
3508
3511
|
this._audit_deps = {
|
|
3509
3512
|
store: store2,
|
|
3510
3513
|
logger: this._logger,
|
|
3511
|
-
event_to_state
|
|
3514
|
+
event_to_state,
|
|
3512
3515
|
states: this._states,
|
|
3513
|
-
known_events: new Set(
|
|
3516
|
+
known_events: new Set(event_to_state.keys()),
|
|
3514
3517
|
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
3515
|
-
routed_events: new Set(
|
|
3518
|
+
routed_events: new Set(event_to_lanes.keys())
|
|
3516
3519
|
};
|
|
3517
3520
|
this._correlate = new CorrelateCycle(
|
|
3518
3521
|
this.registry,
|
|
3519
|
-
|
|
3520
|
-
|
|
3522
|
+
static_targets,
|
|
3523
|
+
has_dynamic_resolvers,
|
|
3521
3524
|
this._cd,
|
|
3522
3525
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3523
3526
|
// Cold start: assume drain is needed (historical events may need processing).
|
|
3524
3527
|
// #803: writer-only instances skip the cold-start arm.
|
|
3525
3528
|
() => {
|
|
3526
|
-
if (this._drain && this._reactive_events.size > 0) this.
|
|
3529
|
+
if (this._drain && this._reactive_events.size > 0) this._arm_all();
|
|
3527
3530
|
}
|
|
3528
3531
|
);
|
|
3529
3532
|
this._settle = new SettleLoop(
|
|
@@ -3533,11 +3536,11 @@ var Act = class {
|
|
|
3533
3536
|
checkpoint: () => this._correlate.checkpoint,
|
|
3534
3537
|
correlate: (q) => this.correlate(q),
|
|
3535
3538
|
drain: (o) => this.drain(o),
|
|
3536
|
-
|
|
3539
|
+
on_settled: (drain) => this.emit("settled", drain)
|
|
3537
3540
|
},
|
|
3538
3541
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
3539
3542
|
);
|
|
3540
|
-
this._notify_disposer = this.
|
|
3543
|
+
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store2());
|
|
3541
3544
|
dispose(() => this.shutdown());
|
|
3542
3545
|
}
|
|
3543
3546
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
@@ -3571,7 +3574,7 @@ var Act = class {
|
|
|
3571
3574
|
* subscription was made). Errors during subscription are logged but
|
|
3572
3575
|
* never thrown — `notify` is a hint, not a contract.
|
|
3573
3576
|
*/
|
|
3574
|
-
async
|
|
3577
|
+
async _wire_notify(s) {
|
|
3575
3578
|
if (this._reactive_events.size === 0) return void 0;
|
|
3576
3579
|
if (!s.notify) return void 0;
|
|
3577
3580
|
if (!this._listen) return void 0;
|
|
@@ -3580,7 +3583,7 @@ var Act = class {
|
|
|
3580
3583
|
try {
|
|
3581
3584
|
this.emit("notified", notification);
|
|
3582
3585
|
if (this._drain) {
|
|
3583
|
-
const armed = this.
|
|
3586
|
+
const armed = this._arm_for_event_names(
|
|
3584
3587
|
notification.events.map((e) => e.name)
|
|
3585
3588
|
);
|
|
3586
3589
|
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
@@ -3686,7 +3689,7 @@ var Act = class {
|
|
|
3686
3689
|
skipValidation
|
|
3687
3690
|
);
|
|
3688
3691
|
if (this._reactive_events.size > 0)
|
|
3689
|
-
this.
|
|
3692
|
+
this._arm_for_event_names(
|
|
3690
3693
|
snapshots.map((s) => s.event.name)
|
|
3691
3694
|
);
|
|
3692
3695
|
this.emit("committed", snapshots);
|
|
@@ -3865,28 +3868,28 @@ var Act = class {
|
|
|
3865
3868
|
async drain(options = {}) {
|
|
3866
3869
|
if (!this._drain)
|
|
3867
3870
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
3868
|
-
return this._scoped(() => this.
|
|
3871
|
+
return this._scoped(() => this._drain_all(options));
|
|
3869
3872
|
}
|
|
3870
3873
|
/** Arm every active lane controller (ACT-1103). */
|
|
3871
|
-
|
|
3874
|
+
_arm_all() {
|
|
3872
3875
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
3873
3876
|
}
|
|
3874
3877
|
/**
|
|
3875
3878
|
* Arm only the lane controllers whose reactions match the supplied
|
|
3876
3879
|
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
3877
|
-
* resolver fall back to `
|
|
3880
|
+
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
3878
3881
|
* resolver's lane isn't known until correlate runs the function.
|
|
3879
3882
|
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
3880
3883
|
* carry them. Returns true when any controller was armed (used by
|
|
3881
3884
|
* the notify handler to decide whether to schedule a settle).
|
|
3882
3885
|
*/
|
|
3883
|
-
|
|
3886
|
+
_arm_for_event_names(names) {
|
|
3884
3887
|
const to_arm = /* @__PURE__ */ new Set();
|
|
3885
3888
|
for (const name of names) {
|
|
3886
3889
|
const set = this._event_to_lanes.get(name);
|
|
3887
3890
|
if (set === void 0) continue;
|
|
3888
3891
|
if (set === ALL_LANES) {
|
|
3889
|
-
this.
|
|
3892
|
+
this._arm_all();
|
|
3890
3893
|
return true;
|
|
3891
3894
|
}
|
|
3892
3895
|
for (const lane of set) to_arm.add(lane);
|
|
@@ -3903,7 +3906,7 @@ var Act = class {
|
|
|
3903
3906
|
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
3904
3907
|
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
3905
3908
|
* `lease.lane`. */
|
|
3906
|
-
async
|
|
3909
|
+
async _drain_all(options) {
|
|
3907
3910
|
const results = await Promise.all(
|
|
3908
3911
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
3909
3912
|
);
|
|
@@ -4023,7 +4026,7 @@ var Act = class {
|
|
|
4023
4026
|
* @see {@link stop_correlations} to stop the worker
|
|
4024
4027
|
*/
|
|
4025
4028
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
4026
|
-
return this._correlate.
|
|
4029
|
+
return this._correlate.start_polling(query, frequency, callback);
|
|
4027
4030
|
}
|
|
4028
4031
|
/**
|
|
4029
4032
|
* Stops the automatic correlation worker.
|
|
@@ -4043,7 +4046,7 @@ var Act = class {
|
|
|
4043
4046
|
* @see {@link start_correlations}
|
|
4044
4047
|
*/
|
|
4045
4048
|
stop_correlations() {
|
|
4046
|
-
this._correlate.
|
|
4049
|
+
this._correlate.stop_polling();
|
|
4047
4050
|
}
|
|
4048
4051
|
/**
|
|
4049
4052
|
* Cancels any pending or active settle cycle.
|
|
@@ -4089,7 +4092,7 @@ var Act = class {
|
|
|
4089
4092
|
async reset(input) {
|
|
4090
4093
|
return this._scoped(async () => {
|
|
4091
4094
|
const count = await store2().reset(input);
|
|
4092
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
4095
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
4093
4096
|
return count;
|
|
4094
4097
|
});
|
|
4095
4098
|
}
|
|
@@ -4123,7 +4126,7 @@ var Act = class {
|
|
|
4123
4126
|
async unblock(input) {
|
|
4124
4127
|
return this._scoped(async () => {
|
|
4125
4128
|
const count = await store2().unblock(input);
|
|
4126
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
4129
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
4127
4130
|
return count;
|
|
4128
4131
|
});
|
|
4129
4132
|
}
|
|
@@ -4349,14 +4352,14 @@ var Act = class {
|
|
|
4349
4352
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
4350
4353
|
return this._scoped(async () => {
|
|
4351
4354
|
await this.correlate({ limit: 1e3 });
|
|
4352
|
-
const
|
|
4353
|
-
const result = await
|
|
4354
|
-
|
|
4355
|
-
|
|
4355
|
+
const close_actor = { id: "$close", name: "close" };
|
|
4356
|
+
const result = await run_close_cycle(targets, {
|
|
4357
|
+
reactive_events_size: this._reactive_events.size,
|
|
4358
|
+
event_to_state: this._event_to_state,
|
|
4356
4359
|
load: this._es.load,
|
|
4357
4360
|
tombstone: this._es.tombstone,
|
|
4358
4361
|
logger: this._logger,
|
|
4359
|
-
correlation:
|
|
4362
|
+
correlation: close_correlation(this._correlator, close_actor)
|
|
4360
4363
|
});
|
|
4361
4364
|
this.emit("closed", result);
|
|
4362
4365
|
return result;
|
|
@@ -4399,17 +4402,17 @@ var Act = class {
|
|
|
4399
4402
|
};
|
|
4400
4403
|
|
|
4401
4404
|
// src/builders/act-builder.ts
|
|
4402
|
-
function
|
|
4405
|
+
function register_batch_handler(proj, batch_handlers) {
|
|
4403
4406
|
if (!proj.batchHandler || !proj.target) return;
|
|
4404
|
-
const existing =
|
|
4407
|
+
const existing = batch_handlers.get(proj.target);
|
|
4405
4408
|
if (existing && existing !== proj.batchHandler) {
|
|
4406
4409
|
throw new Error(`Duplicate batch handler for target "${proj.target}"`);
|
|
4407
4410
|
}
|
|
4408
|
-
|
|
4411
|
+
batch_handlers.set(proj.target, proj.batchHandler);
|
|
4409
4412
|
}
|
|
4410
|
-
function
|
|
4413
|
+
function validate_lane_references(registry, lanes) {
|
|
4411
4414
|
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
4412
|
-
for (const [
|
|
4415
|
+
for (const [event_name, def] of Object.entries(registry.events)) {
|
|
4413
4416
|
const entry = def;
|
|
4414
4417
|
for (const [handlerName, reaction] of entry.reactions) {
|
|
4415
4418
|
const resolver = reaction.resolver;
|
|
@@ -4417,7 +4420,7 @@ function validateLaneReferences(registry, lanes) {
|
|
|
4417
4420
|
const lane = resolver.lane;
|
|
4418
4421
|
if (lane && !declared.has(lane)) {
|
|
4419
4422
|
throw new Error(
|
|
4420
|
-
`Reaction "${handlerName}" on "${
|
|
4423
|
+
`Reaction "${handlerName}" on "${event_name}" targets undeclared lane "${lane}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. Add \`.withLane({ name: "${lane}", ... })\` to act() or correct the .to() declaration.`
|
|
4421
4424
|
);
|
|
4422
4425
|
}
|
|
4423
4426
|
}
|
|
@@ -4430,75 +4433,75 @@ function act() {
|
|
|
4430
4433
|
const registry = {
|
|
4431
4434
|
actions: {},
|
|
4432
4435
|
events: {},
|
|
4433
|
-
sensitive_fields: (
|
|
4434
|
-
disclosure_predicate: (
|
|
4436
|
+
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
4437
|
+
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null
|
|
4435
4438
|
};
|
|
4436
|
-
const
|
|
4437
|
-
const
|
|
4439
|
+
const pending_projections = [];
|
|
4440
|
+
const batch_handlers = /* @__PURE__ */ new Map();
|
|
4438
4441
|
const lanes = [];
|
|
4439
4442
|
let _built = false;
|
|
4440
|
-
const
|
|
4441
|
-
const
|
|
4443
|
+
const finalize_deprecations = () => {
|
|
4444
|
+
const deprecation_summary = [];
|
|
4442
4445
|
for (const state2 of states.values()) {
|
|
4443
|
-
const
|
|
4444
|
-
const deprecated =
|
|
4446
|
+
const event_names = Object.keys(state2.events);
|
|
4447
|
+
const deprecated = deprecated_event_names(event_names);
|
|
4445
4448
|
if (deprecated.size === 0) continue;
|
|
4446
4449
|
state2._deprecated = deprecated;
|
|
4447
4450
|
for (const name of deprecated) {
|
|
4448
|
-
const current =
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
+
const current = current_version_of(name, event_names);
|
|
4452
|
+
deprecation_summary.push({
|
|
4453
|
+
state_name: state2.name,
|
|
4451
4454
|
deprecated: name,
|
|
4452
4455
|
current
|
|
4453
4456
|
});
|
|
4454
4457
|
}
|
|
4455
|
-
for (const [
|
|
4456
|
-
const
|
|
4457
|
-
if (
|
|
4458
|
-
const current =
|
|
4458
|
+
for (const [action_name, handler] of Object.entries(state2.on)) {
|
|
4459
|
+
const static_target = handler?._static_emit;
|
|
4460
|
+
if (static_target && deprecated.has(static_target)) {
|
|
4461
|
+
const current = current_version_of(static_target, event_names);
|
|
4459
4462
|
throw new Error(
|
|
4460
|
-
`Action "${
|
|
4463
|
+
`Action "${action_name}" in state "${state2.name}" emits deprecated event "${static_target}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${static_target}" stays as-is \u2014 historical events still need it.`
|
|
4461
4464
|
);
|
|
4462
4465
|
}
|
|
4463
4466
|
}
|
|
4464
4467
|
}
|
|
4465
|
-
if (
|
|
4466
|
-
const list =
|
|
4467
|
-
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.
|
|
4468
|
+
if (deprecation_summary.length > 0) {
|
|
4469
|
+
const list = deprecation_summary.map(
|
|
4470
|
+
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.state_name}")`
|
|
4468
4471
|
).join(", ");
|
|
4469
4472
|
log().info(
|
|
4470
|
-
`Act registered ${
|
|
4473
|
+
`Act registered ${deprecation_summary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
|
|
4471
4474
|
);
|
|
4472
4475
|
}
|
|
4473
4476
|
};
|
|
4474
4477
|
const builder = {
|
|
4475
4478
|
withState: (state2) => {
|
|
4476
|
-
|
|
4479
|
+
register_state(state2, states, registry.actions, registry.events);
|
|
4477
4480
|
return builder;
|
|
4478
4481
|
},
|
|
4479
4482
|
withSlice: (input) => {
|
|
4480
4483
|
for (const s of input.states.values()) {
|
|
4481
|
-
|
|
4484
|
+
register_state(s, states, registry.actions, registry.events);
|
|
4482
4485
|
}
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
for (const
|
|
4486
|
-
const existing = lanes.find((l) => l.name ===
|
|
4486
|
+
merge_event_register(registry.events, input.events);
|
|
4487
|
+
pending_projections.push(...input.projections);
|
|
4488
|
+
for (const slice_lane of input.lanes) {
|
|
4489
|
+
const existing = lanes.find((l) => l.name === slice_lane.name);
|
|
4487
4490
|
if (!existing) {
|
|
4488
|
-
lanes.push(
|
|
4491
|
+
lanes.push(slice_lane);
|
|
4489
4492
|
continue;
|
|
4490
4493
|
}
|
|
4491
|
-
if (existing.leaseMillis !==
|
|
4494
|
+
if (existing.leaseMillis !== slice_lane.leaseMillis || existing.streamLimit !== slice_lane.streamLimit || existing.cycleMs !== slice_lane.cycleMs) {
|
|
4492
4495
|
throw new Error(
|
|
4493
|
-
`Lane "${
|
|
4496
|
+
`Lane "${slice_lane.name}" was already declared with a different config`
|
|
4494
4497
|
);
|
|
4495
4498
|
}
|
|
4496
4499
|
}
|
|
4497
4500
|
return builder;
|
|
4498
4501
|
},
|
|
4499
4502
|
withProjection: (proj) => {
|
|
4500
|
-
|
|
4501
|
-
|
|
4503
|
+
merge_projection(proj, registry.events);
|
|
4504
|
+
register_batch_handler(proj, batch_handlers);
|
|
4502
4505
|
return builder;
|
|
4503
4506
|
},
|
|
4504
4507
|
withActor: () => builder,
|
|
@@ -4536,18 +4539,18 @@ function act() {
|
|
|
4536
4539
|
}),
|
|
4537
4540
|
build: (options) => {
|
|
4538
4541
|
if (!_built) {
|
|
4539
|
-
for (const proj of
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
+
for (const proj of pending_projections) {
|
|
4543
|
+
merge_projection(proj, registry.events);
|
|
4544
|
+
register_batch_handler(proj, batch_handlers);
|
|
4542
4545
|
}
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
for (const [
|
|
4546
|
+
finalize_deprecations();
|
|
4547
|
+
validate_lane_references(registry, lanes);
|
|
4548
|
+
for (const [event_name, reg] of Object.entries(
|
|
4546
4549
|
registry.events
|
|
4547
4550
|
)) {
|
|
4548
4551
|
const fields = pii_fields(reg.schema);
|
|
4549
4552
|
if (fields.length === 0) continue;
|
|
4550
|
-
_sf.set(
|
|
4553
|
+
_sf.set(event_name, fields);
|
|
4551
4554
|
for (const [name, reaction] of reg.reactions) {
|
|
4552
4555
|
const inner = reaction.handler;
|
|
4553
4556
|
const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
|
|
@@ -4559,9 +4562,9 @@ function act() {
|
|
|
4559
4562
|
for (const state2 of states.values()) {
|
|
4560
4563
|
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
4561
4564
|
const fields_by_event = /* @__PURE__ */ new Map();
|
|
4562
|
-
for (const
|
|
4563
|
-
const fields = _sf.get(
|
|
4564
|
-
if (fields) fields_by_event.set(
|
|
4565
|
+
for (const event_name of Object.keys(state2.events)) {
|
|
4566
|
+
const fields = _sf.get(event_name);
|
|
4567
|
+
if (fields) fields_by_event.set(event_name, fields);
|
|
4565
4568
|
}
|
|
4566
4569
|
if (fields_by_event.size === 0) continue;
|
|
4567
4570
|
if (state2.snap) {
|
|
@@ -4579,12 +4582,12 @@ function act() {
|
|
|
4579
4582
|
const fields = fields_by_event.get(validated.name);
|
|
4580
4583
|
return fields ? pii_split(validated, fields) : validated;
|
|
4581
4584
|
};
|
|
4582
|
-
for (const [
|
|
4583
|
-
const original = state2.patch[
|
|
4584
|
-
state2.patch[
|
|
4585
|
+
for (const [event_name, fields] of fields_by_event) {
|
|
4586
|
+
const original = state2.patch[event_name];
|
|
4587
|
+
state2.patch[event_name] = (event, s) => original(pii_merge(event, fields), s);
|
|
4585
4588
|
}
|
|
4586
4589
|
}
|
|
4587
|
-
for (const [target, original] of
|
|
4590
|
+
for (const [target, original] of batch_handlers) {
|
|
4588
4591
|
const wrapped = async (events, stream) => {
|
|
4589
4592
|
const stripped = events.map((e) => {
|
|
4590
4593
|
const f = _sf.get(e.name);
|
|
@@ -4592,14 +4595,14 @@ function act() {
|
|
|
4592
4595
|
});
|
|
4593
4596
|
return original(stripped, stream);
|
|
4594
4597
|
};
|
|
4595
|
-
|
|
4598
|
+
batch_handlers.set(target, wrapped);
|
|
4596
4599
|
}
|
|
4597
4600
|
_built = true;
|
|
4598
4601
|
}
|
|
4599
4602
|
return new Act(
|
|
4600
4603
|
registry,
|
|
4601
4604
|
states,
|
|
4602
|
-
|
|
4605
|
+
batch_handlers,
|
|
4603
4606
|
options,
|
|
4604
4607
|
lanes
|
|
4605
4608
|
);
|
|
@@ -4612,7 +4615,7 @@ function act() {
|
|
|
4612
4615
|
// src/builders/projection-builder.ts
|
|
4613
4616
|
function _projection(target) {
|
|
4614
4617
|
const events = {};
|
|
4615
|
-
const
|
|
4618
|
+
const default_resolver = typeof target === "string" ? { target } : void 0;
|
|
4616
4619
|
const base = {
|
|
4617
4620
|
on: (entry) => {
|
|
4618
4621
|
const keys = Object.keys(entry);
|
|
@@ -4629,7 +4632,7 @@ function _projection(target) {
|
|
|
4629
4632
|
do: (handler) => {
|
|
4630
4633
|
const reaction = {
|
|
4631
4634
|
handler,
|
|
4632
|
-
resolver:
|
|
4635
|
+
resolver: default_resolver ?? _this_,
|
|
4633
4636
|
options: {
|
|
4634
4637
|
blockOnError: true,
|
|
4635
4638
|
maxRetries: 3
|
|
@@ -4685,7 +4688,7 @@ function slice() {
|
|
|
4685
4688
|
const lanes = [];
|
|
4686
4689
|
const builder = {
|
|
4687
4690
|
withState: (state2) => {
|
|
4688
|
-
|
|
4691
|
+
register_state(state2, states, actions, events);
|
|
4689
4692
|
return builder;
|
|
4690
4693
|
},
|
|
4691
4694
|
withProjection: (proj) => {
|
|
@@ -4741,12 +4744,12 @@ function state(entry) {
|
|
|
4741
4744
|
const keys = Object.keys(entry);
|
|
4742
4745
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
4743
4746
|
const name = keys[0];
|
|
4744
|
-
const
|
|
4747
|
+
const state_schema = entry[name];
|
|
4745
4748
|
return {
|
|
4746
4749
|
init(init) {
|
|
4747
4750
|
return {
|
|
4748
4751
|
emits(events) {
|
|
4749
|
-
const
|
|
4752
|
+
const default_patch = Object.fromEntries(
|
|
4750
4753
|
Object.keys(events).map((k) => {
|
|
4751
4754
|
const fn = Object.assign(({ data }) => data, {
|
|
4752
4755
|
_passthrough: true
|
|
@@ -4757,10 +4760,10 @@ function state(entry) {
|
|
|
4757
4760
|
const internal = {
|
|
4758
4761
|
events,
|
|
4759
4762
|
actions: {},
|
|
4760
|
-
state:
|
|
4763
|
+
state: state_schema,
|
|
4761
4764
|
name,
|
|
4762
4765
|
init,
|
|
4763
|
-
patch:
|
|
4766
|
+
patch: default_patch,
|
|
4764
4767
|
on: {},
|
|
4765
4768
|
// Step delegates initialized as identity. `act().build()`
|
|
4766
4769
|
// overrides on states with `sensitive(...)` events to bake in
|
|
@@ -4802,11 +4805,14 @@ function action_builder(state2) {
|
|
|
4802
4805
|
}
|
|
4803
4806
|
function emit(handler) {
|
|
4804
4807
|
if (typeof handler === "string") {
|
|
4805
|
-
const
|
|
4806
|
-
const
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4808
|
+
const event_name = handler;
|
|
4809
|
+
const emit_fn = Object.assign(
|
|
4810
|
+
(payload) => [event_name, payload],
|
|
4811
|
+
{
|
|
4812
|
+
_static_emit: event_name
|
|
4813
|
+
}
|
|
4814
|
+
);
|
|
4815
|
+
internal.on[action2] = emit_fn;
|
|
4810
4816
|
} else {
|
|
4811
4817
|
internal.on[action2] = handler;
|
|
4812
4818
|
}
|
|
@@ -4859,7 +4865,7 @@ var CsvFile = class {
|
|
|
4859
4865
|
let header = null;
|
|
4860
4866
|
for await (const line of lines) {
|
|
4861
4867
|
if (!line.trim()) continue;
|
|
4862
|
-
const fields =
|
|
4868
|
+
const fields = parse_csv_line(line);
|
|
4863
4869
|
if (!header) {
|
|
4864
4870
|
header = fields;
|
|
4865
4871
|
const expected = CSV_COLUMNS.join(",");
|
|
@@ -4896,21 +4902,21 @@ var CsvFile = class {
|
|
|
4896
4902
|
flags: "w",
|
|
4897
4903
|
encoding: "utf8"
|
|
4898
4904
|
});
|
|
4899
|
-
let
|
|
4905
|
+
let next_id = 1;
|
|
4900
4906
|
try {
|
|
4901
|
-
await
|
|
4907
|
+
await write_line(writer, CSV_COLUMNS.join(","));
|
|
4902
4908
|
await driver(async (event) => {
|
|
4903
|
-
const id =
|
|
4909
|
+
const id = next_id++;
|
|
4904
4910
|
const row = [
|
|
4905
4911
|
String(id),
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4912
|
+
csv_escape(event.name),
|
|
4913
|
+
csv_escape(JSON.stringify(event.data)),
|
|
4914
|
+
csv_escape(event.stream),
|
|
4909
4915
|
String(event.version),
|
|
4910
4916
|
event.created.toISOString(),
|
|
4911
|
-
|
|
4917
|
+
csv_escape(JSON.stringify(event.meta))
|
|
4912
4918
|
].join(",");
|
|
4913
|
-
await
|
|
4919
|
+
await write_line(writer, row);
|
|
4914
4920
|
return id;
|
|
4915
4921
|
});
|
|
4916
4922
|
} finally {
|
|
@@ -4943,7 +4949,7 @@ async function* linesFromBlob(blob) {
|
|
|
4943
4949
|
await Promise.resolve();
|
|
4944
4950
|
}
|
|
4945
4951
|
}
|
|
4946
|
-
function
|
|
4952
|
+
function parse_csv_line(line) {
|
|
4947
4953
|
const fields = [];
|
|
4948
4954
|
let i = 0;
|
|
4949
4955
|
while (i < line.length) {
|
|
@@ -4976,11 +4982,11 @@ function parseCsvLine(line) {
|
|
|
4976
4982
|
}
|
|
4977
4983
|
return fields;
|
|
4978
4984
|
}
|
|
4979
|
-
function
|
|
4985
|
+
function csv_escape(value) {
|
|
4980
4986
|
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
4981
4987
|
return value;
|
|
4982
4988
|
}
|
|
4983
|
-
function
|
|
4989
|
+
function write_line(writer, line) {
|
|
4984
4990
|
return new Promise((resolve, reject) => {
|
|
4985
4991
|
writer.write(`${line}
|
|
4986
4992
|
`, (err) => {
|