@rotorsoft/act 0.42.0 → 0.44.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
@@ -18,7 +18,7 @@ import {
18
18
  sleep,
19
19
  store,
20
20
  validate
21
- } from "./chunk-M5YFKVRV.js";
21
+ } from "./chunk-LKRNWD7C.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
@@ -132,24 +133,18 @@ async function runCloseCycle(targets, deps) {
132
133
  return { truncated, skipped };
133
134
  }
134
135
  async function scanStreamHeads(streams) {
136
+ const stats = await store().query_stats(streams, {
137
+ exclude: [SNAP_EVENT]
138
+ });
135
139
  const out = /* @__PURE__ */ new Map();
136
- await Promise.all(
137
- streams.map(async (s) => {
138
- let maxId = -1;
139
- let version = -1;
140
- let lastEventName = "";
141
- await store().query(
142
- (e) => {
143
- if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
144
- maxId = e.id;
145
- version = e.version;
146
- lastEventName = e.name;
147
- },
148
- { stream: s, stream_exact: true, backward: true, limit: 1 }
149
- );
150
- if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
151
- })
152
- );
140
+ for (const [stream, { head }] of stats) {
141
+ if (head.name === TOMBSTONE_EVENT) continue;
142
+ out.set(stream, {
143
+ maxId: head.id,
144
+ version: head.version,
145
+ lastEventName: head.name
146
+ });
147
+ }
153
148
  return out;
154
149
  }
155
150
  async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
@@ -782,9 +777,12 @@ function computeBackoffDelay(retry, opts) {
782
777
  function finalize(lease, handled, at, error, options, logger) {
783
778
  if (!error) return { lease, handled, at };
784
779
  logger.error(error);
785
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
780
+ const nonRetryable = error instanceof NonRetryableError;
781
+ const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
786
782
  if (block2)
787
- logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
783
+ logger.error(
784
+ nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
785
+ );
788
786
  const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
789
787
  return {
790
788
  lease,
@@ -1924,13 +1922,81 @@ var Act = class {
1924
1922
  * @see {@link Store.reset} for the underlying store primitive
1925
1923
  * @see {@link settle} for the debounced full-catch-up loop
1926
1924
  */
1927
- async reset(streams) {
1925
+ async reset(input) {
1926
+ return this._scoped(async () => {
1927
+ const count = await store().reset(input);
1928
+ if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
1929
+ return count;
1930
+ });
1931
+ }
1932
+ /**
1933
+ * Clear the blocked flag on streams without replaying their history.
1934
+ *
1935
+ * Use this to recover from a poison message after fixing the
1936
+ * underlying issue — the stream resumes from the next event after the
1937
+ * last successful ack, not from the beginning. Compare with
1938
+ * {@link reset}, which rebuilds from event 0 (suitable for projection
1939
+ * rebuilds, wrong for "I fixed the bug, please retry").
1940
+ *
1941
+ * Wraps `store().unblock(streams)` and raises the orchestrator's
1942
+ * internal "needs drain" flag so a settled app picks up the now-free
1943
+ * streams on the next cycle. Equivalent to calling `store().unblock(...)`
1944
+ * directly, but `store().unblock(...)` alone leaves the flag
1945
+ * untouched.
1946
+ *
1947
+ * @param streams - Stream names to unblock
1948
+ * @returns Count of streams that were actually flipped (were blocked)
1949
+ *
1950
+ * @example Recover from a 4xx webhook after fixing the bug
1951
+ * ```typescript
1952
+ * await app.unblock(["webhooks-out-customer-42"]);
1953
+ * // The stream resumes from the next event, not from zero.
1954
+ * ```
1955
+ *
1956
+ * @see {@link Store.unblock} for the underlying store primitive
1957
+ * @see {@link reset} for the rebuild-from-zero alternative
1958
+ */
1959
+ async unblock(input) {
1928
1960
  return this._scoped(async () => {
1929
- const count = await store().reset(streams);
1961
+ const count = await store().unblock(input);
1930
1962
  if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
1931
1963
  return count;
1932
1964
  });
1933
1965
  }
1966
+ /**
1967
+ * Return every currently-blocked stream position. Convenience wrapper
1968
+ * around `store().query_streams(cb, { blocked: true })` for the common
1969
+ * "show me what's broken" operational query.
1970
+ *
1971
+ * Results are ordered by stream name, paginated by `limit` (default
1972
+ * 100). Pass `after` to fetch the next page (keyset cursor on the
1973
+ * stream name). For richer queries — including blocked + source
1974
+ * filters, or full unblocked introspection — drop to
1975
+ * `store().query_streams(...)` directly.
1976
+ *
1977
+ * @returns Array of {@link StreamPosition} for currently-blocked streams.
1978
+ *
1979
+ * @example Discover and recover
1980
+ * ```typescript
1981
+ * const blocked = await app.blocked_streams();
1982
+ * console.table(blocked.map(({ stream, retry, error }) => ({ stream, retry, error })));
1983
+ *
1984
+ * // Operator investigates, then bulk-unblocks the family:
1985
+ * await app.unblock({ stream: "^webhooks-out-" });
1986
+ * ```
1987
+ */
1988
+ async blocked_streams(options) {
1989
+ return this._scoped(async () => {
1990
+ const positions = [];
1991
+ await store().query_streams(
1992
+ (p) => {
1993
+ positions.push(p);
1994
+ },
1995
+ { blocked: true, after: options?.after, limit: options?.limit }
1996
+ );
1997
+ return positions;
1998
+ });
1999
+ }
1934
2000
  /**
1935
2001
  * Bulk-update scheduling priority for streams matching `filter`.
1936
2002
  *
@@ -2386,6 +2452,7 @@ export {
2386
2452
  InMemoryStore,
2387
2453
  InvariantError,
2388
2454
  LogLevels,
2455
+ NonRetryableError,
2389
2456
  PackageSchema,
2390
2457
  QuerySchema,
2391
2458
  SNAP_EVENT,