@rotorsoft/act 1.24.0 → 1.25.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 +43 -10
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/internal/config.d.ts +19 -1
- package/dist/@types/internal/config.d.ts.map +1 -1
- package/dist/@types/internal/disposers.d.ts +29 -0
- package/dist/@types/internal/disposers.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +24 -0
- package/dist/@types/internal/drain-cycle.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/ports.d.ts +5 -0
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +23 -0
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/{chunk-MIHIDR4K.js → chunk-IYXAAZMQ.js} +34 -6
- package/dist/chunk-IYXAAZMQ.js.map +1 -0
- package/dist/index.cjs +146 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +116 -11
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-MIHIDR4K.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -17,11 +17,12 @@ import {
|
|
|
17
17
|
is_literal_source,
|
|
18
18
|
log,
|
|
19
19
|
port,
|
|
20
|
+
register_weak_disposer,
|
|
20
21
|
scoped,
|
|
21
22
|
sleep,
|
|
22
23
|
store,
|
|
23
24
|
validate
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-IYXAAZMQ.js";
|
|
25
26
|
import {
|
|
26
27
|
ActorSchema,
|
|
27
28
|
CausationEventSchema,
|
|
@@ -930,6 +931,18 @@ function resolveSettleConfig(options) {
|
|
|
930
931
|
SettleOptionsSchema.parse(options);
|
|
931
932
|
return options;
|
|
932
933
|
}
|
|
934
|
+
var MAX_SHUTDOWN_GRACE_MS = 3e4;
|
|
935
|
+
var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
|
|
936
|
+
var ShutdownOptionsSchema = z2.object({
|
|
937
|
+
// `0` is legal and is exactly today's behavior: stop scheduling and
|
|
938
|
+
// return without waiting.
|
|
939
|
+
graceMs: z2.number().min(0).optional()
|
|
940
|
+
}).loose();
|
|
941
|
+
function resolveShutdownConfig(options) {
|
|
942
|
+
if (options === void 0) return void 0;
|
|
943
|
+
ShutdownOptionsSchema.parse(options);
|
|
944
|
+
return options;
|
|
945
|
+
}
|
|
933
946
|
var ActOptionsSchema = z2.object({
|
|
934
947
|
maxSubscribedStreams: z2.number().int().min(1).optional(),
|
|
935
948
|
settleDebounceMs: z2.number().int().min(0).optional()
|
|
@@ -2635,6 +2648,16 @@ var DrainController = class {
|
|
|
2635
2648
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
2636
2649
|
_worker;
|
|
2637
2650
|
_stopped = false;
|
|
2651
|
+
/**
|
|
2652
|
+
* Resolves when the cycle currently in flight finishes; `undefined` when
|
|
2653
|
+
* no cycle is running (#1442). `_locked` answers "is a cycle running?" for
|
|
2654
|
+
* the overlap guard; this answers "tell me when it is done" for a graceful
|
|
2655
|
+
* shutdown, which needs to await the handler rather than abandon it
|
|
2656
|
+
* mid-`await` with the stream still leased. Never rejects — `drain()`
|
|
2657
|
+
* contains its own errors — so awaiting it is always safe.
|
|
2658
|
+
*/
|
|
2659
|
+
_inflight;
|
|
2660
|
+
_inflight_done;
|
|
2638
2661
|
_deps;
|
|
2639
2662
|
constructor(deps) {
|
|
2640
2663
|
this._deps = deps;
|
|
@@ -2667,6 +2690,24 @@ var DrainController = class {
|
|
|
2667
2690
|
get armed() {
|
|
2668
2691
|
return this._armed;
|
|
2669
2692
|
}
|
|
2693
|
+
/**
|
|
2694
|
+
* The cycle currently in flight, or `undefined` when idle (#1442). A
|
|
2695
|
+
* graceful shutdown awaits this so an in-flight handler reaches its `ack`
|
|
2696
|
+
* — which releases the stream's lease — instead of being abandoned with
|
|
2697
|
+
* the lease held until it expires.
|
|
2698
|
+
*/
|
|
2699
|
+
get inflight() {
|
|
2700
|
+
return this._inflight;
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* This lane's configured lease budget, or `undefined` when the lane didn't
|
|
2704
|
+
* pin one. It is the operator's own statement of how long a handler may
|
|
2705
|
+
* legitimately hold a stream, which makes it the right basis for a
|
|
2706
|
+
* shutdown grace budget (#1442).
|
|
2707
|
+
*/
|
|
2708
|
+
get lease_millis() {
|
|
2709
|
+
return this._deps.defaults?.leaseMillis;
|
|
2710
|
+
}
|
|
2670
2711
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
2671
2712
|
get lane() {
|
|
2672
2713
|
return this._deps.lane;
|
|
@@ -2713,6 +2754,9 @@ var DrainController = class {
|
|
|
2713
2754
|
const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
|
|
2714
2755
|
try {
|
|
2715
2756
|
this._locked = true;
|
|
2757
|
+
this._inflight = new Promise((done) => {
|
|
2758
|
+
this._inflight_done = done;
|
|
2759
|
+
});
|
|
2716
2760
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2717
2761
|
const leading = streamLimit - lagging;
|
|
2718
2762
|
const cycle = await run_drain_cycle(
|
|
@@ -2754,6 +2798,9 @@ var DrainController = class {
|
|
|
2754
2798
|
return EMPTY_DRAIN;
|
|
2755
2799
|
} finally {
|
|
2756
2800
|
this._locked = false;
|
|
2801
|
+
this._inflight = void 0;
|
|
2802
|
+
this._inflight_done?.();
|
|
2803
|
+
this._inflight_done = void 0;
|
|
2757
2804
|
}
|
|
2758
2805
|
}
|
|
2759
2806
|
};
|
|
@@ -3300,7 +3347,7 @@ var Act = class {
|
|
|
3300
3347
|
this._correlate = this._build_correlate(options, classification);
|
|
3301
3348
|
this._settle = this._build_settle(options);
|
|
3302
3349
|
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
3303
|
-
|
|
3350
|
+
register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
|
|
3304
3351
|
}
|
|
3305
3352
|
/**
|
|
3306
3353
|
* Circuit breaker shared by every store-polling loop (drain, the settle
|
|
@@ -3511,29 +3558,87 @@ var Act = class {
|
|
|
3511
3558
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
3512
3559
|
_shutdown_promise;
|
|
3513
3560
|
/**
|
|
3514
|
-
* Per-instance teardown:
|
|
3515
|
-
*
|
|
3516
|
-
* down the cross-process notify subscription.
|
|
3561
|
+
* Per-instance teardown: stop scheduling new work, give drain cycles
|
|
3562
|
+
* already in flight a bounded chance to finish, then remove lifecycle
|
|
3563
|
+
* listeners and tear down the cross-process notify subscription.
|
|
3564
|
+
*
|
|
3565
|
+
* The order is deliberate (#1442). Scheduling stops first, so nothing new
|
|
3566
|
+
* is claimed while teardown runs. Then in-flight cycles are awaited up to
|
|
3567
|
+
* `graceMs`: a reaction handler parked on an `await` holds its stream's
|
|
3568
|
+
* lease until it acks, so abandoning it costs the replacement worker up to
|
|
3569
|
+
* `leaseMillis` of dead time on that stream and discards the round of work
|
|
3570
|
+
* (which #1418 then redelivers). Listeners come off *after* that wait, not
|
|
3571
|
+
* before, so an `acked` / `blocked` subscriber still observes the work
|
|
3572
|
+
* that completed during the grace window.
|
|
3517
3573
|
*
|
|
3518
|
-
*
|
|
3519
|
-
*
|
|
3520
|
-
*
|
|
3521
|
-
*
|
|
3574
|
+
* The budget is a ceiling, not a delay — teardown continues the moment the
|
|
3575
|
+
* last in-flight cycle finishes. When it is exhausted, teardown proceeds
|
|
3576
|
+
* anyway: one stuck handler must not hang a deploy, which is the failure
|
|
3577
|
+
* mode an unbounded wait would trade for.
|
|
3578
|
+
*
|
|
3579
|
+
* Idempotent — repeated calls return the same promise, and the first
|
|
3580
|
+
* call's `graceMs` is the one that applies. Registered automatically with
|
|
3581
|
+
* the global `dispose()` registry at construction, so process-wide
|
|
3582
|
+
* `dispose()()` covers it; test helpers (or operators that mint
|
|
3583
|
+
* short-lived Acts) call it explicitly for prompt cleanup.
|
|
3584
|
+
*
|
|
3585
|
+
* @param options - See {@link ShutdownOptions}. Defaults the grace budget
|
|
3586
|
+
* to the largest lane `leaseMillis` (capped at 30s).
|
|
3522
3587
|
*/
|
|
3523
|
-
shutdown() {
|
|
3588
|
+
shutdown(options) {
|
|
3524
3589
|
if (!this._shutdown_promise) {
|
|
3590
|
+
resolveShutdownConfig(options);
|
|
3525
3591
|
this._shutdown_promise = (async () => {
|
|
3526
|
-
this._emitter.removeAllListeners();
|
|
3527
3592
|
this.stop_correlations();
|
|
3528
3593
|
this.stop_settling();
|
|
3529
3594
|
this._breaker.stop();
|
|
3530
3595
|
for (const c of this._drain_controllers.values()) c.stop();
|
|
3596
|
+
await this._await_inflight(options?.graceMs);
|
|
3597
|
+
this._emitter.removeAllListeners();
|
|
3531
3598
|
const disposer = await this._notify_disposer;
|
|
3532
3599
|
if (disposer) await disposer();
|
|
3533
3600
|
})();
|
|
3534
3601
|
}
|
|
3535
3602
|
return this._shutdown_promise;
|
|
3536
3603
|
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Wait for every in-flight drain cycle, or for the grace budget to elapse,
|
|
3606
|
+
* whichever comes first. Cycle promises never reject (`drain()` contains
|
|
3607
|
+
* its own errors), so this never throws.
|
|
3608
|
+
*
|
|
3609
|
+
* An omitted budget is derived from the lanes that actually have a cycle
|
|
3610
|
+
* in flight: their `leaseMillis` is the operator's own statement of how
|
|
3611
|
+
* long one of their handlers may hold a stream, which makes it the honest
|
|
3612
|
+
* ceiling for how long teardown should wait for that handler. A parked
|
|
3613
|
+
* lane that pinned no lease contributes `drain()`'s own fallback, and the
|
|
3614
|
+
* whole thing is capped so a long-leased lane cannot hold a deploy open.
|
|
3615
|
+
* Idle lanes do not count — nothing is running on them to wait for.
|
|
3616
|
+
*/
|
|
3617
|
+
_derive_grace_ms(running) {
|
|
3618
|
+
let max = 0;
|
|
3619
|
+
for (const c of running)
|
|
3620
|
+
max = Math.max(max, c.lease_millis ?? DEFAULT_SHUTDOWN_GRACE_MS);
|
|
3621
|
+
return Math.min(max, MAX_SHUTDOWN_GRACE_MS);
|
|
3622
|
+
}
|
|
3623
|
+
async _await_inflight(grace_ms) {
|
|
3624
|
+
const running = [...this._drain_controllers.values()].filter(
|
|
3625
|
+
(c) => c.inflight !== void 0
|
|
3626
|
+
);
|
|
3627
|
+
if (running.length === 0) return;
|
|
3628
|
+
const grace = grace_ms ?? this._derive_grace_ms(running);
|
|
3629
|
+
if (grace <= 0) return;
|
|
3630
|
+
const inflight = running.map((c) => c.inflight);
|
|
3631
|
+
let timer;
|
|
3632
|
+
const budget = new Promise((resolve) => {
|
|
3633
|
+
timer = setTimeout(resolve, grace);
|
|
3634
|
+
timer.unref();
|
|
3635
|
+
});
|
|
3636
|
+
try {
|
|
3637
|
+
await Promise.race([Promise.all(inflight), budget]);
|
|
3638
|
+
} finally {
|
|
3639
|
+
clearTimeout(timer);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3537
3642
|
/**
|
|
3538
3643
|
* Subscribe to {@link Store.notify} when both the store and the
|
|
3539
3644
|
* registry support it. Returns the disposer (or `undefined` when no
|