@rotorsoft/act 1.14.1 → 1.15.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,7 +37,7 @@ __export(index_exports, {
37
37
  ConcurrencyError: () => ConcurrencyError,
38
38
  ConsoleLogger: () => ConsoleLogger,
39
39
  CsvFile: () => CsvFile,
40
- DEFAULT_AUTOCLOSE_CYCLE_MS: () => DEFAULT_AUTOCLOSE_CYCLE_MS,
40
+ DEFAULT_AUTOCLOSE_CYCLE_MINUTES: () => DEFAULT_AUTOCLOSE_CYCLE_MINUTES,
41
41
  DEFAULT_CLOSE_BATCH_SIZE: () => DEFAULT_CLOSE_BATCH_SIZE,
42
42
  DEFAULT_CLOSE_YIELD_MS: () => DEFAULT_CLOSE_YIELD_MS,
43
43
  DEFAULT_LANE: () => DEFAULT_LANE,
@@ -1169,8 +1169,18 @@ var InMemoryStore = class {
1169
1169
  const limit = query?.limit ?? 100;
1170
1170
  const after = query?.after;
1171
1171
  const blocked = query?.blocked;
1172
+ const source_matches = query?.source_matches;
1172
1173
  const stream_re = query?.stream && !query.stream_exact ? new RegExp(query.stream) : void 0;
1173
1174
  const source_re = query?.source && !query.source_exact ? new RegExp(query.source) : void 0;
1175
+ const reverse_cache = /* @__PURE__ */ new Map();
1176
+ const reverse_match = (source) => {
1177
+ let re = reverse_cache.get(source);
1178
+ if (!re) {
1179
+ re = new RegExp(source);
1180
+ reverse_cache.set(source, re);
1181
+ }
1182
+ return source_matches.some((name) => re.test(name));
1183
+ };
1174
1184
  const sorted = [...this._streams.values()].sort(
1175
1185
  (a, b) => a.stream.localeCompare(b.stream)
1176
1186
  );
@@ -1186,6 +1196,9 @@ var InMemoryStore = class {
1186
1196
  if (query.source_exact ? s.source !== query.source : !source_re.test(s.source))
1187
1197
  continue;
1188
1198
  }
1199
+ if (source_matches !== void 0) {
1200
+ if (s.source && !reverse_match(s.source)) continue;
1201
+ }
1189
1202
  if (blocked !== void 0 && s.blocked !== blocked) continue;
1190
1203
  if (query?.lane !== void 0 && s.lane !== query.lane) continue;
1191
1204
  callback({
@@ -1228,6 +1241,8 @@ var InMemoryStore = class {
1228
1241
  const want_count = options?.count ?? false;
1229
1242
  const want_names = options?.names ?? false;
1230
1243
  const before = options?.before;
1244
+ const after = options?.after;
1245
+ const limit = options?.limit;
1231
1246
  const array_targets = Array.isArray(input) ? new Set(input) : null;
1232
1247
  const filter = Array.isArray(input) ? null : input;
1233
1248
  const stream_re = filter?.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
@@ -1266,13 +1281,17 @@ var InMemoryStore = class {
1266
1281
  a.names[n] = (a.names[n] ?? 0) + 1;
1267
1282
  }
1268
1283
  }
1284
+ const ordered = [...acc.keys()].sort((x, y) => x.localeCompare(y));
1269
1285
  const out = /* @__PURE__ */ new Map();
1270
- for (const [stream, a] of acc) {
1286
+ for (const stream of ordered) {
1287
+ if (after !== void 0 && stream <= after) continue;
1288
+ const a = acc.get(stream);
1271
1289
  const stats = { head: a.head };
1272
1290
  if (want_tail) stats.tail = a.tail;
1273
1291
  if (want_count) stats.count = a.count;
1274
1292
  if (want_names) stats.names = a.names;
1275
1293
  out.set(stream, stats);
1294
+ if (limit !== void 0 && out.size >= limit) break;
1276
1295
  }
1277
1296
  return out;
1278
1297
  }
@@ -1871,25 +1890,59 @@ var PASS_FACTORIES = {
1871
1890
 
1872
1891
  // src/internal/autoclose-config.ts
1873
1892
  var import_zod5 = require("zod");
1874
- var DEFAULT_AUTOCLOSE_CYCLE_MS = 6e4;
1893
+ var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
1875
1894
  var DEFAULT_CLOSE_BATCH_SIZE = 64;
1876
1895
  var DEFAULT_CLOSE_YIELD_MS = 0;
1896
+ var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
1897
+ function is_valid_time_zone(tz) {
1898
+ try {
1899
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
1900
+ return true;
1901
+ } catch {
1902
+ return false;
1903
+ }
1904
+ }
1905
+ var AutocloseWindowSchema = import_zod5.z.object({
1906
+ start: import_zod5.z.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
1907
+ end: import_zod5.z.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
1908
+ timeZone: import_zod5.z.string().refine(is_valid_time_zone, {
1909
+ message: "autocloseWindow.timeZone must be a valid IANA time zone"
1910
+ }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
1911
+ }).refine((w) => w.start !== w.end, {
1912
+ message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
1913
+ });
1877
1914
  var AutocloseOptionsSchema = import_zod5.z.object({
1878
- autocloseCycleMs: import_zod5.z.number().min(1e4).max(36e5).default(DEFAULT_AUTOCLOSE_CYCLE_MS),
1915
+ autocloseCycleMinutes: import_zod5.z.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
1879
1916
  closeBatchSize: import_zod5.z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
1880
1917
  closeYieldMs: import_zod5.z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
1881
- closeOnError: import_zod5.z.boolean().default(false)
1918
+ closeOnError: import_zod5.z.boolean().default(false),
1919
+ autocloseWindow: AutocloseWindowSchema.optional()
1882
1920
  });
1883
1921
  function resolveAutocloseConfig(options) {
1884
1922
  return AutocloseOptionsSchema.parse({
1885
- autocloseCycleMs: options?.autocloseCycleMs,
1923
+ autocloseCycleMinutes: options?.autocloseCycleMinutes,
1886
1924
  closeBatchSize: options?.closeBatchSize,
1887
1925
  closeYieldMs: options?.closeYieldMs,
1888
- closeOnError: options?.closeOnError
1926
+ closeOnError: options?.closeOnError,
1927
+ autocloseWindow: options?.autocloseWindow
1889
1928
  });
1890
1929
  }
1930
+ function hour_in_zone(now, timeZone) {
1931
+ const parts = new Intl.DateTimeFormat("en-US", {
1932
+ timeZone,
1933
+ hour: "2-digit",
1934
+ hourCycle: "h23"
1935
+ }).formatToParts(now);
1936
+ return Number(parts.find((p) => p.type === "hour")?.value);
1937
+ }
1938
+ function in_autoclose_window(window, now) {
1939
+ if (!window) return true;
1940
+ const hour = hour_in_zone(now, window.timeZone);
1941
+ return window.start < window.end ? hour >= window.start && hour < window.end : hour >= window.start || hour < window.end;
1942
+ }
1891
1943
 
1892
1944
  // src/internal/close-cycle.ts
1945
+ var SAFETY_PROBE_PAGE_SIZE = 1e3;
1893
1946
  async function run_close_cycle(targets, deps) {
1894
1947
  const target_map = new Map(targets.map((t) => [t.stream, t]));
1895
1948
  const streams = [...target_map.keys()];
@@ -1898,7 +1951,8 @@ async function run_close_cycle(targets, deps) {
1898
1951
  const safe = await partition_by_safety(
1899
1952
  stream_info,
1900
1953
  deps.reactive_events_size,
1901
- skipped
1954
+ skipped,
1955
+ deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
1902
1956
  );
1903
1957
  if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
1904
1958
  const { guarded, guard_events } = await guard_with_tombstones(
@@ -1941,7 +1995,7 @@ async function scan_stream_heads(streams) {
1941
1995
  }
1942
1996
  return out;
1943
1997
  }
1944
- async function partition_by_safety(stream_info, reactive_events_size, skipped) {
1998
+ async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size) {
1945
1999
  if (reactive_events_size === 0) return [...stream_info.keys()];
1946
2000
  const pending_set = /* @__PURE__ */ new Set();
1947
2001
  const source_regex = /* @__PURE__ */ new Map();
@@ -1953,14 +2007,25 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped) {
1953
2007
  }
1954
2008
  return re;
1955
2009
  };
1956
- await store2().query_streams((position) => {
1957
- const source_re = position.source ? get_regex(position.source) : void 0;
1958
- for (const [stream, info] of stream_info) {
1959
- if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
1960
- pending_set.add(stream);
1961
- }
1962
- }
1963
- });
2010
+ const targets = [...stream_info.keys()];
2011
+ let after;
2012
+ for (; ; ) {
2013
+ let last;
2014
+ const { count } = await store2().query_streams(
2015
+ (position) => {
2016
+ last = position.stream;
2017
+ const source_re = position.source ? get_regex(position.source) : void 0;
2018
+ for (const [stream, info] of stream_info) {
2019
+ if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
2020
+ pending_set.add(stream);
2021
+ }
2022
+ }
2023
+ },
2024
+ { after, limit: page_size, source_matches: targets }
2025
+ );
2026
+ if (count < page_size) break;
2027
+ after = last;
2028
+ }
1964
2029
  const safe = [];
1965
2030
  for (const [stream] of stream_info) {
1966
2031
  if (pending_set.has(stream)) skipped.push(stream);
@@ -2081,54 +2146,54 @@ async function run_autoclose_cycle(deps) {
2081
2146
  let predicate_errors = 0;
2082
2147
  const truncated = /* @__PURE__ */ new Map();
2083
2148
  const skipped = [];
2084
- const stats = await s.query_stats(
2085
- {},
2086
- { count: true, exclude: [TOMBSTONE_EVENT] }
2087
- );
2088
- const flush_batch = async (candidates2) => {
2089
- if (candidates2.length === 0) return;
2090
- const result = await run_close_cycle(candidates2, {
2091
- reactive_events_size: deps.reactive_events_size,
2092
- event_to_state: deps.event_to_state,
2093
- load: deps.load,
2094
- tombstone: deps.tombstone,
2095
- logger: deps.logger,
2096
- correlation: deps.correlation
2097
- });
2098
- for (const [stream, entry] of result.truncated) {
2099
- truncated.set(stream, entry);
2100
- }
2101
- for (const stream of result.skipped) skipped.push(stream);
2102
- await yield_between_batches(deps.config.closeYieldMs);
2103
- };
2104
- let candidates = [];
2105
- for (const [stream, { head, count }] of stats) {
2106
- inspected += 1;
2107
- const owner = deps.event_to_state.get(head.name);
2108
- if (!owner) continue;
2109
- const predicate = deps.autoclose_policy(owner.name);
2110
- if (!predicate) continue;
2111
- let close = false;
2112
- try {
2113
- close = predicate(stream, head, count ?? 0);
2114
- evaluated += 1;
2115
- } catch (err) {
2116
- predicate_errors += 1;
2117
- deps.logger.error(
2118
- `Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
2119
- );
2120
- close = deps.config.closeOnError;
2149
+ const page = deps.config.closeBatchSize;
2150
+ let after;
2151
+ for (; ; ) {
2152
+ const stats = await s.query_stats(
2153
+ {},
2154
+ { count: true, exclude: [TOMBSTONE_EVENT], after, limit: page }
2155
+ );
2156
+ if (stats.size === 0) break;
2157
+ inspected += stats.size;
2158
+ const candidates = [];
2159
+ for (const [stream, { head, count }] of stats) {
2160
+ const owner = deps.event_to_state.get(head.name);
2161
+ if (!owner) continue;
2162
+ const predicate = deps.autoclose_policy(owner.name);
2163
+ if (!predicate) continue;
2164
+ let close = false;
2165
+ try {
2166
+ close = predicate(stream, head, count ?? 0);
2167
+ evaluated += 1;
2168
+ } catch (err) {
2169
+ predicate_errors += 1;
2170
+ deps.logger.error(
2171
+ `Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
2172
+ );
2173
+ close = deps.config.closeOnError;
2174
+ }
2175
+ if (!close) continue;
2176
+ const archiver = deps.autoclose_archiver(owner.name);
2177
+ const archive = archiver ? () => archiver(stream, head) : void 0;
2178
+ candidates.push({ stream, archive });
2121
2179
  }
2122
- if (!close) continue;
2123
- const archiver = deps.autoclose_archiver(owner.name);
2124
- const archive = archiver ? () => archiver(stream, head) : void 0;
2125
- candidates.push({ stream, archive });
2126
- if (candidates.length >= deps.config.closeBatchSize) {
2127
- await flush_batch(candidates);
2128
- candidates = [];
2180
+ if (candidates.length > 0) {
2181
+ const result = await run_close_cycle(candidates, {
2182
+ reactive_events_size: deps.reactive_events_size,
2183
+ event_to_state: deps.event_to_state,
2184
+ load: deps.load,
2185
+ tombstone: deps.tombstone,
2186
+ logger: deps.logger,
2187
+ correlation: deps.correlation
2188
+ });
2189
+ for (const [stream, entry] of result.truncated)
2190
+ truncated.set(stream, entry);
2191
+ for (const stream of result.skipped) skipped.push(stream);
2192
+ await yield_between_batches(deps.config.closeYieldMs);
2129
2193
  }
2194
+ if (stats.size < page) break;
2195
+ after = [...stats.keys()].at(-1);
2130
2196
  }
2131
- await flush_batch(candidates);
2132
2197
  return {
2133
2198
  inspected,
2134
2199
  evaluated,
@@ -2158,7 +2223,7 @@ var AutocloseController = class {
2158
2223
  this._timer = setInterval(() => {
2159
2224
  this.run_once().catch(() => {
2160
2225
  });
2161
- }, this.deps.config.autocloseCycleMs);
2226
+ }, this.deps.config.autocloseCycleMinutes * 6e4);
2162
2227
  this._timer.unref();
2163
2228
  return true;
2164
2229
  }
@@ -2190,6 +2255,8 @@ var AutocloseController = class {
2190
2255
  async run_once() {
2191
2256
  if (this._running) return null;
2192
2257
  if (this.deps.breaker.state(Date.now()) === "open") return null;
2258
+ if (!in_autoclose_window(this.deps.config.autocloseWindow, /* @__PURE__ */ new Date()))
2259
+ return null;
2193
2260
  this._running = true;
2194
2261
  try {
2195
2262
  const correlation = close_correlation(
@@ -5465,7 +5532,7 @@ function write_line(writer, line) {
5465
5532
  ConcurrencyError,
5466
5533
  ConsoleLogger,
5467
5534
  CsvFile,
5468
- DEFAULT_AUTOCLOSE_CYCLE_MS,
5535
+ DEFAULT_AUTOCLOSE_CYCLE_MINUTES,
5469
5536
  DEFAULT_CLOSE_BATCH_SIZE,
5470
5537
  DEFAULT_CLOSE_YIELD_MS,
5471
5538
  DEFAULT_LANE,