@rotorsoft/act 1.18.8 → 1.20.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/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/projection-builder.d.ts +64 -5
- package/dist/@types/builders/projection-builder.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +4 -3
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/state-fold.d.ts +18 -0
- package/dist/@types/internal/state-fold.d.ts.map +1 -0
- package/dist/@types/types/ports.d.ts +5 -0
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +12 -0
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/index.cjs +176 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +176 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1242,6 +1242,7 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
|
|
|
1242
1242
|
const state2 = seed_states.get(stream);
|
|
1243
1243
|
if (state2 && entry) {
|
|
1244
1244
|
await cache().set(stream, {
|
|
1245
|
+
stream,
|
|
1245
1246
|
state: state2,
|
|
1246
1247
|
version: entry.committed.version,
|
|
1247
1248
|
event_id: entry.committed.id,
|
|
@@ -1555,7 +1556,7 @@ var DEFAULT_BATCH = 500;
|
|
|
1555
1556
|
async function snap(snapshot) {
|
|
1556
1557
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
1557
1558
|
try {
|
|
1558
|
-
await store().commit(
|
|
1559
|
+
const [committed] = await store().commit(
|
|
1559
1560
|
stream,
|
|
1560
1561
|
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
1561
1562
|
{
|
|
@@ -1565,6 +1566,7 @@ async function snap(snapshot) {
|
|
|
1565
1566
|
version
|
|
1566
1567
|
// IMPORTANT! - state events are committed right after the snapshot event
|
|
1567
1568
|
);
|
|
1569
|
+
return committed;
|
|
1568
1570
|
} catch (error) {
|
|
1569
1571
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1570
1572
|
log().warn(
|
|
@@ -1746,6 +1748,7 @@ async function load(me, target, callback) {
|
|
|
1746
1748
|
);
|
|
1747
1749
|
if (replayed > 0 && !time_travel && event && !me.pii_aware) {
|
|
1748
1750
|
await cache().set(stream, {
|
|
1751
|
+
stream,
|
|
1749
1752
|
state: state2,
|
|
1750
1753
|
version: event.version,
|
|
1751
1754
|
event_id: event.id,
|
|
@@ -1776,7 +1779,7 @@ async function action(me, action2, target, payload, options) {
|
|
|
1776
1779
|
const snapshot = await load(me, { stream, actor: target.actor });
|
|
1777
1780
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
1778
1781
|
throw new StreamClosedError(stream);
|
|
1779
|
-
const expected = expectedVersion ?? snapshot.
|
|
1782
|
+
const expected = expectedVersion ?? (snapshot.version >= 0 ? snapshot.version : void 0);
|
|
1780
1783
|
if (me.given) {
|
|
1781
1784
|
const invariants = me.given[action2] || [];
|
|
1782
1785
|
invariants.forEach(({ valid: valid2, description }) => {
|
|
@@ -1858,17 +1861,22 @@ async function action(me, action2, target, payload, options) {
|
|
|
1858
1861
|
};
|
|
1859
1862
|
});
|
|
1860
1863
|
const last = snapshots.at(-1);
|
|
1861
|
-
const
|
|
1864
|
+
const contiguous = committed[0].version === snapshot.version + 1;
|
|
1865
|
+
const snapped = contiguous && me.snap?.(last);
|
|
1866
|
+
const snap_event = snapped ? await snap(last) : void 0;
|
|
1862
1867
|
if (!me.pii_aware) {
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1868
|
+
if (contiguous)
|
|
1869
|
+
cache().set(stream, {
|
|
1870
|
+
stream,
|
|
1871
|
+
state: last.state,
|
|
1872
|
+
version: snap_event?.version ?? last.event.version,
|
|
1873
|
+
event_id: snap_event?.id ?? last.event.id,
|
|
1874
|
+
patches: snap_event ? 0 : last.patches,
|
|
1875
|
+
snaps: snap_event ? last.snaps + 1 : last.snaps
|
|
1876
|
+
}).catch((err) => log().error(err));
|
|
1877
|
+
else
|
|
1878
|
+
cache().invalidate(stream).catch((err) => log().error(err));
|
|
1870
1879
|
}
|
|
1871
|
-
if (snapped) void snap(last);
|
|
1872
1880
|
return snapshots;
|
|
1873
1881
|
} catch (error) {
|
|
1874
1882
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
@@ -2420,6 +2428,89 @@ var SettleLoop = class {
|
|
|
2420
2428
|
}
|
|
2421
2429
|
};
|
|
2422
2430
|
|
|
2431
|
+
// src/internal/state-fold.ts
|
|
2432
|
+
import { patch as patch2 } from "@rotorsoft/act-patch";
|
|
2433
|
+
import { z as z5 } from "zod";
|
|
2434
|
+
var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
|
|
2435
|
+
var DEFAULT_MAX_CACHED_STATES = 1e4;
|
|
2436
|
+
var FoldOptionsSchema = z5.object({
|
|
2437
|
+
flushEvery: z5.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
|
|
2438
|
+
maxCachedStates: z5.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
|
|
2439
|
+
});
|
|
2440
|
+
function resolveFoldConfig(options) {
|
|
2441
|
+
return FoldOptionsSchema.parse(options);
|
|
2442
|
+
}
|
|
2443
|
+
function make_fold_handler(me, flush, config2) {
|
|
2444
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
2445
|
+
const row = ({ dirty: _, ...row2 }) => row2;
|
|
2446
|
+
const flush_dirty = async () => {
|
|
2447
|
+
const rows = [];
|
|
2448
|
+
const flushed = [];
|
|
2449
|
+
for (const f of cache2.values())
|
|
2450
|
+
if (f.dirty) {
|
|
2451
|
+
rows.push(row(f));
|
|
2452
|
+
flushed.push(f);
|
|
2453
|
+
}
|
|
2454
|
+
if (rows.length === 0) return;
|
|
2455
|
+
await flush(rows);
|
|
2456
|
+
for (const f of flushed) f.dirty = false;
|
|
2457
|
+
};
|
|
2458
|
+
return async (events) => {
|
|
2459
|
+
let folded_since_flush = 0;
|
|
2460
|
+
for (const event of events) {
|
|
2461
|
+
const stream = event.stream;
|
|
2462
|
+
let fold = cache2.get(stream);
|
|
2463
|
+
if (fold) {
|
|
2464
|
+
cache2.delete(stream);
|
|
2465
|
+
cache2.set(stream, fold);
|
|
2466
|
+
} else {
|
|
2467
|
+
if (cache2.size >= config2.maxCachedStates) {
|
|
2468
|
+
const oldest = cache2.keys().next().value;
|
|
2469
|
+
const evictee = cache2.get(oldest);
|
|
2470
|
+
if (evictee.dirty) await flush([row(evictee)]);
|
|
2471
|
+
cache2.delete(oldest);
|
|
2472
|
+
}
|
|
2473
|
+
const snapshot = await load(me, { stream });
|
|
2474
|
+
const entry = await cache().get(stream);
|
|
2475
|
+
const frontier = entry ?? {
|
|
2476
|
+
version: snapshot.event.version,
|
|
2477
|
+
event_id: snapshot.event.id,
|
|
2478
|
+
patches: snapshot.patches,
|
|
2479
|
+
snaps: snapshot.snaps
|
|
2480
|
+
};
|
|
2481
|
+
fold = {
|
|
2482
|
+
stream,
|
|
2483
|
+
state: snapshot.state,
|
|
2484
|
+
version: frontier.version,
|
|
2485
|
+
event_id: frontier.event_id,
|
|
2486
|
+
patches: frontier.patches,
|
|
2487
|
+
snaps: frontier.snaps,
|
|
2488
|
+
dirty: true
|
|
2489
|
+
};
|
|
2490
|
+
cache2.set(stream, fold);
|
|
2491
|
+
}
|
|
2492
|
+
if (event.id > fold.event_id) {
|
|
2493
|
+
const reducer = me.patch[event.name];
|
|
2494
|
+
fold.state = patch2(
|
|
2495
|
+
fold.state,
|
|
2496
|
+
reducer(event, fold.state)
|
|
2497
|
+
);
|
|
2498
|
+
fold.version = event.version;
|
|
2499
|
+
fold.event_id = event.id;
|
|
2500
|
+
fold.patches++;
|
|
2501
|
+
fold.dirty = true;
|
|
2502
|
+
} else {
|
|
2503
|
+
fold.dirty = true;
|
|
2504
|
+
}
|
|
2505
|
+
if (++folded_since_flush >= config2.flushEvery) {
|
|
2506
|
+
await flush_dirty();
|
|
2507
|
+
folded_since_flush = 0;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
await flush_dirty();
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2423
2514
|
// src/act.ts
|
|
2424
2515
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2425
2516
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
@@ -3654,6 +3745,7 @@ function act() {
|
|
|
3654
3745
|
autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
|
|
3655
3746
|
};
|
|
3656
3747
|
const pending_projections = [];
|
|
3748
|
+
const fold_projections = [];
|
|
3657
3749
|
const batch_handlers = /* @__PURE__ */ new Map();
|
|
3658
3750
|
const lanes = [];
|
|
3659
3751
|
let _built = false;
|
|
@@ -3719,6 +3811,8 @@ function act() {
|
|
|
3719
3811
|
withProjection: (proj) => {
|
|
3720
3812
|
merge_projection(proj, registry.events);
|
|
3721
3813
|
register_batch_handler(proj, batch_handlers);
|
|
3814
|
+
if (proj.fold)
|
|
3815
|
+
fold_projections.push(proj);
|
|
3722
3816
|
return builder;
|
|
3723
3817
|
},
|
|
3724
3818
|
withActor: () => builder,
|
|
@@ -3732,6 +3826,26 @@ function act() {
|
|
|
3732
3826
|
for (const proj of pending_projections) {
|
|
3733
3827
|
merge_projection(proj, registry.events);
|
|
3734
3828
|
register_batch_handler(proj, batch_handlers);
|
|
3829
|
+
if (proj.fold) fold_projections.push(proj);
|
|
3830
|
+
}
|
|
3831
|
+
for (const proj of fold_projections) {
|
|
3832
|
+
const fold = proj.fold;
|
|
3833
|
+
const merged = states.get(fold.name);
|
|
3834
|
+
if (!merged)
|
|
3835
|
+
throw new Error(
|
|
3836
|
+
`State projection "${proj.target}" folds "${fold.name}", which is not registered \u2014 add it via withState/withSlice before build()`
|
|
3837
|
+
);
|
|
3838
|
+
const missing = Object.keys(merged.events).filter(
|
|
3839
|
+
(event_name) => !(event_name in proj.events)
|
|
3840
|
+
);
|
|
3841
|
+
if (missing.length > 0)
|
|
3842
|
+
throw new Error(
|
|
3843
|
+
`State projection "${proj.target}" of "${fold.name}" is missing events ${missing.join(", ")} \u2014 pass every partial of the state to .of()`
|
|
3844
|
+
);
|
|
3845
|
+
batch_handlers.set(
|
|
3846
|
+
proj.target,
|
|
3847
|
+
make_fold_handler(merged, fold.flush, fold.config)
|
|
3848
|
+
);
|
|
3735
3849
|
}
|
|
3736
3850
|
finalize_deprecations();
|
|
3737
3851
|
validate_lane_references(registry, lanes);
|
|
@@ -3873,7 +3987,57 @@ function _projection(target) {
|
|
|
3873
3987
|
target,
|
|
3874
3988
|
batchHandler: handler
|
|
3875
3989
|
})
|
|
3876
|
-
})
|
|
3990
|
+
}),
|
|
3991
|
+
of: (...args) => {
|
|
3992
|
+
if (Object.keys(events).length > 0)
|
|
3993
|
+
throw new Error(
|
|
3994
|
+
`Projection "${target}" mixes .of() with .on() handlers \u2014 a projection either folds a state or declares handlers, never both`
|
|
3995
|
+
);
|
|
3996
|
+
const is_state = (a) => !!a && typeof a.init === "function";
|
|
3997
|
+
const partials = args.filter(is_state);
|
|
3998
|
+
const options = args.find((a) => !is_state(a)) ?? {};
|
|
3999
|
+
if (partials.length === 0)
|
|
4000
|
+
throw new Error(`Projection "${target}" .of() requires a state`);
|
|
4001
|
+
const name = partials[0].name;
|
|
4002
|
+
for (const partial of partials)
|
|
4003
|
+
if (partial.name !== name)
|
|
4004
|
+
throw new Error(
|
|
4005
|
+
`Projection "${target}" .of() partials must share one state name \u2014 got "${name}" and "${partial.name}"`
|
|
4006
|
+
);
|
|
4007
|
+
const config2 = resolveFoldConfig(options);
|
|
4008
|
+
const fold_events = {};
|
|
4009
|
+
for (const partial of partials)
|
|
4010
|
+
for (const [event_name, schema] of Object.entries(partial.events)) {
|
|
4011
|
+
if (event_name in fold_events) continue;
|
|
4012
|
+
const noop = {
|
|
4013
|
+
[`${target}_fold`]: async () => {
|
|
4014
|
+
}
|
|
4015
|
+
}[`${target}_fold`];
|
|
4016
|
+
fold_events[event_name] = {
|
|
4017
|
+
schema,
|
|
4018
|
+
reactions: /* @__PURE__ */ new Map([
|
|
4019
|
+
[
|
|
4020
|
+
`${target}_fold`,
|
|
4021
|
+
{
|
|
4022
|
+
handler: noop,
|
|
4023
|
+
resolver: { target },
|
|
4024
|
+
options: { blockOnError: true, maxRetries: 3 }
|
|
4025
|
+
}
|
|
4026
|
+
]
|
|
4027
|
+
])
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
return {
|
|
4031
|
+
flush: (handler) => ({
|
|
4032
|
+
build: () => ({
|
|
4033
|
+
_tag: "Projection",
|
|
4034
|
+
events: fold_events,
|
|
4035
|
+
target,
|
|
4036
|
+
fold: { name, flush: handler, config: config2 }
|
|
4037
|
+
})
|
|
4038
|
+
})
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
3877
4041
|
});
|
|
3878
4042
|
}
|
|
3879
4043
|
return base;
|