@rotorsoft/act 1.4.0 → 1.5.1
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 +29 -4
- 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/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/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 +224 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +183 -155
- 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 +1 -1
- package/dist/chunk-TZWDSNSN.js.map +0 -1
- package/dist/chunk-XKCTGUW2.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -204,10 +204,11 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
204
204
|
|
|
205
205
|
// src/internal/lru-map.ts
|
|
206
206
|
var LruMap = class {
|
|
207
|
-
constructor(_maxSize) {
|
|
208
|
-
this._maxSize = _maxSize;
|
|
209
|
-
}
|
|
210
207
|
_entries = /* @__PURE__ */ new Map();
|
|
208
|
+
_max_size;
|
|
209
|
+
constructor(maxSize) {
|
|
210
|
+
this._max_size = maxSize;
|
|
211
|
+
}
|
|
211
212
|
get(key) {
|
|
212
213
|
const v = this._entries.get(key);
|
|
213
214
|
if (v === void 0) return void 0;
|
|
@@ -220,7 +221,7 @@ var LruMap = class {
|
|
|
220
221
|
}
|
|
221
222
|
set(key, value) {
|
|
222
223
|
this._entries.delete(key);
|
|
223
|
-
if (this._entries.size >= this.
|
|
224
|
+
if (this._entries.size >= this._max_size) {
|
|
224
225
|
const oldest = this._entries.keys().next().value;
|
|
225
226
|
this._entries.delete(oldest);
|
|
226
227
|
}
|
|
@@ -302,44 +303,70 @@ var Errors = {
|
|
|
302
303
|
NonRetryableError: "ERR_NON_RETRYABLE"
|
|
303
304
|
};
|
|
304
305
|
var ValidationError = class extends Error {
|
|
306
|
+
/** The type of target being validated (e.g., "action", "event") */
|
|
307
|
+
target;
|
|
308
|
+
/** The invalid payload that failed validation */
|
|
309
|
+
payload;
|
|
310
|
+
/** Zod validation error details */
|
|
311
|
+
details;
|
|
305
312
|
constructor(target, payload, details) {
|
|
306
313
|
super(`Invalid ${target} payload`);
|
|
314
|
+
this.name = Errors.ValidationError;
|
|
307
315
|
this.target = target;
|
|
308
316
|
this.payload = payload;
|
|
309
317
|
this.details = details;
|
|
310
|
-
this.name = Errors.ValidationError;
|
|
311
318
|
}
|
|
312
319
|
};
|
|
313
320
|
var InvariantError = class extends Error {
|
|
321
|
+
/** The action that was attempted */
|
|
322
|
+
action;
|
|
323
|
+
/** The action payload that was provided */
|
|
324
|
+
payload;
|
|
325
|
+
/** The target stream and actor context */
|
|
326
|
+
target;
|
|
327
|
+
/** The current state snapshot when invariant was checked */
|
|
328
|
+
snapshot;
|
|
329
|
+
/** Human-readable description of why the invariant failed */
|
|
330
|
+
description;
|
|
314
331
|
constructor(action2, payload, target, snapshot, description) {
|
|
315
332
|
super(`${action2} failed invariant: ${description}`);
|
|
333
|
+
this.name = Errors.InvariantError;
|
|
316
334
|
this.action = action2;
|
|
317
335
|
this.payload = payload;
|
|
318
336
|
this.target = target;
|
|
319
337
|
this.snapshot = snapshot;
|
|
320
338
|
this.description = description;
|
|
321
|
-
this.name = Errors.InvariantError;
|
|
322
339
|
}
|
|
323
340
|
};
|
|
324
341
|
var ConcurrencyError = class extends Error {
|
|
342
|
+
/** The stream that had the concurrent modification */
|
|
343
|
+
stream;
|
|
344
|
+
/** The actual current version in the store */
|
|
345
|
+
lastVersion;
|
|
346
|
+
/** The events that were being committed */
|
|
347
|
+
events;
|
|
348
|
+
/** The version number that was expected */
|
|
349
|
+
expectedVersion;
|
|
325
350
|
constructor(stream, lastVersion, events, expectedVersion) {
|
|
326
351
|
super(
|
|
327
352
|
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}`).join(
|
|
328
353
|
", "
|
|
329
354
|
)}". Expected version ${expectedVersion} but found version ${lastVersion}.`
|
|
330
355
|
);
|
|
356
|
+
this.name = Errors.ConcurrencyError;
|
|
331
357
|
this.stream = stream;
|
|
332
358
|
this.lastVersion = lastVersion;
|
|
333
359
|
this.events = events;
|
|
334
360
|
this.expectedVersion = expectedVersion;
|
|
335
|
-
this.name = Errors.ConcurrencyError;
|
|
336
361
|
}
|
|
337
362
|
};
|
|
338
363
|
var StreamClosedError = class extends Error {
|
|
364
|
+
/** The stream that is closed */
|
|
365
|
+
stream;
|
|
339
366
|
constructor(stream) {
|
|
340
367
|
super(`Stream "${stream}" is closed (tombstoned)`);
|
|
341
|
-
this.stream = stream;
|
|
342
368
|
this.name = Errors.StreamClosedError;
|
|
369
|
+
this.stream = stream;
|
|
343
370
|
}
|
|
344
371
|
};
|
|
345
372
|
var NonRetryableError = class extends Error {
|
|
@@ -495,12 +522,8 @@ async function sleep(ms) {
|
|
|
495
522
|
|
|
496
523
|
// src/adapters/in-memory-store.ts
|
|
497
524
|
var InMemoryStream = class {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this.source = source;
|
|
501
|
-
this._priority = priority;
|
|
502
|
-
this._lane = lane;
|
|
503
|
-
}
|
|
525
|
+
stream;
|
|
526
|
+
source;
|
|
504
527
|
_at = -1;
|
|
505
528
|
_retry = -1;
|
|
506
529
|
_blocked = false;
|
|
@@ -509,6 +532,12 @@ var InMemoryStream = class {
|
|
|
509
532
|
_leased_until = void 0;
|
|
510
533
|
_priority = 0;
|
|
511
534
|
_lane = DEFAULT_LANE;
|
|
535
|
+
constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
|
|
536
|
+
this.stream = stream;
|
|
537
|
+
this.source = source;
|
|
538
|
+
this._priority = priority;
|
|
539
|
+
this._lane = lane;
|
|
540
|
+
}
|
|
512
541
|
get priority() {
|
|
513
542
|
return this._priority;
|
|
514
543
|
}
|
|
@@ -1902,18 +1931,23 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
1902
1931
|
|
|
1903
1932
|
// src/internal/correlate-cycle.ts
|
|
1904
1933
|
var CorrelateCycle = class {
|
|
1905
|
-
constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
|
|
1906
|
-
this.registry = registry;
|
|
1907
|
-
this.staticTargets = staticTargets;
|
|
1908
|
-
this.hasDynamicResolvers = hasDynamicResolvers;
|
|
1909
|
-
this.cd = cd;
|
|
1910
|
-
this.onInit = onInit;
|
|
1911
|
-
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
1912
|
-
}
|
|
1913
1934
|
_checkpoint = -1;
|
|
1914
1935
|
_initialized = false;
|
|
1915
1936
|
_timer = void 0;
|
|
1916
1937
|
_subscribed;
|
|
1938
|
+
_registry;
|
|
1939
|
+
_static_targets;
|
|
1940
|
+
_has_dynamic_resolvers;
|
|
1941
|
+
_cd;
|
|
1942
|
+
_on_init;
|
|
1943
|
+
constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
|
|
1944
|
+
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
1945
|
+
this._registry = registry;
|
|
1946
|
+
this._static_targets = staticTargets;
|
|
1947
|
+
this._has_dynamic_resolvers = hasDynamicResolvers;
|
|
1948
|
+
this._cd = cd;
|
|
1949
|
+
this._on_init = onInit;
|
|
1950
|
+
}
|
|
1917
1951
|
/** Last correlated event id. */
|
|
1918
1952
|
get checkpoint() {
|
|
1919
1953
|
return this._checkpoint;
|
|
@@ -1928,10 +1962,10 @@ var CorrelateCycle = class {
|
|
|
1928
1962
|
async init() {
|
|
1929
1963
|
if (this._initialized) return;
|
|
1930
1964
|
this._initialized = true;
|
|
1931
|
-
const { watermark } = await store2().subscribe([...this.
|
|
1965
|
+
const { watermark } = await store2().subscribe([...this._static_targets]);
|
|
1932
1966
|
this._checkpoint = watermark;
|
|
1933
|
-
this.
|
|
1934
|
-
for (const { stream } of this.
|
|
1967
|
+
this._on_init?.();
|
|
1968
|
+
for (const { stream } of this._static_targets) {
|
|
1935
1969
|
this._subscribed.add(stream);
|
|
1936
1970
|
}
|
|
1937
1971
|
}
|
|
@@ -1942,7 +1976,7 @@ var CorrelateCycle = class {
|
|
|
1942
1976
|
*/
|
|
1943
1977
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
1944
1978
|
await this.init();
|
|
1945
|
-
if (!this.
|
|
1979
|
+
if (!this._has_dynamic_resolvers)
|
|
1946
1980
|
return { subscribed: 0, last_id: this._checkpoint };
|
|
1947
1981
|
const after = Math.max(this._checkpoint, query.after || -1);
|
|
1948
1982
|
const correlated = /* @__PURE__ */ new Map();
|
|
@@ -1950,7 +1984,7 @@ var CorrelateCycle = class {
|
|
|
1950
1984
|
await store2().query(
|
|
1951
1985
|
(event) => {
|
|
1952
1986
|
last_id = event.id;
|
|
1953
|
-
const register = this.
|
|
1987
|
+
const register = this._registry.events[event.name];
|
|
1954
1988
|
if (register) {
|
|
1955
1989
|
for (const reaction of register.reactions.values()) {
|
|
1956
1990
|
if (typeof reaction.resolver !== "function") continue;
|
|
@@ -1986,7 +2020,7 @@ var CorrelateCycle = class {
|
|
|
1986
2020
|
lane
|
|
1987
2021
|
})
|
|
1988
2022
|
);
|
|
1989
|
-
const { subscribed } = await this.
|
|
2023
|
+
const { subscribed } = await this._cd.subscribe(streams);
|
|
1990
2024
|
this._checkpoint = last_id;
|
|
1991
2025
|
if (subscribed) {
|
|
1992
2026
|
for (const { stream } of streams) {
|
|
@@ -2610,9 +2644,6 @@ var EMPTY_DRAIN = {
|
|
|
2610
2644
|
blocked: []
|
|
2611
2645
|
};
|
|
2612
2646
|
var DrainController = class {
|
|
2613
|
-
constructor(deps) {
|
|
2614
|
-
this.deps = deps;
|
|
2615
|
-
}
|
|
2616
2647
|
_armed = false;
|
|
2617
2648
|
_locked = false;
|
|
2618
2649
|
_ratio = 0.5;
|
|
@@ -2628,6 +2659,10 @@ var DrainController = class {
|
|
|
2628
2659
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
2629
2660
|
_worker;
|
|
2630
2661
|
_stopped = false;
|
|
2662
|
+
_deps;
|
|
2663
|
+
constructor(deps) {
|
|
2664
|
+
this._deps = deps;
|
|
2665
|
+
}
|
|
2631
2666
|
/**
|
|
2632
2667
|
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
2633
2668
|
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
@@ -2668,7 +2703,7 @@ var DrainController = class {
|
|
|
2668
2703
|
}
|
|
2669
2704
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
2670
2705
|
get lane() {
|
|
2671
|
-
return this.
|
|
2706
|
+
return this._deps.lane;
|
|
2672
2707
|
}
|
|
2673
2708
|
/**
|
|
2674
2709
|
* Start a per-lane worker that drains at the lane's `cycleMs`
|
|
@@ -2702,7 +2737,7 @@ var DrainController = class {
|
|
|
2702
2737
|
async drain(options = {}) {
|
|
2703
2738
|
if (!this._armed) return EMPTY_DRAIN;
|
|
2704
2739
|
if (this._locked) return EMPTY_DRAIN;
|
|
2705
|
-
const d = this.
|
|
2740
|
+
const d = this._deps.defaults ?? {};
|
|
2706
2741
|
const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
|
|
2707
2742
|
const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
|
|
2708
2743
|
const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
|
|
@@ -2711,24 +2746,24 @@ var DrainController = class {
|
|
|
2711
2746
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2712
2747
|
const leading = streamLimit - lagging;
|
|
2713
2748
|
const cycle = await runDrainCycle(
|
|
2714
|
-
this.
|
|
2715
|
-
this.
|
|
2716
|
-
this.
|
|
2717
|
-
this.
|
|
2718
|
-
this.
|
|
2749
|
+
this._deps.ops,
|
|
2750
|
+
this._deps.registry,
|
|
2751
|
+
this._deps.batchHandlers,
|
|
2752
|
+
this._deps.handle,
|
|
2753
|
+
this._deps.handleBatch,
|
|
2719
2754
|
lagging,
|
|
2720
2755
|
leading,
|
|
2721
2756
|
eventLimit,
|
|
2722
2757
|
leaseMillis,
|
|
2723
2758
|
this._backoff.size > 0 ? this.isDeferred : void 0,
|
|
2724
|
-
this.
|
|
2759
|
+
this._deps.lane
|
|
2725
2760
|
);
|
|
2726
2761
|
if (!cycle) {
|
|
2727
2762
|
this._armed = false;
|
|
2728
2763
|
return EMPTY_DRAIN;
|
|
2729
2764
|
}
|
|
2730
2765
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2731
|
-
traceCycle(this.
|
|
2766
|
+
traceCycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
2732
2767
|
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
2733
2768
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
2734
2769
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
@@ -2738,13 +2773,13 @@ var DrainController = class {
|
|
|
2738
2773
|
}
|
|
2739
2774
|
}
|
|
2740
2775
|
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
2741
|
-
if (acked.length) this.
|
|
2742
|
-
if (blocked.length) this.
|
|
2776
|
+
if (acked.length) this._deps.onAcked(acked);
|
|
2777
|
+
if (blocked.length) this._deps.onBlocked(blocked);
|
|
2743
2778
|
const hasErrors = handled.some(({ error }) => error);
|
|
2744
2779
|
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
2745
2780
|
return { fetched, leased, acked, blocked };
|
|
2746
2781
|
} catch (error) {
|
|
2747
|
-
this.
|
|
2782
|
+
this._deps.logger.error(error);
|
|
2748
2783
|
return EMPTY_DRAIN;
|
|
2749
2784
|
} finally {
|
|
2750
2785
|
this._locked = false;
|
|
@@ -3005,12 +3040,15 @@ function buildHandleBatch(logger) {
|
|
|
3005
3040
|
|
|
3006
3041
|
// src/internal/settle.ts
|
|
3007
3042
|
var SettleLoop = class {
|
|
3008
|
-
constructor(deps, defaultDebounceMs) {
|
|
3009
|
-
this.deps = deps;
|
|
3010
|
-
this.defaultDebounceMs = defaultDebounceMs;
|
|
3011
|
-
}
|
|
3012
3043
|
_timer = void 0;
|
|
3013
3044
|
_running = false;
|
|
3045
|
+
_deps;
|
|
3046
|
+
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
3047
|
+
_default_debounce_ms;
|
|
3048
|
+
constructor(deps, defaultDebounceMs) {
|
|
3049
|
+
this._deps = deps;
|
|
3050
|
+
this._default_debounce_ms = defaultDebounceMs;
|
|
3051
|
+
}
|
|
3014
3052
|
/**
|
|
3015
3053
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
3016
3054
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
@@ -3020,7 +3058,7 @@ var SettleLoop = class {
|
|
|
3020
3058
|
*/
|
|
3021
3059
|
schedule(options = {}) {
|
|
3022
3060
|
const {
|
|
3023
|
-
debounceMs = this.
|
|
3061
|
+
debounceMs = this._default_debounce_ms,
|
|
3024
3062
|
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
3025
3063
|
maxPasses = Infinity,
|
|
3026
3064
|
...drainOptions
|
|
@@ -3031,19 +3069,19 @@ var SettleLoop = class {
|
|
|
3031
3069
|
if (this._running) return;
|
|
3032
3070
|
this._running = true;
|
|
3033
3071
|
(async () => {
|
|
3034
|
-
await this.
|
|
3072
|
+
await this._deps.init();
|
|
3035
3073
|
let lastDrain;
|
|
3036
3074
|
for (let i = 0; i < maxPasses; i++) {
|
|
3037
|
-
const { subscribed } = await this.
|
|
3075
|
+
const { subscribed } = await this._deps.correlate({
|
|
3038
3076
|
...correlateQuery,
|
|
3039
|
-
after: this.
|
|
3077
|
+
after: this._deps.checkpoint()
|
|
3040
3078
|
});
|
|
3041
|
-
lastDrain = await this.
|
|
3079
|
+
lastDrain = await this._deps.drain(drainOptions);
|
|
3042
3080
|
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
3043
3081
|
if (!made_progress) break;
|
|
3044
3082
|
}
|
|
3045
|
-
if (lastDrain) this.
|
|
3046
|
-
})().catch((err) => this.
|
|
3083
|
+
if (lastDrain) this._deps.onSettled(lastDrain);
|
|
3084
|
+
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
3047
3085
|
this._running = false;
|
|
3048
3086
|
});
|
|
3049
3087
|
}, debounceMs);
|
|
@@ -3061,6 +3099,117 @@ var SettleLoop = class {
|
|
|
3061
3099
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
3062
3100
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
3063
3101
|
var Act = class {
|
|
3102
|
+
_emitter = new import_node_events.default();
|
|
3103
|
+
/** #803: gate the `Store.notify` subscription side. */
|
|
3104
|
+
_listen;
|
|
3105
|
+
/** #803: gate the local reaction pipeline (drain controllers, settle, correlate). */
|
|
3106
|
+
_drain;
|
|
3107
|
+
/** Event names with at least one registered reaction (computed at build time) */
|
|
3108
|
+
_reactive_events;
|
|
3109
|
+
/** One DrainController per active lane, keyed by lane name. */
|
|
3110
|
+
_drain_controllers;
|
|
3111
|
+
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
3112
|
+
_correlate;
|
|
3113
|
+
/** Debounced correlate→drain catch-up loop. */
|
|
3114
|
+
_settle;
|
|
3115
|
+
/**
|
|
3116
|
+
* Disposer for the cross-process notify subscription, set up eagerly
|
|
3117
|
+
* during construction. Held as a promise because the subscription
|
|
3118
|
+
* itself may be async (the PG adapter checks out a dedicated client
|
|
3119
|
+
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
3120
|
+
* the store doesn't implement `notify` or there are no registered
|
|
3121
|
+
* reactions.
|
|
3122
|
+
*
|
|
3123
|
+
* **Contract:** the configured store must be injected via
|
|
3124
|
+
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
3125
|
+
* orchestrator wires notify against whatever store is current at
|
|
3126
|
+
* construction time — late injection after build is unsupported.
|
|
3127
|
+
*/
|
|
3128
|
+
_notify_disposer;
|
|
3129
|
+
/** Public registry — kept as-is per the no-prefix-on-public convention. */
|
|
3130
|
+
registry;
|
|
3131
|
+
/** Map of state name → state definition; populated by the builder. */
|
|
3132
|
+
_states;
|
|
3133
|
+
/**
|
|
3134
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
3135
|
+
* via {@link ActLifecycleEvents}.
|
|
3136
|
+
*/
|
|
3137
|
+
emit(event, args) {
|
|
3138
|
+
return this._emitter.emit(event, args);
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
3142
|
+
* event-specific payload.
|
|
3143
|
+
*/
|
|
3144
|
+
on(event, listener) {
|
|
3145
|
+
this._emitter.on(event, listener);
|
|
3146
|
+
return this;
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Remove a previously registered lifecycle listener.
|
|
3150
|
+
*/
|
|
3151
|
+
off(event, listener) {
|
|
3152
|
+
this._emitter.off(event, listener);
|
|
3153
|
+
return this;
|
|
3154
|
+
}
|
|
3155
|
+
/** Batch handlers for static-target projections (target → handler) */
|
|
3156
|
+
_batch_handlers;
|
|
3157
|
+
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
3158
|
+
_es;
|
|
3159
|
+
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
3160
|
+
_cd;
|
|
3161
|
+
/**
|
|
3162
|
+
* Event-name → owning state, computed at build time. The duplicate-event
|
|
3163
|
+
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
3164
|
+
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
3165
|
+
* set when seeding a `restart` snapshot in multi-state apps.
|
|
3166
|
+
*/
|
|
3167
|
+
_event_to_state;
|
|
3168
|
+
/**
|
|
3169
|
+
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
3170
|
+
* `classifyRegistry` once per build. `"all"` means at least one of
|
|
3171
|
+
* the event's reactions is a dynamic resolver (lane opaque until
|
|
3172
|
+
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
3173
|
+
* reactions target.
|
|
3174
|
+
*/
|
|
3175
|
+
_event_to_lanes;
|
|
3176
|
+
/**
|
|
3177
|
+
* Audit dependency bag (#723). Built once at construction; held as
|
|
3178
|
+
* an immutable snapshot of the registry state the audit module
|
|
3179
|
+
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
3180
|
+
* carries audit logic, only the deps + a one-liner that hands them
|
|
3181
|
+
* over.
|
|
3182
|
+
*/
|
|
3183
|
+
_audit_deps;
|
|
3184
|
+
/** Logger resolved at construction time (after user port configuration) */
|
|
3185
|
+
_logger = log();
|
|
3186
|
+
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
3187
|
+
* per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
|
|
3188
|
+
* path keeps reading fresh `store()`/`cache()` per call, which matters for
|
|
3189
|
+
* tests that dispose and re-seed mid-suite. */
|
|
3190
|
+
_scoped;
|
|
3191
|
+
/**
|
|
3192
|
+
* Correlation-id generator for originating actions. Bound at
|
|
3193
|
+
* construction from `options.correlator ?? defaultCorrelator`. The
|
|
3194
|
+
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
3195
|
+
* uses it via {@link closeCorrelation}.
|
|
3196
|
+
*/
|
|
3197
|
+
_correlator;
|
|
3198
|
+
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
3199
|
+
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
3200
|
+
_bound_do = this.do.bind(this);
|
|
3201
|
+
_bound_load = this.load.bind(this);
|
|
3202
|
+
_bound_query = this.query.bind(this);
|
|
3203
|
+
_bound_query_array = this.query_array.bind(this);
|
|
3204
|
+
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
3205
|
+
_handle;
|
|
3206
|
+
_handle_batch;
|
|
3207
|
+
/** Declared drain lanes (ACT-1103). */
|
|
3208
|
+
_lanes;
|
|
3209
|
+
/** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
|
|
3210
|
+
get lanes() {
|
|
3211
|
+
return this._lanes;
|
|
3212
|
+
}
|
|
3064
3213
|
/**
|
|
3065
3214
|
* Create a new Act orchestrator. Prefer the {@link act} builder over
|
|
3066
3215
|
* direct construction — `act()...build()` wires the registry, merges
|
|
@@ -3068,16 +3217,16 @@ var Act = class {
|
|
|
3068
3217
|
* and projections in one pass.
|
|
3069
3218
|
*
|
|
3070
3219
|
* @param registry Schemas for every event and action across registered states
|
|
3071
|
-
* @param
|
|
3220
|
+
* @param states Merged map of state name → state definition
|
|
3072
3221
|
* @param batchHandlers Static-target projection batch handlers (target → handler)
|
|
3073
3222
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
3074
3223
|
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
3075
3224
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
3076
3225
|
* instance; later slices fan out one `DrainController` per lane.
|
|
3077
3226
|
*/
|
|
3078
|
-
constructor(registry,
|
|
3227
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
3079
3228
|
this.registry = registry;
|
|
3080
|
-
this._states =
|
|
3229
|
+
this._states = states;
|
|
3081
3230
|
this._batch_handlers = batchHandlers;
|
|
3082
3231
|
this._lanes = lanes;
|
|
3083
3232
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
@@ -3111,6 +3260,8 @@ var Act = class {
|
|
|
3111
3260
|
eventToLanes
|
|
3112
3261
|
} = classifyRegistry(this.registry, this._states);
|
|
3113
3262
|
this._reactive_events = reactiveEvents;
|
|
3263
|
+
this._listen = options.listen !== false;
|
|
3264
|
+
this._drain = options.drain !== false;
|
|
3114
3265
|
this._event_to_state = eventToState;
|
|
3115
3266
|
this._event_to_lanes = eventToLanes;
|
|
3116
3267
|
const allLanes = ["default", ...lanes.map((l) => l.name)];
|
|
@@ -3138,7 +3289,8 @@ var Act = class {
|
|
|
3138
3289
|
leaseMillis: cfg.leaseMillis
|
|
3139
3290
|
}
|
|
3140
3291
|
});
|
|
3141
|
-
if (cfg?.cycleMs !== void 0
|
|
3292
|
+
if (cfg?.cycleMs !== void 0 && options.drain !== false)
|
|
3293
|
+
controller.start(cfg.cycleMs);
|
|
3142
3294
|
this._drain_controllers.set(name, controller);
|
|
3143
3295
|
}
|
|
3144
3296
|
this._audit_deps = {
|
|
@@ -3156,9 +3308,10 @@ var Act = class {
|
|
|
3156
3308
|
hasDynamicResolvers,
|
|
3157
3309
|
this._cd,
|
|
3158
3310
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3159
|
-
// Cold start: assume drain is needed (historical events may need processing)
|
|
3311
|
+
// Cold start: assume drain is needed (historical events may need processing).
|
|
3312
|
+
// #803: writer-only instances skip the cold-start arm.
|
|
3160
3313
|
() => {
|
|
3161
|
-
if (this._reactive_events.size > 0) this._armAll();
|
|
3314
|
+
if (this._drain && this._reactive_events.size > 0) this._armAll();
|
|
3162
3315
|
}
|
|
3163
3316
|
);
|
|
3164
3317
|
this._settle = new SettleLoop(
|
|
@@ -3175,109 +3328,6 @@ var Act = class {
|
|
|
3175
3328
|
this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
|
|
3176
3329
|
dispose(() => this.shutdown());
|
|
3177
3330
|
}
|
|
3178
|
-
_emitter = new import_node_events.default();
|
|
3179
|
-
/** Event names with at least one registered reaction (computed at build time) */
|
|
3180
|
-
_reactive_events;
|
|
3181
|
-
/** One DrainController per active lane, keyed by lane name. */
|
|
3182
|
-
_drain_controllers;
|
|
3183
|
-
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
3184
|
-
_correlate;
|
|
3185
|
-
/** Debounced correlate→drain catch-up loop. */
|
|
3186
|
-
_settle;
|
|
3187
|
-
/**
|
|
3188
|
-
* Disposer for the cross-process notify subscription, set up eagerly
|
|
3189
|
-
* during construction. Held as a promise because the subscription
|
|
3190
|
-
* itself may be async (the PG adapter checks out a dedicated client
|
|
3191
|
-
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
3192
|
-
* the store doesn't implement `notify` or there are no registered
|
|
3193
|
-
* reactions.
|
|
3194
|
-
*
|
|
3195
|
-
* **Contract:** the configured store must be injected via
|
|
3196
|
-
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
3197
|
-
* orchestrator wires notify against whatever store is current at
|
|
3198
|
-
* construction time — late injection after build is unsupported.
|
|
3199
|
-
*/
|
|
3200
|
-
_notify_disposer;
|
|
3201
|
-
/**
|
|
3202
|
-
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
3203
|
-
* via {@link ActLifecycleEvents}.
|
|
3204
|
-
*/
|
|
3205
|
-
emit(event, args) {
|
|
3206
|
-
return this._emitter.emit(event, args);
|
|
3207
|
-
}
|
|
3208
|
-
/**
|
|
3209
|
-
* Register a listener for a lifecycle event. The listener receives the
|
|
3210
|
-
* event-specific payload.
|
|
3211
|
-
*/
|
|
3212
|
-
on(event, listener) {
|
|
3213
|
-
this._emitter.on(event, listener);
|
|
3214
|
-
return this;
|
|
3215
|
-
}
|
|
3216
|
-
/**
|
|
3217
|
-
* Remove a previously registered lifecycle listener.
|
|
3218
|
-
*/
|
|
3219
|
-
off(event, listener) {
|
|
3220
|
-
this._emitter.off(event, listener);
|
|
3221
|
-
return this;
|
|
3222
|
-
}
|
|
3223
|
-
/** Batch handlers for static-target projections (target → handler) */
|
|
3224
|
-
_batch_handlers;
|
|
3225
|
-
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
3226
|
-
_es;
|
|
3227
|
-
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
3228
|
-
_cd;
|
|
3229
|
-
/**
|
|
3230
|
-
* Event-name → owning state, computed at build time. The duplicate-event
|
|
3231
|
-
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
3232
|
-
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
3233
|
-
* set when seeding a `restart` snapshot in multi-state apps.
|
|
3234
|
-
*/
|
|
3235
|
-
_event_to_state;
|
|
3236
|
-
/**
|
|
3237
|
-
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
3238
|
-
* `classifyRegistry` once per build. `"all"` means at least one of
|
|
3239
|
-
* the event's reactions is a dynamic resolver (lane opaque until
|
|
3240
|
-
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
3241
|
-
* reactions target.
|
|
3242
|
-
*/
|
|
3243
|
-
_event_to_lanes;
|
|
3244
|
-
/**
|
|
3245
|
-
* Audit dependency bag (#723). Built once at construction; held as
|
|
3246
|
-
* an immutable snapshot of the registry state the audit module
|
|
3247
|
-
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
3248
|
-
* carries audit logic, only the deps + a one-liner that hands them
|
|
3249
|
-
* over.
|
|
3250
|
-
*/
|
|
3251
|
-
_audit_deps;
|
|
3252
|
-
/** Logger resolved at construction time (after user port configuration) */
|
|
3253
|
-
_logger = log();
|
|
3254
|
-
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
3255
|
-
* per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
|
|
3256
|
-
* path keeps reading fresh `store()`/`cache()` per call, which matters for
|
|
3257
|
-
* tests that dispose and re-seed mid-suite. */
|
|
3258
|
-
_scoped;
|
|
3259
|
-
/**
|
|
3260
|
-
* Correlation-id generator for originating actions. Bound at
|
|
3261
|
-
* construction from `options.correlator ?? defaultCorrelator`. The
|
|
3262
|
-
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
3263
|
-
* uses it via {@link closeCorrelation}.
|
|
3264
|
-
*/
|
|
3265
|
-
_correlator;
|
|
3266
|
-
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
3267
|
-
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
3268
|
-
_bound_do = this.do.bind(this);
|
|
3269
|
-
_bound_load = this.load.bind(this);
|
|
3270
|
-
_bound_query = this.query.bind(this);
|
|
3271
|
-
_bound_query_array = this.query_array.bind(this);
|
|
3272
|
-
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
3273
|
-
_handle;
|
|
3274
|
-
_handle_batch;
|
|
3275
|
-
/** Declared drain lanes (ACT-1103). */
|
|
3276
|
-
_lanes;
|
|
3277
|
-
/** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
|
|
3278
|
-
get lanes() {
|
|
3279
|
-
return this._lanes;
|
|
3280
|
-
}
|
|
3281
3331
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
3282
3332
|
_shutdown_promise;
|
|
3283
3333
|
/**
|
|
@@ -3312,14 +3362,17 @@ var Act = class {
|
|
|
3312
3362
|
async _wireNotify(s) {
|
|
3313
3363
|
if (this._reactive_events.size === 0) return void 0;
|
|
3314
3364
|
if (!s.notify) return void 0;
|
|
3365
|
+
if (!this._listen) return void 0;
|
|
3315
3366
|
try {
|
|
3316
3367
|
return await s.notify((notification) => {
|
|
3317
3368
|
try {
|
|
3318
3369
|
this.emit("notified", notification);
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3370
|
+
if (this._drain) {
|
|
3371
|
+
const armed = this._armForEventNames(
|
|
3372
|
+
notification.events.map((e) => e.name)
|
|
3373
|
+
);
|
|
3374
|
+
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
3375
|
+
}
|
|
3323
3376
|
} catch (err) {
|
|
3324
3377
|
this._logger.error(err, "notified handler threw");
|
|
3325
3378
|
}
|
|
@@ -3570,6 +3623,8 @@ var Act = class {
|
|
|
3570
3623
|
* @see {@link start_correlations} for automatic correlation
|
|
3571
3624
|
*/
|
|
3572
3625
|
async drain(options = {}) {
|
|
3626
|
+
if (!this._drain)
|
|
3627
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
3573
3628
|
return this._scoped(() => this._drainAll(options));
|
|
3574
3629
|
}
|
|
3575
3630
|
/** Arm every active lane controller (ACT-1103). */
|
|
@@ -3670,6 +3725,7 @@ var Act = class {
|
|
|
3670
3725
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
3671
3726
|
*/
|
|
3672
3727
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
3728
|
+
if (!this._drain) return { subscribed: 0, last_id: -1 };
|
|
3673
3729
|
return this._scoped(() => this._correlate.correlate(query));
|
|
3674
3730
|
}
|
|
3675
3731
|
/**
|
|
@@ -4095,6 +4151,7 @@ var Act = class {
|
|
|
4095
4151
|
* @see {@link correlate} for manual correlation
|
|
4096
4152
|
*/
|
|
4097
4153
|
settle(options = {}) {
|
|
4154
|
+
if (!this._drain) return;
|
|
4098
4155
|
this._settle.schedule(options);
|
|
4099
4156
|
}
|
|
4100
4157
|
};
|