@rotorsoft/act 1.18.7 → 1.19.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/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,
@@ -1746,6 +1747,7 @@ async function load(me, target, callback) {
1746
1747
  );
1747
1748
  if (replayed > 0 && !time_travel && event && !me.pii_aware) {
1748
1749
  await cache().set(stream, {
1750
+ stream,
1749
1751
  state: state2,
1750
1752
  version: event.version,
1751
1753
  event_id: event.id,
@@ -1861,6 +1863,7 @@ async function action(me, action2, target, payload, options) {
1861
1863
  const snapped = me.snap?.(last);
1862
1864
  if (!me.pii_aware) {
1863
1865
  cache().set(stream, {
1866
+ stream,
1864
1867
  state: last.state,
1865
1868
  version: last.event.version,
1866
1869
  event_id: last.event.id,
@@ -2420,6 +2423,89 @@ var SettleLoop = class {
2420
2423
  }
2421
2424
  };
2422
2425
 
2426
+ // src/internal/state-fold.ts
2427
+ import { patch as patch2 } from "@rotorsoft/act-patch";
2428
+ import { z as z5 } from "zod";
2429
+ var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
2430
+ var DEFAULT_MAX_CACHED_STATES = 1e4;
2431
+ var FoldOptionsSchema = z5.object({
2432
+ flushEvery: z5.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
2433
+ maxCachedStates: z5.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
2434
+ });
2435
+ function resolveFoldConfig(options) {
2436
+ return FoldOptionsSchema.parse(options);
2437
+ }
2438
+ function make_fold_handler(me, flush, config2) {
2439
+ const cache2 = /* @__PURE__ */ new Map();
2440
+ const row = ({ dirty: _, ...row2 }) => row2;
2441
+ const flush_dirty = async () => {
2442
+ const rows = [];
2443
+ const flushed = [];
2444
+ for (const f of cache2.values())
2445
+ if (f.dirty) {
2446
+ rows.push(row(f));
2447
+ flushed.push(f);
2448
+ }
2449
+ if (rows.length === 0) return;
2450
+ await flush(rows);
2451
+ for (const f of flushed) f.dirty = false;
2452
+ };
2453
+ return async (events) => {
2454
+ let folded_since_flush = 0;
2455
+ for (const event of events) {
2456
+ const stream = event.stream;
2457
+ let fold = cache2.get(stream);
2458
+ if (fold) {
2459
+ cache2.delete(stream);
2460
+ cache2.set(stream, fold);
2461
+ } else {
2462
+ if (cache2.size >= config2.maxCachedStates) {
2463
+ const oldest = cache2.keys().next().value;
2464
+ const evictee = cache2.get(oldest);
2465
+ if (evictee.dirty) await flush([row(evictee)]);
2466
+ cache2.delete(oldest);
2467
+ }
2468
+ const snapshot = await load(me, { stream });
2469
+ const entry = await cache().get(stream);
2470
+ const frontier = entry ?? {
2471
+ version: snapshot.event.version,
2472
+ event_id: snapshot.event.id,
2473
+ patches: snapshot.patches,
2474
+ snaps: snapshot.snaps
2475
+ };
2476
+ fold = {
2477
+ stream,
2478
+ state: snapshot.state,
2479
+ version: frontier.version,
2480
+ event_id: frontier.event_id,
2481
+ patches: frontier.patches,
2482
+ snaps: frontier.snaps,
2483
+ dirty: true
2484
+ };
2485
+ cache2.set(stream, fold);
2486
+ }
2487
+ if (event.id > fold.event_id) {
2488
+ const reducer = me.patch[event.name];
2489
+ fold.state = patch2(
2490
+ fold.state,
2491
+ reducer(event, fold.state)
2492
+ );
2493
+ fold.version = event.version;
2494
+ fold.event_id = event.id;
2495
+ fold.patches++;
2496
+ fold.dirty = true;
2497
+ } else {
2498
+ fold.dirty = true;
2499
+ }
2500
+ if (++folded_since_flush >= config2.flushEvery) {
2501
+ await flush_dirty();
2502
+ folded_since_flush = 0;
2503
+ }
2504
+ }
2505
+ await flush_dirty();
2506
+ };
2507
+ }
2508
+
2423
2509
  // src/act.ts
2424
2510
  var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
2425
2511
  var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
@@ -3873,7 +3959,44 @@ function _projection(target) {
3873
3959
  target,
3874
3960
  batchHandler: handler
3875
3961
  })
3876
- })
3962
+ }),
3963
+ of: (state2, options = {}) => {
3964
+ if (Object.keys(events).length > 0)
3965
+ throw new Error(
3966
+ `Projection "${target}" mixes .of() with .on() handlers \u2014 a projection either folds a state or declares handlers, never both`
3967
+ );
3968
+ const config2 = resolveFoldConfig(options);
3969
+ const fold_events = {};
3970
+ for (const [name, schema] of Object.entries(state2.events)) {
3971
+ const noop = {
3972
+ [`${target}_fold`]: async () => {
3973
+ }
3974
+ }[`${target}_fold`];
3975
+ fold_events[name] = {
3976
+ schema,
3977
+ reactions: /* @__PURE__ */ new Map([
3978
+ [
3979
+ `${target}_fold`,
3980
+ {
3981
+ handler: noop,
3982
+ resolver: { target },
3983
+ options: { blockOnError: true, maxRetries: 3 }
3984
+ }
3985
+ ]
3986
+ ])
3987
+ };
3988
+ }
3989
+ return {
3990
+ flush: (handler) => ({
3991
+ build: () => ({
3992
+ _tag: "Projection",
3993
+ events: fold_events,
3994
+ target,
3995
+ batchHandler: make_fold_handler(state2, handler, config2)
3996
+ })
3997
+ })
3998
+ };
3999
+ }
3877
4000
  });
3878
4001
  }
3879
4002
  return base;