@rotorsoft/act 1.14.1 → 1.16.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.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  sleep,
20
20
  store,
21
21
  validate
22
- } from "./chunk-QKVY5QAQ.js";
22
+ } from "./chunk-46OCUAVF.js";
23
23
  import {
24
24
  ActorSchema,
25
25
  CausationEventSchema,
@@ -489,25 +489,59 @@ var PASS_FACTORIES = {
489
489
 
490
490
  // src/internal/autoclose-config.ts
491
491
  import { z } from "zod";
492
- var DEFAULT_AUTOCLOSE_CYCLE_MS = 6e4;
492
+ var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
493
493
  var DEFAULT_CLOSE_BATCH_SIZE = 64;
494
494
  var DEFAULT_CLOSE_YIELD_MS = 0;
495
+ var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
496
+ function is_valid_time_zone(tz) {
497
+ try {
498
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
499
+ return true;
500
+ } catch {
501
+ return false;
502
+ }
503
+ }
504
+ var AutocloseWindowSchema = z.object({
505
+ start: z.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
506
+ end: z.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
507
+ timeZone: z.string().refine(is_valid_time_zone, {
508
+ message: "autocloseWindow.timeZone must be a valid IANA time zone"
509
+ }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
510
+ }).refine((w) => w.start !== w.end, {
511
+ message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
512
+ });
495
513
  var AutocloseOptionsSchema = z.object({
496
- autocloseCycleMs: z.number().min(1e4).max(36e5).default(DEFAULT_AUTOCLOSE_CYCLE_MS),
514
+ autocloseCycleMinutes: z.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
497
515
  closeBatchSize: z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
498
516
  closeYieldMs: z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
499
- closeOnError: z.boolean().default(false)
517
+ closeOnError: z.boolean().default(false),
518
+ autocloseWindow: AutocloseWindowSchema.optional()
500
519
  });
501
520
  function resolveAutocloseConfig(options) {
502
521
  return AutocloseOptionsSchema.parse({
503
- autocloseCycleMs: options?.autocloseCycleMs,
522
+ autocloseCycleMinutes: options?.autocloseCycleMinutes,
504
523
  closeBatchSize: options?.closeBatchSize,
505
524
  closeYieldMs: options?.closeYieldMs,
506
- closeOnError: options?.closeOnError
525
+ closeOnError: options?.closeOnError,
526
+ autocloseWindow: options?.autocloseWindow
507
527
  });
508
528
  }
529
+ function hour_in_zone(now, timeZone) {
530
+ const parts = new Intl.DateTimeFormat("en-US", {
531
+ timeZone,
532
+ hour: "2-digit",
533
+ hourCycle: "h23"
534
+ }).formatToParts(now);
535
+ return Number(parts.find((p) => p.type === "hour")?.value);
536
+ }
537
+ function in_autoclose_window(window, now) {
538
+ if (!window) return true;
539
+ const hour = hour_in_zone(now, window.timeZone);
540
+ return window.start < window.end ? hour >= window.start && hour < window.end : hour >= window.start || hour < window.end;
541
+ }
509
542
 
510
543
  // src/internal/close-cycle.ts
544
+ var SAFETY_PROBE_PAGE_SIZE = 1e3;
511
545
  async function run_close_cycle(targets, deps) {
512
546
  const target_map = new Map(targets.map((t) => [t.stream, t]));
513
547
  const streams = [...target_map.keys()];
@@ -516,7 +550,8 @@ async function run_close_cycle(targets, deps) {
516
550
  const safe = await partition_by_safety(
517
551
  stream_info,
518
552
  deps.reactive_events_size,
519
- skipped
553
+ skipped,
554
+ deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
520
555
  );
521
556
  if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
522
557
  const { guarded, guard_events } = await guard_with_tombstones(
@@ -559,7 +594,7 @@ async function scan_stream_heads(streams) {
559
594
  }
560
595
  return out;
561
596
  }
562
- async function partition_by_safety(stream_info, reactive_events_size, skipped) {
597
+ async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size) {
563
598
  if (reactive_events_size === 0) return [...stream_info.keys()];
564
599
  const pending_set = /* @__PURE__ */ new Set();
565
600
  const source_regex = /* @__PURE__ */ new Map();
@@ -571,14 +606,25 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped) {
571
606
  }
572
607
  return re;
573
608
  };
574
- await store().query_streams((position) => {
575
- const source_re = position.source ? get_regex(position.source) : void 0;
576
- for (const [stream, info] of stream_info) {
577
- if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
578
- pending_set.add(stream);
579
- }
580
- }
581
- });
609
+ const targets = [...stream_info.keys()];
610
+ let after;
611
+ for (; ; ) {
612
+ let last;
613
+ const { count } = await store().query_streams(
614
+ (position) => {
615
+ last = position.stream;
616
+ const source_re = position.source ? get_regex(position.source) : void 0;
617
+ for (const [stream, info] of stream_info) {
618
+ if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
619
+ pending_set.add(stream);
620
+ }
621
+ }
622
+ },
623
+ { after, limit: page_size, source_matches: targets }
624
+ );
625
+ if (count < page_size) break;
626
+ after = last;
627
+ }
582
628
  const safe = [];
583
629
  for (const [stream] of stream_info) {
584
630
  if (pending_set.has(stream)) skipped.push(stream);
@@ -699,54 +745,54 @@ async function run_autoclose_cycle(deps) {
699
745
  let predicate_errors = 0;
700
746
  const truncated = /* @__PURE__ */ new Map();
701
747
  const skipped = [];
702
- const stats = await s.query_stats(
703
- {},
704
- { count: true, exclude: [TOMBSTONE_EVENT] }
705
- );
706
- const flush_batch = async (candidates2) => {
707
- if (candidates2.length === 0) return;
708
- const result = await run_close_cycle(candidates2, {
709
- reactive_events_size: deps.reactive_events_size,
710
- event_to_state: deps.event_to_state,
711
- load: deps.load,
712
- tombstone: deps.tombstone,
713
- logger: deps.logger,
714
- correlation: deps.correlation
715
- });
716
- for (const [stream, entry] of result.truncated) {
717
- truncated.set(stream, entry);
718
- }
719
- for (const stream of result.skipped) skipped.push(stream);
720
- await yield_between_batches(deps.config.closeYieldMs);
721
- };
722
- let candidates = [];
723
- for (const [stream, { head, count }] of stats) {
724
- inspected += 1;
725
- const owner = deps.event_to_state.get(head.name);
726
- if (!owner) continue;
727
- const predicate = deps.autoclose_policy(owner.name);
728
- if (!predicate) continue;
729
- let close = false;
730
- try {
731
- close = predicate(stream, head, count ?? 0);
732
- evaluated += 1;
733
- } catch (err) {
734
- predicate_errors += 1;
735
- deps.logger.error(
736
- `Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
737
- );
738
- close = deps.config.closeOnError;
748
+ const page = deps.config.closeBatchSize;
749
+ let after;
750
+ for (; ; ) {
751
+ const stats = await s.query_stats(
752
+ {},
753
+ { count: true, exclude: [TOMBSTONE_EVENT], after, limit: page }
754
+ );
755
+ if (stats.size === 0) break;
756
+ inspected += stats.size;
757
+ const candidates = [];
758
+ for (const [stream, { head, count }] of stats) {
759
+ const owner = deps.event_to_state.get(head.name);
760
+ if (!owner) continue;
761
+ const predicate = deps.autoclose_policy(owner.name);
762
+ if (!predicate) continue;
763
+ let close = false;
764
+ try {
765
+ close = predicate(stream, head, count ?? 0);
766
+ evaluated += 1;
767
+ } catch (err) {
768
+ predicate_errors += 1;
769
+ deps.logger.error(
770
+ `Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
771
+ );
772
+ close = deps.config.closeOnError;
773
+ }
774
+ if (!close) continue;
775
+ const archiver = deps.autoclose_archiver(owner.name);
776
+ const archive = archiver ? () => archiver(stream, head) : void 0;
777
+ candidates.push({ stream, archive });
739
778
  }
740
- if (!close) continue;
741
- const archiver = deps.autoclose_archiver(owner.name);
742
- const archive = archiver ? () => archiver(stream, head) : void 0;
743
- candidates.push({ stream, archive });
744
- if (candidates.length >= deps.config.closeBatchSize) {
745
- await flush_batch(candidates);
746
- candidates = [];
779
+ if (candidates.length > 0) {
780
+ const result = await run_close_cycle(candidates, {
781
+ reactive_events_size: deps.reactive_events_size,
782
+ event_to_state: deps.event_to_state,
783
+ load: deps.load,
784
+ tombstone: deps.tombstone,
785
+ logger: deps.logger,
786
+ correlation: deps.correlation
787
+ });
788
+ for (const [stream, entry] of result.truncated)
789
+ truncated.set(stream, entry);
790
+ for (const stream of result.skipped) skipped.push(stream);
791
+ await yield_between_batches(deps.config.closeYieldMs);
747
792
  }
793
+ if (stats.size < page) break;
794
+ after = [...stats.keys()].at(-1);
748
795
  }
749
- await flush_batch(candidates);
750
796
  return {
751
797
  inspected,
752
798
  evaluated,
@@ -776,7 +822,7 @@ var AutocloseController = class {
776
822
  this._timer = setInterval(() => {
777
823
  this.run_once().catch(() => {
778
824
  });
779
- }, this.deps.config.autocloseCycleMs);
825
+ }, this.deps.config.autocloseCycleMinutes * 6e4);
780
826
  this._timer.unref();
781
827
  return true;
782
828
  }
@@ -808,6 +854,8 @@ var AutocloseController = class {
808
854
  async run_once() {
809
855
  if (this._running) return null;
810
856
  if (this.deps.breaker.state(Date.now()) === "open") return null;
857
+ if (!in_autoclose_window(this.deps.config.autocloseWindow, /* @__PURE__ */ new Date()))
858
+ return null;
811
859
  this._running = true;
812
860
  try {
813
861
  const correlation = close_correlation(
@@ -4082,7 +4130,7 @@ export {
4082
4130
  ConcurrencyError,
4083
4131
  ConsoleLogger,
4084
4132
  CsvFile,
4085
- DEFAULT_AUTOCLOSE_CYCLE_MS,
4133
+ DEFAULT_AUTOCLOSE_CYCLE_MINUTES,
4086
4134
  DEFAULT_CLOSE_BATCH_SIZE,
4087
4135
  DEFAULT_CLOSE_YIELD_MS,
4088
4136
  DEFAULT_LANE,