@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/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/build-classify.ts
1858
- var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
1859
- function classify_registry(registry, states) {
1860
- const statics = /* @__PURE__ */ new Map();
1861
- const reactive_events = /* @__PURE__ */ new Set();
1862
- const event_to_lanes = /* @__PURE__ */ new Map();
1863
- let has_dynamic_resolvers = false;
1864
- for (const [name, register] of Object.entries(registry.events)) {
1865
- if (register.reactions.size > 0) reactive_events.add(name);
1866
- for (const reaction of register.reactions.values()) {
1867
- if (typeof reaction.resolver === "function") {
1868
- has_dynamic_resolvers = true;
1869
- event_to_lanes.set(name, ALL_LANES);
1870
- } else {
1871
- const { target, source, priority = 0, lane } = reaction.resolver;
1872
- const lane_name = lane ?? "default";
1873
- const existing_lanes = event_to_lanes.get(name);
1874
- if (existing_lanes !== ALL_LANES) {
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 import_zod5 = require("zod");
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 import_zod5.ZodObject && incoming instanceof import_zod5.ZodObject) {
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
- return this._correlate.start_polling(query, frequency, callback);
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,