@rotorsoft/act 0.41.0 → 0.43.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 (41) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +75 -3
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/adapters/in-memory-store.d.ts +26 -6
  5. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  6. package/dist/@types/internal/close-cycle.d.ts +7 -0
  7. package/dist/@types/internal/close-cycle.d.ts.map +1 -1
  8. package/dist/@types/internal/correlator.d.ts +44 -0
  9. package/dist/@types/internal/correlator.d.ts.map +1 -0
  10. package/dist/@types/internal/event-sourcing.d.ts +10 -3
  11. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  12. package/dist/@types/internal/index.d.ts +2 -1
  13. package/dist/@types/internal/index.d.ts.map +1 -1
  14. package/dist/@types/internal/reactions.d.ts +1 -1
  15. package/dist/@types/internal/reactions.d.ts.map +1 -1
  16. package/dist/@types/internal/tracing.d.ts +2 -2
  17. package/dist/@types/internal/tracing.d.ts.map +1 -1
  18. package/dist/@types/types/action.d.ts +41 -0
  19. package/dist/@types/types/action.d.ts.map +1 -1
  20. package/dist/@types/types/errors.d.ts +56 -0
  21. package/dist/@types/types/errors.d.ts.map +1 -1
  22. package/dist/@types/types/ports.d.ts +80 -8
  23. package/dist/@types/types/ports.d.ts.map +1 -1
  24. package/dist/{chunk-M5YFKVRV.js → chunk-QAB4SDOS.js} +89 -24
  25. package/dist/chunk-QAB4SDOS.js.map +1 -0
  26. package/dist/{chunk-AGWZY6YT.js → chunk-VMX7RPTC.js} +13 -2
  27. package/dist/chunk-VMX7RPTC.js.map +1 -0
  28. package/dist/index.cjs +232 -39
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +138 -20
  31. package/dist/index.js.map +1 -1
  32. package/dist/test/index.cjs +91 -25
  33. package/dist/test/index.cjs.map +1 -1
  34. package/dist/test/index.js +4 -4
  35. package/dist/test/index.js.map +1 -1
  36. package/dist/types/index.cjs +13 -1
  37. package/dist/types/index.cjs.map +1 -1
  38. package/dist/types/index.js +3 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-AGWZY6YT.js.map +0 -1
  41. package/dist/chunk-M5YFKVRV.js.map +0 -1
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  sleep,
19
19
  store,
20
20
  validate
21
- } from "./chunk-M5YFKVRV.js";
21
+ } from "./chunk-QAB4SDOS.js";
22
22
  import {
23
23
  ActorSchema,
24
24
  CausationEventSchema,
@@ -29,12 +29,13 @@ import {
29
29
  EventMetaSchema,
30
30
  InvariantError,
31
31
  LogLevels,
32
+ NonRetryableError,
32
33
  QuerySchema,
33
34
  StreamClosedError,
34
35
  TargetSchema,
35
36
  ValidationError,
36
37
  ZodEmpty
37
- } from "./chunk-AGWZY6YT.js";
38
+ } from "./chunk-VMX7RPTC.js";
38
39
  import "./chunk-5WRI5ZAA.js";
39
40
 
40
41
  // src/signals.ts
@@ -95,7 +96,6 @@ function classifyRegistry(registry, states) {
95
96
  }
96
97
 
97
98
  // src/internal/close-cycle.ts
98
- import { randomUUID } from "crypto";
99
99
  async function runCloseCycle(targets, deps) {
100
100
  const targetMap = new Map(targets.map((t) => [t.stream, t]));
101
101
  const streams = [...targetMap.keys()];
@@ -107,11 +107,10 @@ async function runCloseCycle(targets, deps) {
107
107
  skipped
108
108
  );
109
109
  if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
110
- const correlation = randomUUID();
111
110
  const { guarded, guardEvents } = await guardWithTombstones(
112
111
  safe,
113
112
  streamInfo,
114
- correlation,
113
+ deps.correlation,
115
114
  deps.tombstone,
116
115
  skipped
117
116
  );
@@ -129,7 +128,7 @@ async function runCloseCycle(targets, deps) {
129
128
  guarded,
130
129
  seedStates,
131
130
  guardEvents,
132
- correlation
131
+ deps.correlation
133
132
  );
134
133
  return { truncated, skipped };
135
134
  }
@@ -370,8 +369,32 @@ var CorrelateCycle = class {
370
369
  }
371
370
  };
372
371
 
372
+ // src/internal/correlator.ts
373
+ import { randomInt } from "crypto";
374
+ var BASE = 36;
375
+ var SEG_WIDTH = 4;
376
+ var SEG_SPACE = BASE ** SEG_WIDTH;
377
+ function seg(n) {
378
+ return n.toString(BASE).padStart(SEG_WIDTH, "0");
379
+ }
380
+ var defaultCorrelator = ({ state: state2, action: action2 }) => {
381
+ const s = state2.slice(0, SEG_WIDTH).toLowerCase();
382
+ const a = action2.slice(0, SEG_WIDTH).toLowerCase();
383
+ const ts = seg(Date.now() % SEG_SPACE);
384
+ const rnd = seg(randomInt(SEG_SPACE));
385
+ return `${s}-${a}-${ts}${rnd}`;
386
+ };
387
+ function closeCorrelation(correlator, actor) {
388
+ return correlator({
389
+ state: "$close",
390
+ action: "close",
391
+ stream: "$close",
392
+ actor
393
+ });
394
+ }
395
+
373
396
  // src/internal/drain-cycle.ts
