@rotorsoft/act 1.10.1 → 1.11.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.
Files changed (38) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +71 -9
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  5. package/dist/@types/builders/slice-builder.d.ts +1 -1
  6. package/dist/@types/builders/state-builder.d.ts +88 -1
  7. package/dist/@types/builders/state-builder.d.ts.map +1 -1
  8. package/dist/@types/internal/autoclose-config.d.ts +72 -0
  9. package/dist/@types/internal/autoclose-config.d.ts.map +1 -0
  10. package/dist/@types/internal/autoclose-cycle.d.ts +171 -0
  11. package/dist/@types/internal/autoclose-cycle.d.ts.map +1 -0
  12. package/dist/@types/internal/event-sourcing.d.ts +7 -7
  13. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  14. package/dist/@types/internal/index.d.ts +3 -1
  15. package/dist/@types/internal/index.d.ts.map +1 -1
  16. package/dist/@types/internal/reactions.d.ts.map +1 -1
  17. package/dist/@types/internal/sensitive.d.ts +6 -23
  18. package/dist/@types/internal/sensitive.d.ts.map +1 -1
  19. package/dist/@types/internal/tracing.d.ts.map +1 -1
  20. package/dist/@types/types/action.d.ts +141 -16
  21. package/dist/@types/types/action.d.ts.map +1 -1
  22. package/dist/@types/types/registry.d.ts +35 -3
  23. package/dist/@types/types/registry.d.ts.map +1 -1
  24. package/dist/{chunk-3Z2HU726.js → chunk-5PWLGL5E.js} +2 -2
  25. package/dist/{chunk-BY5JPOZR.js → chunk-NGVWJXEF.js} +9 -24
  26. package/dist/{chunk-BY5JPOZR.js.map → chunk-NGVWJXEF.js.map} +1 -1
  27. package/dist/index.cjs +411 -195
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +399 -174
  30. package/dist/index.js.map +1 -1
  31. package/dist/test/index.cjs +2 -2
  32. package/dist/test/index.cjs.map +1 -1
  33. package/dist/test/index.js +4 -4
  34. package/dist/test/index.js.map +1 -1
  35. package/dist/types/index.cjs.map +1 -1
  36. package/dist/types/index.js +1 -1
  37. package/package.json +2 -2
  38. /package/dist/{chunk-3Z2HU726.js.map → chunk-5PWLGL5E.js.map} +0 -0
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  sleep,
20
20
  store,
21
21
  validate
22
- } from "./chunk-3Z2HU726.js";
22
+ } from "./chunk-5PWLGL5E.js";
23
23
  import {
24
24
  ActorSchema,
25
25
  CausationEventSchema,
@@ -40,11 +40,10 @@ import {
40
40
  ZodEmpty,
41
41
  pii_fields,
42
42
  pii_gate,
43
- pii_merge,
44
43
  pii_split,
45
44
  pii_strip,
46
45
  sensitive
47
- } from "./chunk-BY5JPOZR.js";
46
+ } from "./chunk-NGVWJXEF.js";
48
47
  import "./chunk-5WRI5ZAA.js";
49
48
 
50
49
  // src/signals.ts
@@ -487,57 +486,24 @@ var PASS_FACTORIES = {
487
486
  "clock-anomalies": make_clock_anomalies_pass
488
487
  };
489
488
 
