@rotorsoft/act 1.9.0 → 1.10.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 (39) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +34 -2
  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/state-builder.d.ts +31 -1
  6. package/dist/@types/builders/state-builder.d.ts.map +1 -1
  7. package/dist/@types/internal/event-sourcing.d.ts +7 -2
  8. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  9. package/dist/@types/internal/index.d.ts +1 -0
  10. package/dist/@types/internal/index.d.ts.map +1 -1
  11. package/dist/@types/internal/reactions.d.ts +5 -4
  12. package/dist/@types/internal/reactions.d.ts.map +1 -1
  13. package/dist/@types/internal/sensitive.d.ts +147 -0
  14. package/dist/@types/internal/sensitive.d.ts.map +1 -0
  15. package/dist/@types/internal/tracing.d.ts.map +1 -1
  16. package/dist/@types/types/action.d.ts +57 -0
  17. package/dist/@types/types/action.d.ts.map +1 -1
  18. package/dist/@types/types/registry.d.ts +9 -1
  19. package/dist/@types/types/registry.d.ts.map +1 -1
  20. package/dist/@types/types/schemas.d.ts +36 -0
  21. package/dist/@types/types/schemas.d.ts.map +1 -1
  22. package/dist/{chunk-F4S2JOPN.js → chunk-3ZTFNAY7.js} +2 -2
  23. package/dist/chunk-XSBT63QX.js +267 -0
  24. package/dist/chunk-XSBT63QX.js.map +1 -0
  25. package/dist/index.cjs +291 -78
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +155 -32
  28. package/dist/index.js.map +1 -1
  29. package/dist/test/index.cjs +62 -56
  30. package/dist/test/index.cjs.map +1 -1
  31. package/dist/test/index.js +11 -11
  32. package/dist/test/index.js.map +1 -1
  33. package/dist/types/index.cjs +52 -34
  34. package/dist/types/index.cjs.map +1 -1
  35. package/dist/types/index.js +9 -3
  36. package/package.json +1 -1
  37. package/dist/chunk-PMAZTOSO.js +0 -164
  38. package/dist/chunk-PMAZTOSO.js.map +0 -1
  39. /package/dist/{chunk-F4S2JOPN.js.map → chunk-3ZTFNAY7.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-F4S2JOPN.js";
22
+ } from "./chunk-3ZTFNAY7.js";
23
23
  import {
24
24
  ActorSchema,
25
25
  CausationEventSchema,
@@ -32,11 +32,19 @@ import {
32
32
  LogLevels,
33
33
  NonRetryableError,
34
34
  QuerySchema,
35
+ REDACTED,
36
+ SHREDDED,
35
37
  StreamClosedError,
36
38
  TargetSchema,
37
39
  ValidationError,
38
- ZodEmpty
39
- } from "./chunk-PMAZTOSO.js";
40
+ ZodEmpty,
41
+ pii_fields,
42
+ pii_gate,
43
+ pii_merge,
44
+ pii_split,
45
+ pii_strip,
46
+ sensitive
47
+ } from "./chunk-XSBT63QX.js";
40
48
  import "./chunk-5WRI5ZAA.js";
41
49
 
42
50
  // src/signals.ts
@@ -1044,7 +1052,7 @@ async function scan(source, opts = {}, callback) {
1044
1052
  }
1045
1053
  };
1046
1054
  }
