@rotorsoft/act 1.32.30 → 1.33.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 (123) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/act.d.ts +41 -233
  4. package/dist/@types/act.d.ts.map +1 -1
  5. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  6. package/dist/@types/builders/act-builder.d.ts +2 -115
  7. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  8. package/dist/@types/builders/builder-utils.d.ts +1 -1
  9. package/dist/@types/builders/builder-utils.d.ts.map +1 -1
  10. package/dist/@types/builders/event-builder.d.ts +5 -5
  11. package/dist/@types/builders/event-builder.d.ts.map +1 -1
  12. package/dist/@types/builders/merge.d.ts +1 -1
  13. package/dist/@types/builders/merge.d.ts.map +1 -1
  14. package/dist/@types/builders/projection-builder.d.ts +2 -144
  15. package/dist/@types/builders/projection-builder.d.ts.map +1 -1
  16. package/dist/@types/builders/slice-builder.d.ts +2 -71
  17. package/dist/@types/builders/slice-builder.d.ts.map +1 -1
  18. package/dist/@types/builders/state-builder.d.ts +2 -376
  19. package/dist/@types/builders/state-builder.d.ts.map +1 -1
  20. package/dist/@types/config.d.ts +6 -43
  21. package/dist/@types/config.d.ts.map +1 -1
  22. package/dist/@types/csv.d.ts +1 -15
  23. package/dist/@types/csv.d.ts.map +1 -1
  24. package/dist/@types/index.d.ts +0 -1
  25. package/dist/@types/index.d.ts.map +1 -1
  26. package/dist/@types/internal/autoclose-policy.d.ts +1 -59
  27. package/dist/@types/internal/autoclose-policy.d.ts.map +1 -1
  28. package/dist/@types/internal/autoclose-reaction.d.ts +1 -2
  29. package/dist/@types/internal/autoclose-reaction.d.ts.map +1 -1
  30. package/dist/@types/internal/autoclose-window.d.ts +1 -1
  31. package/dist/@types/internal/autoclose-window.d.ts.map +1 -1
  32. package/dist/@types/internal/circuit-breaker.d.ts +1 -2
  33. package/dist/@types/internal/circuit-breaker.d.ts.map +1 -1
  34. package/dist/@types/{builders/build-classify.d.ts → internal/classify.d.ts} +2 -15
  35. package/dist/@types/internal/classify.d.ts.map +1 -0
  36. package/dist/@types/internal/close-cycle.d.ts.map +1 -1
  37. package/dist/@types/internal/config.d.ts +2 -42
  38. package/dist/@types/internal/config.d.ts.map +1 -1
  39. package/dist/@types/internal/correlate-cycle.d.ts +1 -1
  40. package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
  41. package/dist/@types/internal/{correlator.d.ts → correlation-id.d.ts} +1 -1
  42. package/dist/@types/internal/correlation-id.d.ts.map +1 -0
  43. package/dist/@types/internal/defer-config.d.ts.map +1 -1
  44. package/dist/@types/internal/drain-cycle.d.ts +1 -1
  45. package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
  46. package/dist/@types/internal/{drain.d.ts → drain-ops.d.ts} +1 -1
  47. package/dist/@types/internal/drain-ops.d.ts.map +1 -0
  48. package/dist/@types/internal/event-sourcing.d.ts +1 -8
  49. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  50. package/dist/@types/internal/{sensitive.d.ts → pii.d.ts} +4 -71
  51. package/dist/@types/internal/pii.d.ts.map +1 -0
  52. package/dist/@types/internal/projection-fold.d.ts +1 -2
  53. package/dist/@types/internal/projection-fold.d.ts.map +1 -1
  54. package/dist/@types/internal/{close-signal.d.ts → reaction-dispatch.d.ts} +47 -2
  55. package/dist/@types/internal/reaction-dispatch.d.ts.map +1 -0
  56. package/dist/@types/internal/report-once.d.ts +1 -1
  57. package/dist/@types/internal/settle.d.ts +21 -12
  58. package/dist/@types/internal/settle.d.ts.map +1 -1
  59. package/dist/@types/internal/tracing.d.ts +3 -3
  60. package/dist/@types/internal/tracing.d.ts.map +1 -1
  61. package/dist/@types/ports.d.ts +3 -43
  62. package/dist/@types/ports.d.ts.map +1 -1
  63. package/dist/@types/scoped.d.ts +1 -6
  64. package/dist/@types/scoped.d.ts.map +1 -1
  65. package/dist/@types/sensitive.d.ts +56 -0
  66. package/dist/@types/sensitive.d.ts.map +1 -0
  67. package/dist/@types/test/index.d.ts +1 -0
  68. package/dist/@types/test/index.d.ts.map +1 -1
  69. package/dist/@types/test/sandbox.d.ts +2 -26
  70. package/dist/@types/test/sandbox.d.ts.map +1 -1
  71. package/dist/@types/types/act.d.ts +216 -0
  72. package/dist/@types/types/act.d.ts.map +1 -0
  73. package/dist/@types/types/action.d.ts +67 -0
  74. package/dist/@types/types/action.d.ts.map +1 -1
  75. package/dist/@types/types/builders.d.ts +722 -0
  76. package/dist/@types/types/builders.d.ts.map +1 -0
  77. package/dist/@types/types/errors.d.ts +31 -1
  78. package/dist/@types/types/errors.d.ts.map +1 -1
  79. package/dist/@types/types/index.d.ts +38 -0
  80. package/dist/@types/types/index.d.ts.map +1 -1
  81. package/dist/@types/types/ports.d.ts +15 -9
  82. package/dist/@types/types/ports.d.ts.map +1 -1
  83. package/dist/@types/types/reaction.d.ts +11 -1
  84. package/dist/@types/types/reaction.d.ts.map +1 -1
  85. package/dist/@types/types/sandbox.d.ts +31 -0
  86. package/dist/@types/types/sandbox.d.ts.map +1 -0
  87. package/dist/@types/types/schemas.d.ts +12 -17
  88. package/dist/@types/types/schemas.d.ts.map +1 -1
  89. package/dist/{chunk-HHKLVRR4.js → chunk-2PMUONGF.js} +38 -57
  90. package/dist/chunk-2PMUONGF.js.map +1 -0
  91. package/dist/chunk-HF433MAF.cjs +264 -0
  92. package/dist/chunk-HF433MAF.cjs.map +1 -0
  93. package/dist/{chunk-X3MMQIIP.js → chunk-JABYJGT7.js} +174 -22
  94. package/dist/chunk-JABYJGT7.js.map +1 -0
  95. package/dist/chunk-VE3MTY4A.cjs +1233 -0
  96. package/dist/chunk-VE3MTY4A.cjs.map +1 -0
  97. package/dist/index.cjs +1645 -3041
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.js +751 -813
  100. package/dist/index.js.map +1 -1
  101. package/dist/test/index.cjs +16 -1113
  102. package/dist/test/index.cjs.map +1 -1
  103. package/dist/test/index.js +2 -2
  104. package/dist/test/index.js.map +1 -1
  105. package/dist/types/index.cjs +58 -276
  106. package/dist/types/index.cjs.map +1 -1
  107. package/dist/types/index.js +15 -1
  108. package/package.json +1 -1
  109. package/dist/@types/builders/build-classify.d.ts.map +0 -1
  110. package/dist/@types/builders/builder-base.d.ts +0 -70
  111. package/dist/@types/builders/builder-base.d.ts.map +0 -1
  112. package/dist/@types/builders/reaction-builder.d.ts +0 -55
  113. package/dist/@types/builders/reaction-builder.d.ts.map +0 -1
  114. package/dist/@types/internal/close-signal.d.ts.map +0 -1
  115. package/dist/@types/internal/correlator.d.ts.map +0 -1
  116. package/dist/@types/internal/defer-signal.d.ts +0 -32
  117. package/dist/@types/internal/defer-signal.d.ts.map +0 -1
  118. package/dist/@types/internal/drain.d.ts.map +0 -1
  119. package/dist/@types/internal/index.d.ts +0 -38
  120. package/dist/@types/internal/index.d.ts.map +0 -1
  121. package/dist/@types/internal/sensitive.d.ts.map +0 -1
  122. package/dist/chunk-HHKLVRR4.js.map +0 -1
  123. package/dist/chunk-X3MMQIIP.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  ConsoleLogger,
3
- DEFAULT_LANE,
4
- ExitCodes,
3
+ DEFAULT_BACKOFF,
4
+ DEFAULT_BLOCK_ON_ERROR,
5
+ DEFAULT_EVENT_LIMIT,
6
+ DEFAULT_LEASE_MILLIS,
7
+ DEFAULT_MAX_RETRIES,
8
+ DEFAULT_STREAM_LIMIT,
5
9
  InMemoryCache,
6
10
  InMemoryStore,
7
11
  LruMap,
12
+ MAX_SHUTDOWN_GRACE_MS,
8
13
  PackageSchema,
9
- SNAP_EVENT,
10
- TOMBSTONE_EVENT,
11
14
  cache,
12
15
  config,
13
16
  current_autoclose_window,
@@ -23,37 +26,51 @@ import {
23
26
  make_run_scoped,
24
27
  port,
25
28
  register_weak_disposer,
29
+ resolveActConfig,
30
+ resolveActionConfig,
31
+ resolveAutocloseConfig,
32
+ resolveAutocloseConfig2,
33
+ resolveCircuitBreakerConfig,
34
+ resolveDrainConfig,
35
+ resolveFoldConfig,
36
+ resolveLaneConfig,
37
+ resolveReactionConfig,
38
+ resolveSettleConfig,
39
+ resolveShutdownConfig,
26
40
  sleep,
27
41
  store,
28
42
  validate
29
- } from "./chunk-X3MMQIIP.js";
43
+ } from "./chunk-JABYJGT7.js";
30
44
  import {
31
45
  ActorSchema,
32
46
  CausationEventSchema,
33
47
  CommittedMetaSchema,
34
48
  ConcurrencyError,
49
+ DEFAULT_LANE,
50
+ DEFAULT_MAX_SUBSCRIBED_STREAMS,
51
+ DEFAULT_SETTLE_DEBOUNCE_MS,
52
+ DeferSignal,
35
53
  Environments,
36
54
  Errors,
37
55
  EventMetaSchema,
38
- IDENTITY_GATE,
56
+ ExitCodes,
39
57
  InvariantError,
40
58
  LogLevels,
41
59
  NonRetryableError,
42
60
  QuerySchema,
43
61
  REDACTED,
44
62
  SHREDDED,
63
+ SNAP_EVENT,
45
64
  StoreError,
46
65
  StreamClosedError,
66
+ TOMBSTONE_EVENT,
47
67
  TargetSchema,
48
68
  ValidationError,
49
69
  ZodEmpty,
50
- make_gate,
51
70
  pii_fields,
52
71
  pii_schemas,
53
- pii_split,
54
- pii_strip,
55
72
  sensitive
56
- } from "./chunk-HHKLVRR4.js";
73
+ } from "./chunk-2PMUONGF.js";
57
74
 
