@rotorsoft/act 1.3.0 → 1.5.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 +28 -3
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +6 -6
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +1 -1
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +11 -21
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/lru-map.d.ts +2 -2
- package/dist/@types/internal/lru-map.d.ts.map +1 -1
- package/dist/@types/internal/settle.d.ts +3 -5
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +21 -5
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +4 -30
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/{chunk-XKCTGUW2.js → chunk-I4L224TZ.js} +15 -12
- package/dist/chunk-I4L224TZ.js.map +1 -0
- package/dist/{chunk-TZWDSNSN.js → chunk-PMAZTOSO.js} +31 -5
- package/dist/chunk-PMAZTOSO.js.map +1 -0
- package/dist/index.cjs +281 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +240 -234
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +34 -16
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +7 -6
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs +30 -4
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TZWDSNSN.js.map +0 -1
- package/dist/chunk-XKCTGUW2.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sleep,
|
|
20
20
|
store,
|
|
21
21
|
validate
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-I4L224TZ.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
TargetSchema,
|
|
37
37
|
ValidationError,
|
|
38
38
|
ZodEmpty
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-PMAZTOSO.js";
|
|
40
40
|
import "./chunk-5WRI5ZAA.js";
|
|
41
41
|
|
|
42
42
|
// src/signals.ts
|
|
@@ -681,18 +681,23 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
681
681
|
|
|
682
682
|
// src/internal/correlate-cycle.ts
|
|
683
683
|
var CorrelateCycle = class {
|
|
684
|
-
constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
|
|
685
|
-
this.registry = registry;
|
|
686
|
-
this.staticTargets = staticTargets;
|
|
687
|
-
this.hasDynamicResolvers = hasDynamicResolvers;
|
|
688
|
-
this.cd = cd;
|
|
689
|
-
this.onInit = onInit;
|
|
690
|
-
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
691
|
-
}
|
|
692
684
|
_checkpoint = -1;
|
|
693
685
|
_initialized = false;
|
|
694
686
|
_timer = void 0;
|
|
695
687
|
_subscribed;
|
|
688
|
+
_registry;
|
|
689
|
+
_static_targets;
|
|
690
|
+
_has_dynamic_resolvers;
|
|
691
|
+
_cd;
|
|
692
|
+
_on_init;
|
|
693
|
+
constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
|
|
694
|
+
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
695
|
+
this._registry = registry;
|
|
696
|
+
this._static_targets = staticTargets;
|
|
697
|
+
this._has_dynamic_resolvers = hasDynamicResolvers;
|
|
698
|
+
this._cd = cd;
|
|
699
|
+
this._on_init = onInit;
|
|
700
|
+
}
|
|
696
701
|
/** Last correlated event id. */
|
|
697
702
|
get checkpoint() {
|
|
698
703
|
return this._checkpoint;
|
|
@@ -707,10 +712,10 @@ var CorrelateCycle = class {
|
|
|
707
712
|
async init() {
|
|
708
713
|
if (this._initialized) return;
|
|
709
714
|
this._initialized = true;
|
|
710
|
-
const { watermark } = await store().subscribe([...this.
|
|
715
|
+
const { watermark } = await store().subscribe([...this._static_targets]);
|
|
711
716
|
this._checkpoint = watermark;
|
|
712
|
-
this.
|
|
713
|
-
for (const { stream } of this.
|
|
717
|
+
this._on_init?.();
|
|
718
|
+
for (const { stream } of this._static_targets) {
|
|
714
719
|
this._subscribed.add(stream);
|
|
715
720
|
}
|
|
716
721
|
}
|
|
@@ -721,7 +726,7 @@ var CorrelateCycle = class {
|
|
|
721
726
|
*/
|
|
722
727
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
723
728
|
await this.init();
|
|
724
|
-
if (!this.
|
|
729
|
+
if (!this._has_dynamic_resolvers)
|
|
725
730
|
return { subscribed: 0, last_id: this._checkpoint };
|
|
726
731
|
const after = Math.max(this._checkpoint, query.after || -1);
|
|
727
732
|
const correlated = /* @__PURE__ */ new Map();
|
|
@@ -729,7 +734,7 @@ var CorrelateCycle = class {
|
|
|
729
734
|
await store().query(
|
|
730
735
|
(event) => {
|
|
731
736
|
last_id = event.id;
|
|
732
|
-
const register = this.
|
|
737
|
+
const register = this._registry.events[event.name];
|
|
733
738
|
if (register) {
|
|
734
739
|
for (const reaction of register.reactions.values()) {
|
|
735
740
|
if (typeof reaction.resolver !== "function") continue;
|
|
@@ -765,7 +770,7 @@ var CorrelateCycle = class {
|
|
|
765
770
|
lane
|
|
766
771
|
})
|
|
767
772
|
);
|
|
768
|
-
const { subscribed } = await this.
|
|
773
|
+
const { subscribed } = await this._cd.subscribe(streams);
|
|
769
774
|
this._checkpoint = last_id;
|
|
770
775
|
if (subscribed) {
|
|
771
776
|
for (const { stream } of streams) {
|
|
@@ -868,51 +873,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
868
873
|
|
|
869
874
|
// src/internal/event-sourcing.ts
|
|
870
875
|
import { patch } from "@rotorsoft/act-patch";
|
|
871
|
-
|
|
872
|
-
const state2 = {
|
|
873
|
-
slot: null,
|
|
874
|
-
onProduce: null,
|
|
875
|
-
onConsume: null,
|
|
876
|
-
done: false,
|
|
877
|
-
error: void 0
|
|
878
|
-
};
|
|
879
|
-
const wakeProduce = () => {
|
|
880
|
-
const fn = state2.onProduce;
|
|
881
|
-
state2.onProduce = null;
|
|
882
|
-
if (fn) fn();
|
|
883
|
-
};
|
|
884
|
-
void source.query((event) => {
|
|
885
|
-
state2.slot = event;
|
|
886
|
-
wakeProduce();
|
|
887
|
-
return new Promise((resolve) => {
|
|
888
|
-
state2.onConsume = () => resolve();
|
|
889
|
-
});
|
|
890
|
-
}, filter).then(
|
|
891
|
-
() => {
|
|
892
|
-
state2.done = true;
|
|
893
|
-
wakeProduce();
|
|
894
|
-
},
|
|
895
|
-
(err) => {
|
|
896
|
-
state2.error = err;
|
|
897
|
-
state2.done = true;
|
|
898
|
-
wakeProduce();
|
|
899
|
-
}
|
|
900
|
-
);
|
|
901
|
-
while (true) {
|
|
902
|
-
if (state2.slot === null && !state2.done)
|
|
903
|
-
await new Promise((resolve) => {
|
|
904
|
-
state2.onProduce = resolve;
|
|
905
|
-
});
|
|
906
|
-
if (state2.error) throw state2.error;
|
|
907
|
-
if (state2.slot === null) return;
|
|
908
|
-
const event = state2.slot;
|
|
909
|
-
state2.slot = null;
|
|
910
|
-
const fn = state2.onConsume;
|
|
911
|
-
state2.onConsume = null;
|
|
912
|
-
fn();
|
|
913
|
-
yield event;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
876
|
+
var DEFAULT_BATCH = 500;
|
|
916
877
|
async function snap(snapshot) {
|
|
917
878
|
try {
|
|
918
879
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -944,7 +905,7 @@ async function tombstone(stream, expectedVersion, correlation) {
|
|
|
944
905
|
throw error;
|
|
945
906
|
}
|
|
946
907
|
}
|
|
947
|
-
function
|
|
908
|
+
function is_valid(event) {
|
|
948
909
|
if (event.version < 0) return false;
|
|
949
910
|
if (!(event.created instanceof Date) || Number.isNaN(event.created.getTime()))
|
|
950
911
|
return false;
|
|
@@ -952,48 +913,70 @@ function isValid(event) {
|
|
|
952
913
|
}
|
|
953
914
|
async function scan(source, opts = {}, callback) {
|
|
954
915
|
const { drop_snapshots = false, on_progress } = opts;
|
|
955
|
-
const
|
|
916
|
+
const limit = opts.batch_size ?? DEFAULT_BATCH;
|
|
917
|
+
const id_map = /* @__PURE__ */ new Map();
|
|
956
918
|
let kept = 0;
|
|
957
|
-
let
|
|
919
|
+
let dropped_snaps = 0;
|
|
958
920
|
let processed = 0;
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
921
|
+
let at;
|
|
922
|
+
let max_id;
|
|
923
|
+
const probed = await source.query(
|
|
924
|
+
(e) => {
|
|
925
|
+
max_id = e.id;
|
|
926
|
+
},
|
|
927
|
+
{ backward: true, limit: 1 }
|
|
928
|
+
);
|
|
929
|
+
if (probed !== 1) max_id = void 0;
|
|
930
|
+
while (true) {
|
|
931
|
+
let got = 0;
|
|
932
|
+
let id;
|
|
933
|
+
await source.query(
|
|
934
|
+
async (event) => {
|
|
935
|
+
got++;
|
|
936
|
+
id = event.id;
|
|
937
|
+
processed++;
|
|
938
|
+
if (!is_valid(event))
|
|
939
|
+
throw new Error(`Invalid event at index ${processed}`);
|
|
940
|
+
if (on_progress) on_progress({ processed, id: event.id, max_id });
|
|
941
|
+
if (drop_snapshots && event.name === SNAP_EVENT) {
|
|
942
|
+
dropped_snaps++;
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (!callback) {
|
|
946
|
+
kept++;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
let remapped = event;
|
|
950
|
+
const caused_by = event.meta.causation.event?.id;
|
|
951
|
+
if (caused_by !== void 0) {
|
|
952
|
+
const new_caused_by = id_map.get(caused_by);
|
|
953
|
+
if (new_caused_by !== void 0 && new_caused_by !== caused_by) {
|
|
954
|
+
remapped = {
|
|
955
|
+
...event,
|
|
956
|
+
meta: {
|
|
957
|
+
...event.meta,
|
|
958
|
+
causation: {
|
|
959
|
+
...event.meta.causation,
|
|
960
|
+
event: { ...event.meta.causation.event, id: new_caused_by }
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
};
|
|
984
964
|
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
965
|
+
}
|
|
966
|
+
const new_id = await callback(remapped);
|
|
967
|
+
id_map.set(event.id, new_id);
|
|
968
|
+
kept++;
|
|
969
|
+
},
|
|
970
|
+
{ after: at, limit }
|
|
971
|
+
);
|
|
972
|
+
if (got !== limit) break;
|
|
973
|
+
at = id;
|
|
991
974
|
}
|
|
992
975
|
return {
|
|
993
976
|
kept,
|
|
994
977
|
dropped: {
|
|
995
978
|
closed_streams: 0,
|
|
996
|
-
snapshots:
|
|
979
|
+
snapshots: dropped_snaps,
|
|
997
980
|
empty_streams: 0
|
|
998
981
|
}
|
|
999
982
|
};
|
|
@@ -1411,9 +1394,6 @@ var EMPTY_DRAIN = {
|
|
|
1411
1394
|
blocked: []
|
|
1412
1395
|
};
|
|
1413
1396
|
var DrainController = class {
|
|
1414
|
-
constructor(deps) {
|
|
1415
|
-
this.deps = deps;
|
|
1416
|
-
}
|
|
1417
1397
|
_armed = false;
|
|
1418
1398
|
_locked = false;
|
|
1419
1399
|
_ratio = 0.5;
|
|
@@ -1429,6 +1409,10 @@ var DrainController = class {
|
|
|
1429
1409
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
1430
1410
|
_worker;
|
|
1431
1411
|
_stopped = false;
|
|
1412
|
+
_deps;
|
|
1413
|
+
constructor(deps) {
|
|
1414
|
+
this._deps = deps;
|
|
1415
|
+
}
|
|
1432
1416
|
/**
|
|
1433
1417
|
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
1434
1418
|
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
@@ -1469,7 +1453,7 @@ var DrainController = class {
|
|
|
1469
1453
|
}
|
|
1470
1454
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
1471
1455
|
get lane() {
|
|
1472
|
-
return this.
|
|
1456
|
+
return this._deps.lane;
|
|
1473
1457
|
}
|
|
1474
1458
|
/**
|
|
1475
1459
|
* Start a per-lane worker that drains at the lane's `cycleMs`
|
|
@@ -1503,7 +1487,7 @@ var DrainController = class {
|
|
|
1503
1487
|
async drain(options = {}) {
|
|
1504
1488
|
if (!this._armed) return EMPTY_DRAIN;
|
|
1505
1489
|
if (this._locked) return EMPTY_DRAIN;
|
|
1506
|
-
const d = this.
|
|
1490
|
+
const d = this._deps.defaults ?? {};
|
|
1507
1491
|
const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
|
|
1508
1492
|
const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
|
|
1509
1493
|
const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
|
|
@@ -1512,24 +1496,24 @@ var DrainController = class {
|
|
|
1512
1496
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1513
1497
|
const leading = streamLimit - lagging;
|
|
1514
1498
|
const cycle = await runDrainCycle(
|
|
1515
|
-
this.
|
|
1516
|
-
this.
|
|
1517
|
-
this.
|
|
1518
|
-
this.
|
|
1519
|
-
this.
|
|
1499
|
+
this._deps.ops,
|
|
1500
|
+
this._deps.registry,
|
|
1501
|
+
this._deps.batchHandlers,
|
|
1502
|
+
this._deps.handle,
|
|
1503
|
+
this._deps.handleBatch,
|
|
1520
1504
|
lagging,
|
|
1521
1505
|
leading,
|
|
1522
1506
|
eventLimit,
|
|
1523
1507
|
leaseMillis,
|
|
1524
1508
|
this._backoff.size > 0 ? this.isDeferred : void 0,
|
|
1525
|
-
this.
|
|
1509
|
+
this._deps.lane
|
|
1526
1510
|
);
|
|
1527
1511
|
if (!cycle) {
|
|
1528
1512
|
this._armed = false;
|
|
1529
1513
|
return EMPTY_DRAIN;
|
|
1530
1514
|
}
|
|
1531
1515
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1532
|
-
traceCycle(this.
|
|
1516
|
+
traceCycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
1533
1517
|
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
1534
1518
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1535
1519
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
@@ -1539,13 +1523,13 @@ var DrainController = class {
|
|
|
1539
1523
|
}
|
|
1540
1524
|
}
|
|
1541
1525
|
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
1542
|
-
if (acked.length) this.
|
|
1543
|
-
if (blocked.length) this.
|
|
1526
|
+
if (acked.length) this._deps.onAcked(acked);
|
|
1527
|
+
if (blocked.length) this._deps.onBlocked(blocked);
|
|
1544
1528
|
const hasErrors = handled.some(({ error }) => error);
|
|
1545
1529
|
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
1546
1530
|
return { fetched, leased, acked, blocked };
|
|
1547
1531
|
} catch (error) {
|
|
1548
|
-
this.
|
|
1532
|
+
this._deps.logger.error(error);
|
|
1549
1533
|
return EMPTY_DRAIN;
|
|
1550
1534
|
} finally {
|
|
1551
1535
|
this._locked = false;
|
|
@@ -1806,12 +1790,15 @@ function buildHandleBatch(logger) {
|
|
|
1806
1790
|
|
|
1807
1791
|
// src/internal/settle.ts
|
|
1808
1792
|
var SettleLoop = class {
|
|
1809
|
-
constructor(deps, defaultDebounceMs) {
|
|
1810
|
-
this.deps = deps;
|
|
1811
|
-
this.defaultDebounceMs = defaultDebounceMs;
|
|
1812
|
-
}
|
|
1813
1793
|
_timer = void 0;
|
|
1814
1794
|
_running = false;
|
|
1795
|
+
_deps;
|
|
1796
|
+
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
1797
|
+
_default_debounce_ms;
|
|
1798
|
+
constructor(deps, defaultDebounceMs) {
|
|
1799
|
+
this._deps = deps;
|
|
1800
|
+
this._default_debounce_ms = defaultDebounceMs;
|
|
1801
|
+
}
|
|
1815
1802
|
/**
|
|
1816
1803
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1817
1804
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
@@ -1821,7 +1808,7 @@ var SettleLoop = class {
|
|
|
1821
1808
|
*/
|
|
1822
1809
|
schedule(options = {}) {
|
|
1823
1810
|
const {
|
|
1824
|
-
debounceMs = this.
|
|
1811
|
+
debounceMs = this._default_debounce_ms,
|
|
1825
1812
|
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
1826
1813
|
maxPasses = Infinity,
|
|
1827
1814
|
...drainOptions
|
|
@@ -1832,19 +1819,19 @@ var SettleLoop = class {
|
|
|
1832
1819
|
if (this._running) return;
|
|
1833
1820
|
this._running = true;
|
|
1834
1821
|
(async () => {
|
|
1835
|
-
await this.
|
|
1822
|
+
await this._deps.init();
|
|
1836
1823
|
let lastDrain;
|
|
1837
1824
|
for (let i = 0; i < maxPasses; i++) {
|
|
1838
|
-
const { subscribed } = await this.
|
|
1825
|
+
const { subscribed } = await this._deps.correlate({
|
|
1839
1826
|
...correlateQuery,
|
|
1840
|
-
after: this.
|
|
1827
|
+
after: this._deps.checkpoint()
|
|
1841
1828
|
});
|
|
1842
|
-
lastDrain = await this.
|
|
1829
|
+
lastDrain = await this._deps.drain(drainOptions);
|
|
1843
1830
|
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
1844
1831
|
if (!made_progress) break;
|
|
1845
1832
|
}
|
|
1846
|
-
if (lastDrain) this.
|
|
1847
|
-
})().catch((err) => this.
|
|
1833
|
+
if (lastDrain) this._deps.onSettled(lastDrain);
|
|
1834
|
+
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
1848
1835
|
this._running = false;
|
|
1849
1836
|
});
|
|
1850
1837
|
}, debounceMs);
|
|
@@ -1862,6 +1849,117 @@ var SettleLoop = class {
|
|
|
1862
1849
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
1863
1850
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
1864
1851
|
var Act = class {
|
|
1852
|
+
_emitter = new EventEmitter();
|
|
1853
|
+
/** #803: gate the `Store.notify` subscription side. */
|
|
1854
|
+
_listen;
|
|
1855
|
+
/** #803: gate the local reaction pipeline (drain controllers, settle, correlate). */
|
|
1856
|
+
_drain;
|
|
1857
|
+
/** Event names with at least one registered reaction (computed at build time) */
|
|
1858
|
+
_reactive_events;
|
|
1859
|
+
/** One DrainController per active lane, keyed by lane name. */
|
|
1860
|
+
_drain_controllers;
|
|
1861
|
+
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
1862
|
+
_correlate;
|
|
1863
|
+
/** Debounced correlate→drain catch-up loop. */
|
|
1864
|
+
_settle;
|
|
1865
|
+
/**
|
|
1866
|
+
* Disposer for the cross-process notify subscription, set up eagerly
|
|
1867
|
+
* during construction. Held as a promise because the subscription
|
|
1868
|
+
* itself may be async (the PG adapter checks out a dedicated client
|
|
1869
|
+
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
1870
|
+
* the store doesn't implement `notify` or there are no registered
|
|
1871
|
+
* reactions.
|
|
1872
|
+
*
|
|
1873
|
+
* **Contract:** the configured store must be injected via
|
|
1874
|
+
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
1875
|
+
* orchestrator wires notify against whatever store is current at
|
|
1876
|
+
* construction time — late injection after build is unsupported.
|
|
1877
|
+
*/
|
|
1878
|
+
_notify_disposer;
|
|
1879
|
+
/** Public registry — kept as-is per the no-prefix-on-public convention. */
|
|
1880
|
+
registry;
|
|
1881
|
+
/** Map of state name → state definition; populated by the builder. */
|
|
1882
|
+
_states;
|
|
1883
|
+
/**
|
|
1884
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1885
|
+
* via {@link ActLifecycleEvents}.
|
|
1886
|
+
*/
|
|
1887
|
+
emit(event, args) {
|
|
1888
|
+
return this._emitter.emit(event, args);
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
1892
|
+
* event-specific payload.
|
|
1893
|
+
*/
|
|
1894
|
+
on(event, listener) {
|
|
1895
|
+
this._emitter.on(event, listener);
|
|
1896
|
+
return this;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Remove a previously registered lifecycle listener.
|
|
1900
|
+
*/
|
|
1901
|
+
off(event, listener) {
|
|
1902
|
+
this._emitter.off(event, listener);
|
|
1903
|
+
return this;
|
|
1904
|
+
}
|
|
1905
|
+
/** Batch handlers for static-target projections (target → handler) */
|
|
1906
|
+
_batch_handlers;
|
|
1907
|
+
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
1908
|
+
_es;
|
|
1909
|
+
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
1910
|
+
_cd;
|
|
1911
|
+
/**
|
|
1912
|
+
* Event-name → owning state, computed at build time. The duplicate-event
|
|
1913
|
+
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
1914
|
+
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
1915
|
+
* set when seeding a `restart` snapshot in multi-state apps.
|
|
1916
|
+
*/
|
|
1917
|
+
_event_to_state;
|
|
1918
|
+
/**
|
|
1919
|
+
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
1920
|
+
* `classifyRegistry` once per build. `"all"` means at least one of
|
|
1921
|
+
* the event's reactions is a dynamic resolver (lane opaque until
|
|
1922
|
+
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
1923
|
+
* reactions target.
|
|
1924
|
+
*/
|
|
1925
|
+
_event_to_lanes;
|
|
1926
|
+
/**
|
|
1927
|
+
* Audit dependency bag (#723). Built once at construction; held as
|
|
1928
|
+
* an immutable snapshot of the registry state the audit module
|
|
1929
|
+
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
1930
|
+
* carries audit logic, only the deps + a one-liner that hands them
|
|
1931
|
+
* over.
|
|
1932
|
+
*/
|
|
1933
|
+
_audit_deps;
|
|
1934
|
+
/** Logger resolved at construction time (after user port configuration) */
|
|
1935
|
+
_logger = log();
|
|
1936
|
+
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
1937
|
+
* per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
|
|
1938
|
+
* path keeps reading fresh `store()`/`cache()` per call, which matters for
|
|
1939
|
+
* tests that dispose and re-seed mid-suite. */
|
|
1940
|
+
_scoped;
|
|
1941
|
+
/**
|
|
1942
|
+
* Correlation-id generator for originating actions. Bound at
|
|
1943
|
+
* construction from `options.correlator ?? defaultCorrelator`. The
|
|
1944
|
+
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
1945
|
+
* uses it via {@link closeCorrelation}.
|
|
1946
|
+
*/
|
|
1947
|
+
_correlator;
|
|
1948
|
+
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
1949
|
+
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
1950
|
+
_bound_do = this.do.bind(this);
|
|
1951
|
+
_bound_load = this.load.bind(this);
|
|
1952
|
+
_bound_query = this.query.bind(this);
|
|
1953
|
+
_bound_query_array = this.query_array.bind(this);
|
|
1954
|
+
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
1955
|
+
_handle;
|
|
1956
|
+
_handle_batch;
|
|
1957
|
+
/** Declared drain lanes (ACT-1103). */
|
|
1958
|
+
_lanes;
|
|
1959
|
+
/** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
|
|
1960
|
+
get lanes() {
|
|
1961
|
+
return this._lanes;
|
|
1962
|
+
}
|
|
1865
1963
|
/**
|
|
1866
1964
|
* Create a new Act orchestrator. Prefer the {@link act} builder over
|
|
1867
1965
|
* direct construction — `act()...build()` wires the registry, merges
|
|
@@ -1876,9 +1974,9 @@ var Act = class {
|
|
|
1876
1974
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
1877
1975
|
* instance; later slices fan out one `DrainController` per lane.
|
|
1878
1976
|
*/
|
|
1879
|
-
constructor(registry,
|
|
1977
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
1880
1978
|
this.registry = registry;
|
|
1881
|
-
this._states =
|
|
1979
|
+
this._states = states;
|
|
1882
1980
|
this._batch_handlers = batchHandlers;
|
|
1883
1981
|
this._lanes = lanes;
|
|
1884
1982
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
@@ -1912,6 +2010,8 @@ var Act = class {
|
|
|
1912
2010
|
eventToLanes
|
|
1913
2011
|
} = classifyRegistry(this.registry, this._states);
|
|
1914
2012
|
this._reactive_events = reactiveEvents;
|
|
2013
|
+
this._listen = options.listen !== false;
|
|
2014
|
+
this._drain = options.drain !== false;
|
|
1915
2015
|
this._event_to_state = eventToState;
|
|
1916
2016
|
this._event_to_lanes = eventToLanes;
|
|
1917
2017
|
const allLanes = ["default", ...lanes.map((l) => l.name)];
|
|
@@ -1939,7 +2039,8 @@ var Act = class {
|
|
|
1939
2039
|
leaseMillis: cfg.leaseMillis
|
|
1940
2040
|
}
|
|
1941
2041
|
});
|
|
1942
|
-
if (cfg?.cycleMs !== void 0
|
|
2042
|
+
if (cfg?.cycleMs !== void 0 && options.drain !== false)
|
|
2043
|
+
controller.start(cfg.cycleMs);
|
|
1943
2044
|
this._drain_controllers.set(name, controller);
|
|
1944
2045
|
}
|
|
1945
2046
|
this._audit_deps = {
|
|
@@ -1957,9 +2058,10 @@ var Act = class {
|
|
|
1957
2058
|
hasDynamicResolvers,
|
|
1958
2059
|
this._cd,
|
|
1959
2060
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
1960
|
-
// Cold start: assume drain is needed (historical events may need processing)
|
|
2061
|
+
// Cold start: assume drain is needed (historical events may need processing).
|
|
2062
|
+
// #803: writer-only instances skip the cold-start arm.
|
|
1961
2063
|
() => {
|
|
1962
|
-
if (this._reactive_events.size > 0) this._armAll();
|
|
2064
|
+
if (this._drain && this._reactive_events.size > 0) this._armAll();
|
|
1963
2065
|
}
|
|
1964
2066
|
);
|
|
1965
2067
|
this._settle = new SettleLoop(
|
|
@@ -1976,109 +2078,6 @@ var Act = class {
|
|
|
1976
2078
|
this._notify_disposer = this._wireNotify(options.scoped?.store ?? store());
|
|
1977
2079
|
dispose(() => this.shutdown());
|
|
1978
2080
|
}
|
|
1979
|
-
_emitter = new EventEmitter();
|
|
1980
|
-
/** Event names with at least one registered reaction (computed at build time) */
|
|
1981
|
-
_reactive_events;
|
|
1982
|
-
/** One DrainController per active lane, keyed by lane name. */
|
|
1983
|
-
_drain_controllers;
|
|
1984
|
-
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
1985
|
-
_correlate;
|
|
1986
|
-
/** Debounced correlate→drain catch-up loop. */
|
|
1987
|
-
_settle;
|
|
1988
|
-
/**
|
|
1989
|
-
* Disposer for the cross-process notify subscription, set up eagerly
|
|
1990
|
-
* during construction. Held as a promise because the subscription
|
|
1991
|
-
* itself may be async (the PG adapter checks out a dedicated client
|
|
1992
|
-
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
1993
|
-
* the store doesn't implement `notify` or there are no registered
|
|
1994
|
-
* reactions.
|
|
1995
|
-
*
|
|
1996
|
-
* **Contract:** the configured store must be injected via
|
|
1997
|
-
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
1998
|
-
* orchestrator wires notify against whatever store is current at
|
|
1999
|
-
* construction time — late injection after build is unsupported.
|
|
2000
|
-
*/
|
|
2001
|
-
_notify_disposer;
|
|
2002
|
-
/**
|
|
2003
|
-
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
2004
|
-
* via {@link ActLifecycleEvents}.
|
|
2005
|
-
*/
|
|
2006
|
-
emit(event, args) {
|
|
2007
|
-
return this._emitter.emit(event, args);
|
|
2008
|
-
}
|
|
2009
|
-
/**
|
|
2010
|
-
* Register a listener for a lifecycle event. The listener receives the
|
|
2011
|
-
* event-specific payload.
|
|
2012
|
-
*/
|
|
2013
|
-
on(event, listener) {
|
|
2014
|
-
this._emitter.on(event, listener);
|
|
2015
|
-
return this;
|
|
2016
|
-
}
|
|
2017
|
-
/**
|
|
2018
|
-
* Remove a previously registered lifecycle listener.
|
|
2019
|
-
*/
|
|
2020
|
-
off(event, listener) {
|
|
2021
|
-
this._emitter.off(event, listener);
|
|
2022
|
-
return this;
|
|
2023
|
-
}
|
|
2024
|
-
/** Batch handlers for static-target projections (target → handler) */
|
|
2025
|
-
_batch_handlers;
|
|
2026
|
-
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
2027
|
-
_es;
|
|
2028
|
-
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
2029
|
-
_cd;
|
|
2030
|
-
/**
|
|
2031
|
-
* Event-name → owning state, computed at build time. The duplicate-event
|
|
2032
|
-
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
2033
|
-
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
2034
|
-
* set when seeding a `restart` snapshot in multi-state apps.
|
|
2035
|
-
*/
|
|
2036
|
-
_event_to_state;
|
|
2037
|
-
/**
|
|
2038
|
-
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
2039
|
-
* `classifyRegistry` once per build. `"all"` means at least one of
|
|
2040
|
-
* the event's reactions is a dynamic resolver (lane opaque until
|
|
2041
|
-
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
2042
|
-
* reactions target.
|
|
2043
|
-
*/
|
|
2044
|
-
_event_to_lanes;
|
|
2045
|
-
/**
|
|
2046
|
-
* Audit dependency bag (#723). Built once at construction; held as
|
|
2047
|
-
* an immutable snapshot of the registry state the audit module
|
|
2048
|
-
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
2049
|
-
* carries audit logic, only the deps + a one-liner that hands them
|
|
2050
|
-
* over.
|
|
2051
|
-
*/
|
|
2052
|
-
_audit_deps;
|
|
2053
|
-
/** Logger resolved at construction time (after user port configuration) */
|
|
2054
|
-
_logger = log();
|
|
2055
|
-
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
2056
|
-
* per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
|
|
2057
|
-
* path keeps reading fresh `store()`/`cache()` per call, which matters for
|
|
2058
|
-
* tests that dispose and re-seed mid-suite. */
|
|
2059
|
-
_scoped;
|
|
2060
|
-
/**
|
|
2061
|
-
* Correlation-id generator for originating actions. Bound at
|
|
2062
|
-
* construction from `options.correlator ?? defaultCorrelator`. The
|
|
2063
|
-
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
2064
|
-
* uses it via {@link closeCorrelation}.
|
|
2065
|
-
*/
|
|
2066
|
-
_correlator;
|
|
2067
|
-
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
2068
|
-
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
2069
|
-
_bound_do = this.do.bind(this);
|
|
2070
|
-
_bound_load = this.load.bind(this);
|
|
2071
|
-
_bound_query = this.query.bind(this);
|
|
2072
|
-
_bound_query_array = this.query_array.bind(this);
|
|
2073
|
-
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2074
|
-
_handle;
|
|
2075
|
-
_handle_batch;
|
|
2076
|
-
/** Declared drain lanes (ACT-1103). */
|
|
2077
|
-
_lanes;
|
|
2078
|
-
/** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
|
|
2079
|
-
get lanes() {
|
|
2080
|
-
return this._lanes;
|
|
2081
|
-
}
|
|
2082
2081
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
2083
2082
|
_shutdown_promise;
|
|
2084
2083
|
/**
|
|
@@ -2113,14 +2112,17 @@ var Act = class {
|
|
|
2113
2112
|
async _wireNotify(s) {
|
|
2114
2113
|
if (this._reactive_events.size === 0) return void 0;
|
|
2115
2114
|
if (!s.notify) return void 0;
|
|
2115
|
+
if (!this._listen) return void 0;
|
|
2116
2116
|
try {
|
|
2117
2117
|
return await s.notify((notification) => {
|
|
2118
2118
|
try {
|
|
2119
2119
|
this.emit("notified", notification);
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2120
|
+
if (this._drain) {
|
|
2121
|
+
const armed = this._armForEventNames(
|
|
2122
|
+
notification.events.map((e) => e.name)
|
|
2123
|
+
);
|
|
2124
|
+
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
2125
|
+
}
|
|
2124
2126
|
} catch (err) {
|
|
2125
2127
|
this._logger.error(err, "notified handler threw");
|
|
2126
2128
|
}
|
|
@@ -2371,6 +2373,8 @@ var Act = class {
|
|
|
2371
2373
|
* @see {@link start_correlations} for automatic correlation
|
|
2372
2374
|
*/
|
|
2373
2375
|
async drain(options = {}) {
|
|
2376
|
+
if (!this._drain)
|
|
2377
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2374
2378
|
return this._scoped(() => this._drainAll(options));
|
|
2375
2379
|
}
|
|
2376
2380
|
/** Arm every active lane controller (ACT-1103). */
|
|
@@ -2471,6 +2475,7 @@ var Act = class {
|
|
|
2471
2475
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
2472
2476
|
*/
|
|
2473
2477
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
2478
|
+
if (!this._drain) return { subscribed: 0, last_id: -1 };
|
|
2474
2479
|
return this._scoped(() => this._correlate.correlate(query));
|
|
2475
2480
|
}
|
|
2476
2481
|
/**
|
|
@@ -2896,6 +2901,7 @@ var Act = class {
|
|
|
2896
2901
|
* @see {@link correlate} for manual correlation
|
|
2897
2902
|
*/
|
|
2898
2903
|
settle(options = {}) {
|
|
2904
|
+
if (!this._drain) return;
|
|
2899
2905
|
this._settle.schedule(options);
|
|
2900
2906
|
}
|
|
2901
2907
|
};
|