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