@rotorsoft/act 1.10.2 → 1.12.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 +61 -2
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +117 -1
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-config.d.ts +72 -0
- package/dist/@types/internal/autoclose-config.d.ts.map +1 -0
- package/dist/@types/internal/autoclose-cycle.d.ts +171 -0
- package/dist/@types/internal/autoclose-cycle.d.ts.map +1 -0
- package/dist/@types/internal/autoclose-policy.d.ts +121 -0
- package/dist/@types/internal/autoclose-policy.d.ts.map +1 -0
- package/dist/@types/internal/index.d.ts +3 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +63 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/registry.d.ts +24 -4
- package/dist/@types/types/registry.d.ts.map +1 -1
- package/dist/index.cjs +398 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +392 -77
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,9 @@ __export(index_exports, {
|
|
|
37
37
|
ConcurrencyError: () => ConcurrencyError,
|
|
38
38
|
ConsoleLogger: () => ConsoleLogger,
|
|
39
39
|
CsvFile: () => CsvFile,
|
|
40
|
+
DEFAULT_AUTOCLOSE_CYCLE_MS: () => DEFAULT_AUTOCLOSE_CYCLE_MS,
|
|
41
|
+
DEFAULT_CLOSE_BATCH_SIZE: () => DEFAULT_CLOSE_BATCH_SIZE,
|
|
42
|
+
DEFAULT_CLOSE_YIELD_MS: () => DEFAULT_CLOSE_YIELD_MS,
|
|
40
43
|
DEFAULT_LANE: () => DEFAULT_LANE,
|
|
41
44
|
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
42
45
|
DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
@@ -68,6 +71,7 @@ __export(index_exports, {
|
|
|
68
71
|
log: () => log,
|
|
69
72
|
port: () => port,
|
|
70
73
|
projection: () => projection,
|
|
74
|
+
resolveAutocloseConfig: () => resolveAutocloseConfig,
|
|
71
75
|
scoped: () => scoped,
|
|
72
76
|
sensitive: () => sensitive,
|
|
73
77
|
sleep: () => sleep,
|
|
@@ -1854,57 +1858,24 @@ var PASS_FACTORIES = {
|
|
|
1854
1858
|
"clock-anomalies": make_clock_anomalies_pass
|
|
1855
1859
|
};
|
|
1856
1860
|
|
|
1857
|
-
// src/internal/
|
|
1858
|
-
var
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
1876
|
-
set.add(lane_name);
|
|
1877
|
-
event_to_lanes.set(name, set);
|
|
1878
|
-
}
|
|
1879
|
-
const key = `${target}|${source ?? ""}`;
|
|
1880
|
-
const existing = statics.get(key);
|
|
1881
|
-
if (!existing) {
|
|
1882
|
-
statics.set(key, { stream: target, source, priority, lane });
|
|
1883
|
-
} else {
|
|
1884
|
-
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
1885
|
-
throw new Error(
|
|
1886
|
-
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
1887
|
-
);
|
|
1888
|
-
if (priority > existing.priority) {
|
|
1889
|
-
statics.set(key, { ...existing, priority });
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
const event_to_state = /* @__PURE__ */ new Map();
|
|
1896
|
-
for (const merged of states.values()) {
|
|
1897
|
-
for (const event_name of Object.keys(merged.events)) {
|
|
1898
|
-
event_to_state.set(event_name, merged);
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
return {
|
|
1902
|
-
static_targets: [...statics.values()],
|
|
1903
|
-
has_dynamic_resolvers,
|
|
1904
|
-
reactive_events,
|
|
1905
|
-
event_to_state,
|
|
1906
|
-
event_to_lanes
|
|
1907
|
-
};
|
|
1861
|
+
// src/internal/autoclose-config.ts
|
|
1862
|
+
var import_zod5 = require("zod");
|
|
1863
|
+
var DEFAULT_AUTOCLOSE_CYCLE_MS = 6e4;
|
|
1864
|
+
var DEFAULT_CLOSE_BATCH_SIZE = 64;
|
|
1865
|
+
var DEFAULT_CLOSE_YIELD_MS = 0;
|
|
1866
|
+
var AutocloseOptionsSchema = import_zod5.z.object({
|
|
1867
|
+
autocloseCycleMs: import_zod5.z.number().min(1e4).max(36e5).default(DEFAULT_AUTOCLOSE_CYCLE_MS),
|
|
1868
|
+
closeBatchSize: import_zod5.z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
|
|
1869
|
+
closeYieldMs: import_zod5.z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
|
|
1870
|
+
closeOnError: import_zod5.z.boolean().default(false)
|
|
1871
|
+
});
|
|
1872
|
+
function resolveAutocloseConfig(options) {
|
|
1873
|
+
return AutocloseOptionsSchema.parse({
|
|
1874
|
+
autocloseCycleMs: options?.autocloseCycleMs,
|
|
1875
|
+
closeBatchSize: options?.closeBatchSize,
|
|
1876
|
+
closeYieldMs: options?.closeYieldMs,
|
|
1877
|
+
closeOnError: options?.closeOnError
|
|
1878
|
+
});
|
|
1908
1879
|
}
|
|
1909
1880
|
|
|
1910
1881
|
// src/internal/close-cycle.ts
|
|
@@ -2054,6 +2025,303 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
|
|
|
2054
2025
|
return truncated;
|
|
2055
2026
|
}
|
|
2056
2027
|
|
|
2028
|
+
// src/internal/correlator.ts
|
|
2029
|
+
var import_node_crypto = require("crypto");
|
|
2030
|
+
var BASE = 36;
|
|
2031
|
+
var SEG_WIDTH = 4;
|
|
2032
|
+
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
2033
|
+
function seg(n) {
|
|
2034
|
+
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
2035
|
+
}
|
|
2036
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
2037
|
+
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2038
|
+
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2039
|
+
const ts = seg(Date.now() % SEG_SPACE);
|
|
2040
|
+
const rnd = seg((0, import_node_crypto.randomInt)(SEG_SPACE));
|
|
2041
|
+
return `${s}-${a}-${ts}${rnd}`;
|
|
2042
|
+
};
|
|
2043
|
+
function close_correlation(correlator, actor) {
|
|
2044
|
+
return correlator({
|
|
2045
|
+
state: "$close",
|
|
2046
|
+
action: "close",
|
|
2047
|
+
stream: "$close",
|
|
2048
|
+
actor
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// src/internal/autoclose-cycle.ts
|
|
2053
|
+
function yield_between_batches(ms) {
|
|
2054
|
+
if (ms <= 0) return Promise.resolve();
|
|
2055
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2056
|
+
}
|
|
2057
|
+
async function run_autoclose_cycle(deps) {
|
|
2058
|
+
const s = store2();
|
|
2059
|
+
let inspected = 0;
|
|
2060
|
+
let evaluated = 0;
|
|
2061
|
+
let predicate_errors = 0;
|
|
2062
|
+
const truncated = /* @__PURE__ */ new Map();
|
|
2063
|
+
const skipped = [];
|
|
2064
|
+
const stats = await s.query_stats(
|
|
2065
|
+
{},
|
|
2066
|
+
{ count: true, exclude: [TOMBSTONE_EVENT] }
|
|
2067
|
+
);
|
|
2068
|
+
const flush_batch = async (candidates2) => {
|
|
2069
|
+
if (candidates2.length === 0) return;
|
|
2070
|
+
const result = await run_close_cycle(candidates2, {
|
|
2071
|
+
reactive_events_size: deps.reactive_events_size,
|
|
2072
|
+
event_to_state: deps.event_to_state,
|
|
2073
|
+
load: deps.load,
|
|
2074
|
+
tombstone: deps.tombstone,
|
|
2075
|
+
logger: deps.logger,
|
|
2076
|
+
correlation: deps.correlation
|
|
2077
|
+
});
|
|
2078
|
+
for (const [stream, entry] of result.truncated) {
|
|
2079
|
+
truncated.set(stream, entry);
|
|
2080
|
+
}
|
|
2081
|
+
for (const stream of result.skipped) skipped.push(stream);
|
|
2082
|
+
await yield_between_batches(deps.config.closeYieldMs);
|
|
2083
|
+
};
|
|
2084
|
+
let candidates = [];
|
|
2085
|
+
for (const [stream, { head, count }] of stats) {
|
|
2086
|
+
inspected += 1;
|
|
2087
|
+
const owner = deps.event_to_state.get(head.name);
|
|
2088
|
+
if (!owner) continue;
|
|
2089
|
+
const predicate = deps.autoclose_policy(owner.name);
|
|
2090
|
+
if (!predicate) continue;
|
|
2091
|
+
let close = false;
|
|
2092
|
+
try {
|
|
2093
|
+
close = predicate(stream, head, count ?? 0);
|
|
2094
|
+
evaluated += 1;
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
predicate_errors += 1;
|
|
2097
|
+
deps.logger.error(
|
|
2098
|
+
`Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
|
|
2099
|
+
);
|
|
2100
|
+
close = deps.config.closeOnError;
|
|
2101
|
+
}
|
|
2102
|
+
if (!close) continue;
|
|
2103
|
+
const archiver = deps.autoclose_archiver(owner.name);
|
|
2104
|
+
const archive = archiver ? () => archiver(stream, head) : void 0;
|
|
2105
|
+
candidates.push({ stream, archive });
|
|
2106
|
+
if (candidates.length >= deps.config.closeBatchSize) {
|
|
2107
|
+
await flush_batch(candidates);
|
|
2108
|
+
candidates = [];
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
await flush_batch(candidates);
|
|
2112
|
+
return {
|
|
2113
|
+
inspected,
|
|
2114
|
+
evaluated,
|
|
2115
|
+
predicate_errors,
|
|
2116
|
+
close_result: { truncated, skipped }
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
var AutocloseController = class {
|
|
2120
|
+
deps;
|
|
2121
|
+
_timer;
|
|
2122
|
+
/**
|
|
2123
|
+
* Reentrancy guard. The interval fires the next tick on cadence
|
|
2124
|
+
* regardless of how long the previous one took; this flag drops
|
|
2125
|
+
* overlapping ticks so the cycle stays sequential per controller.
|
|
2126
|
+
*/
|
|
2127
|
+
_running = false;
|
|
2128
|
+
constructor(deps) {
|
|
2129
|
+
this.deps = deps;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Start the cycle ticker. Returns `false` if already running so
|
|
2133
|
+
* accidental double-start doesn't stack timers (matches
|
|
2134
|
+
* `_correlate.start_polling` semantics).
|
|
2135
|
+
*/
|
|
2136
|
+
start() {
|
|
2137
|
+
if (this._timer) return false;
|
|
2138
|
+
this._timer = setInterval(() => {
|
|
2139
|
+
this.run_once().catch((err) => {
|
|
2140
|
+
this.deps.logger.error(
|
|
2141
|
+
`Autoclose cycle errored: ${err instanceof Error ? err.message : String(err)}`
|
|
2142
|
+
);
|
|
2143
|
+
});
|
|
2144
|
+
}, this.deps.config.autocloseCycleMs);
|
|
2145
|
+
this._timer.unref();
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Stop the cycle ticker. Idempotent.
|
|
2150
|
+
*/
|
|
2151
|
+
stop() {
|
|
2152
|
+
if (this._timer) {
|
|
2153
|
+
clearInterval(this._timer);
|
|
2154
|
+
this._timer = void 0;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Whether the ticker is currently running.
|
|
2159
|
+
*/
|
|
2160
|
+
get is_running() {
|
|
2161
|
+
return this._timer !== void 0;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Run one cycle synchronously. The controller's interval callback
|
|
2165
|
+
* delegates here; tests can invoke it directly to deterministically
|
|
2166
|
+
* exercise the cycle without spinning real timers.
|
|
2167
|
+
*
|
|
2168
|
+
* Overlapping invocations short-circuit: if a previous tick is
|
|
2169
|
+
* still running, this one drops and returns `null`. The orchestrator
|
|
2170
|
+
* never sees a queue of pending ticks regardless of how slow the
|
|
2171
|
+
* predicate / archive / truncate are.
|
|
2172
|
+
*/
|
|
2173
|
+
async run_once() {
|
|
2174
|
+
if (this._running) return null;
|
|
2175
|
+
this._running = true;
|
|
2176
|
+
try {
|
|
2177
|
+
const correlation = close_correlation(
|
|
2178
|
+
this.deps.correlator,
|
|
2179
|
+
this.deps.close_actor
|
|
2180
|
+
);
|
|
2181
|
+
const result = await run_autoclose_cycle({
|
|
2182
|
+
autoclose_policy: this.deps.autoclose_policy,
|
|
2183
|
+
autoclose_archiver: this.deps.autoclose_archiver,
|
|
2184
|
+
event_to_state: this.deps.event_to_state,
|
|
2185
|
+
reactive_events_size: this.deps.reactive_events_size,
|
|
2186
|
+
load: this.deps.load,
|
|
2187
|
+
tombstone: this.deps.tombstone,
|
|
2188
|
+
logger: this.deps.logger,
|
|
2189
|
+
config: this.deps.config,
|
|
2190
|
+
correlation
|
|
2191
|
+
});
|
|
2192
|
+
if (result.close_result.truncated.size > 0) {
|
|
2193
|
+
this.deps.on_closed(result.close_result);
|
|
2194
|
+
}
|
|
2195
|
+
return result;
|
|
2196
|
+
} finally {
|
|
2197
|
+
this._running = false;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
// src/internal/autoclose-policy.ts
|
|
2203
|
+
var import_zod6 = require("zod");
|
|
2204
|
+
var MIN_AFTER_MS = 6e4;
|
|
2205
|
+
var AfterSchema = import_zod6.z.object({
|
|
2206
|
+
days: import_zod6.z.number({ message: "autocloses: after.days must be a number" }).positive("autocloses: after.days must be > 0")
|
|
2207
|
+
}).refine((o) => o.days * 864e5 >= MIN_AFTER_MS, {
|
|
2208
|
+
message: `autocloses: after resolves to < ${MIN_AFTER_MS}ms (one minute), which is too short to be a meaningful retention window`
|
|
2209
|
+
});
|
|
2210
|
+
var IsSchema = import_zod6.z.union([
|
|
2211
|
+
import_zod6.z.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
2212
|
+
import_zod6.z.array(
|
|
2213
|
+
import_zod6.z.string().min(1, "autocloses: is entries must be non-empty event names")
|
|
2214
|
+
).min(1, "autocloses: is must include at least one event name").readonly()
|
|
2215
|
+
]);
|
|
2216
|
+
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");
|
|
2217
|
+
var OrBlockSchema = import_zod6.z.object({
|
|
2218
|
+
after: AfterSchema.optional(),
|
|
2219
|
+
is: IsSchema.optional(),
|
|
2220
|
+
reaches: ReachesSchema.optional()
|
|
2221
|
+
}).strict().refine(
|
|
2222
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
2223
|
+
{
|
|
2224
|
+
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
2225
|
+
}
|
|
2226
|
+
);
|
|
2227
|
+
var AutoclosePolicySchema = import_zod6.z.object({
|
|
2228
|
+
after: AfterSchema.optional(),
|
|
2229
|
+
is: IsSchema.optional(),
|
|
2230
|
+
reaches: ReachesSchema.optional(),
|
|
2231
|
+
or: OrBlockSchema.optional()
|
|
2232
|
+
}).strict().refine(
|
|
2233
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
2234
|
+
{
|
|
2235
|
+
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
2236
|
+
}
|
|
2237
|
+
);
|
|
2238
|
+
function compile_after(after) {
|
|
2239
|
+
const after_ms = after.days * 864e5;
|
|
2240
|
+
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
2241
|
+
}
|
|
2242
|
+
function compile_is(is) {
|
|
2243
|
+
const set = new Set(typeof is === "string" ? [is] : is);
|
|
2244
|
+
return (_stream, head) => set.has(head.name);
|
|
2245
|
+
}
|
|
2246
|
+
function compile_reaches(reaches) {
|
|
2247
|
+
return (_stream, _head, count) => count >= reaches;
|
|
2248
|
+
}
|
|
2249
|
+
function compile_autoclose_policy(options) {
|
|
2250
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
2251
|
+
const and_preds = [];
|
|
2252
|
+
if (parsed.after) and_preds.push(compile_after(parsed.after));
|
|
2253
|
+
if (parsed.is) and_preds.push(compile_is(parsed.is));
|
|
2254
|
+
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
2255
|
+
const or_preds = [];
|
|
2256
|
+
if (parsed.or) {
|
|
2257
|
+
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
2258
|
+
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
2259
|
+
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
2260
|
+
}
|
|
2261
|
+
return (stream, head, count) => {
|
|
2262
|
+
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
2263
|
+
return true;
|
|
2264
|
+
}
|
|
2265
|
+
if (or_preds.some((p) => p(stream, head, count))) {
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
return false;
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/internal/build-classify.ts
|
|
2273
|
+
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
2274
|
+
function classify_registry(registry, states) {
|
|
2275
|
+
const statics = /* @__PURE__ */ new Map();
|
|
2276
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
2277
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
2278
|
+
let has_dynamic_resolvers = false;
|
|
2279
|
+
for (const [name, register] of Object.entries(registry.events)) {
|
|
2280
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
2281
|
+
for (const reaction of register.reactions.values()) {
|
|
2282
|
+
if (typeof reaction.resolver === "function") {
|
|
2283
|
+
has_dynamic_resolvers = true;
|
|
2284
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
2285
|
+
} else {
|
|
2286
|
+
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
2287
|
+
const lane_name = lane ?? "default";
|
|
2288
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
2289
|
+
if (existing_lanes !== ALL_LANES) {
|
|
2290
|
+
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
2291
|
+
set.add(lane_name);
|
|
2292
|
+
event_to_lanes.set(name, set);
|
|
2293
|
+
}
|
|
2294
|
+
const key = `${target}|${source ?? ""}`;
|
|
2295
|
+
const existing = statics.get(key);
|
|
2296
|
+
if (!existing) {
|
|
2297
|
+
statics.set(key, { stream: target, source, priority, lane });
|
|
2298
|
+
} else {
|
|
2299
|
+
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
2300
|
+
throw new Error(
|
|
2301
|
+
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
2302
|
+
);
|
|
2303
|
+
if (priority > existing.priority) {
|
|
2304
|
+
statics.set(key, { ...existing, priority });
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
2311
|
+
for (const merged of states.values()) {
|
|
2312
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
2313
|
+
event_to_state.set(event_name, merged);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return {
|
|
2317
|
+
static_targets: [...statics.values()],
|
|
2318
|
+
has_dynamic_resolvers,
|
|
2319
|
+
reactive_events,
|
|
2320
|
+
event_to_state,
|
|
2321
|
+
event_to_lanes
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2057
2325
|
// src/internal/correlate-cycle.ts
|
|
2058
2326
|
var CorrelateCycle = class {
|
|
2059
2327
|
_checkpoint = -1;
|
|
@@ -2182,30 +2450,6 @@ var CorrelateCycle = class {
|
|
|
2182
2450
|
}
|
|
2183
2451
|
};
|
|
2184
2452
|
|
|
2185
|
-
// src/internal/correlator.ts
|
|
2186
|
-
var import_node_crypto = require("crypto");
|
|
2187
|
-
var BASE = 36;
|
|
2188
|
-
var SEG_WIDTH = 4;
|
|
2189
|
-
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
2190
|
-
function seg(n) {
|
|
2191
|
-
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
2192
|
-
}
|
|
2193
|
-
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
2194
|
-
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2195
|
-
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2196
|
-
const ts = seg(Date.now() % SEG_SPACE);
|
|
2197
|
-
const rnd = seg((0, import_node_crypto.randomInt)(SEG_SPACE));
|
|
2198
|
-
return `${s}-${a}-${ts}${rnd}`;
|
|
2199
|
-
};
|
|
2200
|
-
function close_correlation(correlator, actor) {
|
|
2201
|
-
return correlator({
|
|
2202
|
-
state: "$close",
|
|
2203
|
-
action: "close",
|
|
2204
|
-
stream: "$close",
|
|
2205
|
-
actor
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
2453
|
// src/internal/drain-cycle.ts
|
|
2210
2454
|
var import_node_crypto2 = require("crypto");
|
|
2211
2455
|
|
|
@@ -2981,7 +3225,7 @@ var DrainController = class {
|
|
|
2981
3225
|
};
|
|
2982
3226
|
|
|
2983
3227
|
// src/internal/merge.ts
|
|
2984
|
-
var
|
|
3228
|
+
var import_zod7 = require("zod");
|
|
2985
3229
|
function base_type_name(zodType) {
|
|
2986
3230
|
let t = zodType;
|
|
2987
3231
|
while (typeof t.unwrap === "function") {
|
|
@@ -2990,7 +3234,7 @@ function base_type_name(zodType) {
|
|
|
2990
3234
|
return t.constructor.name;
|
|
2991
3235
|
}
|
|
2992
3236
|
function merge_schemas(existing, incoming, state_name) {
|
|
2993
|
-
if (existing instanceof
|
|
3237
|
+
if (existing instanceof import_zod7.ZodObject && incoming instanceof import_zod7.ZodObject) {
|
|
2994
3238
|
const existing_shape = existing.shape;
|
|
2995
3239
|
const incoming_shape = incoming.shape;
|
|
2996
3240
|
for (const key of Object.keys(incoming_shape)) {
|
|
@@ -3309,6 +3553,22 @@ var Act = class {
|
|
|
3309
3553
|
registry;
|
|
3310
3554
|
/** Map of state name → state definition; populated by the builder. */
|
|
3311
3555
|
_states;
|
|
3556
|
+
/**
|
|
3557
|
+
* Resolved autoclose configuration (#837 / epic #802). Frozen at
|
|
3558
|
+
* `act().build()` via {@link resolveAutocloseConfig}.
|
|
3559
|
+
*
|
|
3560
|
+
* @internal
|
|
3561
|
+
*/
|
|
3562
|
+
_autoclose_config;
|
|
3563
|
+
/**
|
|
3564
|
+
* App-level autoclose controller. `undefined` when no state declares
|
|
3565
|
+
* `.autocloses(...)` — the controller is never constructed, so
|
|
3566
|
+
* `start_correlations()` / `stop_correlations()` skip the autoclose
|
|
3567
|
+
* lifecycle and the orchestrator pays zero per-tick cost.
|
|
3568
|
+
*
|
|
3569
|
+
* @internal
|
|
3570
|
+
*/
|
|
3571
|
+
_autoclose;
|
|
3312
3572
|
/**
|
|
3313
3573
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
3314
3574
|
* via {@link ActLifecycleEvents}.
|
|
@@ -3443,6 +3703,7 @@ var Act = class {
|
|
|
3443
3703
|
this._reactive_events = reactive_events;
|
|
3444
3704
|
this._listen = options.listen !== false;
|
|
3445
3705
|
this._drain = options.drain !== false;
|
|
3706
|
+
this._autoclose_config = resolveAutocloseConfig(options);
|
|
3446
3707
|
this._event_to_state = event_to_state;
|
|
3447
3708
|
this._event_to_lanes = event_to_lanes;
|
|
3448
3709
|
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
@@ -3506,6 +3767,26 @@ var Act = class {
|
|
|
3506
3767
|
},
|
|
3507
3768
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
3508
3769
|
);
|
|
3770
|
+
const states_with_policy = [...this._states.values()].filter(
|
|
3771
|
+
(s) => s.autoclose
|
|
3772
|
+
);
|
|
3773
|
+
this._autoclose = states_with_policy.length ? new AutocloseController({
|
|
3774
|
+
// Internal cycle takes the loose `(string) => …` signature; the
|
|
3775
|
+
// public `registry.autoclose_policy` narrows to
|
|
3776
|
+
// `keyof TStateMap & string`. Thin lambdas bridge across the
|
|
3777
|
+
// narrowing — runtime semantics are unchanged.
|
|
3778
|
+
autoclose_policy: (name) => this.registry.autoclose_policy(name),
|
|
3779
|
+
autoclose_archiver: (name) => this.registry.autoclose_archiver(name),
|
|
3780
|
+
event_to_state: this._event_to_state,
|
|
3781
|
+
reactive_events_size: this._reactive_events.size,
|
|
3782
|
+
load: this._es.load,
|
|
3783
|
+
tombstone: this._es.tombstone,
|
|
3784
|
+
logger: this._logger,
|
|
3785
|
+
config: this._autoclose_config,
|
|
3786
|
+
correlator: this._correlator,
|
|
3787
|
+
close_actor: { id: "$autoclose", name: "autoclose" },
|
|
3788
|
+
on_closed: (result) => this.emit("closed", result)
|
|
3789
|
+
}) : void 0;
|
|
3509
3790
|
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store2());
|
|
3510
3791
|
dispose(() => this.shutdown());
|
|
3511
3792
|
}
|
|
@@ -3997,7 +4278,9 @@ var Act = class {
|
|
|
3997
4278
|
* @see {@link stop_correlations} to stop the worker
|
|
3998
4279
|
*/
|
|
3999
4280
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
4000
|
-
|
|
4281
|
+
const started = this._correlate.start_polling(query, frequency, callback);
|
|
4282
|
+
this._autoclose?.start();
|
|
4283
|
+
return started;
|
|
4001
4284
|
}
|
|
4002
4285
|
/**
|
|
4003
4286
|
* Stops the automatic correlation worker.
|
|
@@ -4018,6 +4301,7 @@ var Act = class {
|
|
|
4018
4301
|
*/
|
|
4019
4302
|
stop_correlations() {
|
|
4020
4303
|
this._correlate.stop_polling();
|
|
4304
|
+
this._autoclose?.stop();
|
|
4021
4305
|
}
|
|
4022
4306
|
/**
|
|
4023
4307
|
* Cancels any pending or active settle cycle.
|
|
@@ -4402,13 +4686,17 @@ function act() {
|
|
|
4402
4686
|
const _sf = /* @__PURE__ */ new Map();
|
|
4403
4687
|
const _dp = /* @__PURE__ */ new Map();
|
|
4404
4688
|
const _de = /* @__PURE__ */ new Map();
|
|
4689
|
+
const _ac = /* @__PURE__ */ new Map();
|
|
4690
|
+
const _aa = /* @__PURE__ */ new Map();
|
|
4405
4691
|
const EMPTY_DEPRECATED = /* @__PURE__ */ new Set();
|
|
4406
4692
|
const registry = {
|
|
4407
4693
|
actions: {},
|
|
4408
4694
|
events: {},
|
|
4409
4695
|
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
4410
4696
|
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null,
|
|
4411
|
-
deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED
|
|
4697
|
+
deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED,
|
|
4698
|
+
autoclose_policy: (state_name) => _ac.get(state_name) ?? null,
|
|
4699
|
+
autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
|
|
4412
4700
|
};
|
|
4413
4701
|
const pending_projections = [];
|
|
4414
4702
|
const batch_handlers = /* @__PURE__ */ new Map();
|
|
@@ -4535,6 +4823,8 @@ function act() {
|
|
|
4535
4823
|
}
|
|
4536
4824
|
for (const state2 of states.values()) {
|
|
4537
4825
|
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
4826
|
+
if (state2.autoclose) _ac.set(state2.name, state2.autoclose);
|
|
4827
|
+
if (state2.archive) _aa.set(state2.name, state2.archive);
|
|
4538
4828
|
const fields_by_event = /* @__PURE__ */ new Map();
|
|
4539
4829
|
for (const event_name of Object.keys(state2.events)) {
|
|
4540
4830
|
const fields = _sf.get(event_name);
|
|
@@ -4802,6 +5092,31 @@ function action_builder(state2) {
|
|
|
4802
5092
|
internal.disclose = disclose;
|
|
4803
5093
|
return builder;
|
|
4804
5094
|
},
|
|
5095
|
+
autocloses(predicate_or_policy) {
|
|
5096
|
+
let predicate;
|
|
5097
|
+
if (typeof predicate_or_policy === "function") {
|
|
5098
|
+
predicate = predicate_or_policy;
|
|
5099
|
+
} else if (predicate_or_policy !== null && typeof predicate_or_policy === "object") {
|
|
5100
|
+
predicate = compile_autoclose_policy(
|
|
5101
|
+
predicate_or_policy
|
|
5102
|
+
);
|
|
5103
|
+
} else {
|
|
5104
|
+
throw new Error(
|
|
5105
|
+
".autocloses(...) requires a function or a policy object; got " + typeof predicate_or_policy
|
|
5106
|
+
);
|
|
5107
|
+
}
|
|
5108
|
+
internal.autoclose = predicate;
|
|
5109
|
+
return builder;
|
|
5110
|
+
},
|
|
5111
|
+
archives(archive) {
|
|
5112
|
+
if (typeof archive !== "function") {
|
|
5113
|
+
throw new Error(
|
|
5114
|
+
".archives(archive) requires a function; got " + typeof archive
|
|
5115
|
+
);
|
|
5116
|
+
}
|
|
5117
|
+
internal.archive = archive;
|
|
5118
|
+
return builder;
|
|
5119
|
+
},
|
|
4805
5120
|
build() {
|
|
4806
5121
|
return internal;
|
|
4807
5122
|
}
|
|
@@ -4978,6 +5293,9 @@ function write_line(writer, line) {
|
|
|
4978
5293
|
ConcurrencyError,
|
|
4979
5294
|
ConsoleLogger,
|
|
4980
5295
|
CsvFile,
|
|
5296
|
+
DEFAULT_AUTOCLOSE_CYCLE_MS,
|
|
5297
|
+
DEFAULT_CLOSE_BATCH_SIZE,
|
|
5298
|
+
DEFAULT_CLOSE_YIELD_MS,
|
|
4981
5299
|
DEFAULT_LANE,
|
|
4982
5300
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
4983
5301
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
@@ -5009,6 +5327,7 @@ function write_line(writer, line) {
|
|
|
5009
5327
|
log,
|
|
5010
5328
|
port,
|
|
5011
5329
|
projection,
|
|
5330
|
+
resolveAutocloseConfig,
|
|
5012
5331
|
scoped,
|
|
5013
5332
|
sensitive,
|
|
5014
5333
|
sleep,
|