@rotorsoft/act 1.37.6 → 1.37.8
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/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-reaction.d.ts +8 -0
- package/dist/@types/internal/autoclose-reaction.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/date-reviver.d.ts +6 -3
- package/dist/@types/internal/date-reviver.d.ts.map +1 -1
- package/dist/@types/internal/drain-ops.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/reaction-dispatch.d.ts +6 -0
- package/dist/@types/internal/reaction-dispatch.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +5 -6
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/utils.d.ts +18 -1
- package/dist/@types/utils.d.ts.map +1 -1
- package/dist/{chunk-PRDX3HP6.cjs → chunk-4UWU5FAN.cjs} +25 -24
- package/dist/chunk-4UWU5FAN.cjs.map +1 -0
- package/dist/{chunk-H6KVPVP4.cjs → chunk-BCA423WP.cjs} +1 -1
- package/dist/chunk-BCA423WP.cjs.map +1 -0
- package/dist/{chunk-V5ZB54DC.js → chunk-I5RF2NFR.js} +1 -1
- package/dist/chunk-I5RF2NFR.js.map +1 -0
- package/dist/{chunk-HXW3TPP6.js → chunk-TSDQDT4S.js} +13 -12
- package/dist/chunk-TSDQDT4S.js.map +1 -0
- package/dist/index.cjs +1665 -1608
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1276 -1219
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +4 -4
- package/dist/test/index.js +2 -2
- package/dist/types/index.cjs +2 -2
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-H6KVPVP4.cjs.map +0 -1
- package/dist/chunk-HXW3TPP6.js.map +0 -1
- package/dist/chunk-PRDX3HP6.cjs.map +0 -1
- package/dist/chunk-V5ZB54DC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DEFAULT_EVENT_LIMIT,
|
|
6
6
|
DEFAULT_LEASE_MILLIS,
|
|
7
7
|
DEFAULT_MAX_RETRIES,
|
|
8
|
+
DEFAULT_QUERY_STREAMS_LIMIT,
|
|
8
9
|
DEFAULT_STREAM_LIMIT,
|
|
9
10
|
InMemoryCache,
|
|
10
11
|
InMemoryStore,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
dispose,
|
|
21
22
|
disposeAndExit,
|
|
22
23
|
extend,
|
|
24
|
+
fair_slots,
|
|
23
25
|
is_literal_source,
|
|
24
26
|
log,
|
|
25
27
|
make_reaction_scope,
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
sleep,
|
|
41
43
|
store,
|
|
42
44
|
validate
|
|
43
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-TSDQDT4S.js";
|
|
44
46
|
import {
|
|
45
47
|
ActorSchema,
|
|
46
48
|
CausationEventSchema,
|
|
@@ -74,7 +76,7 @@ import {
|
|
|
74
76
|
pii_fields,
|
|
75
77
|
pii_schemas,
|
|
76
78
|
sensitive
|
|
77
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-I5RF2NFR.js";
|
|
78
80
|
|
|
79
81
|
// src/signals.ts
|
|
80
82
|
process.once("SIGINT", async (arg) => {
|
|
@@ -416,7 +418,9 @@ var make_restart_candidate_pass = (deps, options) => {
|
|
|
416
418
|
stream,
|
|
417
419
|
stream_exact: true,
|
|
418
420
|
names: [SNAP_EVENT],
|
|
419
|
-
with_snaps: true
|
|
421
|
+
with_snaps: true,
|
|
422
|
+
// From the start: on its own, `with_snaps` resumes at the latest.
|
|
423
|
+
after: -1
|
|
420
424
|
}
|
|
421
425
|
);
|
|
422
426
|
findings.push({
|
|
@@ -573,7 +577,7 @@ var make_correlation_gaps_pass = () => {
|
|
|
573
577
|
seen_ids.add(e.id);
|
|
574
578
|
const causation = e.meta?.causation;
|
|
575
579
|
const parent_id = causation?.event?.id;
|
|
576
|
-
if (parent_id !== void 0) {
|
|
580
|
+
if (parent_id !== void 0 && e.name !== TOMBSTONE_EVENT) {
|
|
577
581
|
checks.push({ stream: e.stream, id: e.id, parent_id });
|
|
578
582
|
}
|
|
579
583
|
},
|
|
@@ -795,674 +799,303 @@ function classify_registry(registry, states) {
|
|
|
795
799
|
};
|
|
796
800
|
}
|
|
797
801
|
|
|
798
|
-
// src/internal/
|
|
799
|
-
|
|
800
|
-
var
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
await correlate(CLOSE_CATCH_UP_LIMIT);
|
|
806
|
-
if (cursor.checkpoint <= before) break;
|
|
807
|
-
}
|
|
808
|
-
return cursor.checkpoint;
|
|
802
|
+
// src/internal/autoclose-policy.ts
|
|
803
|
+
import { z } from "zod";
|
|
804
|
+
var DAY_MS = 864e5;
|
|
805
|
+
var MIN_AFTER_DAYS = 1 / 1440;
|
|
806
|
+
var MIN_KEEP_DAYS = 1;
|
|
807
|
+
function days_after(date, days) {
|
|
808
|
+
return new Date(date.getTime() + days * DAY_MS);
|
|
809
809
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
810
|
+
function days_before_now(days) {
|
|
811
|
+
return new Date(Date.now() - days * DAY_MS);
|
|
812
|
+
}
|
|
813
|
+
var window_schema = (field, min_days, floor) => z.object({
|
|
814
|
+
days: z.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
|
|
815
|
+
}).refine((o) => o.days >= min_days, {
|
|
816
|
+
message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
|
|
817
|
+
});
|
|
818
|
+
var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
|
|
819
|
+
var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
|
|
820
|
+
var IsSchema = z.union([
|
|
821
|
+
z.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
822
|
+
z.array(
|
|
823
|
+
z.string().min(1, "autocloses: is entries must be non-empty event names")
|
|
824
|
+
).min(1, "autocloses: is must include at least one event name").readonly()
|
|
825
|
+
]);
|
|
826
|
+
var ReachesSchema = z.number({ message: "autocloses: reaches must be a number" }).int("autocloses: reaches must be an integer").min(1, "autocloses: reaches must be >= 1");
|
|
827
|
+
var OrBlockSchema = z.object({
|
|
828
|
+
after: AfterSchema.optional(),
|
|
829
|
+
is: IsSchema.optional(),
|
|
830
|
+
reaches: ReachesSchema.optional()
|
|
831
|
+
}).strict().refine(
|
|
832
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
833
|
+
{
|
|
834
|
+
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
822
835
|
}
|
|
823
|
-
|
|
824
|
-
var
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
836
|
+
);
|
|
837
|
+
var AutoclosePolicySchema = z.object({
|
|
838
|
+
after: AfterSchema.optional(),
|
|
839
|
+
is: IsSchema.optional(),
|
|
840
|
+
reaches: ReachesSchema.optional(),
|
|
841
|
+
or: OrBlockSchema.optional(),
|
|
842
|
+
keep: KeepSchema.optional()
|
|
843
|
+
}).strict().refine(
|
|
844
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
|
|
845
|
+
{
|
|
846
|
+
message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
|
|
832
847
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
);
|
|
836
|
-
const full = [...target_map.values()].filter((t) => t.before === void 0);
|
|
837
|
-
const skipped = [];
|
|
838
|
-
const windowed_result = windowed.length ? await run_windowed_closes(windowed, deps, skipped) : /* @__PURE__ */ new Map();
|
|
839
|
-
if (!full.length) return { truncated: windowed_result, skipped };
|
|
840
|
-
const truncated = await with_stream_locks(
|
|
841
|
-
full.map((t) => t.stream),
|
|
842
|
-
deps,
|
|
843
|
-
() => run_full_closes(full, target_map, deps, skipped)
|
|
844
|
-
);
|
|
845
|
-
const retired = [...truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
|
|
846
|
-
if (retired.length) deps.forget_subscribed?.(retired);
|
|
847
|
-
for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
|
|
848
|
-
return { truncated, skipped };
|
|
848
|
+
);
|
|
849
|
+
function compile_after(after) {
|
|
850
|
+
return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
|
|
849
851
|
}
|
|
850
|
-
function
|
|
851
|
-
const
|
|
852
|
-
return
|
|
853
|
-
(inner, stream) => () => with_lock(stream, inner),
|
|
854
|
-
work
|
|
855
|
-
)();
|
|
852
|
+
function compile_is(is) {
|
|
853
|
+
const set = new Set(typeof is === "string" ? [is] : is);
|
|
854
|
+
return (_stream, head) => set.has(head.name);
|
|
856
855
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const streams = full.map((t) => t.stream);
|
|
860
|
-
const stream_info = await scan_stream_heads(streams);
|
|
861
|
-
for (const target of full) {
|
|
862
|
-
if (!target.restart) continue;
|
|
863
|
-
const info = stream_info.get(target.stream);
|
|
864
|
-
if (!info) continue;
|
|
865
|
-
const owner = deps.event_to_state.get(info.last_event_name);
|
|
866
|
-
if (owner?.pii_aware) {
|
|
867
|
-
deps.logger.error(
|
|
868
|
-
`Refusing to close "${target.stream}" with restart: state "${owner.name}" carries sensitive fields, so a restart seed would persist redacted values while deleting the originals. Close it without restart to retire the stream, or leave it open.`
|
|
869
|
-
);
|
|
870
|
-
skipped.push(target.stream);
|
|
871
|
-
stream_info.delete(target.stream);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const safe = await partition_by_safety(
|
|
875
|
-
stream_info,
|
|
876
|
-
deps.reactive,
|
|
877
|
-
skipped,
|
|
878
|
-
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
|
|
879
|
-
deps.catch_up_correlation
|
|
880
|
-
);
|
|
881
|
-
if (!safe.length) return none;
|
|
882
|
-
const { guarded, guard_events } = await guard_with_tombstones(
|
|
883
|
-
safe,
|
|
884
|
-
stream_info,
|
|
885
|
-
deps.correlation,
|
|
886
|
-
deps.tombstone,
|
|
887
|
-
skipped
|
|
888
|
-
);
|
|
889
|
-
if (!guarded.length) return none;
|
|
890
|
-
const seed_states = await load_restart_seeds(
|
|
891
|
-
guarded,
|
|
892
|
-
target_map,
|
|
893
|
-
stream_info,
|
|
894
|
-
deps.event_to_state,
|
|
895
|
-
deps.load,
|
|
896
|
-
deps.logger
|
|
897
|
-
);
|
|
898
|
-
await run_archive_callbacks(guarded, target_map);
|
|
899
|
-
const still_guarded = await heads_still_guarded(
|
|
900
|
-
guarded,
|
|
901
|
-
guard_events,
|
|
902
|
-
skipped
|
|
903
|
-
);
|
|
904
|
-
if (!still_guarded.length) return none;
|
|
905
|
-
return truncate_and_warm_cache(
|
|
906
|
-
still_guarded,
|
|
907
|
-
seed_states,
|
|
908
|
-
guard_events,
|
|
909
|
-
deps.correlation
|
|
910
|
-
);
|
|
856
|
+
function compile_reaches(reaches) {
|
|
857
|
+
return (_stream, _head, count) => count >= reaches;
|
|
911
858
|
}
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
) : /* @__PURE__ */ new Map();
|
|
919
|
-
const with_lock = deps.with_stream_lock ?? ((_stream, work) => work());
|
|
920
|
-
const truncated = /* @__PURE__ */ new Map();
|
|
921
|
-
for (const t of windowed) {
|
|
922
|
-
const entry = await with_lock(t.stream, async () => {
|
|
923
|
-
const max_id = min_at.get(t.stream);
|
|
924
|
-
if (!await windowed_prune_pending(t.stream, t.before, max_id))
|
|
925
|
-
return void 0;
|
|
926
|
-
if (t.archive) await t.archive();
|
|
927
|
-
const result = await store().truncate([
|
|
928
|
-
{ stream: t.stream, before: t.before, max_id }
|
|
929
|
-
]);
|
|
930
|
-
return result.get(t.stream);
|
|
931
|
-
});
|
|
932
|
-
if (entry) truncated.set(t.stream, entry);
|
|
933
|
-
else skipped.push(t.stream);
|
|
934
|
-
}
|
|
935
|
-
return truncated;
|
|
859
|
+
function policy_min_after_days(options) {
|
|
860
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
861
|
+
const windows = [];
|
|
862
|
+
if (parsed.after) windows.push(parsed.after.days);
|
|
863
|
+
if (parsed.or?.after) windows.push(parsed.or.after.days);
|
|
864
|
+
return windows.length ? Math.min(...windows) : void 0;
|
|
936
865
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
await store().query(
|
|
941
|
-
(event) => {
|
|
942
|
-
if (min_id === void 0 || event.id < min_id) min_id = event.id;
|
|
943
|
-
if (event.name === SNAP_EVENT && event.created < before && (max_id === void 0 || event.id <= max_id) && (boundary_id === void 0 || event.id > boundary_id))
|
|
944
|
-
boundary_id = event.id;
|
|
945
|
-
},
|
|
946
|
-
{ stream, stream_exact: true, with_snaps: true, after: -1 }
|
|
947
|
-
);
|
|
948
|
-
return boundary_id !== void 0 && min_id < boundary_id;
|
|
866
|
+
function policy_after_windows(options) {
|
|
867
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
868
|
+
return [parsed.after, parsed.or?.after].flatMap((w) => w ? [w.days] : []);
|
|
949
869
|
}
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
await walk_streams(
|
|
954
|
-
store(),
|
|
955
|
-
(position) => {
|
|
956
|
-
const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
|
|
957
|
-
const cap = pending ? position.at : Math.max(position.at, checkpoint);
|
|
958
|
-
for (const stream of streams) {
|
|
959
|
-
if (matches(position.source, stream)) {
|
|
960
|
-
const prev = min_at.get(stream);
|
|
961
|
-
if (prev === void 0 || cap < prev) min_at.set(stream, cap);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
{ limit: page_size, source_matches: streams }
|
|
966
|
-
);
|
|
967
|
-
return min_at;
|
|
870
|
+
function policy_keep_days(options) {
|
|
871
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
872
|
+
return parsed.keep?.days;
|
|
968
873
|
}
|
|
969
|
-
function
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
874
|
+
function compile_autoclose_policy(options) {
|
|
875
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
876
|
+
const and_preds = [];
|
|
877
|
+
if (parsed.after) and_preds.push(compile_after(parsed.after));
|
|
878
|
+
if (parsed.is) and_preds.push(compile_is(parsed.is));
|
|
879
|
+
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
880
|
+
const or_preds = [];
|
|
881
|
+
if (parsed.or) {
|
|
882
|
+
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
883
|
+
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
884
|
+
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
885
|
+
}
|
|
886
|
+
return (stream, head, count) => {
|
|
887
|
+
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
888
|
+
return true;
|
|
977
889
|
}
|
|
978
|
-
|
|
890
|
+
if (or_preds.some((p) => p(stream, head, count))) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
return false;
|
|
979
894
|
};
|
|
980
895
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
for (const [stream, { head }] of domain_heads) {
|
|
991
|
-
const marker_head = stats.get(stream)?.head;
|
|
992
|
-
const interrupted = marker_head?.name === TOMBSTONE_EVENT;
|
|
993
|
-
out.set(stream, {
|
|
994
|
-
max_id: head.id,
|
|
995
|
-
version: true_heads.get(stream).head.version,
|
|
996
|
-
last_event_name: head.name,
|
|
997
|
-
...interrupted ? { resumed_guard: { id: marker_head.id } } : {}
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
return out;
|
|
896
|
+
|
|
897
|
+
// src/internal/autoclose-window.ts
|
|
898
|
+
function hour_in_zone(now, timeZone) {
|
|
899
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
900
|
+
timeZone,
|
|
901
|
+
hour: "2-digit",
|
|
902
|
+
hourCycle: "h23"
|
|
903
|
+
}).formatToParts(now);
|
|
904
|
+
return Number(parts.find((p) => p.type === "hour")?.value);
|
|
1001
905
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
let needed = -1;
|
|
1005
|
-
for (const info of stream_info.values())
|
|
1006
|
-
needed = Math.max(needed, info.max_id);
|
|
1007
|
-
const checkpoint = await catch_up_correlation2(needed);
|
|
1008
|
-
const uncorrelated = /* @__PURE__ */ new Set();
|
|
1009
|
-
for (const [stream, info] of stream_info) {
|
|
1010
|
-
if (checkpoint < info.max_id) uncorrelated.add(stream);
|
|
1011
|
-
}
|
|
1012
|
-
const pending_set = /* @__PURE__ */ new Set();
|
|
1013
|
-
const matches = source_matcher();
|
|
1014
|
-
const targets = [...stream_info.keys()];
|
|
1015
|
-
await walk_streams(
|
|
1016
|
-
store(),
|
|
1017
|
-
(position) => {
|
|
1018
|
-
const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
|
|
1019
|
-
if (!has_work) return;
|
|
1020
|
-
for (const [stream, info] of stream_info) {
|
|
1021
|
-
if (matches(position.source, stream) && position.at < info.max_id)
|
|
1022
|
-
pending_set.add(stream);
|
|
1023
|
-
}
|
|
1024
|
-
},
|
|
1025
|
-
{ limit: page_size, source_matches: targets }
|
|
1026
|
-
);
|
|
1027
|
-
const safe = [];
|
|
1028
|
-
for (const [stream] of stream_info) {
|
|
1029
|
-
if (pending_set.has(stream) || uncorrelated.has(stream))
|
|
1030
|
-
skipped.push(stream);
|
|
1031
|
-
else safe.push(stream);
|
|
1032
|
-
}
|
|
1033
|
-
return safe;
|
|
906
|
+
function hour_in_range(hour, start, end) {
|
|
907
|
+
return start < end ? hour >= start && hour < end : hour >= start || hour < end;
|
|
1034
908
|
}
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const info = stream_info.get(stream);
|
|
1041
|
-
if (info.resumed_guard) {
|
|
1042
|
-
guarded.push(stream);
|
|
1043
|
-
guard_events.set(stream, { id: info.resumed_guard.id, stream });
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
const committed = await tombstone2(stream, info.version, correlation);
|
|
1047
|
-
if (committed) {
|
|
1048
|
-
guarded.push(stream);
|
|
1049
|
-
guard_events.set(stream, { id: committed.id, stream });
|
|
1050
|
-
} else {
|
|
1051
|
-
skipped.push(stream);
|
|
1052
|
-
}
|
|
1053
|
-
})
|
|
1054
|
-
);
|
|
1055
|
-
return { guarded, guard_events };
|
|
909
|
+
function is_dst_gap_open(now, timeZone, start) {
|
|
910
|
+
const hour = hour_in_zone(now, timeZone);
|
|
911
|
+
if (hour <= start) return false;
|
|
912
|
+
const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
|
|
913
|
+
return prev === start - 1 && hour === start + 1;
|
|
1056
914
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const last_event_name = stream_info.get(stream).last_event_name;
|
|
1062
|
-
const owner_state = event_to_state.get(last_event_name);
|
|
1063
|
-
if (!owner_state) {
|
|
1064
|
-
logger.error(
|
|
1065
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
1066
|
-
);
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
const snap2 = await load2(owner_state, { stream });
|
|
1070
|
-
seed_states.set(stream, snap2.state);
|
|
1071
|
-
})
|
|
1072
|
-
);
|
|
1073
|
-
return seed_states;
|
|
915
|
+
function in_autoclose_window(window, now) {
|
|
916
|
+
if (!window) return true;
|
|
917
|
+
const hour = hour_in_zone(now, window.timeZone);
|
|
918
|
+
return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
|
|
1074
919
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
920
|
+
function next_window_open(window, now) {
|
|
921
|
+
const start_ms = now.getTime();
|
|
922
|
+
for (let h = 0; h <= 48; h++) {
|
|
923
|
+
const candidate = new Date(start_ms + h * 36e5);
|
|
924
|
+
if (hour_in_zone(candidate, window.timeZone) === window.start)
|
|
925
|
+
return candidate;
|
|
926
|
+
const boundary = new Date(
|
|
927
|
+
candidate.getTime() - candidate.getTime() % 36e5
|
|
928
|
+
);
|
|
929
|
+
if (boundary.getTime() >= start_ms && is_dst_gap_open(boundary, window.timeZone, window.start))
|
|
930
|
+
return boundary;
|
|
1079
931
|
}
|
|
932
|
+
return new Date(start_ms + 864e5);
|
|
1080
933
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
934
|
+
|
|
935
|
+
// src/internal/defer-config.ts
|
|
936
|
+
import { z as z2 } from "zod";
|
|
937
|
+
var DeferDurationSchema = z2.object({
|
|
938
|
+
days: z2.number().positive().optional(),
|
|
939
|
+
hours: z2.number().positive().optional(),
|
|
940
|
+
minutes: z2.number().positive().optional()
|
|
941
|
+
}).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
|
|
942
|
+
message: "defer: a duration needs at least one of days, hours, or minutes"
|
|
943
|
+
});
|
|
944
|
+
var DeferWhenSchema = z2.object({
|
|
945
|
+
after: DeferDurationSchema.optional(),
|
|
946
|
+
at: z2.date().optional()
|
|
947
|
+
}).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
|
|
948
|
+
message: "defer: specify exactly one of `after` or `at`"
|
|
949
|
+
});
|
|
950
|
+
function duration_ms(d) {
|
|
951
|
+
return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
|
|
1089
952
|
}
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
guarded.map(async (stream) => {
|
|
1108
|
-
const entry = truncated.get(stream);
|
|
1109
|
-
const state2 = seed_states.get(stream);
|
|
1110
|
-
if (state2 && entry) {
|
|
1111
|
-
await cache().set(stream, {
|
|
1112
|
-
stream,
|
|
1113
|
-
state: state2,
|
|
1114
|
-
version: entry.committed.version,
|
|
1115
|
-
event_id: entry.committed.id,
|
|
1116
|
-
patches: 0,
|
|
1117
|
-
snaps: 1
|
|
1118
|
-
});
|
|
1119
|
-
} else {
|
|
1120
|
-
await cache().invalidate(stream);
|
|
1121
|
-
}
|
|
1122
|
-
})
|
|
1123
|
-
);
|
|
1124
|
-
return truncated;
|
|
953
|
+
function resolve_defer_at(when, event) {
|
|
954
|
+
const parsed = DeferWhenSchema.parse(when);
|
|
955
|
+
if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
|
|
956
|
+
return parsed.at.getTime();
|
|
957
|
+
}
|
|
958
|
+
function assert_defer_when(when) {
|
|
959
|
+
DeferWhenSchema.parse(when);
|
|
960
|
+
}
|
|
961
|
+
function make_deferred(handler, schedule) {
|
|
962
|
+
const call = handler;
|
|
963
|
+
const deferred = async (event, stream, app) => {
|
|
964
|
+
const when = typeof schedule === "function" ? schedule(event) : schedule;
|
|
965
|
+
if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
|
|
966
|
+
return call(event, stream, app);
|
|
967
|
+
};
|
|
968
|
+
Object.defineProperty(deferred, "name", { value: handler.name });
|
|
969
|
+
return deferred;
|
|
1125
970
|
}
|
|
1126
971
|
|
|
1127
|
-
// src/internal/
|
|
1128
|
-
import {
|
|
972
|
+
// src/internal/drain-cycle.ts
|
|
973
|
+
import { randomUUID } from "crypto";
|
|
1129
974
|
|
|
1130
|
-
// src/internal/
|
|
1131
|
-
function
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
975
|
+
// src/internal/backoff.ts
|
|
976
|
+
function compute_backoff_delay(retry, opts) {
|
|
977
|
+
if (!opts || opts.baseMs <= 0) return 0;
|
|
978
|
+
const r = Math.max(0, retry);
|
|
979
|
+
let delay;
|
|
980
|
+
switch (opts.strategy) {
|
|
981
|
+
case "fixed":
|
|
982
|
+
delay = opts.baseMs;
|
|
983
|
+
break;
|
|
984
|
+
case "linear":
|
|
985
|
+
delay = opts.baseMs * (r + 1);
|
|
986
|
+
break;
|
|
987
|
+
case "exponential":
|
|
988
|
+
delay = opts.baseMs * 2 ** r;
|
|
989
|
+
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
990
|
+
break;
|
|
991
|
+
default: {
|
|
992
|
+
const _never = opts.strategy;
|
|
993
|
+
throw new Error(`unknown backoff strategy: ${String(_never)}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
997
|
+
return Math.max(0, Math.floor(delay));
|
|
1135
998
|
}
|
|
1136
999
|
|
|
1137
|
-
// src/internal/
|
|
1138
|
-
var
|
|
1139
|
-
var
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
`Reaction "${handler}" resolved onto undeclared lane "${lane}" \u2014 for example target "${target}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. No controller claims it, so the stream would never drain \u2014 running it on "default" instead. The equivalent static \`.to({ lane })\` is rejected at build; a dynamic resolver's lane is only knowable here.`
|
|
1150
|
-
);
|
|
1151
|
-
}
|
|
1152
|
-
function report_lane_conflict(seen, handler, target, kept, dropped) {
|
|
1153
|
-
report_once(
|
|
1154
|
-
seen,
|
|
1155
|
-
`conflict|${handler}|${kept}|${dropped}`,
|
|
1156
|
-
`Reaction "${handler}" resolved lane "${dropped}" for a stream already on "${kept}" \u2014 for example "${target}". These are conflicting lane assignments from two dynamic resolutions at equal priority. Keeping "${kept}", the lane it was first discovered on \u2014 re-laning a live stream would move it out from under a worker holding its lease. The reaction resolving "${dropped}" runs inside the "${kept}" lane's budget, and a process restricted to "${dropped}" via onlyLanes never runs it. The equivalent static declaration is rejected at build; align the resolvers, or split the target.`
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
var CorrelateCycle = class {
|
|
1160
|
-
_checkpoint = -1;
|
|
1161
|
-
// Lease holder id; per instance, so a restart never inherits a stale claim.
|
|
1162
|
-
_by = randomUUID();
|
|
1163
|
-
_key;
|
|
1164
|
-
// Local expiry of the held lease (0 = none); stale only costs a duplicate scan.
|
|
1165
|
-
_lease_until = 0;
|
|
1166
|
-
// Armed while `_raised` (bumped by a commit or notification) is ahead of
|
|
1167
|
-
// `_lowered` (the raise count the last short page saw). Starts armed.
|
|
1168
|
-
_raised = 1;
|
|
1169
|
-
_lowered = 0;
|
|
1170
|
-
// Bumped by a release (shutdown), so a scan or lease reply that was out
|
|
1171
|
-
// across one writes nothing.
|
|
1172
|
-
_epoch = 0;
|
|
1173
|
-
// Passes in flight: a restore waits for them, and refuses new ones.
|
|
1174
|
-
_passes = /* @__PURE__ */ new Set();
|
|
1175
|
-
_restoring = false;
|
|
1176
|
-
_init_promise;
|
|
1177
|
-
// Bounded by `maxSubscribedStreams`; static records live apart so eviction
|
|
1178
|
-
// never lets a resolution re-lane them.
|
|
1179
|
-
_dynamic_subscriptions;
|
|
1180
|
-
_static_subscriptions = /* @__PURE__ */ new Map();
|
|
1181
|
-
_patterns = new LruMap(PATTERN_CACHE_SIZE);
|
|
1182
|
-
_registry;
|
|
1183
|
-
_static_targets;
|
|
1184
|
-
_cd;
|
|
1185
|
-
_on_init;
|
|
1186
|
-
_declared_lanes;
|
|
1187
|
-
// Report-once keys; per instance because `internal/` holds no module state.
|
|
1188
|
-
_reported = /* @__PURE__ */ new Set();
|
|
1189
|
-
constructor({
|
|
1190
|
-
registry,
|
|
1191
|
-
static_targets,
|
|
1192
|
-
cd,
|
|
1193
|
-
max_subscribed_streams,
|
|
1194
|
-
declared_lanes,
|
|
1195
|
-
shard,
|
|
1196
|
-
on_init
|
|
1197
|
-
}) {
|
|
1198
|
-
this._key = shard ? `${registry_key(registry.events)}:${[...shard].sort().join(",")}` : registry_key(registry.events);
|
|
1199
|
-
this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
|
|
1200
|
-
this._registry = registry;
|
|
1201
|
-
this._declared_lanes = declared_lanes;
|
|
1202
|
-
this._static_targets = static_targets;
|
|
1203
|
-
this._cd = cd;
|
|
1204
|
-
this._on_init = on_init;
|
|
1000
|
+
// src/internal/defer-timer.ts
|
|
1001
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1002
|
+
var DeferTimer = class {
|
|
1003
|
+
_due = /* @__PURE__ */ new Map();
|
|
1004
|
+
_timer;
|
|
1005
|
+
_on_wake;
|
|
1006
|
+
/**
|
|
1007
|
+
* @param on_wake - invoked once each time the earliest due-time elapses.
|
|
1008
|
+
* The consumer re-schedules for whatever is still parked.
|
|
1009
|
+
*/
|
|
1010
|
+
constructor(on_wake) {
|
|
1011
|
+
this._on_wake = on_wake;
|
|
1205
1012
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1013
|
+
/** Number of currently parked streams. */
|
|
1014
|
+
get size() {
|
|
1015
|
+
return this._due.size;
|
|
1208
1016
|
}
|
|
1209
|
-
/**
|
|
1210
|
-
|
|
1211
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* Park `stream` for a re-visit at `at` (ms since epoch). A plain
|
|
1019
|
+
* overwrite: the caller computes the authoritative next-visit for the
|
|
1020
|
+
* stream (the drain's `handle` already reconciles a stream's reactions
|
|
1021
|
+
* into one result per cycle), so there is no stale value to merge against.
|
|
1022
|
+
*/
|
|
1023
|
+
set(stream, at) {
|
|
1024
|
+
this._due.set(stream, at);
|
|
1212
1025
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
throw error;
|
|
1217
|
-
});
|
|
1218
|
-
await this._init_promise;
|
|
1026
|
+
/** Drop `stream` from the parked set (e.g. on a successful ack or block). */
|
|
1027
|
+
delete(stream) {
|
|
1028
|
+
this._due.delete(stream);
|
|
1219
1029
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
await this._on_init?.();
|
|
1030
|
+
/**
|
|
1031
|
+
* Drop every entry due at or before `before`. The drain calls it when a
|
|
1032
|
+
* pass that started at `before` claimed nothing: whatever was due by then
|
|
1033
|
+
* was handled elsewhere (another worker) or has no work left, so waking
|
|
1034
|
+
* for it again would spin.
|
|
1035
|
+
*/
|
|
1036
|
+
prune(before) {
|
|
1037
|
+
for (const [stream, at] of this._due)
|
|
1038
|
+
if (at <= before) this._due.delete(stream);
|
|
1232
1039
|
}
|
|
1233
1040
|
/**
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1041
|
+
* (Re)schedule the wake timer at the earliest pending due-time. Idempotent
|
|
1042
|
+
* — collapses many parked streams into a single timer. A no-op clears any
|
|
1043
|
+
* pending timer when the map is empty.
|
|
1044
|
+
*
|
|
1045
|
+
* The timer is `unref()`-ed so pending re-visits never keep the process
|
|
1046
|
+
* alive on their own.
|
|
1236
1047
|
*/
|
|
1237
|
-
|
|
1238
|
-
this.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
} finally {
|
|
1243
|
-
this._restoring = false;
|
|
1048
|
+
schedule() {
|
|
1049
|
+
if (this._timer) clearTimeout(this._timer);
|
|
1050
|
+
if (this._due.size === 0) {
|
|
1051
|
+
this._timer = void 0;
|
|
1052
|
+
return;
|
|
1244
1053
|
}
|
|
1245
|
-
|
|
1246
|
-
this.
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1054
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
1055
|
+
for (const t of this._due.values()) if (t < earliest) earliest = t;
|
|
1056
|
+
const delay = Math.min(
|
|
1057
|
+
Math.max(0, earliest - Date.now()),
|
|
1058
|
+
MAX_TIMER_DELAY_MS
|
|
1059
|
+
);
|
|
1060
|
+
this._timer = setTimeout(() => {
|
|
1061
|
+
this._timer = void 0;
|
|
1062
|
+
this._on_wake();
|
|
1063
|
+
}, delay);
|
|
1064
|
+
this._timer.unref();
|
|
1253
1065
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
if (!pattern) {
|
|
1260
|
-
pattern = new RegExp(source);
|
|
1261
|
-
this._patterns.set(source, pattern);
|
|
1066
|
+
/** Cancel any pending wake timer. Idempotent. Leaves the parked set intact. */
|
|
1067
|
+
stop() {
|
|
1068
|
+
if (this._timer) {
|
|
1069
|
+
clearTimeout(this._timer);
|
|
1070
|
+
this._timer = void 0;
|
|
1262
1071
|
}
|
|
1263
|
-
return pattern.test(stream);
|
|
1264
1072
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// src/internal/drain-ratio.ts
|
|
1076
|
+
var RATIO_MIN = 0.2;
|
|
1077
|
+
var RATIO_MAX = 0.8;
|
|
1078
|
+
var RATIO_DEFAULT = 0.5;
|
|
1079
|
+
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
1080
|
+
let lagging_handled = 0;
|
|
1081
|
+
let leading_handled = 0;
|
|
1082
|
+
for (const { lease, handled: count } of handled) {
|
|
1083
|
+
if (lease.lagging) lagging_handled += count;
|
|
1084
|
+
else leading_handled += count;
|
|
1275
1085
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
by: this._by,
|
|
1290
|
-
millis: CORRELATION_LEASE_MS
|
|
1291
|
-
});
|
|
1292
|
-
if (this._epoch !== epoch) return idle();
|
|
1293
|
-
this._lease_until = correlating ? now + CORRELATION_LEASE_MS : 0;
|
|
1294
|
-
if (correlating === false) return idle();
|
|
1295
|
-
}
|
|
1296
|
-
const after = this._checkpoint;
|
|
1297
|
-
const correlated = /* @__PURE__ */ new Map();
|
|
1298
|
-
let last_id = after;
|
|
1299
|
-
let stopped = false;
|
|
1300
|
-
const found = await store().query(
|
|
1301
|
-
(event) => {
|
|
1302
|
-
if (stopped) return;
|
|
1303
|
-
const register = this._registry.events[event.name];
|
|
1304
|
-
if (register) {
|
|
1305
|
-
for (const reaction of register.reactions.values()) {
|
|
1306
|
-
let resolved;
|
|
1307
|
-
try {
|
|
1308
|
-
resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1309
|
-
} catch (error) {
|
|
1310
|
-
stopped = true;
|
|
1311
|
-
report_once(
|
|
1312
|
-
this._reported,
|
|
1313
|
-
`resolver|${reaction.handler.name}|${event.id}`,
|
|
1314
|
-
`Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}. Correlation stops before this event until the resolver stops throwing; work already found keeps draining.`
|
|
1315
|
-
);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
if (!resolved) continue;
|
|
1319
|
-
let lane = resolved.lane;
|
|
1320
|
-
if (lane !== void 0 && !this._declared_lanes.has(lane)) {
|
|
1321
|
-
report_undeclared_lane(
|
|
1322
|
-
this._reported,
|
|
1323
|
-
reaction.handler.name,
|
|
1324
|
-
resolved.target,
|
|
1325
|
-
lane,
|
|
1326
|
-
this._declared_lanes
|
|
1327
|
-
);
|
|
1328
|
-
lane = void 0;
|
|
1329
|
-
}
|
|
1330
|
-
const fixed = this._static_subscriptions.get(resolved.target);
|
|
1331
|
-
const recorded = fixed ?? this._dynamic_subscriptions.get(resolved.target);
|
|
1332
|
-
const priority = resolved.priority ?? 0;
|
|
1333
|
-
const upgraded = !recorded || !fixed && priority > recorded.priority;
|
|
1334
|
-
const carried = upgraded ? { priority, lane } : { priority: recorded.priority, lane: recorded.lane };
|
|
1335
|
-
const entry = correlated.get(resolved.target) || {
|
|
1336
|
-
source: resolved.source,
|
|
1337
|
-
priority: carried.priority,
|
|
1338
|
-
lane: carried.lane,
|
|
1339
|
-
upgraded,
|
|
1340
|
-
missed: !recorded,
|
|
1341
|
-
handler: reaction.handler.name,
|
|
1342
|
-
correlated_at: void 0
|
|
1343
|
-
};
|
|
1344
|
-
const seen_in_scan = correlated.has(resolved.target);
|
|
1345
|
-
const held_lane = (seen_in_scan ? entry.lane : recorded?.lane) ?? DEFAULT_LANE;
|
|
1346
|
-
const held_priority = seen_in_scan ? entry.priority : recorded?.priority;
|
|
1347
|
-
const resolved_lane = lane ?? DEFAULT_LANE;
|
|
1348
|
-
if (held_priority === priority && held_lane !== resolved_lane)
|
|
1349
|
-
report_lane_conflict(
|
|
1350
|
-
this._reported,
|
|
1351
|
-
reaction.handler.name,
|
|
1352
|
-
resolved.target,
|
|
1353
|
-
held_lane,
|
|
1354
|
-
resolved_lane
|
|
1355
|
-
);
|
|
1356
|
-
if (carried.priority > entry.priority) {
|
|
1357
|
-
entry.priority = carried.priority;
|
|
1358
|
-
entry.lane = carried.lane;
|
|
1359
|
-
entry.upgraded = upgraded;
|
|
1360
|
-
entry.handler = reaction.handler.name;
|
|
1361
|
-
}
|
|
1362
|
-
if (this._in_fetch_window(resolved.source, event.stream))
|
|
1363
|
-
entry.correlated_at = event.id;
|
|
1364
|
-
correlated.set(resolved.target, entry);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
last_id = event.id;
|
|
1368
|
-
},
|
|
1369
|
-
// Resolvers see `pii: null`: correlate runs actor-less, and one
|
|
1370
|
-
// undecryptable row would otherwise stop every stream's reactions.
|
|
1371
|
-
{ after, limit, with_pii: false }
|
|
1372
|
-
);
|
|
1373
|
-
if (this._declared_lanes.size > 1) await this._recall(correlated);
|
|
1374
|
-
const read = last_id > after;
|
|
1375
|
-
if (this._epoch !== epoch) return idle();
|
|
1376
|
-
const streams = [];
|
|
1377
|
-
for (const [stream, entry] of correlated) {
|
|
1378
|
-
if (entry.upgraded || entry.correlated_at !== void 0)
|
|
1379
|
-
streams.push({
|
|
1380
|
-
stream,
|
|
1381
|
-
source: entry.source,
|
|
1382
|
-
priority: entry.priority,
|
|
1383
|
-
lane: entry.lane,
|
|
1384
|
-
correlated_at: entry.correlated_at
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
if (streams.length) {
|
|
1388
|
-
const renewed_at = Date.now();
|
|
1389
|
-
const { subscribed, correlating } = await this._cd.subscribe(
|
|
1390
|
-
streams,
|
|
1391
|
-
last_id,
|
|
1392
|
-
lease ? { key: this._key, by: this._by, millis: CORRELATION_LEASE_MS } : void 0
|
|
1393
|
-
);
|
|
1394
|
-
if (lease && correlating !== false)
|
|
1395
|
-
this._lease_until = renewed_at + CORRELATION_LEASE_MS;
|
|
1396
|
-
const marked = streams.filter(
|
|
1397
|
-
(entry) => entry.correlated_at !== void 0
|
|
1398
|
-
).length;
|
|
1399
|
-
this._checkpoint = last_id;
|
|
1400
|
-
for (const { stream, priority, lane } of streams) {
|
|
1401
|
-
if (correlated.get(stream)?.upgraded)
|
|
1402
|
-
this._dynamic_subscriptions.set(stream, {
|
|
1403
|
-
priority,
|
|
1404
|
-
lane
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
return { kind: "scanned", last_id, subscribed, marked, read };
|
|
1408
|
-
}
|
|
1409
|
-
this._checkpoint = last_id;
|
|
1410
|
-
if (!stopped && found !== limit) this._lowered = raised;
|
|
1411
|
-
return { kind: "scanned", last_id, subscribed: 0, marked: 0, read };
|
|
1412
|
-
}
|
|
1413
|
-
// Read the row of each target this scan had no record of (new or evicted),
|
|
1414
|
-
// so a resolution that doesn't beat its priority keeps the row's lane.
|
|
1415
|
-
// Only called with more than one lane declared.
|
|
1416
|
-
async _recall(correlated) {
|
|
1417
|
-
const missed = [...correlated].filter(([, entry]) => entry.missed);
|
|
1418
|
-
await Promise.all(
|
|
1419
|
-
missed.map(async ([stream, entry]) => {
|
|
1420
|
-
let row;
|
|
1421
|
-
await store().query_streams(
|
|
1422
|
-
(position) => {
|
|
1423
|
-
row = position;
|
|
1424
|
-
},
|
|
1425
|
-
{ stream, stream_exact: true, limit: 1 }
|
|
1426
|
-
);
|
|
1427
|
-
if (!row) return;
|
|
1428
|
-
this._dynamic_subscriptions.set(stream, {
|
|
1429
|
-
priority: row.priority,
|
|
1430
|
-
lane: row.lane
|
|
1431
|
-
});
|
|
1432
|
-
if (entry.priority > row.priority) return;
|
|
1433
|
-
const held_lane = row.lane ?? DEFAULT_LANE;
|
|
1434
|
-
const resolved_lane = entry.lane ?? DEFAULT_LANE;
|
|
1435
|
-
if (entry.priority === row.priority && held_lane !== resolved_lane)
|
|
1436
|
-
report_lane_conflict(
|
|
1437
|
-
this._reported,
|
|
1438
|
-
entry.handler,
|
|
1439
|
-
stream,
|
|
1440
|
-
held_lane,
|
|
1441
|
-
resolved_lane
|
|
1442
|
-
);
|
|
1443
|
-
entry.priority = row.priority;
|
|
1444
|
-
entry.lane = row.lane;
|
|
1445
|
-
entry.upgraded = false;
|
|
1446
|
-
})
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
/** Release the lease early by re-acquiring it for zero ms. Best-effort. */
|
|
1450
|
-
async release_correlation() {
|
|
1451
|
-
try {
|
|
1452
|
-
this._lease_until = 0;
|
|
1453
|
-
this._epoch++;
|
|
1454
|
-
await this._cd.subscribe([], void 0, {
|
|
1455
|
-
key: this._key,
|
|
1456
|
-
by: this._by,
|
|
1457
|
-
millis: 0
|
|
1458
|
-
});
|
|
1459
|
-
} catch (error) {
|
|
1460
|
-
log().warn(
|
|
1461
|
-
`Could not hand back the correlation lease during shutdown; it expires on its own within ${CORRELATION_LEASE_MS}ms and correlation resumes normally after that. On a single-writer store this usually means another connection holds the database. Cause: ${String(error.cause ?? error)}`
|
|
1462
|
-
);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
};
|
|
1086
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1087
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1088
|
+
const total = lagging_avg + leading_avg;
|
|
1089
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
1090
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/internal/report-once.ts
|
|
1094
|
+
function report_once(seen, key, message) {
|
|
1095
|
+
if (seen.has(key)) return;
|
|
1096
|
+
seen.add(key);
|
|
1097
|
+
log().error(new Error(message));
|
|
1098
|
+
}
|
|
1466
1099
|
|
|
1467
1100
|
// src/internal/correlation-id.ts
|
|
1468
1101
|
import { randomInt } from "crypto";
|
|
@@ -1488,134 +1121,13 @@ function close_correlation(correlator, actor) {
|
|
|
1488
1121
|
});
|
|
1489
1122
|
}
|
|
1490
1123
|
|
|
1491
|
-
// src/internal/drain-cycle.ts
|
|
1492
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1493
|
-
|
|
1494
|
-
// src/internal/backoff.ts
|
|
1495
|
-
function compute_backoff_delay(retry, opts) {
|
|
1496
|
-
if (!opts || opts.baseMs <= 0) return 0;
|
|
1497
|
-
const r = Math.max(0, retry);
|
|
1498
|
-
let delay;
|
|
1499
|
-
switch (opts.strategy) {
|
|
1500
|
-
case "fixed":
|
|
1501
|
-
delay = opts.baseMs;
|
|
1502
|
-
break;
|
|
1503
|
-
case "linear":
|
|
1504
|
-
delay = opts.baseMs * (r + 1);
|
|
1505
|
-
break;
|
|
1506
|
-
case "exponential":
|
|
1507
|
-
delay = opts.baseMs * 2 ** r;
|
|
1508
|
-
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
1509
|
-
break;
|
|
1510
|
-
default: {
|
|
1511
|
-
const _never = opts.strategy;
|
|
1512
|
-
throw new Error(`unknown backoff strategy: ${String(_never)}`);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
1516
|
-
return Math.max(0, Math.floor(delay));
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// src/internal/defer-timer.ts
|
|
1520
|
-
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1521
|
-
var DeferTimer = class {
|
|
1522
|
-
_due = /* @__PURE__ */ new Map();
|
|
1523
|
-
_timer;
|
|
1524
|
-
_on_wake;
|
|
1525
|
-
/**
|
|
1526
|
-
* @param on_wake - invoked once each time the earliest due-time elapses.
|
|
1527
|
-
* The consumer re-schedules for whatever is still parked.
|
|
1528
|
-
*/
|
|
1529
|
-
constructor(on_wake) {
|
|
1530
|
-
this._on_wake = on_wake;
|
|
1531
|
-
}
|
|
1532
|
-
/** Number of currently parked streams. */
|
|
1533
|
-
get size() {
|
|
1534
|
-
return this._due.size;
|
|
1535
|
-
}
|
|
1536
|
-
/**
|
|
1537
|
-
* Park `stream` for a re-visit at `at` (ms since epoch). A plain
|
|
1538
|
-
* overwrite: the caller computes the authoritative next-visit for the
|
|
1539
|
-
* stream (the drain's `handle` already reconciles a stream's reactions
|
|
1540
|
-
* into one result per cycle), so there is no stale value to merge against.
|
|
1541
|
-
*/
|
|
1542
|
-
set(stream, at) {
|
|
1543
|
-
this._due.set(stream, at);
|
|
1544
|
-
}
|
|
1545
|
-
/** Drop `stream` from the parked set (e.g. on a successful ack or block). */
|
|
1546
|
-
delete(stream) {
|
|
1547
|
-
this._due.delete(stream);
|
|
1548
|
-
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Drop every entry due at or before `before`. The drain calls it when a
|
|
1551
|
-
* pass that started at `before` claimed nothing: whatever was due by then
|
|
1552
|
-
* was handled elsewhere (another worker) or has no work left, so waking
|
|
1553
|
-
* for it again would spin.
|
|
1554
|
-
*/
|
|
1555
|
-
prune(before) {
|
|
1556
|
-
for (const [stream, at] of this._due)
|
|
1557
|
-
if (at <= before) this._due.delete(stream);
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* (Re)schedule the wake timer at the earliest pending due-time. Idempotent
|
|
1561
|
-
* — collapses many parked streams into a single timer. A no-op clears any
|
|
1562
|
-
* pending timer when the map is empty.
|
|
1563
|
-
*
|
|
1564
|
-
* The timer is `unref()`-ed so pending re-visits never keep the process
|
|
1565
|
-
* alive on their own.
|
|
1566
|
-
*/
|
|
1567
|
-
schedule() {
|
|
1568
|
-
if (this._timer) clearTimeout(this._timer);
|
|
1569
|
-
if (this._due.size === 0) {
|
|
1570
|
-
this._timer = void 0;
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
let earliest = Number.POSITIVE_INFINITY;
|
|
1574
|
-
for (const t of this._due.values()) if (t < earliest) earliest = t;
|
|
1575
|
-
const delay = Math.min(
|
|
1576
|
-
Math.max(0, earliest - Date.now()),
|
|
1577
|
-
MAX_TIMER_DELAY_MS
|
|
1578
|
-
);
|
|
1579
|
-
this._timer = setTimeout(() => {
|
|
1580
|
-
this._timer = void 0;
|
|
1581
|
-
this._on_wake();
|
|
1582
|
-
}, delay);
|
|
1583
|
-
this._timer.unref();
|
|
1584
|
-
}
|
|
1585
|
-
/** Cancel any pending wake timer. Idempotent. Leaves the parked set intact. */
|
|
1586
|
-
stop() {
|
|
1587
|
-
if (this._timer) {
|
|
1588
|
-
clearTimeout(this._timer);
|
|
1589
|
-
this._timer = void 0;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
};
|
|
1593
|
-
|
|
1594
|
-
// src/internal/drain-ratio.ts
|
|
1595
|
-
var RATIO_MIN = 0.2;
|
|
1596
|
-
var RATIO_MAX = 0.8;
|
|
1597
|
-
var RATIO_DEFAULT = 0.5;
|
|
1598
|
-
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
1599
|
-
let lagging_handled = 0;
|
|
1600
|
-
let leading_handled = 0;
|
|
1601
|
-
for (const { lease, handled: count } of handled) {
|
|
1602
|
-
if (lease.lagging) lagging_handled += count;
|
|
1603
|
-
else leading_handled += count;
|
|
1604
|
-
}
|
|
1605
|
-
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1606
|
-
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1607
|
-
const total = lagging_avg + leading_avg;
|
|
1608
|
-
if (total === 0) return RATIO_DEFAULT;
|
|
1609
|
-
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
1124
|
// src/internal/drain-ops.ts
|
|
1613
1125
|
var claim = (lagging, leading, by, millis, lane) => store().claim(lagging, leading, by, millis, lane);
|
|
1614
1126
|
async function fetch(leased, eventLimit) {
|
|
1615
1127
|
return Promise.all(
|
|
1616
1128
|
leased.map(async ({ stream, source, at, lagging }) => {
|
|
1617
1129
|
const events = [];
|
|
1618
|
-
const stream_exact = source !== void 0 && is_literal_source(source) ? true : void 0;
|
|
1130
|
+
const stream_exact = source !== void 0 && is_literal_source(source, stream) ? true : void 0;
|
|
1619
1131
|
try {
|
|
1620
1132
|
await store().query((e) => events.push(e), {
|
|
1621
1133
|
stream: source,
|
|
@@ -1837,6 +1349,7 @@ async function scan(source, opts = {}, callback) {
|
|
|
1837
1349
|
async function load(me, target, callback, patch_fn = bare_patch) {
|
|
1838
1350
|
const { stream, actor, asOf } = target;
|
|
1839
1351
|
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1352
|
+
const seeded = time_travel && asOf.limit === void 0 && !asOf.created_after;
|
|
1840
1353
|
const cached = time_travel ? void 0 : await cache().get(stream);
|
|
1841
1354
|
const cache_hit = !!cached;
|
|
1842
1355
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
@@ -1846,6 +1359,7 @@ async function load(me, target, callback, patch_fn = bare_patch) {
|
|
|
1846
1359
|
let event;
|
|
1847
1360
|
await store().query(
|
|
1848
1361
|
(raw) => {
|
|
1362
|
+
if (seeded && raw.name === SNAP_EVENT && replayed > 0) return;
|
|
1849
1363
|
event = me.view(raw, actor);
|
|
1850
1364
|
if (event.name === SNAP_EVENT) {
|
|
1851
1365
|
state2 = event.data;
|
|
@@ -1878,7 +1392,11 @@ async function load(me, target, callback, patch_fn = bare_patch) {
|
|
|
1878
1392
|
// `with_snaps` on every non-time-travel load: cold, it resumes from the
|
|
1879
1393
|
// latest snapshot; warm, it keeps a snapshot above a stale checkpoint,
|
|
1880
1394
|
// so a windowed close's prune can't fold the tail onto stale state.
|
|
1881
|
-
...cached ? { after: cached.event_id, with_snaps: true } :
|
|
1395
|
+
...cached ? { after: cached.event_id, with_snaps: true } : seeded ? (
|
|
1396
|
+
// From the start, past the resume floor, which could jump over the
|
|
1397
|
+
// time bound. The snapshots are for the seed.
|
|
1398
|
+
{ with_snaps: true, after: -1, ...asOf }
|
|
1399
|
+
) : time_travel ? asOf : { with_snaps: true }
|
|
1882
1400
|
}
|
|
1883
1401
|
);
|
|
1884
1402
|
const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
|
|
@@ -2272,7 +1790,7 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
2272
1790
|
const leased = await ops.claim(
|
|
2273
1791
|
lagging,
|
|
2274
1792
|
leading,
|
|
2275
|
-
|
|
1793
|
+
randomUUID(),
|
|
2276
1794
|
leaseMillis,
|
|
2277
1795
|
lane
|
|
2278
1796
|
);
|
|
@@ -2475,52 +1993,942 @@ var DrainController = class {
|
|
|
2475
1993
|
this._inflight = void 0;
|
|
2476
1994
|
}
|
|
2477
1995
|
);
|
|
2478
|
-
this._inflight = pass;
|
|
2479
|
-
return pass;
|
|
1996
|
+
this._inflight = pass;
|
|
1997
|
+
return pass;
|
|
1998
|
+
}
|
|
1999
|
+
// One claim → fetch → handle → ack pass. Never throws.
|
|
2000
|
+
async _pass(streamLimit, eventLimit, leaseMillis) {
|
|
2001
|
+
try {
|
|
2002
|
+
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2003
|
+
const leading = streamLimit - lagging;
|
|
2004
|
+
const started = Date.now();
|
|
2005
|
+
const raised = this._raised;
|
|
2006
|
+
const cycle = await run_drain_cycle(this._deps, {
|
|
2007
|
+
lagging,
|
|
2008
|
+
leading,
|
|
2009
|
+
eventLimit,
|
|
2010
|
+
leaseMillis,
|
|
2011
|
+
misrouted: this._misrouted,
|
|
2012
|
+
store_failing: this._deps.breaker.failing
|
|
2013
|
+
});
|
|
2014
|
+
if (!cycle) {
|
|
2015
|
+
this._defer.prune(started);
|
|
2016
|
+
this._deps.breaker.passed();
|
|
2017
|
+
this._lowered = raised;
|
|
2018
|
+
return EMPTY_DRAIN;
|
|
2019
|
+
}
|
|
2020
|
+
const { leased, fetched, handled, acked, blocked, closeable } = cycle;
|
|
2021
|
+
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
2022
|
+
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
2023
|
+
for (const lease of acked) this._defer.delete(lease.stream);
|
|
2024
|
+
for (const lease of blocked) this._defer.delete(lease.stream);
|
|
2025
|
+
for (const h of handled) {
|
|
2026
|
+
const next = h.defer ?? h.next_attempt_at;
|
|
2027
|
+
if (next !== void 0) this._defer.set(h.lease.stream, next);
|
|
2028
|
+
}
|
|
2029
|
+
if (acked.length) this._deps.on_acked(acked);
|
|
2030
|
+
if (closeable.length) await this._deps.on_close(closeable);
|
|
2031
|
+
this._deps.breaker.passed();
|
|
2032
|
+
return { fetched, leased, acked, blocked };
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
this._defer.set(
|
|
2035
|
+
RETRY,
|
|
2036
|
+
Date.now() + Math.max(leaseMillis, this._deps.breaker.cooldown_ms)
|
|
2037
|
+
);
|
|
2038
|
+
this._deps.breaker.failed(Date.now(), error);
|
|
2039
|
+
return EMPTY_DRAIN;
|
|
2040
|
+
} finally {
|
|
2041
|
+
if (!this._stopped) this._defer.schedule();
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// src/internal/reaction-dispatch.ts
|
|
2047
|
+
var CloseSignal = class extends Error {
|
|
2048
|
+
/**
|
|
2049
|
+
* Stream to close. Omitted → the reaction's own lease stream (a self-close).
|
|
2050
|
+
* The synthesized autoclose reaction sets it to the aggregate stream, since
|
|
2051
|
+
* its lease runs on a synthetic `__autoclose__` target.
|
|
2052
|
+
*/
|
|
2053
|
+
stream;
|
|
2054
|
+
/** Optional archive callback to run while the stream is guarded. */
|
|
2055
|
+
archive;
|
|
2056
|
+
/**
|
|
2057
|
+
* Watermark to ack the requesting reaction to before the close runs. The
|
|
2058
|
+
* close-cycle safety guard skips a stream whose subscriptions (matched by
|
|
2059
|
+
* source) lag the head; the autoclose reaction's `source` is the aggregate,
|
|
2060
|
+
* so it must advance its own watermark to the live head id it evaluated
|
|
2061
|
+
* against — otherwise it blocks its own close. Defaults to the triggering
|
|
2062
|
+
* event id when omitted.
|
|
2063
|
+
*/
|
|
2064
|
+
at;
|
|
2065
|
+
/**
|
|
2066
|
+
* Windowed close: prune events older than this cutoff behind
|
|
2067
|
+
* the closest safe snapshot instead of retiring the stream. Thrown by
|
|
2068
|
+
* the autoclose reaction of a `.autocloses({ keep })` state; flows
|
|
2069
|
+
* into {@link CloseTarget.before}. Omitted → a full close.
|
|
2070
|
+
*/
|
|
2071
|
+
before;
|
|
2072
|
+
/**
|
|
2073
|
+
* Revisit the requesting reaction at this time. Only useful with an `at`
|
|
2074
|
+
* below the head: a reaction runs only when an event is waiting for it.
|
|
2075
|
+
*/
|
|
2076
|
+
defer;
|
|
2077
|
+
constructor(opts) {
|
|
2078
|
+
super("reaction requested close");
|
|
2079
|
+
this.name = "CloseSignal";
|
|
2080
|
+
this.stream = opts?.stream;
|
|
2081
|
+
this.archive = opts?.archive;
|
|
2082
|
+
this.at = opts?.at;
|
|
2083
|
+
this.before = opts?.before;
|
|
2084
|
+
this.defer = opts?.defer;
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
function build_handle(deps) {
|
|
2088
|
+
const { logger, reaction_scope } = deps;
|
|
2089
|
+
return async (lease, payloads) => {
|
|
2090
|
+
if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
|
|
2091
|
+
const stream = lease.stream;
|
|
2092
|
+
const last_index_of = /* @__PURE__ */ new Map();
|
|
2093
|
+
for (let i = 0; i < payloads.length; i++)
|
|
2094
|
+
last_index_of.set(payloads[i].event.id, i);
|
|
2095
|
+
let at = lease.at;
|
|
2096
|
+
let handled = 0;
|
|
2097
|
+
if (lease.retry > 0)
|
|
2098
|
+
logger.warn(
|
|
2099
|
+
`Retrying ${stream}@${payloads.at(0).event.id} (${lease.retry}).`
|
|
2100
|
+
);
|
|
2101
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
2102
|
+
const payload = payloads[i];
|
|
2103
|
+
const { event, handler } = payload;
|
|
2104
|
+
try {
|
|
2105
|
+
await reaction_scope.run(
|
|
2106
|
+
event,
|
|
2107
|
+
() => handler(event, stream, reaction_scope.app)
|
|
2108
|
+
);
|
|
2109
|
+
if (last_index_of.get(event.id) === i) {
|
|
2110
|
+
at = event.id;
|
|
2111
|
+
handled++;
|
|
2112
|
+
}
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
if (error instanceof DeferSignal) {
|
|
2115
|
+
let defer;
|
|
2116
|
+
try {
|
|
2117
|
+
defer = resolve_defer_at(error.when, event);
|
|
2118
|
+
} catch (invalid) {
|
|
2119
|
+
return finalize(
|
|
2120
|
+
lease,
|
|
2121
|
+
handled,
|
|
2122
|
+
at,
|
|
2123
|
+
invalid,
|
|
2124
|
+
payload.options,
|
|
2125
|
+
logger,
|
|
2126
|
+
event.id
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
return { lease, handled, acked_at: at, defer };
|
|
2130
|
+
}
|
|
2131
|
+
if (error instanceof CloseSignal)
|
|
2132
|
+
return {
|
|
2133
|
+
lease,
|
|
2134
|
+
handled: handled + 1,
|
|
2135
|
+
// Advance to the live head the handler evaluated against (when
|
|
2136
|
+
// provided) so the close-cycle guard sees this reaction caught up.
|
|
2137
|
+
acked_at: error.at ?? event.id,
|
|
2138
|
+
defer: error.defer?.getTime(),
|
|
2139
|
+
// Close the signalled stream (the autoclose reaction's aggregate,
|
|
2140
|
+
// which differs from its synthetic lease stream); a self-closing
|
|
2141
|
+
// user reaction omits it and closes its own lease stream. A
|
|
2142
|
+
// carried `before` makes it a windowed close (prune, not retire).
|
|
2143
|
+
close: {
|
|
2144
|
+
stream: error.stream ?? stream,
|
|
2145
|
+
archive: error.archive,
|
|
2146
|
+
before: error.before
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
return finalize(
|
|
2150
|
+
lease,
|
|
2151
|
+
handled,
|
|
2152
|
+
at,
|
|
2153
|
+
error,
|
|
2154
|
+
payload.options,
|
|
2155
|
+
logger,
|
|
2156
|
+
event.id
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
function build_handle_batch(logger) {
|
|
2164
|
+
return async (lease, payloads, batchHandler) => {
|
|
2165
|
+
const stream = lease.stream;
|
|
2166
|
+
const events = [];
|
|
2167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2168
|
+
for (const p of payloads) {
|
|
2169
|
+
if (seen.has(p.event.id)) continue;
|
|
2170
|
+
seen.add(p.event.id);
|
|
2171
|
+
events.push(p.event);
|
|
2172
|
+
}
|
|
2173
|
+
const options = payloads[0].options;
|
|
2174
|
+
if (lease.retry > 0)
|
|
2175
|
+
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
2176
|
+
try {
|
|
2177
|
+
await batchHandler(events, stream);
|
|
2178
|
+
return finalize(
|
|
2179
|
+
lease,
|
|
2180
|
+
events.length,
|
|
2181
|
+
events.at(-1).id,
|
|
2182
|
+
void 0,
|
|
2183
|
+
options,
|
|
2184
|
+
logger
|
|
2185
|
+
);
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
return finalize(lease, 0, lease.at, error, options, logger);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/internal/autoclose-reaction.ts
|
|
2193
|
+
var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
|
|
2194
|
+
function synthesize_autoclose_reactions(registry, states, read_window) {
|
|
2195
|
+
for (const st of states.values()) {
|
|
2196
|
+
const predicate = st.autoclose;
|
|
2197
|
+
if (!predicate) continue;
|
|
2198
|
+
const after_windows = st.autoclose_after_windows;
|
|
2199
|
+
const keep_days = st.autoclose_keep_days;
|
|
2200
|
+
const archiver = st.archive;
|
|
2201
|
+
const reaction = {
|
|
2202
|
+
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
2203
|
+
// per-aggregate `__autoclose__` key — so the autoclose reaction never
|
|
2204
|
+
// shares a watermark with the aggregate's own reactions. A shared
|
|
2205
|
+
// watermark would let autoclose's defer short-circuit the aggregate's
|
|
2206
|
+
// other reactions (the "a defer affects all reactions on a stream"
|
|
2207
|
+
// hazard). The close still targets the aggregate (`source`).
|
|
2208
|
+
// A stream name with a regex metacharacter would read as a pattern, and a
|
|
2209
|
+
// store may refuse it at subscribe, stopping every reaction. Such a stream
|
|
2210
|
+
// has no autoclose: `app.close` still closes it.
|
|
2211
|
+
resolver: (e) => is_literal_source(e.stream) ? {
|
|
2212
|
+
target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
|
|
2213
|
+
source: e.stream
|
|
2214
|
+
} : void 0,
|
|
2215
|
+
// Never block on autoclose: a transient query/store error should retry,
|
|
2216
|
+
// not quarantine the synthetic stream.
|
|
2217
|
+
options: { blockOnError: false, maxRetries: DEFAULT_MAX_RETRIES },
|
|
2218
|
+
handler: async (event) => {
|
|
2219
|
+
const aggregate = event.stream;
|
|
2220
|
+
const window = read_window();
|
|
2221
|
+
if (!in_autoclose_window(window, /* @__PURE__ */ new Date()))
|
|
2222
|
+
throw new DeferSignal({ at: next_window_open(window, /* @__PURE__ */ new Date()) });
|
|
2223
|
+
const stats = await store().query_stats([aggregate], {
|
|
2224
|
+
count: true,
|
|
2225
|
+
tail: keep_days !== void 0 ? true : void 0,
|
|
2226
|
+
exclude: [TOMBSTONE_EVENT, SNAP_EVENT]
|
|
2227
|
+
});
|
|
2228
|
+
const entry = stats.get(aggregate);
|
|
2229
|
+
if (!entry) return;
|
|
2230
|
+
const head = entry.head;
|
|
2231
|
+
if (predicate(aggregate, head, entry.count))
|
|
2232
|
+
throw new CloseSignal({
|
|
2233
|
+
stream: aggregate,
|
|
2234
|
+
archive: archiver ? () => archiver(aggregate, head) : void 0,
|
|
2235
|
+
// Ack this reaction's own watermark to the live head so the
|
|
2236
|
+
// close-cycle guard (which matches subscriptions by source =
|
|
2237
|
+
// aggregate) sees it caught up instead of blocking its own close.
|
|
2238
|
+
at: head.id
|
|
2239
|
+
});
|
|
2240
|
+
if (keep_days !== void 0) {
|
|
2241
|
+
const cutoff = days_before_now(keep_days);
|
|
2242
|
+
if (entry.tail.created < cutoff) {
|
|
2243
|
+
const waiting = after_windows.some(
|
|
2244
|
+
(days) => days_after(head.created, days).getTime() > Date.now()
|
|
2245
|
+
);
|
|
2246
|
+
throw new CloseSignal({
|
|
2247
|
+
stream: aggregate,
|
|
2248
|
+
before: cutoff,
|
|
2249
|
+
archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
|
|
2250
|
+
at: waiting ? head.id - 1 : head.id,
|
|
2251
|
+
defer: waiting ? days_after(/* @__PURE__ */ new Date(), 1) : void 0
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const dues = [];
|
|
2256
|
+
for (const days of after_windows)
|
|
2257
|
+
dues.push(days_after(head.created, days));
|
|
2258
|
+
if (keep_days !== void 0)
|
|
2259
|
+
dues.push(days_after(entry.tail.created, keep_days));
|
|
2260
|
+
const future = dues.map((d) => d.getTime()).filter((t) => t > Date.now());
|
|
2261
|
+
if (future.length)
|
|
2262
|
+
throw new DeferSignal({ at: new Date(Math.min(...future)) });
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
const key = `__autoclose_${st.name}`;
|
|
2266
|
+
for (const event_name of Object.keys(st.events)) {
|
|
2267
|
+
registry.events[event_name]?.reactions.set(
|
|
2268
|
+
key,
|
|
2269
|
+
reaction
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/internal/close-cycle.ts
|
|
2276
|
+
var CLOSE_CATCH_UP_LIMIT = 1e3;
|
|
2277
|
+
var CLOSE_CATCH_UP_PASSES = 20;
|
|
2278
|
+
async function catch_up_correlation(cursor, correlate, until) {
|
|
2279
|
+
for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && cursor.checkpoint < until; pass++) {
|
|
2280
|
+
const before = cursor.checkpoint;
|
|
2281
|
+
cursor.arm();
|
|
2282
|
+
await correlate(CLOSE_CATCH_UP_LIMIT);
|
|
2283
|
+
if (cursor.checkpoint <= before) break;
|
|
2284
|
+
}
|
|
2285
|
+
return cursor.checkpoint;
|
|
2286
|
+
}
|
|
2287
|
+
var CloseLock = class {
|
|
2288
|
+
_tails = /* @__PURE__ */ new Map();
|
|
2289
|
+
/** Run `work` after every earlier close of `stream` has settled. */
|
|
2290
|
+
run(stream, work) {
|
|
2291
|
+
const prev = this._tails.get(stream) ?? Promise.resolve();
|
|
2292
|
+
const next = prev.then(work, work);
|
|
2293
|
+
this._tails.set(stream, next);
|
|
2294
|
+
const cleanup = () => {
|
|
2295
|
+
if (this._tails.get(stream) === next) this._tails.delete(stream);
|
|
2296
|
+
};
|
|
2297
|
+
next.then(cleanup, cleanup);
|
|
2298
|
+
return next;
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
var SAFETY_PROBE_PAGE_SIZE = 1e3;
|
|
2302
|
+
async function run_close_cycle(targets, deps) {
|
|
2303
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
2304
|
+
for (const t of target_map.values()) {
|
|
2305
|
+
if (t.before !== void 0 && t.restart)
|
|
2306
|
+
throw new Error(
|
|
2307
|
+
`close: \`before\` and \`restart\` are mutually exclusive (stream "${t.stream}") \u2014 a windowed close keeps the stream live behind a real snapshot; restart reseeds it`
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const windowed = [...target_map.values()].filter(
|
|
2311
|
+
(t) => t.before !== void 0
|
|
2312
|
+
);
|
|
2313
|
+
const full = [...target_map.values()].filter((t) => t.before === void 0);
|
|
2314
|
+
const skipped = [];
|
|
2315
|
+
const windowed_result = windowed.length ? await run_windowed_closes(windowed, deps, skipped) : /* @__PURE__ */ new Map();
|
|
2316
|
+
if (!full.length) return { truncated: windowed_result, skipped };
|
|
2317
|
+
const truncated = await with_stream_locks(
|
|
2318
|
+
full.map((t) => t.stream),
|
|
2319
|
+
deps,
|
|
2320
|
+
() => run_full_closes(full, target_map, deps, skipped)
|
|
2321
|
+
);
|
|
2322
|
+
const retired = [...truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
|
|
2323
|
+
if (retired.length) deps.forget_subscribed?.(retired);
|
|
2324
|
+
for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
|
|
2325
|
+
return { truncated, skipped };
|
|
2326
|
+
}
|
|
2327
|
+
function with_stream_locks(streams, deps, work) {
|
|
2328
|
+
const with_lock = deps.with_stream_lock ?? ((_stream, w) => w());
|
|
2329
|
+
return [...streams].sort().reduceRight(
|
|
2330
|
+
(inner, stream) => () => with_lock(stream, inner),
|
|
2331
|
+
work
|
|
2332
|
+
)();
|
|
2333
|
+
}
|
|
2334
|
+
async function run_full_closes(full, target_map, deps, skipped) {
|
|
2335
|
+
const none = /* @__PURE__ */ new Map();
|
|
2336
|
+
const streams = full.map((t) => t.stream);
|
|
2337
|
+
const stream_info = await scan_stream_heads(streams);
|
|
2338
|
+
for (const target of full) {
|
|
2339
|
+
if (!target.restart) continue;
|
|
2340
|
+
const info = stream_info.get(target.stream);
|
|
2341
|
+
if (!info) continue;
|
|
2342
|
+
const owner = deps.event_to_state.get(info.last_event_name);
|
|
2343
|
+
if (owner?.pii_aware) {
|
|
2344
|
+
deps.logger.error(
|
|
2345
|
+
`Refusing to close "${target.stream}" with restart: state "${owner.name}" carries sensitive fields, so a restart seed would persist redacted values while deleting the originals. Close it without restart to retire the stream, or leave it open.`
|
|
2346
|
+
);
|
|
2347
|
+
skipped.push(target.stream);
|
|
2348
|
+
stream_info.delete(target.stream);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const safe = await partition_by_safety(
|
|
2352
|
+
stream_info,
|
|
2353
|
+
deps.reactive,
|
|
2354
|
+
skipped,
|
|
2355
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
|
|
2356
|
+
deps.catch_up_correlation
|
|
2357
|
+
);
|
|
2358
|
+
if (!safe.length) return none;
|
|
2359
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
2360
|
+
safe,
|
|
2361
|
+
stream_info,
|
|
2362
|
+
deps.correlation,
|
|
2363
|
+
deps.tombstone,
|
|
2364
|
+
skipped
|
|
2365
|
+
);
|
|
2366
|
+
if (!guarded.length) return none;
|
|
2367
|
+
const seed_states = await load_restart_seeds(
|
|
2368
|
+
guarded,
|
|
2369
|
+
target_map,
|
|
2370
|
+
stream_info,
|
|
2371
|
+
deps.event_to_state,
|
|
2372
|
+
deps.load,
|
|
2373
|
+
deps.logger
|
|
2374
|
+
);
|
|
2375
|
+
await run_archive_callbacks(guarded, target_map);
|
|
2376
|
+
const still_guarded = await heads_still_guarded(
|
|
2377
|
+
guarded,
|
|
2378
|
+
guard_events,
|
|
2379
|
+
skipped
|
|
2380
|
+
);
|
|
2381
|
+
if (!still_guarded.length) return none;
|
|
2382
|
+
return truncate_and_warm_cache(
|
|
2383
|
+
still_guarded,
|
|
2384
|
+
seed_states,
|
|
2385
|
+
guard_events,
|
|
2386
|
+
deps.correlation
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
async function run_windowed_closes(windowed, deps, skipped) {
|
|
2390
|
+
const checkpoint = deps.reactive ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
|
|
2391
|
+
const min_at = deps.reactive ? await probe_min_watermarks(
|
|
2392
|
+
windowed.map((t) => t.stream),
|
|
2393
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
|
|
2394
|
+
checkpoint
|
|
2395
|
+
) : /* @__PURE__ */ new Map();
|
|
2396
|
+
const with_lock = deps.with_stream_lock ?? ((_stream, work) => work());
|
|
2397
|
+
const truncated = /* @__PURE__ */ new Map();
|
|
2398
|
+
for (const t of windowed) {
|
|
2399
|
+
const entry = await with_lock(t.stream, async () => {
|
|
2400
|
+
const max_id = min_at.get(t.stream);
|
|
2401
|
+
if (!await windowed_prune_pending(t.stream, t.before, max_id))
|
|
2402
|
+
return void 0;
|
|
2403
|
+
if (t.archive) await t.archive();
|
|
2404
|
+
const result = await store().truncate([
|
|
2405
|
+
{ stream: t.stream, before: t.before, max_id }
|
|
2406
|
+
]);
|
|
2407
|
+
return result.get(t.stream);
|
|
2408
|
+
});
|
|
2409
|
+
if (entry) truncated.set(t.stream, entry);
|
|
2410
|
+
else skipped.push(t.stream);
|
|
2411
|
+
}
|
|
2412
|
+
return truncated;
|
|
2413
|
+
}
|
|
2414
|
+
async function windowed_prune_pending(stream, before, max_id) {
|
|
2415
|
+
let boundary_id;
|
|
2416
|
+
let min_id;
|
|
2417
|
+
await store().query(
|
|
2418
|
+
(event) => {
|
|
2419
|
+
if (min_id === void 0 || event.id < min_id) min_id = event.id;
|
|
2420
|
+
if (event.name === SNAP_EVENT && event.created < before && (max_id === void 0 || event.id <= max_id) && (boundary_id === void 0 || event.id > boundary_id))
|
|
2421
|
+
boundary_id = event.id;
|
|
2422
|
+
},
|
|
2423
|
+
{ stream, stream_exact: true, with_snaps: true, after: -1 }
|
|
2424
|
+
);
|
|
2425
|
+
return boundary_id !== void 0 && min_id < boundary_id;
|
|
2426
|
+
}
|
|
2427
|
+
async function probe_min_watermarks(streams, page_size, checkpoint) {
|
|
2428
|
+
const min_at = /* @__PURE__ */ new Map();
|
|
2429
|
+
const matches = source_matcher();
|
|
2430
|
+
await walk_streams(
|
|
2431
|
+
store(),
|
|
2432
|
+
(position) => {
|
|
2433
|
+
const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
|
|
2434
|
+
const cap = pending ? position.at : Math.max(position.at, checkpoint);
|
|
2435
|
+
for (const stream of streams) {
|
|
2436
|
+
if (matches(position.source, position.stream, stream)) {
|
|
2437
|
+
const prev = min_at.get(stream);
|
|
2438
|
+
if (prev === void 0 || cap < prev) min_at.set(stream, cap);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
},
|
|
2442
|
+
{ limit: page_size }
|
|
2443
|
+
);
|
|
2444
|
+
return min_at;
|
|
2445
|
+
}
|
|
2446
|
+
function source_matcher() {
|
|
2447
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
2448
|
+
return (source, target, stream) => {
|
|
2449
|
+
if (!source) return true;
|
|
2450
|
+
if (is_literal_source(source, target)) return source === stream;
|
|
2451
|
+
let re = compiled.get(source);
|
|
2452
|
+
if (!re) {
|
|
2453
|
+
re = new RegExp(source);
|
|
2454
|
+
compiled.set(source, re);
|
|
2455
|
+
}
|
|
2456
|
+
return re.test(stream);
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
async function scan_stream_heads(streams) {
|
|
2460
|
+
const stats = await store().query_stats(streams, {
|
|
2461
|
+
exclude: [SNAP_EVENT]
|
|
2462
|
+
});
|
|
2463
|
+
const domain_heads = await store().query_stats(streams, {
|
|
2464
|
+
exclude: [SNAP_EVENT, TOMBSTONE_EVENT]
|
|
2465
|
+
});
|
|
2466
|
+
const true_heads = await store().query_stats(streams, {});
|
|
2467
|
+
const out = /* @__PURE__ */ new Map();
|
|
2468
|
+
for (const [stream, { head }] of domain_heads) {
|
|
2469
|
+
const marker_head = stats.get(stream)?.head;
|
|
2470
|
+
const interrupted = marker_head?.name === TOMBSTONE_EVENT;
|
|
2471
|
+
out.set(stream, {
|
|
2472
|
+
max_id: head.id,
|
|
2473
|
+
version: true_heads.get(stream).head.version,
|
|
2474
|
+
last_event_name: head.name,
|
|
2475
|
+
...interrupted ? { resumed_guard: { id: marker_head.id } } : {}
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
return out;
|
|
2479
|
+
}
|
|
2480
|
+
async function partition_by_safety(stream_info, reactive, skipped, page_size, catch_up_correlation2) {
|
|
2481
|
+
if (!reactive) return [...stream_info.keys()];
|
|
2482
|
+
let needed = -1;
|
|
2483
|
+
for (const info of stream_info.values())
|
|
2484
|
+
needed = Math.max(needed, info.max_id);
|
|
2485
|
+
const checkpoint = await catch_up_correlation2(needed);
|
|
2486
|
+
const uncorrelated = /* @__PURE__ */ new Set();
|
|
2487
|
+
for (const [stream, info] of stream_info) {
|
|
2488
|
+
if (checkpoint < info.max_id) uncorrelated.add(stream);
|
|
2489
|
+
}
|
|
2490
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
2491
|
+
const matches = source_matcher();
|
|
2492
|
+
await walk_streams(
|
|
2493
|
+
store(),
|
|
2494
|
+
(position) => {
|
|
2495
|
+
const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
|
|
2496
|
+
if (!has_work) return;
|
|
2497
|
+
if (position.stream.startsWith(AUTOCLOSE_TARGET_PREFIX)) return;
|
|
2498
|
+
for (const [stream, info] of stream_info) {
|
|
2499
|
+
if (matches(position.source, position.stream, stream) && position.at < info.max_id)
|
|
2500
|
+
pending_set.add(stream);
|
|
2501
|
+
}
|
|
2502
|
+
},
|
|
2503
|
+
{ limit: page_size }
|
|
2504
|
+
);
|
|
2505
|
+
const safe = [];
|
|
2506
|
+
for (const [stream] of stream_info) {
|
|
2507
|
+
if (pending_set.has(stream) || uncorrelated.has(stream))
|
|
2508
|
+
skipped.push(stream);
|
|
2509
|
+
else safe.push(stream);
|
|
2510
|
+
}
|
|
2511
|
+
return safe;
|
|
2512
|
+
}
|
|
2513
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
2514
|
+
const guarded = [];
|
|
2515
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
2516
|
+
await Promise.all(
|
|
2517
|
+
safe.map(async (stream) => {
|
|
2518
|
+
const info = stream_info.get(stream);
|
|
2519
|
+
if (info.resumed_guard) {
|
|
2520
|
+
guarded.push(stream);
|
|
2521
|
+
guard_events.set(stream, { id: info.resumed_guard.id, stream });
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const committed = await tombstone2(stream, info.version, correlation);
|
|
2525
|
+
if (committed) {
|
|
2526
|
+
guarded.push(stream);
|
|
2527
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
2528
|
+
} else {
|
|
2529
|
+
skipped.push(stream);
|
|
2530
|
+
}
|
|
2531
|
+
})
|
|
2532
|
+
);
|
|
2533
|
+
return { guarded, guard_events };
|
|
2534
|
+
}
|
|
2535
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
2536
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
2537
|
+
await Promise.all(
|
|
2538
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
2539
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
2540
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
2541
|
+
if (!owner_state) {
|
|
2542
|
+
logger.error(
|
|
2543
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
2544
|
+
);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const snap2 = await load2(owner_state, { stream });
|
|
2548
|
+
seed_states.set(stream, snap2.state);
|
|
2549
|
+
})
|
|
2550
|
+
);
|
|
2551
|
+
return seed_states;
|
|
2552
|
+
}
|
|
2553
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
2554
|
+
for (const stream of guarded) {
|
|
2555
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
2556
|
+
if (archive_fn) await archive_fn();
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
async function heads_still_guarded(guarded, guard_events, skipped) {
|
|
2560
|
+
const heads = await store().query_stats(guarded, {});
|
|
2561
|
+
return guarded.filter((stream) => {
|
|
2562
|
+
if (heads.get(stream)?.head.id === guard_events.get(stream).id)
|
|
2563
|
+
return true;
|
|
2564
|
+
skipped.push(stream);
|
|
2565
|
+
return false;
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
2569
|
+
const trunc_targets = guarded.map((stream) => {
|
|
2570
|
+
const snapshot = seed_states.get(stream);
|
|
2571
|
+
const guard = guard_events.get(stream);
|
|
2572
|
+
return {
|
|
2573
|
+
stream,
|
|
2574
|
+
snapshot,
|
|
2575
|
+
meta: {
|
|
2576
|
+
correlation,
|
|
2577
|
+
causation: {
|
|
2578
|
+
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
});
|
|
2583
|
+
const truncated = await store().truncate(trunc_targets);
|
|
2584
|
+
await Promise.all(
|
|
2585
|
+
guarded.map(async (stream) => {
|
|
2586
|
+
const entry = truncated.get(stream);
|
|
2587
|
+
const state2 = seed_states.get(stream);
|
|
2588
|
+
if (state2 && entry) {
|
|
2589
|
+
await cache().set(stream, {
|
|
2590
|
+
stream,
|
|
2591
|
+
state: state2,
|
|
2592
|
+
version: entry.committed.version,
|
|
2593
|
+
event_id: entry.committed.id,
|
|
2594
|
+
patches: 0,
|
|
2595
|
+
snaps: 1
|
|
2596
|
+
});
|
|
2597
|
+
} else {
|
|
2598
|
+
await cache().invalidate(stream);
|
|
2599
|
+
}
|
|
2600
|
+
})
|
|
2601
|
+
);
|
|
2602
|
+
return truncated;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// src/internal/correlate-cycle.ts
|
|
2606
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2607
|
+
var COLD_START_BACK_SCAN = 1e4;
|
|
2608
|
+
var CORRELATION_LEASE_MS = 5e3;
|
|
2609
|
+
var registry_key = (events) => {
|
|
2610
|
+
const shape = Object.keys(events).filter((name) => events[name].reactions.size > 0).sort().map((name) => [name, [...events[name].reactions.keys()].sort()]);
|
|
2611
|
+
return createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 32);
|
|
2612
|
+
};
|
|
2613
|
+
var PATTERN_CACHE_SIZE = 32;
|
|
2614
|
+
function report_undeclared_lane(seen, handler, target, lane, declared) {
|
|
2615
|
+
report_once(
|
|
2616
|
+
seen,
|
|
2617
|
+
`lane|${handler}|${lane}`,
|
|
2618
|
+
`Reaction "${handler}" resolved onto undeclared lane "${lane}" \u2014 for example target "${target}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. No controller claims it, so the stream would never drain \u2014 running it on "default" instead. The equivalent static \`.to({ lane })\` is rejected at build; a dynamic resolver's lane is only knowable here.`
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
function report_lane_conflict(seen, handler, target, kept, dropped) {
|
|
2622
|
+
report_once(
|
|
2623
|
+
seen,
|
|
2624
|
+
`conflict|${handler}|${kept}|${dropped}`,
|
|
2625
|
+
`Reaction "${handler}" resolved lane "${dropped}" for a stream already on "${kept}" \u2014 for example "${target}". These are conflicting lane assignments from two dynamic resolutions at equal priority. Keeping "${kept}", the lane it was first discovered on \u2014 re-laning a live stream would move it out from under a worker holding its lease. The reaction resolving "${dropped}" runs inside the "${kept}" lane's budget, and a process restricted to "${dropped}" via onlyLanes never runs it. The equivalent static declaration is rejected at build; align the resolvers, or split the target.`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
var CorrelateCycle = class {
|
|
2629
|
+
_checkpoint = -1;
|
|
2630
|
+
// Lease holder id; per instance, so a restart never inherits a stale claim.
|
|
2631
|
+
_by = randomUUID2();
|
|
2632
|
+
_key;
|
|
2633
|
+
// Local expiry of the held lease (0 = none); stale only costs a duplicate scan.
|
|
2634
|
+
_lease_until = 0;
|
|
2635
|
+
// Armed while `_raised` (bumped by a commit or notification) is ahead of
|
|
2636
|
+
// `_lowered` (the raise count the last short page saw). Starts armed.
|
|
2637
|
+
_raised = 1;
|
|
2638
|
+
_lowered = 0;
|
|
2639
|
+
// Bumped by a release (shutdown), so a scan or lease reply that was out
|
|
2640
|
+
// across one writes nothing.
|
|
2641
|
+
_epoch = 0;
|
|
2642
|
+
// Passes in flight: a restore waits for them, and refuses new ones.
|
|
2643
|
+
_passes = /* @__PURE__ */ new Set();
|
|
2644
|
+
_restoring = false;
|
|
2645
|
+
_init_promise;
|
|
2646
|
+
// Bounded by `maxSubscribedStreams`; static records live apart so eviction
|
|
2647
|
+
// never lets a resolution re-lane them.
|
|
2648
|
+
_dynamic_subscriptions;
|
|
2649
|
+
_static_subscriptions = /* @__PURE__ */ new Map();
|
|
2650
|
+
_patterns = new LruMap(PATTERN_CACHE_SIZE);
|
|
2651
|
+
_registry;
|
|
2652
|
+
_static_targets;
|
|
2653
|
+
_cd;
|
|
2654
|
+
_on_init;
|
|
2655
|
+
_declared_lanes;
|
|
2656
|
+
// Report-once keys; per instance because `internal/` holds no module state.
|
|
2657
|
+
_reported = /* @__PURE__ */ new Set();
|
|
2658
|
+
constructor({
|
|
2659
|
+
registry,
|
|
2660
|
+
static_targets,
|
|
2661
|
+
cd,
|
|
2662
|
+
max_subscribed_streams,
|
|
2663
|
+
declared_lanes,
|
|
2664
|
+
shard,
|
|
2665
|
+
on_init
|
|
2666
|
+
}) {
|
|
2667
|
+
this._key = shard ? `${registry_key(registry.events)}:${[...shard].sort().join(",")}` : registry_key(registry.events);
|
|
2668
|
+
this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
|
|
2669
|
+
this._registry = registry;
|
|
2670
|
+
this._declared_lanes = declared_lanes;
|
|
2671
|
+
this._static_targets = static_targets;
|
|
2672
|
+
this._cd = cd;
|
|
2673
|
+
this._on_init = on_init;
|
|
2674
|
+
}
|
|
2675
|
+
get checkpoint() {
|
|
2676
|
+
return this._checkpoint;
|
|
2677
|
+
}
|
|
2678
|
+
/** A commit (local or remote) may have produced uncorrelated events. */
|
|
2679
|
+
arm() {
|
|
2680
|
+
this._raised++;
|
|
2681
|
+
}
|
|
2682
|
+
async _init() {
|
|
2683
|
+
this._init_promise ??= this._run_init().catch((error) => {
|
|
2684
|
+
this._init_promise = void 0;
|
|
2685
|
+
throw error;
|
|
2686
|
+
});
|
|
2687
|
+
await this._init_promise;
|
|
2688
|
+
}
|
|
2689
|
+
async _run_init() {
|
|
2690
|
+
const { watermark, correlated_at } = await this._cd.subscribe([
|
|
2691
|
+
...this._static_targets
|
|
2692
|
+
]);
|
|
2693
|
+
this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - COLD_START_BACK_SCAN);
|
|
2694
|
+
for (const { stream, priority = 0, lane } of this._static_targets) {
|
|
2695
|
+
this._static_subscriptions.set(stream, {
|
|
2696
|
+
priority,
|
|
2697
|
+
lane
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
await this._on_init?.();
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Replace the log with no pass in flight, then cold-start: a pass that
|
|
2704
|
+
* read the old log writes before the restore, never into the new one.
|
|
2705
|
+
*/
|
|
2706
|
+
async restart(restore) {
|
|
2707
|
+
this._restoring = true;
|
|
2708
|
+
try {
|
|
2709
|
+
await Promise.allSettled(this._passes);
|
|
2710
|
+
await restore();
|
|
2711
|
+
} finally {
|
|
2712
|
+
this._restoring = false;
|
|
2713
|
+
}
|
|
2714
|
+
this._init_promise = void 0;
|
|
2715
|
+
this._checkpoint = -1;
|
|
2716
|
+
this._dynamic_subscriptions.clear();
|
|
2717
|
+
this._static_subscriptions.clear();
|
|
2718
|
+
}
|
|
2719
|
+
/** Forget dynamic targets whose rows a full close deleted, so they re-subscribe. */
|
|
2720
|
+
forget_subscribed(streams) {
|
|
2721
|
+
for (const stream of streams) this._dynamic_subscriptions.delete(stream);
|
|
2722
|
+
}
|
|
2723
|
+
// Would the target's own `fetch` (filtered by `source`) return this event?
|
|
2724
|
+
_in_fetch_window(source, stream) {
|
|
2725
|
+
if (source === void 0 || source === stream) return true;
|
|
2726
|
+
if (is_literal_source(source)) return false;
|
|
2727
|
+
let pattern = this._patterns.get(source);
|
|
2728
|
+
if (!pattern) {
|
|
2729
|
+
pattern = new RegExp(source);
|
|
2730
|
+
this._patterns.set(source, pattern);
|
|
2731
|
+
}
|
|
2732
|
+
return pattern.test(stream);
|
|
2733
|
+
}
|
|
2734
|
+
/** Scan past the checkpoint, subscribe new targets and raise their work marks. */
|
|
2735
|
+
async correlate(limit, lease = false) {
|
|
2736
|
+
if (this._restoring) return { kind: "idle", last_id: this._checkpoint };
|
|
2737
|
+
const pass = this._scan(limit, lease);
|
|
2738
|
+
this._passes.add(pass);
|
|
2739
|
+
try {
|
|
2740
|
+
return await pass;
|
|
2741
|
+
} finally {
|
|
2742
|
+
this._passes.delete(pass);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
async _scan(limit, lease) {
|
|
2746
|
+
await this._init();
|
|
2747
|
+
const epoch = this._epoch;
|
|
2748
|
+
const idle = () => ({
|
|
2749
|
+
kind: "idle",
|
|
2750
|
+
last_id: this._checkpoint
|
|
2751
|
+
});
|
|
2752
|
+
if (this._raised === this._lowered) return idle();
|
|
2753
|
+
const raised = this._raised;
|
|
2754
|
+
const now = Date.now();
|
|
2755
|
+
if (lease && now >= this._lease_until - CORRELATION_LEASE_MS / 2) {
|
|
2756
|
+
const { correlating } = await this._cd.subscribe([], void 0, {
|
|
2757
|
+
key: this._key,
|
|
2758
|
+
by: this._by,
|
|
2759
|
+
millis: CORRELATION_LEASE_MS
|
|
2760
|
+
});
|
|
2761
|
+
if (this._epoch !== epoch) return idle();
|
|
2762
|
+
this._lease_until = correlating ? now + CORRELATION_LEASE_MS : 0;
|
|
2763
|
+
if (correlating === false) return idle();
|
|
2764
|
+
}
|
|
2765
|
+
const after = this._checkpoint;
|
|
2766
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
2767
|
+
let last_id = after;
|
|
2768
|
+
let stopped = false;
|
|
2769
|
+
const found = await store().query(
|
|
2770
|
+
(event) => {
|
|
2771
|
+
if (stopped) return;
|
|
2772
|
+
const register = this._registry.events[event.name];
|
|
2773
|
+
if (register) {
|
|
2774
|
+
for (const reaction of register.reactions.values()) {
|
|
2775
|
+
let resolved;
|
|
2776
|
+
try {
|
|
2777
|
+
resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
2778
|
+
} catch (error) {
|
|
2779
|
+
stopped = true;
|
|
2780
|
+
report_once(
|
|
2781
|
+
this._reported,
|
|
2782
|
+
`resolver|${reaction.handler.name}|${event.id}`,
|
|
2783
|
+
`Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}. Correlation stops before this event until the resolver stops throwing; work already found keeps draining.`
|
|
2784
|
+
);
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
if (!resolved) continue;
|
|
2788
|
+
let lane = resolved.lane;
|
|
2789
|
+
if (lane !== void 0 && !this._declared_lanes.has(lane)) {
|
|
2790
|
+
report_undeclared_lane(
|
|
2791
|
+
this._reported,
|
|
2792
|
+
reaction.handler.name,
|
|
2793
|
+
resolved.target,
|
|
2794
|
+
lane,
|
|
2795
|
+
this._declared_lanes
|
|
2796
|
+
);
|
|
2797
|
+
lane = void 0;
|
|
2798
|
+
}
|
|
2799
|
+
const fixed = this._static_subscriptions.get(resolved.target);
|
|
2800
|
+
const recorded = fixed ?? this._dynamic_subscriptions.get(resolved.target);
|
|
2801
|
+
const priority = resolved.priority ?? 0;
|
|
2802
|
+
const upgraded = !recorded || !fixed && priority > recorded.priority;
|
|
2803
|
+
const carried = upgraded ? { priority, lane } : { priority: recorded.priority, lane: recorded.lane };
|
|
2804
|
+
const entry = correlated.get(resolved.target) || {
|
|
2805
|
+
source: resolved.source,
|
|
2806
|
+
priority: carried.priority,
|
|
2807
|
+
lane: carried.lane,
|
|
2808
|
+
upgraded,
|
|
2809
|
+
missed: !recorded,
|
|
2810
|
+
handler: reaction.handler.name,
|
|
2811
|
+
correlated_at: void 0
|
|
2812
|
+
};
|
|
2813
|
+
const seen_in_scan = correlated.has(resolved.target);
|
|
2814
|
+
const held_lane = (seen_in_scan ? entry.lane : recorded?.lane) ?? DEFAULT_LANE;
|
|
2815
|
+
const held_priority = seen_in_scan ? entry.priority : recorded?.priority;
|
|
2816
|
+
const resolved_lane = lane ?? DEFAULT_LANE;
|
|
2817
|
+
if (held_priority === priority && held_lane !== resolved_lane)
|
|
2818
|
+
report_lane_conflict(
|
|
2819
|
+
this._reported,
|
|
2820
|
+
reaction.handler.name,
|
|
2821
|
+
resolved.target,
|
|
2822
|
+
held_lane,
|
|
2823
|
+
resolved_lane
|
|
2824
|
+
);
|
|
2825
|
+
if (carried.priority > entry.priority) {
|
|
2826
|
+
entry.priority = carried.priority;
|
|
2827
|
+
entry.lane = carried.lane;
|
|
2828
|
+
entry.upgraded = upgraded;
|
|
2829
|
+
entry.handler = reaction.handler.name;
|
|
2830
|
+
}
|
|
2831
|
+
if (this._in_fetch_window(resolved.source, event.stream))
|
|
2832
|
+
entry.correlated_at = event.id;
|
|
2833
|
+
correlated.set(resolved.target, entry);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
last_id = event.id;
|
|
2837
|
+
},
|
|
2838
|
+
// Resolvers see `pii: null`: correlate runs actor-less, and one
|
|
2839
|
+
// undecryptable row would otherwise stop every stream's reactions.
|
|
2840
|
+
{ after, limit, with_pii: false }
|
|
2841
|
+
);
|
|
2842
|
+
if (this._declared_lanes.size > 1) await this._recall(correlated);
|
|
2843
|
+
const read = last_id > after;
|
|
2844
|
+
if (this._epoch !== epoch) return idle();
|
|
2845
|
+
const streams = [];
|
|
2846
|
+
for (const [stream, entry] of correlated) {
|
|
2847
|
+
if (entry.upgraded || entry.correlated_at !== void 0)
|
|
2848
|
+
streams.push({
|
|
2849
|
+
stream,
|
|
2850
|
+
source: entry.source,
|
|
2851
|
+
priority: entry.priority,
|
|
2852
|
+
lane: entry.lane,
|
|
2853
|
+
correlated_at: entry.correlated_at
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
if (streams.length) {
|
|
2857
|
+
const renewed_at = Date.now();
|
|
2858
|
+
const { subscribed, correlating } = await this._cd.subscribe(
|
|
2859
|
+
streams,
|
|
2860
|
+
last_id,
|
|
2861
|
+
lease ? { key: this._key, by: this._by, millis: CORRELATION_LEASE_MS } : void 0
|
|
2862
|
+
);
|
|
2863
|
+
if (lease && correlating !== false)
|
|
2864
|
+
this._lease_until = renewed_at + CORRELATION_LEASE_MS;
|
|
2865
|
+
const marked = streams.filter(
|
|
2866
|
+
(entry) => entry.correlated_at !== void 0
|
|
2867
|
+
).length;
|
|
2868
|
+
this._checkpoint = last_id;
|
|
2869
|
+
for (const { stream, priority, lane } of streams) {
|
|
2870
|
+
if (correlated.get(stream)?.upgraded)
|
|
2871
|
+
this._dynamic_subscriptions.set(stream, {
|
|
2872
|
+
priority,
|
|
2873
|
+
lane
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
return { kind: "scanned", last_id, subscribed, marked, read };
|
|
2877
|
+
}
|
|
2878
|
+
this._checkpoint = last_id;
|
|
2879
|
+
if (!stopped && found !== limit) this._lowered = raised;
|
|
2880
|
+
return { kind: "scanned", last_id, subscribed: 0, marked: 0, read };
|
|
2881
|
+
}
|
|
2882
|
+
// Read the row of each target this scan had no record of (new or evicted),
|
|
2883
|
+
// so a resolution that doesn't beat its priority keeps the row's lane.
|
|
2884
|
+
// Only called with more than one lane declared.
|
|
2885
|
+
async _recall(correlated) {
|
|
2886
|
+
const missed = [...correlated].filter(([, entry]) => entry.missed);
|
|
2887
|
+
await Promise.all(
|
|
2888
|
+
missed.map(async ([stream, entry]) => {
|
|
2889
|
+
let row;
|
|
2890
|
+
await store().query_streams(
|
|
2891
|
+
(position) => {
|
|
2892
|
+
row = position;
|
|
2893
|
+
},
|
|
2894
|
+
{ stream, stream_exact: true, limit: 1 }
|
|
2895
|
+
);
|
|
2896
|
+
if (!row) return;
|
|
2897
|
+
this._dynamic_subscriptions.set(stream, {
|
|
2898
|
+
priority: row.priority,
|
|
2899
|
+
lane: row.lane
|
|
2900
|
+
});
|
|
2901
|
+
if (entry.priority > row.priority) return;
|
|
2902
|
+
const held_lane = row.lane ?? DEFAULT_LANE;
|
|
2903
|
+
const resolved_lane = entry.lane ?? DEFAULT_LANE;
|
|
2904
|
+
if (entry.priority === row.priority && held_lane !== resolved_lane)
|
|
2905
|
+
report_lane_conflict(
|
|
2906
|
+
this._reported,
|
|
2907
|
+
entry.handler,
|
|
2908
|
+
stream,
|
|
2909
|
+
held_lane,
|
|
2910
|
+
resolved_lane
|
|
2911
|
+
);
|
|
2912
|
+
entry.priority = row.priority;
|
|
2913
|
+
entry.lane = row.lane;
|
|
2914
|
+
entry.upgraded = false;
|
|
2915
|
+
})
|
|
2916
|
+
);
|
|
2480
2917
|
}
|
|
2481
|
-
|
|
2482
|
-
async
|
|
2918
|
+
/** Release the lease early by re-acquiring it for zero ms. Best-effort. */
|
|
2919
|
+
async release_correlation() {
|
|
2483
2920
|
try {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
leading,
|
|
2491
|
-
eventLimit,
|
|
2492
|
-
leaseMillis,
|
|
2493
|
-
misrouted: this._misrouted,
|
|
2494
|
-
store_failing: this._deps.breaker.failing
|
|
2921
|
+
this._lease_until = 0;
|
|
2922
|
+
this._epoch++;
|
|
2923
|
+
await this._cd.subscribe([], void 0, {
|
|
2924
|
+
key: this._key,
|
|
2925
|
+
by: this._by,
|
|
2926
|
+
millis: 0
|
|
2495
2927
|
});
|
|
2496
|
-
if (!cycle) {
|
|
2497
|
-
this._defer.prune(started);
|
|
2498
|
-
this._deps.breaker.passed();
|
|
2499
|
-
this._lowered = raised;
|
|
2500
|
-
return EMPTY_DRAIN;
|
|
2501
|
-
}
|
|
2502
|
-
const { leased, fetched, handled, acked, blocked, closeable } = cycle;
|
|
2503
|
-
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
2504
|
-
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
2505
|
-
for (const lease of acked) this._defer.delete(lease.stream);
|
|
2506
|
-
for (const lease of blocked) this._defer.delete(lease.stream);
|
|
2507
|
-
for (const h of handled) {
|
|
2508
|
-
const next = h.defer ?? h.next_attempt_at;
|
|
2509
|
-
if (next !== void 0) this._defer.set(h.lease.stream, next);
|
|
2510
|
-
}
|
|
2511
|
-
if (acked.length) this._deps.on_acked(acked);
|
|
2512
|
-
if (closeable.length) await this._deps.on_close(closeable);
|
|
2513
|
-
this._deps.breaker.passed();
|
|
2514
|
-
return { fetched, leased, acked, blocked };
|
|
2515
2928
|
} catch (error) {
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
Date.now() + Math.max(leaseMillis, this._deps.breaker.cooldown_ms)
|
|
2929
|
+
log().warn(
|
|
2930
|
+
`Could not hand back the correlation lease during shutdown; it expires on its own within ${CORRELATION_LEASE_MS}ms and correlation resumes normally after that. On a single-writer store this usually means another connection holds the database. Cause: ${String(error.cause ?? error)}`
|
|
2519
2931
|
);
|
|
2520
|
-
this._deps.breaker.failed(Date.now(), error);
|
|
2521
|
-
return EMPTY_DRAIN;
|
|
2522
|
-
} finally {
|
|
2523
|
-
if (!this._stopped) this._defer.schedule();
|
|
2524
2932
|
}
|
|
2525
2933
|
}
|
|
2526
2934
|
};
|
|
@@ -2630,210 +3038,34 @@ function make_fold_handler(me, flush, config2, sensitive_fields) {
|
|
|
2630
3038
|
fold = await first_sight(stream);
|
|
2631
3039
|
}
|
|
2632
3040
|
const contiguous = event.version === fold.version + 1;
|
|
2633
|
-
if (event.id > fold.event_id && !contiguous) {
|
|
2634
|
-
cache2.delete(stream);
|
|
2635
|
-
fold = await first_sight(stream);
|
|
2636
|
-
}
|
|
2637
|
-
if (event.id > fold.event_id) {
|
|
2638
|
-
const reducer = me.patch[event.name];
|
|
2639
|
-
fold.state = bare_patch(
|
|
2640
|
-
me,
|
|
2641
|
-
fold.state,
|
|
2642
|
-
reducer(event, fold.state),
|
|
2643
|
-
event
|
|
2644
|
-
);
|
|
2645
|
-
fold.version = event.version;
|
|
2646
|
-
fold.event_id = event.id;
|
|
2647
|
-
fold.patches++;
|
|
2648
|
-
fold.dirty = true;
|
|
2649
|
-
} else {
|
|
2650
|
-
fold.dirty = true;
|
|
2651
|
-
}
|
|
2652
|
-
if (++folded_since_flush >= config2.flushEvery) {
|
|
2653
|
-
await flush_dirty();
|
|
2654
|
-
folded_since_flush = 0;
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
await flush_dirty();
|
|
2658
|
-
};
|
|
2659
|
-
Object.defineProperty(handler, FOLD_RESET, { value: () => cache2.clear() });
|
|
2660
|
-
return handler;
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
// src/internal/defer-config.ts
|
|
2664
|
-
import { z } from "zod";
|
|
2665
|
-
var DeferDurationSchema = z.object({
|
|
2666
|
-
days: z.number().positive().optional(),
|
|
2667
|
-
hours: z.number().positive().optional(),
|
|
2668
|
-
minutes: z.number().positive().optional()
|
|
2669
|
-
}).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
|
|
2670
|
-
message: "defer: a duration needs at least one of days, hours, or minutes"
|
|
2671
|
-
});
|
|
2672
|
-
var DeferWhenSchema = z.object({
|
|
2673
|
-
after: DeferDurationSchema.optional(),
|
|
2674
|
-
at: z.date().optional()
|
|
2675
|
-
}).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
|
|
2676
|
-
message: "defer: specify exactly one of `after` or `at`"
|
|
2677
|
-
});
|
|
2678
|
-
function duration_ms(d) {
|
|
2679
|
-
return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
|
|
2680
|
-
}
|
|
2681
|
-
function resolve_defer_at(when, event) {
|
|
2682
|
-
const parsed = DeferWhenSchema.parse(when);
|
|
2683
|
-
if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
|
|
2684
|
-
return parsed.at.getTime();
|
|
2685
|
-
}
|
|
2686
|
-
function assert_defer_when(when) {
|
|
2687
|
-
DeferWhenSchema.parse(when);
|
|
2688
|
-
}
|
|
2689
|
-
function make_deferred(handler, schedule) {
|
|
2690
|
-
const call = handler;
|
|
2691
|
-
const deferred = async (event, stream, app) => {
|
|
2692
|
-
const when = typeof schedule === "function" ? schedule(event) : schedule;
|
|
2693
|
-
if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
|
|
2694
|
-
return call(event, stream, app);
|
|
2695
|
-
};
|
|
2696
|
-
Object.defineProperty(deferred, "name", { value: handler.name });
|
|
2697
|
-
return deferred;
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
// src/internal/reaction-dispatch.ts
|
|
2701
|
-
var CloseSignal = class extends Error {
|
|
2702
|
-
/**
|
|
2703
|
-
* Stream to close. Omitted → the reaction's own lease stream (a self-close).
|
|
2704
|
-
* The synthesized autoclose reaction sets it to the aggregate stream, since
|
|
2705
|
-
* its lease runs on a synthetic `__autoclose__` target.
|
|
2706
|
-
*/
|
|
2707
|
-
stream;
|
|
2708
|
-
/** Optional archive callback to run while the stream is guarded. */
|
|
2709
|
-
archive;
|
|
2710
|
-
/**
|
|
2711
|
-
* Watermark to ack the requesting reaction to before the close runs. The
|
|
2712
|
-
* close-cycle safety guard skips a stream whose subscriptions (matched by
|
|
2713
|
-
* source) lag the head; the autoclose reaction's `source` is the aggregate,
|
|
2714
|
-
* so it must advance its own watermark to the live head id it evaluated
|
|
2715
|
-
* against — otherwise it blocks its own close. Defaults to the triggering
|
|
2716
|
-
* event id when omitted.
|
|
2717
|
-
*/
|
|
2718
|
-
at;
|
|
2719
|
-
/**
|
|
2720
|
-
* Windowed close: prune events older than this cutoff behind
|
|
2721
|
-
* the closest safe snapshot instead of retiring the stream. Thrown by
|
|
2722
|
-
* the autoclose reaction of a `.autocloses({ keep })` state; flows
|
|
2723
|
-
* into {@link CloseTarget.before}. Omitted → a full close.
|
|
2724
|
-
*/
|
|
2725
|
-
before;
|
|
2726
|
-
constructor(opts) {
|
|
2727
|
-
super("reaction requested close");
|
|
2728
|
-
this.name = "CloseSignal";
|
|
2729
|
-
this.stream = opts?.stream;
|
|
2730
|
-
this.archive = opts?.archive;
|
|
2731
|
-
this.at = opts?.at;
|
|
2732
|
-
this.before = opts?.before;
|
|
2733
|
-
}
|
|
2734
|
-
};
|
|
2735
|
-
function build_handle(deps) {
|
|
2736
|
-
const { logger, reaction_scope } = deps;
|
|
2737
|
-
return async (lease, payloads) => {
|
|
2738
|
-
if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
|
|
2739
|
-
const stream = lease.stream;
|
|
2740
|
-
const last_index_of = /* @__PURE__ */ new Map();
|
|
2741
|
-
for (let i = 0; i < payloads.length; i++)
|
|
2742
|
-
last_index_of.set(payloads[i].event.id, i);
|
|
2743
|
-
let at = lease.at;
|
|
2744
|
-
let handled = 0;
|
|
2745
|
-
if (lease.retry > 0)
|
|
2746
|
-
logger.warn(
|
|
2747
|
-
`Retrying ${stream}@${payloads.at(0).event.id} (${lease.retry}).`
|
|
2748
|
-
);
|
|
2749
|
-
for (let i = 0; i < payloads.length; i++) {
|
|
2750
|
-
const payload = payloads[i];
|
|
2751
|
-
const { event, handler } = payload;
|
|
2752
|
-
try {
|
|
2753
|
-
await reaction_scope.run(
|
|
2754
|
-
event,
|
|
2755
|
-
() => handler(event, stream, reaction_scope.app)
|
|
2756
|
-
);
|
|
2757
|
-
if (last_index_of.get(event.id) === i) {
|
|
2758
|
-
at = event.id;
|
|
2759
|
-
handled++;
|
|
2760
|
-
}
|
|
2761
|
-
} catch (error) {
|
|
2762
|
-
if (error instanceof DeferSignal) {
|
|
2763
|
-
let defer;
|
|
2764
|
-
try {
|
|
2765
|
-
defer = resolve_defer_at(error.when, event);
|
|
2766
|
-
} catch (invalid) {
|
|
2767
|
-
return finalize(
|
|
2768
|
-
lease,
|
|
2769
|
-
handled,
|
|
2770
|
-
at,
|
|
2771
|
-
invalid,
|
|
2772
|
-
payload.options,
|
|
2773
|
-
logger,
|
|
2774
|
-
event.id
|
|
2775
|
-
);
|
|
2776
|
-
}
|
|
2777
|
-
return { lease, handled, acked_at: at, defer };
|
|
2778
|
-
}
|
|
2779
|
-
if (error instanceof CloseSignal)
|
|
2780
|
-
return {
|
|
2781
|
-
lease,
|
|
2782
|
-
handled: handled + 1,
|
|
2783
|
-
// Advance to the live head the handler evaluated against (when
|
|
2784
|
-
// provided) so the close-cycle guard sees this reaction caught up.
|
|
2785
|
-
acked_at: error.at ?? event.id,
|
|
2786
|
-
// Close the signalled stream (the autoclose reaction's aggregate,
|
|
2787
|
-
// which differs from its synthetic lease stream); a self-closing
|
|
2788
|
-
// user reaction omits it and closes its own lease stream. A
|
|
2789
|
-
// carried `before` makes it a windowed close (prune, not retire).
|
|
2790
|
-
close: {
|
|
2791
|
-
stream: error.stream ?? stream,
|
|
2792
|
-
archive: error.archive,
|
|
2793
|
-
before: error.before
|
|
2794
|
-
}
|
|
2795
|
-
};
|
|
2796
|
-
return finalize(
|
|
2797
|
-
lease,
|
|
2798
|
-
handled,
|
|
2799
|
-
at,
|
|
2800
|
-
error,
|
|
2801
|
-
payload.options,
|
|
2802
|
-
logger,
|
|
2803
|
-
event.id
|
|
3041
|
+
if (event.id > fold.event_id && !contiguous) {
|
|
3042
|
+
cache2.delete(stream);
|
|
3043
|
+
fold = await first_sight(stream);
|
|
3044
|
+
}
|
|
3045
|
+
if (event.id > fold.event_id) {
|
|
3046
|
+
const reducer = me.patch[event.name];
|
|
3047
|
+
fold.state = bare_patch(
|
|
3048
|
+
me,
|
|
3049
|
+
fold.state,
|
|
3050
|
+
reducer(event, fold.state),
|
|
3051
|
+
event
|
|
2804
3052
|
);
|
|
3053
|
+
fold.version = event.version;
|
|
3054
|
+
fold.event_id = event.id;
|
|
3055
|
+
fold.patches++;
|
|
3056
|
+
fold.dirty = true;
|
|
3057
|
+
} else {
|
|
3058
|
+
fold.dirty = true;
|
|
3059
|
+
}
|
|
3060
|
+
if (++folded_since_flush >= config2.flushEvery) {
|
|
3061
|
+
await flush_dirty();
|
|
3062
|
+
folded_since_flush = 0;
|
|
2805
3063
|
}
|
|
2806
3064
|
}
|
|
2807
|
-
|
|
2808
|
-
};
|
|
2809
|
-
}
|
|
2810
|
-
function build_handle_batch(logger) {
|
|
2811
|
-
return async (lease, payloads, batchHandler) => {
|
|
2812
|
-
const stream = lease.stream;
|
|
2813
|
-
const events = [];
|
|
2814
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2815
|
-
for (const p of payloads) {
|
|
2816
|
-
if (seen.has(p.event.id)) continue;
|
|
2817
|
-
seen.add(p.event.id);
|
|
2818
|
-
events.push(p.event);
|
|
2819
|
-
}
|
|
2820
|
-
const options = payloads[0].options;
|
|
2821
|
-
if (lease.retry > 0)
|
|
2822
|
-
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
2823
|
-
try {
|
|
2824
|
-
await batchHandler(events, stream);
|
|
2825
|
-
return finalize(
|
|
2826
|
-
lease,
|
|
2827
|
-
events.length,
|
|
2828
|
-
events.at(-1).id,
|
|
2829
|
-
void 0,
|
|
2830
|
-
options,
|
|
2831
|
-
logger
|
|
2832
|
-
);
|
|
2833
|
-
} catch (error) {
|
|
2834
|
-
return finalize(lease, 0, lease.at, error, options, logger);
|
|
2835
|
-
}
|
|
3065
|
+
await flush_dirty();
|
|
2836
3066
|
};
|
|
3067
|
+
Object.defineProperty(handler, FOLD_RESET, { value: () => cache2.clear() });
|
|
3068
|
+
return handler;
|
|
2837
3069
|
}
|
|
2838
3070
|
|
|
2839
3071
|
// src/internal/settle.ts
|
|
@@ -3955,214 +4187,6 @@ var Act = class {
|
|
|
3955
4187
|
}
|
|
3956
4188
|
};
|
|
3957
4189
|
|
|
3958
|
-
// src/internal/autoclose-policy.ts
|
|
3959
|
-
import { z as z2 } from "zod";
|
|
3960
|
-
var DAY_MS = 864e5;
|
|
3961
|
-
var MIN_AFTER_DAYS = 1 / 1440;
|
|
3962
|
-
var MIN_KEEP_DAYS = 1;
|
|
3963
|
-
function days_after(date, days) {
|
|
3964
|
-
return new Date(date.getTime() + days * DAY_MS);
|
|
3965
|
-
}
|
|
3966
|
-
function days_before_now(days) {
|
|
3967
|
-
return new Date(Date.now() - days * DAY_MS);
|
|
3968
|
-
}
|
|
3969
|
-
var window_schema = (field, min_days, floor) => z2.object({
|
|
3970
|
-
days: z2.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
|
|
3971
|
-
}).refine((o) => o.days >= min_days, {
|
|
3972
|
-
message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
|
|
3973
|
-
});
|
|
3974
|
-
var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
|
|
3975
|
-
var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
|
|
3976
|
-
var IsSchema = z2.union([
|
|
3977
|
-
z2.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
3978
|
-
z2.array(
|
|
3979
|
-
z2.string().min(1, "autocloses: is entries must be non-empty event names")
|
|
3980
|
-
).min(1, "autocloses: is must include at least one event name").readonly()
|
|
3981
|
-
]);
|
|
3982
|
-
var ReachesSchema = z2.number({ message: "autocloses: reaches must be a number" }).int("autocloses: reaches must be an integer").min(1, "autocloses: reaches must be >= 1");
|
|
3983
|
-
var OrBlockSchema = z2.object({
|
|
3984
|
-
after: AfterSchema.optional(),
|
|
3985
|
-
is: IsSchema.optional(),
|
|
3986
|
-
reaches: ReachesSchema.optional()
|
|
3987
|
-
}).strict().refine(
|
|
3988
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
3989
|
-
{
|
|
3990
|
-
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
3991
|
-
}
|
|
3992
|
-
);
|
|
3993
|
-
var AutoclosePolicySchema = z2.object({
|
|
3994
|
-
after: AfterSchema.optional(),
|
|
3995
|
-
is: IsSchema.optional(),
|
|
3996
|
-
reaches: ReachesSchema.optional(),
|
|
3997
|
-
or: OrBlockSchema.optional(),
|
|
3998
|
-
keep: KeepSchema.optional()
|
|
3999
|
-
}).strict().refine(
|
|
4000
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
|
|
4001
|
-
{
|
|
4002
|
-
message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
|
|
4003
|
-
}
|
|
4004
|
-
);
|
|
4005
|
-
function compile_after(after) {
|
|
4006
|
-
return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
|
|
4007
|
-
}
|
|
4008
|
-
function compile_is(is) {
|
|
4009
|
-
const set = new Set(typeof is === "string" ? [is] : is);
|
|
4010
|
-
return (_stream, head) => set.has(head.name);
|
|
4011
|
-
}
|
|
4012
|
-
function compile_reaches(reaches) {
|
|
4013
|
-
return (_stream, _head, count) => count >= reaches;
|
|
4014
|
-
}
|
|
4015
|
-
function policy_min_after_days(options) {
|
|
4016
|
-
const parsed = AutoclosePolicySchema.parse(options);
|
|
4017
|
-
const windows = [];
|
|
4018
|
-
if (parsed.after) windows.push(parsed.after.days);
|
|
4019
|
-
if (parsed.or?.after) windows.push(parsed.or.after.days);
|
|
4020
|
-
return windows.length ? Math.min(...windows) : void 0;
|
|
4021
|
-
}
|
|
4022
|
-
function policy_after_windows(options) {
|
|
4023
|
-
const parsed = AutoclosePolicySchema.parse(options);
|
|
4024
|
-
return [parsed.after, parsed.or?.after].flatMap((w) => w ? [w.days] : []);
|
|
4025
|
-
}
|
|
4026
|
-
function policy_keep_days(options) {
|
|
4027
|
-
const parsed = AutoclosePolicySchema.parse(options);
|
|
4028
|
-
return parsed.keep?.days;
|
|
4029
|
-
}
|
|
4030
|
-
function compile_autoclose_policy(options) {
|
|
4031
|
-
const parsed = AutoclosePolicySchema.parse(options);
|
|
4032
|
-
const and_preds = [];
|
|
4033
|
-
if (parsed.after) and_preds.push(compile_after(parsed.after));
|
|
4034
|
-
if (parsed.is) and_preds.push(compile_is(parsed.is));
|
|
4035
|
-
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
4036
|
-
const or_preds = [];
|
|
4037
|
-
if (parsed.or) {
|
|
4038
|
-
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
4039
|
-
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
4040
|
-
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
4041
|
-
}
|
|
4042
|
-
return (stream, head, count) => {
|
|
4043
|
-
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
4044
|
-
return true;
|
|
4045
|
-
}
|
|
4046
|
-
if (or_preds.some((p) => p(stream, head, count))) {
|
|
4047
|
-
return true;
|
|
4048
|
-
}
|
|
4049
|
-
return false;
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
// src/internal/autoclose-window.ts
|
|
4054
|
-
function hour_in_zone(now, timeZone) {
|
|
4055
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
4056
|
-
timeZone,
|
|
4057
|
-
hour: "2-digit",
|
|
4058
|
-
hourCycle: "h23"
|
|
4059
|
-
}).formatToParts(now);
|
|
4060
|
-
return Number(parts.find((p) => p.type === "hour")?.value);
|
|
4061
|
-
}
|
|
4062
|
-
function hour_in_range(hour, start, end) {
|
|
4063
|
-
return start < end ? hour >= start && hour < end : hour >= start || hour < end;
|
|
4064
|
-
}
|
|
4065
|
-
function is_dst_gap_open(now, timeZone, start) {
|
|
4066
|
-
const hour = hour_in_zone(now, timeZone);
|
|
4067
|
-
if (hour <= start) return false;
|
|
4068
|
-
const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
|
|
4069
|
-
return prev === start - 1 && hour === start + 1;
|
|
4070
|
-
}
|
|
4071
|
-
function in_autoclose_window(window, now) {
|
|
4072
|
-
if (!window) return true;
|
|
4073
|
-
const hour = hour_in_zone(now, window.timeZone);
|
|
4074
|
-
return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
|
|
4075
|
-
}
|
|
4076
|
-
function next_window_open(window, now) {
|
|
4077
|
-
const start_ms = now.getTime();
|
|
4078
|
-
for (let h = 0; h <= 48; h++) {
|
|
4079
|
-
const candidate = new Date(start_ms + h * 36e5);
|
|
4080
|
-
if (hour_in_zone(candidate, window.timeZone) === window.start)
|
|
4081
|
-
return candidate;
|
|
4082
|
-
const boundary = new Date(
|
|
4083
|
-
candidate.getTime() - candidate.getTime() % 36e5
|
|
4084
|
-
);
|
|
4085
|
-
if (boundary.getTime() >= start_ms && is_dst_gap_open(boundary, window.timeZone, window.start))
|
|
4086
|
-
return boundary;
|
|
4087
|
-
}
|
|
4088
|
-
return new Date(start_ms + 864e5);
|
|
4089
|
-
}
|
|
4090
|
-
|
|
4091
|
-
// src/internal/autoclose-reaction.ts
|
|
4092
|
-
var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
|
|
4093
|
-
function synthesize_autoclose_reactions(registry, states, read_window) {
|
|
4094
|
-
for (const st of states.values()) {
|
|
4095
|
-
const predicate = st.autoclose;
|
|
4096
|
-
if (!predicate) continue;
|
|
4097
|
-
const after_windows = st.autoclose_after_windows;
|
|
4098
|
-
const keep_days = st.autoclose_keep_days;
|
|
4099
|
-
const archiver = st.archive;
|
|
4100
|
-
const reaction = {
|
|
4101
|
-
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
4102
|
-
// per-aggregate `__autoclose__` key — so the autoclose reaction never
|
|
4103
|
-
// shares a watermark with the aggregate's own reactions. A shared
|
|
4104
|
-
// watermark would let autoclose's defer short-circuit the aggregate's
|
|
4105
|
-
// other reactions (the "a defer affects all reactions on a stream"
|
|
4106
|
-
// hazard). The close still targets the aggregate (`source`).
|
|
4107
|
-
resolver: (e) => ({
|
|
4108
|
-
target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
|
|
4109
|
-
source: e.stream
|
|
4110
|
-
}),
|
|
4111
|
-
// Never block on autoclose: a transient query/store error should retry,
|
|
4112
|
-
// not quarantine the synthetic stream.
|
|
4113
|
-
options: { blockOnError: false, maxRetries: DEFAULT_MAX_RETRIES },
|
|
4114
|
-
handler: async (event) => {
|
|
4115
|
-
const aggregate = event.stream;
|
|
4116
|
-
const window = read_window();
|
|
4117
|
-
if (!in_autoclose_window(window, /* @__PURE__ */ new Date()))
|
|
4118
|
-
throw new DeferSignal({ at: next_window_open(window, /* @__PURE__ */ new Date()) });
|
|
4119
|
-
const stats = await store().query_stats([aggregate], {
|
|
4120
|
-
count: true,
|
|
4121
|
-
tail: keep_days !== void 0 ? true : void 0,
|
|
4122
|
-
exclude: [TOMBSTONE_EVENT, SNAP_EVENT]
|
|
4123
|
-
});
|
|
4124
|
-
const entry = stats.get(aggregate);
|
|
4125
|
-
if (!entry) return;
|
|
4126
|
-
const head = entry.head;
|
|
4127
|
-
if (predicate(aggregate, head, entry.count))
|
|
4128
|
-
throw new CloseSignal({
|
|
4129
|
-
stream: aggregate,
|
|
4130
|
-
archive: archiver ? () => archiver(aggregate, head) : void 0,
|
|
4131
|
-
// Ack this reaction's own watermark to the live head so the
|
|
4132
|
-
// close-cycle guard (which matches subscriptions by source =
|
|
4133
|
-
// aggregate) sees it caught up instead of blocking its own close.
|
|
4134
|
-
at: head.id
|
|
4135
|
-
});
|
|
4136
|
-
if (keep_days !== void 0) {
|
|
4137
|
-
const cutoff = days_before_now(keep_days);
|
|
4138
|
-
if (entry.tail.created < cutoff)
|
|
4139
|
-
throw new CloseSignal({
|
|
4140
|
-
stream: aggregate,
|
|
4141
|
-
before: cutoff,
|
|
4142
|
-
archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
|
|
4143
|
-
at: head.id
|
|
4144
|
-
});
|
|
4145
|
-
}
|
|
4146
|
-
const dues = [];
|
|
4147
|
-
for (const days of after_windows)
|
|
4148
|
-
dues.push(days_after(head.created, days));
|
|
4149
|
-
if (keep_days !== void 0)
|
|
4150
|
-
dues.push(days_after(entry.tail.created, keep_days));
|
|
4151
|
-
const future = dues.map((d) => d.getTime()).filter((t) => t > Date.now());
|
|
4152
|
-
if (future.length)
|
|
4153
|
-
throw new DeferSignal({ at: new Date(Math.min(...future)) });
|
|
4154
|
-
}
|
|
4155
|
-
};
|
|
4156
|
-
const key = `__autoclose_${st.name}`;
|
|
4157
|
-
for (const event_name of Object.keys(st.events)) {
|
|
4158
|
-
registry.events[event_name]?.reactions.set(
|
|
4159
|
-
key,
|
|
4160
|
-
reaction
|
|
4161
|
-
);
|
|
4162
|
-
}
|
|
4163
|
-
}
|
|
4164
|
-
}
|
|
4165
|
-
|
|
4166
4190
|
// src/builders/merge.ts
|
|
4167
4191
|
import { ZodObject } from "zod";
|
|
4168
4192
|
function base_type_name(zodType) {
|
|
@@ -4422,25 +4446,47 @@ import { z as z4 } from "zod";
|
|
|
4422
4446
|
|
|
4423
4447
|
// src/internal/date-reviver.ts
|
|
4424
4448
|
import { z as z3 } from "zod";
|
|
4425
|
-
function variant_schema(schema) {
|
|
4449
|
+
function variant_schema(schema, walk) {
|
|
4426
4450
|
const shape = def_of(schema)?.shape;
|
|
4427
4451
|
if (!shape) {
|
|
4428
|
-
const dates =
|
|
4452
|
+
const dates = walk(schema);
|
|
4429
4453
|
return { schema: dates ?? schema, dated: !!dates };
|
|
4430
4454
|
}
|
|
4431
4455
|
const next = {};
|
|
4432
4456
|
let dated = false;
|
|
4433
4457
|
for (const [key, field] of Object.entries(shape)) {
|
|
4434
|
-
const dates =
|
|
4458
|
+
const dates = walk(field);
|
|
4435
4459
|
if (dates) dated = true;
|
|
4436
4460
|
next[key] = (dates ?? field).optional();
|
|
4437
4461
|
}
|
|
4438
4462
|
return { schema: z3.looseObject(next), dated };
|
|
4439
4463
|
}
|
|
4440
4464
|
function date_reviver_schema(schema) {
|
|
4465
|
+
let recursive = false;
|
|
4466
|
+
const pass = (tie) => {
|
|
4467
|
+
const done = /* @__PURE__ */ new Map();
|
|
4468
|
+
const active = /* @__PURE__ */ new Set();
|
|
4469
|
+
const walk = (node) => {
|
|
4470
|
+
if (done.has(node)) return done.get(node);
|
|
4471
|
+
if (active.has(node)) {
|
|
4472
|
+
recursive = true;
|
|
4473
|
+
return tie ? z3.lazy(() => done.get(node)) : void 0;
|
|
4474
|
+
}
|
|
4475
|
+
active.add(node);
|
|
4476
|
+
const result = build(node, walk);
|
|
4477
|
+
active.delete(node);
|
|
4478
|
+
done.set(node, result);
|
|
4479
|
+
return result;
|
|
4480
|
+
};
|
|
4481
|
+
return walk(schema);
|
|
4482
|
+
};
|
|
4483
|
+
const first = pass(false);
|
|
4484
|
+
return first && recursive ? pass(true) : first;
|
|
4485
|
+
}
|
|
4486
|
+
function build(schema, walk) {
|
|
4441
4487
|
const def = def_of(schema);
|
|
4442
4488
|
if (!def) return void 0;
|
|
4443
|
-
const inner = () =>
|
|
4489
|
+
const inner = () => walk(def.innerType);
|
|
4444
4490
|
switch (def.type) {
|
|
4445
4491
|
case "date":
|
|
4446
4492
|
return z3.coerce.date();
|
|
@@ -4449,31 +4495,40 @@ function date_reviver_schema(schema) {
|
|
|
4449
4495
|
if (!shape) return void 0;
|
|
4450
4496
|
const dated = {};
|
|
4451
4497
|
for (const [key, field] of Object.entries(shape)) {
|
|
4452
|
-
const dates =
|
|
4498
|
+
const dates = walk(field);
|
|
4453
4499
|
if (dates) dated[key] = dates.optional();
|
|
4454
4500
|
}
|
|
4455
4501
|
return Object.keys(dated).length ? z3.looseObject(dated) : void 0;
|
|
4456
4502
|
}
|
|
4457
4503
|
case "array": {
|
|
4458
|
-
const element =
|
|
4504
|
+
const element = walk(def.element);
|
|
4459
4505
|
return element && z3.array(element);
|
|
4460
4506
|
}
|
|
4461
4507
|
case "tuple": {
|
|
4462
|
-
const items = def.items.map((i) =>
|
|
4508
|
+
const items = def.items.map((i) => walk(i));
|
|
4463
4509
|
return items.some(Boolean) ? z3.tuple(items.map((i) => i ?? z3.unknown())) : void 0;
|
|
4464
4510
|
}
|
|
4465
4511
|
case "record": {
|
|
4466
|
-
const value =
|
|
4512
|
+
const value = walk(def.valueType);
|
|
4467
4513
|
return value && z3.record(z3.string(), value);
|
|
4468
4514
|
}
|
|
4469
4515
|
case "union": {
|
|
4470
|
-
const variants = def.options.map(
|
|
4516
|
+
const variants = def.options.map(
|
|
4517
|
+
(o) => variant_schema(o, walk)
|
|
4518
|
+
);
|
|
4471
4519
|
return variants.some((v) => v.dated) ? z3.union(variants.map((v) => v.schema)) : void 0;
|
|
4472
4520
|
}
|
|
4473
4521
|
case "nullable":
|
|
4474
4522
|
return inner()?.nullable();
|
|
4475
4523
|
case "pipe":
|
|
4476
|
-
return
|
|
4524
|
+
return walk(def.out);
|
|
4525
|
+
case "lazy":
|
|
4526
|
+
return walk(def.getter());
|
|
4527
|
+
case "intersection": {
|
|
4528
|
+
const left = walk(def.left);
|
|
4529
|
+
const right = walk(def.right);
|
|
4530
|
+
return left && right ? left.pipe(right) : left ?? right;
|
|
4531
|
+
}
|
|
4477
4532
|
default:
|
|
4478
4533
|
return inner_of(def) ? inner() : void 0;
|
|
4479
4534
|
}
|
|
@@ -5284,6 +5339,7 @@ export {
|
|
|
5284
5339
|
CsvFile,
|
|
5285
5340
|
DEFAULT_LANE,
|
|
5286
5341
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
5342
|
+
DEFAULT_QUERY_STREAMS_LIMIT,
|
|
5287
5343
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
5288
5344
|
DeferSignal,
|
|
5289
5345
|
Environments,
|
|
@@ -5315,6 +5371,7 @@ export {
|
|
|
5315
5371
|
dispose,
|
|
5316
5372
|
disposeAndExit,
|
|
5317
5373
|
extend,
|
|
5374
|
+
fair_slots,
|
|
5318
5375
|
is_literal_source,
|
|
5319
5376
|
log,
|
|
5320
5377
|
pii_fields,
|