@rotorsoft/act 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +55 -43
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +13 -4
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +35 -9
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-config.d.ts +39 -15
- package/dist/@types/internal/autoclose-config.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-policy.d.ts +39 -13
- package/dist/@types/internal/autoclose-policy.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-reaction.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +5 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/close-signal.d.ts +8 -0
- package/dist/@types/internal/close-signal.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +2 -2
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +46 -13
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +29 -3
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-TODGOWEZ.js → chunk-KBTC3NGQ.js} +64 -14
- package/dist/chunk-KBTC3NGQ.js.map +1 -0
- package/dist/index.cjs +237 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +175 -26
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +63 -13
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TODGOWEZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sleep,
|
|
20
20
|
store,
|
|
21
21
|
validate
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-KBTC3NGQ.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -538,15 +538,33 @@ function in_autoclose_window(window, now) {
|
|
|
538
538
|
const hour = hour_in_zone(now, window.timeZone);
|
|
539
539
|
return window.start < window.end ? hour >= window.start && hour < window.end : hour >= window.start || hour < window.end;
|
|
540
540
|
}
|
|
541
|
+
function next_window_open(window, now) {
|
|
542
|
+
for (let h = 0; h <= 48; h++) {
|
|
543
|
+
const candidate = new Date(now.getTime() + h * 36e5);
|
|
544
|
+
if (hour_in_zone(candidate, window.timeZone) === window.start)
|
|
545
|
+
return candidate;
|
|
546
|
+
}
|
|
547
|
+
return new Date(now.getTime() + 864e5);
|
|
548
|
+
}
|
|
541
549
|
|
|
542
550
|
// src/internal/autoclose-policy.ts
|
|
543
551
|
import { z as z2 } from "zod";
|
|
544
|
-
var
|
|
545
|
-
var
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
552
|
+
var DAY_MS = 864e5;
|
|
553
|
+
var MIN_AFTER_DAYS = 1 / 1440;
|
|
554
|
+
var MIN_KEEP_DAYS = 1;
|
|
555
|
+
function days_after(date, days) {
|
|
556
|
+
return new Date(date.getTime() + days * DAY_MS);
|
|
557
|
+
}
|
|
558
|
+
function days_before_now(days) {
|
|
559
|
+
return new Date(Date.now() - days * DAY_MS);
|
|
560
|
+
}
|
|
561
|
+
var window_schema = (field, min_days, floor) => z2.object({
|
|
562
|
+
days: z2.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
|
|
563
|
+
}).refine((o) => o.days >= min_days, {
|
|
564
|
+
message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
|
|
549
565
|
});
|
|
566
|
+
var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
|
|
567
|
+
var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
|
|
550
568
|
var IsSchema = z2.union([
|
|
551
569
|
z2.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
552
570
|
z2.array(
|
|
@@ -568,16 +586,16 @@ var AutoclosePolicySchema = z2.object({
|
|
|
568
586
|
after: AfterSchema.optional(),
|
|
569
587
|
is: IsSchema.optional(),
|
|
570
588
|
reaches: ReachesSchema.optional(),
|
|
571
|
-
or: OrBlockSchema.optional()
|
|
589
|
+
or: OrBlockSchema.optional(),
|
|
590
|
+
keep: KeepSchema.optional()
|
|
572
591
|
}).strict().refine(
|
|
573
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
592
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
|
|
574
593
|
{
|
|
575
|
-
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
594
|
+
message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
|
|
576
595
|
}
|
|
577
596
|
);
|
|
578
597
|
function compile_after(after) {
|
|
579
|
-
|
|
580
|
-
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
598
|
+
return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
|
|
581
599
|
}
|
|
582
600
|
function compile_is(is) {
|
|
583
601
|
const set = new Set(typeof is === "string" ? [is] : is);
|
|
@@ -586,13 +604,17 @@ function compile_is(is) {
|
|
|
586
604
|
function compile_reaches(reaches) {
|
|
587
605
|
return (_stream, _head, count) => count >= reaches;
|
|
588
606
|
}
|
|
589
|
-
function
|
|
607
|
+
function policy_min_after_days(options) {
|
|
590
608
|
const parsed = AutoclosePolicySchema.parse(options);
|
|
591
609
|
const windows = [];
|
|
592
|
-
if (parsed.after) windows.push(parsed.after.days
|
|
593
|
-
if (parsed.or?.after) windows.push(parsed.or.after.days
|
|
610
|
+
if (parsed.after) windows.push(parsed.after.days);
|
|
611
|
+
if (parsed.or?.after) windows.push(parsed.or.after.days);
|
|
594
612
|
return windows.length ? Math.min(...windows) : void 0;
|
|
595
613
|
}
|
|
614
|
+
function policy_keep_days(options) {
|
|
615
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
616
|
+
return parsed.keep?.days;
|
|
617
|
+
}
|
|
596
618
|
function compile_autoclose_policy(options) {
|
|
597
619
|
const parsed = AutoclosePolicySchema.parse(options);
|
|
598
620
|
const and_preds = [];
|
|
@@ -635,12 +657,20 @@ var CloseSignal = class extends Error {
|
|
|
635
657
|
* event id when omitted.
|
|
636
658
|
*/
|
|
637
659
|
at;
|
|
660
|
+
/**
|
|
661
|
+
* Windowed close (#1011): prune events older than this cutoff behind
|
|
662
|
+
* the closest safe snapshot instead of retiring the stream. Thrown by
|
|
663
|
+
* the autoclose reaction of a `.autocloses({ keep })` state; flows
|
|
664
|
+
* into {@link CloseTarget.before}. Omitted → a full close.
|
|
665
|
+
*/
|
|
666
|
+
before;
|
|
638
667
|
constructor(opts) {
|
|
639
668
|
super("reaction requested close");
|
|
640
669
|
this.name = "CloseSignal";
|
|
641
670
|
this.stream = opts?.stream;
|
|
642
671
|
this.archive = opts?.archive;
|
|
643
672
|
this.at = opts?.at;
|
|
673
|
+
this.before = opts?.before;
|
|
644
674
|
}
|
|
645
675
|
};
|
|
646
676
|
|
|
@@ -664,7 +694,8 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
664
694
|
for (const st of states.values()) {
|
|
665
695
|
const predicate = st.autoclose;
|
|
666
696
|
if (!predicate) continue;
|
|
667
|
-
const
|
|
697
|
+
const after_days = st.autoclose_after_days;
|
|
698
|
+
const keep_days = st.autoclose_keep_days;
|
|
668
699
|
const archiver = st.archive;
|
|
669
700
|
const reaction = {
|
|
670
701
|
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
@@ -684,11 +715,12 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
684
715
|
const aggregate = event.stream;
|
|
685
716
|
if (!in_autoclose_window(config2.autocloseWindow, /* @__PURE__ */ new Date()))
|
|
686
717
|
throw new DeferSignal({
|
|
687
|
-
at:
|
|
718
|
+
at: next_window_open(config2.autocloseWindow, /* @__PURE__ */ new Date())
|
|
688
719
|
});
|
|
689
720
|
const stats = await store().query_stats([aggregate], {
|
|
690
721
|
count: true,
|
|
691
|
-
|
|
722
|
+
tail: keep_days !== void 0 ? true : void 0,
|
|
723
|
+
exclude: keep_days !== void 0 ? [TOMBSTONE_EVENT, SNAP_EVENT] : [TOMBSTONE_EVENT]
|
|
692
724
|
});
|
|
693
725
|
const entry = stats.get(aggregate);
|
|
694
726
|
if (!entry) return;
|
|
@@ -702,9 +734,24 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
702
734
|
// aggregate) sees it caught up instead of blocking its own close.
|
|
703
735
|
at: head.id
|
|
704
736
|
});
|
|
705
|
-
if (
|
|
737
|
+
if (keep_days !== void 0) {
|
|
738
|
+
const cutoff = days_before_now(keep_days);
|
|
739
|
+
if (entry.tail.created < cutoff)
|
|
740
|
+
throw new CloseSignal({
|
|
741
|
+
stream: aggregate,
|
|
742
|
+
before: cutoff,
|
|
743
|
+
archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
|
|
744
|
+
at: head.id
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const dues = [];
|
|
748
|
+
if (after_days !== void 0)
|
|
749
|
+
dues.push(days_after(head.created, after_days));
|
|
750
|
+
if (keep_days !== void 0)
|
|
751
|
+
dues.push(days_after(entry.tail.created, keep_days));
|
|
752
|
+
if (dues.length)
|
|
706
753
|
throw new DeferSignal({
|
|
707
|
-
at: new Date(
|
|
754
|
+
at: new Date(Math.min(...dues.map((d) => d.getTime())))
|
|
708
755
|
});
|
|
709
756
|
}
|
|
710
757
|
};
|
|
@@ -1091,8 +1138,20 @@ var CircuitBreaker = class {
|
|
|
1091
1138
|
var SAFETY_PROBE_PAGE_SIZE = 1e3;
|
|
1092
1139
|
async function run_close_cycle(targets, deps) {
|
|
1093
1140
|
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
1094
|
-
const
|
|
1141
|
+
for (const t of target_map.values()) {
|
|
1142
|
+
if (t.before !== void 0 && t.restart)
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`close: \`before\` and \`restart\` are mutually exclusive (stream "${t.stream}") \u2014 a windowed close keeps the stream live behind a real snapshot; restart reseeds it`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const windowed = [...target_map.values()].filter(
|
|
1148
|
+
(t) => t.before !== void 0
|
|
1149
|
+
);
|
|
1150
|
+
const full = [...target_map.values()].filter((t) => t.before === void 0);
|
|
1095
1151
|
const skipped = [];
|
|
1152
|
+
const windowed_result = windowed.length ? await run_windowed_closes(windowed, deps, skipped) : /* @__PURE__ */ new Map();
|
|
1153
|
+
if (!full.length) return { truncated: windowed_result, skipped };
|
|
1154
|
+
const streams = full.map((t) => t.stream);
|
|
1096
1155
|
const stream_info = await scan_stream_heads(streams);
|
|
1097
1156
|
const safe = await partition_by_safety(
|
|
1098
1157
|
stream_info,
|
|
@@ -1100,7 +1159,7 @@ async function run_close_cycle(targets, deps) {
|
|
|
1100
1159
|
skipped,
|
|
1101
1160
|
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
1102
1161
|
);
|
|
1103
|
-
if (!safe.length) return { truncated:
|
|
1162
|
+
if (!safe.length) return { truncated: windowed_result, skipped };
|
|
1104
1163
|
const { guarded, guard_events } = await guard_with_tombstones(
|
|
1105
1164
|
safe,
|
|
1106
1165
|
stream_info,
|
|
@@ -1108,7 +1167,7 @@ async function run_close_cycle(targets, deps) {
|
|
|
1108
1167
|
deps.tombstone,
|
|
1109
1168
|
skipped
|
|
1110
1169
|
);
|
|
1111
|
-
if (!guarded.length) return { truncated:
|
|
1170
|
+
if (!guarded.length) return { truncated: windowed_result, skipped };
|
|
1112
1171
|
const seed_states = await load_restart_seeds(
|
|
1113
1172
|
guarded,
|
|
1114
1173
|
target_map,
|
|
@@ -1124,8 +1183,62 @@ async function run_close_cycle(targets, deps) {
|
|
|
1124
1183
|
guard_events,
|
|
1125
1184
|
deps.correlation
|
|
1126
1185
|
);
|
|
1186
|
+
for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
|
|
1127
1187
|
return { truncated, skipped };
|
|
1128
1188
|
}
|
|
1189
|
+
async function run_windowed_closes(windowed, deps, skipped) {
|
|
1190
|
+
const min_at = deps.reactive_events_size > 0 ? await probe_min_watermarks(
|
|
1191
|
+
windowed.map((t) => t.stream),
|
|
1192
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
1193
|
+
) : /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const t of windowed) {
|
|
1195
|
+
if (t.archive) await t.archive();
|
|
1196
|
+
}
|
|
1197
|
+
const truncated = await store().truncate(
|
|
1198
|
+
windowed.map((t) => ({
|
|
1199
|
+
stream: t.stream,
|
|
1200
|
+
before: t.before,
|
|
1201
|
+
max_id: min_at.get(t.stream)
|
|
1202
|
+
}))
|
|
1203
|
+
);
|
|
1204
|
+
for (const t of windowed) {
|
|
1205
|
+
if (!truncated.has(t.stream)) skipped.push(t.stream);
|
|
1206
|
+
}
|
|
1207
|
+
return truncated;
|
|
1208
|
+
}
|
|
1209
|
+
async function probe_min_watermarks(streams, page_size) {
|
|
1210
|
+
const min_at = /* @__PURE__ */ new Map();
|
|
1211
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
1212
|
+
const get_regex = (source) => {
|
|
1213
|
+
let re = source_regex.get(source);
|
|
1214
|
+
if (!re) {
|
|
1215
|
+
re = new RegExp(source);
|
|
1216
|
+
source_regex.set(source, re);
|
|
1217
|
+
}
|
|
1218
|
+
return re;
|
|
1219
|
+
};
|
|
1220
|
+
let after;
|
|
1221
|
+
for (; ; ) {
|
|
1222
|
+
let last;
|
|
1223
|
+
const { count } = await store().query_streams(
|
|
1224
|
+
(position) => {
|
|
1225
|
+
last = position.stream;
|
|
1226
|
+
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
1227
|
+
for (const stream of streams) {
|
|
1228
|
+
if (!source_re || source_re.test(stream)) {
|
|
1229
|
+
const prev = min_at.get(stream);
|
|
1230
|
+
if (prev === void 0 || position.at < prev)
|
|
1231
|
+
min_at.set(stream, position.at);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
{ after, limit: page_size, source_matches: streams }
|
|
1236
|
+
);
|
|
1237
|
+
if (count < page_size) break;
|
|
1238
|
+
after = last;
|
|
1239
|
+
}
|
|
1240
|
+
return min_at;
|
|
1241
|
+
}
|
|
1129
1242
|
async function scan_stream_heads(streams) {
|
|
1130
1243
|
const stats = await store().query_stats(streams, {
|
|
1131
1244
|
exclude: [SNAP_EVENT]
|
|
@@ -2326,8 +2439,13 @@ function build_handle(deps) {
|
|
|
2326
2439
|
acked_at: error.at ?? event.id,
|
|
2327
2440
|
// Close the signalled stream (the autoclose reaction's aggregate,
|
|
2328
2441
|
// which differs from its synthetic lease stream); a self-closing
|
|
2329
|
-
// user reaction omits it and closes its own lease stream.
|
|
2330
|
-
|
|
2442
|
+
// user reaction omits it and closes its own lease stream. A
|
|
2443
|
+
// carried `before` makes it a windowed close (prune, not retire).
|
|
2444
|
+
close: {
|
|
2445
|
+
stream: error.stream ?? stream,
|
|
2446
|
+
archive: error.archive,
|
|
2447
|
+
before: error.before
|
|
2448
|
+
}
|
|
2331
2449
|
};
|
|
2332
2450
|
return finalize(
|
|
2333
2451
|
lease,
|
|
@@ -3630,7 +3748,19 @@ var Act = class {
|
|
|
3630
3748
|
* 7. **Cache** — invalidate (tombstoned) or warm (restarted)
|
|
3631
3749
|
* 8. **Emit "closed"** — lifecycle event with results
|
|
3632
3750
|
*
|
|
3633
|
-
*
|
|
3751
|
+
* Targets carrying `before` take the **windowed** branch instead —
|
|
3752
|
+
* close the books on a rolling window: probe the min consumer
|
|
3753
|
+
* watermark (so the boundary never rises past a lagging reaction),
|
|
3754
|
+
* run the archive callback against the cutoff, then prune the prefix
|
|
3755
|
+
* below the closest safe `__snapshot__`. No tombstone guard (the
|
|
3756
|
+
* pre-cutoff prefix is immutable), no seed, cache untouched — the
|
|
3757
|
+
* stream stays live and keeps accepting actions. Requires the state
|
|
3758
|
+
* to snapshot via `.snap(...)`; no qualifying snapshot ⇒ the stream
|
|
3759
|
+
* lands in `skipped` (retry after the next snapshot). Windowed
|
|
3760
|
+
* entries in the result echo `before` and carry the surviving
|
|
3761
|
+
* boundary snapshot as `committed`.
|
|
3762
|
+
*
|
|
3763
|
+
* @param targets - Per-stream close options (stream, restart?, archive?, before?)
|
|
3634
3764
|
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
3635
3765
|
*
|
|
3636
3766
|
* @example Archive and close
|
|
@@ -3648,6 +3778,19 @@ var Act = class {
|
|
|
3648
3778
|
* { stream: "counter-2" }, // tombstoned
|
|
3649
3779
|
* ]);
|
|
3650
3780
|
* ```
|
|
3781
|
+
*
|
|
3782
|
+
* @example Windowed close — keep the last 180 days of real events
|
|
3783
|
+
* ```typescript
|
|
3784
|
+
* const cutoff = new Date();
|
|
3785
|
+
* cutoff.setDate(cutoff.getDate() - 180);
|
|
3786
|
+
* await app.close([
|
|
3787
|
+
* {
|
|
3788
|
+
* stream: "ledger-acme",
|
|
3789
|
+
* before: cutoff,
|
|
3790
|
+
* archive: async () => { await archiveToS3("ledger-acme", cutoff); },
|
|
3791
|
+
* },
|
|
3792
|
+
* ]);
|
|
3793
|
+
* ```
|
|
3651
3794
|
*/
|
|
3652
3795
|
async close(targets) {
|
|
3653
3796
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
@@ -4180,10 +4323,16 @@ function action_builder(state2) {
|
|
|
4180
4323
|
".autocloses(...) requires a policy object; got " + typeof policy
|
|
4181
4324
|
);
|
|
4182
4325
|
}
|
|
4326
|
+
if (policy.keep && !internal.snap) {
|
|
4327
|
+
throw new Error(
|
|
4328
|
+
".autocloses({ keep }) requires .snap(...) earlier in the chain \u2014 a rolling window prunes behind a real snapshot, so a state that never snapshots has nothing to prune behind."
|
|
4329
|
+
);
|
|
4330
|
+
}
|
|
4183
4331
|
internal.autoclose = compile_autoclose_policy(
|
|
4184
4332
|
policy
|
|
4185
4333
|
);
|
|
4186
|
-
internal.
|
|
4334
|
+
internal.autoclose_after_days = policy_min_after_days(policy);
|
|
4335
|
+
internal.autoclose_keep_days = policy_keep_days(policy);
|
|
4187
4336
|
return builder;
|
|
4188
4337
|
},
|
|
4189
4338
|
archives(archive) {
|