490
- // src/internal/build-classify.ts
491
- var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
492
- function classify_registry(registry, states) {
493
- const statics = /* @__PURE__ */ new Map();
494
- const reactive_events = /* @__PURE__ */ new Set();
495
- const event_to_lanes = /* @__PURE__ */ new Map();
496
- let has_dynamic_resolvers = false;
497
- for (const [name, register] of Object.entries(registry.events)) {
498
- if (register.reactions.size > 0) reactive_events.add(name);
499
- for (const reaction of register.reactions.values()) {
500
- if (typeof reaction.resolver === "function") {
501
- has_dynamic_resolvers = true;
502
- event_to_lanes.set(name, ALL_LANES);
503
- } else {
504
- const { target, source, priority = 0, lane } = reaction.resolver;
505
- const lane_name = lane ?? "default";
506
- const existing_lanes = event_to_lanes.get(name);
507
- if (existing_lanes !== ALL_LANES) {
508
- const set = existing_lanes ?? /* @__PURE__ */ new Set();
509
- set.add(lane_name);
510
- event_to_lanes.set(name, set);
511
- }
512
- const key = `${target}|${source ?? ""}`;
513
- const existing = statics.get(key);
514
- if (!existing) {
515
- statics.set(key, { stream: target, source, priority, lane });
516
- } else {
517
- if ((existing.lane ?? void 0) !== (lane ?? void 0))
518
- throw new Error(
519
- `Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
520
- );
521
- if (priority > existing.priority) {
522
- statics.set(key, { ...existing, priority });
523
- }
524
- }
525
- }
526
- }
527
- }
528
- const event_to_state = /* @__PURE__ */ new Map();
529
- for (const merged of states.values()) {
530
- for (const event_name of Object.keys(merged.events)) {
531
- event_to_state.set(event_name, merged);
532
- }
533
- }
534
- return {
535
- static_targets: [...statics.values()],
536
- has_dynamic_resolvers,
537
- reactive_events,
538
- event_to_state,
539
- event_to_lanes
540
- };
489
+ // src/internal/autoclose-config.ts
490
+ import { z } from "zod";
491
+ var DEFAULT_AUTOCLOSE_CYCLE_MS = 6e4;
492
+ var DEFAULT_CLOSE_BATCH_SIZE = 64;
493
+ var DEFAULT_CLOSE_YIELD_MS = 0;
494
+ var AutocloseOptionsSchema = z.object({
495
+ autocloseCycleMs: z.number().min(1e4).max(36e5).default(DEFAULT_AUTOCLOSE_CYCLE_MS),
496
+ closeBatchSize: z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
497
+ closeYieldMs: z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
498
+ closeOnError: z.boolean().default(false)
499
+ });
500
+ function resolveAutocloseConfig(options) {
501
+ return AutocloseOptionsSchema.parse({
502
+ autocloseCycleMs: options?.autocloseCycleMs,
503
+ closeBatchSize: options?.closeBatchSize,
504
+ closeYieldMs: options?.closeYieldMs,
505
+ closeOnError: options?.closeOnError
506
+ });
541
507
  }
542
508
 
543
509
  // src/internal/close-cycle.ts
@@ -639,7 +605,7 @@ async function load_restart_seeds(guarded, target_map, stream_info, event_to_sta
639
605
  );
640
606
  return;
641
607
  }
642
- const snap2 = await load2(owner_state, stream);
608
+ const snap2 = await load2(owner_state, { stream });
643
609
  seed_states.set(stream, snap2.state);
644
610
  })
645
611
  );
@@ -687,6 +653,233 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
687
653
  return truncated;
688
654
  }
689
655
 
656
+ // src/internal/correlator.ts
657
+ import { randomInt } from "crypto";
658
+ var BASE = 36;
659
+ var SEG_WIDTH = 4;
660
+ var SEG_SPACE = BASE ** SEG_WIDTH;
661
+ function seg(n) {
662
+ return n.toString(BASE).padStart(SEG_WIDTH, "0");
663
+ }
664
+ var default_correlator = ({ state: state2, action: action2 }) => {
665
+ const s = state2.slice(0, SEG_WIDTH).toLowerCase();
666
+ const a = action2.slice(0, SEG_WIDTH).toLowerCase();
667
+ const ts = seg(Date.now() % SEG_SPACE);
668
+ const rnd = seg(randomInt(SEG_SPACE));
669
+ return `${s}-${a}-${ts}${rnd}`;
670
+ };
671
+ function close_correlation(correlator, actor) {
672
+ return correlator({
673
+ state: "$close",
674
+ action: "close",
675
+ stream: "$close",
676
+ actor
677
+ });
678
+ }
679
+
680
+ // src/internal/autoclose-cycle.ts
681
+ function yield_between_batches(ms) {
682
+ if (ms <= 0) return Promise.resolve();
683
+ return new Promise((resolve) => setTimeout(resolve, ms));
684
+ }
685
+ async function run_autoclose_cycle(deps) {
686
+ const s = store();
687
+ let inspected = 0;
688
+ let evaluated = 0;
689
+ let predicate_errors = 0;
690
+ const truncated = /* @__PURE__ */ new Map();
691
+ const skipped = [];
692
+ const stats = await s.query_stats(
693
+ {},
694
+ { count: true, exclude: [TOMBSTONE_EVENT] }
695
+ );
696
+ const flush_batch = async (candidates2) => {
697
+ if (candidates2.length === 0) return;
698
+ const result = await run_close_cycle(candidates2, {
699
+ reactive_events_size: deps.reactive_events_size,
700
+ event_to_state: deps.event_to_state,
701
+ load: deps.load,
702
+ tombstone: deps.tombstone,
703
+ logger: deps.logger,
704
+ correlation: deps.correlation
705
+ });
706
+ for (const [stream, entry] of result.truncated) {
707
+ truncated.set(stream, entry);
708
+ }
709
+ for (const stream of result.skipped) skipped.push(stream);
710
+ await yield_between_batches(deps.config.closeYieldMs);
711
+ };
712
+ let candidates = [];
713
+ for (const [stream, { head, count }] of stats) {
714
+ inspected += 1;
715
+ const owner = deps.event_to_state.get(head.name);
716
+ if (!owner) continue;
717
+ const predicate = deps.autoclose_policy(owner.name);
718
+ if (!predicate) continue;
719
+ let close = false;
720
+ try {
721
+ close = predicate(stream, head, count ?? 0);
722
+ evaluated += 1;
723
+ } catch (err) {
724
+ predicate_errors += 1;
725
+ deps.logger.error(
726
+ `Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
727
+ );
728
+ close = deps.config.closeOnError;
729
+ }
730
+ if (!close) continue;
731
+ const archiver = deps.autoclose_archiver(owner.name);
732
+ const archive = archiver ? () => archiver(stream, head) : void 0;
733
+ candidates.push({ stream, archive });
734
+ if (candidates.length >= deps.config.closeBatchSize) {
735
+ await flush_batch(candidates);
736
+ candidates = [];
737
+ }
738
+ }
739
+ await flush_batch(candidates);
740
+ return {
741
+ inspected,
742
+ evaluated,
743
+ predicate_errors,
744
+ close_result: { truncated, skipped }
745
+ };
746
+ }
747
+ var AutocloseController = class {
748
+ deps;
749
+ _timer;
750
+ /**
751
+ * Reentrancy guard. The interval fires the next tick on cadence
752
+ * regardless of how long the previous one took; this flag drops
753
+ * overlapping ticks so the cycle stays sequential per controller.
754
+ */
755
+ _running = false;
756
+ constructor(deps) {
757
+ this.deps = deps;
758
+ }
759
+ /**
760
+ * Start the cycle ticker. Returns `false` if already running so
761
+ * accidental double-start doesn't stack timers (matches
762
+ * `_correlate.start_polling` semantics).
763
+ */
764
+ start() {
765
+ if (this._timer) return false;
766
+ this._timer = setInterval(() => {
767
+ this.run_once().catch((err) => {
768
+ this.deps.logger.error(
769
+ `Autoclose cycle errored: ${err instanceof Error ? err.message : String(err)}`
770
+ );
771
+ });
772
+ }, this.deps.config.autocloseCycleMs);
773
+ this._timer.unref();
774
+ return true;
775
+ }
776
+ /**
777
+ * Stop the cycle ticker. Idempotent.
778
+ */
779
+ stop() {
780
+ if (this._timer) {
781
+ clearInterval(this._timer);
782
+ this._timer = void 0;
783
+ }
784
+ }
785
+ /**
786
+ * Whether the ticker is currently running.
787
+ */
788
+ get is_running() {
789
+ return this._timer !== void 0;
790
+ }
791
+ /**
792
+ * Run one cycle synchronously. The controller's interval callback
793
+ * delegates here; tests can invoke it directly to deterministically
794
+ * exercise the cycle without spinning real timers.
795
+ *
796
+ * Overlapping invocations short-circuit: if a previous tick is
797
+ * still running, this one drops and returns `null`. The orchestrator
798
+ * never sees a queue of pending ticks regardless of how slow the
799
+ * predicate / archive / truncate are.
800
+ */
801
+ async run_once() {
802
+ if (this._running) return null;
803
+ this._running = true;
804
+ try {
805
+ const correlation = close_correlation(
806
+ this.deps.correlator,
807
+ this.deps.close_actor
808
+ );
809
+ const result = await run_autoclose_cycle({
810
+ autoclose_policy: this.deps.autoclose_policy,
811
+ autoclose_archiver: this.deps.autoclose_archiver,
812
+ event_to_state: this.deps.event_to_state,
813
+ reactive_events_size: this.deps.reactive_events_size,
814
+ load: this.deps.load,
815
+ tombstone: this.deps.tombstone,
816
+ logger: this.deps.logger,
817
+ config: this.deps.config,
818
+ correlation
819
+ });
820
+ if (result.close_result.truncated.size > 0) {
821
+ this.deps.on_closed(result.close_result);
822
+ }
823
+ return result;
824
+ } finally {
825
+ this._running = false;
826
+ }
827
+ }
828
+ };
829
+
830
+ // src/internal/build-classify.ts
831
+ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
832
+ function classify_registry(registry, states) {
833
+ const statics = /* @__PURE__ */ new Map();
834
+ const reactive_events = /* @__PURE__ */ new Set();
835
+ const event_to_lanes = /* @__PURE__ */ new Map();
836
+ let has_dynamic_resolvers = false;
837
+ for (const [name, register] of Object.entries(registry.events)) {
838
+ if (register.reactions.size > 0) reactive_events.add(name);
839
+ for (const reaction of register.reactions.values()) {
840
+ if (typeof reaction.resolver === "function") {
841
+ has_dynamic_resolvers = true;
842
+ event_to_lanes.set(name, ALL_LANES);
843
+ } else {
844
+ const { target, source, priority = 0, lane } = reaction.resolver;
845
+ const lane_name = lane ?? "default";
846
+ const existing_lanes = event_to_lanes.get(name);
847
+ if (existing_lanes !== ALL_LANES) {
848
+ const set = existing_lanes ?? /* @__PURE__ */ new Set();
849
+ set.add(lane_name);
850
+ event_to_lanes.set(name, set);
851
+ }
852
+ const key = `${target}|${source ?? ""}`;
853
+ const existing = statics.get(key);
854
+ if (!existing) {
855
+ statics.set(key, { stream: target, source, priority, lane });
856
+ } else {
857
+ if ((existing.lane ?? void 0) !== (lane ?? void 0))
858
+ throw new Error(
859
+ `Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
860
+ );
861
+ if (priority > existing.priority) {
862
+ statics.set(key, { ...existing, priority });
863
+ }
864
+ }
865
+ }
866
+ }
867
+ }
868
+ const event_to_state = /* @__PURE__ */ new Map();
869
+ for (const merged of states.values()) {
870
+ for (const event_name of Object.keys(merged.events)) {
871
+ event_to_state.set(event_name, merged);
872
+ }
873
+ }
874
+ return {
875
+ static_targets: [...statics.values()],
876
+ has_dynamic_resolvers,
877
+ reactive_events,
878
+ event_to_state,
879
+ event_to_lanes
880
+ };
881
+ }
882
+
690
883
  // src/internal/correlate-cycle.ts
691
884
  var CorrelateCycle = class {
692
885
  _checkpoint = -1;
@@ -815,30 +1008,6 @@ var CorrelateCycle = class {
815
1008
  }
816
1009
  };
817
1010
 
818
- // src/internal/correlator.ts
819
- import { randomInt } from "crypto";
820
- var BASE = 36;
821
- var SEG_WIDTH = 4;
822
- var SEG_SPACE = BASE ** SEG_WIDTH;
823
- function seg(n) {
824
- return n.toString(BASE).padStart(SEG_WIDTH, "0");
825
- }
826
- var default_correlator = ({ state: state2, action: action2 }) => {
827
- const s = state2.slice(0, SEG_WIDTH).toLowerCase();
828
- const a = action2.slice(0, SEG_WIDTH).toLowerCase();
829
- const ts = seg(Date.now() % SEG_SPACE);
830
- const rnd = seg(randomInt(SEG_SPACE));
831
- return `${s}-${a}-${ts}${rnd}`;
832
- };
833
- function close_correlation(correlator, actor) {
834
- return correlator({
835
- state: "$close",
836
- action: "close",
837
- stream: "$close",
838
- actor
839
- });
840
- }
841
-
842
1011
  // src/internal/drain-cycle.ts
843
1012
  import { randomUUID } from "crypto";
844
1013
 
@@ -1003,7 +1172,6 @@ async function scan(source, opts = {}, callback) {
1003
1172
  migrated = {
1004
1173
  ...event,
1005
1174
  name: migration.to,
1006
- // biome-ignore lint/suspicious/noExplicitAny: migration target shape is caller-defined
1007
1175
  data: new_data
1008
1176
  };
1009
1177
  migrated_count++;
@@ -1052,39 +1220,37 @@ async function scan(source, opts = {}, callback) {
1052
1220
  }
1053
1221
  };
1054
1222
  }
1055
- async function load(me, stream, callback, asOf, actor) {
1223
+ async function load(me, target, callback) {
1224
+ const { stream, actor, asOf } = target;
1056
1225
  const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1057
1226
  const cached = time_travel ? void 0 : await cache().get(stream);
1058
1227
  const cache_hit = !!cached;
1059
1228
  let state2 = cached?.state ?? (me.init ? me.init() : {});
1060
1229
  let patches = cached?.patches ?? 0;
1061
1230
  let snaps = cached?.snaps ?? 0;
1062
- let version = cached?.version ?? -1;
1063
1231
  let replayed = 0;
1064
1232
  let event;
1065
1233
  await store().query(
1066
- (e) => {
1067
- version = e.version;
1068
- const typed = e;
1069
- event = me.view(typed, actor);
1070
- if (e.name === SNAP_EVENT) {
1071
- state2 = e.data;
1234
+ (raw) => {
1235
+ event = me.view(raw, actor);
1236
+ if (event.name === SNAP_EVENT) {
1237
+ state2 = event.data;
1072
1238
  snaps++;
1073
1239
  patches = 0;
1074
1240
  replayed++;
1075
- } else if (me.patch[e.name]) {
1076
- state2 = patch(state2, me.patch[e.name](typed, state2));
1241
+ } else if (me.patch[event.name]) {
1242
+ state2 = patch(state2, me.patch[event.name](event, state2));
1077
1243
  patches++;
1078
1244
  replayed++;
1079
- } else if (e.name !== TOMBSTONE_EVENT) {
1245
+ } else if (event.name !== TOMBSTONE_EVENT) {
1080
1246
  log().warn(
1081
- `Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
1247
+ `Skipping unknown event "${String(event.name)}" on stream "${stream}" (id=${event.id}) \u2014 no reducer in state "${me.name}"`
1082
1248
  );
1083
1249
  }
1084
1250
  callback?.({
1085
1251
  event,
1086
1252
  state: state2,
1087
- version,
1253
+ version: event.version,
1088
1254
  patches,
1089
1255
  snaps,
1090
1256
  cache_hit,
@@ -1097,39 +1263,43 @@ async function load(me, stream, callback, asOf, actor) {
1097
1263
  ...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
1098
1264
  }
1099
1265
  );
1100
- if (replayed > 0 && !time_travel && event) {
1266
+ if (replayed > 0 && !time_travel && event && !me.pii_aware) {
1101
1267
  await cache().set(stream, {
1102
1268
  state: state2,
1103
- version,
1269
+ version: event.version,
1104
1270
  event_id: event.id,
1105
1271
  patches,
1106
1272
  snaps
1107
1273
  });
1108
1274
  }
1109
- return { event, state: state2, version, patches, snaps, cache_hit, replayed };
1275
+ return {
1276
+ event,
1277
+ state: state2,
1278
+ version: event?.version ?? cached?.version ?? -1,
1279
+ patches,
1280
+ snaps,
1281
+ cache_hit,
1282
+ replayed
1283
+ };
1110
1284
  }
1111
- async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = default_correlator) {
1285
+ async function action(me, action2, target, payload, options) {
1112
1286
  const { stream, expectedVersion, actor } = target;
1113
1287
  if (!stream) throw new Error("Missing target stream");
1114
- const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
1288
+ const reactingTo = options?.reactingTo;
1289
+ const correlator = options?.correlator ?? default_correlator;
1290
+ const validated = validate(action2, payload, me.actions[action2]);
1115
1291
  const opts = me.options?.[action2];
1116
1292
  const max_retries = opts?.maxRetries ?? 0;
1117
1293
  for (let attempt = 0; ; attempt++) {
1118
1294
  try {
1119
- const snapshot = await load(
1120
- me,
1121
- stream,
1122
- void 0,
1123
- void 0,
1124
- target.actor
1125
- );
1295
+ const snapshot = await load(me, { stream, actor: target.actor });
1126
1296
  if (snapshot.event?.name === TOMBSTONE_EVENT)
1127
1297
  throw new StreamClosedError(stream);
1128
1298
  const expected = expectedVersion ?? snapshot.event?.version;
1129
1299
  if (me.given) {
1130
1300
  const invariants = me.given[action2] || [];
1131
- invariants.forEach(({ valid, description }) => {
1132
- if (!valid(snapshot.state, actor))
1301
+ invariants.forEach(({ valid: valid2, description }) => {
1302
+ if (!valid2(snapshot.state, actor))
1133
1303
  throw new InvariantError(
1134
1304
  action2,
1135
1305
  validated,
@@ -1145,24 +1315,11 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1145
1315
  return [snapshot];
1146
1316
  }
1147
1317
  const tuples = Array.isArray(result[0]) ? result : [result];
1148
- const deprecated = me._deprecated;
1149
- if (deprecated && deprecated.size > 0) {
1150
- const me_ = me;
1151
- const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
1152
- for (const [name] of tuples) {
1153
- const evt = name;
1154
- if (deprecated.has(evt) && !warned.has(evt)) {
1155
- warned.add(evt);
1156
- log().warn(
1157
- `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
1158
- );
1159
- }
1160
- }
1161
- }
1162
- const emitted = tuples.map(([name, data]) => {
1163
- const validated2 = skipValidation ? data : validate(name, data, me.events[name]);
1164
- return me.message({ name, data: validated2 });
1165
- });
1318
+ const valid = tuples.map(([name, data]) => ({
1319
+ name,
1320
+ data: validate(name, data, me.events[name])
1321
+ }));
1322
+ const emitted = valid.map((e) => me.message(e));
1166
1323
  const meta = {
1167
1324
  correlation: reactingTo?.meta.correlation || correlator({
1168
1325
  action: action2,
@@ -1203,12 +1360,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1203
1360
  throw error;
1204
1361
  }
1205
1362
  let { state: state2, patches } = snapshot;
1206
- const snapshots = committed.map((event) => {
1363
+ const snapshots = committed.map((row, i) => {
1364
+ const event = { ...row, data: valid[i].data };
1207
1365
  const p = me.patch[event.name](event, state2);
1208
1366
  state2 = patch(state2, p);
1209
1367
  patches++;
1210
1368
  return {
1211
- event: me.view(event, target.actor),
1369
+ event,
1212
1370
  state: state2,
1213
1371
  version: event.version,
1214
1372
  patches,
@@ -1220,13 +1378,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1220
1378
  });
1221
1379
  const last = snapshots.at(-1);
1222
1380
  const snapped = me.snap?.(last);
1223
- cache().set(stream, {
1224
- state: last.state,
1225
- version: last.event.version,
1226
- event_id: last.event.id,
1227
- patches: snapped ? 0 : last.patches,
1228
- snaps: snapped ? last.snaps + 1 : last.snaps
1229
- }).catch((err) => log().error(err));
1381
+ if (!me.pii_aware) {
1382
+ cache().set(stream, {
1383
+ state: last.state,
1384
+ version: last.event.version,
1385
+ event_id: last.event.id,
1386
+ patches: snapped ? 0 : last.patches,
1387
+ snaps: snapped ? last.snaps + 1 : last.snaps
1388
+ }).catch((err) => log().error(err));
1389
+ }
1230
1390
  if (snapped) void snap(last);
1231
1391
  return snapshots;
1232
1392
  } catch (error) {
@@ -1293,15 +1453,7 @@ var traced = (inner, exit, entry) => (async (...args) => {
1293
1453
  return result;
1294
1454
  });
1295
1455
  function build_es(logger, correlator = default_correlator) {
1296
- const bound_action = (me, action_name, target, payload, reactingTo, skipValidation = false) => action(
1297
- me,
1298
- action_name,
1299
- target,
1300
- payload,
1301
- reactingTo,
1302
- skipValidation,
1303
- correlator
1304
- );
1456
+ const bound_action = (me, action_name, target, payload, options) => action(me, action_name, target, payload, { correlator, ...options });
1305
1457
  if (logger.level !== "trace") {
1306
1458
  return {
1307
1459
  snap,
@@ -1320,7 +1472,7 @@ function build_es(logger, correlator = default_correlator) {
1320
1472
  )
1321
1473
  );
1322
1474
  }),
1323
- load: traced(load, (result, _me, stream, _cb, asOf) => {
1475
+ load: traced(load, (result, _me, target) => {
1324
1476
  const stats = stats_marker(
1325
1477
  result.version,
1326
1478
  result.replayed,
@@ -1331,7 +1483,7 @@ function build_es(logger, correlator = default_correlator) {
1331
1483
  es_caption(
1332
1484
  "load",
1333
1485
  C_GREEN,
1334
- `${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
1486
+ `${target.stream}${as_of_marker(target.asOf)} ${cache_marker(result.cache_hit)} ${stats}`
1335
1487
  )
1336
1488
  );
1337
1489
  }),
@@ -1819,13 +1971,10 @@ function build_handle(deps) {
1819
1971
  };
1820
1972
  for (const payload of payloads) {
1821
1973
  const { event, handler } = payload;
1822
- scoped_app.do = (action2, target, action_payload, reactingTo, skipValidation) => bound_do(
1823
- action2,
1824
- target,
1825
- action_payload,
1826
- reactingTo ?? event,
1827
- skipValidation
1828
- );
1974
+ scoped_app.do = (action2, target, action_payload, options) => bound_do(action2, target, action_payload, {
1975
+ ...options,
1976
+ reactingTo: options?.reactingTo ?? event
1977
+ });
1829
1978
  try {
1830
1979
  await handler(event, stream, scoped_app);
1831
1980
  at = event.id;
@@ -1962,6 +2111,22 @@ var Act = class {
1962
2111
  registry;
1963
2112
  /** Map of state name → state definition; populated by the builder. */
1964
2113
  _states;
2114
+ /**
2115
+ * Resolved autoclose configuration (#837 / epic #802). Frozen at
2116
+ * `act().build()` via {@link resolveAutocloseConfig}.
2117
+ *
2118
+ * @internal
2119
+ */
2120
+ _autoclose_config;
2121
+ /**
2122
+ * App-level autoclose controller. `undefined` when no state declares
2123
+ * `.autocloses(...)` — the controller is never constructed, so
2124
+ * `start_correlations()` / `stop_correlations()` skip the autoclose
2125
+ * lifecycle and the orchestrator pays zero per-tick cost.
2126
+ *
2127
+ * @internal
2128
+ */
2129
+ _autoclose;
1965
2130
  /**
1966
2131
  * Emit a lifecycle event. The payload type is inferred from the event name
1967
2132
  * via {@link ActLifecycleEvents}.
@@ -2096,6 +2261,7 @@ var Act = class {
2096
2261
  this._reactive_events = reactive_events;
2097
2262
  this._listen = options.listen !== false;
2098
2263
  this._drain = options.drain !== false;
2264
+ this._autoclose_config = resolveAutocloseConfig(options);
2099
2265
  this._event_to_state = event_to_state;
2100
2266
  this._event_to_lanes = event_to_lanes;
2101
2267
  const all_lanes = ["default", ...lanes.map((l) => l.name)];
@@ -2159,6 +2325,26 @@ var Act = class {
2159
2325
  },
2160
2326
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2161
2327
  );
2328
+ const states_with_policy = [...this._states.values()].filter(
2329
+ (s) => s.autoclose
2330
+ );
2331
+ this._autoclose = states_with_policy.length ? new AutocloseController({
2332
+ // Internal cycle takes the loose `(string) => …` signature; the
2333
+ // public `registry.autoclose_policy` narrows to
2334
+ // `keyof TStateMap & string`. Thin lambdas bridge across the
2335
+ // narrowing — runtime semantics are unchanged.
2336
+ autoclose_policy: (name) => this.registry.autoclose_policy(name),
2337
+ autoclose_archiver: (name) => this.registry.autoclose_archiver(name),
2338
+ event_to_state: this._event_to_state,
2339
+ reactive_events_size: this._reactive_events.size,
2340
+ load: this._es.load,
2341
+ tombstone: this._es.tombstone,
2342
+ logger: this._logger,
2343
+ config: this._autoclose_config,
2344
+ correlator: this._correlator,
2345
+ close_actor: { id: "$autoclose", name: "autoclose" },
2346
+ on_closed: (result) => this.emit("closed", result)
2347
+ }) : void 0;
2162
2348
  this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
2163
2349
  dispose(() => this.shutdown());
2164
2350
  }
@@ -2231,8 +2417,9 @@ var Act = class {
2231
2417
  * @param action - The name of the action to execute
2232
2418
  * @param target - Target specification with stream ID and actor context
2233
2419
  * @param payload - Action payload matching the action's schema
2234
- * @param reactingTo - Optional event that triggered this action (for correlation)
2235
- * @param skipValidation - Skip schema validation (use carefully, for performance)
2420
+ * @param options - Per-call dispatch options ({@link DoOptions}) —
2421
+ * `reactingTo` to thread correlation, `correlator` to override the
2422
+ * framework or orchestrator-level correlator for this call only.
2236
2423
  * @returns Array of snapshots for all affected states (usually one)
2237
2424
  *
2238
2425
  * @throws {ValidationError} If payload doesn't match action schema
@@ -2287,7 +2474,7 @@ var Act = class {
2287
2474
  * { amount: event.data.items.length }
2288
2475
  * );
2289
2476
  * // To use a different correlation, pass reactingTo explicitly:
2290
- * // await app.do("reduceStock", target, payload, customEvent);
2477
+ * // await app.do("reduceStock", target, payload, { reactingTo: customEvent });
2291
2478
  * })
2292
2479
  * .to("inventory-1")
2293
2480
  * .build();
@@ -2297,15 +2484,14 @@ var Act = class {
2297
2484
  * @see {@link Snapshot} for return value structure
2298
2485
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
2299
2486
  */
2300
- async do(action2, target, payload, reactingTo, skipValidation = false) {
2487
+ async do(action2, target, payload, options) {
2301
2488
  return this._scoped(async () => {
2302
2489
  const snapshots = await this._es.action(
2303
2490
  this.registry.actions[action2],
2304
2491
  action2,
2305
2492
  target,
2306
2493
  payload,
2307
- reactingTo,
2308
- skipValidation
2494
+ options
2309
2495
  );
2310
2496
  if (this._reactive_events.size > 0)
2311
2497
  this._arm_for_event_names(
@@ -2315,7 +2501,7 @@ var Act = class {
2315
2501
  return snapshots;
2316
2502
  });
2317
2503
  }
2318
- async load(stateOrName, stream, callback, asOf, actor) {
2504
+ async load(stateOrName, streamOrTarget, callback, asOf) {
2319
2505
  return this._scoped(async () => {
2320
2506
  let merged;
2321
2507
  if (typeof stateOrName === "string") {
@@ -2325,7 +2511,12 @@ var Act = class {
2325
2511
  } else {
2326
2512
  merged = this._states.get(stateOrName.name) || stateOrName;
2327
2513
  }
2328
- return await this._es.load(merged, stream, callback, asOf, actor);
2514
+ const target = typeof streamOrTarget === "string" ? {
2515
+ stream: streamOrTarget,
2516
+ actor: void 0,
2517
+ asOf
2518
+ } : streamOrTarget;
2519
+ return await this._es.load(merged, target, callback);
2329
2520
  });
2330
2521
  }
2331
2522
  /**
@@ -2645,7 +2836,9 @@ var Act = class {
2645
2836
  * @see {@link stop_correlations} to stop the worker
2646
2837
  */
2647
2838
  start_correlations(query = {}, frequency = 1e4, callback) {
2648
- return this._correlate.start_polling(query, frequency, callback);
2839
+ const started = this._correlate.start_polling(query, frequency, callback);
2840
+ this._autoclose?.start();
2841
+ return started;
2649
2842
  }
2650
2843
  /**
2651
2844
  * Stops the automatic correlation worker.
@@ -2666,6 +2859,7 @@ var Act = class {
2666
2859
  */
2667
2860
  stop_correlations() {
2668
2861
  this._correlate.stop_polling();
2862
+ this._autoclose?.stop();
2669
2863
  }
2670
2864
  /**
2671
2865
  * Cancels any pending or active settle cycle.
@@ -3049,11 +3243,18 @@ function act() {
3049
3243
  const states = /* @__PURE__ */ new Map();
3050
3244
  const _sf = /* @__PURE__ */ new Map();
3051
3245
  const _dp = /* @__PURE__ */ new Map();
3246
+ const _de = /* @__PURE__ */ new Map();
3247
+ const _ac = /* @__PURE__ */ new Map();
3248
+ const _aa = /* @__PURE__ */ new Map();
3249
+ const EMPTY_DEPRECATED = /* @__PURE__ */ new Set();
3052
3250
  const registry = {
3053
3251
  actions: {},
3054
3252
  events: {},
3055
3253
  sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
3056
- disclosure_predicate: (state_name) => _dp.get(state_name) ?? null
3254
+ disclosure_predicate: (state_name) => _dp.get(state_name) ?? null,
3255
+ deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED,
3256
+ autoclose_policy: (state_name) => _ac.get(state_name) ?? null,
3257
+ autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
3057
3258
  };
3058
3259
  const pending_projections = [];
3059
3260
  const batch_handlers = /* @__PURE__ */ new Map();
@@ -3065,7 +3266,7 @@ function act() {
3065
3266
  const event_names = Object.keys(state2.events);
3066
3267
  const deprecated = deprecated_event_names(event_names);
3067
3268
  if (deprecated.size === 0) continue;
3068
- state2._deprecated = deprecated;
3269
+ _de.set(state2.name, deprecated);
3069
3270
  for (const name of deprecated) {
3070
3271
  const current = current_version_of(name, event_names);
3071
3272
  deprecation_summary.push({
@@ -3180,6 +3381,8 @@ function act() {
3180
3381
  }
3181
3382
  for (const state2 of states.values()) {
3182
3383
  if (state2.disclose) _dp.set(state2.name, state2.disclose);
3384
+ if (state2.autoclose) _ac.set(state2.name, state2.autoclose);
3385
+ if (state2.archive) _aa.set(state2.name, state2.archive);
3183
3386
  const fields_by_event = /* @__PURE__ */ new Map();
3184
3387
  for (const event_name of Object.keys(state2.events)) {
3185
3388
  const fields = _sf.get(event_name);
@@ -3193,18 +3396,17 @@ function act() {
3193
3396
  );
3194
3397
  }
3195
3398
  const disclose = state2.disclose ?? null;
3196
- state2.view = (event, actor) => {
3197
- const fields = fields_by_event.get(event.name);
3198
- return fields ? pii_gate(event, fields, disclose, actor) : event;
3199
- };
3399
+ state2.pii_aware = true;
3400
+ state2.view = (event, actor) => pii_gate(
3401
+ event,
3402
+ fields_by_event.get(event.name) ?? [],
3403
+ disclose,
3404
+ actor
3405
+ );
3200
3406
  state2.message = (validated) => {
3201
3407
  const fields = fields_by_event.get(validated.name);
3202
3408
  return fields ? pii_split(validated, fields) : validated;
3203
3409
  };
3204
- for (const [event_name, fields] of fields_by_event) {
3205
- const original = state2.patch[event_name];
3206
- state2.patch[event_name] = (event, s) => original(pii_merge(event, fields), s);
3207
- }
3208
3410
  }
3209
3411
  for (const [target, original] of batch_handlers) {
3210
3412
  const wrapped = async (events, stream) => {
@@ -3384,9 +3586,10 @@ function state(entry) {
3384
3586
  init,
3385
3587
  patch: default_patch,
3386
3588
  on: {},
3387
- // Step delegates initialized as identity. `act().build()`
3388
- // overrides on states with `sensitive(...)` events to bake in
3389
- // the gate / split.
3589
+ // Step delegates initialized as identity, `pii_aware` false.
3590
+ // `act().build()` flips both on states with `sensitive(...)`
3591
+ // events to bake in the gate / split and gate the cache write.
3592
+ pii_aware: false,
3390
3593
  view: (event) => event,
3391
3594
  message: (validated) => validated
3392
3595
  };
@@ -3447,6 +3650,24 @@ function action_builder(state2) {
3447
3650
  internal.disclose = disclose;
3448
3651
  return builder;
3449
3652
  },
3653
+ autocloses(predicate) {
3654
+ if (typeof predicate !== "function") {
3655
+ throw new Error(
3656
+ ".autocloses(predicate) requires a function; got " + typeof predicate
3657
+ );
3658
+ }
3659
+ internal.autoclose = predicate;
3660
+ return builder;
3661
+ },
3662
+ archives(archive) {
3663
+ if (typeof archive !== "function") {
3664
+ throw new Error(
3665
+ ".archives(archive) requires a function; got " + typeof archive
3666
+ );
3667
+ }
3668
+ internal.archive = archive;
3669
+ return builder;
3670
+ },
3450
3671
  build() {
3451
3672
  return internal;
3452
3673
  }
@@ -3622,6 +3843,9 @@ export {
3622
3843
  ConcurrencyError,
3623
3844
  ConsoleLogger,
3624
3845
  CsvFile,
3846
+ DEFAULT_AUTOCLOSE_CYCLE_MS,
3847
+ DEFAULT_CLOSE_BATCH_SIZE,
3848
+ DEFAULT_CLOSE_YIELD_MS,
3625
3849
  DEFAULT_LANE,
3626
3850
  DEFAULT_MAX_SUBSCRIBED_STREAMS,
3627
3851
  DEFAULT_SETTLE_DEBOUNCE_MS,
@@ -3653,6 +3877,7 @@ export {
3653
3877
  log,
3654
3878
  port,
3655
3879
  projection,
3880
+ resolveAutocloseConfig,
3656
3881
  scoped,
3657
3882
  sensitive,
3658
3883
  sleep,