@rotorsoft/act 1.19.0 → 1.21.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 +26 -1
- 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/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/projection-builder.d.ts +41 -12
- package/dist/@types/builders/projection-builder.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-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/event-sourcing.d.ts +1 -1
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -1
- 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 +302 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +240 -58
- 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,
|
|
@@ -541,12 +541,22 @@ function in_autoclose_window(window, now) {
|
|
|
541
541
|
|
|
542
542
|
// src/internal/autoclose-policy.ts
|
|
543
543
|
import { z as z2 } from "zod";
|
|
544
|
-
var
|
|
545
|
-
var
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
544
|
+
var DAY_MS = 864e5;
|
|
545
|
+
var MIN_AFTER_DAYS = 1 / 1440;
|
|
546
|
+
var MIN_KEEP_DAYS = 1;
|
|
547
|
+
function days_after(date, days) {
|
|
548
|
+
return new Date(date.getTime() + days * DAY_MS);
|
|
549
|
+
}
|
|
550
|
+
function days_before_now(days) {
|
|
551
|
+
return new Date(Date.now() - days * DAY_MS);
|
|
552
|
+
}
|
|
553
|
+
var window_schema = (field, min_days, floor) => z2.object({
|
|
554
|
+
days: z2.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
|
|
555
|
+
}).refine((o) => o.days >= min_days, {
|
|
556
|
+
message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
|
|
549
557
|
});
|
|
558
|
+
var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
|
|
559
|
+
var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
|
|
550
560
|
var IsSchema = z2.union([
|
|
551
561
|
z2.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
552
562
|
z2.array(
|
|
@@ -568,16 +578,16 @@ var AutoclosePolicySchema = z2.object({
|
|
|
568
578
|
after: AfterSchema.optional(),
|
|
569
579
|
is: IsSchema.optional(),
|
|
570
580
|
reaches: ReachesSchema.optional(),
|
|
571
|
-
or: OrBlockSchema.optional()
|
|
581
|
+
or: OrBlockSchema.optional(),
|
|
582
|
+
keep: KeepSchema.optional()
|
|
572
583
|
}).strict().refine(
|
|
573
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
584
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
|
|
574
585
|
{
|
|
575
|
-
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
586
|
+
message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
|
|
576
587
|
}
|
|
577
588
|
);
|
|
578
589
|
function compile_after(after) {
|
|
579
|
-
|
|
580
|
-
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
590
|
+
return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
|
|
581
591
|
}
|
|
582
592
|
function compile_is(is) {
|
|
583
593
|
const set = new Set(typeof is === "string" ? [is] : is);
|
|
@@ -586,13 +596,17 @@ function compile_is(is) {
|
|
|
586
596
|
function compile_reaches(reaches) {
|
|
587
597
|
return (_stream, _head, count) => count >= reaches;
|
|
588
598
|
}
|
|
589
|
-
function
|
|
599
|
+
function policy_min_after_days(options) {
|
|
590
600
|
const parsed = AutoclosePolicySchema.parse(options);
|
|
591
601
|
const windows = [];
|
|
592
|
-
if (parsed.after) windows.push(parsed.after.days
|
|
593
|
-
if (parsed.or?.after) windows.push(parsed.or.after.days
|
|
602
|
+
if (parsed.after) windows.push(parsed.after.days);
|
|
603
|
+
if (parsed.or?.after) windows.push(parsed.or.after.days);
|
|
594
604
|
return windows.length ? Math.min(...windows) : void 0;
|
|
595
605
|
}
|
|
606
|
+
function policy_keep_days(options) {
|
|
607
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
608
|
+
return parsed.keep?.days;
|
|
609
|
+
}
|
|
596
610
|
function compile_autoclose_policy(options) {
|
|
597
611
|
const parsed = AutoclosePolicySchema.parse(options);
|
|
598
612
|
const and_preds = [];
|
|
@@ -635,12 +649,20 @@ var CloseSignal = class extends Error {
|
|
|
635
649
|
* event id when omitted.
|
|
636
650
|
*/
|
|
637
651
|
at;
|
|
652
|
+
/**
|
|
653
|
+
* Windowed close (#1011): prune events older than this cutoff behind
|
|
654
|
+
* the closest safe snapshot instead of retiring the stream. Thrown by
|
|
655
|
+
* the autoclose reaction of a `.autocloses({ keep })` state; flows
|
|
656
|
+
* into {@link CloseTarget.before}. Omitted → a full close.
|
|
657
|
+
*/
|
|
658
|
+
before;
|
|
638
659
|
constructor(opts) {
|
|
639
660
|
super("reaction requested close");
|
|
640
661
|
this.name = "CloseSignal";
|
|
641
662
|
this.stream = opts?.stream;
|
|
642
663
|
this.archive = opts?.archive;
|
|
643
664
|
this.at = opts?.at;
|
|
665
|
+
this.before = opts?.before;
|
|
644
666
|
}
|
|
645
667
|
};
|
|
646
668
|
|
|
@@ -664,7 +686,8 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
664
686
|
for (const st of states.values()) {
|
|
665
687
|
const predicate = st.autoclose;
|
|
666
688
|
if (!predicate) continue;
|
|
667
|
-
const
|
|
689
|
+
const after_days = st.autoclose_after_days;
|
|
690
|
+
const keep_days = st.autoclose_keep_days;
|
|
668
691
|
const archiver = st.archive;
|
|
669
692
|
const reaction = {
|
|
670
693
|
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
@@ -688,7 +711,8 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
688
711
|
});
|
|
689
712
|
const stats = await store().query_stats([aggregate], {
|
|
690
713
|
count: true,
|
|
691
|
-
|
|
714
|
+
tail: keep_days !== void 0 ? true : void 0,
|
|
715
|
+
exclude: keep_days !== void 0 ? [TOMBSTONE_EVENT, SNAP_EVENT] : [TOMBSTONE_EVENT]
|
|
692
716
|
});
|
|
693
717
|
const entry = stats.get(aggregate);
|
|
694
718
|
if (!entry) return;
|
|
@@ -702,9 +726,24 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
702
726
|
// aggregate) sees it caught up instead of blocking its own close.
|
|
703
727
|
at: head.id
|
|
704
728
|
});
|
|
705
|
-
if (
|
|
729
|
+
if (keep_days !== void 0) {
|
|
730
|
+
const cutoff = days_before_now(keep_days);
|
|
731
|
+
if (entry.tail.created < cutoff)
|
|
732
|
+
throw new CloseSignal({
|
|
733
|
+
stream: aggregate,
|
|
734
|
+
before: cutoff,
|
|
735
|
+
archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
|
|
736
|
+
at: head.id
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
const dues = [];
|
|
740
|
+
if (after_days !== void 0)
|
|
741
|
+
dues.push(days_after(head.created, after_days));
|
|
742
|
+
if (keep_days !== void 0)
|
|
743
|
+
dues.push(days_after(entry.tail.created, keep_days));
|
|
744
|
+
if (dues.length)
|
|
706
745
|
throw new DeferSignal({
|
|
707
|
-
at: new Date(
|
|
746
|
+
at: new Date(Math.min(...dues.map((d) => d.getTime())))
|
|
708
747
|
});
|
|
709
748
|
}
|
|
710
749
|
};
|
|
@@ -1091,8 +1130,20 @@ var CircuitBreaker = class {
|
|
|
1091
1130
|
var SAFETY_PROBE_PAGE_SIZE = 1e3;
|
|
1092
1131
|
async function run_close_cycle(targets, deps) {
|
|
1093
1132
|
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
1094
|
-
const
|
|
1133
|
+
for (const t of target_map.values()) {
|
|
1134
|
+
if (t.before !== void 0 && t.restart)
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`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`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const windowed = [...target_map.values()].filter(
|
|
1140
|
+
(t) => t.before !== void 0
|
|
1141
|
+
);
|
|
1142
|
+
const full = [...target_map.values()].filter((t) => t.before === void 0);
|
|
1095
1143
|
const skipped = [];
|
|
1144
|
+
const windowed_result = windowed.length ? await run_windowed_closes(windowed, deps, skipped) : /* @__PURE__ */ new Map();
|
|
1145
|
+
if (!full.length) return { truncated: windowed_result, skipped };
|
|
1146
|
+
const streams = full.map((t) => t.stream);
|
|
1096
1147
|
const stream_info = await scan_stream_heads(streams);
|
|
1097
1148
|
const safe = await partition_by_safety(
|
|
1098
1149
|
stream_info,
|
|
@@ -1100,7 +1151,7 @@ async function run_close_cycle(targets, deps) {
|
|
|
1100
1151
|
skipped,
|
|
1101
1152
|
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
1102
1153
|
);
|
|
1103
|
-
if (!safe.length) return { truncated:
|
|
1154
|
+
if (!safe.length) return { truncated: windowed_result, skipped };
|
|
1104
1155
|
const { guarded, guard_events } = await guard_with_tombstones(
|
|
1105
1156
|
safe,
|
|
1106
1157
|
stream_info,
|
|
@@ -1108,7 +1159,7 @@ async function run_close_cycle(targets, deps) {
|
|
|
1108
1159
|
deps.tombstone,
|
|
1109
1160
|
skipped
|
|
1110
1161
|
);
|
|
1111
|
-
if (!guarded.length) return { truncated:
|
|
1162
|
+
if (!guarded.length) return { truncated: windowed_result, skipped };
|
|
1112
1163
|
const seed_states = await load_restart_seeds(
|
|
1113
1164
|
guarded,
|
|
1114
1165
|
target_map,
|
|
@@ -1124,8 +1175,62 @@ async function run_close_cycle(targets, deps) {
|
|
|
1124
1175
|
guard_events,
|
|
1125
1176
|
deps.correlation
|
|
1126
1177
|
);
|
|
1178
|
+
for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
|
|
1127
1179
|
return { truncated, skipped };
|
|
1128
1180
|
}
|
|
1181
|
+
async function run_windowed_closes(windowed, deps, skipped) {
|
|
1182
|
+
const min_at = deps.reactive_events_size > 0 ? await probe_min_watermarks(
|
|
1183
|
+
windowed.map((t) => t.stream),
|
|
1184
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
1185
|
+
) : /* @__PURE__ */ new Map();
|
|
1186
|
+
for (const t of windowed) {
|
|
1187
|
+
if (t.archive) await t.archive();
|
|
1188
|
+
}
|
|
1189
|
+
const truncated = await store().truncate(
|
|
1190
|
+
windowed.map((t) => ({
|
|
1191
|
+
stream: t.stream,
|
|
1192
|
+
before: t.before,
|
|
1193
|
+
max_id: min_at.get(t.stream)
|
|
1194
|
+
}))
|
|
1195
|
+
);
|
|
1196
|
+
for (const t of windowed) {
|
|
1197
|
+
if (!truncated.has(t.stream)) skipped.push(t.stream);
|
|
1198
|
+
}
|
|
1199
|
+
return truncated;
|
|
1200
|
+
}
|
|
1201
|
+
async function probe_min_watermarks(streams, page_size) {
|
|
1202
|
+
const min_at = /* @__PURE__ */ new Map();
|
|
1203
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
1204
|
+
const get_regex = (source) => {
|
|
1205
|
+
let re = source_regex.get(source);
|
|
1206
|
+
if (!re) {
|
|
1207
|
+
re = new RegExp(source);
|
|
1208
|
+
source_regex.set(source, re);
|
|
1209
|
+
}
|
|
1210
|
+
return re;
|
|
1211
|
+
};
|
|
1212
|
+
let after;
|
|
1213
|
+
for (; ; ) {
|
|
1214
|
+
let last;
|
|
1215
|
+
const { count } = await store().query_streams(
|
|
1216
|
+
(position) => {
|
|
1217
|
+
last = position.stream;
|
|
1218
|
+
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
1219
|
+
for (const stream of streams) {
|
|
1220
|
+
if (!source_re || source_re.test(stream)) {
|
|
1221
|
+
const prev = min_at.get(stream);
|
|
1222
|
+
if (prev === void 0 || position.at < prev)
|
|
1223
|
+
min_at.set(stream, position.at);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
{ after, limit: page_size, source_matches: streams }
|
|
1228
|
+
);
|
|
1229
|
+
if (count < page_size) break;
|
|
1230
|
+
after = last;
|
|
1231
|
+
}
|
|
1232
|
+
return min_at;
|
|
1233
|
+
}
|
|
1129
1234
|
async function scan_stream_heads(streams) {
|
|
1130
1235
|
const stats = await store().query_stats(streams, {
|
|
1131
1236
|
exclude: [SNAP_EVENT]
|
|
@@ -1556,7 +1661,7 @@ var DEFAULT_BATCH = 500;
|
|
|
1556
1661
|
async function snap(snapshot) {
|
|
1557
1662
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
1558
1663
|
try {
|
|
1559
|
-
await store().commit(
|
|
1664
|
+
const [committed] = await store().commit(
|
|
1560
1665
|
stream,
|
|
1561
1666
|
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
1562
1667
|
{
|
|
@@ -1566,6 +1671,7 @@ async function snap(snapshot) {
|
|
|
1566
1671
|
version
|
|
1567
1672
|
// IMPORTANT! - state events are committed right after the snapshot event
|
|
1568
1673
|
);
|
|
1674
|
+
return committed;
|
|
1569
1675
|
} catch (error) {
|
|
1570
1676
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1571
1677
|
log().warn(
|
|
@@ -1778,7 +1884,7 @@ async function action(me, action2, target, payload, options) {
|
|
|
1778
1884
|
const snapshot = await load(me, { stream, actor: target.actor });
|
|
1779
1885
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
1780
1886
|
throw new StreamClosedError(stream);
|
|
1781
|
-
const expected = expectedVersion ?? snapshot.
|
|
1887
|
+
const expected = expectedVersion ?? (snapshot.version >= 0 ? snapshot.version : void 0);
|
|
1782
1888
|
if (me.given) {
|
|
1783
1889
|
const invariants = me.given[action2] || [];
|
|
1784
1890
|
invariants.forEach(({ valid: valid2, description }) => {
|
|
@@ -1860,18 +1966,22 @@ async function action(me, action2, target, payload, options) {
|
|
|
1860
1966
|
};
|
|
1861
1967
|
});
|
|
1862
1968
|
const last = snapshots.at(-1);
|
|
1863
|
-
const
|
|
1969
|
+
const contiguous = committed[0].version === snapshot.version + 1;
|
|
1970
|
+
const snapped = contiguous && me.snap?.(last);
|
|
1971
|
+
const snap_event = snapped ? await snap(last) : void 0;
|
|
1864
1972
|
if (!me.pii_aware) {
|
|
1865
|
-
|
|
1866
|
-
stream,
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1973
|
+
if (contiguous)
|
|
1974
|
+
cache().set(stream, {
|
|
1975
|
+
stream,
|
|
1976
|
+
state: last.state,
|
|
1977
|
+
version: snap_event?.version ?? last.event.version,
|
|
1978
|
+
event_id: snap_event?.id ?? last.event.id,
|
|
1979
|
+
patches: snap_event ? 0 : last.patches,
|
|
1980
|
+
snaps: snap_event ? last.snaps + 1 : last.snaps
|
|
1981
|
+
}).catch((err) => log().error(err));
|
|
1982
|
+
else
|
|
1983
|
+
cache().invalidate(stream).catch((err) => log().error(err));
|
|
1873
1984
|
}
|
|
1874
|
-
if (snapped) void snap(last);
|
|
1875
1985
|
return snapshots;
|
|
1876
1986
|
} catch (error) {
|
|
1877
1987
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
@@ -2321,8 +2431,13 @@ function build_handle(deps) {
|
|
|
2321
2431
|
acked_at: error.at ?? event.id,
|
|
2322
2432
|
// Close the signalled stream (the autoclose reaction's aggregate,
|
|
2323
2433
|
// which differs from its synthetic lease stream); a self-closing
|
|
2324
|
-
// user reaction omits it and closes its own lease stream.
|
|
2325
|
-
|
|
2434
|
+
// user reaction omits it and closes its own lease stream. A
|
|
2435
|
+
// carried `before` makes it a windowed close (prune, not retire).
|
|
2436
|
+
close: {
|
|
2437
|
+
stream: error.stream ?? stream,
|
|
2438
|
+
archive: error.archive,
|
|
2439
|
+
before: error.before
|
|
2440
|
+
}
|
|
2326
2441
|
};
|
|
2327
2442
|
return finalize(
|
|
2328
2443
|
lease,
|
|
@@ -3625,7 +3740,19 @@ var Act = class {
|
|
|
3625
3740
|
* 7. **Cache** — invalidate (tombstoned) or warm (restarted)
|
|
3626
3741
|
* 8. **Emit "closed"** — lifecycle event with results
|
|
3627
3742
|
*
|
|
3628
|
-
*
|
|
3743
|
+
* Targets carrying `before` take the **windowed** branch instead —
|
|
3744
|
+
* close the books on a rolling window: probe the min consumer
|
|
3745
|
+
* watermark (so the boundary never rises past a lagging reaction),
|
|
3746
|
+
* run the archive callback against the cutoff, then prune the prefix
|
|
3747
|
+
* below the closest safe `__snapshot__`. No tombstone guard (the
|
|
3748
|
+
* pre-cutoff prefix is immutable), no seed, cache untouched — the
|
|
3749
|
+
* stream stays live and keeps accepting actions. Requires the state
|
|
3750
|
+
* to snapshot via `.snap(...)`; no qualifying snapshot ⇒ the stream
|
|
3751
|
+
* lands in `skipped` (retry after the next snapshot). Windowed
|
|
3752
|
+
* entries in the result echo `before` and carry the surviving
|
|
3753
|
+
* boundary snapshot as `committed`.
|
|
3754
|
+
*
|
|
3755
|
+
* @param targets - Per-stream close options (stream, restart?, archive?, before?)
|
|
3629
3756
|
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
3630
3757
|
*
|
|
3631
3758
|
* @example Archive and close
|
|
@@ -3643,6 +3770,19 @@ var Act = class {
|
|
|
3643
3770
|
* { stream: "counter-2" }, // tombstoned
|
|
3644
3771
|
* ]);
|
|
3645
3772
|
* ```
|
|
3773
|
+
*
|
|
3774
|
+
* @example Windowed close — keep the last 180 days of real events
|
|
3775
|
+
* ```typescript
|
|
3776
|
+
* const cutoff = new Date();
|
|
3777
|
+
* cutoff.setDate(cutoff.getDate() - 180);
|
|
3778
|
+
* await app.close([
|
|
3779
|
+
* {
|
|
3780
|
+
* stream: "ledger-acme",
|
|
3781
|
+
* before: cutoff,
|
|
3782
|
+
* archive: async () => { await archiveToS3("ledger-acme", cutoff); },
|
|
3783
|
+
* },
|
|
3784
|
+
* ]);
|
|
3785
|
+
* ```
|
|
3646
3786
|
*/
|
|
3647
3787
|
async close(targets) {
|
|
3648
3788
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
@@ -3740,6 +3880,7 @@ function act() {
|
|
|
3740
3880
|
autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
|
|
3741
3881
|
};
|
|
3742
3882
|
const pending_projections = [];
|
|
3883
|
+
const fold_projections = [];
|
|
3743
3884
|
const batch_handlers = /* @__PURE__ */ new Map();
|
|
3744
3885
|
const lanes = [];
|
|
3745
3886
|
let _built = false;
|
|
@@ -3805,6 +3946,8 @@ function act() {
|
|
|
3805
3946
|
withProjection: (proj) => {
|
|
3806
3947
|
merge_projection(proj, registry.events);
|
|
3807
3948
|
register_batch_handler(proj, batch_handlers);
|
|
3949
|
+
if (proj.fold)
|
|
3950
|
+
fold_projections.push(proj);
|
|
3808
3951
|
return builder;
|
|
3809
3952
|
},
|
|
3810
3953
|
withActor: () => builder,
|
|
@@ -3818,6 +3961,26 @@ function act() {
|
|
|
3818
3961
|
for (const proj of pending_projections) {
|
|
3819
3962
|
merge_projection(proj, registry.events);
|
|
3820
3963
|
register_batch_handler(proj, batch_handlers);
|
|
3964
|
+
if (proj.fold) fold_projections.push(proj);
|
|
3965
|
+
}
|
|
3966
|
+
for (const proj of fold_projections) {
|
|
3967
|
+
const fold = proj.fold;
|
|
3968
|
+
const merged = states.get(fold.name);
|
|
3969
|
+
if (!merged)
|
|
3970
|
+
throw new Error(
|
|
3971
|
+
`State projection "${proj.target}" folds "${fold.name}", which is not registered \u2014 add it via withState/withSlice before build()`
|
|
3972
|
+
);
|
|
3973
|
+
const missing = Object.keys(merged.events).filter(
|
|
3974
|
+
(event_name) => !(event_name in proj.events)
|
|
3975
|
+
);
|
|
3976
|
+
if (missing.length > 0)
|
|
3977
|
+
throw new Error(
|
|
3978
|
+
`State projection "${proj.target}" of "${fold.name}" is missing events ${missing.join(", ")} \u2014 pass every partial of the state to .of()`
|
|
3979
|
+
);
|
|
3980
|
+
batch_handlers.set(
|
|
3981
|
+
proj.target,
|
|
3982
|
+
make_fold_handler(merged, fold.flush, fold.config)
|
|
3983
|
+
);
|
|
3821
3984
|
}
|
|
3822
3985
|
finalize_deprecations();
|
|
3823
3986
|
validate_lane_references(registry, lanes);
|
|
@@ -3960,39 +4123,52 @@ function _projection(target) {
|
|
|
3960
4123
|
batchHandler: handler
|
|
3961
4124
|
})
|
|
3962
4125
|
}),
|
|
3963
|
-
of: (
|
|
4126
|
+
of: (...args) => {
|
|
3964
4127
|
if (Object.keys(events).length > 0)
|
|
3965
4128
|
throw new Error(
|
|
3966
4129
|
`Projection "${target}" mixes .of() with .on() handlers \u2014 a projection either folds a state or declares handlers, never both`
|
|
3967
4130
|
);
|
|
4131
|
+
const is_state = (a) => !!a && typeof a.init === "function";
|
|
4132
|
+
const partials = args.filter(is_state);
|
|
4133
|
+
const options = args.find((a) => !is_state(a)) ?? {};
|
|
4134
|
+
if (partials.length === 0)
|
|
4135
|
+
throw new Error(`Projection "${target}" .of() requires a state`);
|
|
4136
|
+
const name = partials[0].name;
|
|
4137
|
+
for (const partial of partials)
|
|
4138
|
+
if (partial.name !== name)
|
|
4139
|
+
throw new Error(
|
|
4140
|
+
`Projection "${target}" .of() partials must share one state name \u2014 got "${name}" and "${partial.name}"`
|
|
4141
|
+
);
|
|
3968
4142
|
const config2 = resolveFoldConfig(options);
|
|
3969
4143
|
const fold_events = {};
|
|
3970
|
-
for (const
|
|
3971
|
-
const
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
4144
|
+
for (const partial of partials)
|
|
4145
|
+
for (const [event_name, schema] of Object.entries(partial.events)) {
|
|
4146
|
+
if (event_name in fold_events) continue;
|
|
4147
|
+
const noop = {
|
|
4148
|
+
[`${target}_fold`]: async () => {
|
|
4149
|
+
}
|
|
4150
|
+
}[`${target}_fold`];
|
|
4151
|
+
fold_events[event_name] = {
|
|
4152
|
+
schema,
|
|
4153
|
+
reactions: /* @__PURE__ */ new Map([
|
|
4154
|
+
[
|
|
4155
|
+
`${target}_fold`,
|
|
4156
|
+
{
|
|
4157
|
+
handler: noop,
|
|
4158
|
+
resolver: { target },
|
|
4159
|
+
options: { blockOnError: true, maxRetries: 3 }
|
|
4160
|
+
}
|
|
4161
|
+
]
|
|
4162
|
+
])
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
3989
4165
|
return {
|
|
3990
4166
|
flush: (handler) => ({
|
|
3991
4167
|
build: () => ({
|
|
3992
4168
|
_tag: "Projection",
|
|
3993
4169
|
events: fold_events,
|
|
3994
4170
|
target,
|
|
3995
|
-
|
|
4171
|
+
fold: { name, flush: handler, config: config2 }
|
|
3996
4172
|
})
|
|
3997
4173
|
})
|
|
3998
4174
|
};
|
|
@@ -4139,10 +4315,16 @@ function action_builder(state2) {
|
|
|
4139
4315
|
".autocloses(...) requires a policy object; got " + typeof policy
|
|
4140
4316
|
);
|
|
4141
4317
|
}
|
|
4318
|
+
if (policy.keep && !internal.snap) {
|
|
4319
|
+
throw new Error(
|
|
4320
|
+
".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."
|
|
4321
|
+
);
|
|
4322
|
+
}
|
|
4142
4323
|
internal.autoclose = compile_autoclose_policy(
|
|
4143
4324
|
policy
|
|
4144
4325
|
);
|
|
4145
|
-
internal.
|
|
4326
|
+
internal.autoclose_after_days = policy_min_after_days(policy);
|
|
4327
|
+
internal.autoclose_keep_days = policy_keep_days(policy);
|
|
4146
4328
|
return builder;
|
|
4147
4329
|
},
|
|
4148
4330
|
archives(archive) {
|