@rotorsoft/act 1.16.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +0 -9
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +10 -0
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts +3 -6
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +16 -8
- package/dist/@types/builders/slice-builder.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +19 -26
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-policy.d.ts +16 -0
- package/dist/@types/internal/autoclose-policy.d.ts.map +1 -1
- package/dist/@types/internal/builder-utils.d.ts +65 -0
- package/dist/@types/internal/builder-utils.d.ts.map +1 -0
- package/dist/@types/internal/close-signal.d.ts +51 -0
- package/dist/@types/internal/close-signal.d.ts.map +1 -0
- package/dist/@types/internal/defer-config.d.ts +64 -0
- package/dist/@types/internal/defer-config.d.ts.map +1 -0
- package/dist/@types/internal/defer-signal.d.ts +35 -0
- package/dist/@types/internal/defer-signal.d.ts.map +1 -0
- package/dist/@types/internal/defer-timer.d.ts +67 -0
- package/dist/@types/internal/defer-timer.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +39 -17
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain.d.ts +3 -1
- package/dist/@types/internal/drain.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +7 -5
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +36 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +31 -0
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-46OCUAVF.js → chunk-7XME4ZLK.js} +52 -2
- package/dist/chunk-7XME4ZLK.js.map +1 -0
- package/dist/index.cjs +852 -754
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +728 -681
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +55 -5
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +5 -5
- package/dist/test/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/@types/internal/autoclose-cycle.d.ts +0 -185
- package/dist/@types/internal/autoclose-cycle.d.ts.map +0 -1
- package/dist/chunk-46OCUAVF.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,7 @@ __export(index_exports, {
|
|
|
43
43
|
DEFAULT_LANE: () => DEFAULT_LANE,
|
|
44
44
|
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
45
45
|
DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
46
|
+
DeferSignal: () => DeferSignal,
|
|
46
47
|
Environments: () => Environments,
|
|
47
48
|
Errors: () => Errors,
|
|
48
49
|
EventMetaSchema: () => EventMetaSchema,
|
|
@@ -631,6 +632,11 @@ var InMemoryStream = class {
|
|
|
631
632
|
_leased_until = void 0;
|
|
632
633
|
_priority = 0;
|
|
633
634
|
_lane = DEFAULT_LANE;
|
|
635
|
+
// Persisted next-visit time (#1090). When set and still in the future, the
|
|
636
|
+
// stream is held out of `claim` entirely — so a deferred reaction is not
|
|
637
|
+
// re-claimed (and `retry` is never bumped) until its due-time passes. Unlike
|
|
638
|
+
// in-process backoff, this is durable store state shared across workers.
|
|
639
|
+
_deferred_at = void 0;
|
|
634
640
|
constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
|
|
635
641
|
this.stream = stream;
|
|
636
642
|
this.source = source;
|
|
@@ -662,7 +668,17 @@ var InMemoryStream = class {
|
|
|
662
668
|
this._priority = priority;
|
|
663
669
|
}
|
|
664
670
|
get is_available() {
|
|
665
|
-
return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date())
|
|
671
|
+
return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date()) && // A stream deferred to a future time is not claimable until due (#1090).
|
|
672
|
+
(!this._deferred_at || this._deferred_at <= Date.now());
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Hold this stream out of `claim` until `deferred_at` (ms since epoch).
|
|
676
|
+
* Set by a deliberate `defer` outcome — not a failure, so retry/blocked
|
|
677
|
+
* state is untouched. Cleared by ack/block/reset/unblock.
|
|
678
|
+
*/
|
|
679
|
+
defer(deferred_at) {
|
|
680
|
+
this._deferred_at = deferred_at;
|
|
681
|
+
this._retry = -1;
|
|
666
682
|
}
|
|
667
683
|
get at() {
|
|
668
684
|
return this._at;
|
|
@@ -714,6 +730,7 @@ var InMemoryStream = class {
|
|
|
714
730
|
this._leased_until = void 0;
|
|
715
731
|
this._at = lease.at;
|
|
716
732
|
this._retry = -1;
|
|
733
|
+
this._deferred_at = void 0;
|
|
717
734
|
return {
|
|
718
735
|
stream: this.stream,
|
|
719
736
|
source: this.source,
|
|
@@ -734,6 +751,7 @@ var InMemoryStream = class {
|
|
|
734
751
|
if (this._leased_by === lease.by) {
|
|
735
752
|
this._blocked = true;
|
|
736
753
|
this._error = error;
|
|
754
|
+
this._deferred_at = void 0;
|
|
737
755
|
return {
|
|
738
756
|
stream: this.stream,
|
|
739
757
|
source: this.source,
|
|
@@ -758,6 +776,7 @@ var InMemoryStream = class {
|
|
|
758
776
|
this._error = "";
|
|
759
777
|
this._leased_by = void 0;
|
|
760
778
|
this._leased_until = void 0;
|
|
779
|
+
this._deferred_at = void 0;
|
|
761
780
|
}
|
|
762
781
|
/**
|
|
763
782
|
* Clear the blocked flag and lease bookkeeping without touching the
|
|
@@ -771,6 +790,7 @@ var InMemoryStream = class {
|
|
|
771
790
|
this._error = "";
|
|
772
791
|
this._leased_by = void 0;
|
|
773
792
|
this._leased_until = void 0;
|
|
793
|
+
this._deferred_at = void 0;
|
|
774
794
|
return true;
|
|
775
795
|
}
|
|
776
796
|
};
|
|
@@ -1050,6 +1070,37 @@ var InMemoryStore = class {
|
|
|
1050
1070
|
await sleep();
|
|
1051
1071
|
return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
|
|
1052
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Hold the matched streams out of {@link claim} until `deferred_at`
|
|
1075
|
+
* (ms since epoch) — see {@link Store.defer}. Accepts an explicit list
|
|
1076
|
+
* of names or a {@link StreamFilter}, mirroring {@link reset}/{@link unblock}.
|
|
1077
|
+
* Persisted store state (unlike in-process backoff), so the skip is honored
|
|
1078
|
+
* by every competing worker. Unknown names are silently skipped.
|
|
1079
|
+
*
|
|
1080
|
+
* @returns Count of streams whose `deferred_at` was set.
|
|
1081
|
+
*/
|
|
1082
|
+
async defer(input, deferred_at) {
|
|
1083
|
+
await sleep();
|
|
1084
|
+
let count = 0;
|
|
1085
|
+
if (Array.isArray(input)) {
|
|
1086
|
+
for (const name of input) {
|
|
1087
|
+
const s = this._streams.get(name);
|
|
1088
|
+
if (s) {
|
|
1089
|
+
s.defer(deferred_at);
|
|
1090
|
+
count++;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
const matches = this._filter_predicate(input);
|
|
1095
|
+
for (const s of this._streams.values()) {
|
|
1096
|
+
if (matches(s)) {
|
|
1097
|
+
s.defer(deferred_at);
|
|
1098
|
+
count++;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return count;
|
|
1103
|
+
}
|
|
1053
1104
|
/**
|
|
1054
1105
|
* Build a predicate from a {@link StreamFilter}. Compiled regexes are
|
|
1055
1106
|
* cached in the closure so callers can apply it across the streams
|
|
@@ -1950,483 +2001,388 @@ function in_autoclose_window(window, now) {
|
|
|
1950
2001
|
return window.start < window.end ? hour >= window.start && hour < window.end : hour >= window.start || hour < window.end;
|
|
1951
2002
|
}
|
|
1952
2003
|
|
|
1953
|
-
// src/internal/
|
|
1954
|
-
var
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
)
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
)
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
guarded,
|
|
1977
|
-
target_map,
|
|
1978
|
-
stream_info,
|
|
1979
|
-
deps.event_to_state,
|
|
1980
|
-
deps.load,
|
|
1981
|
-
deps.logger
|
|
1982
|
-
);
|
|
1983
|
-
await run_archive_callbacks(guarded, target_map);
|
|
1984
|
-
const truncated = await truncate_and_warm_cache(
|
|
1985
|
-
guarded,
|
|
1986
|
-
seed_states,
|
|
1987
|
-
guard_events,
|
|
1988
|
-
deps.correlation
|
|
1989
|
-
);
|
|
1990
|
-
return { truncated, skipped };
|
|
1991
|
-
}
|
|
1992
|
-
async function scan_stream_heads(streams) {
|
|
1993
|
-
const stats = await store2().query_stats(streams, {
|
|
1994
|
-
exclude: [SNAP_EVENT]
|
|
1995
|
-
});
|
|
1996
|
-
const out = /* @__PURE__ */ new Map();
|
|
1997
|
-
for (const [stream, { head }] of stats) {
|
|
1998
|
-
if (head.name === TOMBSTONE_EVENT) continue;
|
|
1999
|
-
out.set(stream, {
|
|
2000
|
-
max_id: head.id,
|
|
2001
|
-
version: head.version,
|
|
2002
|
-
last_event_name: head.name
|
|
2003
|
-
});
|
|
2004
|
+
// src/internal/autoclose-policy.ts
|
|
2005
|
+
var import_zod6 = require("zod");
|
|
2006
|
+
var MIN_AFTER_MS = 6e4;
|
|
2007
|
+
var AfterSchema = import_zod6.z.object({
|
|
2008
|
+
days: import_zod6.z.number({ message: "autocloses: after.days must be a number" }).positive("autocloses: after.days must be > 0")
|
|
2009
|
+
}).refine((o) => o.days * 864e5 >= MIN_AFTER_MS, {
|
|
2010
|
+
message: `autocloses: after resolves to < ${MIN_AFTER_MS}ms (one minute), which is too short to be a meaningful retention window`
|
|
2011
|
+
});
|
|
2012
|
+
var IsSchema = import_zod6.z.union([
|
|
2013
|
+
import_zod6.z.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
2014
|
+
import_zod6.z.array(
|
|
2015
|
+
import_zod6.z.string().min(1, "autocloses: is entries must be non-empty event names")
|
|
2016
|
+
).min(1, "autocloses: is must include at least one event name").readonly()
|
|
2017
|
+
]);
|
|
2018
|
+
var ReachesSchema = import_zod6.z.number({ message: "autocloses: reaches must be a number" }).int("autocloses: reaches must be an integer").min(1, "autocloses: reaches must be >= 1");
|
|
2019
|
+
var OrBlockSchema = import_zod6.z.object({
|
|
2020
|
+
after: AfterSchema.optional(),
|
|
2021
|
+
is: IsSchema.optional(),
|
|
2022
|
+
reaches: ReachesSchema.optional()
|
|
2023
|
+
}).strict().refine(
|
|
2024
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
2025
|
+
{
|
|
2026
|
+
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
2004
2027
|
}
|
|
2005
|
-
|
|
2028
|
+
);
|
|
2029
|
+
var AutoclosePolicySchema = import_zod6.z.object({
|
|
2030
|
+
after: AfterSchema.optional(),
|
|
2031
|
+
is: IsSchema.optional(),
|
|
2032
|
+
reaches: ReachesSchema.optional(),
|
|
2033
|
+
or: OrBlockSchema.optional()
|
|
2034
|
+
}).strict().refine(
|
|
2035
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
2036
|
+
{
|
|
2037
|
+
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
function compile_after(after) {
|
|
2041
|
+
const after_ms = after.days * 864e5;
|
|
2042
|
+
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
2006
2043
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2044
|
+
function compile_is(is) {
|
|
2045
|
+
const set = new Set(typeof is === "string" ? [is] : is);
|
|
2046
|
+
return (_stream, head) => set.has(head.name);
|
|
2047
|
+
}
|
|
2048
|
+
function compile_reaches(reaches) {
|
|
2049
|
+
return (_stream, _head, count) => count >= reaches;
|
|
2050
|
+
}
|
|
2051
|
+
function policy_min_after_ms(options) {
|
|
2052
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
2053
|
+
const windows = [];
|
|
2054
|
+
if (parsed.after) windows.push(parsed.after.days * 864e5);
|
|
2055
|
+
if (parsed.or?.after) windows.push(parsed.or.after.days * 864e5);
|
|
2056
|
+
return windows.length ? Math.min(...windows) : void 0;
|
|
2057
|
+
}
|
|
2058
|
+
function compile_autoclose_policy(options) {
|
|
2059
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
2060
|
+
const and_preds = [];
|
|
2061
|
+
if (parsed.after) and_preds.push(compile_after(parsed.after));
|
|
2062
|
+
if (parsed.is) and_preds.push(compile_is(parsed.is));
|
|
2063
|
+
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
2064
|
+
const or_preds = [];
|
|
2065
|
+
if (parsed.or) {
|
|
2066
|
+
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
2067
|
+
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
2068
|
+
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
2069
|
+
}
|
|
2070
|
+
return (stream, head, count) => {
|
|
2071
|
+
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
2072
|
+
return true;
|
|
2016
2073
|
}
|
|
2017
|
-
|
|
2074
|
+
if (or_preds.some((p) => p(stream, head, count))) {
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
return false;
|
|
2018
2078
|
};
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/internal/build-classify.ts
|
|
2082
|
+
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
2083
|
+
function classify_registry(registry, states) {
|
|
2084
|
+
const statics = /* @__PURE__ */ new Map();
|
|
2085
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
2086
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
2087
|
+
let has_dynamic_resolvers = false;
|
|
2088
|
+
for (const [name, register] of Object.entries(registry.events)) {
|
|
2089
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
2090
|
+
for (const reaction of register.reactions.values()) {
|
|
2091
|
+
if (typeof reaction.resolver === "function") {
|
|
2092
|
+
has_dynamic_resolvers = true;
|
|
2093
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
2094
|
+
} else {
|
|
2095
|
+
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
2096
|
+
const lane_name = lane ?? "default";
|
|
2097
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
2098
|
+
if (existing_lanes !== ALL_LANES) {
|
|
2099
|
+
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
2100
|
+
set.add(lane_name);
|
|
2101
|
+
event_to_lanes.set(name, set);
|
|
2102
|
+
}
|
|
2103
|
+
const key = `${target}|${source ?? ""}`;
|
|
2104
|
+
const existing = statics.get(key);
|
|
2105
|
+
if (!existing) {
|
|
2106
|
+
statics.set(key, { stream: target, source, priority, lane });
|
|
2107
|
+
} else {
|
|
2108
|
+
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
2111
|
+
);
|
|
2112
|
+
if (priority > existing.priority) {
|
|
2113
|
+
statics.set(key, { ...existing, priority });
|
|
2030
2114
|
}
|
|
2031
2115
|
}
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
);
|
|
2035
|
-
if (count < page_size) break;
|
|
2036
|
-
after = last;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2037
2118
|
}
|
|
2038
|
-
const
|
|
2039
|
-
for (const
|
|
2040
|
-
|
|
2041
|
-
|
|
2119
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
2120
|
+
for (const merged of states.values()) {
|
|
2121
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
2122
|
+
event_to_state.set(event_name, merged);
|
|
2123
|
+
}
|
|
2042
2124
|
}
|
|
2043
|
-
return
|
|
2125
|
+
return {
|
|
2126
|
+
static_targets: [...statics.values()],
|
|
2127
|
+
has_dynamic_resolvers,
|
|
2128
|
+
reactive_events,
|
|
2129
|
+
event_to_state,
|
|
2130
|
+
event_to_lanes
|
|
2131
|
+
};
|
|
2044
2132
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2133
|
+
|
|
2134
|
+
// src/internal/defer-config.ts
|
|
2135
|
+
var import_zod7 = require("zod");
|
|
2136
|
+
|
|
2137
|
+
// src/internal/defer-signal.ts
|
|
2138
|
+
var DeferSignal = class extends Error {
|
|
2139
|
+
/**
|
|
2140
|
+
* The unresolved schedule. The dispatcher turns this into an absolute
|
|
2141
|
+
* due-time by resolving it against the triggering event.
|
|
2142
|
+
*/
|
|
2143
|
+
when;
|
|
2144
|
+
constructor(when) {
|
|
2145
|
+
super("reaction deferred");
|
|
2146
|
+
this.name = "DeferSignal";
|
|
2147
|
+
this.when = when;
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
// src/internal/defer-config.ts
|
|
2152
|
+
var DeferDurationSchema = import_zod7.z.object({
|
|
2153
|
+
days: import_zod7.z.number().positive().optional(),
|
|
2154
|
+
hours: import_zod7.z.number().positive().optional(),
|
|
2155
|
+
minutes: import_zod7.z.number().positive().optional()
|
|
2156
|
+
}).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
|
|
2157
|
+
message: "defer: a duration needs at least one of days, hours, or minutes"
|
|
2158
|
+
});
|
|
2159
|
+
var DeferWhenSchema = import_zod7.z.object({
|
|
2160
|
+
after: DeferDurationSchema.optional(),
|
|
2161
|
+
at: import_zod7.z.date().optional()
|
|
2162
|
+
}).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
|
|
2163
|
+
message: "defer: specify exactly one of `after` or `at`"
|
|
2164
|
+
});
|
|
2165
|
+
function duration_ms(d) {
|
|
2166
|
+
return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
|
|
2061
2167
|
}
|
|
2062
|
-
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
const last_event_name = stream_info.get(stream).last_event_name;
|
|
2067
|
-
const owner_state = event_to_state.get(last_event_name);
|
|
2068
|
-
if (!owner_state) {
|
|
2069
|
-
logger.error(
|
|
2070
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
2071
|
-
);
|
|
2072
|
-
return;
|
|
2073
|
-
}
|
|
2074
|
-
const snap2 = await load2(owner_state, { stream });
|
|
2075
|
-
seed_states.set(stream, snap2.state);
|
|
2076
|
-
})
|
|
2077
|
-
);
|
|
2078
|
-
return seed_states;
|
|
2168
|
+
function resolve_defer_at(when, event) {
|
|
2169
|
+
const parsed = DeferWhenSchema.parse(when);
|
|
2170
|
+
if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
|
|
2171
|
+
return parsed.at.getTime();
|
|
2079
2172
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
const archive_fn = target_map.get(stream)?.archive;
|
|
2083
|
-
if (archive_fn) await archive_fn();
|
|
2084
|
-
}
|
|
2173
|
+
function assert_defer_when(when) {
|
|
2174
|
+
DeferWhenSchema.parse(when);
|
|
2085
2175
|
}
|
|
2086
|
-
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
causation: {
|
|
2096
|
-
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
};
|
|
2100
|
-
});
|
|
2101
|
-
const truncated = await store2().truncate(trunc_targets);
|
|
2102
|
-
await Promise.all(
|
|
2103
|
-
guarded.map(async (stream) => {
|
|
2104
|
-
const entry = truncated.get(stream);
|
|
2105
|
-
const state2 = seed_states.get(stream);
|
|
2106
|
-
if (state2 && entry) {
|
|
2107
|
-
await cache2().set(stream, {
|
|
2108
|
-
state: state2,
|
|
2109
|
-
version: entry.committed.version,
|
|
2110
|
-
event_id: entry.committed.id,
|
|
2111
|
-
patches: 0,
|
|
2112
|
-
snaps: 1
|
|
2113
|
-
});
|
|
2114
|
-
} else {
|
|
2115
|
-
await cache2().invalidate(stream);
|
|
2116
|
-
}
|
|
2117
|
-
})
|
|
2118
|
-
);
|
|
2119
|
-
return truncated;
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
// src/internal/correlator.ts
|
|
2123
|
-
var import_node_crypto = require("crypto");
|
|
2124
|
-
var BASE = 36;
|
|
2125
|
-
var SEG_WIDTH = 4;
|
|
2126
|
-
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
2127
|
-
function seg(n) {
|
|
2128
|
-
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
2129
|
-
}
|
|
2130
|
-
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
2131
|
-
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2132
|
-
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2133
|
-
const ts = seg(Date.now() % SEG_SPACE);
|
|
2134
|
-
const rnd = seg((0, import_node_crypto.randomInt)(SEG_SPACE));
|
|
2135
|
-
return `${s}-${a}-${ts}${rnd}`;
|
|
2136
|
-
};
|
|
2137
|
-
function close_correlation(correlator, actor) {
|
|
2138
|
-
return correlator({
|
|
2139
|
-
state: "$close",
|
|
2140
|
-
action: "close",
|
|
2141
|
-
stream: "$close",
|
|
2142
|
-
actor
|
|
2143
|
-
});
|
|
2176
|
+
function make_deferred(handler, schedule) {
|
|
2177
|
+
const call = handler;
|
|
2178
|
+
const deferred = async (event, stream, app) => {
|
|
2179
|
+
const when = typeof schedule === "function" ? schedule(event) : schedule;
|
|
2180
|
+
if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
|
|
2181
|
+
return call(event, stream, app);
|
|
2182
|
+
};
|
|
2183
|
+
Object.defineProperty(deferred, "name", { value: handler.name });
|
|
2184
|
+
return deferred;
|
|
2144
2185
|
}
|
|
2145
2186
|
|
|
2146
|
-
// src/internal/
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2187
|
+
// src/internal/merge.ts
|
|
2188
|
+
var import_zod8 = require("zod");
|
|
2189
|
+
function base_type_name(zodType) {
|
|
2190
|
+
let t = zodType;
|
|
2191
|
+
while (typeof t.unwrap === "function") {
|
|
2192
|
+
t = t.unwrap();
|
|
2193
|
+
}
|
|
2194
|
+
return t.constructor.name;
|
|
2150
2195
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
);
|
|
2165
|
-
if (stats.size === 0) break;
|
|
2166
|
-
inspected += stats.size;
|
|
2167
|
-
const candidates = [];
|
|
2168
|
-
for (const [stream, { head, count }] of stats) {
|
|
2169
|
-
const owner = deps.event_to_state.get(head.name);
|
|
2170
|
-
if (!owner) continue;
|
|
2171
|
-
const predicate = deps.autoclose_policy(owner.name);
|
|
2172
|
-
if (!predicate) continue;
|
|
2173
|
-
let close = false;
|
|
2174
|
-
try {
|
|
2175
|
-
close = predicate(stream, head, count ?? 0);
|
|
2176
|
-
evaluated += 1;
|
|
2177
|
-
} catch (err) {
|
|
2178
|
-
predicate_errors += 1;
|
|
2179
|
-
deps.logger.error(
|
|
2180
|
-
`Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
|
|
2181
|
-
);
|
|
2182
|
-
close = deps.config.closeOnError;
|
|
2196
|
+
function merge_schemas(existing, incoming, state_name) {
|
|
2197
|
+
if (existing instanceof import_zod8.ZodObject && incoming instanceof import_zod8.ZodObject) {
|
|
2198
|
+
const existing_shape = existing.shape;
|
|
2199
|
+
const incoming_shape = incoming.shape;
|
|
2200
|
+
for (const key of Object.keys(incoming_shape)) {
|
|
2201
|
+
if (key in existing_shape) {
|
|
2202
|
+
const existing_base = base_type_name(existing_shape[key]);
|
|
2203
|
+
const incoming_base = base_type_name(incoming_shape[key]);
|
|
2204
|
+
if (existing_base !== incoming_base) {
|
|
2205
|
+
throw new Error(
|
|
2206
|
+
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2183
2209
|
}
|
|
2184
|
-
if (!close) continue;
|
|
2185
|
-
const archiver = deps.autoclose_archiver(owner.name);
|
|
2186
|
-
const archive = archiver ? () => archiver(stream, head) : void 0;
|
|
2187
|
-
candidates.push({ stream, archive });
|
|
2188
2210
|
}
|
|
2189
|
-
|
|
2190
|
-
const result = await run_close_cycle(candidates, {
|
|
2191
|
-
reactive_events_size: deps.reactive_events_size,
|
|
2192
|
-
event_to_state: deps.event_to_state,
|
|
2193
|
-
load: deps.load,
|
|
2194
|
-
tombstone: deps.tombstone,
|
|
2195
|
-
logger: deps.logger,
|
|
2196
|
-
correlation: deps.correlation
|
|
2197
|
-
});
|
|
2198
|
-
for (const [stream, entry] of result.truncated)
|
|
2199
|
-
truncated.set(stream, entry);
|
|
2200
|
-
for (const stream of result.skipped) skipped.push(stream);
|
|
2201
|
-
await yield_between_batches(deps.config.closeYieldMs);
|
|
2202
|
-
}
|
|
2203
|
-
if (stats.size < page) break;
|
|
2204
|
-
after = [...stats.keys()].at(-1);
|
|
2211
|
+
return existing.extend(incoming_shape);
|
|
2205
2212
|
}
|
|
2206
|
-
return
|
|
2207
|
-
inspected,
|
|
2208
|
-
evaluated,
|
|
2209
|
-
predicate_errors,
|
|
2210
|
-
close_result: { truncated, skipped }
|
|
2211
|
-
};
|
|
2213
|
+
return existing;
|
|
2212
2214
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
constructor(deps) {
|
|
2223
|
-
this.deps = deps;
|
|
2215
|
+
function merge_inits(existing, incoming) {
|
|
2216
|
+
return () => ({ ...existing(), ...incoming() });
|
|
2217
|
+
}
|
|
2218
|
+
function register_state(state2, states, actions, events) {
|
|
2219
|
+
const existing = states.get(state2.name);
|
|
2220
|
+
if (existing) {
|
|
2221
|
+
merge_into_existing(state2, existing, states, actions, events);
|
|
2222
|
+
} else {
|
|
2223
|
+
register_new_state(state2, states, actions, events);
|
|
2224
2224
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
if (this._timer) return false;
|
|
2232
|
-
this._timer = setInterval(() => {
|
|
2233
|
-
this.run_once().catch(() => {
|
|
2234
|
-
});
|
|
2235
|
-
}, this.deps.config.autocloseCycleMinutes * 6e4);
|
|
2236
|
-
this._timer.unref();
|
|
2237
|
-
return true;
|
|
2225
|
+
}
|
|
2226
|
+
function register_new_state(state2, states, actions, events) {
|
|
2227
|
+
states.set(state2.name, state2);
|
|
2228
|
+
for (const name of Object.keys(state2.actions)) {
|
|
2229
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2230
|
+
actions[name] = state2;
|
|
2238
2231
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
stop() {
|
|
2243
|
-
if (this._timer) {
|
|
2244
|
-
clearInterval(this._timer);
|
|
2245
|
-
this._timer = void 0;
|
|
2246
|
-
}
|
|
2232
|
+
for (const name of Object.keys(state2.events)) {
|
|
2233
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2234
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2247
2235
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2236
|
+
}
|
|
2237
|
+
function merge_into_existing(state2, existing, states, actions, events) {
|
|
2238
|
+
for (const name of Object.keys(state2.actions)) {
|
|
2239
|
+
if (existing.actions[name] === state2.actions[name]) continue;
|
|
2240
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2253
2241
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
* Overlapping invocations short-circuit: if a previous tick is
|
|
2260
|
-
* still running, this one drops and returns `null`. The orchestrator
|
|
2261
|
-
* never sees a queue of pending ticks regardless of how slow the
|
|
2262
|
-
* predicate / archive / truncate are.
|
|
2263
|
-
*/
|
|
2264
|
-
async run_once() {
|
|
2265
|
-
if (this._running) return null;
|
|
2266
|
-
if (this.deps.breaker.state(Date.now()) === "open") return null;
|
|
2267
|
-
if (!in_autoclose_window(this.deps.config.autocloseWindow, /* @__PURE__ */ new Date()))
|
|
2268
|
-
return null;
|
|
2269
|
-
this._running = true;
|
|
2270
|
-
try {
|
|
2271
|
-
const correlation = close_correlation(
|
|
2272
|
-
this.deps.correlator,
|
|
2273
|
-
this.deps.close_actor
|
|
2242
|
+
for (const name of Object.keys(state2.events)) {
|
|
2243
|
+
if (existing.events[name] === state2.events[name]) continue;
|
|
2244
|
+
if (existing.events[name]) {
|
|
2245
|
+
throw new Error(
|
|
2246
|
+
`Event "${name}" in state "${state2.name}" is declared with different Zod schemas across slices. Cross-slice event schemas must reference the same instance \u2014 extract a shared schema (e.g. \`export const ${name} = z.object({ ... })\` in a shared module) and import it in every slice that declares it.`
|
|
2274
2247
|
);
|
|
2275
|
-
const result = await run_autoclose_cycle({
|
|
2276
|
-
autoclose_policy: this.deps.autoclose_policy,
|
|
2277
|
-
autoclose_archiver: this.deps.autoclose_archiver,
|
|
2278
|
-
event_to_state: this.deps.event_to_state,
|
|
2279
|
-
reactive_events_size: this.deps.reactive_events_size,
|
|
2280
|
-
load: this.deps.load,
|
|
2281
|
-
tombstone: this.deps.tombstone,
|
|
2282
|
-
logger: this.deps.logger,
|
|
2283
|
-
config: this.deps.config,
|
|
2284
|
-
correlation
|
|
2285
|
-
});
|
|
2286
|
-
this.deps.breaker.passed();
|
|
2287
|
-
if (result.close_result.truncated.size > 0) {
|
|
2288
|
-
this.deps.on_closed(result.close_result);
|
|
2289
|
-
}
|
|
2290
|
-
return result;
|
|
2291
|
-
} catch (error) {
|
|
2292
|
-
this.deps.breaker.failed(Date.now(), error);
|
|
2293
|
-
throw error;
|
|
2294
|
-
} finally {
|
|
2295
|
-
this._running = false;
|
|
2296
2248
|
}
|
|
2249
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2297
2250
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
is: IsSchema.optional(),
|
|
2318
|
-
reaches: ReachesSchema.optional()
|
|
2319
|
-
}).strict().refine(
|
|
2320
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
2321
|
-
{
|
|
2322
|
-
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
2251
|
+
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
2252
|
+
const merged = {
|
|
2253
|
+
...existing,
|
|
2254
|
+
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
2255
|
+
init: merge_inits(existing.init, state2.init),
|
|
2256
|
+
events: { ...existing.events, ...state2.events },
|
|
2257
|
+
actions: { ...existing.actions, ...state2.actions },
|
|
2258
|
+
patch: merged_patch,
|
|
2259
|
+
on: { ...existing.on, ...state2.on },
|
|
2260
|
+
given: { ...existing.given, ...state2.given },
|
|
2261
|
+
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
`Duplicate snap strategy for state "${state2.name}"`
|
|
2264
|
+
);
|
|
2265
|
+
})() : state2.snap || existing.snap
|
|
2266
|
+
};
|
|
2267
|
+
states.set(state2.name, merged);
|
|
2268
|
+
for (const name of Object.keys(merged.actions)) {
|
|
2269
|
+
actions[name] = merged;
|
|
2323
2270
|
}
|
|
2324
|
-
)
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
is: IsSchema.optional(),
|
|
2328
|
-
reaches: ReachesSchema.optional(),
|
|
2329
|
-
or: OrBlockSchema.optional()
|
|
2330
|
-
}).strict().refine(
|
|
2331
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
2332
|
-
{
|
|
2333
|
-
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
2271
|
+
for (const name of Object.keys(state2.events)) {
|
|
2272
|
+
if (events[name]) continue;
|
|
2273
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2334
2274
|
}
|
|
2335
|
-
);
|
|
2336
|
-
function compile_after(after) {
|
|
2337
|
-
const after_ms = after.days * 864e5;
|
|
2338
|
-
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
2339
|
-
}
|
|
2340
|
-
function compile_is(is) {
|
|
2341
|
-
const set = new Set(typeof is === "string" ? [is] : is);
|
|
2342
|
-
return (_stream, head) => set.has(head.name);
|
|
2343
2275
|
}
|
|
2344
|
-
function
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
2353
|
-
const or_preds = [];
|
|
2354
|
-
if (parsed.or) {
|
|
2355
|
-
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
2356
|
-
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
2357
|
-
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
2358
|
-
}
|
|
2359
|
-
return (stream, head, count) => {
|
|
2360
|
-
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
2361
|
-
return true;
|
|
2276
|
+
function merge_patches(existing, incoming, state_name) {
|
|
2277
|
+
const merged = { ...existing };
|
|
2278
|
+
for (const name of Object.keys(incoming)) {
|
|
2279
|
+
const existing_p = existing[name];
|
|
2280
|
+
const incoming_p = incoming[name];
|
|
2281
|
+
if (!existing_p) {
|
|
2282
|
+
merged[name] = incoming_p;
|
|
2283
|
+
continue;
|
|
2362
2284
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2285
|
+
const existing_is_default = existing_p._passthrough;
|
|
2286
|
+
const incoming_is_default = incoming_p._passthrough;
|
|
2287
|
+
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
2288
|
+
throw new Error(
|
|
2289
|
+
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
2290
|
+
);
|
|
2365
2291
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2292
|
+
if (existing_is_default && !incoming_is_default) {
|
|
2293
|
+
merged[name] = incoming_p;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
return merged;
|
|
2368
2297
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
2398
|
-
throw new Error(
|
|
2399
|
-
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
2400
|
-
);
|
|
2401
|
-
if (priority > existing.priority) {
|
|
2402
|
-
statics.set(key, { ...existing, priority });
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2298
|
+
function merge_event_register(target, source) {
|
|
2299
|
+
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
2300
|
+
const target_reg = target[event_name];
|
|
2301
|
+
if (!target_reg) continue;
|
|
2302
|
+
for (const [name, reaction] of source_reg.reactions) {
|
|
2303
|
+
const existing = target_reg.reactions.get(name);
|
|
2304
|
+
if (existing !== void 0 && existing !== reaction)
|
|
2305
|
+
throw new Error(
|
|
2306
|
+
`Duplicate reaction "${name}" for event "${event_name}". Reaction handlers are keyed by function name; rename one of them.`
|
|
2307
|
+
);
|
|
2308
|
+
target_reg.reactions.set(name, reaction);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function merge_projection(proj, events) {
|
|
2313
|
+
for (const event_name of Object.keys(proj.events)) {
|
|
2314
|
+
const proj_register = proj.events[event_name];
|
|
2315
|
+
const existing = events[event_name];
|
|
2316
|
+
if (!existing) {
|
|
2317
|
+
events[event_name] = {
|
|
2318
|
+
schema: proj_register.schema,
|
|
2319
|
+
reactions: new Map(proj_register.reactions)
|
|
2320
|
+
};
|
|
2321
|
+
} else {
|
|
2322
|
+
for (const [name, reaction] of proj_register.reactions) {
|
|
2323
|
+
let key = name;
|
|
2324
|
+
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
2325
|
+
existing.reactions.set(key, reaction);
|
|
2405
2326
|
}
|
|
2406
2327
|
}
|
|
2407
2328
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2329
|
+
}
|
|
2330
|
+
var _this_ = ({ stream }) => ({
|
|
2331
|
+
source: stream,
|
|
2332
|
+
target: stream
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
// src/internal/builder-utils.ts
|
|
2336
|
+
function register_lane(config2, lanes) {
|
|
2337
|
+
if (config2.name === DEFAULT_LANE)
|
|
2338
|
+
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
2339
|
+
if (lanes.some((l) => l.name === config2.name))
|
|
2340
|
+
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
2341
|
+
lanes.push(config2);
|
|
2342
|
+
}
|
|
2343
|
+
function reaction_on(event, events, builder) {
|
|
2344
|
+
const register = (handler, options, schedule) => {
|
|
2345
|
+
if (!handler.name)
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
`Reaction handler for "${String(event)}" must be a named function`
|
|
2348
|
+
);
|
|
2349
|
+
if (events[event].reactions.has(handler.name))
|
|
2350
|
+
throw new Error(
|
|
2351
|
+
`Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
|
|
2352
|
+
);
|
|
2353
|
+
if (schedule && typeof schedule !== "function") assert_defer_when(schedule);
|
|
2354
|
+
const reaction = {
|
|
2355
|
+
handler: schedule ? make_deferred(handler, schedule) : handler,
|
|
2356
|
+
resolver: _this_,
|
|
2357
|
+
options: {
|
|
2358
|
+
blockOnError: options?.blockOnError ?? true,
|
|
2359
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
2360
|
+
backoff: options?.backoff
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
events[event].reactions.set(handler.name, reaction);
|
|
2364
|
+
return Object.assign(builder, {
|
|
2365
|
+
to(resolver) {
|
|
2366
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2367
|
+
return builder;
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
};
|
|
2414
2371
|
return {
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
event_to_lanes
|
|
2372
|
+
do: (handler, options) => register(handler, options),
|
|
2373
|
+
defer: (schedule) => ({
|
|
2374
|
+
do: (handler, options) => register(handler, options, schedule)
|
|
2375
|
+
})
|
|
2420
2376
|
};
|
|
2421
2377
|
}
|
|
2422
2378
|
|
|
2423
2379
|
// src/internal/circuit-breaker.ts
|
|
2424
|
-
var
|
|
2380
|
+
var import_zod9 = require("zod");
|
|
2425
2381
|
var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
|
|
2426
2382
|
var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
|
|
2427
|
-
var CircuitBreakerOptionsSchema =
|
|
2428
|
-
failureThreshold:
|
|
2429
|
-
cooldownMs:
|
|
2383
|
+
var CircuitBreakerOptionsSchema = import_zod9.z.object({
|
|
2384
|
+
failureThreshold: import_zod9.z.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
|
|
2385
|
+
cooldownMs: import_zod9.z.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
|
|
2430
2386
|
});
|
|
2431
2387
|
var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
|
|
2432
2388
|
var CircuitBreaker = class {
|
|
@@ -2507,6 +2463,203 @@ var CircuitBreaker = class {
|
|
|
2507
2463
|
}
|
|
2508
2464
|
};
|
|
2509
2465
|
|
|
2466
|
+
// src/internal/close-cycle.ts
|
|
2467
|
+
var SAFETY_PROBE_PAGE_SIZE = 1e3;
|
|
2468
|
+
async function run_close_cycle(targets, deps) {
|
|
2469
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
2470
|
+
const streams = [...target_map.keys()];
|
|
2471
|
+
const skipped = [];
|
|
2472
|
+
const stream_info = await scan_stream_heads(streams);
|
|
2473
|
+
const safe = await partition_by_safety(
|
|
2474
|
+
stream_info,
|
|
2475
|
+
deps.reactive_events_size,
|
|
2476
|
+
skipped,
|
|
2477
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
2478
|
+
);
|
|
2479
|
+
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
2480
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
2481
|
+
safe,
|
|
2482
|
+
stream_info,
|
|
2483
|
+
deps.correlation,
|
|
2484
|
+
deps.tombstone,
|
|
2485
|
+
skipped
|
|
2486
|
+
);
|
|
2487
|
+
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
2488
|
+
const seed_states = await load_restart_seeds(
|
|
2489
|
+
guarded,
|
|
2490
|
+
target_map,
|
|
2491
|
+
stream_info,
|
|
2492
|
+
deps.event_to_state,
|
|
2493
|
+
deps.load,
|
|
2494
|
+
deps.logger
|
|
2495
|
+
);
|
|
2496
|
+
await run_archive_callbacks(guarded, target_map);
|
|
2497
|
+
const truncated = await truncate_and_warm_cache(
|
|
2498
|
+
guarded,
|
|
2499
|
+
seed_states,
|
|
2500
|
+
guard_events,
|
|
2501
|
+
deps.correlation
|
|
2502
|
+
);
|
|
2503
|
+
return { truncated, skipped };
|
|
2504
|
+
}
|
|
2505
|
+
async function scan_stream_heads(streams) {
|
|
2506
|
+
const stats = await store2().query_stats(streams, {
|
|
2507
|
+
exclude: [SNAP_EVENT]
|
|
2508
|
+
});
|
|
2509
|
+
const out = /* @__PURE__ */ new Map();
|
|
2510
|
+
for (const [stream, { head }] of stats) {
|
|
2511
|
+
if (head.name === TOMBSTONE_EVENT) continue;
|
|
2512
|
+
out.set(stream, {
|
|
2513
|
+
max_id: head.id,
|
|
2514
|
+
version: head.version,
|
|
2515
|
+
last_event_name: head.name
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
return out;
|
|
2519
|
+
}
|
|
2520
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size) {
|
|
2521
|
+
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
2522
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
2523
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
2524
|
+
const get_regex = (source) => {
|
|
2525
|
+
let re = source_regex.get(source);
|
|
2526
|
+
if (!re) {
|
|
2527
|
+
re = new RegExp(source);
|
|
2528
|
+
source_regex.set(source, re);
|
|
2529
|
+
}
|
|
2530
|
+
return re;
|
|
2531
|
+
};
|
|
2532
|
+
const targets = [...stream_info.keys()];
|
|
2533
|
+
let after;
|
|
2534
|
+
for (; ; ) {
|
|
2535
|
+
let last;
|
|
2536
|
+
const { count } = await store2().query_streams(
|
|
2537
|
+
(position) => {
|
|
2538
|
+
last = position.stream;
|
|
2539
|
+
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
2540
|
+
for (const [stream, info] of stream_info) {
|
|
2541
|
+
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
2542
|
+
pending_set.add(stream);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
},
|
|
2546
|
+
{ after, limit: page_size, source_matches: targets }
|
|
2547
|
+
);
|
|
2548
|
+
if (count < page_size) break;
|
|
2549
|
+
after = last;
|
|
2550
|
+
}
|
|
2551
|
+
const safe = [];
|
|
2552
|
+
for (const [stream] of stream_info) {
|
|
2553
|
+
if (pending_set.has(stream)) skipped.push(stream);
|
|
2554
|
+
else safe.push(stream);
|
|
2555
|
+
}
|
|
2556
|
+
return safe;
|
|
2557
|
+
}
|
|
2558
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
2559
|
+
const guarded = [];
|
|
2560
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
2561
|
+
await Promise.all(
|
|
2562
|
+
safe.map(async (stream) => {
|
|
2563
|
+
const info = stream_info.get(stream);
|
|
2564
|
+
const committed = await tombstone2(stream, info.version, correlation);
|
|
2565
|
+
if (committed) {
|
|
2566
|
+
guarded.push(stream);
|
|
2567
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
2568
|
+
} else {
|
|
2569
|
+
skipped.push(stream);
|
|
2570
|
+
}
|
|
2571
|
+
})
|
|
2572
|
+
);
|
|
2573
|
+
return { guarded, guard_events };
|
|
2574
|
+
}
|
|
2575
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
2576
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
2577
|
+
await Promise.all(
|
|
2578
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
2579
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
2580
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
2581
|
+
if (!owner_state) {
|
|
2582
|
+
logger.error(
|
|
2583
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
2584
|
+
);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const snap2 = await load2(owner_state, { stream });
|
|
2588
|
+
seed_states.set(stream, snap2.state);
|
|
2589
|
+
})
|
|
2590
|
+
);
|
|
2591
|
+
return seed_states;
|
|
2592
|
+
}
|
|
2593
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
2594
|
+
for (const stream of guarded) {
|
|
2595
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
2596
|
+
if (archive_fn) await archive_fn();
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
2600
|
+
const trunc_targets = guarded.map((stream) => {
|
|
2601
|
+
const snapshot = seed_states.get(stream);
|
|
2602
|
+
const guard = guard_events.get(stream);
|
|
2603
|
+
return {
|
|
2604
|
+
stream,
|
|
2605
|
+
snapshot,
|
|
2606
|
+
meta: {
|
|
2607
|
+
correlation,
|
|
2608
|
+
causation: {
|
|
2609
|
+
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
});
|
|
2614
|
+
const truncated = await store2().truncate(trunc_targets);
|
|
2615
|
+
await Promise.all(
|
|
2616
|
+
guarded.map(async (stream) => {
|
|
2617
|
+
const entry = truncated.get(stream);
|
|
2618
|
+
const state2 = seed_states.get(stream);
|
|
2619
|
+
if (state2 && entry) {
|
|
2620
|
+
await cache2().set(stream, {
|
|
2621
|
+
state: state2,
|
|
2622
|
+
version: entry.committed.version,
|
|
2623
|
+
event_id: entry.committed.id,
|
|
2624
|
+
patches: 0,
|
|
2625
|
+
snaps: 1
|
|
2626
|
+
});
|
|
2627
|
+
} else {
|
|
2628
|
+
await cache2().invalidate(stream);
|
|
2629
|
+
}
|
|
2630
|
+
})
|
|
2631
|
+
);
|
|
2632
|
+
return truncated;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/internal/close-signal.ts
|
|
2636
|
+
var CloseSignal = class extends Error {
|
|
2637
|
+
/**
|
|
2638
|
+
* Stream to close. Omitted → the reaction's own lease stream (a self-close).
|
|
2639
|
+
* The synthesized autoclose reaction sets it to the aggregate stream, since
|
|
2640
|
+
* its lease runs on a synthetic `__autoclose__` target.
|
|
2641
|
+
*/
|
|
2642
|
+
stream;
|
|
2643
|
+
/** Optional archive callback to run while the stream is guarded. */
|
|
2644
|
+
archive;
|
|
2645
|
+
/**
|
|
2646
|
+
* Watermark to ack the requesting reaction to before the close runs. The
|
|
2647
|
+
* close-cycle safety guard skips a stream whose subscriptions (matched by
|
|
2648
|
+
* source) lag the head; the autoclose reaction's `source` is the aggregate,
|
|
2649
|
+
* so it must advance its own watermark to the live head id it evaluated
|
|
2650
|
+
* against — otherwise it blocks its own close. Defaults to the triggering
|
|
2651
|
+
* event id when omitted.
|
|
2652
|
+
*/
|
|
2653
|
+
at;
|
|
2654
|
+
constructor(opts) {
|
|
2655
|
+
super("reaction requested close");
|
|
2656
|
+
this.name = "CloseSignal";
|
|
2657
|
+
this.stream = opts?.stream;
|
|
2658
|
+
this.archive = opts?.archive;
|
|
2659
|
+
this.at = opts?.at;
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
|
|
2510
2663
|
// src/internal/correlate-cycle.ts
|
|
2511
2664
|
var CorrelateCycle = class {
|
|
2512
2665
|
_checkpoint = -1;
|
|
@@ -2611,33 +2764,135 @@ var CorrelateCycle = class {
|
|
|
2611
2764
|
return { subscribed: 0, last_id };
|
|
2612
2765
|
}
|
|
2613
2766
|
/**
|
|
2614
|
-
* Start a periodic correlation worker. Returns false if one is already
|
|
2615
|
-
* running. Errors from `correlate()` are routed through `log()` so they
|
|
2616
|
-
* land in the configured logger (the timer keeps running on failure).
|
|
2767
|
+
* Start a periodic correlation worker. Returns false if one is already
|
|
2768
|
+
* running. Errors from `correlate()` are routed through `log()` so they
|
|
2769
|
+
* land in the configured logger (the timer keeps running on failure).
|
|
2770
|
+
*/
|
|
2771
|
+
start_polling(query = {}, frequency = 1e4, callback) {
|
|
2772
|
+
if (this._timer) return false;
|
|
2773
|
+
const limit = query.limit || 100;
|
|
2774
|
+
this._timer = setInterval(
|
|
2775
|
+
() => this.correlate({ ...query, after: this._checkpoint, limit }).then((result) => {
|
|
2776
|
+
if (callback && result.subscribed) callback(result.subscribed);
|
|
2777
|
+
}).catch((err) => log().error(err)),
|
|
2778
|
+
frequency
|
|
2779
|
+
);
|
|
2780
|
+
return true;
|
|
2781
|
+
}
|
|
2782
|
+
/** Stop the periodic correlation worker. Idempotent. */
|
|
2783
|
+
stop_polling() {
|
|
2784
|
+
if (this._timer) {
|
|
2785
|
+
clearInterval(this._timer);
|
|
2786
|
+
this._timer = void 0;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2791
|
+
// src/internal/correlator.ts
|
|
2792
|
+
var import_node_crypto = require("crypto");
|
|
2793
|
+
var BASE = 36;
|
|
2794
|
+
var SEG_WIDTH = 4;
|
|
2795
|
+
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
2796
|
+
function seg(n) {
|
|
2797
|
+
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
2798
|
+
}
|
|
2799
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
2800
|
+
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2801
|
+
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2802
|
+
const ts = seg(Date.now() % SEG_SPACE);
|
|
2803
|
+
const rnd = seg((0, import_node_crypto.randomInt)(SEG_SPACE));
|
|
2804
|
+
return `${s}-${a}-${ts}${rnd}`;
|
|
2805
|
+
};
|
|
2806
|
+
function close_correlation(correlator, actor) {
|
|
2807
|
+
return correlator({
|
|
2808
|
+
state: "$close",
|
|
2809
|
+
action: "close",
|
|
2810
|
+
stream: "$close",
|
|
2811
|
+
actor
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/internal/drain-cycle.ts
|
|
2816
|
+
var import_node_crypto2 = require("crypto");
|
|
2817
|
+
|
|
2818
|
+
// src/internal/defer-timer.ts
|
|
2819
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
2820
|
+
var DeferTimer = class {
|
|
2821
|
+
_due = /* @__PURE__ */ new Map();
|
|
2822
|
+
_timer;
|
|
2823
|
+
_on_wake;
|
|
2824
|
+
/**
|
|
2825
|
+
* @param on_wake - invoked once each time the earliest due-time elapses,
|
|
2826
|
+
* after the come-due entries have been removed. Consumers use it to
|
|
2827
|
+
* re-arm their loop (the drain sets its `armed` flag; autoclose runs a
|
|
2828
|
+
* tick).
|
|
2829
|
+
*/
|
|
2830
|
+
constructor(on_wake) {
|
|
2831
|
+
this._on_wake = on_wake;
|
|
2832
|
+
}
|
|
2833
|
+
/** Number of currently parked streams. */
|
|
2834
|
+
get size() {
|
|
2835
|
+
return this._due.size;
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* True while `stream` is parked with a due-time still in the future.
|
|
2839
|
+
* Consumers skip work for deferred streams until their window elapses.
|
|
2617
2840
|
*/
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2841
|
+
is_deferred = (stream) => {
|
|
2842
|
+
const next = this._due.get(stream);
|
|
2843
|
+
return next !== void 0 && next > Date.now();
|
|
2844
|
+
};
|
|
2845
|
+
/**
|
|
2846
|
+
* Park `stream` for a re-visit at `at` (ms since epoch). A plain
|
|
2847
|
+
* overwrite: the caller computes the authoritative next-visit for the
|
|
2848
|
+
* stream (the drain's `handle` already reconciles a stream's reactions
|
|
2849
|
+
* into one result per cycle), so there is no stale value to merge against.
|
|
2850
|
+
*/
|
|
2851
|
+
set(stream, at) {
|
|
2852
|
+
this._due.set(stream, at);
|
|
2853
|
+
}
|
|
2854
|
+
/** Drop `stream` from the parked set (e.g. on a successful ack or block). */
|
|
2855
|
+
delete(stream) {
|
|
2856
|
+
this._due.delete(stream);
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* (Re)schedule the wake timer at the earliest pending due-time. Idempotent
|
|
2860
|
+
* — collapses many parked streams into a single timer. A no-op clears any
|
|
2861
|
+
* pending timer when the map is empty.
|
|
2862
|
+
*
|
|
2863
|
+
* The timer is `unref()`-ed so pending re-visits never keep the process
|
|
2864
|
+
* alive on their own.
|
|
2865
|
+
*/
|
|
2866
|
+
schedule() {
|
|
2867
|
+
if (this._timer) clearTimeout(this._timer);
|
|
2868
|
+
if (this._due.size === 0) {
|
|
2869
|
+
this._timer = void 0;
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
2873
|
+
for (const t of this._due.values()) if (t < earliest) earliest = t;
|
|
2874
|
+
const delay = Math.min(
|
|
2875
|
+
Math.max(0, earliest - Date.now()),
|
|
2876
|
+
MAX_TIMER_DELAY_MS
|
|
2626
2877
|
);
|
|
2627
|
-
|
|
2878
|
+
this._timer = setTimeout(() => {
|
|
2879
|
+
this._timer = void 0;
|
|
2880
|
+
const now = Date.now();
|
|
2881
|
+
for (const [stream, at] of this._due)
|
|
2882
|
+
if (at <= now) this._due.delete(stream);
|
|
2883
|
+
this._on_wake();
|
|
2884
|
+
}, delay);
|
|
2885
|
+
this._timer.unref();
|
|
2628
2886
|
}
|
|
2629
|
-
/**
|
|
2630
|
-
|
|
2887
|
+
/** Cancel any pending wake timer. Idempotent. Leaves the parked set intact. */
|
|
2888
|
+
stop() {
|
|
2631
2889
|
if (this._timer) {
|
|
2632
|
-
|
|
2890
|
+
clearTimeout(this._timer);
|
|
2633
2891
|
this._timer = void 0;
|
|
2634
2892
|
}
|
|
2635
2893
|
}
|
|
2636
2894
|
};
|
|
2637
2895
|
|
|
2638
|
-
// src/internal/drain-cycle.ts
|
|
2639
|
-
var import_node_crypto2 = require("crypto");
|
|
2640
|
-
|
|
2641
2896
|
// src/internal/drain-ratio.ts
|
|
2642
2897
|
var RATIO_MIN = 0.2;
|
|
2643
2898
|
var RATIO_MAX = 0.8;
|
|
@@ -2673,6 +2928,7 @@ async function fetch(leased, eventLimit) {
|
|
|
2673
2928
|
}
|
|
2674
2929
|
var ack = (leases) => store2().ack(leases);
|
|
2675
2930
|
var block = (leases) => store2().block(leases);
|
|
2931
|
+
var defer = (input, deferred_at) => store2().defer(input, deferred_at);
|
|
2676
2932
|
var subscribe = (streams) => store2().subscribe(streams);
|
|
2677
2933
|
|
|
2678
2934
|
// src/internal/event-sourcing.ts
|
|
@@ -3150,6 +3406,7 @@ function build_drain(logger) {
|
|
|
3150
3406
|
fetch,
|
|
3151
3407
|
ack,
|
|
3152
3408
|
block,
|
|
3409
|
+
defer,
|
|
3153
3410
|
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
3154
3411
|
if (!result.subscribed) return;
|
|
3155
3412
|
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
@@ -3218,7 +3475,8 @@ async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_bat
|
|
|
3218
3475
|
fetched: [],
|
|
3219
3476
|
handled: [],
|
|
3220
3477
|
acked: [],
|
|
3221
|
-
blocked: []
|
|
3478
|
+
blocked: [],
|
|
3479
|
+
closeable: []
|
|
3222
3480
|
};
|
|
3223
3481
|
}
|
|
3224
3482
|
const fetched = await ops.fetch(active, eventLimit);
|
|
@@ -3252,12 +3510,23 @@ async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_bat
|
|
|
3252
3510
|
})
|
|
3253
3511
|
);
|
|
3254
3512
|
const acked = await ops.ack(
|
|
3255
|
-
handled.filter((h) => h.handled > 0 || !h.error).map((h) => ({ ...h.lease, at: h.acked_at }))
|
|
3513
|
+
handled.filter((h) => h.defer === void 0 && (h.handled > 0 || !h.error)).map((h) => ({ ...h.lease, at: h.acked_at }))
|
|
3256
3514
|
);
|
|
3257
3515
|
const blocked = await ops.block(
|
|
3258
3516
|
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
3259
3517
|
);
|
|
3260
|
-
|
|
3518
|
+
const deferrals = handled.filter((h) => h.defer !== void 0);
|
|
3519
|
+
if (deferrals.length) {
|
|
3520
|
+
const by_time = /* @__PURE__ */ new Map();
|
|
3521
|
+
for (const h of deferrals) {
|
|
3522
|
+
const list = by_time.get(h.defer);
|
|
3523
|
+
if (list) list.push(h.lease.stream);
|
|
3524
|
+
else by_time.set(h.defer, [h.lease.stream]);
|
|
3525
|
+
}
|
|
3526
|
+
for (const [at, streams] of by_time) await ops.defer(streams, at);
|
|
3527
|
+
}
|
|
3528
|
+
const closeable = handled.filter((h) => h.close !== void 0).map((h) => h.close);
|
|
3529
|
+
return { leased, fetched, handled, acked, blocked, closeable };
|
|
3261
3530
|
}
|
|
3262
3531
|
var EMPTY_DRAIN = {
|
|
3263
3532
|
fetched: [],
|
|
@@ -3270,14 +3539,16 @@ var DrainController = class {
|
|
|
3270
3539
|
_locked = false;
|
|
3271
3540
|
_ratio = 0.5;
|
|
3272
3541
|
/**
|
|
3273
|
-
* Per-stream
|
|
3274
|
-
*
|
|
3275
|
-
* ack or terminal block. Lives in
|
|
3276
|
-
* by design (see {@link BackoffOptions}
|
|
3542
|
+
* Per-stream re-visit schedule (#1090): `stream → next visit` (ms since
|
|
3543
|
+
* epoch). Holds both retry backoff (`HandleResult.next_attempt_at`) and the
|
|
3544
|
+
* `defer` outcome; cleared on successful ack or terminal block. Lives in
|
|
3545
|
+
* process memory — per-worker pacing by design (see {@link BackoffOptions}
|
|
3546
|
+
* for the multi-worker trade-off). Its wake re-arms drain at the earliest
|
|
3547
|
+
* pending visit.
|
|
3277
3548
|
*/
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3549
|
+
_defer = new DeferTimer(() => {
|
|
3550
|
+
this._armed = true;
|
|
3551
|
+
});
|
|
3281
3552
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
3282
3553
|
_worker;
|
|
3283
3554
|
_stopped = false;
|
|
@@ -3297,32 +3568,6 @@ var DrainController = class {
|
|
|
3297
3568
|
get armed() {
|
|
3298
3569
|
return this._armed;
|
|
3299
3570
|
}
|
|
3300
|
-
/** Returns true when `stream` is currently within a backoff window. */
|
|
3301
|
-
is_deferred = (stream) => {
|
|
3302
|
-
const next = this._backoff.get(stream);
|
|
3303
|
-
return next !== void 0 && next > Date.now();
|
|
3304
|
-
};
|
|
3305
|
-
/**
|
|
3306
|
-
* Schedule the next drain re-arm at the earliest pending backoff
|
|
3307
|
-
* expiry. Called only when the backoff map is non-empty (caller guard).
|
|
3308
|
-
* Idempotent — collapses many simultaneously deferred streams into a
|
|
3309
|
-
* single timer.
|
|
3310
|
-
*/
|
|
3311
|
-
schedule_backoff_wake() {
|
|
3312
|
-
if (this._backoff_timer) clearTimeout(this._backoff_timer);
|
|
3313
|
-
let earliest = Number.POSITIVE_INFINITY;
|
|
3314
|
-
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
3315
|
-
const delay = Math.max(0, earliest - Date.now());
|
|
3316
|
-
this._backoff_timer = setTimeout(() => {
|
|
3317
|
-
this._backoff_timer = void 0;
|
|
3318
|
-
const now = Date.now();
|
|
3319
|
-
for (const [stream, at] of this._backoff) {
|
|
3320
|
-
if (at <= now) this._backoff.delete(stream);
|
|
3321
|
-
}
|
|
3322
|
-
this._armed = true;
|
|
3323
|
-
}, delay);
|
|
3324
|
-
this._backoff_timer.unref();
|
|
3325
|
-
}
|
|
3326
3571
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
3327
3572
|
get lane() {
|
|
3328
3573
|
return this._deps.lane;
|
|
@@ -3354,6 +3599,7 @@ var DrainController = class {
|
|
|
3354
3599
|
clearTimeout(this._worker);
|
|
3355
3600
|
this._worker = void 0;
|
|
3356
3601
|
}
|
|
3602
|
+
this._defer.stop();
|
|
3357
3603
|
}
|
|
3358
3604
|
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
3359
3605
|
async drain(options = {}) {
|
|
@@ -3379,7 +3625,7 @@ var DrainController = class {
|
|
|
3379
3625
|
leading,
|
|
3380
3626
|
eventLimit,
|
|
3381
3627
|
leaseMillis,
|
|
3382
|
-
this.
|
|
3628
|
+
this._defer.size > 0 ? this._defer.is_deferred : void 0,
|
|
3383
3629
|
this._deps.lane
|
|
3384
3630
|
);
|
|
3385
3631
|
this._deps.breaker.passed();
|
|
@@ -3387,19 +3633,19 @@ var DrainController = class {
|
|
|
3387
3633
|
this._armed = false;
|
|
3388
3634
|
return EMPTY_DRAIN;
|
|
3389
3635
|
}
|
|
3390
|
-
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
3636
|
+
const { leased, fetched, handled, acked, blocked, closeable } = cycle;
|
|
3391
3637
|
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
3392
3638
|
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
3393
|
-
for (const lease of acked) this.
|
|
3394
|
-
for (const lease of blocked) this.
|
|
3639
|
+
for (const lease of acked) this._defer.delete(lease.stream);
|
|
3640
|
+
for (const lease of blocked) this._defer.delete(lease.stream);
|
|
3395
3641
|
for (const h of handled) {
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
}
|
|
3642
|
+
const next = h.defer ?? (h.block ? void 0 : h.next_attempt_at);
|
|
3643
|
+
if (next !== void 0) this._defer.set(h.lease.stream, next);
|
|
3399
3644
|
}
|
|
3400
|
-
if (this.
|
|
3645
|
+
if (this._defer.size > 0) this._defer.schedule();
|
|
3401
3646
|
if (acked.length) this._deps.on_acked(acked);
|
|
3402
3647
|
if (blocked.length) this._deps.on_blocked(blocked);
|
|
3648
|
+
if (closeable.length) await this._deps.on_close(closeable);
|
|
3403
3649
|
const has_errors = handled.some(({ error }) => error);
|
|
3404
3650
|
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
3405
3651
|
return { fetched, leased, acked, blocked };
|
|
@@ -3412,154 +3658,6 @@ var DrainController = class {
|
|
|
3412
3658
|
}
|
|
3413
3659
|
};
|
|
3414
3660
|
|
|
3415
|
-
// src/internal/merge.ts
|
|
3416
|
-
var import_zod8 = require("zod");
|
|
3417
|
-
function base_type_name(zodType) {
|
|
3418
|
-
let t = zodType;
|
|
3419
|
-
while (typeof t.unwrap === "function") {
|
|
3420
|
-
t = t.unwrap();
|
|
3421
|
-
}
|
|
3422
|
-
return t.constructor.name;
|
|
3423
|
-
}
|
|
3424
|
-
function merge_schemas(existing, incoming, state_name) {
|
|
3425
|
-
if (existing instanceof import_zod8.ZodObject && incoming instanceof import_zod8.ZodObject) {
|
|
3426
|
-
const existing_shape = existing.shape;
|
|
3427
|
-
const incoming_shape = incoming.shape;
|
|
3428
|
-
for (const key of Object.keys(incoming_shape)) {
|
|
3429
|
-
if (key in existing_shape) {
|
|
3430
|
-
const existing_base = base_type_name(existing_shape[key]);
|
|
3431
|
-
const incoming_base = base_type_name(incoming_shape[key]);
|
|
3432
|
-
if (existing_base !== incoming_base) {
|
|
3433
|
-
throw new Error(
|
|
3434
|
-
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
3435
|
-
);
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
return existing.extend(incoming_shape);
|
|
3440
|
-
}
|
|
3441
|
-
return existing;
|
|
3442
|
-
}
|
|
3443
|
-
function merge_inits(existing, incoming) {
|
|
3444
|
-
return () => ({ ...existing(), ...incoming() });
|
|
3445
|
-
}
|
|
3446
|
-
function register_state(state2, states, actions, events) {
|
|
3447
|
-
const existing = states.get(state2.name);
|
|
3448
|
-
if (existing) {
|
|
3449
|
-
merge_into_existing(state2, existing, states, actions, events);
|
|
3450
|
-
} else {
|
|
3451
|
-
register_new_state(state2, states, actions, events);
|
|
3452
|
-
}
|
|
3453
|
-
}
|
|
3454
|
-
function register_new_state(state2, states, actions, events) {
|
|
3455
|
-
states.set(state2.name, state2);
|
|
3456
|
-
for (const name of Object.keys(state2.actions)) {
|
|
3457
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
3458
|
-
actions[name] = state2;
|
|
3459
|
-
}
|
|
3460
|
-
for (const name of Object.keys(state2.events)) {
|
|
3461
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
3462
|
-
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
|
-
function merge_into_existing(state2, existing, states, actions, events) {
|
|
3466
|
-
for (const name of Object.keys(state2.actions)) {
|
|
3467
|
-
if (existing.actions[name] === state2.actions[name]) continue;
|
|
3468
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
3469
|
-
}
|
|
3470
|
-
for (const name of Object.keys(state2.events)) {
|
|
3471
|
-
if (existing.events[name] === state2.events[name]) continue;
|
|
3472
|
-
if (existing.events[name]) {
|
|
3473
|
-
throw new Error(
|
|
3474
|
-
`Event "${name}" in state "${state2.name}" is declared with different Zod schemas across slices. Cross-slice event schemas must reference the same instance \u2014 extract a shared schema (e.g. \`export const ${name} = z.object({ ... })\` in a shared module) and import it in every slice that declares it.`
|
|
3475
|
-
);
|
|
3476
|
-
}
|
|
3477
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
3478
|
-
}
|
|
3479
|
-
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
3480
|
-
const merged = {
|
|
3481
|
-
...existing,
|
|
3482
|
-
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
3483
|
-
init: merge_inits(existing.init, state2.init),
|
|
3484
|
-
events: { ...existing.events, ...state2.events },
|
|
3485
|
-
actions: { ...existing.actions, ...state2.actions },
|
|
3486
|
-
patch: merged_patch,
|
|
3487
|
-
on: { ...existing.on, ...state2.on },
|
|
3488
|
-
given: { ...existing.given, ...state2.given },
|
|
3489
|
-
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
3490
|
-
throw new Error(
|
|
3491
|
-
`Duplicate snap strategy for state "${state2.name}"`
|
|
3492
|
-
);
|
|
3493
|
-
})() : state2.snap || existing.snap
|
|
3494
|
-
};
|
|
3495
|
-
states.set(state2.name, merged);
|
|
3496
|
-
for (const name of Object.keys(merged.actions)) {
|
|
3497
|
-
actions[name] = merged;
|
|
3498
|
-
}
|
|
3499
|
-
for (const name of Object.keys(state2.events)) {
|
|
3500
|
-
if (events[name]) continue;
|
|
3501
|
-
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
function merge_patches(existing, incoming, state_name) {
|
|
3505
|
-
const merged = { ...existing };
|
|
3506
|
-
for (const name of Object.keys(incoming)) {
|
|
3507
|
-
const existing_p = existing[name];
|
|
3508
|
-
const incoming_p = incoming[name];
|
|
3509
|
-
if (!existing_p) {
|
|
3510
|
-
merged[name] = incoming_p;
|
|
3511
|
-
continue;
|
|
3512
|
-
}
|
|
3513
|
-
const existing_is_default = existing_p._passthrough;
|
|
3514
|
-
const incoming_is_default = incoming_p._passthrough;
|
|
3515
|
-
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
3516
|
-
throw new Error(
|
|
3517
|
-
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
3518
|
-
);
|
|
3519
|
-
}
|
|
3520
|
-
if (existing_is_default && !incoming_is_default) {
|
|
3521
|
-
merged[name] = incoming_p;
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
return merged;
|
|
3525
|
-
}
|
|
3526
|
-
function merge_event_register(target, source) {
|
|
3527
|
-
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
3528
|
-
const target_reg = target[event_name];
|
|
3529
|
-
if (!target_reg) continue;
|
|
3530
|
-
for (const [name, reaction] of source_reg.reactions) {
|
|
3531
|
-
const existing = target_reg.reactions.get(name);
|
|
3532
|
-
if (existing !== void 0 && existing !== reaction)
|
|
3533
|
-
throw new Error(
|
|
3534
|
-
`Duplicate reaction "${name}" for event "${event_name}". Reaction handlers are keyed by function name; rename one of them.`
|
|
3535
|
-
);
|
|
3536
|
-
target_reg.reactions.set(name, reaction);
|
|
3537
|
-
}
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
function merge_projection(proj, events) {
|
|
3541
|
-
for (const event_name of Object.keys(proj.events)) {
|
|
3542
|
-
const proj_register = proj.events[event_name];
|
|
3543
|
-
const existing = events[event_name];
|
|
3544
|
-
if (!existing) {
|
|
3545
|
-
events[event_name] = {
|
|
3546
|
-
schema: proj_register.schema,
|
|
3547
|
-
reactions: new Map(proj_register.reactions)
|
|
3548
|
-
};
|
|
3549
|
-
} else {
|
|
3550
|
-
for (const [name, reaction] of proj_register.reactions) {
|
|
3551
|
-
let key = name;
|
|
3552
|
-
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
3553
|
-
existing.reactions.set(key, reaction);
|
|
3554
|
-
}
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
var _this_ = ({ stream }) => ({
|
|
3559
|
-
source: stream,
|
|
3560
|
-
target: stream
|
|
3561
|
-
});
|
|
3562
|
-
|
|
3563
3661
|
// src/internal/reactions.ts
|
|
3564
3662
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
3565
3663
|
if (!error) return { lease, handled, acked_at: at };
|
|
@@ -3615,6 +3713,25 @@ function build_handle(deps) {
|
|
|
3615
3713
|
at = event.id;
|
|
3616
3714
|
handled++;
|
|
3617
3715
|
} catch (error) {
|
|
3716
|
+
if (error instanceof DeferSignal)
|
|
3717
|
+
return {
|
|
3718
|
+
lease,
|
|
3719
|
+
handled,
|
|
3720
|
+
acked_at: at,
|
|
3721
|
+
defer: resolve_defer_at(error.when, event)
|
|
3722
|
+
};
|
|
3723
|
+
if (error instanceof CloseSignal)
|
|
3724
|
+
return {
|
|
3725
|
+
lease,
|
|
3726
|
+
handled: handled + 1,
|
|
3727
|
+
// Advance to the live head the handler evaluated against (when
|
|
3728
|
+
// provided) so the close-cycle guard sees this reaction caught up.
|
|
3729
|
+
acked_at: error.at ?? event.id,
|
|
3730
|
+
// Close the signalled stream (the autoclose reaction's aggregate,
|
|
3731
|
+
// which differs from its synthetic lease stream); a self-closing
|
|
3732
|
+
// user reaction omits it and closes its own lease stream.
|
|
3733
|
+
close: { stream: error.stream ?? stream, archive: error.archive }
|
|
3734
|
+
};
|
|
3618
3735
|
return finalize(
|
|
3619
3736
|
lease,
|
|
3620
3737
|
handled,
|
|
@@ -3717,6 +3834,7 @@ var SettleLoop = class {
|
|
|
3717
3834
|
// src/act.ts
|
|
3718
3835
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
3719
3836
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
3837
|
+
var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
|
|
3720
3838
|
var Act = class {
|
|
3721
3839
|
_emitter = new import_node_events.default();
|
|
3722
3840
|
/** ACT-984: orchestrator-owned circuit breaker shared by all drain lanes. */
|
|
@@ -3758,15 +3876,6 @@ var Act = class {
|
|
|
3758
3876
|
* @internal
|
|
3759
3877
|
*/
|
|
3760
3878
|
_autoclose_config;
|
|
3761
|
-
/**
|
|
3762
|
-
* App-level autoclose controller. `undefined` when no state declares
|
|
3763
|
-
* `.autocloses(...)` — the controller is never constructed, so
|
|
3764
|
-
* `start_correlations()` / `stop_correlations()` skip the autoclose
|
|
3765
|
-
* lifecycle and the orchestrator pays zero per-tick cost.
|
|
3766
|
-
*
|
|
3767
|
-
* @internal
|
|
3768
|
-
*/
|
|
3769
|
-
_autoclose;
|
|
3770
3879
|
/**
|
|
3771
3880
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
3772
3881
|
* via {@link ActLifecycleEvents}.
|
|
@@ -3907,6 +4016,62 @@ var Act = class {
|
|
|
3907
4016
|
bound_forget: this._bound_forget
|
|
3908
4017
|
});
|
|
3909
4018
|
this._handle_batch = build_handle_batch(this._logger);
|
|
4019
|
+
for (const st of this._states.values()) {
|
|
4020
|
+
const predicate = st.autoclose;
|
|
4021
|
+
if (!predicate) continue;
|
|
4022
|
+
const after_ms = st.autoclose_after_ms;
|
|
4023
|
+
const archiver = st.archive;
|
|
4024
|
+
const reaction = {
|
|
4025
|
+
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
4026
|
+
// per-aggregate `__autoclose__` key — so the autoclose reaction never
|
|
4027
|
+
// shares a watermark with the aggregate's own reactions. A shared
|
|
4028
|
+
// watermark would let autoclose's defer short-circuit the aggregate's
|
|
4029
|
+
// other reactions (the "a defer affects all reactions on a stream"
|
|
4030
|
+
// hazard). The close still targets the aggregate (`source`).
|
|
4031
|
+
resolver: (e) => ({
|
|
4032
|
+
target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
|
|
4033
|
+
source: e.stream
|
|
4034
|
+
}),
|
|
4035
|
+
// Never block on autoclose: a transient query/store error should retry,
|
|
4036
|
+
// not quarantine the synthetic stream.
|
|
4037
|
+
options: { blockOnError: false, maxRetries: 3 },
|
|
4038
|
+
handler: async (event) => {
|
|
4039
|
+
const aggregate = event.stream;
|
|
4040
|
+
const config2 = this._autoclose_config;
|
|
4041
|
+
if (!in_autoclose_window(config2.autocloseWindow, /* @__PURE__ */ new Date()))
|
|
4042
|
+
throw new DeferSignal({
|
|
4043
|
+
at: new Date(Date.now() + config2.autocloseCycleMinutes * 6e4)
|
|
4044
|
+
});
|
|
4045
|
+
const stats = await store2().query_stats([aggregate], {
|
|
4046
|
+
count: true,
|
|
4047
|
+
exclude: [TOMBSTONE_EVENT]
|
|
4048
|
+
});
|
|
4049
|
+
const entry = stats.get(aggregate);
|
|
4050
|
+
if (!entry) return;
|
|
4051
|
+
const head = entry.head;
|
|
4052
|
+
if (predicate(aggregate, head, entry.count))
|
|
4053
|
+
throw new CloseSignal({
|
|
4054
|
+
stream: aggregate,
|
|
4055
|
+
archive: archiver ? () => archiver(aggregate, head) : void 0,
|
|
4056
|
+
// Ack this reaction's own watermark to the live head so the
|
|
4057
|
+
// close-cycle guard (which matches subscriptions by source =
|
|
4058
|
+
// aggregate) sees it caught up instead of blocking its own close.
|
|
4059
|
+
at: head.id
|
|
4060
|
+
});
|
|
4061
|
+
if (after_ms !== void 0)
|
|
4062
|
+
throw new DeferSignal({
|
|
4063
|
+
at: new Date(head.created.getTime() + after_ms)
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
};
|
|
4067
|
+
const key = `__autoclose_${st.name}`;
|
|
4068
|
+
for (const event_name of Object.keys(st.events)) {
|
|
4069
|
+
this.registry.events[event_name]?.reactions.set(
|
|
4070
|
+
key,
|
|
4071
|
+
reaction
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
3910
4075
|
const {
|
|
3911
4076
|
static_targets,
|
|
3912
4077
|
has_dynamic_resolvers,
|
|
@@ -3955,6 +4120,22 @@ var Act = class {
|
|
|
3955
4120
|
handle_batch: this._handle_batch,
|
|
3956
4121
|
on_acked: (acked) => this.emit("acked", acked),
|
|
3957
4122
|
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
4123
|
+
// Reaction-requested close (#1090). Runs the same close machinery as
|
|
4124
|
+
// `app.close` (tombstone guard + archive + atomic truncate) for the
|
|
4125
|
+
// targets a handler signalled via `CloseSignal`. No `correlate()` here
|
|
4126
|
+
// — the drain that produced these targets has already correlated.
|
|
4127
|
+
on_close: async (targets) => {
|
|
4128
|
+
const close_actor = { id: "$close", name: "close" };
|
|
4129
|
+
const result = await run_close_cycle(targets, {
|
|
4130
|
+
reactive_events_size: this._reactive_events.size,
|
|
4131
|
+
event_to_state: this._event_to_state,
|
|
4132
|
+
load: this._es.load,
|
|
4133
|
+
tombstone: this._es.tombstone,
|
|
4134
|
+
logger: this._logger,
|
|
4135
|
+
correlation: close_correlation(this._correlator, close_actor)
|
|
4136
|
+
});
|
|
4137
|
+
this.emit("closed", result);
|
|
4138
|
+
},
|
|
3958
4139
|
breaker: this._breaker,
|
|
3959
4140
|
// Pass lane only when a true per-lane controller is active.
|
|
3960
4141
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
@@ -4001,27 +4182,6 @@ var Act = class {
|
|
|
4001
4182
|
},
|
|
4002
4183
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
4003
4184
|
);
|
|
4004
|
-
const states_with_policy = [...this._states.values()].filter(
|
|
4005
|
-
(s) => s.autoclose
|
|
4006
|
-
);
|
|
4007
|
-
this._autoclose = states_with_policy.length ? new AutocloseController({
|
|
4008
|
-
// Internal cycle takes the loose `(string) => …` signature; the
|
|
4009
|
-
// public `registry.autoclose_policy` narrows to
|
|
4010
|
-
// `keyof TStateMap & string`. Thin lambdas bridge across the
|
|
4011
|
-
// narrowing — runtime semantics are unchanged.
|
|
4012
|
-
autoclose_policy: (name) => this.registry.autoclose_policy(name),
|
|
4013
|
-
autoclose_archiver: (name) => this.registry.autoclose_archiver(name),
|
|
4014
|
-
event_to_state: this._event_to_state,
|
|
4015
|
-
reactive_events_size: this._reactive_events.size,
|
|
4016
|
-
load: this._es.load,
|
|
4017
|
-
tombstone: this._es.tombstone,
|
|
4018
|
-
logger: this._logger,
|
|
4019
|
-
config: this._autoclose_config,
|
|
4020
|
-
correlator: this._correlator,
|
|
4021
|
-
close_actor: { id: "$autoclose", name: "autoclose" },
|
|
4022
|
-
on_closed: (result) => this.emit("closed", result),
|
|
4023
|
-
breaker: this._breaker
|
|
4024
|
-
}) : void 0;
|
|
4025
4185
|
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store2());
|
|
4026
4186
|
dispose(() => this.shutdown());
|
|
4027
4187
|
}
|
|
@@ -4515,7 +4675,6 @@ var Act = class {
|
|
|
4515
4675
|
*/
|
|
4516
4676
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
4517
4677
|
const started = this._correlate.start_polling(query, frequency, callback);
|
|
4518
|
-
this._autoclose?.start();
|
|
4519
4678
|
return started;
|
|
4520
4679
|
}
|
|
4521
4680
|
/**
|
|
@@ -4537,7 +4696,6 @@ var Act = class {
|
|
|
4537
4696
|
*/
|
|
4538
4697
|
stop_correlations() {
|
|
4539
4698
|
this._correlate.stop_polling();
|
|
4540
|
-
this._autoclose?.stop();
|
|
4541
4699
|
}
|
|
4542
4700
|
/**
|
|
4543
4701
|
* Cancels any pending or active settle cycle.
|
|
@@ -5004,41 +5162,10 @@ function act() {
|
|
|
5004
5162
|
},
|
|
5005
5163
|
withActor: () => builder,
|
|
5006
5164
|
withLane: (config2) => {
|
|
5007
|
-
|
|
5008
|
-
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
5009
|
-
if (lanes.some((l) => l.name === config2.name))
|
|
5010
|
-
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
5011
|
-
lanes.push(config2);
|
|
5165
|
+
register_lane(config2, lanes);
|
|
5012
5166
|
return builder;
|
|
5013
5167
|
},
|
|
5014
|
-
on: (event) => (
|
|
5015
|
-
do: (handler, options) => {
|
|
5016
|
-
const reaction = {
|
|
5017
|
-
handler,
|
|
5018
|
-
resolver: _this_,
|
|
5019
|
-
options: {
|
|
5020
|
-
blockOnError: options?.blockOnError ?? true,
|
|
5021
|
-
maxRetries: options?.maxRetries ?? 3,
|
|
5022
|
-
backoff: options?.backoff
|
|
5023
|
-
}
|
|
5024
|
-
};
|
|
5025
|
-
if (!handler.name)
|
|
5026
|
-
throw new Error(
|
|
5027
|
-
`Reaction handler for "${String(event)}" must be a named function`
|
|
5028
|
-
);
|
|
5029
|
-
if (registry.events[event].reactions.has(handler.name))
|
|
5030
|
-
throw new Error(
|
|
5031
|
-
`Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
|
|
5032
|
-
);
|
|
5033
|
-
registry.events[event].reactions.set(handler.name, reaction);
|
|
5034
|
-
return Object.assign(builder, {
|
|
5035
|
-
to(resolver) {
|
|
5036
|
-
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
5037
|
-
return builder;
|
|
5038
|
-
}
|
|
5039
|
-
});
|
|
5040
|
-
}
|
|
5041
|
-
}),
|
|
5168
|
+
on: (event) => reaction_on(event, registry.events, builder),
|
|
5042
5169
|
build: (options) => {
|
|
5043
5170
|
if (!_built) {
|
|
5044
5171
|
for (const proj of pending_projections) {
|
|
@@ -5203,41 +5330,10 @@ function slice() {
|
|
|
5203
5330
|
return builder;
|
|
5204
5331
|
},
|
|
5205
5332
|
withLane: (config2) => {
|
|
5206
|
-
|
|
5207
|
-
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
5208
|
-
if (lanes.some((l) => l.name === config2.name))
|
|
5209
|
-
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
5210
|
-
lanes.push(config2);
|
|
5333
|
+
register_lane(config2, lanes);
|
|
5211
5334
|
return builder;
|
|
5212
5335
|
},
|
|
5213
|
-
on: (event) => (
|
|
5214
|
-
do: (handler, options) => {
|
|
5215
|
-
const reaction = {
|
|
5216
|
-
handler,
|
|
5217
|
-
resolver: _this_,
|
|
5218
|
-
options: {
|
|
5219
|
-
blockOnError: options?.blockOnError ?? true,
|
|
5220
|
-
maxRetries: options?.maxRetries ?? 3,
|
|
5221
|
-
backoff: options?.backoff
|
|
5222
|
-
}
|
|
5223
|
-
};
|
|
5224
|
-
if (!handler.name)
|
|
5225
|
-
throw new Error(
|
|
5226
|
-
`Reaction handler for "${String(event)}" must be a named function`
|
|
5227
|
-
);
|
|
5228
|
-
if (events[event].reactions.has(handler.name))
|
|
5229
|
-
throw new Error(
|
|
5230
|
-
`Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
|
|
5231
|
-
);
|
|
5232
|
-
events[event].reactions.set(handler.name, reaction);
|
|
5233
|
-
return Object.assign(builder, {
|
|
5234
|
-
to(resolver) {
|
|
5235
|
-
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
5236
|
-
return builder;
|
|
5237
|
-
}
|
|
5238
|
-
});
|
|
5239
|
-
}
|
|
5240
|
-
}),
|
|
5336
|
+
on: (event) => reaction_on(event, events, builder),
|
|
5241
5337
|
build: () => ({
|
|
5242
5338
|
_tag: "Slice",
|
|
5243
5339
|
states,
|
|
@@ -5340,20 +5436,21 @@ function action_builder(state2) {
|
|
|
5340
5436
|
internal.disclose = disclose;
|
|
5341
5437
|
return builder;
|
|
5342
5438
|
},
|
|
5343
|
-
autocloses(
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
} else if (predicate_or_policy !== null && typeof predicate_or_policy === "object") {
|
|
5348
|
-
predicate = compile_autoclose_policy(
|
|
5349
|
-
predicate_or_policy
|
|
5439
|
+
autocloses(policy) {
|
|
5440
|
+
if (typeof policy === "function") {
|
|
5441
|
+
throw new Error(
|
|
5442
|
+
".autocloses(fn) is no longer supported \u2014 pass a declarative policy ({ after, is, reaches, or }) or call app.close(...) from your own reaction for custom logic."
|
|
5350
5443
|
);
|
|
5351
|
-
}
|
|
5444
|
+
}
|
|
5445
|
+
if (policy === null || typeof policy !== "object") {
|
|
5352
5446
|
throw new Error(
|
|
5353
|
-
".autocloses(...) requires a
|
|
5447
|
+
".autocloses(...) requires a policy object; got " + typeof policy
|
|
5354
5448
|
);
|
|
5355
5449
|
}
|
|
5356
|
-
internal.autoclose =
|
|
5450
|
+
internal.autoclose = compile_autoclose_policy(
|
|
5451
|
+
policy
|
|
5452
|
+
);
|
|
5453
|
+
internal.autoclose_after_ms = policy_min_after_ms(policy);
|
|
5357
5454
|
return builder;
|
|
5358
5455
|
},
|
|
5359
5456
|
archives(archive) {
|
|
@@ -5547,6 +5644,7 @@ function write_line(writer, line) {
|
|
|
5547
5644
|
DEFAULT_LANE,
|
|
5548
5645
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
5549
5646
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
5647
|
+
DeferSignal,
|
|
5550
5648
|
Environments,
|
|
5551
5649
|
Errors,
|
|
5552
5650
|
EventMetaSchema,
|