1047
- async function load(me, stream, callback, asOf) {
1055
+ async function load(me, stream, callback, asOf, actor) {
1048
1056
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1049
1057
  const cached = timeTravel ? void 0 : await cache().get(stream);
1050
1058
  const cache_hit = !!cached;
@@ -1056,15 +1064,16 @@ async function load(me, stream, callback, asOf) {
1056
1064
  let event;
1057
1065
  await store().query(
1058
1066
  (e) => {
1059
- event = e;
1060
1067
  version = e.version;
1068
+ const typed = e;
1069
+ event = me.view(typed, actor);
1061
1070
  if (e.name === SNAP_EVENT) {
1062
1071
  state2 = e.data;
1063
1072
  snaps++;
1064
1073
  patches = 0;
1065
1074
  replayed++;
1066
1075
  } else if (me.patch[e.name]) {
1067
- state2 = patch(state2, me.patch[e.name](event, state2));
1076
+ state2 = patch(state2, me.patch[e.name](typed, state2));
1068
1077
  patches++;
1069
1078
  replayed++;
1070
1079
  } else if (e.name !== TOMBSTONE_EVENT) {
@@ -1107,7 +1116,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1107
1116
  const maxRetries = opts?.maxRetries ?? 0;
1108
1117
  for (let attempt = 0; ; attempt++) {
1109
1118
  try {
1110
- const snapshot = await load(me, stream);
1119
+ const snapshot = await load(
1120
+ me,
1121
+ stream,
1122
+ void 0,
1123
+ void 0,
1124
+ target.actor
1125
+ );
1111
1126
  if (snapshot.event?.name === TOMBSTONE_EVENT)
1112
1127
  throw new StreamClosedError(stream);
1113
1128
  const expected = expectedVersion ?? snapshot.event?.version;
@@ -1144,10 +1159,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1144
1159
  }
1145
1160
  }
1146
1161
  }
1147
- const emitted = tuples.map(([name, data]) => ({
1148
- name,
1149
- data: skipValidation ? data : validate(name, data, me.events[name])
1150
- }));
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
+ });
1151
1166
  const meta = {
1152
1167
  correlation: reactingTo?.meta.correlation || correlator({
1153
1168
  action: action2,
@@ -1159,8 +1174,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1159
1174
  action: {
1160
1175
  name: action2,
1161
1176
  ...target
1162
- // payload intentionally omitted: it can be large or contain PII,
1163
- // and callers correlate via the correlation id when they need it.
1177
+ // payload intentionally omitted from causation metadata
1178
+ // callers correlate via the correlation id when they need it.
1164
1179
  },
1165
1180
  event: reactingTo ? {
1166
1181
  id: reactingTo.id,
@@ -1193,7 +1208,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1193
1208
  state2 = patch(state2, p);
1194
1209
  patches++;
1195
1210
  return {
1196
- event,
1211
+ event: me.view(event, target.actor),
1197
1212
  state: state2,
1198
1213
  version: event.version,
1199
1214
  patches,
@@ -1278,7 +1293,7 @@ var traced = (inner, exit, entry) => (async (...args) => {
1278
1293
  return result;
1279
1294
  });
1280
1295
  function buildEs(logger, correlator = defaultCorrelator) {
1281
- const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
1296
+ const bound_action = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
1282
1297
  me,
1283
1298
  actionName,
1284
1299
  target,
@@ -1291,7 +1306,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
1291
1306
  return {
1292
1307
  snap,
1293
1308
  load,
1294
- action: boundAction,
1309
+ action: bound_action,
1295
1310
  tombstone
1296
1311
  };
1297
1312
  }
@@ -1321,7 +1336,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
1321
1336
  );
1322
1337
  }),
1323
1338
  action: traced(
1324
- boundAction,
1339
+ bound_action,
1325
1340
  (snapshots, _me, _action, target) => {
1326
1341
  const committed = snapshots.filter((s) => s.event);
1327
1342
  if (committed.length) {
@@ -1778,7 +1793,14 @@ function finalize(lease, handled, at, error, options, logger, failed_at) {
1778
1793
  };
1779
1794
  }
1780
1795
  function buildHandle(deps) {
1781
- const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
1796
+ const {
1797
+ logger,
1798
+ bound_do,
1799
+ bound_load,
1800
+ bound_query,
1801
+ bound_query_array,
1802
+ bound_forget
1803
+ } = deps;
1782
1804
  return async (lease, payloads) => {
1783
1805
  if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
1784
1806
  const stream = lease.stream;
@@ -1787,14 +1809,15 @@ function buildHandle(deps) {
1787
1809
  if (lease.retry > 0)
1788
1810
  logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1789
1811
  const scopedApp = {
1790
- do: boundDo,
1791
- load: boundLoad,
1792
- query: boundQuery,
1793
- query_array: boundQueryArray
1812
+ do: bound_do,
1813
+ load: bound_load,
1814
+ query: bound_query,
1815
+ query_array: bound_query_array,
1816
+ forget: bound_forget
1794
1817
  };
1795
1818
  for (const payload of payloads) {
1796
1819
  const { event, handler } = payload;
1797
- scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
1820
+ scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => bound_do(
1798
1821
  action2,
1799
1822
  target,
1800
1823
  actionPayload,
@@ -1823,7 +1846,9 @@ function buildHandle(deps) {
1823
1846
  function buildHandleBatch(logger) {
1824
1847
  return async (lease, payloads, batchHandler) => {
1825
1848
  const stream = lease.stream;
1826
- const events = payloads.map((p) => p.event);
1849
+ const events = payloads.map(
1850
+ (p) => p.event
1851
+ );
1827
1852
  const options = payloads[0].options;
1828
1853
  if (lease.retry > 0)
1829
1854
  logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
@@ -2006,6 +2031,7 @@ var Act = class {
2006
2031
  _bound_load = this.load.bind(this);
2007
2032
  _bound_query = this.query.bind(this);
2008
2033
  _bound_query_array = this.query_array.bind(this);
2034
+ _bound_forget = this.forget.bind(this);
2009
2035
  /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
2010
2036
  _handle;
2011
2037
  _handle_batch;
@@ -2051,10 +2077,11 @@ var Act = class {
2051
2077
  this._cd = buildDrain(this._logger);
2052
2078
  this._handle = buildHandle({
2053
2079
  logger: this._logger,
2054
- boundDo: this._bound_do,
2055
- boundLoad: this._bound_load,
2056
- boundQuery: this._bound_query,
2057
- boundQueryArray: this._bound_query_array
2080
+ bound_do: this._bound_do,
2081
+ bound_load: this._bound_load,
2082
+ bound_query: this._bound_query,
2083
+ bound_query_array: this._bound_query_array,
2084
+ bound_forget: this._bound_forget
2058
2085
  });
2059
2086
  this._handle_batch = buildHandleBatch(this._logger);
2060
2087
  const {
@@ -2286,7 +2313,7 @@ var Act = class {
2286
2313
  return snapshots;
2287
2314
  });
2288
2315
  }
2289
- async load(stateOrName, stream, callback, asOf) {
2316
+ async load(stateOrName, stream, callback, asOf, actor) {
2290
2317
  return this._scoped(async () => {
2291
2318
  let merged;
2292
2319
  if (typeof stateOrName === "string") {
@@ -2296,7 +2323,7 @@ var Act = class {
2296
2323
  } else {
2297
2324
  merged = this._states.get(stateOrName.name) || stateOrName;
2298
2325
  }
2299
- return await this._es.load(merged, stream, callback, asOf);
2326
+ return await this._es.load(merged, stream, callback, asOf, actor);
2300
2327
  });
2301
2328
  }
2302
2329
  /**
@@ -2390,6 +2417,34 @@ var Act = class {
2390
2417
  return events;
2391
2418
  });
2392
2419
  }
2420
+ /**
2421
+ * Wipe the sensitive-data payload for every event on the stream — see
2422
+ * {@link IAct.forget}. Application-level half of #566.
2423
+ *
2424
+ * Throws on adapters without `Store.forget_pii`, invalidates the cache
2425
+ * entry for the stream, emits the `forgotten` lifecycle event with the
2426
+ * row count. Idempotent: a second call returns `{eventCount: 0}` and
2427
+ * does NOT re-emit.
2428
+ *
2429
+ * @param stream - Target stream.
2430
+ * @returns `{eventCount}` — number of events whose PII column was wiped.
2431
+ */
2432
+ async forget(stream) {
2433
+ return this._scoped(async () => {
2434
+ const s = store();
2435
+ if (!s.forget_pii) {
2436
+ throw new Error(
2437
+ `Store does not implement forget_pii \u2014 adapter cannot comply with sensitive-data erasure. Use an adapter that declares pii_isolation: true (e.g. @rotorsoft/act on the in-memory store).`
2438
+ );
2439
+ }
2440
+ const eventCount = await s.forget_pii(stream);
2441
+ await cache().invalidate(stream);
2442
+ if (eventCount > 0) {
2443
+ this.emit("forgotten", { stream, at: /* @__PURE__ */ new Date(), eventCount });
2444
+ }
2445
+ return { eventCount };
2446
+ });
2447
+ }
2393
2448
  /**
2394
2449
  * Processes pending reactions by draining uncommitted events from the event store.
2395
2450
  *
@@ -2990,9 +3045,13 @@ function validateLaneReferences(registry, lanes) {
2990
3045
  }
2991
3046
  function act() {
2992
3047
  const states = /* @__PURE__ */ new Map();
3048
+ const _sf = /* @__PURE__ */ new Map();
3049
+ const _dp = /* @__PURE__ */ new Map();
2993
3050
  const registry = {
2994
3051
  actions: {},
2995
- events: {}
3052
+ events: {},
3053
+ sensitive_fields: (eventName) => _sf.get(eventName) ?? [],
3054
+ disclosure_predicate: (stateName) => _dp.get(stateName) ?? null
2996
3055
  };
2997
3056
  const pendingProjections = [];
2998
3057
  const batchHandlers = /* @__PURE__ */ new Map();
@@ -3103,6 +3162,58 @@ function act() {
3103
3162
  }
3104
3163
  finalizeDeprecations();
3105
3164
  validateLaneReferences(registry, lanes);
3165
+ for (const [eventName, reg] of Object.entries(
3166
+ registry.events
3167
+ )) {
3168
+ const fields = pii_fields(reg.schema);
3169
+ if (fields.length === 0) continue;
3170
+ _sf.set(eventName, fields);
3171
+ for (const [name, reaction] of reg.reactions) {
3172
+ const inner = reaction.handler;
3173
+ const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
3174
+ Object.defineProperty(wrapped, "name", { value: inner.name });
3175
+ reaction.handler = wrapped;
3176
+ reg.reactions.set(name, reaction);
3177
+ }
3178
+ }
3179
+ for (const state2 of states.values()) {
3180
+ if (state2.disclose) _dp.set(state2.name, state2.disclose);
3181
+ const fields_by_event = /* @__PURE__ */ new Map();
3182
+ for (const eventName of Object.keys(state2.events)) {
3183
+ const fields = _sf.get(eventName);
3184
+ if (fields) fields_by_event.set(eventName, fields);
3185
+ }
3186
+ if (fields_by_event.size === 0) continue;
3187
+ if (state2.snap) {
3188
+ const offending = [...fields_by_event.keys()];
3189
+ throw new Error(
3190
+ `State "${state2.name}" cannot snapshot \u2014 events {${offending.join(", ")}} carry sensitive fields. Snapshots write derived state into __snapshot__.data, which forget_pii cannot reach. Remove .snap() or remove sensitive(...) markers.`
3191
+ );
3192
+ }
3193
+ const disclose = state2.disclose ?? null;
3194
+ state2.view = (event, actor) => {
3195
+ const fields = fields_by_event.get(event.name);
3196
+ return fields ? pii_gate(event, fields, disclose, actor) : event;
3197
+ };
3198
+ state2.message = (validated) => {
3199
+ const fields = fields_by_event.get(validated.name);
3200
+ return fields ? pii_split(validated, fields) : validated;
3201
+ };
3202
+ for (const [eventName, fields] of fields_by_event) {
3203
+ const original = state2.patch[eventName];
3204
+ state2.patch[eventName] = (event, s) => original(pii_merge(event, fields), s);
3205
+ }
3206
+ }
3207
+ for (const [target, original] of batchHandlers) {
3208
+ const wrapped = async (events, stream) => {
3209
+ const stripped = events.map((e) => {
3210
+ const f = _sf.get(e.name);
3211
+ return f ? pii_strip(e, f) : e;
3212
+ });
3213
+ return original(stripped, stream);
3214
+ };
3215
+ batchHandlers.set(target, wrapped);
3216
+ }
3106
3217
  _built = true;
3107
3218
  }
3108
3219
  return new Act(
@@ -3270,7 +3381,12 @@ function state(entry) {
3270
3381
  name,
3271
3382
  init,
3272
3383
  patch: defaultPatch,
3273
- on: {}
3384
+ on: {},
3385
+ // Step delegates initialized as identity. `act().build()`
3386
+ // overrides on states with `sensitive(...)` events to bake in
3387
+ // the gate / split.
3388
+ view: (event) => event,
3389
+ message: (validated) => validated
3274
3390
  };
3275
3391
  const builder = action_builder(internal);
3276
3392
  return Object.assign(builder, {
@@ -3322,6 +3438,10 @@ function action_builder(state2) {
3322
3438
  internal.snap = snap2;
3323
3439
  return builder;
3324
3440
  },
3441
+ discloses(disclose) {
3442
+ internal.disclose = disclose;
3443
+ return builder;
3444
+ },
3325
3445
  build() {
3326
3446
  return internal;
3327
3447
  }
@@ -3511,6 +3631,8 @@ export {
3511
3631
  NonRetryableError,
3512
3632
  PackageSchema,
3513
3633
  QuerySchema,
3634
+ REDACTED,
3635
+ SHREDDED,
3514
3636
  SNAP_EVENT,
3515
3637
  StreamClosedError,
3516
3638
  TOMBSTONE_EVENT,
@@ -3527,6 +3649,7 @@ export {
3527
3649
  port,
3528
3650
  projection,
3529
3651
  scoped,
3652
+ sensitive,
3530
3653
  sleep,
3531
3654
  slice,
3532
3655
  state,