@rotorsoft/act 1.14.0 → 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.
Files changed (32) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +34 -15
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  5. package/dist/@types/internal/autoclose-config.d.ts +44 -11
  6. package/dist/@types/internal/autoclose-config.d.ts.map +1 -1
  7. package/dist/@types/internal/autoclose-cycle.d.ts +11 -6
  8. package/dist/@types/internal/autoclose-cycle.d.ts.map +1 -1
  9. package/dist/@types/internal/close-cycle.d.ts +14 -0
  10. package/dist/@types/internal/close-cycle.d.ts.map +1 -1
  11. package/dist/@types/internal/index.d.ts +1 -1
  12. package/dist/@types/internal/index.d.ts.map +1 -1
  13. package/dist/@types/types/ports.d.ts +49 -0
  14. package/dist/@types/types/ports.d.ts.map +1 -1
  15. package/dist/{chunk-QKVY5QAQ.js → chunk-DBQT3ALY.js} +21 -2
  16. package/dist/{chunk-QKVY5QAQ.js.map → chunk-DBQT3ALY.js.map} +1 -1
  17. package/dist/{chunk-5WRI5ZAA.js → chunk-YLJ4XMA6.js} +6 -2
  18. package/dist/{dist-INXH4GUE.js → dist-EOG547W7.js} +2 -2
  19. package/dist/index.cjs +139 -63
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +120 -63
  22. package/dist/index.js.map +1 -1
  23. package/dist/{magic-string.es-ZGO77SPS.js → magic-string.es-5F2IVYTG.js} +2 -2
  24. package/dist/test/index.cjs +27 -4
  25. package/dist/test/index.cjs.map +1 -1
  26. package/dist/test/index.js +4 -4
  27. package/dist/test/index.js.map +1 -1
  28. package/dist/types/index.js +1 -1
  29. package/package.json +2 -2
  30. /package/dist/{chunk-5WRI5ZAA.js.map → chunk-YLJ4XMA6.js.map} +0 -0
  31. /package/dist/{dist-INXH4GUE.js.map → dist-EOG547W7.js.map} +0 -0
  32. /package/dist/{magic-string.es-ZGO77SPS.js.map → magic-string.es-5F2IVYTG.js.map} +0 -0
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,17 +1995,37 @@ 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
- await store2().query_streams((position) => {
1948
- const source_re = position.source ? RegExp(position.source) : void 0;
1949
- for (const [stream, info] of stream_info) {
1950
- if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
1951
- pending_set.add(stream);
1952
- }
2001
+ const source_regex = /* @__PURE__ */ new Map();
2002
+ const get_regex = (source) => {
2003
+ let re = source_regex.get(source);
2004
+ if (!re) {
2005
+ re = new RegExp(source);
2006
+ source_regex.set(source, re);
1953
2007
  }
1954
- });
2008
+ return re;
2009
+ };
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
+ }
1955
2029
  const safe = [];
1956
2030
  for (const [stream] of stream_info) {
1957
2031
  if (pending_set.has(stream)) skipped.push(stream);
@@ -2072,54 +2146,54 @@ async function run_autoclose_cycle(deps) {
2072
2146
  let predicate_errors = 0;
2073
2147
  const truncated = /* @__PURE__ */ new Map();
2074
2148
  const skipped = [];
2075
- const stats = await s.query_stats(
2076
- {},
2077
- { count: true, exclude: [TOMBSTONE_EVENT] }
2078
- );
2079
- const flush_batch = async (candidates2) => {
2080
- if (candidates2.length === 0) return;
2081
- const result = await run_close_cycle(candidates2, {
2082
- reactive_events_size: deps.reactive_events_size,
2083
- event_to_state: deps.event_to_state,
2084
- load: deps.load,
2085
- tombstone: deps.tombstone,
2086
- logger: deps.logger,
2087
- correlation: deps.correlation
2088
- });
2089
- for (const [stream, entry] of result.truncated) {
2090
- truncated.set(stream, entry);
2091
- }
2092
- for (const stream of result.skipped) skipped.push(stream);
2093
- await yield_between_batches(deps.config.closeYieldMs);
2094
- };
2095
- let candidates = [];
2096
- for (const [stream, { head, count }] of stats) {
2097
- inspected += 1;
2098
- const owner = deps.event_to_state.get(head.name);
2099
- if (!owner) continue;
2100
- const predicate = deps.autoclose_policy(owner.name);
2101
- if (!predicate) continue;
2102
- let close = false;
2103
- try {
2104
- close = predicate(stream, head, count ?? 0);
2105
- evaluated += 1;
2106
- } catch (err) {
2107
- predicate_errors += 1;
2108
- deps.logger.error(
2109
- `Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
2110
- );
2111
- 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 });
2112
2179
  }
2113
- if (!close) continue;
2114
- const archiver = deps.autoclose_archiver(owner.name);
2115
- const archive = archiver ? () => archiver(stream, head) : void 0;
2116
- candidates.push({ stream, archive });
2117
- if (candidates.length >= deps.config.closeBatchSize) {
2118
- await flush_batch(candidates);
2119
- 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);
2120
2193
  }
2194
+ if (stats.size < page) break;
2195
+ after = [...stats.keys()].at(-1);
2121
2196
  }
2122
- await flush_batch(candidates);
2123
2197
  return {
2124
2198
  inspected,
2125
2199
  evaluated,
@@ -2149,7 +2223,7 @@ var AutocloseController = class {
2149
2223
  this._timer = setInterval(() => {
2150
2224
  this.run_once().catch(() => {
2151
2225
  });
2152
- }, this.deps.config.autocloseCycleMs);
2226
+ }, this.deps.config.autocloseCycleMinutes * 6e4);
2153
2227
  this._timer.unref();
2154
2228
  return true;
2155
2229
  }
@@ -2181,6 +2255,8 @@ var AutocloseController = class {
2181
2255
  async run_once() {
2182
2256
  if (this._running) return null;
2183
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;
2184
2260
  this._running = true;
2185
2261
  try {
2186
2262
  const correlation = close_correlation(
@@ -5456,7 +5532,7 @@ function write_line(writer, line) {
5456
5532
  ConcurrencyError,
5457
5533
  ConsoleLogger,
5458
5534
  CsvFile,
5459
- DEFAULT_AUTOCLOSE_CYCLE_MS,
5535
+ DEFAULT_AUTOCLOSE_CYCLE_MINUTES,
5460
5536
  DEFAULT_CLOSE_BATCH_SIZE,
5461
5537
  DEFAULT_CLOSE_YIELD_MS,
5462
5538
  DEFAULT_LANE,