@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/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +57 -3
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +43 -6
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts +1 -1
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +12 -3
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +56 -0
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +274 -8
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-M5YFKVRV.js → chunk-LKRNWD7C.js} +160 -24
- package/dist/chunk-LKRNWD7C.js.map +1 -0
- package/dist/{chunk-AGWZY6YT.js → chunk-VMX7RPTC.js} +13 -2
- package/dist/chunk-VMX7RPTC.js.map +1 -0
- package/dist/index.cjs +257 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +90 -23
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +162 -25
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +4 -4
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs +13 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +3 -1
- package/package.json +2 -2
- package/dist/chunk-AGWZY6YT.js.map +0 -1
- package/dist/chunk-M5YFKVRV.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -46,6 +46,7 @@ __export(index_exports, {
|
|
|
46
46
|
InMemoryStore: () => InMemoryStore,
|
|
47
47
|
InvariantError: () => InvariantError,
|
|
48
48
|
LogLevels: () => LogLevels,
|
|
49
|
+
NonRetryableError: () => NonRetryableError,
|
|
49
50
|
PackageSchema: () => PackageSchema,
|
|
50
51
|
QuerySchema: () => QuerySchema,
|
|
51
52
|
SNAP_EVENT: () => SNAP_EVENT,
|
|
@@ -286,7 +287,8 @@ var Errors = {
|
|
|
286
287
|
ValidationError: "ERR_VALIDATION",
|
|
287
288
|
InvariantError: "ERR_INVARIANT",
|
|
288
289
|
ConcurrencyError: "ERR_CONCURRENCY",
|
|
289
|
-
StreamClosedError: "ERR_STREAM_CLOSED"
|
|
290
|
+
StreamClosedError: "ERR_STREAM_CLOSED",
|
|
291
|
+
NonRetryableError: "ERR_NON_RETRYABLE"
|
|
290
292
|
};
|
|
291
293
|
var ValidationError = class extends Error {
|
|
292
294
|
constructor(target, payload, details) {
|
|
@@ -329,6 +331,15 @@ var StreamClosedError = class extends Error {
|
|
|
329
331
|
this.name = Errors.StreamClosedError;
|
|
330
332
|
}
|
|
331
333
|
};
|
|
334
|
+
var NonRetryableError = class extends Error {
|
|
335
|
+
/** The original failure, if any. Mirrors the standard `Error.cause` shape. */
|
|
336
|
+
cause;
|
|
337
|
+
constructor(message, options) {
|
|
338
|
+
super(message);
|
|
339
|
+
this.name = Errors.NonRetryableError;
|
|
340
|
+
this.cause = options?.cause;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
332
343
|
|
|
333
344
|
// src/utils.ts
|
|
334
345
|
var import_zod3 = require("zod");
|
|
@@ -597,6 +608,20 @@ var InMemoryStream = class {
|
|
|
597
608
|
this._leased_by = void 0;
|
|
598
609
|
this._leased_until = void 0;
|
|
599
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Clear the blocked flag and lease bookkeeping without touching the
|
|
613
|
+
* watermark. Returns true if the stream was actually blocked (and is
|
|
614
|
+
* now flipped); false otherwise.
|
|
615
|
+
*/
|
|
616
|
+
unblock() {
|
|
617
|
+
if (!this._blocked) return false;
|
|
618
|
+
this._blocked = false;
|
|
619
|
+
this._retry = -1;
|
|
620
|
+
this._error = "";
|
|
621
|
+
this._leased_by = void 0;
|
|
622
|
+
this._leased_until = void 0;
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
600
625
|
};
|
|
601
626
|
var InMemoryStore = class {
|
|
602
627
|
// stored events
|
|
@@ -832,19 +857,81 @@ var InMemoryStore = class {
|
|
|
832
857
|
return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
|
|
833
858
|
}
|
|
834
859
|
/**
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
860
|
+
* Build a predicate from a {@link StreamFilter}. Compiled regexes are
|
|
861
|
+
* cached in the closure so callers can apply it across the streams
|
|
862
|
+
* map without re-compiling per iteration.
|
|
863
|
+
*/
|
|
864
|
+
_filterPredicate(filter) {
|
|
865
|
+
const streamRe = filter.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
866
|
+
const sourceRe = filter.source && !filter.source_exact ? new RegExp(filter.source) : void 0;
|
|
867
|
+
return (s) => {
|
|
868
|
+
if (filter.stream !== void 0) {
|
|
869
|
+
if (filter.stream_exact ? s.stream !== filter.stream : !streamRe.test(s.stream))
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
if (filter.source !== void 0) {
|
|
873
|
+
if (s.source === void 0) return false;
|
|
874
|
+
if (filter.source_exact ? s.source !== filter.source : !sourceRe.test(s.source))
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
878
|
+
return false;
|
|
879
|
+
return true;
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Reset watermarks to -1, clearing retry, blocked, error, and lease
|
|
884
|
+
* state so the matched streams can be replayed from the beginning.
|
|
885
|
+
* Accepts either an explicit list of names or a {@link StreamFilter}.
|
|
886
|
+
*
|
|
887
|
+
* @param input - Stream names or a filter selecting the streams to reset.
|
|
838
888
|
* @returns Count of streams that were actually reset.
|
|
839
889
|
*/
|
|
840
|
-
async reset(
|
|
890
|
+
async reset(input) {
|
|
841
891
|
await sleep();
|
|
842
892
|
let count = 0;
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
s
|
|
847
|
-
|
|
893
|
+
if (Array.isArray(input)) {
|
|
894
|
+
for (const name of input) {
|
|
895
|
+
const s = this._streams.get(name);
|
|
896
|
+
if (s) {
|
|
897
|
+
s.reset();
|
|
898
|
+
count++;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
const matches = this._filterPredicate(input);
|
|
903
|
+
for (const s of this._streams.values()) {
|
|
904
|
+
if (matches(s)) {
|
|
905
|
+
s.reset();
|
|
906
|
+
count++;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return count;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Clear the blocked flag (and retry / error / lease) on the matched
|
|
914
|
+
* streams without touching the watermark. Streams that aren't blocked
|
|
915
|
+
* at call time are silently skipped. Accepts either an explicit list
|
|
916
|
+
* of names or a {@link StreamFilter}. The filter form always restricts
|
|
917
|
+
* to blocked streams — passing `blocked: false` matches nothing.
|
|
918
|
+
* See {@link Store.unblock}.
|
|
919
|
+
*
|
|
920
|
+
* @param input - Stream names or a filter selecting the streams to unblock.
|
|
921
|
+
* @returns Count of streams that were actually flipped (were blocked).
|
|
922
|
+
*/
|
|
923
|
+
async unblock(input) {
|
|
924
|
+
await sleep();
|
|
925
|
+
let count = 0;
|
|
926
|
+
if (Array.isArray(input)) {
|
|
927
|
+
for (const name of input) {
|
|
928
|
+
const s = this._streams.get(name);
|
|
929
|
+
if (s?.unblock()) count++;
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
const matches = this._filterPredicate({ ...input, blocked: true });
|
|
933
|
+
for (const s of this._streams.values()) {
|
|
934
|
+
if (matches(s) && s.unblock()) count++;
|
|
848
935
|
}
|
|
849
936
|
}
|
|
850
937
|
return count;
|
|
@@ -860,21 +947,10 @@ var InMemoryStore = class {
|
|
|
860
947
|
*/
|
|
861
948
|
async prioritize(filter, priority) {
|
|
862
949
|
await sleep();
|
|
863
|
-
const
|
|
864
|
-
const sourceRe = filter.source && !filter.source_exact ? new RegExp(filter.source) : void 0;
|
|
950
|
+
const matches = this._filterPredicate(filter);
|
|
865
951
|
let count = 0;
|
|
866
952
|
for (const s of this._streams.values()) {
|
|
867
|
-
if (
|
|
868
|
-
if (filter.stream_exact ? s.stream !== filter.stream : !streamRe.test(s.stream))
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
if (filter.source !== void 0) {
|
|
872
|
-
if (s.source === void 0) continue;
|
|
873
|
-
if (filter.source_exact ? s.source !== filter.source : !sourceRe.test(s.source))
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
877
|
-
continue;
|
|
953
|
+
if (!matches(s)) continue;
|
|
878
954
|
if (s.priority !== priority) {
|
|
879
955
|
s.setPriority(priority);
|
|
880
956
|
count++;
|
|
@@ -926,6 +1002,77 @@ var InMemoryStore = class {
|
|
|
926
1002
|
}
|
|
927
1003
|
return { maxEventId: this._events.length - 1, count };
|
|
928
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Per-stream aggregated stats — see {@link Store.query_stats}.
|
|
1007
|
+
*
|
|
1008
|
+
* Single forward scan over the in-memory event list, accumulating per
|
|
1009
|
+
* stream. The "cheap heads" cost tier from durable adapters doesn't
|
|
1010
|
+
* apply here (InMemory has no indexes); correctness is the goal, perf
|
|
1011
|
+
* is a non-issue.
|
|
1012
|
+
*
|
|
1013
|
+
* Scope rules:
|
|
1014
|
+
* - Array `input` — explicit stream names, regardless of subscription.
|
|
1015
|
+
* - Filter `input` — `stream`/`stream_exact` match against event-bearing
|
|
1016
|
+
* stream names; `source`/`source_exact`/`blocked` require a
|
|
1017
|
+
* corresponding subscription in `_streams` (those are subscription
|
|
1018
|
+
* concepts, not event concepts). Empty filter `{}` matches every
|
|
1019
|
+
* event-bearing stream.
|
|
1020
|
+
*/
|
|
1021
|
+
async query_stats(input, options) {
|
|
1022
|
+
await sleep();
|
|
1023
|
+
const exclude = new Set(options?.exclude ?? []);
|
|
1024
|
+
const wantTail = options?.tail ?? false;
|
|
1025
|
+
const wantCount = options?.count ?? false;
|
|
1026
|
+
const wantNames = options?.names ?? false;
|
|
1027
|
+
const before = options?.before;
|
|
1028
|
+
const arrayTargets = Array.isArray(input) ? new Set(input) : null;
|
|
1029
|
+
const filter = Array.isArray(input) ? null : input;
|
|
1030
|
+
const streamRe = filter?.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
1031
|
+
const scopeCache = /* @__PURE__ */ new Map();
|
|
1032
|
+
const inScope = (stream) => {
|
|
1033
|
+
const cached = scopeCache.get(stream);
|
|
1034
|
+
if (cached !== void 0) return cached;
|
|
1035
|
+
let ok = true;
|
|
1036
|
+
if (arrayTargets) {
|
|
1037
|
+
ok = arrayTargets.has(stream);
|
|
1038
|
+
} else if (filter?.stream !== void 0) {
|
|
1039
|
+
ok = filter.stream_exact ? stream === filter.stream : (
|
|
1040
|
+
// biome-ignore lint/style/noNonNullAssertion: streamRe set when stream is regex
|
|
1041
|
+
streamRe.test(stream)
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
scopeCache.set(stream, ok);
|
|
1045
|
+
return ok;
|
|
1046
|
+
};
|
|
1047
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1048
|
+
for (const e of this._events) {
|
|
1049
|
+
if (before !== void 0 && e.id >= before) continue;
|
|
1050
|
+
if (!inScope(e.stream)) continue;
|
|
1051
|
+
if (exclude.has(e.name)) continue;
|
|
1052
|
+
let a = acc.get(e.stream);
|
|
1053
|
+
if (!a) {
|
|
1054
|
+
a = { head: e, count: 0 };
|
|
1055
|
+
if (wantTail) a.tail = e;
|
|
1056
|
+
if (wantNames) a.names = {};
|
|
1057
|
+
acc.set(e.stream, a);
|
|
1058
|
+
}
|
|
1059
|
+
a.head = e;
|
|
1060
|
+
a.count++;
|
|
1061
|
+
if (wantNames) {
|
|
1062
|
+
const n = String(e.name);
|
|
1063
|
+
a.names[n] = (a.names[n] ?? 0) + 1;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const out = /* @__PURE__ */ new Map();
|
|
1067
|
+
for (const [stream, a] of acc) {
|
|
1068
|
+
const stats = { head: a.head };
|
|
1069
|
+
if (wantTail) stats.tail = a.tail;
|
|
1070
|
+
if (wantCount) stats.count = a.count;
|
|
1071
|
+
if (wantNames) stats.names = a.names;
|
|
1072
|
+
out.set(stream, stats);
|
|
1073
|
+
}
|
|
1074
|
+
return out;
|
|
1075
|
+
}
|
|
929
1076
|
/**
|
|
930
1077
|
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
931
1078
|
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
@@ -1127,24 +1274,18 @@ async function runCloseCycle(targets, deps) {
|
|
|
1127
1274
|
return { truncated, skipped };
|
|
1128
1275
|
}
|
|
1129
1276
|
async function scanStreamHeads(streams) {
|
|
1277
|
+
const stats = await store2().query_stats(streams, {
|
|
1278
|
+
exclude: [SNAP_EVENT]
|
|
1279
|
+
});
|
|
1130
1280
|
const out = /* @__PURE__ */ new Map();
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
maxId = e.id;
|
|
1140
|
-
version = e.version;
|
|
1141
|
-
lastEventName = e.name;
|
|
1142
|
-
},
|
|
1143
|
-
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
1144
|
-
);
|
|
1145
|
-
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
1146
|
-
})
|
|
1147
|
-
);
|
|
1281
|
+
for (const [stream, { head }] of stats) {
|
|
1282
|
+
if (head.name === TOMBSTONE_EVENT) continue;
|
|
1283
|
+
out.set(stream, {
|
|
1284
|
+
maxId: head.id,
|
|
1285
|
+
version: head.version,
|
|
1286
|
+
lastEventName: head.name
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1148
1289
|
return out;
|
|
1149
1290
|
}
|
|
1150
1291
|
async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
|
|
@@ -1777,9 +1918,12 @@ function computeBackoffDelay(retry, opts) {
|
|
|
1777
1918
|
function finalize(lease, handled, at, error, options, logger) {
|
|
1778
1919
|
if (!error) return { lease, handled, at };
|
|
1779
1920
|
logger.error(error);
|
|
1780
|
-
const
|
|
1921
|
+
const nonRetryable = error instanceof NonRetryableError;
|
|
1922
|
+
const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
|
|
1781
1923
|
if (block2)
|
|
1782
|
-
logger.error(
|
|
1924
|
+
logger.error(
|
|
1925
|
+
nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1926
|
+
);
|
|
1783
1927
|
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
1784
1928
|
return {
|
|
1785
1929
|
lease,
|
|
@@ -2919,13 +3063,81 @@ var Act = class {
|
|
|
2919
3063
|
* @see {@link Store.reset} for the underlying store primitive
|
|
2920
3064
|
* @see {@link settle} for the debounced full-catch-up loop
|
|
2921
3065
|
*/
|
|
2922
|
-
async reset(
|
|
3066
|
+
async reset(input) {
|
|
3067
|
+
return this._scoped(async () => {
|
|
3068
|
+
const count = await store2().reset(input);
|
|
3069
|
+
if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
|
|
3070
|
+
return count;
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Clear the blocked flag on streams without replaying their history.
|
|
3075
|
+
*
|
|
3076
|
+
* Use this to recover from a poison message after fixing the
|
|
3077
|
+
* underlying issue — the stream resumes from the next event after the
|
|
3078
|
+
* last successful ack, not from the beginning. Compare with
|
|
3079
|
+
* {@link reset}, which rebuilds from event 0 (suitable for projection
|
|
3080
|
+
* rebuilds, wrong for "I fixed the bug, please retry").
|
|
3081
|
+
*
|
|
3082
|
+
* Wraps `store().unblock(streams)` and raises the orchestrator's
|
|
3083
|
+
* internal "needs drain" flag so a settled app picks up the now-free
|
|
3084
|
+
* streams on the next cycle. Equivalent to calling `store().unblock(...)`
|
|
3085
|
+
* directly, but `store().unblock(...)` alone leaves the flag
|
|
3086
|
+
* untouched.
|
|
3087
|
+
*
|
|
3088
|
+
* @param streams - Stream names to unblock
|
|
3089
|
+
* @returns Count of streams that were actually flipped (were blocked)
|
|
3090
|
+
*
|
|
3091
|
+
* @example Recover from a 4xx webhook after fixing the bug
|
|
3092
|
+
* ```typescript
|
|
3093
|
+
* await app.unblock(["webhooks-out-customer-42"]);
|
|
3094
|
+
* // The stream resumes from the next event, not from zero.
|
|
3095
|
+
* ```
|
|
3096
|
+
*
|
|
3097
|
+
* @see {@link Store.unblock} for the underlying store primitive
|
|
3098
|
+
* @see {@link reset} for the rebuild-from-zero alternative
|
|
3099
|
+
*/
|
|
3100
|
+
async unblock(input) {
|
|
2923
3101
|
return this._scoped(async () => {
|
|
2924
|
-
const count = await store2().
|
|
3102
|
+
const count = await store2().unblock(input);
|
|
2925
3103
|
if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
|
|
2926
3104
|
return count;
|
|
2927
3105
|
});
|
|
2928
3106
|
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Return every currently-blocked stream position. Convenience wrapper
|
|
3109
|
+
* around `store().query_streams(cb, { blocked: true })` for the common
|
|
3110
|
+
* "show me what's broken" operational query.
|
|
3111
|
+
*
|
|
3112
|
+
* Results are ordered by stream name, paginated by `limit` (default
|
|
3113
|
+
* 100). Pass `after` to fetch the next page (keyset cursor on the
|
|
3114
|
+
* stream name). For richer queries — including blocked + source
|
|
3115
|
+
* filters, or full unblocked introspection — drop to
|
|
3116
|
+
* `store().query_streams(...)` directly.
|
|
3117
|
+
*
|
|
3118
|
+
* @returns Array of {@link StreamPosition} for currently-blocked streams.
|
|
3119
|
+
*
|
|
3120
|
+
* @example Discover and recover
|
|
3121
|
+
* ```typescript
|
|
3122
|
+
* const blocked = await app.blocked_streams();
|
|
3123
|
+
* console.table(blocked.map(({ stream, retry, error }) => ({ stream, retry, error })));
|
|
3124
|
+
*
|
|
3125
|
+
* // Operator investigates, then bulk-unblocks the family:
|
|
3126
|
+
* await app.unblock({ stream: "^webhooks-out-" });
|
|
3127
|
+
* ```
|
|
3128
|
+
*/
|
|
3129
|
+
async blocked_streams(options) {
|
|
3130
|
+
return this._scoped(async () => {
|
|
3131
|
+
const positions = [];
|
|
3132
|
+
await store2().query_streams(
|
|
3133
|
+
(p) => {
|
|
3134
|
+
positions.push(p);
|
|
3135
|
+
},
|
|
3136
|
+
{ blocked: true, after: options?.after, limit: options?.limit }
|
|
3137
|
+
);
|
|
3138
|
+
return positions;
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
2929
3141
|
/**
|
|
2930
3142
|
* Bulk-update scheduling priority for streams matching `filter`.
|
|
2931
3143
|
*
|
|
@@ -3382,6 +3594,7 @@ function action_builder(state2) {
|
|
|
3382
3594
|
InMemoryStore,
|
|
3383
3595
|
InvariantError,
|
|
3384
3596
|
LogLevels,
|
|
3597
|
+
NonRetryableError,
|
|
3385
3598
|
PackageSchema,
|
|
3386
3599
|
QuerySchema,
|
|
3387
3600
|
SNAP_EVENT,
|