@rotorsoft/act 0.40.0 → 0.42.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 +19 -1
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts.map +1 -1
- package/dist/@types/internal/backoff.d.ts +22 -0
- package/dist/@types/internal/backoff.d.ts.map +1 -0
- package/dist/@types/internal/close-cycle.d.ts +7 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlator.d.ts +44 -0
- package/dist/@types/internal/correlator.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +34 -1
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +10 -3
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +2 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/lru-map.d.ts.map +1 -0
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts +2 -2
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +32 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +44 -0
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/{chunk-TP2OZWHP.js → chunk-M5YFKVRV.js} +2 -2
- package/dist/chunk-M5YFKVRV.js.map +1 -0
- package/dist/index.cjs +144 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +146 -22
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +1 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/@types/lru-map.d.ts.map +0 -1
- package/dist/chunk-TP2OZWHP.js.map +0 -1
- /package/dist/@types/{lru-map.d.ts → internal/lru-map.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
sleep,
|
|
19
19
|
store,
|
|
20
20
|
validate
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-M5YFKVRV.js";
|
|
22
22
|
import {
|
|
23
23
|
ActorSchema,
|
|
24
24
|
CausationEventSchema,
|
|
@@ -95,7 +95,6 @@ function classifyRegistry(registry, states) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// src/internal/close-cycle.ts
|
|
98
|
-
import { randomUUID } from "crypto";
|
|
99
98
|
async function runCloseCycle(targets, deps) {
|
|
100
99
|
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
101
100
|
const streams = [...targetMap.keys()];
|
|
@@ -107,11 +106,10 @@ async function runCloseCycle(targets, deps) {
|
|
|
107
106
|
skipped
|
|
108
107
|
);
|
|
109
108
|
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
110
|
-
const correlation = randomUUID();
|
|
111
109
|
const { guarded, guardEvents } = await guardWithTombstones(
|
|
112
110
|
safe,
|
|
113
111
|
streamInfo,
|
|
114
|
-
correlation,
|
|
112
|
+
deps.correlation,
|
|
115
113
|
deps.tombstone,
|
|
116
114
|
skipped
|
|
117
115
|
);
|
|
@@ -129,7 +127,7 @@ async function runCloseCycle(targets, deps) {
|
|
|
129
127
|
guarded,
|
|
130
128
|
seedStates,
|
|
131
129
|
guardEvents,
|
|
132
|
-
correlation
|
|
130
|
+
deps.correlation
|
|
133
131
|
);
|
|
134
132
|
return { truncated, skipped };
|
|
135
133
|
}
|
|
@@ -370,8 +368,32 @@ var CorrelateCycle = class {
|
|
|
370
368
|
}
|
|
371
369
|
};
|
|
372
370
|
|
|
371
|
+
// src/internal/correlator.ts
|
|
372
|
+
import { randomInt } from "crypto";
|
|
373
|
+
var BASE = 36;
|
|
374
|
+
var SEG_WIDTH = 4;
|
|
375
|
+
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
376
|
+
function seg(n) {
|
|
377
|
+
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
378
|
+
}
|
|
379
|
+
var defaultCorrelator = ({ state: state2, action: action2 }) => {
|
|
380
|
+
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
381
|
+
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
382
|
+
const ts = seg(Date.now() % SEG_SPACE);
|
|
383
|
+
const rnd = seg(randomInt(SEG_SPACE));
|
|
384
|
+
return `${s}-${a}-${ts}${rnd}`;
|
|
385
|
+
};
|
|
386
|
+
function closeCorrelation(correlator, actor) {
|
|
387
|
+
return correlator({
|
|
388
|
+
state: "$close",
|
|
389
|
+
action: "close",
|
|
390
|
+
stream: "$close",
|
|
391
|
+
actor
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
373
395
|
// src/internal/drain-cycle.ts
|
|
374
|
-
import { randomUUID
|
|
396
|
+
import { randomUUID } from "crypto";
|
|
375
397
|
|
|
376
398
|
// src/internal/drain-ratio.ts
|
|
377
399
|
var RATIO_MIN = 0.2;
|
|
@@ -392,10 +414,20 @@ function computeLagLeadRatio(handled, lagging, leading) {
|
|
|
392
414
|
}
|
|
393
415
|
|
|
394
416
|
// src/internal/drain-cycle.ts
|
|
395
|
-
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
396
|
-
const leased = await ops.claim(lagging, leading,
|
|
417
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred) {
|
|
418
|
+
const leased = await ops.claim(lagging, leading, randomUUID(), leaseMillis);
|
|
397
419
|
if (!leased.length) return void 0;
|
|
398
|
-
const
|
|
420
|
+
const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
|
|
421
|
+
if (!active.length) {
|
|
422
|
+
return {
|
|
423
|
+
leased,
|
|
424
|
+
fetched: [],
|
|
425
|
+
handled: [],
|
|
426
|
+
acked: [],
|
|
427
|
+
blocked: []
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const fetched = await ops.fetch(active, eventLimit);
|
|
399
431
|
const fetchMap = /* @__PURE__ */ new Map();
|
|
400
432
|
const fetch_window_at = fetched.reduce(
|
|
401
433
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
@@ -414,7 +446,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
414
446
|
fetchMap.set(stream, { fetch: f, payloads });
|
|
415
447
|
}
|
|
416
448
|
const handled = await Promise.all(
|
|
417
|
-
|
|
449
|
+
active.map((lease) => {
|
|
418
450
|
const entry = fetchMap.get(lease.stream);
|
|
419
451
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
420
452
|
const { payloads } = entry;
|
|
@@ -446,6 +478,15 @@ var DrainController = class {
|
|
|
446
478
|
_armed = false;
|
|
447
479
|
_locked = false;
|
|
448
480
|
_ratio = 0.5;
|
|
481
|
+
/**
|
|
482
|
+
* Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
|
|
483
|
+
* `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
|
|
484
|
+
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
485
|
+
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
486
|
+
*/
|
|
487
|
+
_backoff = /* @__PURE__ */ new Map();
|
|
488
|
+
/** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
|
|
489
|
+
_backoffTimer;
|
|
449
490
|
/**
|
|
450
491
|
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
451
492
|
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
@@ -458,6 +499,32 @@ var DrainController = class {
|
|
|
458
499
|
get armed() {
|
|
459
500
|
return this._armed;
|
|
460
501
|
}
|
|
502
|
+
/** Returns true when `stream` is currently within a backoff window. */
|
|
503
|
+
isDeferred = (stream) => {
|
|
504
|
+
const next = this._backoff.get(stream);
|
|
505
|
+
return next !== void 0 && next > Date.now();
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* Schedule the next drain re-arm at the earliest pending backoff
|
|
509
|
+
* expiry. Called only when the backoff map is non-empty (caller guard).
|
|
510
|
+
* Idempotent — collapses many simultaneously deferred streams into a
|
|
511
|
+
* single timer.
|
|
512
|
+
*/
|
|
513
|
+
scheduleBackoffWake() {
|
|
514
|
+
if (this._backoffTimer) clearTimeout(this._backoffTimer);
|
|
515
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
516
|
+
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
517
|
+
const delay = Math.max(0, earliest - Date.now());
|
|
518
|
+
this._backoffTimer = setTimeout(() => {
|
|
519
|
+
this._backoffTimer = void 0;
|
|
520
|
+
const now = Date.now();
|
|
521
|
+
for (const [stream, at] of this._backoff) {
|
|
522
|
+
if (at <= now) this._backoff.delete(stream);
|
|
523
|
+
}
|
|
524
|
+
this._armed = true;
|
|
525
|
+
}, delay);
|
|
526
|
+
this._backoffTimer.unref();
|
|
527
|
+
}
|
|
461
528
|
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
462
529
|
async drain({
|
|
463
530
|
streamLimit = 10,
|
|
@@ -479,7 +546,8 @@ var DrainController = class {
|
|
|
479
546
|
lagging,
|
|
480
547
|
leading,
|
|
481
548
|
eventLimit,
|
|
482
|
-
leaseMillis
|
|
549
|
+
leaseMillis,
|
|
550
|
+
this._backoff.size > 0 ? this.isDeferred : void 0
|
|
483
551
|
);
|
|
484
552
|
if (!cycle) {
|
|
485
553
|
this._armed = false;
|
|
@@ -487,6 +555,14 @@ var DrainController = class {
|
|
|
487
555
|
}
|
|
488
556
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
489
557
|
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
558
|
+
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
559
|
+
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
560
|
+
for (const h of handled) {
|
|
561
|
+
if (h.nextAttemptAt !== void 0 && !h.block) {
|
|
562
|
+
this._backoff.set(h.lease.stream, h.nextAttemptAt);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
490
566
|
if (acked.length) this.deps.onAcked(acked);
|
|
491
567
|
if (blocked.length) this.deps.onBlocked(blocked);
|
|
492
568
|
const hasErrors = handled.some(({ error }) => error);
|
|
@@ -681,6 +757,27 @@ var _this_ = ({ stream }) => ({
|
|
|
681
757
|
target: stream
|
|
682
758
|
});
|
|
683
759
|
|
|
760
|
+
// src/internal/backoff.ts
|
|
761
|
+
function computeBackoffDelay(retry, opts) {
|
|
762
|
+
if (!opts || opts.baseMs <= 0) return 0;
|
|
763
|
+
const r = Math.max(0, retry);
|
|
764
|
+
let delay;
|
|
765
|
+
switch (opts.strategy) {
|
|
766
|
+
case "fixed":
|
|
767
|
+
delay = opts.baseMs;
|
|
768
|
+
break;
|
|
769
|
+
case "linear":
|
|
770
|
+
delay = opts.baseMs * (r + 1);
|
|
771
|
+
break;
|
|
772
|
+
case "exponential":
|
|
773
|
+
delay = opts.baseMs * 2 ** r;
|
|
774
|
+
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
778
|
+
return Math.max(0, Math.floor(delay));
|
|
779
|
+
}
|
|
780
|
+
|
|
684
781
|
// src/internal/reactions.ts
|
|
685
782
|
function finalize(lease, handled, at, error, options, logger) {
|
|
686
783
|
if (!error) return { lease, handled, at };
|
|
@@ -688,12 +785,14 @@ function finalize(lease, handled, at, error, options, logger) {
|
|
|
688
785
|
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
689
786
|
if (block2)
|
|
690
787
|
logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
|
|
788
|
+
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
691
789
|
return {
|
|
692
790
|
lease,
|
|
693
791
|
handled,
|
|
694
792
|
at,
|
|
695
793
|
error: handled === 0 ? error.message : void 0,
|
|
696
|
-
block: block2
|
|
794
|
+
block: block2,
|
|
795
|
+
nextAttemptAt
|
|
697
796
|
};
|
|
698
797
|
}
|
|
699
798
|
function buildHandle(deps) {
|
|
@@ -835,7 +934,6 @@ var block = (leases) => store().block(leases);
|
|
|
835
934
|
var subscribe = (streams) => store().subscribe(streams);
|
|
836
935
|
|
|
837
936
|
// src/internal/event-sourcing.ts
|
|
838
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
839
937
|
import { patch } from "@rotorsoft/act-patch";
|
|
840
938
|
async function snap(snapshot) {
|
|
841
939
|
try {
|
|
@@ -923,7 +1021,7 @@ async function load(me, stream, callback, asOf) {
|
|
|
923
1021
|
}
|
|
924
1022
|
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
925
1023
|
}
|
|
926
|
-
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
1024
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
|
|
927
1025
|
const { stream, expectedVersion, actor } = target;
|
|
928
1026
|
if (!stream) throw new Error("Missing target stream");
|
|
929
1027
|
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
@@ -969,7 +1067,12 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
969
1067
|
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
970
1068
|
}));
|
|
971
1069
|
const meta = {
|
|
972
|
-
correlation: reactingTo?.meta.correlation ||
|
|
1070
|
+
correlation: reactingTo?.meta.correlation || correlator({
|
|
1071
|
+
action: action2,
|
|
1072
|
+
state: me.name,
|
|
1073
|
+
stream,
|
|
1074
|
+
actor: target.actor
|
|
1075
|
+
}),
|
|
973
1076
|
causation: {
|
|
974
1077
|
action: {
|
|
975
1078
|
name: action2,
|
|
@@ -1073,12 +1176,21 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
1073
1176
|
exit?.(result, ...args);
|
|
1074
1177
|
return result;
|
|
1075
1178
|
});
|
|
1076
|
-
function buildEs(logger) {
|
|
1179
|
+
function buildEs(logger, correlator = defaultCorrelator) {
|
|
1180
|
+
const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
|
|
1181
|
+
me,
|
|
1182
|
+
actionName,
|
|
1183
|
+
target,
|
|
1184
|
+
payload,
|
|
1185
|
+
reactingTo,
|
|
1186
|
+
skipValidation,
|
|
1187
|
+
correlator
|
|
1188
|
+
);
|
|
1077
1189
|
if (logger.level !== "trace") {
|
|
1078
1190
|
return {
|
|
1079
1191
|
snap,
|
|
1080
1192
|
load,
|
|
1081
|
-
action,
|
|
1193
|
+
action: boundAction,
|
|
1082
1194
|
tombstone
|
|
1083
1195
|
};
|
|
1084
1196
|
}
|
|
@@ -1108,7 +1220,7 @@ function buildEs(logger) {
|
|
|
1108
1220
|
);
|
|
1109
1221
|
}),
|
|
1110
1222
|
action: traced(
|
|
1111
|
-
|
|
1223
|
+
boundAction,
|
|
1112
1224
|
(snapshots, _me, _action, target) => {
|
|
1113
1225
|
const committed = snapshots.filter((s) => s.event);
|
|
1114
1226
|
if (committed.length) {
|
|
@@ -1216,7 +1328,8 @@ var Act = class {
|
|
|
1216
1328
|
this._states = _states;
|
|
1217
1329
|
this._batch_handlers = batchHandlers;
|
|
1218
1330
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
1219
|
-
this.
|
|
1331
|
+
this._correlator = options.correlator ?? defaultCorrelator;
|
|
1332
|
+
this._es = buildEs(this._logger, this._correlator);
|
|
1220
1333
|
this._cd = buildDrain(this._logger);
|
|
1221
1334
|
this._handle = buildHandle({
|
|
1222
1335
|
logger: this._logger,
|
|
@@ -1329,6 +1442,13 @@ var Act = class {
|
|
|
1329
1442
|
* path keeps reading fresh `store()`/`cache()` per call, which matters for
|
|
1330
1443
|
* tests that dispose and re-seed mid-suite. */
|
|
1331
1444
|
_scoped;
|
|
1445
|
+
/**
|
|
1446
|
+
* Correlation-id generator for originating actions. Bound at
|
|
1447
|
+
* construction from `options.correlator ?? defaultCorrelator`. The
|
|
1448
|
+
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
1449
|
+
* uses it via {@link closeCorrelation}.
|
|
1450
|
+
*/
|
|
1451
|
+
_correlator;
|
|
1332
1452
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
1333
1453
|
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
1334
1454
|
_bound_do = this.do.bind(this);
|
|
@@ -1889,12 +2009,14 @@ var Act = class {
|
|
|
1889
2009
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
1890
2010
|
return this._scoped(async () => {
|
|
1891
2011
|
await this.correlate({ limit: 1e3 });
|
|
2012
|
+
const closeActor = { id: "$close", name: "close" };
|
|
1892
2013
|
const result = await runCloseCycle(targets, {
|
|
1893
2014
|
reactiveEventsSize: this._reactive_events.size,
|
|
1894
2015
|
eventToState: this._event_to_state,
|
|
1895
2016
|
load: this._es.load,
|
|
1896
2017
|
tombstone: this._es.tombstone,
|
|
1897
|
-
logger: this._logger
|
|
2018
|
+
logger: this._logger,
|
|
2019
|
+
correlation: closeCorrelation(this._correlator, closeActor)
|
|
1898
2020
|
});
|
|
1899
2021
|
this.emit("closed", result);
|
|
1900
2022
|
return result;
|
|
@@ -2013,7 +2135,8 @@ function act() {
|
|
|
2013
2135
|
resolver: _this_,
|
|
2014
2136
|
options: {
|
|
2015
2137
|
blockOnError: options?.blockOnError ?? true,
|
|
2016
|
-
maxRetries: options?.maxRetries ?? 3
|
|
2138
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
2139
|
+
backoff: options?.backoff
|
|
2017
2140
|
}
|
|
2018
2141
|
};
|
|
2019
2142
|
if (!handler.name)
|
|
@@ -2139,7 +2262,8 @@ function slice() {
|
|
|
2139
2262
|
resolver: _this_,
|
|
2140
2263
|
options: {
|
|
2141
2264
|
blockOnError: options?.blockOnError ?? true,
|
|
2142
|
-
maxRetries: options?.maxRetries ?? 3
|
|
2265
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
2266
|
+
backoff: options?.backoff
|
|
2143
2267
|
}
|
|
2144
2268
|
};
|
|
2145
2269
|
if (!handler.name)
|