@rotorsoft/act 1.24.1 → 1.25.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 +43 -10
- 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/config.d.ts +19 -1
- package/dist/@types/internal/config.d.ts.map +1 -1
- 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 +1 -1
- package/dist/@types/internal/index.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-IYXAAZMQ.js → chunk-NHJQCCBS.js} +1 -2
- package/dist/{chunk-IYXAAZMQ.js.map → chunk-NHJQCCBS.js.map} +1 -1
- package/dist/index.cjs +113 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +114 -10
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +0 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1011,7 +1011,6 @@ var InMemoryStore = class {
|
|
|
1011
1011
|
async claim(lagging, leading, by, millis, lane) {
|
|
1012
1012
|
await sleep();
|
|
1013
1013
|
const has_work = (s) => {
|
|
1014
|
-
if (s.at < 0) return true;
|
|
1015
1014
|
if (!s.source) return s.at < this._max_non_snap_event_id;
|
|
1016
1015
|
if (is_literal_source(s.source)) {
|
|
1017
1016
|
const max_id = this._max_event_id_by_stream.get(s.source);
|
|
@@ -2499,6 +2498,18 @@ function resolveSettleConfig(options) {
|
|
|
2499
2498
|
SettleOptionsSchema.parse(options);
|
|
2500
2499
|
return options;
|
|
2501
2500
|
}
|
|
2501
|
+
var MAX_SHUTDOWN_GRACE_MS = 3e4;
|
|
2502
|
+
var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
|
|
2503
|
+
var ShutdownOptionsSchema = import_zod6.z.object({
|
|
2504
|
+
// `0` is legal and is exactly today's behavior: stop scheduling and
|
|
2505
|
+
// return without waiting.
|
|
2506
|
+
graceMs: import_zod6.z.number().min(0).optional()
|
|
2507
|
+
}).loose();
|
|
2508
|
+
function resolveShutdownConfig(options) {
|
|
2509
|
+
if (options === void 0) return void 0;
|
|
2510
|
+
ShutdownOptionsSchema.parse(options);
|
|
2511
|
+
return options;
|
|
2512
|
+
}
|
|
2502
2513
|
var ActOptionsSchema = import_zod6.z.object({
|
|
2503
2514
|
maxSubscribedStreams: import_zod6.z.number().int().min(1).optional(),
|
|
2504
2515
|
settleDebounceMs: import_zod6.z.number().int().min(0).optional()
|
|
@@ -4204,6 +4215,16 @@ var DrainController = class {
|
|
|
4204
4215
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
4205
4216
|
_worker;
|
|
4206
4217
|
_stopped = false;
|
|
4218
|
+
/**
|
|
4219
|
+
* Resolves when the cycle currently in flight finishes; `undefined` when
|
|
4220
|
+
* no cycle is running (#1442). `_locked` answers "is a cycle running?" for
|
|
4221
|
+
* the overlap guard; this answers "tell me when it is done" for a graceful
|
|
4222
|
+
* shutdown, which needs to await the handler rather than abandon it
|
|
4223
|
+
* mid-`await` with the stream still leased. Never rejects — `drain()`
|
|
4224
|
+
* contains its own errors — so awaiting it is always safe.
|
|
4225
|
+
*/
|
|
4226
|
+
_inflight;
|
|
4227
|
+
_inflight_done;
|
|
4207
4228
|
_deps;
|
|
4208
4229
|
constructor(deps) {
|
|
4209
4230
|
this._deps = deps;
|
|
@@ -4236,6 +4257,24 @@ var DrainController = class {
|
|
|
4236
4257
|
get armed() {
|
|
4237
4258
|
return this._armed;
|
|
4238
4259
|
}
|
|
4260
|
+
/**
|
|
4261
|
+
* The cycle currently in flight, or `undefined` when idle (#1442). A
|
|
4262
|
+
* graceful shutdown awaits this so an in-flight handler reaches its `ack`
|
|
4263
|
+
* — which releases the stream's lease — instead of being abandoned with
|
|
4264
|
+
* the lease held until it expires.
|
|
4265
|
+
*/
|
|
4266
|
+
get inflight() {
|
|
4267
|
+
return this._inflight;
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* This lane's configured lease budget, or `undefined` when the lane didn't
|
|
4271
|
+
* pin one. It is the operator's own statement of how long a handler may
|
|
4272
|
+
* legitimately hold a stream, which makes it the right basis for a
|
|
4273
|
+
* shutdown grace budget (#1442).
|
|
4274
|
+
*/
|
|
4275
|
+
get lease_millis() {
|
|
4276
|
+
return this._deps.defaults?.leaseMillis;
|
|
4277
|
+
}
|
|
4239
4278
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
4240
4279
|
get lane() {
|
|
4241
4280
|
return this._deps.lane;
|
|
@@ -4282,6 +4321,9 @@ var DrainController = class {
|
|
|
4282
4321
|
const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
|
|
4283
4322
|
try {
|
|
4284
4323
|
this._locked = true;
|
|
4324
|
+
this._inflight = new Promise((done) => {
|
|
4325
|
+
this._inflight_done = done;
|
|
4326
|
+
});
|
|
4285
4327
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
4286
4328
|
const leading = streamLimit - lagging;
|
|
4287
4329
|
const cycle = await run_drain_cycle(
|
|
@@ -4323,6 +4365,9 @@ var DrainController = class {
|
|
|
4323
4365
|
return EMPTY_DRAIN;
|
|
4324
4366
|
} finally {
|
|
4325
4367
|
this._locked = false;
|
|
4368
|
+
this._inflight = void 0;
|
|
4369
|
+
this._inflight_done?.();
|
|
4370
|
+
this._inflight_done = void 0;
|
|
4326
4371
|
}
|
|
4327
4372
|
}
|
|
4328
4373
|
};
|
|
@@ -5080,29 +5125,87 @@ var Act = class {
|
|
|
5080
5125
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
5081
5126
|
_shutdown_promise;
|
|
5082
5127
|
/**
|
|
5083
|
-
* Per-instance teardown:
|
|
5084
|
-
*
|
|
5085
|
-
* down the cross-process notify subscription.
|
|
5128
|
+
* Per-instance teardown: stop scheduling new work, give drain cycles
|
|
5129
|
+
* already in flight a bounded chance to finish, then remove lifecycle
|
|
5130
|
+
* listeners and tear down the cross-process notify subscription.
|
|
5131
|
+
*
|
|
5132
|
+
* The order is deliberate (#1442). Scheduling stops first, so nothing new
|
|
5133
|
+
* is claimed while teardown runs. Then in-flight cycles are awaited up to
|
|
5134
|
+
* `graceMs`: a reaction handler parked on an `await` holds its stream's
|
|
5135
|
+
* lease until it acks, so abandoning it costs the replacement worker up to
|
|
5136
|
+
* `leaseMillis` of dead time on that stream and discards the round of work
|
|
5137
|
+
* (which #1418 then redelivers). Listeners come off *after* that wait, not
|
|
5138
|
+
* before, so an `acked` / `blocked` subscriber still observes the work
|
|
5139
|
+
* that completed during the grace window.
|
|
5086
5140
|
*
|
|
5087
|
-
*
|
|
5088
|
-
*
|
|
5089
|
-
*
|
|
5090
|
-
*
|
|
5141
|
+
* The budget is a ceiling, not a delay — teardown continues the moment the
|
|
5142
|
+
* last in-flight cycle finishes. When it is exhausted, teardown proceeds
|
|
5143
|
+
* anyway: one stuck handler must not hang a deploy, which is the failure
|
|
5144
|
+
* mode an unbounded wait would trade for.
|
|
5145
|
+
*
|
|
5146
|
+
* Idempotent — repeated calls return the same promise, and the first
|
|
5147
|
+
* call's `graceMs` is the one that applies. Registered automatically with
|
|
5148
|
+
* the global `dispose()` registry at construction, so process-wide
|
|
5149
|
+
* `dispose()()` covers it; test helpers (or operators that mint
|
|
5150
|
+
* short-lived Acts) call it explicitly for prompt cleanup.
|
|
5151
|
+
*
|
|
5152
|
+
* @param options - See {@link ShutdownOptions}. Defaults the grace budget
|
|
5153
|
+
* to the largest lane `leaseMillis` (capped at 30s).
|
|
5091
5154
|
*/
|
|
5092
|
-
shutdown() {
|
|
5155
|
+
shutdown(options) {
|
|
5093
5156
|
if (!this._shutdown_promise) {
|
|
5157
|
+
resolveShutdownConfig(options);
|
|
5094
5158
|
this._shutdown_promise = (async () => {
|
|
5095
|
-
this._emitter.removeAllListeners();
|
|
5096
5159
|
this.stop_correlations();
|
|
5097
5160
|
this.stop_settling();
|
|
5098
5161
|
this._breaker.stop();
|
|
5099
5162
|
for (const c of this._drain_controllers.values()) c.stop();
|
|
5163
|
+
await this._await_inflight(options?.graceMs);
|
|
5164
|
+
this._emitter.removeAllListeners();
|
|
5100
5165
|
const disposer = await this._notify_disposer;
|
|
5101
5166
|
if (disposer) await disposer();
|
|
5102
5167
|
})();
|
|
5103
5168
|
}
|
|
5104
5169
|
return this._shutdown_promise;
|
|
5105
5170
|
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Wait for every in-flight drain cycle, or for the grace budget to elapse,
|
|
5173
|
+
* whichever comes first. Cycle promises never reject (`drain()` contains
|
|
5174
|
+
* its own errors), so this never throws.
|
|
5175
|
+
*
|
|
5176
|
+
* An omitted budget is derived from the lanes that actually have a cycle
|
|
5177
|
+
* in flight: their `leaseMillis` is the operator's own statement of how
|
|
5178
|
+
* long one of their handlers may hold a stream, which makes it the honest
|
|
5179
|
+
* ceiling for how long teardown should wait for that handler. A parked
|
|
5180
|
+
* lane that pinned no lease contributes `drain()`'s own fallback, and the
|
|
5181
|
+
* whole thing is capped so a long-leased lane cannot hold a deploy open.
|
|
5182
|
+
* Idle lanes do not count — nothing is running on them to wait for.
|
|
5183
|
+
*/
|
|
5184
|
+
_derive_grace_ms(running) {
|
|
5185
|
+
let max = 0;
|
|
5186
|
+
for (const c of running)
|
|
5187
|
+
max = Math.max(max, c.lease_millis ?? DEFAULT_SHUTDOWN_GRACE_MS);
|
|
5188
|
+
return Math.min(max, MAX_SHUTDOWN_GRACE_MS);
|
|
5189
|
+
}
|
|
5190
|
+
async _await_inflight(grace_ms) {
|
|
5191
|
+
const running = [...this._drain_controllers.values()].filter(
|
|
5192
|
+
(c) => c.inflight !== void 0
|
|
5193
|
+
);
|
|
5194
|
+
if (running.length === 0) return;
|
|
5195
|
+
const grace = grace_ms ?? this._derive_grace_ms(running);
|
|
5196
|
+
if (grace <= 0) return;
|
|
5197
|
+
const inflight = running.map((c) => c.inflight);
|
|
5198
|
+
let timer;
|
|
5199
|
+
const budget = new Promise((resolve) => {
|
|
5200
|
+
timer = setTimeout(resolve, grace);
|
|
5201
|
+
timer.unref();
|
|
5202
|
+
});
|
|
5203
|
+
try {
|
|
5204
|
+
await Promise.race([Promise.all(inflight), budget]);
|
|
5205
|
+
} finally {
|
|
5206
|
+
clearTimeout(timer);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5106
5209
|
/**
|
|
5107
5210
|
* Subscribe to {@link Store.notify} when both the store and the
|
|
5108
5211
|
* registry support it. Returns the disposer (or `undefined` when no
|