58
75
  // src/signals.ts
59
76
  process.once("SIGINT", async (arg) => {
@@ -76,55 +93,6 @@ process.once("unhandledRejection", async (arg) => {
76
93
  // src/act.ts
77
94
  import EventEmitter from "events";
78
95
 
79
- // src/builders/build-classify.ts
80
- var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
81
- function classify_registry(registry, states) {
82
- const statics = /* @__PURE__ */ new Map();
83
- const target_lanes = /* @__PURE__ */ new Map();
84
- const event_to_lanes = /* @__PURE__ */ new Map();
85
- for (const [name, register] of Object.entries(registry.events)) {
86
- for (const reaction of register.reactions.values()) {
87
- if (typeof reaction.resolver === "function") {
88
- event_to_lanes.set(name, ALL_LANES);
89
- } else {
90
- const { target, source, priority = 0, lane } = reaction.resolver;
91
- const lane_name = lane ?? "default";
92
- const existing_lanes = event_to_lanes.get(name);
93
- if (existing_lanes !== ALL_LANES) {
94
- const set = existing_lanes ?? /* @__PURE__ */ new Set();
95
- set.add(lane_name);
96
- event_to_lanes.set(name, set);
97
- }
98
- const recorded_lane = target_lanes.get(target);
99
- if (recorded_lane === void 0) {
100
- target_lanes.set(target, lane_name);
101
- } else if (recorded_lane !== lane_name) {
102
- throw new Error(
103
- `Stream "${target}" has conflicting lane assignments ("${recorded_lane}" vs "${lane_name}")`
104
- );
105
- }
106
- const existing = statics.get(target);
107
- if (!existing) {
108
- statics.set(target, { stream: target, source, priority, lane });
109
- } else if (priority > existing.priority) {
110
- statics.set(target, { ...existing, priority });
111
- }
112
- }
113
- }
114
- }
115
- const event_to_state = /* @__PURE__ */ new Map();
116
- for (const merged of states.values()) {
117
- for (const event_name of Object.keys(merged.events)) {
118
- event_to_state.set(event_name, merged);
119
- }
120
- }
121
- return {
122
- static_targets: [...statics.values()],
123
- event_to_state,
124
- event_to_lanes
125
- };
126
- }
127
-
128
96
  // src/internal/event-versions.ts
129
97
  var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
130
98
  function parse(name) {
@@ -683,433 +651,6 @@ var PASS_FACTORIES = {
683
651
  "unreadable-events": make_unreadable_events_pass
684
652
  };
685
653
 
686
- // src/internal/autoclose-policy.ts
687
- import { z } from "zod";
688
- var DAY_MS = 864e5;
689
- var MIN_AFTER_DAYS = 1 / 1440;
690
- var MIN_KEEP_DAYS = 1;
691
- function days_after(date, days) {
692
- return new Date(date.getTime() + days * DAY_MS);
693
- }
694
- function days_before_now(days) {
695
- return new Date(Date.now() - days * DAY_MS);
696
- }
697
- var window_schema = (field, min_days, floor) => z.object({
698
- days: z.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
699
- }).refine((o) => o.days >= min_days, {
700
- message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
701
- });
702
- var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
703
- var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
704
- var IsSchema = z.union([
705
- z.string().min(1, "autocloses: is must be a non-empty event name"),
706
- z.array(
707
- z.string().min(1, "autocloses: is entries must be non-empty event names")
708
- ).min(1, "autocloses: is must include at least one event name").readonly()
709
- ]);
710
- var ReachesSchema = z.number({ message: "autocloses: reaches must be a number" }).int("autocloses: reaches must be an integer").min(1, "autocloses: reaches must be >= 1");
711
- var OrBlockSchema = z.object({
712
- after: AfterSchema.optional(),
713
- is: IsSchema.optional(),
714
- reaches: ReachesSchema.optional()
715
- }).strict().refine(
716
- (o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
717
- {
718
- message: "autocloses: `or` block must include at least one of after / is / reaches"
719
- }
720
- );
721
- var AutoclosePolicySchema = z.object({
722
- after: AfterSchema.optional(),
723
- is: IsSchema.optional(),
724
- reaches: ReachesSchema.optional(),
725
- or: OrBlockSchema.optional(),
726
- keep: KeepSchema.optional()
727
- }).strict().refine(
728
- (o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
729
- {
730
- message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
731
- }
732
- );
733
- function compile_after(after) {
734
- return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
735
- }
736
- function compile_is(is) {
737
- const set = new Set(typeof is === "string" ? [is] : is);
738
- return (_stream, head) => set.has(head.name);
739
- }
740
- function compile_reaches(reaches) {
741
- return (_stream, _head, count) => count >= reaches;
742
- }
743
- function policy_min_after_days(options) {
744
- const parsed = AutoclosePolicySchema.parse(options);
745
- const windows = [];
746
- if (parsed.after) windows.push(parsed.after.days);
747
- if (parsed.or?.after) windows.push(parsed.or.after.days);
748
- return windows.length ? Math.min(...windows) : void 0;
749
- }
750
- function policy_keep_days(options) {
751
- const parsed = AutoclosePolicySchema.parse(options);
752
- return parsed.keep?.days;
753
- }
754
- function compile_autoclose_policy(options) {
755
- const parsed = AutoclosePolicySchema.parse(options);
756
- const and_preds = [];
757
- if (parsed.after) and_preds.push(compile_after(parsed.after));
758
- if (parsed.is) and_preds.push(compile_is(parsed.is));
759
- if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
760
- const or_preds = [];
761
- if (parsed.or) {
762
- if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
763
- if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
764
- if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
765
- }
766
- return (stream, head, count) => {
767
- if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
768
- return true;
769
- }
770
- if (or_preds.some((p) => p(stream, head, count))) {
771
- return true;
772
- }
773
- return false;
774
- };
775
- }
776
-
777
- // src/internal/autoclose-window.ts
778
- function hour_in_zone(now, timeZone) {
779
- const parts = new Intl.DateTimeFormat("en-US", {
780
- timeZone,
781
- hour: "2-digit",
782
- hourCycle: "h23"
783
- }).formatToParts(now);
784
- return Number(parts.find((p) => p.type === "hour")?.value);
785
- }
786
- function hour_in_range(hour, start, end) {
787
- return start < end ? hour >= start && hour < end : hour >= start || hour < end;
788
- }
789
- function is_dst_gap_open(now, timeZone, start) {
790
- const hour = hour_in_zone(now, timeZone);
791
- if (hour <= start) return false;
792
- const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
793
- return prev === start - 1 && hour === start + 1;
794
- }
795
- function in_autoclose_window(window, now) {
796
- if (!window) return true;
797
- const hour = hour_in_zone(now, window.timeZone);
798
- return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
799
- }
800
- function next_window_open(window, now) {
801
- const start_ms = now.getTime();
802
- for (let h = 0; h <= 48; h++) {
803
- const candidate = new Date(start_ms + h * 36e5);
804
- if (hour_in_zone(candidate, window.timeZone) === window.start)
805
- return candidate;
806
- const boundary = new Date(
807
- candidate.getTime() - candidate.getTime() % 36e5
808
- );
809
- if (boundary.getTime() >= start_ms && is_dst_gap_open(boundary, window.timeZone, window.start))
810
- return boundary;
811
- }
812
- return new Date(start_ms + 864e5);
813
- }
814
-
815
- // src/internal/close-signal.ts
816
- var CloseSignal = class extends Error {
817
- /**
818
- * Stream to close. Omitted → the reaction's own lease stream (a self-close).
819
- * The synthesized autoclose reaction sets it to the aggregate stream, since
820
- * its lease runs on a synthetic `__autoclose__` target.
821
- */
822
- stream;
823
- /** Optional archive callback to run while the stream is guarded. */
824
- archive;
825
- /**
826
- * Watermark to ack the requesting reaction to before the close runs. The
827
- * close-cycle safety guard skips a stream whose subscriptions (matched by
828
- * source) lag the head; the autoclose reaction's `source` is the aggregate,
829
- * so it must advance its own watermark to the live head id it evaluated
830
- * against — otherwise it blocks its own close. Defaults to the triggering
831
- * event id when omitted.
832
- */
833
- at;
834
- /**
835
- * Windowed close: prune events older than this cutoff behind
836
- * the closest safe snapshot instead of retiring the stream. Thrown by
837
- * the autoclose reaction of a `.autocloses({ keep })` state; flows
838
- * into {@link CloseTarget.before}. Omitted → a full close.
839
- */
840
- before;
841
- constructor(opts) {
842
- super("reaction requested close");
843
- this.name = "CloseSignal";
844
- this.stream = opts?.stream;
845
- this.archive = opts?.archive;
846
- this.at = opts?.at;
847
- this.before = opts?.before;
848
- }
849
- };
850
-
851
- // src/internal/config.ts
852
- import { z as z2 } from "zod";
853
- var BackoffOptionsSchema = z2.object({
854
- strategy: z2.enum(["fixed", "linear", "exponential"]),
855
- baseMs: z2.number().min(0),
856
- maxMs: z2.number().gt(0).optional(),
857
- jitter: z2.boolean().optional()
858
- });
859
- var DEFAULT_BACKOFF = {
860
- strategy: "fixed",
861
- baseMs: 1e4
862
- };
863
- var DEFAULT_MAX_RETRIES = 3;
864
- var DEFAULT_BLOCK_ON_ERROR = true;
865
- var ReactionOptionsSchema = z2.object({
866
- blockOnError: z2.boolean(),
867
- maxRetries: z2.number().int().min(0),
868
- backoff: BackoffOptionsSchema.optional()
869
- });
870
- function resolveReactionConfig(options) {
871
- return ReactionOptionsSchema.parse(options);
872
- }
873
- var ActionOptionsSchema = z2.object({
874
- maxRetries: z2.number().int().min(0).optional(),
875
- backoff: BackoffOptionsSchema.optional()
876
- });
877
- function resolveActionConfig(options) {
878
- return ActionOptionsSchema.parse(options);
879
- }
880
- var LaneConfigSchema = z2.object({
881
- name: z2.string().min(1),
882
- leaseMillis: z2.number().min(0).optional(),
883
- streamLimit: z2.number().int().min(0).optional(),
884
- cycleMs: z2.number().min(0).optional()
885
- });
886
- function resolveLaneConfig(options) {
887
- LaneConfigSchema.parse(options);
888
- return options;
889
- }
890
- var DEFAULT_STREAM_LIMIT = 10;
891
- var DEFAULT_EVENT_LIMIT = 10;
892
- var DEFAULT_LEASE_MILLIS = 1e4;
893
- var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
894
- var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
895
- var DrainOptionsSchema = z2.object({
896
- streamLimit: z2.number().int().min(0).optional(),
897
- eventLimit: z2.number().int().min(0).optional(),
898
- leaseMillis: z2.number().min(0).optional()
899
- });
900
- function resolveDrainConfig(options) {
901
- if (options === void 0) return void 0;
902
- DrainOptionsSchema.parse(options);
903
- return options;
904
- }
905
- var SettleOptionsSchema = DrainOptionsSchema.extend({
906
- debounceMs: z2.number().min(0).optional(),
907
- // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
908
- // a non-negative integer — `0` is legal and means "run no passes".
909
- //
910
- // `Infinity` is accepted explicitly. It is the documented default
911
- // (`Act.settle`'s doc-comment and `SettleOptions.maxPasses` both name it),
912
- // and `settle.ts` destructures `maxPasses = Infinity`, so the one value the
913
- // docs advertise was the one value `z.number().int()` refused — Zod 4
914
- // rejects `±Infinity`, and an integer check cannot pass it either. Wrapper
915
- // code normalizing an optional cap (`maxPasses: opts.maxPasses ?? Infinity`)
916
- // is the natural way to hit it. Accepting it also honors this module's own
917
- // rule that a value which works today is never newly rejected.
918
- maxPasses: z2.union([z2.number().int().min(0), z2.literal(Number.POSITIVE_INFINITY)]).optional()
919
- }).loose();
920
- function resolveSettleConfig(options) {
921
- if (options === void 0) return void 0;
922
- SettleOptionsSchema.parse(options);
923
- return options;
924
- }
925
- var MAX_SHUTDOWN_GRACE_MS = 3e4;
926
- var ShutdownOptionsSchema = z2.object({
927
- // `0` is legal and is exactly today's behavior: stop scheduling and
928
- // return without waiting.
929
- graceMs: z2.number().min(0).optional()
930
- }).loose();
931
- function resolveShutdownConfig(options) {
932
- if (options === void 0) return void 0;
933
- ShutdownOptionsSchema.parse(options);
934
- return options;
935
- }
936
- var ActOptionsSchema = z2.object({
937
- maxSubscribedStreams: z2.number().int().min(1).optional(),
938
- settleDebounceMs: z2.number().int().min(0).optional()
939
- }).loose();
940
- function resolveActConfig(options) {
941
- if (options === void 0) return void 0;
942
- ActOptionsSchema.parse(options);
943
- return options;
944
- }
945
- var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
946
- var DEFAULT_CLOSE_BATCH_SIZE = 64;
947
- var DEFAULT_CLOSE_YIELD_MS = 0;
948
- var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
949
- function is_valid_time_zone(tz) {
950
- try {
951
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
952
- return true;
953
- } catch {
954
- return false;
955
- }
956
- }
957
- var AutocloseWindowSchema = z2.object({
958
- start: z2.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
959
- end: z2.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
960
- timeZone: z2.string().refine(is_valid_time_zone, {
961
- message: "autocloseWindow.timeZone must be a valid IANA time zone"
962
- }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
963
- }).refine((w) => w.start !== w.end, {
964
- message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
965
- });
966
- var AutocloseOptionsSchema = z2.object({
967
- autocloseCycleMinutes: z2.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
968
- closeBatchSize: z2.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
969
- closeYieldMs: z2.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
970
- closeOnError: z2.boolean().default(false),
971
- autocloseWindow: AutocloseWindowSchema.optional()
972
- });
973
- function resolveAutocloseConfig(options) {
974
- return AutocloseOptionsSchema.parse({
975
- autocloseCycleMinutes: options?.autocloseCycleMinutes,
976
- closeBatchSize: options?.closeBatchSize,
977
- closeYieldMs: options?.closeYieldMs,
978
- closeOnError: options?.closeOnError,
979
- autocloseWindow: options?.autocloseWindow
980
- });
981
- }
982
- var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
983
- var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
984
- var CircuitBreakerOptionsSchema = z2.object({
985
- failureThreshold: z2.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
986
- cooldownMs: z2.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
987
- });
988
- var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
989
- var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
990
- var DEFAULT_MAX_CACHED_STATES = 1e4;
991
- var FoldOptionsSchema = z2.object({
992
- flushEvery: z2.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
993
- maxCachedStates: z2.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
994
- });
995
- function resolveFoldConfig(options) {
996
- return FoldOptionsSchema.parse(options);
997
- }
998
-
999
- // src/internal/defer-signal.ts
1000
- var DeferSignal = class extends Error {
1001
- /**
1002
- * The unresolved schedule. The dispatcher turns this into an absolute
1003
- * due-time by resolving it against the triggering event.
1004
- */
1005
- when;
1006
- constructor(when) {
1007
- super("reaction deferred");
1008
- this.name = "DeferSignal";
1009
- this.when = when;
1010
- }
1011
- };
1012
-
1013
- // src/internal/autoclose-reaction.ts
1014
- var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
1015
- function synthesize_autoclose_reactions(registry, states, read_window) {
1016
- for (const st of states.values()) {
1017
- const predicate = st.autoclose;
1018
- if (!predicate) continue;
1019
- const after_days = st.autoclose_after_days;
1020
- const keep_days = st.autoclose_keep_days;
1021
- const archiver = st.archive;
1022
- const reaction = {
1023
- // Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
1024
- // per-aggregate `__autoclose__` key — so the autoclose reaction never
1025
- // shares a watermark with the aggregate's own reactions. A shared
1026
- // watermark would let autoclose's defer short-circuit the aggregate's
1027
- // other reactions (the "a defer affects all reactions on a stream"
1028
- // hazard). The close still targets the aggregate (`source`).
1029
- resolver: (e) => ({
1030
- target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
1031
- source: e.stream
1032
- }),
1033
- // Never block on autoclose: a transient query/store error should retry,
1034
- // not quarantine the synthetic stream.
1035
- options: { blockOnError: false, maxRetries: DEFAULT_MAX_RETRIES },
1036
- handler: async (event) => {
1037
- const aggregate = event.stream;
1038
- const window = read_window();
1039
- if (!in_autoclose_window(window, /* @__PURE__ */ new Date()))
1040
- throw new DeferSignal({ at: next_window_open(window, /* @__PURE__ */ new Date()) });
1041
- const stats = await store().query_stats([aggregate], {
1042
- count: true,
1043
- tail: keep_days !== void 0 ? true : void 0,
1044
- exclude: [TOMBSTONE_EVENT, SNAP_EVENT]
1045
- });
1046
- const entry = stats.get(aggregate);
1047
- if (!entry) return;
1048
- const head = entry.head;
1049
- if (predicate(aggregate, head, entry.count))
1050
- throw new CloseSignal({
1051
- stream: aggregate,
1052
- archive: archiver ? () => archiver(aggregate, head) : void 0,
1053
- // Ack this reaction's own watermark to the live head so the
1054
- // close-cycle guard (which matches subscriptions by source =
1055
- // aggregate) sees it caught up instead of blocking its own close.
1056
- at: head.id
1057
- });
1058
- if (keep_days !== void 0) {
1059
- const cutoff = days_before_now(keep_days);
1060
- if (entry.tail.created < cutoff)
1061
- throw new CloseSignal({
1062
- stream: aggregate,
1063
- before: cutoff,
1064
- archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
1065
- at: head.id
1066
- });
1067
- }
1068
- const dues = [];
1069
- if (after_days !== void 0)
1070
- dues.push(days_after(head.created, after_days));
1071
- if (keep_days !== void 0)
1072
- dues.push(days_after(entry.tail.created, keep_days));
1073
- const future = dues.map((d) => d.getTime()).filter((t) => t > Date.now());
1074
- if (future.length)
1075
- throw new DeferSignal({ at: new Date(Math.min(...future)) });
1076
- }
1077
- };
1078
- const key = `__autoclose_${st.name}`;
1079
- for (const event_name of Object.keys(st.events)) {
1080
- registry.events[event_name]?.reactions.set(
1081
- key,
1082
- reaction
1083
- );
1084
- }
1085
- }
1086
- }
1087
-
1088
- // src/internal/backoff.ts
1089
- function compute_backoff_delay(retry, opts) {
1090
- if (!opts || opts.baseMs <= 0) return 0;
1091
- const r = Math.max(0, retry);
1092
- let delay;
1093
- switch (opts.strategy) {
1094
- case "fixed":
1095
- delay = opts.baseMs;
1096
- break;
1097
- case "linear":
1098
- delay = opts.baseMs * (r + 1);
1099
- break;
1100
- case "exponential":
1101
- delay = opts.baseMs * 2 ** r;
1102
- if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
1103
- break;
1104
- default: {
1105
- const _never = opts.strategy;
1106
- throw new Error(`unknown backoff strategy: ${String(_never)}`);
1107
- }
1108
- }
1109
- if (opts.jitter) delay = delay * (0.5 + Math.random());
1110
- return Math.max(0, Math.floor(delay));
1111
- }
1112
-
1113
654
  // src/internal/circuit-breaker.ts
1114
655
  var CircuitBreaker = class {
1115
656
  _failures = 0;
@@ -1188,20 +729,69 @@ var CircuitBreaker = class {
1188
729
  this._wake = void 0;
1189
730
  }
1190
731
  }
1191
- /** Pure state transition for a failure — no side effects. */
1192
- _advance(now) {
1193
- if (this._opened_at !== void 0) {
1194
- this._opened_at = now;
1195
- return "open";
1196
- }
1197
- this._failures += 1;
1198
- if (this._failures >= this._threshold) {
1199
- this._opened_at = now;
1200
- return "open";
732
+ /** Pure state transition for a failure — no side effects. */
733
+ _advance(now) {
734
+ if (this._opened_at !== void 0) {
735
+ this._opened_at = now;
736
+ return "open";
737
+ }
738
+ this._failures += 1;
739
+ if (this._failures >= this._threshold) {
740
+ this._opened_at = now;
741
+ return "open";
742
+ }
743
+ return "closed";
744
+ }
745
+ };
746
+
747
+ // src/internal/classify.ts
748
+ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
749
+ function classify_registry(registry, states) {
750
+ const statics = /* @__PURE__ */ new Map();
751
+ const target_lanes = /* @__PURE__ */ new Map();
752
+ const event_to_lanes = /* @__PURE__ */ new Map();
753
+ for (const [name, register] of Object.entries(registry.events)) {
754
+ for (const reaction of register.reactions.values()) {
755
+ if (typeof reaction.resolver === "function") {
756
+ event_to_lanes.set(name, ALL_LANES);
757
+ } else {
758
+ const { target, source, priority = 0, lane } = reaction.resolver;
759
+ const lane_name = lane ?? "default";
760
+ const existing_lanes = event_to_lanes.get(name);
761
+ if (existing_lanes !== ALL_LANES) {
762
+ const set = existing_lanes ?? /* @__PURE__ */ new Set();
763
+ set.add(lane_name);
764
+ event_to_lanes.set(name, set);
765
+ }
766
+ const recorded_lane = target_lanes.get(target);
767
+ if (recorded_lane === void 0) {
768
+ target_lanes.set(target, lane_name);
769
+ } else if (recorded_lane !== lane_name) {
770
+ throw new Error(
771
+ `Stream "${target}" has conflicting lane assignments ("${recorded_lane}" vs "${lane_name}")`
772
+ );
773
+ }
774
+ const existing = statics.get(target);
775
+ if (!existing) {
776
+ statics.set(target, { stream: target, source, priority, lane });
777
+ } else if (priority > existing.priority) {
778
+ statics.set(target, { ...existing, priority });
779
+ }
780
+ }
781
+ }
782
+ }
783
+ const event_to_state = /* @__PURE__ */ new Map();
784
+ for (const merged of states.values()) {
785
+ for (const event_name of Object.keys(merged.events)) {
786
+ event_to_state.set(event_name, merged);
1201
787
  }
1202
- return "closed";
1203
788
  }
1204
- };
789
+ return {
790
+ static_targets: [...statics.values()],
791
+ event_to_state,
792
+ event_to_lanes
793
+ };
794
+ }
1205
795
 
1206
796
  // src/internal/close-cycle.ts
1207
797
  var CLOSE_CATCH_UP_LIMIT = 1e3;
@@ -1862,7 +1452,7 @@ var CorrelateCycle = class {
1862
1452
  }
1863
1453
  };
1864
1454
 
1865
- // src/internal/correlator.ts
1455
+ // src/internal/correlation-id.ts
1866
1456
  import { randomInt } from "crypto";
1867
1457
  var BASE = 36;
1868
1458
  var SEG_WIDTH = 4;
@@ -1886,111 +1476,34 @@ function close_correlation(correlator, actor) {
1886
1476
  });
1887
1477
  }
1888
1478
 
1889
- // src/internal/date-reviver.ts
1890
- import { z as z3 } from "zod";
1891
- var def_of = (schema) => schema._zod?.def ?? schema.def;
1892
- function variant_schema(schema) {
1893
- const shape = def_of(schema)?.shape;
1894
- if (!shape) {
1895
- const dates = date_reviver_schema(schema);
1896
- return { schema: dates ?? schema, dated: !!dates };
1897
- }
1898
- const next = {};
1899
- let dated = false;
1900
- for (const [key, field] of Object.entries(shape)) {
1901
- const dates = date_reviver_schema(field);
1902
- if (dates) dated = true;
1903
- next[key] = (dates ?? field).optional();
1904
- }
1905
- return { schema: z3.looseObject(next), dated };
1906
- }
1907
- function date_reviver_schema(schema) {
1908
- const def = def_of(schema);
1909
- if (!def) return void 0;
1910
- const inner = () => date_reviver_schema(def.innerType);
1911
- switch (def.type) {
1912
- case "date":
1913
- return z3.coerce.date();
1914
- case "object": {
1915
- const shape = def.shape;
1916
- if (!shape) return void 0;
1917
- const dated = {};
1918
- for (const [key, field] of Object.entries(shape)) {
1919
- const dates = date_reviver_schema(field);
1920
- if (dates) dated[key] = dates.optional();
1921
- }
1922
- return Object.keys(dated).length ? z3.looseObject(dated) : void 0;
1923
- }
1924
- case "array": {
1925
- const element = date_reviver_schema(def.element);
1926
- return element && z3.array(element);
1927
- }
1928
- case "tuple": {
1929
- const items = def.items.map((i) => date_reviver_schema(i));
1930
- return items.some(Boolean) ? z3.tuple(items.map((i) => i ?? z3.unknown())) : void 0;
1931
- }
1932
- case "record": {
1933
- const value = date_reviver_schema(def.valueType);
1934
- return value && z3.record(z3.string(), value);
1935
- }
1936
- case "union": {
1937
- const variants = def.options.map(variant_schema);
1938
- return variants.some((v) => v.dated) ? z3.union(variants.map((v) => v.schema)) : void 0;
1479
+ // src/internal/drain-cycle.ts
1480
+ import { randomUUID as randomUUID2 } from "crypto";
1481
+
1482
+ // src/internal/backoff.ts
1483
+ function compute_backoff_delay(retry, opts) {
1484
+ if (!opts || opts.baseMs <= 0) return 0;
1485
+ const r = Math.max(0, retry);
1486
+ let delay;
1487
+ switch (opts.strategy) {
1488
+ case "fixed":
1489
+ delay = opts.baseMs;
1490
+ break;
1491
+ case "linear":
1492
+ delay = opts.baseMs * (r + 1);
1493
+ break;
1494
+ case "exponential":
1495
+ delay = opts.baseMs * 2 ** r;
1496
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
1497
+ break;
1498
+ default: {
1499
+ const _never = opts.strategy;
1500
+ throw new Error(`unknown backoff strategy: ${String(_never)}`);
1939
1501
  }
1940
- case "nullable":
1941
- return inner()?.nullable();
1942
- case "optional":
1943
- case "nonoptional":
1944
- case "readonly":
1945
- case "default":
1946
- case "prefault":
1947
- case "catch":
1948
- return inner();
1949
- default:
1950
- return void 0;
1951
1502
  }
1503
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
1504
+ return Math.max(0, Math.floor(delay));
1952
1505
  }
1953
1506
 
1954
- // src/internal/defer-config.ts
1955
- import { z as z4 } from "zod";
1956
- var DeferDurationSchema = z4.object({
1957
- days: z4.number().positive().optional(),
1958
- hours: z4.number().positive().optional(),
1959
- minutes: z4.number().positive().optional()
1960
- }).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
1961
- message: "defer: a duration needs at least one of days, hours, or minutes"
1962
- });
1963
- var DeferWhenSchema = z4.object({
1964
- after: DeferDurationSchema.optional(),
1965
- at: z4.date().optional()
1966
- }).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
1967
- message: "defer: specify exactly one of `after` or `at`"
1968
- });
1969
- function duration_ms(d) {
1970
- return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
1971
- }
1972
- function resolve_defer_at(when, event) {
1973
- const parsed = DeferWhenSchema.parse(when);
1974
- if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
1975
- return parsed.at.getTime();
1976
- }
1977
- function assert_defer_when(when) {
1978
- DeferWhenSchema.parse(when);
1979
- }
1980
- function make_deferred(handler, schedule) {
1981
- const call = handler;
1982
- const deferred = async (event, stream, app) => {
1983
- const when = typeof schedule === "function" ? schedule(event) : schedule;
1984
- if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
1985
- return call(event, stream, app);
1986
- };
1987
- Object.defineProperty(deferred, "name", { value: handler.name });
1988
- return deferred;
1989
- }
1990
-
1991
- // src/internal/drain-cycle.ts
1992
- import { randomUUID as randomUUID2 } from "crypto";
1993
-
1994
1507
  // src/internal/defer-timer.ts
1995
1508
  var MAX_TIMER_DELAY_MS = 2147483647;
1996
1509
  var DeferTimer = class {
@@ -2084,7 +1597,7 @@ function compute_lag_lead_ratio(handled, lagging, leading) {
2084
1597
  return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
2085
1598
  }
2086
1599
 
2087
- // src/internal/drain.ts
1600
+ // src/internal/drain-ops.ts
2088
1601
  var claim = (lagging, leading, by, millis, lane) => store().claim(lagging, leading, by, millis, lane);
2089
1602
  async function fetch(leased, eventLimit) {
2090
1603
  return Promise.all(
@@ -2968,6 +2481,54 @@ var DrainController = class {
2968
2481
  }
2969
2482
  };
2970
2483
 
2484
+ // src/internal/pii.ts
2485
+ function pii_split(emitted, fields) {
2486
+ const data = { ...emitted.data };
2487
+ const pii = {};
2488
+ for (const f of fields) {
2489
+ if (f in data) {
2490
+ pii[f] = data[f];
2491
+ delete data[f];
2492
+ }
2493
+ }
2494
+ return { name: emitted.name, data, pii };
2495
+ }
2496
+ function pii_gate(event, fields, predicate, actor) {
2497
+ const data = event.data;
2498
+ const { pii, ...rest } = event;
2499
+ if (pii == null) {
2500
+ const shredded = { ...data };
2501
+ for (const f of fields) shredded[f] = SHREDDED;
2502
+ return { ...rest, data: shredded };
2503
+ }
2504
+ const allowed = !!actor && !!predicate && predicate(event, actor);
2505
+ if (allowed) {
2506
+ return {
2507
+ ...rest,
2508
+ data: { ...data, ...pii }
2509
+ };
2510
+ }
2511
+ const redacted = { ...data };
2512
+ for (const f of fields) redacted[f] = REDACTED;
2513
+ return { ...rest, data: redacted };
2514
+ }
2515
+ var IDENTITY_GATE = (event) => event;
2516
+ function make_gate(fields, predicate) {
2517
+ return (event, actor) => pii_gate(event, fields, predicate, actor);
2518
+ }
2519
+ function pii_strip(event, fields) {
2520
+ const data = event.data;
2521
+ const stripped = {};
2522
+ for (const k of Object.keys(data)) {
2523
+ if (!fields.includes(k)) stripped[k] = data[k];
2524
+ }
2525
+ const { pii: _drop_pii, ...rest } = event;
2526
+ return {
2527
+ ...rest,
2528
+ data: stripped
2529
+ };
2530
+ }
2531
+
2971
2532
  // src/internal/projection-fold.ts
2972
2533
  var FOLD_RESET = /* @__PURE__ */ Symbol("act.fold.reset");
2973
2534
  function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_fields) {
@@ -3055,108 +2616,78 @@ function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_
3055
2616
  return handler;
3056
2617
  }
3057
2618
 
3058
- // src/internal/settle.ts
3059
- var SettleLoop = class {
3060
- _timer = void 0;
3061
- _running = false;
3062
- /**
3063
- * Resolves when the cycle in flight finishes, so shutdown can wait for
3064
- * it (`stop()` cancels scheduling only). Never rejects.
3065
- */
3066
- _inflight;
3067
- /**
3068
- * A wake-up that fired while a cycle was running. The running cycle's
3069
- * `finally` schedules one more pass with these options, so it isn't lost.
3070
- */
3071
- _pending = void 0;
3072
- _deps;
3073
- /** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
3074
- _default_debounce_ms;
3075
- constructor(deps, default_debounce_ms) {
3076
- this._deps = deps;
3077
- this._default_debounce_ms = default_debounce_ms;
3078
- }
2619
+ // src/internal/defer-config.ts
2620
+ import { z } from "zod";
2621
+ var DeferDurationSchema = z.object({
2622
+ days: z.number().positive().optional(),
2623
+ hours: z.number().positive().optional(),
2624
+ minutes: z.number().positive().optional()
2625
+ }).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
2626
+ message: "defer: a duration needs at least one of days, hours, or minutes"
2627
+ });
2628
+ var DeferWhenSchema = z.object({
2629
+ after: DeferDurationSchema.optional(),
2630
+ at: z.date().optional()
2631
+ }).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
2632
+ message: "defer: specify exactly one of `after` or `at`"
2633
+ });
2634
+ function duration_ms(d) {
2635
+ return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
2636
+ }
2637
+ function resolve_defer_at(when, event) {
2638
+ const parsed = DeferWhenSchema.parse(when);
2639
+ if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
2640
+ return parsed.at.getTime();
2641
+ }
2642
+ function assert_defer_when(when) {
2643
+ DeferWhenSchema.parse(when);
2644
+ }
2645
+ function make_deferred(handler, schedule) {
2646
+ const call = handler;
2647
+ const deferred = async (event, stream, app) => {
2648
+ const when = typeof schedule === "function" ? schedule(event) : schedule;
2649
+ if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
2650
+ return call(event, stream, app);
2651
+ };
2652
+ Object.defineProperty(deferred, "name", { value: handler.name });
2653
+ return deferred;
2654
+ }
2655
+
2656
+ // src/internal/reaction-dispatch.ts
2657
+ var CloseSignal = class extends Error {
3079
2658
  /**
3080
- * Schedule a settle pass. Multiple calls inside the debounce window
3081
- * coalesce into one cycle. The cycle runs correlate→drain in a loop
3082
- * until no progress is made (no new subscriptions, no events read, no
3083
- * streams leased)
3084
- * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
3085
- * via {@link SettleDeps.on_settled}.
3086
- */
3087
- schedule(options = {}) {
3088
- const {
3089
- debounceMs = this._default_debounce_ms,
3090
- correlate: correlate_query = { after: -1, limit: 100 },
3091
- maxPasses = Infinity,
3092
- ...drain_options
3093
- } = options;
3094
- if (this._timer) clearTimeout(this._timer);
3095
- this._timer = setTimeout(() => {
3096
- this._timer = void 0;
3097
- if (this._running) {
3098
- this._pending = options;
3099
- return;
3100
- }
3101
- this._running = true;
3102
- let settle_done;
3103
- this._inflight = new Promise((done) => {
3104
- settle_done = done;
3105
- });
3106
- (async () => {
3107
- await this._deps.init();
3108
- let settled_drain;
3109
- for (let i = 0; i < maxPasses; i++) {
3110
- const after_before = this._deps.checkpoint();
3111
- const { subscribed, last_id, scanned } = await this._deps.correlate({
3112
- ...correlate_query,
3113
- after: after_before
3114
- });
3115
- if (scanned) this._deps.breaker.passed();
3116
- const drain = await this._deps.drain(drain_options);
3117
- settled_drain = settled_drain ? {
3118
- fetched: [...settled_drain.fetched, ...drain.fetched],
3119
- leased: [...settled_drain.leased, ...drain.leased],
3120
- acked: [...settled_drain.acked, ...drain.acked],
3121
- blocked: [...settled_drain.blocked, ...drain.blocked]
3122
- } : drain;
3123
- const made_progress = subscribed > 0 || drain.leased.length > 0 || last_id > after_before;
3124
- if (!made_progress) break;
3125
- }
3126
- if (settled_drain) this._deps.on_settled(settled_drain);
3127
- })().catch((err) => {
3128
- this._deps.breaker.failed(Date.now(), err);
3129
- }).finally(() => {
3130
- this._running = false;
3131
- this._inflight = void 0;
3132
- settle_done();
3133
- const pending = this._pending;
3134
- if (pending !== void 0) {
3135
- this._pending = void 0;
3136
- this.schedule(pending);
3137
- }
3138
- });
3139
- }, debounceMs);
3140
- }
2659
+ * Stream to close. Omitted → the reaction's own lease stream (a self-close).
2660
+ * The synthesized autoclose reaction sets it to the aggregate stream, since
2661
+ * its lease runs on a synthetic `__autoclose__` target.
2662
+ */
2663
+ stream;
2664
+ /** Optional archive callback to run while the stream is guarded. */
2665
+ archive;
3141
2666
  /**
3142
- * The cycle currently in flight, or `undefined` when idle. A
3143
- * graceful shutdown awaits this alongside the drain controllers so a
3144
- * settle parked in `correlate` does not resume after teardown.
2667
+ * Watermark to ack the requesting reaction to before the close runs. The
2668
+ * close-cycle safety guard skips a stream whose subscriptions (matched by
2669
+ * source) lag the head; the autoclose reaction's `source` is the aggregate,
2670
+ * so it must advance its own watermark to the live head id it evaluated
2671
+ * against — otherwise it blocks its own close. Defaults to the triggering
2672
+ * event id when omitted.
3145
2673
  */
3146
- get inflight() {
3147
- return this._inflight;
3148
- }
3149
- /** Cancel any pending or active settle cycle. Idempotent. */
3150
- stop() {
3151
- this._pending = void 0;
3152
- if (this._timer) {
3153
- clearTimeout(this._timer);
3154
- this._timer = void 0;
3155
- }
2674
+ at;
2675
+ /**
2676
+ * Windowed close: prune events older than this cutoff behind
2677
+ * the closest safe snapshot instead of retiring the stream. Thrown by
2678
+ * the autoclose reaction of a `.autocloses({ keep })` state; flows
2679
+ * into {@link CloseTarget.before}. Omitted → a full close.
2680
+ */
2681
+ before;
2682
+ constructor(opts) {
2683
+ super("reaction requested close");
2684
+ this.name = "CloseSignal";
2685
+ this.stream = opts?.stream;
2686
+ this.archive = opts?.archive;
2687
+ this.at = opts?.at;
2688
+ this.before = opts?.before;
3156
2689
  }
3157
2690
  };
3158
-
3159
- // src/builders/reaction-builder.ts
3160
2691
  function build_handle(deps) {
3161
2692
  const { logger, reaction_scope } = deps;
3162
2693
  return async (lease, payloads) => {
@@ -3251,6 +2782,126 @@ function build_handle_batch(logger) {
3251
2782
  };
3252
2783
  }
3253
2784
 
2785
+ // src/internal/settle.ts
2786
+ var SettleLoop = class {
2787
+ _timer = void 0;
2788
+ _running = false;
2789
+ /**
2790
+ * Resolves when the cycle in flight finishes, so shutdown can wait for
2791
+ * it (`stop()` cancels scheduling only). Never rejects.
2792
+ */
2793
+ _inflight;
2794
+ /** A wake-up arrived while a cycle ran: run one more pass after it. */
2795
+ _pending = false;
2796
+ /** The last caller's options, used by the next pass. */
2797
+ _next = void 0;
2798
+ /** When the scheduled pass fires (epoch ms). */
2799
+ _due = 0;
2800
+ /** Set at shutdown: nothing schedules again. */
2801
+ _stopped = false;
2802
+ _deps;
2803
+ /** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
2804
+ _default_debounce_ms;
2805
+ constructor(deps, default_debounce_ms) {
2806
+ this._deps = deps;
2807
+ this._default_debounce_ms = default_debounce_ms;
2808
+ }
2809
+ /**
2810
+ * Schedule a settle pass. A pass already scheduled is never put off or
2811
+ * replaced: calls that arrive before it runs join it (a shorter
2812
+ * `debounceMs` brings it forward), and a call during a running cycle
2813
+ * queues one more pass after it. Callers pass options, which the next
2814
+ * pass uses; internal wake-ups pass none, so they never discard a
2815
+ * caller's. The cycle runs correlate→drain in a
2816
+ * loop until no progress is made (no new subscriptions, no events read,
2817
+ * no streams leased) or `maxPasses` is reached, then emits the
2818
+ * `"settled"` lifecycle event via {@link SettleDeps.on_settled}.
2819
+ */
2820
+ schedule(options) {
2821
+ if (this._stopped) return;
2822
+ if (options) this._next = options;
2823
+ if (this._running) {
2824
+ this._pending = true;
2825
+ return;
2826
+ }
2827
+ const delay = options?.debounceMs ?? this._default_debounce_ms;
2828
+ const due = Date.now() + delay;
2829
+ if (this._timer) {
2830
+ if (due >= this._due) return;
2831
+ clearTimeout(this._timer);
2832
+ }
2833
+ this._due = due;
2834
+ this._timer = setTimeout(() => {
2835
+ this._timer = void 0;
2836
+ const {
2837
+ debounceMs: _,
2838
+ correlate: correlate_query = {},
2839
+ maxPasses = Infinity,
2840
+ ...drain_options
2841
+ } = this._next ?? {};
2842
+ this._next = void 0;
2843
+ this._running = true;
2844
+ let settle_done;
2845
+ this._inflight = new Promise((done) => {
2846
+ settle_done = done;
2847
+ });
2848
+ (async () => {
2849
+ await this._deps.init();
2850
+ let settled_drain;
2851
+ for (let i = 0; i < maxPasses; i++) {
2852
+ const after_before = this._deps.checkpoint();
2853
+ const { subscribed, last_id, scanned } = await this._deps.correlate({
2854
+ ...correlate_query,
2855
+ after: after_before
2856
+ });
2857
+ if (scanned) this._deps.breaker.passed();
2858
+ const drain = await this._deps.drain(drain_options);
2859
+ settled_drain = settled_drain ? {
2860
+ fetched: [...settled_drain.fetched, ...drain.fetched],
2861
+ leased: [...settled_drain.leased, ...drain.leased],
2862
+ acked: [...settled_drain.acked, ...drain.acked],
2863
+ blocked: [...settled_drain.blocked, ...drain.blocked]
2864
+ } : drain;
2865
+ const made_progress = subscribed > 0 || drain.leased.length > 0 || last_id > after_before;
2866
+ if (!made_progress) break;
2867
+ }
2868
+ if (settled_drain) this._deps.on_settled(settled_drain);
2869
+ })().catch((err) => {
2870
+ this._deps.breaker.failed(Date.now(), err);
2871
+ }).finally(() => {
2872
+ this._running = false;
2873
+ this._inflight = void 0;
2874
+ settle_done();
2875
+ if (this._pending) {
2876
+ this._pending = false;
2877
+ this.schedule(this._next);
2878
+ }
2879
+ });
2880
+ }, delay);
2881
+ }
2882
+ /**
2883
+ * The cycle currently in flight, or `undefined` when idle. A
2884
+ * graceful shutdown awaits this alongside the drain controllers so a
2885
+ * settle parked in `correlate` does not resume after teardown.
2886
+ */
2887
+ get inflight() {
2888
+ return this._inflight;
2889
+ }
2890
+ /**
2891
+ * Cancel any pending settle pass. Idempotent. `final` (shutdown) also
2892
+ * stops all later scheduling.
2893
+ */
2894
+ stop(final = false) {
2895
+ if (final) this._stopped = true;
2896
+ this._pending = false;
2897
+ this._next = void 0;
2898
+ if (this._timer) {
2899
+ clearTimeout(this._timer);
2900
+ this._timer = void 0;
2901
+ }
2902
+ }
2903
+ };
2904
+
3254
2905
  // src/act.ts
3255
2906
  function validate_only_lanes(options, lanes) {
3256
2907
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
@@ -3266,14 +2917,15 @@ var Act = class {
3266
2917
  _breaker;
3267
2918
  _listen;
3268
2919
  _drain;
2920
+ _auto_settle;
3269
2921
  /** Whether any event has a reaction; nothing arms without one. */
3270
2922
  _reactive;
3271
2923
  _drain_controllers;
3272
2924
  _correlate;
3273
2925
  _settle;
3274
2926
  _poll;
3275
- /** The deprecated `start_correlations` callback, while the poll runs. */
3276
- _on_discovered;
2927
+ /** The poll's correlate `limit`, used by every settle pass while it runs. */
2928
+ _poll_limit;
3277
2929
  /**
3278
2930
  * Wired at construction against the store current then, so a store
3279
2931
  * injected after `build()` is never notified.
@@ -3402,6 +3054,7 @@ var Act = class {
3402
3054
  this._event_to_lanes = classification.event_to_lanes;
3403
3055
  this._listen = options.listen !== false;
3404
3056
  this._drain = options.drain !== false;
3057
+ this._auto_settle = options.autoSettle !== false;
3405
3058
  this._breaker = this._build_breaker(options);
3406
3059
  this._drain_controllers = this._build_drain_controllers(options, lanes);
3407
3060
  this._advise_orphaned_lanes(options);
@@ -3419,9 +3072,7 @@ var Act = class {
3419
3072
  on_error: (error, circuit) => this._emit_error(error, circuit),
3420
3073
  // When the cooldown elapses, settle once: its scan or claim is the
3421
3074
  // half-open probe that closes or re-opens the breaker.
3422
- on_retry: () => {
3423
- this.settle({ debounceMs: 0 });
3424
- }
3075
+ on_retry: () => this._schedule()
3425
3076
  }
3426
3077
  );
3427
3078
  }
@@ -3523,7 +3174,10 @@ var Act = class {
3523
3174
  {
3524
3175
  init: () => this._scoped(() => this._correlate.init()),
3525
3176
  checkpoint: () => this._correlate.checkpoint,
3526
- correlate: (q) => this._correlate_scanned(q, true),
3177
+ correlate: (q) => this._correlate_scanned(
3178
+ { ...q, limit: q.limit ?? this._poll_limit ?? 100 },
3179
+ true
3180
+ ),
3527
3181
  drain: (o) => this.drain(o),
3528
3182
  on_settled: (drain) => this.emit("settled", drain),
3529
3183
  breaker: this._breaker
@@ -3559,7 +3213,7 @@ var Act = class {
3559
3213
  this.stop_correlations();
3560
3214
  const disposer = await this._notify_disposer;
3561
3215
  if (disposer) await disposer();
3562
- this._settle.stop();
3216
+ this._settle.stop(true);
3563
3217
  this._breaker.stop();
3564
3218
  for (const c of this._drain_controllers.values()) c.stop();
3565
3219
  await this._await_inflight(options?.graceMs);
@@ -3613,7 +3267,7 @@ var Act = class {
3613
3267
  const armed = this._arm_for_event_names(
3614
3268
  notification.events.map((e) => e.name)
3615
3269
  );
3616
- if (armed) this._settle.schedule({ debounceMs: 0 });
3270
+ this._settle_armed(armed);
3617
3271
  }
3618
3272
  } catch (err) {
3619
3273
  this._logger.error(err, "notified handler threw");
@@ -3677,9 +3331,10 @@ var Act = class {
3677
3331
  payload,
3678
3332
  do_options
3679
3333
  );
3680
- this._arm_for_event_names(
3334
+ const armed = this._arm_for_event_names(
3681
3335
  snapshots.map((s) => s.event.name)
3682
3336
  );
3337
+ this._settle_armed(armed);
3683
3338
  this.emit("committed", snapshots);
3684
3339
  return snapshots;
3685
3340
  });
@@ -3842,6 +3497,18 @@ var Act = class {
3842
3497
  this._correlate.arm();
3843
3498
  for (const c of this._drain_controllers.values()) c.arm();
3844
3499
  }
3500
+ /**
3501
+ * Settles what a change armed (a commit, a reset, an unblock, a restore,
3502
+ * a notification from another process), unless the app walks the
3503
+ * pipeline itself (`autoSettle: false`).
3504
+ */
3505
+ _settle_armed(armed) {
3506
+ if (armed && this._auto_settle) this._schedule();
3507
+ }
3508
+ /** Schedules a settle pass; writer-only instances (`drain: false`) skip it. */
3509
+ _schedule(options) {
3510
+ if (this._drain) this._settle.schedule(options);
3511
+ }
3845
3512
  /** Arms only the lanes these events react on; true when any was armed. */
3846
3513
  _arm_for_event_names(names) {
3847
3514
  const to_arm = /* @__PURE__ */ new Set();
@@ -3921,7 +3588,6 @@ var Act = class {
3921
3588
  return this._scoped(async () => {
3922
3589
  const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query, lease);
3923
3590
  if (subscribed > 0 || marked > 0) this._arm_all();
3924
- if (subscribed > 0) this._on_discovered?.(subscribed);
3925
3591
  return { subscribed, last_id, scanned };
3926
3592
  });
3927
3593
  }
@@ -3941,9 +3607,6 @@ var Act = class {
3941
3607
  * reads (default 100). The scan always starts at the correlation
3942
3608
  * checkpoint, so `after` is ignored.
3943
3609
  * @param frequency - Milliseconds between ticks (default: 10000)
3944
- * @param callback - Called with the number of newly subscribed streams
3945
- * after each correlate pass that found some.
3946
- * @deprecated Listen to the `"settled"` lifecycle event instead.
3947
3610
  * @returns `true` if the poll started, `false` if one is already running
3948
3611
  *
3949
3612
  * @example
@@ -3956,13 +3619,12 @@ var Act = class {
3956
3619
  * @see {@link settle} for what each tick runs
3957
3620
  * @see {@link stop_correlations} to stop the poll
3958
3621
  */
3959
- start_correlations(query = {}, frequency = 1e4, callback) {
3622
+ start_correlations(query = {}, frequency = 1e4) {
3960
3623
  if (this._poll) return false;
3961
- this._on_discovered = callback;
3962
- const correlate = { limit: query.limit || 100 };
3624
+ this._poll_limit = query.limit || void 0;
3963
3625
  this._poll = setInterval(() => {
3964
3626
  this._correlate.arm();
3965
- this.settle({ debounceMs: 0, correlate });
3627
+ this._schedule();
3966
3628
  }, frequency);
3967
3629
  return true;
3968
3630
  }
@@ -3976,12 +3638,13 @@ var Act = class {
3976
3638
  if (this._poll) {
3977
3639
  clearInterval(this._poll);
3978
3640
  this._poll = void 0;
3641
+ this._poll_limit = void 0;
3979
3642
  }
3980
- this._on_discovered = void 0;
3981
3643
  void this._scoped(() => this._correlate.release_correlation());
3982
3644
  }
3983
3645
  /**
3984
- * Cancels any pending or active settle cycle.
3646
+ * Drops the settle pass scheduled but not yet started. A cycle already
3647
+ * running finishes; later changes schedule new passes.
3985
3648
  *
3986
3649
  * @see {@link settle}
3987
3650
  */
@@ -3989,8 +3652,8 @@ var Act = class {
3989
3652
  this._settle.stop();
3990
3653
  }
3991
3654
  /**
3992
- * Reset reaction stream watermarks and request a drain on the next
3993
- * `drain()` / `settle()` cycle.
3655
+ * Reset reaction stream watermarks and settle them, replaying their
3656
+ * events through their reactions.
3994
3657
  *
3995
3658
  * Use this to replay events through projections (or other reaction targets)
3996
3659
  * after changing handler logic. Equivalent to calling `store().reset(streams)`
@@ -3998,10 +3661,10 @@ var Act = class {
3998
3661
  * `store().reset(...)` alone leaves the flag untouched, so a settled app
3999
3662
  * would short-circuit and skip the replay.
4000
3663
  *
4001
- * Pair with `app.settle()` (or a single `app.drain()` for small streams).
4002
- * `settle()` loops correlate→drain until no progress is made, so one call
4003
- * fully catches up paginated streams without forcing callers to roll
4004
- * their own loop.
3664
+ * The reset schedules a settle on its own, which loops correlate→drain
3665
+ * until the replay catches up. Call `app.settle(options)` right after to
3666
+ * give that pass batch options, or
3667
+ * `correlate()` / `drain()` to drive the replay by hand.
4005
3668
  *
4006
3669
  * @param input - Reaction target streams (e.g., projection names) to reset, or a {@link StreamFilter} for bulk operations
4007
3670
  * @returns Count of streams that were actually reset
@@ -4009,7 +3672,7 @@ var Act = class {
4009
3672
  * @example Rebuild a projection (production)
4010
3673
  * ```typescript
4011
3674
  * await app.reset(["my-projection"]);
4012
- * app.settle({ eventLimit: 1000 }); // emits "settled" when fully replayed
3675
+ * app.settle({ eventLimit: 1000 }); // optional: bigger batches; emits "settled"
4013
3676
  * ```
4014
3677
  *
4015
3678
  * @see {@link Store.reset} for the underlying store primitive
@@ -4020,7 +3683,10 @@ var Act = class {
4020
3683
  const count = await store().reset(input);
4021
3684
  for (const handler of this._batch_handlers.values())
4022
3685
  handler[FOLD_RESET]?.();
4023
- if (count > 0) this._arm_all();
3686
+ if (count > 0) {
3687
+ this._arm_all();
3688
+ this._settle_armed(this._reactive);
3689
+ }
4024
3690
  return count;
4025
3691
  });
4026
3692
  }
@@ -4033,11 +3699,9 @@ var Act = class {
4033
3699
  * {@link reset}, which rebuilds from event 0 (suitable for projection
4034
3700
  * rebuilds, wrong for "I fixed the bug, please retry").
4035
3701
  *
4036
- * Wraps `store().unblock(streams)` and raises the orchestrator's
4037
- * internal "needs drain" flag so a settled app picks up the now-free
4038
- * streams on the next cycle. Equivalent to calling `store().unblock(...)`
4039
- * directly, but `store().unblock(...)` alone leaves the flag
4040
- * untouched.
3702
+ * Wraps `store().unblock(streams)` and settles the now-free streams on
3703
+ * its own. `store().unblock(...)` alone leaves them waiting for the next
3704
+ * commit.
4041
3705
  *
4042
3706
  * @param input - Stream names to unblock, or a {@link StreamFilter} for bulk recovery
4043
3707
  * @returns Count of streams that were actually flipped (were blocked)
@@ -4054,7 +3718,10 @@ var Act = class {
4054
3718
  async unblock(input) {
4055
3719
  return this._scoped(async () => {
4056
3720
  const count = await store().unblock(input);
4057
- if (count > 0) this._arm_all();
3721
+ if (count > 0) {
3722
+ this._arm_all();
3723
+ this._settle_armed(this._reactive);
3724
+ }
4058
3725
  return count;
4059
3726
  });
4060
3727
  }
@@ -4116,6 +3783,7 @@ var Act = class {
4116
3783
  if (!sink) {
4117
3784
  this._correlate.restart();
4118
3785
  this._arm_all();
3786
+ this._settle_armed(this._reactive);
4119
3787
  }
4120
3788
  return { kept, migrated, dropped, duration_ms: Date.now() - started };
4121
3789
  });
@@ -4255,84 +3923,290 @@ var Act = class {
4255
3923
  this.emit("closed", result);
4256
3924
  return result;
4257
3925
  }
4258
- /**
4259
- * Close the books — guard, archive, truncate, and optionally restart streams.
4260
- *
4261
- * Safely removes historical events from the operational store:
4262
- *
4263
- * 1. **Correlate** — discover pending reaction targets
4264
- * 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
4265
- * 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
4266
- * 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
4267
- * 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
4268
- * 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
4269
- * 7. **Cache** — invalidate (tombstoned) or warm (restarted)
4270
- * 8. **Emit "closed"** — lifecycle event with results
4271
- *
4272
- * Targets carrying `before` take the **windowed** branch instead —
4273
- * close the books on a rolling window: probe the min consumer
4274
- * watermark (so the boundary never rises past a lagging reaction),
4275
- * run the archive callback against the cutoff, then prune the prefix
4276
- * below the closest safe `__snapshot__`. No tombstone guard (the
4277
- * pre-cutoff prefix is immutable), no seed, cache untouched — the
4278
- * stream stays live and keeps accepting actions. Requires the state
4279
- * to snapshot via `.snap(...)`; no qualifying snapshot ⇒ the stream
4280
- * lands in `skipped` (retry after the next snapshot). Windowed
4281
- * entries in the result echo `before` and carry the surviving
4282
- * boundary snapshot as `committed`.
4283
- *
4284
- * @param targets - Per-stream close options (stream, restart?, archive?, before?)
4285
- * @returns `{ truncated: TruncateResult, skipped: string[] }`
4286
- *
4287
- * @example Archive and close
4288
- * ```typescript
4289
- * await app.close([
4290
- * { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
4291
- * { stream: "order-456" },
4292
- * ]);
4293
- * ```
4294
- */
4295
- async close(targets) {
4296
- if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
4297
- return this._scoped(() => this._run_close(targets));
3926
+ /**
3927
+ * Close the books — guard, archive, truncate, and optionally restart streams.
3928
+ *
3929
+ * Safely removes historical events from the operational store:
3930
+ *
3931
+ * 1. **Correlate** — discover pending reaction targets
3932
+ * 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
3933
+ * 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
3934
+ * 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
3935
+ * 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
3936
+ * 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
3937
+ * 7. **Cache** — invalidate (tombstoned) or warm (restarted)
3938
+ * 8. **Emit "closed"** — lifecycle event with results
3939
+ *
3940
+ * Targets carrying `before` take the **windowed** branch instead —
3941
+ * close the books on a rolling window: probe the min consumer
3942
+ * watermark (so the boundary never rises past a lagging reaction),
3943
+ * run the archive callback against the cutoff, then prune the prefix
3944
+ * below the closest safe `__snapshot__`. No tombstone guard (the
3945
+ * pre-cutoff prefix is immutable), no seed, cache untouched — the
3946
+ * stream stays live and keeps accepting actions. Requires the state
3947
+ * to snapshot via `.snap(...)`; no qualifying snapshot ⇒ the stream
3948
+ * lands in `skipped` (retry after the next snapshot). Windowed
3949
+ * entries in the result echo `before` and carry the surviving
3950
+ * boundary snapshot as `committed`.
3951
+ *
3952
+ * @param targets - Per-stream close options (stream, restart?, archive?, before?)
3953
+ * @returns `{ truncated: TruncateResult, skipped: string[] }`
3954
+ *
3955
+ * @example Archive and close
3956
+ * ```typescript
3957
+ * await app.close([
3958
+ * { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
3959
+ * { stream: "order-456" },
3960
+ * ]);
3961
+ * ```
3962
+ */
3963
+ async close(targets) {
3964
+ if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
3965
+ return this._scoped(() => this._run_close(targets));
3966
+ }
3967
+ /**
3968
+ * Debounced, non-blocking correlate→drain cycle.
3969
+ *
3970
+ * A local change that frees reaction work (a commit, `reset()`,
3971
+ * `unblock()`, `restore()`) schedules this on its own unless the app
3972
+ * sets `autoSettle: false`. Call it at startup, to run what was pending
3973
+ * before a restart, or with options for the next pass. A pass already
3974
+ * scheduled is never put off: calls before it runs join it (a shorter
3975
+ * `debounceMs` brings it forward), and a call during a running cycle
3976
+ * queues one more pass after it. Runs correlate→drain in a loop until a pass makes no
3977
+ * progress — no new subscriptions, no events read, no streams leased —
3978
+ * then emits the `"settled"` lifecycle event, so one call fully catches
3979
+ * up paginated streams without forcing callers to loop.
3980
+ *
3981
+ * @param options - Settle configuration — see {@link SettleOptions} for fields:
3982
+ * `debounceMs` (default 10), `correlate.limit` (default 100, or the
3983
+ * poll's limit while `start_correlations` runs; `after` is always the
3984
+ * checkpoint),
3985
+ * `maxPasses` (default `Infinity` — kill-switch for runaway loops),
3986
+ * `streamLimit` (default 10), `eventLimit` (default 10),
3987
+ * `leaseMillis` (default 10000).
3988
+ *
3989
+ * @example Startup
3990
+ * ```typescript
3991
+ * app.settle(); // run what was pending before the restart
3992
+ *
3993
+ * app.on("settled", (drain) => {
3994
+ * // notify SSE clients, invalidate caches, etc.
3995
+ * });
3996
+ * ```
3997
+ *
3998
+ * @see {@link drain} for single synchronous drain cycles
3999
+ * @see {@link correlate} for manual correlation
4000
+ */
4001
+ settle(options) {
4002
+ resolveSettleConfig(options ?? {});
4003
+ this._schedule(options);
4004
+ }
4005
+ };
4006
+
4007
+ // src/internal/autoclose-policy.ts
4008
+ import { z as z2 } from "zod";
4009
+ var DAY_MS = 864e5;
4010
+ var MIN_AFTER_DAYS = 1 / 1440;
4011
+ var MIN_KEEP_DAYS = 1;
4012
+ function days_after(date, days) {
4013
+ return new Date(date.getTime() + days * DAY_MS);
4014
+ }
4015
+ function days_before_now(days) {
4016
+ return new Date(Date.now() - days * DAY_MS);
4017
+ }
4018
+ var window_schema = (field, min_days, floor) => z2.object({
4019
+ days: z2.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
4020
+ }).refine((o) => o.days >= min_days, {
4021
+ message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
4022
+ });
4023
+ var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
4024
+ var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
4025
+ var IsSchema = z2.union([
4026
+ z2.string().min(1, "autocloses: is must be a non-empty event name"),
4027
+ z2.array(
4028
+ z2.string().min(1, "autocloses: is entries must be non-empty event names")
4029
+ ).min(1, "autocloses: is must include at least one event name").readonly()
4030
+ ]);
4031
+ var ReachesSchema = z2.number({ message: "autocloses: reaches must be a number" }).int("autocloses: reaches must be an integer").min(1, "autocloses: reaches must be >= 1");
4032
+ var OrBlockSchema = z2.object({
4033
+ after: AfterSchema.optional(),
4034
+ is: IsSchema.optional(),
4035
+ reaches: ReachesSchema.optional()
4036
+ }).strict().refine(
4037
+ (o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
4038
+ {
4039
+ message: "autocloses: `or` block must include at least one of after / is / reaches"
4040
+ }
4041
+ );
4042
+ var AutoclosePolicySchema = z2.object({
4043
+ after: AfterSchema.optional(),
4044
+ is: IsSchema.optional(),
4045
+ reaches: ReachesSchema.optional(),
4046
+ or: OrBlockSchema.optional(),
4047
+ keep: KeepSchema.optional()
4048
+ }).strict().refine(
4049
+ (o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
4050
+ {
4051
+ message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
4052
+ }
4053
+ );
4054
+ function compile_after(after) {
4055
+ return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
4056
+ }
4057
+ function compile_is(is) {
4058
+ const set = new Set(typeof is === "string" ? [is] : is);
4059
+ return (_stream, head) => set.has(head.name);
4060
+ }
4061
+ function compile_reaches(reaches) {
4062
+ return (_stream, _head, count) => count >= reaches;
4063
+ }
4064
+ function policy_min_after_days(options) {
4065
+ const parsed = AutoclosePolicySchema.parse(options);
4066
+ const windows = [];
4067
+ if (parsed.after) windows.push(parsed.after.days);
4068
+ if (parsed.or?.after) windows.push(parsed.or.after.days);
4069
+ return windows.length ? Math.min(...windows) : void 0;
4070
+ }
4071
+ function policy_keep_days(options) {
4072
+ const parsed = AutoclosePolicySchema.parse(options);
4073
+ return parsed.keep?.days;
4074
+ }
4075
+ function compile_autoclose_policy(options) {
4076
+ const parsed = AutoclosePolicySchema.parse(options);
4077
+ const and_preds = [];
4078
+ if (parsed.after) and_preds.push(compile_after(parsed.after));
4079
+ if (parsed.is) and_preds.push(compile_is(parsed.is));
4080
+ if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
4081
+ const or_preds = [];
4082
+ if (parsed.or) {
4083
+ if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
4084
+ if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
4085
+ if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
4086
+ }
4087
+ return (stream, head, count) => {
4088
+ if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
4089
+ return true;
4090
+ }
4091
+ if (or_preds.some((p) => p(stream, head, count))) {
4092
+ return true;
4093
+ }
4094
+ return false;
4095
+ };
4096
+ }
4097
+
4098
+ // src/internal/autoclose-window.ts
4099
+ function hour_in_zone(now, timeZone) {
4100
+ const parts = new Intl.DateTimeFormat("en-US", {
4101
+ timeZone,
4102
+ hour: "2-digit",
4103
+ hourCycle: "h23"
4104
+ }).formatToParts(now);
4105
+ return Number(parts.find((p) => p.type === "hour")?.value);
4106
+ }
4107
+ function hour_in_range(hour, start, end) {
4108
+ return start < end ? hour >= start && hour < end : hour >= start || hour < end;
4109
+ }
4110
+ function is_dst_gap_open(now, timeZone, start) {
4111
+ const hour = hour_in_zone(now, timeZone);
4112
+ if (hour <= start) return false;
4113
+ const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
4114
+ return prev === start - 1 && hour === start + 1;
4115
+ }
4116
+ function in_autoclose_window(window, now) {
4117
+ if (!window) return true;
4118
+ const hour = hour_in_zone(now, window.timeZone);
4119
+ return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
4120
+ }
4121
+ function next_window_open(window, now) {
4122
+ const start_ms = now.getTime();
4123
+ for (let h = 0; h <= 48; h++) {
4124
+ const candidate = new Date(start_ms + h * 36e5);
4125
+ if (hour_in_zone(candidate, window.timeZone) === window.start)
4126
+ return candidate;
4127
+ const boundary = new Date(
4128
+ candidate.getTime() - candidate.getTime() % 36e5
4129
+ );
4130
+ if (boundary.getTime() >= start_ms && is_dst_gap_open(boundary, window.timeZone, window.start))
4131
+ return boundary;
4298
4132
  }
4299
- /**
4300
- * Debounced, non-blocking correlate→drain cycle.
4301
- *
4302
- * Call this after `app.do()` (or `app.reset()`) to schedule a background
4303
- * drain. Multiple rapid calls within the debounce window are coalesced
4304
- * into a single cycle. Runs correlate→drain in a loop until a pass makes
4305
- * no progress — no new subscriptions, no events read, no streams leased —
4306
- * then emits
4307
- * the `"settled"` lifecycle event. This means a single `settle()` call
4308
- * fully catches up paginated streams (e.g. after `reset()` on a long
4309
- * projection) without forcing callers to loop.
4310
- *
4311
- * @param options - Settle configuration — see {@link SettleOptions} for fields:
4312
- * `debounceMs` (default 10), `correlate` (default `{ after: -1, limit: 100 }`),
4313
- * `maxPasses` (default `Infinity` — kill-switch for runaway loops),
4314
- * `streamLimit` (default 10), `eventLimit` (default 10),
4315
- * `leaseMillis` (default 10000).
4316
- *
4317
- * @example API mutations
4318
- * ```typescript
4319
- * await app.do("CreateItem", target, input);
4320
- * app.settle(); // non-blocking, returns immediately
4321
- *
4322
- * app.on("settled", (drain) => {
4323
- * // notify SSE clients, invalidate caches, etc.
4324
- * });
4325
- * ```
4326
- *
4327
- * @see {@link drain} for single synchronous drain cycles
4328
- * @see {@link correlate} for manual correlation
4329
- */
4330
- settle(options = {}) {
4331
- resolveSettleConfig(options);
4332
- if (!this._drain) return;
4333
- this._settle.schedule(options);
4133
+ return new Date(start_ms + 864e5);
4134
+ }
4135
+
4136
+ // src/internal/autoclose-reaction.ts
4137
+ var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
4138
+ function synthesize_autoclose_reactions(registry, states, read_window) {
4139
+ for (const st of states.values()) {
4140
+ const predicate = st.autoclose;
4141
+ if (!predicate) continue;
4142
+ const after_days = st.autoclose_after_days;
4143
+ const keep_days = st.autoclose_keep_days;
4144
+ const archiver = st.archive;
4145
+ const reaction = {
4146
+ // Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
4147
+ // per-aggregate `__autoclose__` key — so the autoclose reaction never
4148
+ // shares a watermark with the aggregate's own reactions. A shared
4149
+ // watermark would let autoclose's defer short-circuit the aggregate's
4150
+ // other reactions (the "a defer affects all reactions on a stream"
4151
+ // hazard). The close still targets the aggregate (`source`).
4152
+ resolver: (e) => ({
4153
+ target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
4154
+ source: e.stream
4155
+ }),
4156
+ // Never block on autoclose: a transient query/store error should retry,
4157
+ // not quarantine the synthetic stream.
4158
+ options: { blockOnError: false, maxRetries: DEFAULT_MAX_RETRIES },
4159
+ handler: async (event) => {
4160
+ const aggregate = event.stream;
4161
+ const window = read_window();
4162
+ if (!in_autoclose_window(window, /* @__PURE__ */ new Date()))
4163
+ throw new DeferSignal({ at: next_window_open(window, /* @__PURE__ */ new Date()) });
4164
+ const stats = await store().query_stats([aggregate], {
4165
+ count: true,
4166
+ tail: keep_days !== void 0 ? true : void 0,
4167
+ exclude: [TOMBSTONE_EVENT, SNAP_EVENT]
4168
+ });
4169
+ const entry = stats.get(aggregate);
4170
+ if (!entry) return;
4171
+ const head = entry.head;
4172
+ if (predicate(aggregate, head, entry.count))
4173
+ throw new CloseSignal({
4174
+ stream: aggregate,
4175
+ archive: archiver ? () => archiver(aggregate, head) : void 0,
4176
+ // Ack this reaction's own watermark to the live head so the
4177
+ // close-cycle guard (which matches subscriptions by source =
4178
+ // aggregate) sees it caught up instead of blocking its own close.
4179
+ at: head.id
4180
+ });
4181
+ if (keep_days !== void 0) {
4182
+ const cutoff = days_before_now(keep_days);
4183
+ if (entry.tail.created < cutoff)
4184
+ throw new CloseSignal({
4185
+ stream: aggregate,
4186
+ before: cutoff,
4187
+ archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
4188
+ at: head.id
4189
+ });
4190
+ }
4191
+ const dues = [];
4192
+ if (after_days !== void 0)
4193
+ dues.push(days_after(head.created, after_days));
4194
+ if (keep_days !== void 0)
4195
+ dues.push(days_after(entry.tail.created, keep_days));
4196
+ const future = dues.map((d) => d.getTime()).filter((t) => t > Date.now());
4197
+ if (future.length)
4198
+ throw new DeferSignal({ at: new Date(Math.min(...future)) });
4199
+ }
4200
+ };
4201
+ const key = `__autoclose_${st.name}`;
4202
+ for (const event_name of Object.keys(st.events)) {
4203
+ registry.events[event_name]?.reactions.set(
4204
+ key,
4205
+ reaction
4206
+ );
4207
+ }
4334
4208
  }
4335
- };
4209
+ }
4336
4210
 
4337
4211
  // src/builders/merge.ts
4338
4212
  import { ZodObject } from "zod";
@@ -4571,7 +4445,74 @@ function reaction_on(event, events, builder) {
4571
4445
  }
4572
4446
 
4573
4447
  // src/builders/event-builder.ts
4574
- import { z as z5 } from "zod";
4448
+ import { z as z4 } from "zod";
4449
+
4450
+ // src/internal/date-reviver.ts
4451
+ import { z as z3 } from "zod";
4452
+ var def_of = (schema) => schema._zod?.def ?? schema.def;
4453
+ function variant_schema(schema) {
4454
+ const shape = def_of(schema)?.shape;
4455
+ if (!shape) {
4456
+ const dates = date_reviver_schema(schema);
4457
+ return { schema: dates ?? schema, dated: !!dates };
4458
+ }
4459
+ const next = {};
4460
+ let dated = false;
4461
+ for (const [key, field] of Object.entries(shape)) {
4462
+ const dates = date_reviver_schema(field);
4463
+ if (dates) dated = true;
4464
+ next[key] = (dates ?? field).optional();
4465
+ }
4466
+ return { schema: z3.looseObject(next), dated };
4467
+ }
4468
+ function date_reviver_schema(schema) {
4469
+ const def = def_of(schema);
4470
+ if (!def) return void 0;
4471
+ const inner = () => date_reviver_schema(def.innerType);
4472
+ switch (def.type) {
4473
+ case "date":
4474
+ return z3.coerce.date();
4475
+ case "object": {
4476
+ const shape = def.shape;
4477
+ if (!shape) return void 0;
4478
+ const dated = {};
4479
+ for (const [key, field] of Object.entries(shape)) {
4480
+ const dates = date_reviver_schema(field);
4481
+ if (dates) dated[key] = dates.optional();
4482
+ }
4483
+ return Object.keys(dated).length ? z3.looseObject(dated) : void 0;
4484
+ }
4485
+ case "array": {
4486
+ const element = date_reviver_schema(def.element);
4487
+ return element && z3.array(element);
4488
+ }
4489
+ case "tuple": {
4490
+ const items = def.items.map((i) => date_reviver_schema(i));
4491
+ return items.some(Boolean) ? z3.tuple(items.map((i) => i ?? z3.unknown())) : void 0;
4492
+ }
4493
+ case "record": {
4494
+ const value = date_reviver_schema(def.valueType);
4495
+ return value && z3.record(z3.string(), value);
4496
+ }
4497
+ case "union": {
4498
+ const variants = def.options.map(variant_schema);
4499
+ return variants.some((v) => v.dated) ? z3.union(variants.map((v) => v.schema)) : void 0;
4500
+ }
4501
+ case "nullable":
4502
+ return inner()?.nullable();
4503
+ case "optional":
4504
+ case "nonoptional":
4505
+ case "readonly":
4506
+ case "default":
4507
+ case "prefault":
4508
+ case "catch":
4509
+ return inner();
4510
+ default:
4511
+ return void 0;
4512
+ }
4513
+ }
4514
+
4515
+ // src/builders/event-builder.ts
4575
4516
  function event_tags(schema) {
4576
4517
  const pii = pii_schemas(schema);
4577
4518
  const pii_dates = {};
@@ -4588,7 +4529,7 @@ function event_tags(schema) {
4588
4529
  return {
4589
4530
  sensitive: Object.keys(pii),
4590
4531
  date_reviver: data_reviver_schema && revive(data_reviver_schema),
4591
- pii_date_reviver: dated_pii ? revive(z5.looseObject(pii_dates)) : void 0
4532
+ pii_date_reviver: dated_pii ? revive(z4.looseObject(pii_dates)) : void 0
4592
4533
  };
4593
4534
  }
4594
4535
  function make_event_reader(tags, disclosure, predicate = null) {
@@ -5375,9 +5316,6 @@ export {
5375
5316
  ConcurrencyError,
5376
5317
  ConsoleLogger,
5377
5318
  CsvFile,
5378
- DEFAULT_AUTOCLOSE_CYCLE_MINUTES,
5379
- DEFAULT_CLOSE_BATCH_SIZE,
5380
- DEFAULT_CLOSE_YIELD_MS,
5381
5319
  DEFAULT_LANE,
5382
5320
  DEFAULT_MAX_SUBSCRIBED_STREAMS,
5383
5321
  DEFAULT_SETTLE_DEBOUNCE_MS,
@@ -5415,7 +5353,7 @@ export {
5415
5353
  pii_fields,
5416
5354
  port,
5417
5355
  projection,
5418
- resolveAutocloseConfig,
5356
+ resolveAutocloseConfig2 as resolveAutocloseConfig,
5419
5357
  sensitive,
5420
5358
  sleep,
5421
5359
  slice,