@rotorsoft/act 1.32.30 → 1.33.1
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/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +96 -385
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts +2 -115
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/builder-utils.d.ts +1 -1
- package/dist/@types/builders/builder-utils.d.ts.map +1 -1
- package/dist/@types/builders/event-builder.d.ts +5 -5
- package/dist/@types/builders/event-builder.d.ts.map +1 -1
- package/dist/@types/builders/merge.d.ts +1 -1
- package/dist/@types/builders/merge.d.ts.map +1 -1
- package/dist/@types/builders/projection-builder.d.ts +2 -144
- package/dist/@types/builders/projection-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +2 -71
- package/dist/@types/builders/slice-builder.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +2 -376
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/config.d.ts +6 -43
- package/dist/@types/config.d.ts.map +1 -1
- package/dist/@types/csv.d.ts +1 -15
- package/dist/@types/csv.d.ts.map +1 -1
- package/dist/@types/index.d.ts +0 -1
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-policy.d.ts +1 -59
- package/dist/@types/internal/autoclose-policy.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-reaction.d.ts +1 -2
- package/dist/@types/internal/autoclose-reaction.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-window.d.ts +1 -1
- package/dist/@types/internal/autoclose-window.d.ts.map +1 -1
- package/dist/@types/internal/circuit-breaker.d.ts +1 -2
- package/dist/@types/internal/circuit-breaker.d.ts.map +1 -1
- package/dist/@types/{builders/build-classify.d.ts → internal/classify.d.ts} +2 -15
- package/dist/@types/internal/classify.d.ts.map +1 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/config.d.ts +2 -42
- package/dist/@types/internal/config.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/{correlator.d.ts → correlation-id.d.ts} +1 -1
- package/dist/@types/internal/correlation-id.d.ts.map +1 -0
- package/dist/@types/internal/defer-config.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +1 -1
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/{drain.d.ts → drain-ops.d.ts} +1 -1
- package/dist/@types/internal/drain-ops.d.ts.map +1 -0
- package/dist/@types/internal/event-sourcing.d.ts +1 -8
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/{sensitive.d.ts → pii.d.ts} +4 -71
- package/dist/@types/internal/pii.d.ts.map +1 -0
- package/dist/@types/internal/projection-fold.d.ts +1 -2
- package/dist/@types/internal/projection-fold.d.ts.map +1 -1
- package/dist/@types/internal/{close-signal.d.ts → reaction-dispatch.d.ts} +47 -2
- package/dist/@types/internal/reaction-dispatch.d.ts.map +1 -0
- package/dist/@types/internal/report-once.d.ts +1 -1
- package/dist/@types/internal/settle.d.ts +21 -12
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts +3 -3
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +3 -43
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/scoped.d.ts +1 -6
- package/dist/@types/scoped.d.ts.map +1 -1
- package/dist/@types/sensitive.d.ts +56 -0
- package/dist/@types/sensitive.d.ts.map +1 -0
- package/dist/@types/test/index.d.ts +1 -0
- package/dist/@types/test/index.d.ts.map +1 -1
- package/dist/@types/test/sandbox.d.ts +2 -26
- package/dist/@types/test/sandbox.d.ts.map +1 -1
- package/dist/@types/types/act.d.ts +214 -0
- package/dist/@types/types/act.d.ts.map +1 -0
- package/dist/@types/types/action.d.ts +69 -2
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/builders.d.ts +721 -0
- package/dist/@types/types/builders.d.ts.map +1 -0
- package/dist/@types/types/errors.d.ts +31 -1
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/types/index.d.ts +38 -0
- package/dist/@types/types/index.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +20 -18
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +11 -1
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/@types/types/sandbox.d.ts +31 -0
- package/dist/@types/types/sandbox.d.ts.map +1 -0
- package/dist/@types/types/schemas.d.ts +12 -17
- package/dist/@types/types/schemas.d.ts.map +1 -1
- package/dist/chunk-NNWP65YM.cjs +264 -0
- package/dist/chunk-NNWP65YM.cjs.map +1 -0
- package/dist/{chunk-X3MMQIIP.js → chunk-NYH6FNTM.js} +174 -22
- package/dist/chunk-NYH6FNTM.js.map +1 -0
- package/dist/{chunk-HHKLVRR4.js → chunk-TJ4JRRUR.js} +38 -57
- package/dist/chunk-TJ4JRRUR.js.map +1 -0
- package/dist/chunk-YKL44PYX.cjs +1233 -0
- package/dist/chunk-YKL44PYX.cjs.map +1 -0
- package/dist/index.cjs +1954 -3440
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +911 -1063
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +16 -1113
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +2 -2
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs +58 -276
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +15 -1
- package/package.json +2 -2
- package/dist/@types/builders/build-classify.d.ts.map +0 -1
- package/dist/@types/builders/builder-base.d.ts +0 -70
- package/dist/@types/builders/builder-base.d.ts.map +0 -1
- package/dist/@types/builders/reaction-builder.d.ts +0 -55
- package/dist/@types/builders/reaction-builder.d.ts.map +0 -1
- package/dist/@types/internal/close-signal.d.ts.map +0 -1
- package/dist/@types/internal/correlator.d.ts.map +0 -1
- package/dist/@types/internal/defer-signal.d.ts +0 -32
- package/dist/@types/internal/defer-signal.d.ts.map +0 -1
- package/dist/@types/internal/drain.d.ts.map +0 -1
- package/dist/@types/internal/index.d.ts +0 -38
- package/dist/@types/internal/index.d.ts.map +0 -1
- package/dist/@types/internal/sensitive.d.ts.map +0 -1
- package/dist/chunk-HHKLVRR4.js.map +0 -1
- package/dist/chunk-X3MMQIIP.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConsoleLogger,
|
|
3
|
-
|
|
4
|
-
|
|
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-
|
|
43
|
+
} from "./chunk-NYH6FNTM.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
|
-
|
|
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-
|
|
73
|
+
} from "./chunk-TJ4JRRUR.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,495 +651,68 @@ var PASS_FACTORIES = {
|
|
|
683
651
|
"unreadable-events": make_unreadable_events_pass
|
|
684
652
|
};
|
|
685
653
|
|
|
686
|
-
// src/internal/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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"
|
|
654
|
+
// src/internal/circuit-breaker.ts
|
|
655
|
+
var CircuitBreaker = class {
|
|
656
|
+
_failures = 0;
|
|
657
|
+
_opened_at;
|
|
658
|
+
_wake;
|
|
659
|
+
_threshold;
|
|
660
|
+
_cooldown_ms;
|
|
661
|
+
_hooks;
|
|
662
|
+
constructor(config2, hooks = {}) {
|
|
663
|
+
this._threshold = config2.failureThreshold;
|
|
664
|
+
this._cooldown_ms = config2.cooldownMs;
|
|
665
|
+
this._hooks = hooks;
|
|
719
666
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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"
|
|
667
|
+
/**
|
|
668
|
+
* True when the store has failed at least once since the last pass that
|
|
669
|
+
* went all the way through. The breaker can still read "closed" here: it
|
|
670
|
+
* only opens after several failures in a row, so this catches the first
|
|
671
|
+
* one, which `state()` cannot.
|
|
672
|
+
*/
|
|
673
|
+
get failing() {
|
|
674
|
+
return this._failures > 0;
|
|
731
675
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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));
|
|
676
|
+
/** How long the store is left alone after a failure before a retry. */
|
|
677
|
+
get cooldown_ms() {
|
|
678
|
+
return this._cooldown_ms;
|
|
765
679
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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;
|
|
680
|
+
/** Current state given the wall-clock `now`. */
|
|
681
|
+
state(now) {
|
|
682
|
+
if (this._opened_at === void 0) return "closed";
|
|
683
|
+
return now - this._opened_at >= this._cooldown_ms ? "half-open" : "open";
|
|
684
|
+
}
|
|
685
|
+
/** A store op passed — reset to closed and cancel any pending retry. */
|
|
686
|
+
passed() {
|
|
687
|
+
this._failures = 0;
|
|
688
|
+
this._opened_at = void 0;
|
|
689
|
+
this._clear_wake();
|
|
811
690
|
}
|
|
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
691
|
/**
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
692
|
+
* A store op failed. Opens the breaker when the consecutive-failure
|
|
693
|
+
* threshold is reached, or immediately re-opens (restarting the cooldown)
|
|
694
|
+
* if a half-open trial just failed. Returns the resulting state.
|
|
695
|
+
*
|
|
696
|
+
* Callers gate on the state directly — `state(now) === "open"` means skip.
|
|
697
|
+
* Every failure (re)schedules the `on_retry` wake, so one failed pass on
|
|
698
|
+
* an idle app retries without waiting for the breaker to open; always
|
|
699
|
+
* surfaces the failure via `on_error`.
|
|
832
700
|
*/
|
|
833
|
-
|
|
701
|
+
failed(now, error) {
|
|
702
|
+
const circuit = this._advance(now);
|
|
703
|
+
this._schedule_wake();
|
|
704
|
+
this._hooks.on_error?.(error, circuit);
|
|
705
|
+
return circuit;
|
|
706
|
+
}
|
|
707
|
+
/** Cancel the pending retry timer. Idempotent; call on shutdown. */
|
|
708
|
+
stop() {
|
|
709
|
+
this._clear_wake();
|
|
710
|
+
}
|
|
834
711
|
/**
|
|
835
|
-
*
|
|
836
|
-
* the
|
|
837
|
-
* the
|
|
838
|
-
*
|
|
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
|
-
// src/internal/circuit-breaker.ts
|
|
1114
|
-
var CircuitBreaker = class {
|
|
1115
|
-
_failures = 0;
|
|
1116
|
-
_opened_at;
|
|
1117
|
-
_wake;
|
|
1118
|
-
_threshold;
|
|
1119
|
-
_cooldown_ms;
|
|
1120
|
-
_hooks;
|
|
1121
|
-
constructor(config2, hooks = {}) {
|
|
1122
|
-
this._threshold = config2.failureThreshold;
|
|
1123
|
-
this._cooldown_ms = config2.cooldownMs;
|
|
1124
|
-
this._hooks = hooks;
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* True when the store has failed at least once since the last pass that
|
|
1128
|
-
* went all the way through. The breaker can still read "closed" here: it
|
|
1129
|
-
* only opens after several failures in a row, so this catches the first
|
|
1130
|
-
* one, which `state()` cannot.
|
|
1131
|
-
*/
|
|
1132
|
-
get failing() {
|
|
1133
|
-
return this._failures > 0;
|
|
1134
|
-
}
|
|
1135
|
-
/** How long the store is left alone after a failure before a retry. */
|
|
1136
|
-
get cooldown_ms() {
|
|
1137
|
-
return this._cooldown_ms;
|
|
1138
|
-
}
|
|
1139
|
-
/** Current state given the wall-clock `now`. */
|
|
1140
|
-
state(now) {
|
|
1141
|
-
if (this._opened_at === void 0) return "closed";
|
|
1142
|
-
return now - this._opened_at >= this._cooldown_ms ? "half-open" : "open";
|
|
1143
|
-
}
|
|
1144
|
-
/** A store op passed — reset to closed and cancel any pending retry. */
|
|
1145
|
-
passed() {
|
|
1146
|
-
this._failures = 0;
|
|
1147
|
-
this._opened_at = void 0;
|
|
1148
|
-
this._clear_wake();
|
|
1149
|
-
}
|
|
1150
|
-
/**
|
|
1151
|
-
* A store op failed. Opens the breaker when the consecutive-failure
|
|
1152
|
-
* threshold is reached, or immediately re-opens (restarting the cooldown)
|
|
1153
|
-
* if a half-open trial just failed. Returns the resulting state.
|
|
1154
|
-
*
|
|
1155
|
-
* Callers gate on the state directly — `state(now) === "open"` means skip.
|
|
1156
|
-
* Every failure (re)schedules the `on_retry` wake, so one failed pass on
|
|
1157
|
-
* an idle app retries without waiting for the breaker to open; always
|
|
1158
|
-
* surfaces the failure via `on_error`.
|
|
1159
|
-
*/
|
|
1160
|
-
failed(now, error) {
|
|
1161
|
-
const circuit = this._advance(now);
|
|
1162
|
-
this._schedule_wake();
|
|
1163
|
-
this._hooks.on_error?.(error, circuit);
|
|
1164
|
-
return circuit;
|
|
1165
|
-
}
|
|
1166
|
-
/** Cancel the pending retry timer. Idempotent; call on shutdown. */
|
|
1167
|
-
stop() {
|
|
1168
|
-
this._clear_wake();
|
|
1169
|
-
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Schedule the `on_retry` wake `cooldownMs` out so the breaker re-trials
|
|
1172
|
-
* the store on its own. No-op when no `on_retry` hook is wired (e.g. unit
|
|
1173
|
-
* tests of the pure state machine). The timer is `unref`'d so it never
|
|
1174
|
-
* keeps the process alive.
|
|
712
|
+
* Schedule the `on_retry` wake `cooldownMs` out so the breaker re-trials
|
|
713
|
+
* the store on its own. No-op when no `on_retry` hook is wired (e.g. unit
|
|
714
|
+
* tests of the pure state machine). The timer is `unref`'d so it never
|
|
715
|
+
* keeps the process alive.
|
|
1175
716
|
*/
|
|
1176
717
|
_schedule_wake() {
|
|
1177
718
|
if (!this._hooks.on_retry) return;
|
|
@@ -1203,6 +744,55 @@ var CircuitBreaker = class {
|
|
|
1203
744
|
}
|
|
1204
745
|
};
|
|
1205
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);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
static_targets: [...statics.values()],
|
|
791
|
+
event_to_state,
|
|
792
|
+
event_to_lanes
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
1206
796
|
// src/internal/close-cycle.ts
|
|
1207
797
|
var CLOSE_CATCH_UP_LIMIT = 1e3;
|
|
1208
798
|
var CLOSE_CATCH_UP_PASSES = 20;
|
|
@@ -1862,7 +1452,7 @@ var CorrelateCycle = class {
|
|
|
1862
1452
|
}
|
|
1863
1453
|
};
|
|
1864
1454
|
|
|
1865
|
-
// src/internal/
|
|
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/
|
|
1890
|
-
import {
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
if (!
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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/
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
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 {
|
|
3067
2658
|
/**
|
|
3068
|
-
*
|
|
3069
|
-
*
|
|
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.
|
|
3070
2662
|
*/
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
_default_debounce_ms;
|
|
3075
|
-
constructor(deps, default_debounce_ms) {
|
|
3076
|
-
this._deps = deps;
|
|
3077
|
-
this._default_debounce_ms = default_debounce_ms;
|
|
3078
|
-
}
|
|
2663
|
+
stream;
|
|
2664
|
+
/** Optional archive callback to run while the stream is guarded. */
|
|
2665
|
+
archive;
|
|
3079
2666
|
/**
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3084
|
-
*
|
|
3085
|
-
*
|
|
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.
|
|
3086
2673
|
*/
|
|
3087
|
-
|
|
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
|
-
}
|
|
2674
|
+
at;
|
|
3141
2675
|
/**
|
|
3142
|
-
*
|
|
3143
|
-
*
|
|
3144
|
-
*
|
|
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.
|
|
3145
2680
|
*/
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
this.
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
this._timer = void 0;
|
|
3155
|
-
}
|
|
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,12 +2782,132 @@ function build_handle_batch(logger) {
|
|
|
3251
2782
|
};
|
|
3252
2783
|
}
|
|
3253
2784
|
|
|
3254
|
-
// src/
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
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
|
+
|
|
2905
|
+
// src/act.ts
|
|
2906
|
+
function validate_only_lanes(options, lanes) {
|
|
2907
|
+
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
2908
|
+
const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
|
|
2909
|
+
const unknown = options.onlyLanes.filter((l) => !declared.has(l));
|
|
2910
|
+
if (unknown.length > 0)
|
|
3260
2911
|
throw new Error(
|
|
3261
2912
|
`ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
|
|
3262
2913
|
);
|
|
@@ -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
|
|
3276
|
-
|
|
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(
|
|
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
|
|
@@ -3533,21 +3187,14 @@ var Act = class {
|
|
|
3533
3187
|
}
|
|
3534
3188
|
_shutdown_promise;
|
|
3535
3189
|
/**
|
|
3536
|
-
* Per-instance teardown:
|
|
3537
|
-
*
|
|
3538
|
-
* listeners and
|
|
3539
|
-
*
|
|
3540
|
-
* Scheduling stops first, then in-flight cycles get up to `graceMs` to
|
|
3541
|
-
* reach their `ack` (an abandoned handler holds its lease until it
|
|
3542
|
-
* expires). Listeners come off after the wait, so they still see work that
|
|
3543
|
-
* finished during it. The budget is a ceiling, not a delay; when it runs
|
|
3544
|
-
* out, teardown proceeds anyway.
|
|
3190
|
+
* Per-instance teardown: stops scheduling, gives the cycles in flight up
|
|
3191
|
+
* to `graceMs` to reach their `ack` (an abandoned handler holds its lease
|
|
3192
|
+
* until it expires), then removes listeners and the notify subscription.
|
|
3193
|
+
* The budget is a ceiling, not a delay.
|
|
3545
3194
|
*
|
|
3546
|
-
* Idempotent
|
|
3547
|
-
* call's `graceMs
|
|
3548
|
-
*
|
|
3549
|
-
* `dispose()()` covers it; test helpers (or operators that mint
|
|
3550
|
-
* short-lived Acts) call it explicitly for prompt cleanup.
|
|
3195
|
+
* Idempotent: repeated calls return the same promise, with the first
|
|
3196
|
+
* call's `graceMs`. Registered with the global `dispose()` registry, so
|
|
3197
|
+
* `dispose()()` covers it; call it directly for prompt cleanup.
|
|
3551
3198
|
*
|
|
3552
3199
|
* @param options - See {@link ShutdownOptions}. Defaults the grace budget
|
|
3553
3200
|
* to the largest lane `leaseMillis` (capped at 30s).
|
|
@@ -3559,7 +3206,7 @@ var Act = class {
|
|
|
3559
3206
|
this.stop_correlations();
|
|
3560
3207
|
const disposer = await this._notify_disposer;
|
|
3561
3208
|
if (disposer) await disposer();
|
|
3562
|
-
this._settle.stop();
|
|
3209
|
+
this._settle.stop(true);
|
|
3563
3210
|
this._breaker.stop();
|
|
3564
3211
|
for (const c of this._drain_controllers.values()) c.stop();
|
|
3565
3212
|
await this._await_inflight(options?.graceMs);
|
|
@@ -3613,7 +3260,7 @@ var Act = class {
|
|
|
3613
3260
|
const armed = this._arm_for_event_names(
|
|
3614
3261
|
notification.events.map((e) => e.name)
|
|
3615
3262
|
);
|
|
3616
|
-
|
|
3263
|
+
this._settle_armed(armed);
|
|
3617
3264
|
}
|
|
3618
3265
|
} catch (err) {
|
|
3619
3266
|
this._logger.error(err, "notified handler threw");
|
|
@@ -3625,15 +3272,10 @@ var Act = class {
|
|
|
3625
3272
|
}
|
|
3626
3273
|
}
|
|
3627
3274
|
/**
|
|
3628
|
-
* Executes an action
|
|
3629
|
-
*
|
|
3630
|
-
*
|
|
3631
|
-
*
|
|
3632
|
-
* 2. Loads the current state snapshot
|
|
3633
|
-
* 3. Checks invariants (business rules)
|
|
3634
|
-
* 4. Executes the action handler to generate events
|
|
3635
|
-
* 5. Applies events to create new state
|
|
3636
|
-
* 6. Commits events to the store with optimistic concurrency control
|
|
3275
|
+
* Executes an action: validates the payload, loads the state, checks its
|
|
3276
|
+
* invariants, and commits the events its handler emits, with optimistic
|
|
3277
|
+
* concurrency on the stream. The reactions to those events settle on
|
|
3278
|
+
* their own (unless `autoSettle: false`).
|
|
3637
3279
|
*
|
|
3638
3280
|
* @template TKey - Action name from registered actions
|
|
3639
3281
|
* @param action - The name of the action to execute
|
|
@@ -3677,9 +3319,10 @@ var Act = class {
|
|
|
3677
3319
|
payload,
|
|
3678
3320
|
do_options
|
|
3679
3321
|
);
|
|
3680
|
-
this._arm_for_event_names(
|
|
3322
|
+
const armed = this._arm_for_event_names(
|
|
3681
3323
|
snapshots.map((s) => s.event.name)
|
|
3682
3324
|
);
|
|
3325
|
+
this._settle_armed(armed);
|
|
3683
3326
|
this.emit("committed", snapshots);
|
|
3684
3327
|
return snapshots;
|
|
3685
3328
|
});
|
|
@@ -3711,9 +3354,7 @@ var Act = class {
|
|
|
3711
3354
|
*
|
|
3712
3355
|
* For small result sets, consider using {@link query_array} instead.
|
|
3713
3356
|
*
|
|
3714
|
-
* @param query - Filter criteria
|
|
3715
|
-
* (`stream`, `name`, `after`, `before`, `created_after`, `created_before`,
|
|
3716
|
-
* `limit`, `with_snaps`, `stream_exact`)
|
|
3357
|
+
* @param query - Filter criteria; see {@link Query} for the fields
|
|
3717
3358
|
* @param callback - Optional callback invoked for each matching event
|
|
3718
3359
|
* @returns Object with first event, last event, and total count
|
|
3719
3360
|
*
|
|
@@ -3797,31 +3438,23 @@ var Act = class {
|
|
|
3797
3438
|
});
|
|
3798
3439
|
}
|
|
3799
3440
|
/**
|
|
3800
|
-
*
|
|
3441
|
+
* Runs one drain cycle: claims streams with marked work (leasing them),
|
|
3442
|
+
* fetches their events, runs the matching reactions, and acks or blocks
|
|
3443
|
+
* each stream.
|
|
3801
3444
|
*
|
|
3802
|
-
*
|
|
3803
|
-
*
|
|
3804
|
-
*
|
|
3805
|
-
*
|
|
3806
|
-
*
|
|
3807
|
-
* 5. Acknowledges successful reactions or blocks failing ones
|
|
3808
|
-
*
|
|
3809
|
-
* Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
|
|
3810
|
-
* vs active streams (leading). The ratio adapts based on event pressure.
|
|
3811
|
-
*
|
|
3812
|
-
* Call `correlate()` before `drain()`. It is not only how dynamic targets
|
|
3813
|
-
* are discovered: a stream is claimable while `at < correlated_at`, and
|
|
3814
|
-
* `correlate` is the only component that raises that mark, so a
|
|
3815
|
-
* commit no correlate has seen is not drainable, static targets included. For a
|
|
3816
|
-
* higher-level API that handles debouncing, correlation, and signaling
|
|
3817
|
-
* automatically, use {@link settle}.
|
|
3445
|
+
* A stream is claimable only once `correlate()` has marked it, static
|
|
3446
|
+
* targets included, so a commit no correlate has seen is not drainable.
|
|
3447
|
+
* Apps rarely call this: each commit settles its reactions on its own.
|
|
3448
|
+
* With `autoSettle: false`, tests walk the pipeline with `correlate()`
|
|
3449
|
+
* then `drain()` (see the testing guide).
|
|
3818
3450
|
*
|
|
3819
3451
|
* @param options - Drain configuration — see {@link DrainOptions} for fields
|
|
3820
3452
|
* (`streamLimit`, `eventLimit`, `leaseMillis`).
|
|
3821
3453
|
* @returns Drain statistics with fetched, leased, acked, and blocked counts
|
|
3822
3454
|
*
|
|
3823
|
-
* @example
|
|
3455
|
+
* @example Walking the pipeline in a test
|
|
3824
3456
|
* ```typescript
|
|
3457
|
+
* const app = act().withState(User).withProjection(Users).build({ autoSettle: false });
|
|
3825
3458
|
* await app.do("createUser", target, payload);
|
|
3826
3459
|
* await app.correlate();
|
|
3827
3460
|
* await app.drain();
|
|
@@ -3829,7 +3462,6 @@ var Act = class {
|
|
|
3829
3462
|
*
|
|
3830
3463
|
* @see {@link settle} for debounced correlate→drain with lifecycle events
|
|
3831
3464
|
* @see {@link correlate} for dynamic stream discovery
|
|
3832
|
-
* @see {@link start_correlations} for automatic correlation
|
|
3833
3465
|
*/
|
|
3834
3466
|
async drain(options = {}) {
|
|
3835
3467
|
resolveDrainConfig(options);
|
|
@@ -3842,6 +3474,18 @@ var Act = class {
|
|
|
3842
3474
|
this._correlate.arm();
|
|
3843
3475
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
3844
3476
|
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Settles what a change armed (a commit, a reset, an unblock, a restore,
|
|
3479
|
+
* a notification from another process), unless the app walks the
|
|
3480
|
+
* pipeline itself (`autoSettle: false`).
|
|
3481
|
+
*/
|
|
3482
|
+
_settle_armed(armed) {
|
|
3483
|
+
if (armed && this._auto_settle) this._schedule();
|
|
3484
|
+
}
|
|
3485
|
+
/** Schedules a settle pass; writer-only instances (`drain: false`) skip it. */
|
|
3486
|
+
_schedule(options) {
|
|
3487
|
+
if (this._drain) this._settle.schedule(options);
|
|
3488
|
+
}
|
|
3845
3489
|
/** Arms only the lanes these events react on; true when any was armed. */
|
|
3846
3490
|
_arm_for_event_names(names) {
|
|
3847
3491
|
const to_arm = /* @__PURE__ */ new Set();
|
|
@@ -3884,32 +3528,20 @@ var Act = class {
|
|
|
3884
3528
|
return { fetched, leased, acked, blocked };
|
|
3885
3529
|
}
|
|
3886
3530
|
/**
|
|
3887
|
-
*
|
|
3888
|
-
*
|
|
3889
|
-
*
|
|
3890
|
-
* based on event content. For example, you might create a stats stream for each user
|
|
3891
|
-
* when they perform certain actions.
|
|
3892
|
-
*
|
|
3893
|
-
* This method scans events matching the query and identifies new target streams based
|
|
3894
|
-
* on reaction resolvers. It then registers these streams so they'll be picked up by
|
|
3895
|
-
* the next drain cycle.
|
|
3531
|
+
* Scans committed events from the correlation checkpoint, subscribes the
|
|
3532
|
+
* target streams their reactions resolve to (static or computed from
|
|
3533
|
+
* the event), and marks their work so the next drain can claim it.
|
|
3896
3534
|
*
|
|
3897
3535
|
* @param query - Scan filter — see {@link Query} for fields (typically
|
|
3898
|
-
* `{
|
|
3536
|
+
* `{ limit: <count> }`; the scan starts at the checkpoint)
|
|
3899
3537
|
* @returns The number of newly subscribed streams and the last scanned event id
|
|
3900
3538
|
*
|
|
3901
3539
|
* @example Manual correlation
|
|
3902
3540
|
* ```typescript
|
|
3903
|
-
*
|
|
3904
|
-
* const { subscribed, last_id } = await app.correlate({ after: 0, limit: 100 });
|
|
3905
|
-
* console.log(`Subscribed ${subscribed} new streams`);
|
|
3906
|
-
*
|
|
3907
|
-
* // Save last_id for next scan
|
|
3908
|
-
* await saveCheckpoint(last_id);
|
|
3541
|
+
* const { subscribed } = await app.correlate({ limit: 100 });
|
|
3909
3542
|
* ```
|
|
3910
3543
|
*
|
|
3911
3544
|
* @see {@link start_correlations} for automatic periodic correlation
|
|
3912
|
-
* @see {@link stop_correlations} to stop automatic correlation
|
|
3913
3545
|
*/
|
|
3914
3546
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
3915
3547
|
const { subscribed, last_id } = await this._correlate_scanned(query);
|
|
@@ -3921,7 +3553,6 @@ var Act = class {
|
|
|
3921
3553
|
return this._scoped(async () => {
|
|
3922
3554
|
const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query, lease);
|
|
3923
3555
|
if (subscribed > 0 || marked > 0) this._arm_all();
|
|
3924
|
-
if (subscribed > 0) this._on_discovered?.(subscribed);
|
|
3925
3556
|
return { subscribed, last_id, scanned };
|
|
3926
3557
|
});
|
|
3927
3558
|
}
|
|
@@ -3941,9 +3572,6 @@ var Act = class {
|
|
|
3941
3572
|
* reads (default 100). The scan always starts at the correlation
|
|
3942
3573
|
* checkpoint, so `after` is ignored.
|
|
3943
3574
|
* @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
3575
|
* @returns `true` if the poll started, `false` if one is already running
|
|
3948
3576
|
*
|
|
3949
3577
|
* @example
|
|
@@ -3956,13 +3584,12 @@ var Act = class {
|
|
|
3956
3584
|
* @see {@link settle} for what each tick runs
|
|
3957
3585
|
* @see {@link stop_correlations} to stop the poll
|
|
3958
3586
|
*/
|
|
3959
|
-
start_correlations(query = {}, frequency = 1e4
|
|
3587
|
+
start_correlations(query = {}, frequency = 1e4) {
|
|
3960
3588
|
if (this._poll) return false;
|
|
3961
|
-
this.
|
|
3962
|
-
const correlate = { limit: query.limit || 100 };
|
|
3589
|
+
this._poll_limit = query.limit || void 0;
|
|
3963
3590
|
this._poll = setInterval(() => {
|
|
3964
3591
|
this._correlate.arm();
|
|
3965
|
-
this.
|
|
3592
|
+
this._schedule();
|
|
3966
3593
|
}, frequency);
|
|
3967
3594
|
return true;
|
|
3968
3595
|
}
|
|
@@ -3976,12 +3603,13 @@ var Act = class {
|
|
|
3976
3603
|
if (this._poll) {
|
|
3977
3604
|
clearInterval(this._poll);
|
|
3978
3605
|
this._poll = void 0;
|
|
3606
|
+
this._poll_limit = void 0;
|
|
3979
3607
|
}
|
|
3980
|
-
this._on_discovered = void 0;
|
|
3981
3608
|
void this._scoped(() => this._correlate.release_correlation());
|
|
3982
3609
|
}
|
|
3983
3610
|
/**
|
|
3984
|
-
*
|
|
3611
|
+
* Drops the settle pass scheduled but not yet started. A cycle already
|
|
3612
|
+
* running finishes; later changes schedule new passes.
|
|
3985
3613
|
*
|
|
3986
3614
|
* @see {@link settle}
|
|
3987
3615
|
*/
|
|
@@ -3989,19 +3617,17 @@ var Act = class {
|
|
|
3989
3617
|
this._settle.stop();
|
|
3990
3618
|
}
|
|
3991
3619
|
/**
|
|
3992
|
-
* Reset reaction stream watermarks and
|
|
3993
|
-
*
|
|
3620
|
+
* Reset reaction stream watermarks and settle them, replaying their
|
|
3621
|
+
* events through their reactions.
|
|
3994
3622
|
*
|
|
3995
3623
|
* Use this to replay events through projections (or other reaction targets)
|
|
3996
|
-
* after changing handler logic.
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
3999
|
-
*
|
|
4000
|
-
*
|
|
4001
|
-
*
|
|
4002
|
-
*
|
|
4003
|
-
* fully catches up paginated streams without forcing callers to roll
|
|
4004
|
-
* their own loop.
|
|
3624
|
+
* after changing handler logic. It rewinds the same watermarks as
|
|
3625
|
+
* `store().reset(streams)`, and also wakes the drain and schedules a
|
|
3626
|
+
* settle, which loops correlate→drain until the replay catches up;
|
|
3627
|
+
* `store().reset(...)` alone leaves a settled app idle. Call
|
|
3628
|
+
* `app.settle(options)` right after to give that pass batch options, or,
|
|
3629
|
+
* with `autoSettle: false`, `correlate()` / `drain()` to drive the replay
|
|
3630
|
+
* by hand.
|
|
4005
3631
|
*
|
|
4006
3632
|
* @param input - Reaction target streams (e.g., projection names) to reset, or a {@link StreamFilter} for bulk operations
|
|
4007
3633
|
* @returns Count of streams that were actually reset
|
|
@@ -4009,7 +3635,7 @@ var Act = class {
|
|
|
4009
3635
|
* @example Rebuild a projection (production)
|
|
4010
3636
|
* ```typescript
|
|
4011
3637
|
* await app.reset(["my-projection"]);
|
|
4012
|
-
* app.settle({ eventLimit: 1000 });
|
|
3638
|
+
* app.settle({ eventLimit: 1000 }); // optional: bigger batches; emits "settled"
|
|
4013
3639
|
* ```
|
|
4014
3640
|
*
|
|
4015
3641
|
* @see {@link Store.reset} for the underlying store primitive
|
|
@@ -4020,7 +3646,10 @@ var Act = class {
|
|
|
4020
3646
|
const count = await store().reset(input);
|
|
4021
3647
|
for (const handler of this._batch_handlers.values())
|
|
4022
3648
|
handler[FOLD_RESET]?.();
|
|
4023
|
-
if (count > 0)
|
|
3649
|
+
if (count > 0) {
|
|
3650
|
+
this._arm_all();
|
|
3651
|
+
this._settle_armed(this._reactive);
|
|
3652
|
+
}
|
|
4024
3653
|
return count;
|
|
4025
3654
|
});
|
|
4026
3655
|
}
|
|
@@ -4033,11 +3662,9 @@ var Act = class {
|
|
|
4033
3662
|
* {@link reset}, which rebuilds from event 0 (suitable for projection
|
|
4034
3663
|
* rebuilds, wrong for "I fixed the bug, please retry").
|
|
4035
3664
|
*
|
|
4036
|
-
* Wraps `store().unblock(streams)` and
|
|
4037
|
-
*
|
|
4038
|
-
*
|
|
4039
|
-
* directly, but `store().unblock(...)` alone leaves the flag
|
|
4040
|
-
* untouched.
|
|
3665
|
+
* Wraps `store().unblock(streams)` and settles the now-free streams on
|
|
3666
|
+
* its own. `store().unblock(...)` alone leaves them waiting for the next
|
|
3667
|
+
* commit.
|
|
4041
3668
|
*
|
|
4042
3669
|
* @param input - Stream names to unblock, or a {@link StreamFilter} for bulk recovery
|
|
4043
3670
|
* @returns Count of streams that were actually flipped (were blocked)
|
|
@@ -4054,7 +3681,10 @@ var Act = class {
|
|
|
4054
3681
|
async unblock(input) {
|
|
4055
3682
|
return this._scoped(async () => {
|
|
4056
3683
|
const count = await store().unblock(input);
|
|
4057
|
-
if (count > 0)
|
|
3684
|
+
if (count > 0) {
|
|
3685
|
+
this._arm_all();
|
|
3686
|
+
this._settle_armed(this._reactive);
|
|
3687
|
+
}
|
|
4058
3688
|
return count;
|
|
4059
3689
|
});
|
|
4060
3690
|
}
|
|
@@ -4116,223 +3746,377 @@ var Act = class {
|
|
|
4116
3746
|
if (!sink) {
|
|
4117
3747
|
this._correlate.restart();
|
|
4118
3748
|
this._arm_all();
|
|
3749
|
+
this._settle_armed(this._reactive);
|
|
3750
|
+
}
|
|
3751
|
+
return { kept, migrated, dropped, duration_ms: Date.now() - started };
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Return every currently-blocked stream position. Convenience wrapper
|
|
3756
|
+
* around `store().query_streams(cb, { blocked: true })` for the common
|
|
3757
|
+
* "show me what's broken" operational query.
|
|
3758
|
+
*
|
|
3759
|
+
* Results are ordered by stream name, paginated by `limit` (default
|
|
3760
|
+
* 100). Pass `after` to fetch the next page (keyset cursor on the
|
|
3761
|
+
* stream name). For richer queries — including blocked + source
|
|
3762
|
+
* filters, or full unblocked introspection — drop to
|
|
3763
|
+
* `store().query_streams(...)` directly.
|
|
3764
|
+
*
|
|
3765
|
+
* @returns Array of {@link StreamPosition} for currently-blocked streams.
|
|
3766
|
+
*
|
|
3767
|
+
* @example Discover and recover
|
|
3768
|
+
* ```typescript
|
|
3769
|
+
* const blocked = await app.blocked_streams();
|
|
3770
|
+
* console.table(blocked.map(({ stream, retry, error }) => ({ stream, retry, error })));
|
|
3771
|
+
*
|
|
3772
|
+
* // Operator investigates, then bulk-unblocks the family:
|
|
3773
|
+
* await app.unblock({ stream: "^webhooks-out-" });
|
|
3774
|
+
* ```
|
|
3775
|
+
*/
|
|
3776
|
+
async blocked_streams(options) {
|
|
3777
|
+
return this._scoped(async () => {
|
|
3778
|
+
const positions = [];
|
|
3779
|
+
await store().query_streams(
|
|
3780
|
+
(p) => {
|
|
3781
|
+
positions.push(p);
|
|
3782
|
+
},
|
|
3783
|
+
{ blocked: true, after: options?.after, limit: options?.limit }
|
|
3784
|
+
);
|
|
3785
|
+
return positions;
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
/**
|
|
3789
|
+
* Operator-driven store audit: walks the store and yields findings per
|
|
3790
|
+
* category, each with the remediation it suggests. Never run by the
|
|
3791
|
+
* framework. Pass a subset of categories to scope the work; the iterable
|
|
3792
|
+
* can be broken early, and findings stream without buffering the report.
|
|
3793
|
+
*
|
|
3794
|
+
* @param categories - Subset of categories to run (default: all).
|
|
3795
|
+
* @param options - Query window + per-category thresholds.
|
|
3796
|
+
* @returns Async iterable of {@link AuditFinding}.
|
|
3797
|
+
*
|
|
3798
|
+
* @example Schema drift since the last scan
|
|
3799
|
+
* ```typescript
|
|
3800
|
+
* for await (const f of app.audit(["schema"], { query: { created_after: lastScan } }))
|
|
3801
|
+
* await escalate(f);
|
|
3802
|
+
* ```
|
|
3803
|
+
*/
|
|
3804
|
+
async *audit(categories, options) {
|
|
3805
|
+
const it = audit(this._audit_deps, categories, options)[Symbol.asyncIterator]();
|
|
3806
|
+
while (true) {
|
|
3807
|
+
const { value, done } = await this._scoped(() => it.next());
|
|
3808
|
+
if (done) break;
|
|
3809
|
+
yield value;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Sets the scheduling priority of every stream matching `filter`, as-is:
|
|
3814
|
+
* unlike build-time priorities (the highest across reactions wins), this
|
|
3815
|
+
* overrides them. Useful to let a long replay jump ahead of other lagging
|
|
3816
|
+
* streams, or to yield slots back. **An empty filter (`{}`) updates every
|
|
3817
|
+
* registered stream.**
|
|
3818
|
+
*
|
|
3819
|
+
* @param filter - Selection criteria (regex by default, like {@link query}).
|
|
3820
|
+
* @param priority - New priority value. Set as-is — no clamp.
|
|
3821
|
+
* @returns Count of streams whose priority changed.
|
|
3822
|
+
*
|
|
3823
|
+
* @example Boost a specific projection mid-replay
|
|
3824
|
+
* ```typescript
|
|
3825
|
+
* await app.prioritize({ stream: "^proj-orders$", stream_exact: false }, 10);
|
|
3826
|
+
* ```
|
|
3827
|
+
*
|
|
3828
|
+
* @see {@link Store.prioritize} for the underlying primitive
|
|
3829
|
+
*/
|
|
3830
|
+
async prioritize(filter, priority) {
|
|
3831
|
+
return this._scoped(() => store().prioritize(filter, priority));
|
|
3832
|
+
}
|
|
3833
|
+
/** Shared by `close()` and reaction-requested closes. */
|
|
3834
|
+
async _run_close(targets) {
|
|
3835
|
+
const close_actor = { id: "$close", name: "close" };
|
|
3836
|
+
const result = await run_close_cycle(targets, {
|
|
3837
|
+
reactive: this._reactive,
|
|
3838
|
+
catch_up_correlation: (until) => catch_up_correlation(
|
|
3839
|
+
this._correlate,
|
|
3840
|
+
(limit) => this.correlate({ limit }),
|
|
3841
|
+
until
|
|
3842
|
+
),
|
|
3843
|
+
event_to_state: this._event_to_state,
|
|
3844
|
+
load: this._es.load,
|
|
3845
|
+
tombstone: this._es.tombstone,
|
|
3846
|
+
logger: this._logger,
|
|
3847
|
+
correlation: close_correlation(this._correlator, close_actor),
|
|
3848
|
+
with_stream_lock: (stream, work) => this._close_lock.run(stream, work),
|
|
3849
|
+
forget_subscribed: (streams) => this._correlate.forget_subscribed(streams)
|
|
3850
|
+
});
|
|
3851
|
+
this.emit("closed", result);
|
|
3852
|
+
return result;
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Close the books: guard, archive, truncate, and optionally restart
|
|
3856
|
+
* streams. A stream with pending reactions is skipped; an archive
|
|
3857
|
+
* failure aborts the whole close while the streams are still guarded.
|
|
3858
|
+
* Targets carrying `before` prune a rolling window instead and leave the
|
|
3859
|
+
* stream live (needs `.snap(...)`). The phases and their failure
|
|
3860
|
+
* semantics are in the close-cycle architecture page.
|
|
3861
|
+
*
|
|
3862
|
+
* @param targets - Per-stream close options (stream, restart?, archive?, before?)
|
|
3863
|
+
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
3864
|
+
*
|
|
3865
|
+
* @example Archive and close
|
|
3866
|
+
* ```typescript
|
|
3867
|
+
* await app.close([
|
|
3868
|
+
* { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
|
|
3869
|
+
* { stream: "order-456" },
|
|
3870
|
+
* ]);
|
|
3871
|
+
* ```
|
|
3872
|
+
*/
|
|
3873
|
+
async close(targets) {
|
|
3874
|
+
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
3875
|
+
return this._scoped(() => this._run_close(targets));
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Debounced, non-blocking correlate→drain cycle.
|
|
3879
|
+
*
|
|
3880
|
+
* A local change that frees reaction work (a commit, `reset()`,
|
|
3881
|
+
* `unblock()`, `restore()`) schedules this on its own unless the app
|
|
3882
|
+
* sets `autoSettle: false`. Call it at startup, to run what was pending
|
|
3883
|
+
* before a restart, or with options for the next pass. A pass already
|
|
3884
|
+
* scheduled is never put off: calls before it runs join it (a shorter
|
|
3885
|
+
* `debounceMs` brings it forward), and a call during a running cycle
|
|
3886
|
+
* queues one more pass after it. Runs correlate→drain in a loop until a pass makes no
|
|
3887
|
+
* progress — no new subscriptions, no events read, no streams leased —
|
|
3888
|
+
* then emits the `"settled"` lifecycle event, so one call fully catches
|
|
3889
|
+
* up paginated streams without forcing callers to loop.
|
|
3890
|
+
*
|
|
3891
|
+
* @param options - Settle configuration — see {@link SettleOptions} for fields:
|
|
3892
|
+
* `debounceMs` (default 10), `correlate.limit` (default 100, or the
|
|
3893
|
+
* poll's limit while `start_correlations` runs; `after` is always the
|
|
3894
|
+
* checkpoint),
|
|
3895
|
+
* `maxPasses` (default `Infinity` — kill-switch for runaway loops),
|
|
3896
|
+
* `streamLimit` (default 10), `eventLimit` (default 10),
|
|
3897
|
+
* `leaseMillis` (default 10000).
|
|
3898
|
+
*
|
|
3899
|
+
* @example Startup
|
|
3900
|
+
* ```typescript
|
|
3901
|
+
* app.settle(); // run what was pending before the restart
|
|
3902
|
+
*
|
|
3903
|
+
* app.on("settled", (drain) => {
|
|
3904
|
+
* // notify SSE clients, invalidate caches, etc.
|
|
3905
|
+
* });
|
|
3906
|
+
* ```
|
|
3907
|
+
*
|
|
3908
|
+
* @see {@link drain} for single synchronous drain cycles
|
|
3909
|
+
* @see {@link correlate} for manual correlation
|
|
3910
|
+
*/
|
|
3911
|
+
settle(options) {
|
|
3912
|
+
resolveSettleConfig(options ?? {});
|
|
3913
|
+
this._schedule(options);
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
|
|
3917
|
+
// src/internal/autoclose-policy.ts
|
|
3918
|
+
import { z as z2 } from "zod";
|
|
3919
|
+
var DAY_MS = 864e5;
|
|
3920
|
+
var MIN_AFTER_DAYS = 1 / 1440;
|
|
3921
|
+
var MIN_KEEP_DAYS = 1;
|
|
3922
|
+
function days_after(date, days) {
|
|
3923
|
+
return new Date(date.getTime() + days * DAY_MS);
|
|
3924
|
+
}
|
|
3925
|
+
function days_before_now(days) {
|
|
3926
|
+
return new Date(Date.now() - days * DAY_MS);
|
|
3927
|
+
}
|
|
3928
|
+
var window_schema = (field, min_days, floor) => z2.object({
|
|
3929
|
+
days: z2.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
|
|
3930
|
+
}).refine((o) => o.days >= min_days, {
|
|
3931
|
+
message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
|
|
3932
|
+
});
|
|
3933
|
+
var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
|
|
3934
|
+
var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
|
|
3935
|
+
var IsSchema = z2.union([
|
|
3936
|
+
z2.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
3937
|
+
z2.array(
|
|
3938
|
+
z2.string().min(1, "autocloses: is entries must be non-empty event names")
|
|
3939
|
+
).min(1, "autocloses: is must include at least one event name").readonly()
|
|
3940
|
+
]);
|
|
3941
|
+
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");
|
|
3942
|
+
var OrBlockSchema = z2.object({
|
|
3943
|
+
after: AfterSchema.optional(),
|
|
3944
|
+
is: IsSchema.optional(),
|
|
3945
|
+
reaches: ReachesSchema.optional()
|
|
3946
|
+
}).strict().refine(
|
|
3947
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
3948
|
+
{
|
|
3949
|
+
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
3950
|
+
}
|
|
3951
|
+
);
|
|
3952
|
+
var AutoclosePolicySchema = z2.object({
|
|
3953
|
+
after: AfterSchema.optional(),
|
|
3954
|
+
is: IsSchema.optional(),
|
|
3955
|
+
reaches: ReachesSchema.optional(),
|
|
3956
|
+
or: OrBlockSchema.optional(),
|
|
3957
|
+
keep: KeepSchema.optional()
|
|
3958
|
+
}).strict().refine(
|
|
3959
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
|
|
3960
|
+
{
|
|
3961
|
+
message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
|
|
3962
|
+
}
|
|
3963
|
+
);
|
|
3964
|
+
function compile_after(after) {
|
|
3965
|
+
return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
|
|
3966
|
+
}
|
|
3967
|
+
function compile_is(is) {
|
|
3968
|
+
const set = new Set(typeof is === "string" ? [is] : is);
|
|
3969
|
+
return (_stream, head) => set.has(head.name);
|
|
3970
|
+
}
|
|
3971
|
+
function compile_reaches(reaches) {
|
|
3972
|
+
return (_stream, _head, count) => count >= reaches;
|
|
3973
|
+
}
|
|
3974
|
+
function policy_min_after_days(options) {
|
|
3975
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
3976
|
+
const windows = [];
|
|
3977
|
+
if (parsed.after) windows.push(parsed.after.days);
|
|
3978
|
+
if (parsed.or?.after) windows.push(parsed.or.after.days);
|
|
3979
|
+
return windows.length ? Math.min(...windows) : void 0;
|
|
3980
|
+
}
|
|
3981
|
+
function policy_keep_days(options) {
|
|
3982
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
3983
|
+
return parsed.keep?.days;
|
|
3984
|
+
}
|
|
3985
|
+
function compile_autoclose_policy(options) {
|
|
3986
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
3987
|
+
const and_preds = [];
|
|
3988
|
+
if (parsed.after) and_preds.push(compile_after(parsed.after));
|
|
3989
|
+
if (parsed.is) and_preds.push(compile_is(parsed.is));
|
|
3990
|
+
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
3991
|
+
const or_preds = [];
|
|
3992
|
+
if (parsed.or) {
|
|
3993
|
+
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
3994
|
+
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
3995
|
+
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
3996
|
+
}
|
|
3997
|
+
return (stream, head, count) => {
|
|
3998
|
+
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
3999
|
+
return true;
|
|
4000
|
+
}
|
|
4001
|
+
if (or_preds.some((p) => p(stream, head, count))) {
|
|
4002
|
+
return true;
|
|
4003
|
+
}
|
|
4004
|
+
return false;
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// src/internal/autoclose-window.ts
|
|
4009
|
+
function hour_in_zone(now, timeZone) {
|
|
4010
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
4011
|
+
timeZone,
|
|
4012
|
+
hour: "2-digit",
|
|
4013
|
+
hourCycle: "h23"
|
|
4014
|
+
}).formatToParts(now);
|
|
4015
|
+
return Number(parts.find((p) => p.type === "hour")?.value);
|
|
4016
|
+
}
|
|
4017
|
+
function hour_in_range(hour, start, end) {
|
|
4018
|
+
return start < end ? hour >= start && hour < end : hour >= start || hour < end;
|
|
4019
|
+
}
|
|
4020
|
+
function is_dst_gap_open(now, timeZone, start) {
|
|
4021
|
+
const hour = hour_in_zone(now, timeZone);
|
|
4022
|
+
if (hour <= start) return false;
|
|
4023
|
+
const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
|
|
4024
|
+
return prev === start - 1 && hour === start + 1;
|
|
4025
|
+
}
|
|
4026
|
+
function in_autoclose_window(window, now) {
|
|
4027
|
+
if (!window) return true;
|
|
4028
|
+
const hour = hour_in_zone(now, window.timeZone);
|
|
4029
|
+
return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
|
|
4030
|
+
}
|
|
4031
|
+
function next_window_open(window, now) {
|
|
4032
|
+
const start_ms = now.getTime();
|
|
4033
|
+
for (let h = 0; h <= 48; h++) {
|
|
4034
|
+
const candidate = new Date(start_ms + h * 36e5);
|
|
4035
|
+
if (hour_in_zone(candidate, window.timeZone) === window.start)
|
|
4036
|
+
return candidate;
|
|
4037
|
+
const boundary = new Date(
|
|
4038
|
+
candidate.getTime() - candidate.getTime() % 36e5
|
|
4039
|
+
);
|
|
4040
|
+
if (boundary.getTime() >= start_ms && is_dst_gap_open(boundary, window.timeZone, window.start))
|
|
4041
|
+
return boundary;
|
|
4042
|
+
}
|
|
4043
|
+
return new Date(start_ms + 864e5);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// src/internal/autoclose-reaction.ts
|
|
4047
|
+
var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
|
|
4048
|
+
function synthesize_autoclose_reactions(registry, states, read_window) {
|
|
4049
|
+
for (const st of states.values()) {
|
|
4050
|
+
const predicate = st.autoclose;
|
|
4051
|
+
if (!predicate) continue;
|
|
4052
|
+
const after_days = st.autoclose_after_days;
|
|
4053
|
+
const keep_days = st.autoclose_keep_days;
|
|
4054
|
+
const archiver = st.archive;
|
|
4055
|
+
const reaction = {
|
|
4056
|
+
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
4057
|
+
// per-aggregate `__autoclose__` key — so the autoclose reaction never
|
|
4058
|
+
// shares a watermark with the aggregate's own reactions. A shared
|
|
4059
|
+
// watermark would let autoclose's defer short-circuit the aggregate's
|
|
4060
|
+
// other reactions (the "a defer affects all reactions on a stream"
|
|
4061
|
+
// hazard). The close still targets the aggregate (`source`).
|
|
4062
|
+
resolver: (e) => ({
|
|
4063
|
+
target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
|
|
4064
|
+
source: e.stream
|
|
4065
|
+
}),
|
|
4066
|
+
// Never block on autoclose: a transient query/store error should retry,
|
|
4067
|
+
// not quarantine the synthetic stream.
|
|
4068
|
+
options: { blockOnError: false, maxRetries: DEFAULT_MAX_RETRIES },
|
|
4069
|
+
handler: async (event) => {
|
|
4070
|
+
const aggregate = event.stream;
|
|
4071
|
+
const window = read_window();
|
|
4072
|
+
if (!in_autoclose_window(window, /* @__PURE__ */ new Date()))
|
|
4073
|
+
throw new DeferSignal({ at: next_window_open(window, /* @__PURE__ */ new Date()) });
|
|
4074
|
+
const stats = await store().query_stats([aggregate], {
|
|
4075
|
+
count: true,
|
|
4076
|
+
tail: keep_days !== void 0 ? true : void 0,
|
|
4077
|
+
exclude: [TOMBSTONE_EVENT, SNAP_EVENT]
|
|
4078
|
+
});
|
|
4079
|
+
const entry = stats.get(aggregate);
|
|
4080
|
+
if (!entry) return;
|
|
4081
|
+
const head = entry.head;
|
|
4082
|
+
if (predicate(aggregate, head, entry.count))
|
|
4083
|
+
throw new CloseSignal({
|
|
4084
|
+
stream: aggregate,
|
|
4085
|
+
archive: archiver ? () => archiver(aggregate, head) : void 0,
|
|
4086
|
+
// Ack this reaction's own watermark to the live head so the
|
|
4087
|
+
// close-cycle guard (which matches subscriptions by source =
|
|
4088
|
+
// aggregate) sees it caught up instead of blocking its own close.
|
|
4089
|
+
at: head.id
|
|
4090
|
+
});
|
|
4091
|
+
if (keep_days !== void 0) {
|
|
4092
|
+
const cutoff = days_before_now(keep_days);
|
|
4093
|
+
if (entry.tail.created < cutoff)
|
|
4094
|
+
throw new CloseSignal({
|
|
4095
|
+
stream: aggregate,
|
|
4096
|
+
before: cutoff,
|
|
4097
|
+
archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
|
|
4098
|
+
at: head.id
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
const dues = [];
|
|
4102
|
+
if (after_days !== void 0)
|
|
4103
|
+
dues.push(days_after(head.created, after_days));
|
|
4104
|
+
if (keep_days !== void 0)
|
|
4105
|
+
dues.push(days_after(entry.tail.created, keep_days));
|
|
4106
|
+
const future = dues.map((d) => d.getTime()).filter((t) => t > Date.now());
|
|
4107
|
+
if (future.length)
|
|
4108
|
+
throw new DeferSignal({ at: new Date(Math.min(...future)) });
|
|
4119
4109
|
}
|
|
4120
|
-
|
|
4121
|
-
}
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
* "show me what's broken" operational query.
|
|
4127
|
-
*
|
|
4128
|
-
* Results are ordered by stream name, paginated by `limit` (default
|
|
4129
|
-
* 100). Pass `after` to fetch the next page (keyset cursor on the
|
|
4130
|
-
* stream name). For richer queries — including blocked + source
|
|
4131
|
-
* filters, or full unblocked introspection — drop to
|
|
4132
|
-
* `store().query_streams(...)` directly.
|
|
4133
|
-
*
|
|
4134
|
-
* @returns Array of {@link StreamPosition} for currently-blocked streams.
|
|
4135
|
-
*
|
|
4136
|
-
* @example Discover and recover
|
|
4137
|
-
* ```typescript
|
|
4138
|
-
* const blocked = await app.blocked_streams();
|
|
4139
|
-
* console.table(blocked.map(({ stream, retry, error }) => ({ stream, retry, error })));
|
|
4140
|
-
*
|
|
4141
|
-
* // Operator investigates, then bulk-unblocks the family:
|
|
4142
|
-
* await app.unblock({ stream: "^webhooks-out-" });
|
|
4143
|
-
* ```
|
|
4144
|
-
*/
|
|
4145
|
-
async blocked_streams(options) {
|
|
4146
|
-
return this._scoped(async () => {
|
|
4147
|
-
const positions = [];
|
|
4148
|
-
await store().query_streams(
|
|
4149
|
-
(p) => {
|
|
4150
|
-
positions.push(p);
|
|
4151
|
-
},
|
|
4152
|
-
{ blocked: true, after: options?.after, limit: options?.limit }
|
|
4110
|
+
};
|
|
4111
|
+
const key = `__autoclose_${st.name}`;
|
|
4112
|
+
for (const event_name of Object.keys(st.events)) {
|
|
4113
|
+
registry.events[event_name]?.reactions.set(
|
|
4114
|
+
key,
|
|
4115
|
+
reaction
|
|
4153
4116
|
);
|
|
4154
|
-
return positions;
|
|
4155
|
-
});
|
|
4156
|
-
}
|
|
4157
|
-
/**
|
|
4158
|
-
* Operator-driven store audit.
|
|
4159
|
-
*
|
|
4160
|
-
* Walks the connected store and yields per-category findings —
|
|
4161
|
-
* each tagged with the remediation it suggests. Same operator-
|
|
4162
|
-
* driven category as `app.close()` / `app.reset()` /
|
|
4163
|
-
* `app.unblock()` / `app.blocked_streams()`: never auto-invoked by
|
|
4164
|
-
* the framework; the operator decides when to run it (CI gate,
|
|
4165
|
-
* scheduled job, ad-hoc forensics) and what to do with the
|
|
4166
|
-
* findings.
|
|
4167
|
-
*
|
|
4168
|
-
* Categories are independent — pass a subset to scope the work,
|
|
4169
|
-
* or omit to run everything:
|
|
4170
|
-
*
|
|
4171
|
-
* ```typescript
|
|
4172
|
-
* // Targeted: schema drift + deprecated-event load only
|
|
4173
|
-
* for await (const f of app.audit(["schema", "deprecated-load"], {
|
|
4174
|
-
* query: { created_after: lastScan },
|
|
4175
|
-
* thresholds: { deprecatedLoadShareMin: 0.10 },
|
|
4176
|
-
* })) {
|
|
4177
|
-
* await escalate(f);
|
|
4178
|
-
* }
|
|
4179
|
-
*
|
|
4180
|
-
* // Full audit, default thresholds
|
|
4181
|
-
* for await (const f of app.audit()) console.log(f);
|
|
4182
|
-
* ```
|
|
4183
|
-
*
|
|
4184
|
-
* Returns an `AsyncIterable` so callers can `break` early — the
|
|
4185
|
-
* underlying store paginations respect the iterator protocol and
|
|
4186
|
-
* stop cleanly. Each finding is emitted independently, so
|
|
4187
|
-
* pipelining into Slack / persistence / further analysis works
|
|
4188
|
-
* without buffering the full report in memory.
|
|
4189
|
-
*
|
|
4190
|
-
* Findings shape — see {@link AuditFinding}. The discriminated
|
|
4191
|
-
* union carries enough context for the operator to act on each
|
|
4192
|
-
* finding directly: stream id, event id, recommendation hints.
|
|
4193
|
-
*
|
|
4194
|
-
* @param categories - Subset of categories to run (default: all).
|
|
4195
|
-
* @param options - Query window + per-category thresholds.
|
|
4196
|
-
* @returns Async iterable of {@link AuditFinding}.
|
|
4197
|
-
*/
|
|
4198
|
-
async *audit(categories, options) {
|
|
4199
|
-
const it = audit(this._audit_deps, categories, options)[Symbol.asyncIterator]();
|
|
4200
|
-
while (true) {
|
|
4201
|
-
const { value, done } = await this._scoped(() => it.next());
|
|
4202
|
-
if (done) break;
|
|
4203
|
-
yield value;
|
|
4204
4117
|
}
|
|
4205
4118
|
}
|
|
4206
|
-
|
|
4207
|
-
* Bulk-update scheduling priority for streams matching `filter`.
|
|
4208
|
-
*
|
|
4209
|
-
* Operator-grade override of the `claim()` lagging-frontier
|
|
4210
|
-
* ordering. Useful when a long-running replay needs to
|
|
4211
|
-
* jump ahead of other lagging streams, or when a no-longer-urgent
|
|
4212
|
-
* job should yield slots back to the rest. Build-time priorities
|
|
4213
|
-
* (set via the resolver's `priority` field) are subject to a
|
|
4214
|
-
* `max()` invariant across reactions; this API ignores that and
|
|
4215
|
-
* sets the priority outright on every matching row.
|
|
4216
|
-
*
|
|
4217
|
-
* Filter shape mirrors {@link query} / {@link Store.query_streams}:
|
|
4218
|
-
* `stream` / `source` are regex by default, exact with the
|
|
4219
|
-
* `*_exact` flags; `blocked` restricts to blocked or unblocked
|
|
4220
|
-
* rows. **An empty filter (`{}`) updates every registered stream.**
|
|
4221
|
-
*
|
|
4222
|
-
* @param filter - Selection criteria (regex by default).
|
|
4223
|
-
* @param priority - New priority value. Set as-is — no clamp.
|
|
4224
|
-
* @returns Count of streams whose priority changed.
|
|
4225
|
-
*
|
|
4226
|
-
* @example Boost a specific projection mid-replay
|
|
4227
|
-
* ```typescript
|
|
4228
|
-
* await app.prioritize({ stream: "^proj-orders$", stream_exact: false }, 10);
|
|
4229
|
-
* ```
|
|
4230
|
-
*
|
|
4231
|
-
* @see {@link Store.prioritize} for the underlying primitive
|
|
4232
|
-
* @see {@link claim} for how priority biases scheduling
|
|
4233
|
-
*/
|
|
4234
|
-
async prioritize(filter, priority) {
|
|
4235
|
-
return this._scoped(() => store().prioritize(filter, priority));
|
|
4236
|
-
}
|
|
4237
|
-
/** Shared by `close()` and reaction-requested closes. */
|
|
4238
|
-
async _run_close(targets) {
|
|
4239
|
-
const close_actor = { id: "$close", name: "close" };
|
|
4240
|
-
const result = await run_close_cycle(targets, {
|
|
4241
|
-
reactive: this._reactive,
|
|
4242
|
-
catch_up_correlation: (until) => catch_up_correlation(
|
|
4243
|
-
this._correlate,
|
|
4244
|
-
(limit) => this.correlate({ limit }),
|
|
4245
|
-
until
|
|
4246
|
-
),
|
|
4247
|
-
event_to_state: this._event_to_state,
|
|
4248
|
-
load: this._es.load,
|
|
4249
|
-
tombstone: this._es.tombstone,
|
|
4250
|
-
logger: this._logger,
|
|
4251
|
-
correlation: close_correlation(this._correlator, close_actor),
|
|
4252
|
-
with_stream_lock: (stream, work) => this._close_lock.run(stream, work),
|
|
4253
|
-
forget_subscribed: (streams) => this._correlate.forget_subscribed(streams)
|
|
4254
|
-
});
|
|
4255
|
-
this.emit("closed", result);
|
|
4256
|
-
return result;
|
|
4257
|
-
}
|
|
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));
|
|
4298
|
-
}
|
|
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);
|
|
4334
|
-
}
|
|
4335
|
-
};
|
|
4119
|
+
}
|
|
4336
4120
|
|
|
4337
4121
|
// src/builders/merge.ts
|
|
4338
4122
|
import { ZodObject } from "zod";
|
|
@@ -4571,7 +4355,74 @@ function reaction_on(event, events, builder) {
|
|
|
4571
4355
|
}
|
|
4572
4356
|
|
|
4573
4357
|
// src/builders/event-builder.ts
|
|
4574
|
-
import { z as
|
|
4358
|
+
import { z as z4 } from "zod";
|
|
4359
|
+
|
|
4360
|
+
// src/internal/date-reviver.ts
|
|
4361
|
+
import { z as z3 } from "zod";
|
|
4362
|
+
var def_of = (schema) => schema._zod?.def ?? schema.def;
|
|
4363
|
+
function variant_schema(schema) {
|
|
4364
|
+
const shape = def_of(schema)?.shape;
|
|
4365
|
+
if (!shape) {
|
|
4366
|
+
const dates = date_reviver_schema(schema);
|
|
4367
|
+
return { schema: dates ?? schema, dated: !!dates };
|
|
4368
|
+
}
|
|
4369
|
+
const next = {};
|
|
4370
|
+
let dated = false;
|
|
4371
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
4372
|
+
const dates = date_reviver_schema(field);
|
|
4373
|
+
if (dates) dated = true;
|
|
4374
|
+
next[key] = (dates ?? field).optional();
|
|
4375
|
+
}
|
|
4376
|
+
return { schema: z3.looseObject(next), dated };
|
|
4377
|
+
}
|
|
4378
|
+
function date_reviver_schema(schema) {
|
|
4379
|
+
const def = def_of(schema);
|
|
4380
|
+
if (!def) return void 0;
|
|
4381
|
+
const inner = () => date_reviver_schema(def.innerType);
|
|
4382
|
+
switch (def.type) {
|
|
4383
|
+
case "date":
|
|
4384
|
+
return z3.coerce.date();
|
|
4385
|
+
case "object": {
|
|
4386
|
+
const shape = def.shape;
|
|
4387
|
+
if (!shape) return void 0;
|
|
4388
|
+
const dated = {};
|
|
4389
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
4390
|
+
const dates = date_reviver_schema(field);
|
|
4391
|
+
if (dates) dated[key] = dates.optional();
|
|
4392
|
+
}
|
|
4393
|
+
return Object.keys(dated).length ? z3.looseObject(dated) : void 0;
|
|
4394
|
+
}
|
|
4395
|
+
case "array": {
|
|
4396
|
+
const element = date_reviver_schema(def.element);
|
|
4397
|
+
return element && z3.array(element);
|
|
4398
|
+
}
|
|
4399
|
+
case "tuple": {
|
|
4400
|
+
const items = def.items.map((i) => date_reviver_schema(i));
|
|
4401
|
+
return items.some(Boolean) ? z3.tuple(items.map((i) => i ?? z3.unknown())) : void 0;
|
|
4402
|
+
}
|
|
4403
|
+
case "record": {
|
|
4404
|
+
const value = date_reviver_schema(def.valueType);
|
|
4405
|
+
return value && z3.record(z3.string(), value);
|
|
4406
|
+
}
|
|
4407
|
+
case "union": {
|
|
4408
|
+
const variants = def.options.map(variant_schema);
|
|
4409
|
+
return variants.some((v) => v.dated) ? z3.union(variants.map((v) => v.schema)) : void 0;
|
|
4410
|
+
}
|
|
4411
|
+
case "nullable":
|
|
4412
|
+
return inner()?.nullable();
|
|
4413
|
+
case "optional":
|
|
4414
|
+
case "nonoptional":
|
|
4415
|
+
case "readonly":
|
|
4416
|
+
case "default":
|
|
4417
|
+
case "prefault":
|
|
4418
|
+
case "catch":
|
|
4419
|
+
return inner();
|
|
4420
|
+
default:
|
|
4421
|
+
return void 0;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
// src/builders/event-builder.ts
|
|
4575
4426
|
function event_tags(schema) {
|
|
4576
4427
|
const pii = pii_schemas(schema);
|
|
4577
4428
|
const pii_dates = {};
|
|
@@ -4588,7 +4439,7 @@ function event_tags(schema) {
|
|
|
4588
4439
|
return {
|
|
4589
4440
|
sensitive: Object.keys(pii),
|
|
4590
4441
|
date_reviver: data_reviver_schema && revive(data_reviver_schema),
|
|
4591
|
-
pii_date_reviver: dated_pii ? revive(
|
|
4442
|
+
pii_date_reviver: dated_pii ? revive(z4.looseObject(pii_dates)) : void 0
|
|
4592
4443
|
};
|
|
4593
4444
|
}
|
|
4594
4445
|
function make_event_reader(tags, disclosure, predicate = null) {
|
|
@@ -5375,9 +5226,6 @@ export {
|
|
|
5375
5226
|
ConcurrencyError,
|
|
5376
5227
|
ConsoleLogger,
|
|
5377
5228
|
CsvFile,
|
|
5378
|
-
DEFAULT_AUTOCLOSE_CYCLE_MINUTES,
|
|
5379
|
-
DEFAULT_CLOSE_BATCH_SIZE,
|
|
5380
|
-
DEFAULT_CLOSE_YIELD_MS,
|
|
5381
5229
|
DEFAULT_LANE,
|
|
5382
5230
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
5383
5231
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
@@ -5415,7 +5263,7 @@ export {
|
|
|
5415
5263
|
pii_fields,
|
|
5416
5264
|
port,
|
|
5417
5265
|
projection,
|
|
5418
|
-
resolveAutocloseConfig,
|
|
5266
|
+
resolveAutocloseConfig2 as resolveAutocloseConfig,
|
|
5419
5267
|
sensitive,
|
|
5420
5268
|
sleep,
|
|
5421
5269
|
slice,
|