@rotorsoft/act 1.18.5 → 1.18.7
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 +30 -7
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-reaction.d.ts +39 -0
- package/dist/@types/internal/autoclose-reaction.d.ts.map +1 -0
- package/dist/@types/internal/index.d.ts +1 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/index.cjs +190 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +190 -157
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -616,6 +616,108 @@ function compile_autoclose_policy(options) {
|
|
|
616
616
|
};
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
+
// src/internal/close-signal.ts
|
|
620
|
+
var CloseSignal = class extends Error {
|
|
621
|
+
/**
|
|
622
|
+
* Stream to close. Omitted → the reaction's own lease stream (a self-close).
|
|
623
|
+
* The synthesized autoclose reaction sets it to the aggregate stream, since
|
|
624
|
+
* its lease runs on a synthetic `__autoclose__` target.
|
|
625
|
+
*/
|
|
626
|
+
stream;
|
|
627
|
+
/** Optional archive callback to run while the stream is guarded. */
|
|
628
|
+
archive;
|
|
629
|
+
/**
|
|
630
|
+
* Watermark to ack the requesting reaction to before the close runs. The
|
|
631
|
+
* close-cycle safety guard skips a stream whose subscriptions (matched by
|
|
632
|
+
* source) lag the head; the autoclose reaction's `source` is the aggregate,
|
|
633
|
+
* so it must advance its own watermark to the live head id it evaluated
|
|
634
|
+
* against — otherwise it blocks its own close. Defaults to the triggering
|
|
635
|
+
* event id when omitted.
|
|
636
|
+
*/
|
|
637
|
+
at;
|
|
638
|
+
constructor(opts) {
|
|
639
|
+
super("reaction requested close");
|
|
640
|
+
this.name = "CloseSignal";
|
|
641
|
+
this.stream = opts?.stream;
|
|
642
|
+
this.archive = opts?.archive;
|
|
643
|
+
this.at = opts?.at;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/internal/defer-signal.ts
|
|
648
|
+
var DeferSignal = class extends Error {
|
|
649
|
+
/**
|
|
650
|
+
* The unresolved schedule. The dispatcher turns this into an absolute
|
|
651
|
+
* due-time by resolving it against the triggering event.
|
|
652
|
+
*/
|
|
653
|
+
when;
|
|
654
|
+
constructor(when) {
|
|
655
|
+
super("reaction deferred");
|
|
656
|
+
this.name = "DeferSignal";
|
|
657
|
+
this.when = when;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// src/internal/autoclose-reaction.ts
|
|
662
|
+
var AUTOCLOSE_TARGET_PREFIX = "__autoclose__:";
|
|
663
|
+
function synthesize_autoclose_reactions(registry, states, config2) {
|
|
664
|
+
for (const st of states.values()) {
|
|
665
|
+
const predicate = st.autoclose;
|
|
666
|
+
if (!predicate) continue;
|
|
667
|
+
const after_ms = st.autoclose_after_ms;
|
|
668
|
+
const archiver = st.archive;
|
|
669
|
+
const reaction = {
|
|
670
|
+
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
671
|
+
// per-aggregate `__autoclose__` key — so the autoclose reaction never
|
|
672
|
+
// shares a watermark with the aggregate's own reactions. A shared
|
|
673
|
+
// watermark would let autoclose's defer short-circuit the aggregate's
|
|
674
|
+
// other reactions (the "a defer affects all reactions on a stream"
|
|
675
|
+
// hazard). The close still targets the aggregate (`source`).
|
|
676
|
+
resolver: (e) => ({
|
|
677
|
+
target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
|
|
678
|
+
source: e.stream
|
|
679
|
+
}),
|
|
680
|
+
// Never block on autoclose: a transient query/store error should retry,
|
|
681
|
+
// not quarantine the synthetic stream.
|
|
682
|
+
options: { blockOnError: false, maxRetries: 3 },
|
|
683
|
+
handler: async (event) => {
|
|
684
|
+
const aggregate = event.stream;
|
|
685
|
+
if (!in_autoclose_window(config2.autocloseWindow, /* @__PURE__ */ new Date()))
|
|
686
|
+
throw new DeferSignal({
|
|
687
|
+
at: new Date(Date.now() + config2.autocloseCycleMinutes * 6e4)
|
|
688
|
+
});
|
|
689
|
+
const stats = await store().query_stats([aggregate], {
|
|
690
|
+
count: true,
|
|
691
|
+
exclude: [TOMBSTONE_EVENT]
|
|
692
|
+
});
|
|
693
|
+
const entry = stats.get(aggregate);
|
|
694
|
+
if (!entry) return;
|
|
695
|
+
const head = entry.head;
|
|
696
|
+
if (predicate(aggregate, head, entry.count))
|
|
697
|
+
throw new CloseSignal({
|
|
698
|
+
stream: aggregate,
|
|
699
|
+
archive: archiver ? () => archiver(aggregate, head) : void 0,
|
|
700
|
+
// Ack this reaction's own watermark to the live head so the
|
|
701
|
+
// close-cycle guard (which matches subscriptions by source =
|
|
702
|
+
// aggregate) sees it caught up instead of blocking its own close.
|
|
703
|
+
at: head.id
|
|
704
|
+
});
|
|
705
|
+
if (after_ms !== void 0)
|
|
706
|
+
throw new DeferSignal({
|
|
707
|
+
at: new Date(head.created.getTime() + after_ms)
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const key = `__autoclose_${st.name}`;
|
|
712
|
+
for (const event_name of Object.keys(st.events)) {
|
|
713
|
+
registry.events[event_name]?.reactions.set(
|
|
714
|
+
key,
|
|
715
|
+
reaction
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
619
721
|
// src/internal/build-classify.ts
|
|
620
722
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
621
723
|
function classify_registry(registry, states) {
|
|
@@ -671,22 +773,6 @@ function classify_registry(registry, states) {
|
|
|
671
773
|
|
|
672
774
|
// src/internal/defer-config.ts
|
|
673
775
|
import { z as z3 } from "zod";
|
|
674
|
-
|
|
675
|
-
// src/internal/defer-signal.ts
|
|
676
|
-
var DeferSignal = class extends Error {
|
|
677
|
-
/**
|
|
678
|
-
* The unresolved schedule. The dispatcher turns this into an absolute
|
|
679
|
-
* due-time by resolving it against the triggering event.
|
|
680
|
-
*/
|
|
681
|
-
when;
|
|
682
|
-
constructor(when) {
|
|
683
|
-
super("reaction deferred");
|
|
684
|
-
this.name = "DeferSignal";
|
|
685
|
-
this.when = when;
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
// src/internal/defer-config.ts
|
|
690
776
|
var DeferDurationSchema = z3.object({
|
|
691
777
|
days: z3.number().positive().optional(),
|
|
692
778
|
hours: z3.number().positive().optional(),
|
|
@@ -1170,34 +1256,6 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
|
|
|
1170
1256
|
return truncated;
|
|
1171
1257
|
}
|
|
1172
1258
|
|
|
1173
|
-
// src/internal/close-signal.ts
|
|
1174
|
-
var CloseSignal = class extends Error {
|
|
1175
|
-
/**
|
|
1176
|
-
* Stream to close. Omitted → the reaction's own lease stream (a self-close).
|
|
1177
|
-
* The synthesized autoclose reaction sets it to the aggregate stream, since
|
|
1178
|
-
* its lease runs on a synthetic `__autoclose__` target.
|
|
1179
|
-
*/
|
|
1180
|
-
stream;
|
|
1181
|
-
/** Optional archive callback to run while the stream is guarded. */
|
|
1182
|
-
archive;
|
|
1183
|
-
/**
|
|
1184
|
-
* Watermark to ack the requesting reaction to before the close runs. The
|
|
1185
|
-
* close-cycle safety guard skips a stream whose subscriptions (matched by
|
|
1186
|
-
* source) lag the head; the autoclose reaction's `source` is the aggregate,
|
|
1187
|
-
* so it must advance its own watermark to the live head id it evaluated
|
|
1188
|
-
* against — otherwise it blocks its own close. Defaults to the triggering
|
|
1189
|
-
* event id when omitted.
|
|
1190
|
-
*/
|
|
1191
|
-
at;
|
|
1192
|
-
constructor(opts) {
|
|
1193
|
-
super("reaction requested close");
|
|
1194
|
-
this.name = "CloseSignal";
|
|
1195
|
-
this.stream = opts?.stream;
|
|
1196
|
-
this.archive = opts?.archive;
|
|
1197
|
-
this.at = opts?.at;
|
|
1198
|
-
}
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
1259
|
// src/internal/correlate-cycle.ts
|
|
1202
1260
|
var CorrelateCycle = class {
|
|
1203
1261
|
_checkpoint = -1;
|
|
@@ -2365,7 +2423,15 @@ var SettleLoop = class {
|
|
|
2365
2423
|
// src/act.ts
|
|
2366
2424
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2367
2425
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
2368
|
-
|
|
2426
|
+
function validate_only_lanes(options, lanes) {
|
|
2427
|
+
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
2428
|
+
const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
|
|
2429
|
+
const unknown = options.onlyLanes.filter((l) => !declared.has(l));
|
|
2430
|
+
if (unknown.length > 0)
|
|
2431
|
+
throw new Error(
|
|
2432
|
+
`ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2369
2435
|
var Act = class {
|
|
2370
2436
|
_emitter = new EventEmitter();
|
|
2371
2437
|
/** ACT-984: orchestrator-owned circuit breaker shared by all drain lanes. */
|
|
@@ -2400,13 +2466,6 @@ var Act = class {
|
|
|
2400
2466
|
registry;
|
|
2401
2467
|
/** Map of state name → state definition; populated by the builder. */
|
|
2402
2468
|
_states;
|
|
2403
|
-
/**
|
|
2404
|
-
* Resolved autoclose configuration (#837 / epic #802). Frozen at
|
|
2405
|
-
* `act().build()` via {@link resolveAutocloseConfig}.
|
|
2406
|
-
*
|
|
2407
|
-
* @internal
|
|
2408
|
-
*/
|
|
2409
|
-
_autoclose_config;
|
|
2410
2469
|
/**
|
|
2411
2470
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
2412
2471
|
* via {@link ActLifecycleEvents}.
|
|
@@ -2523,17 +2582,7 @@ var Act = class {
|
|
|
2523
2582
|
this._states = states;
|
|
2524
2583
|
this._batch_handlers = batch_handlers;
|
|
2525
2584
|
this._lanes = lanes;
|
|
2526
|
-
|
|
2527
|
-
const declared = /* @__PURE__ */ new Set([
|
|
2528
|
-
"default",
|
|
2529
|
-
...lanes.map((l) => l.name)
|
|
2530
|
-
]);
|
|
2531
|
-
const unknown = options.onlyLanes.filter((l) => !declared.has(l));
|
|
2532
|
-
if (unknown.length > 0)
|
|
2533
|
-
throw new Error(
|
|
2534
|
-
`ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
|
|
2535
|
-
);
|
|
2536
|
-
}
|
|
2585
|
+
validate_only_lanes(options, lanes);
|
|
2537
2586
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2538
2587
|
this._correlator = options.correlator ?? default_correlator;
|
|
2539
2588
|
this._es = build_es(this._logger, this._correlator);
|
|
@@ -2547,99 +2596,59 @@ var Act = class {
|
|
|
2547
2596
|
bound_forget: this._bound_forget
|
|
2548
2597
|
});
|
|
2549
2598
|
this._handle_batch = build_handle_batch(this._logger);
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
const archiver = st.archive;
|
|
2555
|
-
const reaction = {
|
|
2556
|
-
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
2557
|
-
// per-aggregate `__autoclose__` key — so the autoclose reaction never
|
|
2558
|
-
// shares a watermark with the aggregate's own reactions. A shared
|
|
2559
|
-
// watermark would let autoclose's defer short-circuit the aggregate's
|
|
2560
|
-
// other reactions (the "a defer affects all reactions on a stream"
|
|
2561
|
-
// hazard). The close still targets the aggregate (`source`).
|
|
2562
|
-
resolver: (e) => ({
|
|
2563
|
-
target: `${AUTOCLOSE_TARGET_PREFIX}${e.stream}`,
|
|
2564
|
-
source: e.stream
|
|
2565
|
-
}),
|
|
2566
|
-
// Never block on autoclose: a transient query/store error should retry,
|
|
2567
|
-
// not quarantine the synthetic stream.
|
|
2568
|
-
options: { blockOnError: false, maxRetries: 3 },
|
|
2569
|
-
handler: async (event) => {
|
|
2570
|
-
const aggregate = event.stream;
|
|
2571
|
-
const config2 = this._autoclose_config;
|
|
2572
|
-
if (!in_autoclose_window(config2.autocloseWindow, /* @__PURE__ */ new Date()))
|
|
2573
|
-
throw new DeferSignal({
|
|
2574
|
-
at: new Date(Date.now() + config2.autocloseCycleMinutes * 6e4)
|
|
2575
|
-
});
|
|
2576
|
-
const stats = await store().query_stats([aggregate], {
|
|
2577
|
-
count: true,
|
|
2578
|
-
exclude: [TOMBSTONE_EVENT]
|
|
2579
|
-
});
|
|
2580
|
-
const entry = stats.get(aggregate);
|
|
2581
|
-
if (!entry) return;
|
|
2582
|
-
const head = entry.head;
|
|
2583
|
-
if (predicate(aggregate, head, entry.count))
|
|
2584
|
-
throw new CloseSignal({
|
|
2585
|
-
stream: aggregate,
|
|
2586
|
-
archive: archiver ? () => archiver(aggregate, head) : void 0,
|
|
2587
|
-
// Ack this reaction's own watermark to the live head so the
|
|
2588
|
-
// close-cycle guard (which matches subscriptions by source =
|
|
2589
|
-
// aggregate) sees it caught up instead of blocking its own close.
|
|
2590
|
-
at: head.id
|
|
2591
|
-
});
|
|
2592
|
-
if (after_ms !== void 0)
|
|
2593
|
-
throw new DeferSignal({
|
|
2594
|
-
at: new Date(head.created.getTime() + after_ms)
|
|
2595
|
-
});
|
|
2596
|
-
}
|
|
2597
|
-
};
|
|
2598
|
-
const key = `__autoclose_${st.name}`;
|
|
2599
|
-
for (const event_name of Object.keys(st.events)) {
|
|
2600
|
-
this.registry.events[event_name]?.reactions.set(
|
|
2601
|
-
key,
|
|
2602
|
-
reaction
|
|
2603
|
-
);
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
const {
|
|
2607
|
-
static_targets,
|
|
2608
|
-
has_dynamic_resolvers,
|
|
2609
|
-
reactive_events,
|
|
2610
|
-
event_to_state,
|
|
2611
|
-
event_to_lanes
|
|
2612
|
-
} = classify_registry(this.registry, this._states);
|
|
2613
|
-
this._reactive_events = reactive_events;
|
|
2599
|
+
const classification = classify_registry(this.registry, this._states);
|
|
2600
|
+
this._reactive_events = classification.reactive_events;
|
|
2601
|
+
this._event_to_state = classification.event_to_state;
|
|
2602
|
+
this._event_to_lanes = classification.event_to_lanes;
|
|
2614
2603
|
this._listen = options.listen !== false;
|
|
2615
2604
|
this._drain = options.drain !== false;
|
|
2616
|
-
this.
|
|
2617
|
-
this.
|
|
2605
|
+
this._breaker = this._build_breaker(options);
|
|
2606
|
+
this._drain_controllers = this._build_drain_controllers(options, lanes);
|
|
2607
|
+
this._audit_deps = this._build_audit_deps();
|
|
2608
|
+
this._correlate = this._build_correlate(options, classification);
|
|
2609
|
+
this._settle = this._build_settle(options);
|
|
2610
|
+
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2611
|
+
dispose(() => this.shutdown());
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Circuit breaker shared by every store-polling loop (drain, the settle
|
|
2615
|
+
* correlate, autoclose). Validates the knobs eagerly so out-of-range
|
|
2616
|
+
* values throw at build time, not on the first cycle tick.
|
|
2617
|
+
*/
|
|
2618
|
+
_build_breaker(options) {
|
|
2619
|
+
return new CircuitBreaker(
|
|
2618
2620
|
resolveCircuitBreakerConfig(options.circuitBreaker),
|
|
2619
2621
|
{
|
|
2620
2622
|
on_error: (error, circuit) => this._emit_error(error, circuit),
|
|
2621
2623
|
// Re-probe the store when the cooldown elapses, so recovery is
|
|
2622
2624
|
// automatic even on the default lane (which has no periodic poller).
|
|
2623
|
-
//
|
|
2624
|
-
//
|
|
2625
|
-
//
|
|
2626
|
-
//
|
|
2627
|
-
//
|
|
2628
|
-
//
|
|
2629
|
-
//
|
|
2630
|
-
// re-opens the breaker and reschedules the wake.
|
|
2625
|
+
// Any polling loop can trip the shared breaker — but a bare `drain()`
|
|
2626
|
+
// only touches the store when the controller is armed, so a trip
|
|
2627
|
+
// from a correlate failure could go unprobed. `settle()` always runs
|
|
2628
|
+
// a correlate (a store query) before draining, so it re-probes
|
|
2629
|
+
// regardless of which loop opened the breaker: one success closes it
|
|
2630
|
+
// and every loop resumes on its own cadence. A failed probe re-opens
|
|
2631
|
+
// the breaker and reschedules the wake.
|
|
2631
2632
|
on_retry: () => {
|
|
2632
2633
|
this.settle({ debounceMs: 0 });
|
|
2633
2634
|
}
|
|
2634
2635
|
}
|
|
2635
2636
|
);
|
|
2636
|
-
|
|
2637
|
-
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* One DrainController per active lane. The implicit "default" lane is
|
|
2640
|
+
* always present unless onlyLanes excludes it. Each controller filters
|
|
2641
|
+
* its claim() by its lane name; the legacy single-controller path is the
|
|
2642
|
+
* active set === { "default" } case with `lane: undefined` deps so
|
|
2643
|
+
* claim() doesn't filter (preserves the single-lane SQL planner shape
|
|
2644
|
+
* for apps that never call withLane).
|
|
2645
|
+
*/
|
|
2646
|
+
_build_drain_controllers(options, lanes) {
|
|
2638
2647
|
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
2639
2648
|
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
2640
2649
|
const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
|
|
2641
2650
|
const single_default_lane = active_lanes.length === 1 && active_lanes[0] === "default";
|
|
2642
|
-
|
|
2651
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
2643
2652
|
for (const name of active_lanes) {
|
|
2644
2653
|
const cfg = lanes.find((l) => l.name === name);
|
|
2645
2654
|
const controller = new DrainController({
|
|
@@ -2651,10 +2660,11 @@ var Act = class {
|
|
|
2651
2660
|
handle_batch: this._handle_batch,
|
|
2652
2661
|
on_acked: (acked) => this.emit("acked", acked),
|
|
2653
2662
|
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
2654
|
-
// Reaction-requested close
|
|
2663
|
+
// Reaction-requested close. Runs the same close machinery as
|
|
2655
2664
|
// `app.close` (tombstone guard + archive + atomic truncate) for the
|
|
2656
|
-
// targets a handler signalled via `CloseSignal`. No `correlate()`
|
|
2657
|
-
// — the drain that produced these targets has already
|
|
2665
|
+
// targets a handler signalled via `CloseSignal`. No `correlate()`
|
|
2666
|
+
// here — the drain that produced these targets has already
|
|
2667
|
+
// correlated.
|
|
2658
2668
|
on_close: async (targets) => {
|
|
2659
2669
|
const close_actor = { id: "$close", name: "close" };
|
|
2660
2670
|
const result = await run_close_cycle(targets, {
|
|
@@ -2670,7 +2680,7 @@ var Act = class {
|
|
|
2670
2680
|
breaker: this._breaker,
|
|
2671
2681
|
// Pass lane only when a true per-lane controller is active.
|
|
2672
2682
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
2673
|
-
// adapter SQL collapses to the
|
|
2683
|
+
// adapter SQL collapses to the single-lane shape.
|
|
2674
2684
|
lane: single_default_lane ? void 0 : name,
|
|
2675
2685
|
defaults: cfg && {
|
|
2676
2686
|
streamLimit: cfg.streamLimit,
|
|
@@ -2679,30 +2689,47 @@ var Act = class {
|
|
|
2679
2689
|
});
|
|
2680
2690
|
if (cfg?.cycleMs !== void 0 && options.drain !== false)
|
|
2681
2691
|
controller.start(cfg.cycleMs);
|
|
2682
|
-
|
|
2692
|
+
controllers.set(name, controller);
|
|
2683
2693
|
}
|
|
2684
|
-
|
|
2694
|
+
return controllers;
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Audit deps bag. Snapshotted after registry classification and
|
|
2698
|
+
* drain-controller build so the audit module sees the finalized lane
|
|
2699
|
+
* set. Held as an immutable bag — the orchestrator never carries audit
|
|
2700
|
+
* logic itself, only this typed contract.
|
|
2701
|
+
*/
|
|
2702
|
+
_build_audit_deps() {
|
|
2703
|
+
return {
|
|
2685
2704
|
store,
|
|
2686
2705
|
logger: this._logger,
|
|
2687
|
-
event_to_state,
|
|
2706
|
+
event_to_state: this._event_to_state,
|
|
2688
2707
|
states: this._states,
|
|
2689
|
-
known_events: new Set(
|
|
2708
|
+
known_events: new Set(this._event_to_state.keys()),
|
|
2690
2709
|
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
2691
|
-
routed_events: new Set(
|
|
2710
|
+
routed_events: new Set(this._event_to_lanes.keys())
|
|
2692
2711
|
};
|
|
2693
|
-
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Correlate cycle over the classified registry. The cold-start callback
|
|
2715
|
+
* arms every controller — historical events may need processing —
|
|
2716
|
+
* except on writer-only instances (`drain: false`).
|
|
2717
|
+
*/
|
|
2718
|
+
_build_correlate(options, classification) {
|
|
2719
|
+
return new CorrelateCycle(
|
|
2694
2720
|
this.registry,
|
|
2695
|
-
static_targets,
|
|
2696
|
-
has_dynamic_resolvers,
|
|
2721
|
+
classification.static_targets,
|
|
2722
|
+
classification.has_dynamic_resolvers,
|
|
2697
2723
|
this._cd,
|
|
2698
2724
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2699
|
-
// Cold start: assume drain is needed (historical events may need processing).
|
|
2700
|
-
// #803: writer-only instances skip the cold-start arm.
|
|
2701
2725
|
() => {
|
|
2702
2726
|
if (this._drain && this._reactive_events.size > 0) this._arm_all();
|
|
2703
2727
|
}
|
|
2704
2728
|
);
|
|
2705
|
-
|
|
2729
|
+
}
|
|
2730
|
+
/** Settle loop driving correlate + drain to quiescence. */
|
|
2731
|
+
_build_settle(options) {
|
|
2732
|
+
return new SettleLoop(
|
|
2706
2733
|
{
|
|
2707
2734
|
init: () => this._correlate.init(),
|
|
2708
2735
|
checkpoint: () => this._correlate.checkpoint,
|
|
@@ -2713,8 +2740,6 @@ var Act = class {
|
|
|
2713
2740
|
},
|
|
2714
2741
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2715
2742
|
);
|
|
2716
|
-
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2717
|
-
dispose(() => this.shutdown());
|
|
2718
2743
|
}
|
|
2719
2744
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
2720
2745
|
_shutdown_promise;
|
|
@@ -3763,6 +3788,14 @@ function act() {
|
|
|
3763
3788
|
};
|
|
3764
3789
|
batch_handlers.set(target, wrapped);
|
|
3765
3790
|
}
|
|
3791
|
+
synthesize_autoclose_reactions(
|
|
3792
|
+
registry,
|
|
3793
|
+
states,
|
|
3794
|
+
resolveAutocloseConfig(options ?? {})
|
|
3795
|
+
);
|
|
3796
|
+
Object.freeze(registry.actions);
|
|
3797
|
+
Object.freeze(registry.events);
|
|
3798
|
+
Object.freeze(registry);
|
|
3766
3799
|
_built = true;
|
|
3767
3800
|
}
|
|
3768
3801
|
return new Act(
|