374
- import { randomUUID as randomUUID2 } from "crypto";
397
+ import { randomUUID } from "crypto";
375
398
 
376
399
  // src/internal/drain-ratio.ts
377
400
  var RATIO_MIN = 0.2;
@@ -393,7 +416,7 @@ function computeLagLeadRatio(handled, lagging, leading) {
393
416
 
394
417
  // src/internal/drain-cycle.ts
395
418
  async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred) {
396
- const leased = await ops.claim(lagging, leading, randomUUID2(), leaseMillis);
419
+ const leased = await ops.claim(lagging, leading, randomUUID(), leaseMillis);
397
420
  if (!leased.length) return void 0;
398
421
  const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
399
422
  if (!active.length) {
@@ -760,9 +783,12 @@ function computeBackoffDelay(retry, opts) {
760
783
  function finalize(lease, handled, at, error, options, logger) {
761
784
  if (!error) return { lease, handled, at };
762
785
  logger.error(error);
763
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
786
+ const nonRetryable = error instanceof NonRetryableError;
787
+ const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
764
788
  if (block2)
765
- logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
789
+ logger.error(
790
+ nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
791
+ );
766
792
  const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
767
793
  return {
768
794
  lease,
@@ -912,7 +938,6 @@ var block = (leases) => store().block(leases);
912
938
  var subscribe = (streams) => store().subscribe(streams);
913
939
 
914
940
  // src/internal/event-sourcing.ts
915
- import { randomUUID as randomUUID3 } from "crypto";
916
941
  import { patch } from "@rotorsoft/act-patch";
917
942
  async function snap(snapshot) {
918
943
  try {
@@ -1000,7 +1025,7 @@ async function load(me, stream, callback, asOf) {
1000
1025
  }
1001
1026
  return { event, state: state2, version, patches, snaps, cache_hit, replayed };
1002
1027
  }
1003
- async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
1028
+ async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
1004
1029
  const { stream, expectedVersion, actor } = target;
1005
1030
  if (!stream) throw new Error("Missing target stream");
1006
1031
  const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
@@ -1046,7 +1071,12 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1046
1071
  data: skipValidation ? data : validate(name, data, me.events[name])
1047
1072
  }));
1048
1073
  const meta = {
1049
- correlation: reactingTo?.meta.correlation || randomUUID3(),
1074
+ correlation: reactingTo?.meta.correlation || correlator({
1075
+ action: action2,
1076
+ state: me.name,
1077
+ stream,
1078
+ actor: target.actor
1079
+ }),
1050
1080
  causation: {
1051
1081
  action: {
1052
1082
  name: action2,
@@ -1150,12 +1180,21 @@ var traced = (inner, exit, entry) => (async (...args) => {
1150
1180
  exit?.(result, ...args);
1151
1181
  return result;
1152
1182
  });
1153
- function buildEs(logger) {
1183
+ function buildEs(logger, correlator = defaultCorrelator) {
1184
+ const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
1185
+ me,
1186
+ actionName,
1187
+ target,
1188
+ payload,
1189
+ reactingTo,
1190
+ skipValidation,
1191
+ correlator
1192
+ );
1154
1193
  if (logger.level !== "trace") {
1155
1194
  return {
1156
1195
  snap,
1157
1196
  load,
1158
- action,
1197
+ action: boundAction,
1159
1198
  tombstone
1160
1199
  };
1161
1200
  }
@@ -1185,7 +1224,7 @@ function buildEs(logger) {
1185
1224
  );
1186
1225
  }),
1187
1226
  action: traced(
1188
- action,
1227
+ boundAction,
1189
1228
  (snapshots, _me, _action, target) => {
1190
1229
  const committed = snapshots.filter((s) => s.event);
1191
1230
  if (committed.length) {
@@ -1293,7 +1332,8 @@ var Act = class {
1293
1332
  this._states = _states;
1294
1333
  this._batch_handlers = batchHandlers;
1295
1334
  this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
1296
- this._es = buildEs(this._logger);
1335
+ this._correlator = options.correlator ?? defaultCorrelator;
1336
+ this._es = buildEs(this._logger, this._correlator);
1297
1337
  this._cd = buildDrain(this._logger);
1298
1338
  this._handle = buildHandle({
1299
1339
  logger: this._logger,
@@ -1406,6 +1446,13 @@ var Act = class {
1406
1446
  * path keeps reading fresh `store()`/`cache()` per call, which matters for
1407
1447
  * tests that dispose and re-seed mid-suite. */
1408
1448
  _scoped;
1449
+ /**
1450
+ * Correlation-id generator for originating actions. Bound at
1451
+ * construction from `options.correlator ?? defaultCorrelator`. The
1452
+ * `do()` path passes this into the `_es.action` closure; close-cycle
1453
+ * uses it via {@link closeCorrelation}.
1454
+ */
1455
+ _correlator;
1409
1456
  /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
1410
1457
  * payload (it captures the triggering event for reactingTo auto-inject). */
1411
1458
  _bound_do = this.do.bind(this);
@@ -1881,13 +1928,81 @@ var Act = class {
1881
1928
  * @see {@link Store.reset} for the underlying store primitive
1882
1929
  * @see {@link settle} for the debounced full-catch-up loop
1883
1930
  */
1884
- async reset(streams) {
1931
+ async reset(input) {
1885
1932
  return this._scoped(async () => {
1886
- const count = await store().reset(streams);
1933
+ const count = await store().reset(input);
1887
1934
  if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
1888
1935
  return count;
1889
1936
  });
1890
1937
  }
1938
+ /**
1939
+ * Clear the blocked flag on streams without replaying their history.
1940
+ *
1941
+ * Use this to recover from a poison message after fixing the
1942
+ * underlying issue — the stream resumes from the next event after the
1943
+ * last successful ack, not from the beginning. Compare with
1944
+ * {@link reset}, which rebuilds from event 0 (suitable for projection
1945
+ * rebuilds, wrong for "I fixed the bug, please retry").
1946
+ *
1947
+ * Wraps `store().unblock(streams)` and raises the orchestrator's
1948
+ * internal "needs drain" flag so a settled app picks up the now-free
1949
+ * streams on the next cycle. Equivalent to calling `store().unblock(...)`
1950
+ * directly, but `store().unblock(...)` alone leaves the flag
1951
+ * untouched.
1952
+ *
1953
+ * @param streams - Stream names to unblock
1954
+ * @returns Count of streams that were actually flipped (were blocked)
1955
+ *
1956
+ * @example Recover from a 4xx webhook after fixing the bug
1957
+ * ```typescript
1958
+ * await app.unblock(["webhooks-out-customer-42"]);
1959
+ * // The stream resumes from the next event, not from zero.
1960
+ * ```
1961
+ *
1962
+ * @see {@link Store.unblock} for the underlying store primitive
1963
+ * @see {@link reset} for the rebuild-from-zero alternative
1964
+ */
1965
+ async unblock(input) {
1966
+ return this._scoped(async () => {
1967
+ const count = await store().unblock(input);
1968
+ if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
1969
+ return count;
1970
+ });
1971
+ }
1972
+ /**
1973
+ * Return every currently-blocked stream position. Convenience wrapper
1974
+ * around `store().query_streams(cb, { blocked: true })` for the common
1975
+ * "show me what's broken" operational query.
1976
+ *
1977
+ * Results are ordered by stream name, paginated by `limit` (default
1978
+ * 100). Pass `after` to fetch the next page (keyset cursor on the
1979
+ * stream name). For richer queries — including blocked + source
1980
+ * filters, or full unblocked introspection — drop to
1981
+ * `store().query_streams(...)` directly.
1982
+ *
1983
+ * @returns Array of {@link StreamPosition} for currently-blocked streams.
1984
+ *
1985
+ * @example Discover and recover
1986
+ * ```typescript
1987
+ * const blocked = await app.blocked_streams();
1988
+ * console.table(blocked.map(({ stream, retry, error }) => ({ stream, retry, error })));
1989
+ *
1990
+ * // Operator investigates, then bulk-unblocks the family:
1991
+ * await app.unblock({ stream: "^webhooks-out-" });
1992
+ * ```
1993
+ */
1994
+ async blocked_streams(options) {
1995
+ return this._scoped(async () => {
1996
+ const positions = [];
1997
+ await store().query_streams(
1998
+ (p) => {
1999
+ positions.push(p);
2000
+ },
2001
+ { blocked: true, after: options?.after, limit: options?.limit }
2002
+ );
2003
+ return positions;
2004
+ });
2005
+ }
1891
2006
  /**
1892
2007
  * Bulk-update scheduling priority for streams matching `filter`.
1893
2008
  *
@@ -1966,12 +2081,14 @@ var Act = class {
1966
2081
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
1967
2082
  return this._scoped(async () => {
1968
2083
  await this.correlate({ limit: 1e3 });
2084
+ const closeActor = { id: "$close", name: "close" };
1969
2085
  const result = await runCloseCycle(targets, {
1970
2086
  reactiveEventsSize: this._reactive_events.size,
1971
2087
  eventToState: this._event_to_state,
1972
2088
  load: this._es.load,
1973
2089
  tombstone: this._es.tombstone,
1974
- logger: this._logger
2090
+ logger: this._logger,
2091
+ correlation: closeCorrelation(this._correlator, closeActor)
1975
2092
  });
1976
2093
  this.emit("closed", result);
1977
2094
  return result;
@@ -2341,6 +2458,7 @@ export {
2341
2458
  InMemoryStore,
2342
2459
  InvariantError,
2343
2460
  LogLevels,
2461
+ NonRetryableError,
2344
2462
  PackageSchema,
2345
2463
  QuerySchema,
2346
2464
  SNAP_EVENT,