@rotorsoft/act 1.13.0 → 1.14.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 +37 -0
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-cycle.d.ts +9 -0
- package/dist/@types/internal/autoclose-cycle.d.ts.map +1 -1
- package/dist/@types/internal/circuit-breaker.d.ts +90 -0
- package/dist/@types/internal/circuit-breaker.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +9 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/settle.d.ts +10 -2
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +31 -0
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/{chunk-NGVWJXEF.js → chunk-4TJN4G7L.js} +13 -2
- package/dist/chunk-4TJN4G7L.js.map +1 -0
- package/dist/{chunk-QCSXT5MB.js → chunk-QKVY5QAQ.js} +2 -2
- package/dist/{chunk-5WRI5ZAA.js → chunk-YLJ4XMA6.js} +6 -2
- package/dist/{dist-INXH4GUE.js → dist-EOG547W7.js} +2 -2
- package/dist/index.cjs +169 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +159 -13
- package/dist/index.js.map +1 -1
- package/dist/{magic-string.es-ZGO77SPS.js → magic-string.es-5F2IVYTG.js} +2 -2
- package/dist/test/index.cjs +9 -4
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +5 -5
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs +13 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +4 -2
- package/package.json +2 -2
- package/dist/chunk-NGVWJXEF.js.map +0 -1
- /package/dist/{chunk-QCSXT5MB.js.map → chunk-QKVY5QAQ.js.map} +0 -0
- /package/dist/{chunk-5WRI5ZAA.js.map → chunk-YLJ4XMA6.js.map} +0 -0
- /package/dist/{dist-INXH4GUE.js.map → dist-EOG547W7.js.map} +0 -0
- /package/dist/{magic-string.es-ZGO77SPS.js.map → magic-string.es-5F2IVYTG.js.map} +0 -0
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-QKVY5QAQ.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
QuerySchema,
|
|
35
35
|
REDACTED,
|
|
36
36
|
SHREDDED,
|
|
37
|
+
StoreError,
|
|
37
38
|
StreamClosedError,
|
|
38
39
|
TargetSchema,
|
|
39
40
|
ValidationError,
|
|
@@ -43,8 +44,8 @@ import {
|
|
|
43
44
|
pii_split,
|
|
44
45
|
pii_strip,
|
|
45
46
|
sensitive
|
|
46
|
-
} from "./chunk-
|
|
47
|
-
import "./chunk-
|
|
47
|
+
} from "./chunk-4TJN4G7L.js";
|
|
48
|
+
import "./chunk-YLJ4XMA6.js";
|
|
48
49
|
|
|
49
50
|
// src/signals.ts
|
|
50
51
|
process.once("SIGINT", async (arg) => {
|
|
@@ -561,8 +562,17 @@ async function scan_stream_heads(streams) {
|
|
|
561
562
|
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
562
563
|
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
563
564
|
const pending_set = /* @__PURE__ */ new Set();
|
|
565
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
566
|
+
const get_regex = (source) => {
|
|
567
|
+
let re = source_regex.get(source);
|
|
568
|
+
if (!re) {
|
|
569
|
+
re = new RegExp(source);
|
|
570
|
+
source_regex.set(source, re);
|
|
571
|
+
}
|
|
572
|
+
return re;
|
|
573
|
+
};
|
|
564
574
|
await store().query_streams((position) => {
|
|
565
|
-
const source_re = position.source ?
|
|
575
|
+
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
566
576
|
for (const [stream, info] of stream_info) {
|
|
567
577
|
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
568
578
|
pending_set.add(stream);
|
|
@@ -764,10 +774,7 @@ var AutocloseController = class {
|
|
|
764
774
|
start() {
|
|
765
775
|
if (this._timer) return false;
|
|
766
776
|
this._timer = setInterval(() => {
|
|
767
|
-
this.run_once().catch((
|
|
768
|
-
this.deps.logger.error(
|
|
769
|
-
`Autoclose cycle errored: ${err instanceof Error ? err.message : String(err)}`
|
|
770
|
-
);
|
|
777
|
+
this.run_once().catch(() => {
|
|
771
778
|
});
|
|
772
779
|
}, this.deps.config.autocloseCycleMs);
|
|
773
780
|
this._timer.unref();
|
|
@@ -800,6 +807,7 @@ var AutocloseController = class {
|
|
|
800
807
|
*/
|
|
801
808
|
async run_once() {
|
|
802
809
|
if (this._running) return null;
|
|
810
|
+
if (this.deps.breaker.state(Date.now()) === "open") return null;
|
|
803
811
|
this._running = true;
|
|
804
812
|
try {
|
|
805
813
|
const correlation = close_correlation(
|
|
@@ -817,10 +825,14 @@ var AutocloseController = class {
|
|
|
817
825
|
config: this.deps.config,
|
|
818
826
|
correlation
|
|
819
827
|
});
|
|
828
|
+
this.deps.breaker.passed();
|
|
820
829
|
if (result.close_result.truncated.size > 0) {
|
|
821
830
|
this.deps.on_closed(result.close_result);
|
|
822
831
|
}
|
|
823
832
|
return result;
|
|
833
|
+
} catch (error) {
|
|
834
|
+
this.deps.breaker.failed(Date.now(), error);
|
|
835
|
+
throw error;
|
|
824
836
|
} finally {
|
|
825
837
|
this._running = false;
|
|
826
838
|
}
|
|
@@ -950,6 +962,93 @@ function classify_registry(registry, states) {
|
|
|
950
962
|
};
|
|
951
963
|
}
|
|
952
964
|
|
|
965
|
+
// src/internal/circuit-breaker.ts
|
|
966
|
+
import { z as z3 } from "zod";
|
|
967
|
+
var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
|
|
968
|
+
var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
|
|
969
|
+
var CircuitBreakerOptionsSchema = z3.object({
|
|
970
|
+
failureThreshold: z3.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
|
|
971
|
+
cooldownMs: z3.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
|
|
972
|
+
});
|
|
973
|
+
var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
|
|
974
|
+
var CircuitBreaker = class {
|
|
975
|
+
_failures = 0;
|
|
976
|
+
_opened_at;
|
|
977
|
+
_wake;
|
|
978
|
+
_threshold;
|
|
979
|
+
_cooldown_ms;
|
|
980
|
+
_hooks;
|
|
981
|
+
constructor(config2, hooks = {}) {
|
|
982
|
+
this._threshold = config2.failureThreshold;
|
|
983
|
+
this._cooldown_ms = config2.cooldownMs;
|
|
984
|
+
this._hooks = hooks;
|
|
985
|
+
}
|
|
986
|
+
/** Current state given the wall-clock `now`. */
|
|
987
|
+
state(now) {
|
|
988
|
+
if (this._opened_at === void 0) return "closed";
|
|
989
|
+
return now - this._opened_at >= this._cooldown_ms ? "half-open" : "open";
|
|
990
|
+
}
|
|
991
|
+
/** A store op passed — reset to closed and cancel any pending retry. */
|
|
992
|
+
passed() {
|
|
993
|
+
this._failures = 0;
|
|
994
|
+
this._opened_at = void 0;
|
|
995
|
+
this._clear_wake();
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* A store op failed. Opens the breaker when the consecutive-failure
|
|
999
|
+
* threshold is reached, or immediately re-opens (restarting the cooldown)
|
|
1000
|
+
* if a half-open trial just failed. Returns the resulting state.
|
|
1001
|
+
*
|
|
1002
|
+
* Callers gate on the state directly — `state(now) === "open"` means skip.
|
|
1003
|
+
* On opening, schedules the `on_retry` wake so recovery is automatic;
|
|
1004
|
+
* always surfaces the failure via `on_error`.
|
|
1005
|
+
*/
|
|
1006
|
+
failed(now, error) {
|
|
1007
|
+
const circuit = this._advance(now);
|
|
1008
|
+
if (circuit === "open") this._schedule_wake();
|
|
1009
|
+
this._hooks.on_error?.(error, circuit);
|
|
1010
|
+
return circuit;
|
|
1011
|
+
}
|
|
1012
|
+
/** Cancel the pending retry timer. Idempotent; call on shutdown. */
|
|
1013
|
+
stop() {
|
|
1014
|
+
this._clear_wake();
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Schedule the `on_retry` wake `cooldownMs` out so the breaker re-trials
|
|
1018
|
+
* the store on its own. No-op when no `on_retry` hook is wired (e.g. unit
|
|
1019
|
+
* tests of the pure state machine). The timer is `unref`'d so it never
|
|
1020
|
+
* keeps the process alive.
|
|
1021
|
+
*/
|
|
1022
|
+
_schedule_wake() {
|
|
1023
|
+
if (!this._hooks.on_retry) return;
|
|
1024
|
+
this._clear_wake();
|
|
1025
|
+
this._wake = setTimeout(() => {
|
|
1026
|
+
this._wake = void 0;
|
|
1027
|
+
this._hooks.on_retry?.();
|
|
1028
|
+
}, this._cooldown_ms);
|
|
1029
|
+
this._wake.unref();
|
|
1030
|
+
}
|
|
1031
|
+
_clear_wake() {
|
|
1032
|
+
if (this._wake) {
|
|
1033
|
+
clearTimeout(this._wake);
|
|
1034
|
+
this._wake = void 0;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/** Pure state transition for a failure — no side effects. */
|
|
1038
|
+
_advance(now) {
|
|
1039
|
+
if (this._opened_at !== void 0) {
|
|
1040
|
+
this._opened_at = now;
|
|
1041
|
+
return "open";
|
|
1042
|
+
}
|
|
1043
|
+
this._failures += 1;
|
|
1044
|
+
if (this._failures >= this._threshold) {
|
|
1045
|
+
this._opened_at = now;
|
|
1046
|
+
return "open";
|
|
1047
|
+
}
|
|
1048
|
+
return "closed";
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
|
|
953
1052
|
// src/internal/correlate-cycle.ts
|
|
954
1053
|
var CorrelateCycle = class {
|
|
955
1054
|
_checkpoint = -1;
|
|
@@ -1802,6 +1901,8 @@ var DrainController = class {
|
|
|
1802
1901
|
async drain(options = {}) {
|
|
1803
1902
|
if (!this._armed) return EMPTY_DRAIN;
|
|
1804
1903
|
if (this._locked) return EMPTY_DRAIN;
|
|
1904
|
+
if (this._deps.breaker.state(Date.now()) === "open")
|
|
1905
|
+
return EMPTY_DRAIN;
|
|
1805
1906
|
const d = this._deps.defaults ?? {};
|
|
1806
1907
|
const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
|
|
1807
1908
|
const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
|
|
@@ -1823,6 +1924,7 @@ var DrainController = class {
|
|
|
1823
1924
|
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
1824
1925
|
this._deps.lane
|
|
1825
1926
|
);
|
|
1927
|
+
this._deps.breaker.passed();
|
|
1826
1928
|
if (!cycle) {
|
|
1827
1929
|
this._armed = false;
|
|
1828
1930
|
return EMPTY_DRAIN;
|
|
@@ -1844,7 +1946,7 @@ var DrainController = class {
|
|
|
1844
1946
|
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
1845
1947
|
return { fetched, leased, acked, blocked };
|
|
1846
1948
|
} catch (error) {
|
|
1847
|
-
this._deps.
|
|
1949
|
+
this._deps.breaker.failed(Date.now(), error);
|
|
1848
1950
|
return EMPTY_DRAIN;
|
|
1849
1951
|
} finally {
|
|
1850
1952
|
this._locked = false;
|
|
@@ -2132,12 +2234,15 @@ var SettleLoop = class {
|
|
|
2132
2234
|
...correlate_query,
|
|
2133
2235
|
after: this._deps.checkpoint()
|
|
2134
2236
|
});
|
|
2237
|
+
this._deps.breaker.passed();
|
|
2135
2238
|
last_drain = await this._deps.drain(drain_options);
|
|
2136
2239
|
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
2137
2240
|
if (!made_progress) break;
|
|
2138
2241
|
}
|
|
2139
2242
|
if (last_drain) this._deps.on_settled(last_drain);
|
|
2140
|
-
})().catch((err) =>
|
|
2243
|
+
})().catch((err) => {
|
|
2244
|
+
this._deps.breaker.failed(Date.now(), err);
|
|
2245
|
+
}).finally(() => {
|
|
2141
2246
|
this._running = false;
|
|
2142
2247
|
});
|
|
2143
2248
|
}, debounceMs);
|
|
@@ -2156,6 +2261,8 @@ var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
|
2156
2261
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
2157
2262
|
var Act = class {
|
|
2158
2263
|
_emitter = new EventEmitter();
|
|
2264
|
+
/** ACT-984: orchestrator-owned circuit breaker shared by all drain lanes. */
|
|
2265
|
+
_breaker;
|
|
2159
2266
|
/** #803: gate the `Store.notify` subscription side. */
|
|
2160
2267
|
_listen;
|
|
2161
2268
|
/** #803: gate the local reaction pipeline (drain controllers, settle, correlate). */
|
|
@@ -2209,6 +2316,22 @@ var Act = class {
|
|
|
2209
2316
|
emit(event, args) {
|
|
2210
2317
|
return this._emitter.emit(event, args);
|
|
2211
2318
|
}
|
|
2319
|
+
/**
|
|
2320
|
+
* The single store-failure handler: log it, then emit the `error`
|
|
2321
|
+
* lifecycle event. Wired into the circuit breaker's `on_error`, so it
|
|
2322
|
+
* runs on every `failed()` — drain / settle / autoclose just call
|
|
2323
|
+
* `breaker.failed(now, error)` and never log or emit themselves.
|
|
2324
|
+
*
|
|
2325
|
+
* The emit is guarded against Node's `EventEmitter` contract that an
|
|
2326
|
+
* unhandled `"error"` emission is rethrown (which would crash the process
|
|
2327
|
+
* from inside the drain catch); logging is unconditional so failures are
|
|
2328
|
+
* never silent.
|
|
2329
|
+
*/
|
|
2330
|
+
_emit_error(error, circuit) {
|
|
2331
|
+
this._logger.error(error);
|
|
2332
|
+
if (this._emitter.listenerCount("error") > 0)
|
|
2333
|
+
this.emit("error", { error, circuit });
|
|
2334
|
+
}
|
|
2212
2335
|
/**
|
|
2213
2336
|
* Register a listener for a lifecycle event. The listener receives the
|
|
2214
2337
|
* event-specific payload.
|
|
@@ -2337,6 +2460,25 @@ var Act = class {
|
|
|
2337
2460
|
this._listen = options.listen !== false;
|
|
2338
2461
|
this._drain = options.drain !== false;
|
|
2339
2462
|
this._autoclose_config = resolveAutocloseConfig(options);
|
|
2463
|
+
this._breaker = new CircuitBreaker(
|
|
2464
|
+
resolveCircuitBreakerConfig(options.circuitBreaker),
|
|
2465
|
+
{
|
|
2466
|
+
on_error: (error, circuit) => this._emit_error(error, circuit),
|
|
2467
|
+
// Re-probe the store when the cooldown elapses, so recovery is
|
|
2468
|
+
// automatic even on the default lane (which has no periodic poller).
|
|
2469
|
+
// The breaker is shared by every store-polling loop (drain, the
|
|
2470
|
+
// settle correlate, autoclose), so any of them can trip it — but a
|
|
2471
|
+
// bare `drain()` only touches the store when the controller is armed,
|
|
2472
|
+
// so a trip from a correlate failure could go unprobed. `settle()`
|
|
2473
|
+
// always runs a correlate (a store query) before draining, so it
|
|
2474
|
+
// re-probes regardless of which loop opened the breaker: one success
|
|
2475
|
+
// closes it and every loop resumes on its own cadence. A failed probe
|
|
2476
|
+
// re-opens the breaker and reschedules the wake.
|
|
2477
|
+
on_retry: () => {
|
|
2478
|
+
this.settle({ debounceMs: 0 });
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
);
|
|
2340
2482
|
this._event_to_state = event_to_state;
|
|
2341
2483
|
this._event_to_lanes = event_to_lanes;
|
|
2342
2484
|
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
@@ -2355,6 +2497,7 @@ var Act = class {
|
|
|
2355
2497
|
handle_batch: this._handle_batch,
|
|
2356
2498
|
on_acked: (acked) => this.emit("acked", acked),
|
|
2357
2499
|
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
2500
|
+
breaker: this._breaker,
|
|
2358
2501
|
// Pass lane only when a true per-lane controller is active.
|
|
2359
2502
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
2360
2503
|
// adapter SQL collapses to the pre-1103 shape.
|
|
@@ -2391,12 +2534,12 @@ var Act = class {
|
|
|
2391
2534
|
);
|
|
2392
2535
|
this._settle = new SettleLoop(
|
|
2393
2536
|
{
|
|
2394
|
-
logger: this._logger,
|
|
2395
2537
|
init: () => this._correlate.init(),
|
|
2396
2538
|
checkpoint: () => this._correlate.checkpoint,
|
|
2397
2539
|
correlate: (q) => this.correlate(q),
|
|
2398
2540
|
drain: (o) => this.drain(o),
|
|
2399
|
-
on_settled: (drain) => this.emit("settled", drain)
|
|
2541
|
+
on_settled: (drain) => this.emit("settled", drain),
|
|
2542
|
+
breaker: this._breaker
|
|
2400
2543
|
},
|
|
2401
2544
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2402
2545
|
);
|
|
@@ -2418,7 +2561,8 @@ var Act = class {
|
|
|
2418
2561
|
config: this._autoclose_config,
|
|
2419
2562
|
correlator: this._correlator,
|
|
2420
2563
|
close_actor: { id: "$autoclose", name: "autoclose" },
|
|
2421
|
-
on_closed: (result) => this.emit("closed", result)
|
|
2564
|
+
on_closed: (result) => this.emit("closed", result),
|
|
2565
|
+
breaker: this._breaker
|
|
2422
2566
|
}) : void 0;
|
|
2423
2567
|
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2424
2568
|
dispose(() => this.shutdown());
|
|
@@ -2441,6 +2585,7 @@ var Act = class {
|
|
|
2441
2585
|
this._emitter.removeAllListeners();
|
|
2442
2586
|
this.stop_correlations();
|
|
2443
2587
|
this.stop_settling();
|
|
2588
|
+
this._breaker.stop();
|
|
2444
2589
|
for (const c of this._drain_controllers.values()) c.stop();
|
|
2445
2590
|
const disposer = await this._notify_disposer;
|
|
2446
2591
|
if (disposer) await disposer();
|
|
@@ -3957,6 +4102,7 @@ export {
|
|
|
3957
4102
|
REDACTED,
|
|
3958
4103
|
SHREDDED,
|
|
3959
4104
|
SNAP_EVENT,
|
|
4105
|
+
StoreError,
|
|
3960
4106
|
StreamClosedError,
|
|
3961
4107
|
TOMBSTONE_EVENT,
|
|
3962
4108
|
TargetSchema,
|