actly 1.2.0 → 1.3.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/CHANGELOG.md +234 -0
- package/MIGRATION.md +141 -0
- package/README.md +326 -218
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +406 -220
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +405 -222
- package/dist/core/executor.cjs +0 -27
- package/dist/core/executor.d.ts +0 -41
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +0 -28
- package/dist/core/health.cjs +146 -17
- package/dist/core/health.d.ts +20 -5
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +140 -18
- package/dist/core/shutdown.cjs +19 -8
- package/dist/core/shutdown.d.ts +1 -4
- package/dist/core/shutdown.d.ts.map +1 -1
- package/dist/core/shutdown.js +18 -9
- package/dist/core/tenant.cjs +61 -19
- package/dist/core/tenant.d.ts +5 -10
- package/dist/core/tenant.d.ts.map +1 -1
- package/dist/core/tenant.js +61 -20
- package/dist/errors.cjs +70 -103
- package/dist/errors.d.ts +33 -90
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +66 -103
- package/dist/index.cjs +25 -22
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/observability.cjs +0 -39
- package/dist/observability.d.ts +19 -78
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +0 -40
- package/dist/policies/bulkhead.cjs +44 -4
- package/dist/policies/bulkhead.d.ts +0 -0
- package/dist/policies/bulkhead.d.ts.map +1 -1
- package/dist/policies/bulkhead.js +44 -5
- package/dist/policies/cache.cjs +22 -142
- package/dist/policies/cache.d.ts +0 -46
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +22 -143
- package/dist/policies/circuitBreaker.cjs +125 -31
- package/dist/policies/circuitBreaker.d.ts +0 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -1
- package/dist/policies/circuitBreaker.js +125 -32
- package/dist/policies/dedupe.cjs +19 -66
- package/dist/policies/dedupe.d.ts +0 -23
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +19 -67
- package/dist/policies/noop.cjs +6 -0
- package/dist/policies/noop.d.ts +3 -0
- package/dist/policies/noop.d.ts.map +1 -0
- package/dist/policies/noop.js +3 -0
- package/dist/policies/rateLimit.cjs +15 -5
- package/dist/policies/rateLimit.d.ts +0 -0
- package/dist/policies/rateLimit.d.ts.map +1 -1
- package/dist/policies/rateLimit.js +15 -6
- package/dist/policies/retry.cjs +100 -65
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +100 -66
- package/dist/policies/timeout.cjs +32 -60
- package/dist/policies/timeout.d.ts +0 -13
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +32 -71
- package/dist/stores/base.cjs +0 -10
- package/dist/stores/base.d.ts +4 -67
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +0 -11
- package/dist/stores/memory.cjs +75 -88
- package/dist/stores/memory.d.ts +7 -74
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +75 -89
- package/dist/testing/index.cjs +56 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +52 -0
- package/dist/types/index.cjs +0 -1
- package/dist/types/index.d.ts +17 -307
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +14 -111
- package/dist/utils/abort.d.ts +3 -74
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +14 -112
- package/dist/utils/abortPool.cjs +1 -2
- package/dist/utils/abortPool.d.ts +0 -0
- package/dist/utils/abortPool.d.ts.map +1 -1
- package/dist/utils/abortPool.js +1 -3
- package/dist/utils/backoff.cjs +3 -21
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +3 -22
- package/dist/utils/decorator.cjs +44 -0
- package/dist/utils/decorator.d.ts +3 -0
- package/dist/utils/decorator.d.ts.map +1 -0
- package/dist/utils/decorator.js +41 -0
- package/dist/utils/key.cjs +13 -45
- package/dist/utils/key.d.ts +0 -27
- package/dist/utils/key.d.ts.map +1 -1
- package/dist/utils/key.js +13 -46
- package/dist/utils/limits.cjs +9 -27
- package/dist/utils/limits.d.ts +9 -27
- package/dist/utils/limits.d.ts.map +1 -1
- package/dist/utils/limits.js +9 -28
- package/dist/utils/safeCall.cjs +28 -0
- package/dist/utils/safeCall.d.ts +2 -0
- package/dist/utils/safeCall.d.ts.map +1 -0
- package/dist/utils/safeCall.js +25 -0
- package/dist/utils/sanitize.cjs +22 -1
- package/dist/utils/sanitize.d.ts +0 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +22 -2
- package/dist/utils/validate.cjs +81 -18
- package/dist/utils/validate.d.ts +6 -19
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +81 -19
- package/package.json +38 -4
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/core/health.js.map +0 -1
- package/dist/core/shutdown.js.map +0 -1
- package/dist/core/tenant.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observability.js.map +0 -1
- package/dist/policies/bulkhead.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/circuitBreaker.js.map +0 -1
- package/dist/policies/dedupe.js.map +0 -1
- package/dist/policies/rateLimit.js.map +0 -1
- package/dist/policies/retry.js.map +0 -1
- package/dist/policies/timeout.js.map +0 -1
- package/dist/state/store.cjs +0 -14
- package/dist/state/store.d.ts +0 -11
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -11
- package/dist/state/store.js.map +0 -1
- package/dist/stores/base.js.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/abort.js.map +0 -1
- package/dist/utils/abortPool.js.map +0 -1
- package/dist/utils/backoff.js.map +0 -1
- package/dist/utils/key.js.map +0 -1
- package/dist/utils/limits.js.map +0 -1
- package/dist/utils/sanitize.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/core/executor.cjs
CHANGED
|
@@ -3,31 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.REQUIRES_SYNC_STORE = void 0;
|
|
4
4
|
exports.execute = execute;
|
|
5
5
|
const base_js_1 = require("../stores/base.js");
|
|
6
|
-
/**
|
|
7
|
-
* Symbol stamped onto `PolicyApplier` functions by `dedupePolicy`.
|
|
8
|
-
*
|
|
9
|
-
* Lets `execute()` detect a dedupe policy without importing the policy
|
|
10
|
-
* module (which would create a circular dep) or doing fragile name-sniffing.
|
|
11
|
-
*/
|
|
12
6
|
exports.REQUIRES_SYNC_STORE = Symbol('actly.requiresSyncStore');
|
|
13
|
-
/**
|
|
14
|
-
* Pure execution engine.
|
|
15
|
-
*
|
|
16
|
-
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
17
|
-
* — a type alias defined in `/types`. Policy implementations live in
|
|
18
|
-
* `/policies` and are wired in `core/act.ts`.
|
|
19
|
-
*
|
|
20
|
-
* # Public API
|
|
21
|
-
*
|
|
22
|
-
* Exported so consumers can build custom policy chains with explicit stores
|
|
23
|
-
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
24
|
-
* module-level default store is wrong).
|
|
25
|
-
*/
|
|
26
7
|
async function execute(input) {
|
|
27
|
-
// Guard: if any policy in the chain requires a sync store, the provided
|
|
28
|
-
// store must be synchronous. An async store + dedupePolicy is a silent
|
|
29
|
-
// correctness failure — catch it here rather than letting it produce
|
|
30
|
-
// subtly wrong dedupe behaviour at runtime.
|
|
31
8
|
const needsSync = input.policies.some(p => p[exports.REQUIRES_SYNC_STORE]);
|
|
32
9
|
if (needsSync && !(0, base_js_1.isSyncStore)(input.store)) {
|
|
33
10
|
throw new Error('Actly: dedupePolicy requires a SyncStateStore (store._sync === true). ' +
|
|
@@ -38,12 +15,8 @@ async function execute(input) {
|
|
|
38
15
|
key: input.key,
|
|
39
16
|
store: input.store,
|
|
40
17
|
meta: input.meta,
|
|
41
|
-
// Thread observability through. Policies read this lazily.
|
|
42
18
|
observability: input.observability,
|
|
43
19
|
};
|
|
44
|
-
// Build the call chain from inside out.
|
|
45
|
-
// reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
|
|
46
20
|
const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
|
|
47
|
-
// Call the outermost wrapper with the root signal.
|
|
48
21
|
return wrapped(input.signal);
|
|
49
22
|
}
|
package/dist/core/executor.d.ts
CHANGED
|
@@ -1,54 +1,13 @@
|
|
|
1
1
|
import type { ActFn, PolicyApplier, AnyStateStore, RunMeta, ObservabilityContext } from '../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Symbol stamped onto `PolicyApplier` functions by `dedupePolicy`.
|
|
4
|
-
*
|
|
5
|
-
* Lets `execute()` detect a dedupe policy without importing the policy
|
|
6
|
-
* module (which would create a circular dep) or doing fragile name-sniffing.
|
|
7
|
-
*/
|
|
8
2
|
export declare const REQUIRES_SYNC_STORE: unique symbol;
|
|
9
3
|
export interface ExecutorInput<T> {
|
|
10
4
|
key: string;
|
|
11
5
|
fn: ActFn<T>;
|
|
12
|
-
/**
|
|
13
|
-
* Policies ordered outermost -> innermost.
|
|
14
|
-
* `policies[0]` intercepts first; `policies[last]` is closest to `fn`.
|
|
15
|
-
*
|
|
16
|
-
* Canonical order: `[totalTimeout, cache, dedupe, retry, timeout]`
|
|
17
|
-
* totalTimeout -> hard wall-clock budget over the entire operation
|
|
18
|
-
* cache -> a hit skips everything below it
|
|
19
|
-
* dedupe -> collapses concurrent callers before retry fires
|
|
20
|
-
* retry -> owns the attempt loop
|
|
21
|
-
* timeout -> each individual attempt races against the clock
|
|
22
|
-
*/
|
|
23
6
|
policies: ReadonlyArray<PolicyApplier<T>>;
|
|
24
7
|
store: AnyStateStore;
|
|
25
8
|
meta: RunMeta;
|
|
26
|
-
/**
|
|
27
|
-
* Root AbortSignal for the operation. Propagated inward through the
|
|
28
|
-
* policy chain: each policy receives it as the `signal` argument to
|
|
29
|
-
* its wrapped `ActFn`. The outermost policy may layer its own signal
|
|
30
|
-
* (e.g. `totalTimeoutPolicy` arms a timer) and pass the composite
|
|
31
|
-
* inward.
|
|
32
|
-
*/
|
|
33
9
|
signal: AbortSignal;
|
|
34
|
-
/**
|
|
35
|
-
* Observability context. When present, policies emit events
|
|
36
|
-
* via the hooks. When absent (the common case), zero overhead.
|
|
37
|
-
*/
|
|
38
10
|
observability?: ObservabilityContext;
|
|
39
11
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Pure execution engine.
|
|
42
|
-
*
|
|
43
|
-
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
44
|
-
* — a type alias defined in `/types`. Policy implementations live in
|
|
45
|
-
* `/policies` and are wired in `core/act.ts`.
|
|
46
|
-
*
|
|
47
|
-
* # Public API
|
|
48
|
-
*
|
|
49
|
-
* Exported so consumers can build custom policy chains with explicit stores
|
|
50
|
-
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
51
|
-
* module-level default store is wrong).
|
|
52
|
-
*/
|
|
53
12
|
export declare function execute<T>(input: ExecutorInput<T>): Promise<T>;
|
|
54
13
|
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,aAAa,EAEb,aAAa,EACb,OAAO,EACP,oBAAoB,EACrB,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,aAAa,EAEb,aAAa,EACb,OAAO,EACP,oBAAoB,EACrB,MAAM,mBAAmB,CAAA;AAQ1B,eAAO,MAAM,mBAAmB,eAAoC,CAAA;AAEpE,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,GAAG,EAAO,MAAM,CAAA;IAChB,EAAE,EAAQ,KAAK,CAAC,CAAC,CAAC,CAAA;IAYlB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,KAAK,EAAK,aAAa,CAAA;IACvB,IAAI,EAAM,OAAO,CAAA;IAOjB,MAAM,EAAI,WAAW,CAAA;IAKrB,aAAa,CAAC,EAAE,oBAAoB,CAAA;CACrC;AAWD,wBAAsB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA+BpE"}
|
package/dist/core/executor.js
CHANGED
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
import { isSyncStore } from '../stores/base.js';
|
|
2
|
-
/**
|
|
3
|
-
* Symbol stamped onto `PolicyApplier` functions by `dedupePolicy`.
|
|
4
|
-
*
|
|
5
|
-
* Lets `execute()` detect a dedupe policy without importing the policy
|
|
6
|
-
* module (which would create a circular dep) or doing fragile name-sniffing.
|
|
7
|
-
*/
|
|
8
2
|
export const REQUIRES_SYNC_STORE = Symbol('actly.requiresSyncStore');
|
|
9
|
-
/**
|
|
10
|
-
* Pure execution engine.
|
|
11
|
-
*
|
|
12
|
-
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
13
|
-
* — a type alias defined in `/types`. Policy implementations live in
|
|
14
|
-
* `/policies` and are wired in `core/act.ts`.
|
|
15
|
-
*
|
|
16
|
-
* # Public API
|
|
17
|
-
*
|
|
18
|
-
* Exported so consumers can build custom policy chains with explicit stores
|
|
19
|
-
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
20
|
-
* module-level default store is wrong).
|
|
21
|
-
*/
|
|
22
3
|
export async function execute(input) {
|
|
23
|
-
// Guard: if any policy in the chain requires a sync store, the provided
|
|
24
|
-
// store must be synchronous. An async store + dedupePolicy is a silent
|
|
25
|
-
// correctness failure — catch it here rather than letting it produce
|
|
26
|
-
// subtly wrong dedupe behaviour at runtime.
|
|
27
4
|
const needsSync = input.policies.some(p => p[REQUIRES_SYNC_STORE]);
|
|
28
5
|
if (needsSync && !isSyncStore(input.store)) {
|
|
29
6
|
throw new Error('Actly: dedupePolicy requires a SyncStateStore (store._sync === true). ' +
|
|
@@ -34,13 +11,8 @@ export async function execute(input) {
|
|
|
34
11
|
key: input.key,
|
|
35
12
|
store: input.store,
|
|
36
13
|
meta: input.meta,
|
|
37
|
-
// Thread observability through. Policies read this lazily.
|
|
38
14
|
observability: input.observability,
|
|
39
15
|
};
|
|
40
|
-
// Build the call chain from inside out.
|
|
41
|
-
// reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
|
|
42
16
|
const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
|
|
43
|
-
// Call the outermost wrapper with the root signal.
|
|
44
17
|
return wrapped(input.signal);
|
|
45
18
|
}
|
|
46
|
-
//# sourceMappingURL=executor.js.map
|
package/dist/core/health.cjs
CHANGED
|
@@ -1,34 +1,163 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerStoreScope = registerStoreScope;
|
|
4
|
+
exports.resolveStoreScope = resolveStoreScope;
|
|
3
5
|
exports.registerInflight = registerInflight;
|
|
4
6
|
exports.unregisterInflight = unregisterInflight;
|
|
7
|
+
exports.enableWatchdog = enableWatchdog;
|
|
8
|
+
exports.registerWatchdogHooks = registerWatchdogHooks;
|
|
9
|
+
exports.unregisterWatchdogHooks = unregisterWatchdogHooks;
|
|
10
|
+
exports.disableWatchdog = disableWatchdog;
|
|
5
11
|
exports.recordError = recordError;
|
|
6
12
|
exports.recordSuccess = recordSuccess;
|
|
7
13
|
exports.createHealthCheck = createHealthCheck;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
+
const limits_js_1 = require("../utils/limits.js");
|
|
15
|
+
const errors_js_1 = require("../errors.js");
|
|
16
|
+
const safeCall_js_1 = require("../utils/safeCall.js");
|
|
17
|
+
const storeScopes = new WeakMap();
|
|
18
|
+
function registerStoreScope(store, scope) {
|
|
19
|
+
storeScopes.set(store, scope);
|
|
14
20
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
21
|
+
function resolveStoreScope(store) {
|
|
22
|
+
return storeScopes.get(store);
|
|
17
23
|
}
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
const healthStates = new Map();
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
const INFLIGHT_LIMIT_DISABLED = process.env.ACTLY_NO_INFLIGHT_LIMIT === '1' ||
|
|
27
|
+
process.env.ACTLY_NO_INFLIGHT_LIMIT === 'true';
|
|
28
|
+
const INFLIGHT_LIMIT = limits_js_1.LIMITS.MAX_GLOBAL_INFLIGHT;
|
|
29
|
+
let globalInflightCount = 0;
|
|
30
|
+
let inflightBusySince;
|
|
31
|
+
let watchdogTimer;
|
|
32
|
+
let watchdogThresholdMs = 60_000;
|
|
33
|
+
const watchdogHooks = new Set();
|
|
34
|
+
let watchdogFiredForBusySince;
|
|
35
|
+
function noteInflightUp() {
|
|
36
|
+
if (inflightBusySince === undefined) {
|
|
37
|
+
inflightBusySince = Date.now();
|
|
38
|
+
}
|
|
20
39
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
40
|
+
function noteInflightDown() {
|
|
41
|
+
if (globalInflightCount === 0) {
|
|
42
|
+
inflightBusySince = undefined;
|
|
43
|
+
watchdogFiredForBusySince = undefined;
|
|
44
|
+
}
|
|
23
45
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
46
|
+
function getState(scope) {
|
|
47
|
+
let s = healthStates.get(scope);
|
|
48
|
+
if (!s) {
|
|
49
|
+
s = { inflight: 0 };
|
|
50
|
+
healthStates.set(scope, s);
|
|
51
|
+
}
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
function registerInflight(scope) {
|
|
55
|
+
if (!INFLIGHT_LIMIT_DISABLED && globalInflightCount >= INFLIGHT_LIMIT) {
|
|
56
|
+
throw new errors_js_1.ResourceExhaustedError(globalInflightCount, INFLIGHT_LIMIT);
|
|
57
|
+
}
|
|
58
|
+
globalInflightCount++;
|
|
59
|
+
noteInflightUp();
|
|
60
|
+
getState(scope).inflight++;
|
|
61
|
+
}
|
|
62
|
+
function unregisterInflight(scope) {
|
|
63
|
+
globalInflightCount = Math.max(0, globalInflightCount - 1);
|
|
64
|
+
noteInflightDown();
|
|
65
|
+
const s = getState(scope);
|
|
66
|
+
s.inflight = Math.max(0, s.inflight - 1);
|
|
67
|
+
if (s.inflight === 0 && s.lastError === undefined && scope !== 'default') {
|
|
68
|
+
healthStates.delete(scope);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function enableWatchdog(thresholdMs = 60_000, hooks) {
|
|
72
|
+
const prevThreshold = watchdogThresholdMs;
|
|
73
|
+
watchdogThresholdMs = thresholdMs;
|
|
74
|
+
if (hooks)
|
|
75
|
+
watchdogHooks.add(hooks);
|
|
76
|
+
if (watchdogTimer && thresholdMs === prevThreshold)
|
|
77
|
+
return;
|
|
78
|
+
if (watchdogTimer) {
|
|
79
|
+
clearInterval(watchdogTimer);
|
|
80
|
+
watchdogTimer = undefined;
|
|
81
|
+
}
|
|
82
|
+
const intervalMs = Math.max(50, Math.floor(thresholdMs / 4));
|
|
83
|
+
watchdogTimer = setInterval(() => {
|
|
84
|
+
if (globalInflightCount === 0)
|
|
85
|
+
return;
|
|
86
|
+
if (inflightBusySince === undefined)
|
|
87
|
+
return;
|
|
88
|
+
const elapsed = Date.now() - inflightBusySince;
|
|
89
|
+
if (elapsed < watchdogThresholdMs)
|
|
90
|
+
return;
|
|
91
|
+
if (watchdogFiredForBusySince === inflightBusySince)
|
|
92
|
+
return;
|
|
93
|
+
watchdogFiredForBusySince = inflightBusySince;
|
|
94
|
+
const event = {
|
|
95
|
+
type: 'watchdog',
|
|
96
|
+
key: '<unknown>',
|
|
97
|
+
traceId: '<watchdog>',
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
elapsedMs: elapsed,
|
|
100
|
+
scope: '<process>',
|
|
101
|
+
};
|
|
102
|
+
for (const h of watchdogHooks) {
|
|
103
|
+
(0, safeCall_js_1.safeCall)(h.onWatchdog, event);
|
|
104
|
+
}
|
|
105
|
+
}, intervalMs);
|
|
106
|
+
const t = watchdogTimer;
|
|
107
|
+
if (typeof t.unref === 'function')
|
|
108
|
+
t.unref();
|
|
109
|
+
}
|
|
110
|
+
function registerWatchdogHooks(hooks) {
|
|
111
|
+
watchdogHooks.add(hooks);
|
|
112
|
+
}
|
|
113
|
+
function unregisterWatchdogHooks(hooks) {
|
|
114
|
+
watchdogHooks.delete(hooks);
|
|
115
|
+
}
|
|
116
|
+
function disableWatchdog() {
|
|
117
|
+
if (watchdogTimer) {
|
|
118
|
+
clearInterval(watchdogTimer);
|
|
119
|
+
watchdogTimer = undefined;
|
|
120
|
+
}
|
|
121
|
+
watchdogHooks.clear();
|
|
122
|
+
}
|
|
123
|
+
function recordError(scope, code, message) {
|
|
124
|
+
const s = getState(scope);
|
|
125
|
+
s.lastError = { code, message, timestamp: Date.now() };
|
|
126
|
+
}
|
|
127
|
+
function recordSuccess(scope) {
|
|
128
|
+
getState(scope).lastSuccessAt = Date.now();
|
|
129
|
+
}
|
|
130
|
+
function createHealthCheck(store, options) {
|
|
131
|
+
const scope = options?.scope ?? resolveStoreScope(store) ?? 'default';
|
|
132
|
+
const probeIntervalMs = options?.probeIntervalMs;
|
|
133
|
+
let probeTimer;
|
|
134
|
+
if (probeIntervalMs && probeIntervalMs > 0) {
|
|
135
|
+
probeTimer = setInterval(() => {
|
|
136
|
+
const s = healthStates.get(scope);
|
|
137
|
+
if (s && s.inflight > 0) {
|
|
138
|
+
console.warn(`Actly: health probe detected ${s.inflight} in-flight calls in scope "${scope}" ` +
|
|
139
|
+
`at ${new Date().toISOString()}. If this persists, a fn may be hung.`);
|
|
140
|
+
}
|
|
141
|
+
}, probeIntervalMs);
|
|
142
|
+
const t = probeTimer;
|
|
143
|
+
if (typeof t.unref === 'function')
|
|
144
|
+
t.unref();
|
|
145
|
+
}
|
|
146
|
+
const checkFn = () => {
|
|
147
|
+
const s = healthStates.get(scope);
|
|
26
148
|
return {
|
|
27
149
|
storeSize: store.size(),
|
|
28
|
-
pendingInflight:
|
|
150
|
+
pendingInflight: s?.inflight ?? 0,
|
|
29
151
|
uptimeMs: Date.now() - startTime,
|
|
30
|
-
lastError:
|
|
31
|
-
lastSuccessAt:
|
|
152
|
+
lastError: s?.lastError,
|
|
153
|
+
lastSuccessAt: s?.lastSuccessAt,
|
|
32
154
|
};
|
|
33
155
|
};
|
|
156
|
+
checkFn.dispose = () => {
|
|
157
|
+
if (probeTimer !== undefined) {
|
|
158
|
+
clearInterval(probeTimer);
|
|
159
|
+
probeTimer = undefined;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return checkFn;
|
|
34
163
|
}
|
package/dist/core/health.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { InMemoryStore } from '../stores/memory.js';
|
|
2
|
+
import type { AnyStateStore } from '../types/index.js';
|
|
3
|
+
import type { ObservabilityHooks } from '../observability.js';
|
|
4
|
+
export declare function registerStoreScope(store: AnyStateStore, scope: string): void;
|
|
5
|
+
export declare function resolveStoreScope(store: AnyStateStore): string | undefined;
|
|
2
6
|
export interface HealthStatus {
|
|
3
7
|
storeSize: number;
|
|
4
8
|
pendingInflight: number;
|
|
@@ -10,9 +14,20 @@ export interface HealthStatus {
|
|
|
10
14
|
};
|
|
11
15
|
lastSuccessAt?: number;
|
|
12
16
|
}
|
|
13
|
-
export declare function registerInflight(
|
|
14
|
-
export declare function unregisterInflight(
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
17
|
+
export declare function registerInflight(scope: string): void;
|
|
18
|
+
export declare function unregisterInflight(scope: string): void;
|
|
19
|
+
export declare function enableWatchdog(thresholdMs?: number, hooks?: ObservabilityHooks): void;
|
|
20
|
+
export declare function registerWatchdogHooks(hooks: ObservabilityHooks): void;
|
|
21
|
+
export declare function unregisterWatchdogHooks(hooks: ObservabilityHooks): void;
|
|
22
|
+
export declare function disableWatchdog(): void;
|
|
23
|
+
export declare function recordError(scope: string, code: string, message: string): void;
|
|
24
|
+
export declare function recordSuccess(scope: string): void;
|
|
25
|
+
export interface HealthCheckFn {
|
|
26
|
+
(): HealthStatus;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function createHealthCheck(store: InMemoryStore, options?: {
|
|
30
|
+
scope?: string;
|
|
31
|
+
probeIntervalMs?: number;
|
|
32
|
+
}): HealthCheckFn;
|
|
18
33
|
//# sourceMappingURL=health.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/core/health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/core/health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAGtD,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,qBAAqB,CAAA;AAkB5E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE5E;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAE1E;AAOD,MAAM,WAAW,YAAY;IAE3B,SAAS,EAAE,MAAM,CAAA;IAEjB,eAAe,EAAE,MAAM,CAAA;IAEvB,QAAQ,EAAE,MAAM,CAAA;IAEhB,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAEhE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAwED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAUpD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAYtD;AAgBD,wBAAgB,cAAc,CAC5B,WAAW,SAAS,EACpB,KAAK,CAAC,EAAE,kBAAkB,GACzB,IAAI,CAgDN;AAOD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAErE;AAUD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAEvE;AAMD,wBAAgB,eAAe,IAAI,IAAI,CAMtC;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAG9E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAMD,MAAM,WAAW,aAAa;IAC5B,IAAI,YAAY,CAAA;IAEhB,OAAO,IAAI,IAAI,CAAA;CAChB;AAmBD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,aAAa,CA2Cf"}
|
package/dist/core/health.js
CHANGED
|
@@ -1,28 +1,150 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export function
|
|
6
|
-
|
|
1
|
+
import { LIMITS } from '../utils/limits.js';
|
|
2
|
+
import { ResourceExhaustedError } from '../errors.js';
|
|
3
|
+
import { safeCall } from '../utils/safeCall.js';
|
|
4
|
+
const storeScopes = new WeakMap();
|
|
5
|
+
export function registerStoreScope(store, scope) {
|
|
6
|
+
storeScopes.set(store, scope);
|
|
7
7
|
}
|
|
8
|
-
export function
|
|
9
|
-
|
|
8
|
+
export function resolveStoreScope(store) {
|
|
9
|
+
return storeScopes.get(store);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const healthStates = new Map();
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
const INFLIGHT_LIMIT_DISABLED = process.env.ACTLY_NO_INFLIGHT_LIMIT === '1' ||
|
|
14
|
+
process.env.ACTLY_NO_INFLIGHT_LIMIT === 'true';
|
|
15
|
+
const INFLIGHT_LIMIT = LIMITS.MAX_GLOBAL_INFLIGHT;
|
|
16
|
+
let globalInflightCount = 0;
|
|
17
|
+
let inflightBusySince;
|
|
18
|
+
let watchdogTimer;
|
|
19
|
+
let watchdogThresholdMs = 60_000;
|
|
20
|
+
const watchdogHooks = new Set();
|
|
21
|
+
let watchdogFiredForBusySince;
|
|
22
|
+
function noteInflightUp() {
|
|
23
|
+
if (inflightBusySince === undefined) {
|
|
24
|
+
inflightBusySince = Date.now();
|
|
25
|
+
}
|
|
13
26
|
}
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
function noteInflightDown() {
|
|
28
|
+
if (globalInflightCount === 0) {
|
|
29
|
+
inflightBusySince = undefined;
|
|
30
|
+
watchdogFiredForBusySince = undefined;
|
|
31
|
+
}
|
|
16
32
|
}
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
function getState(scope) {
|
|
34
|
+
let s = healthStates.get(scope);
|
|
35
|
+
if (!s) {
|
|
36
|
+
s = { inflight: 0 };
|
|
37
|
+
healthStates.set(scope, s);
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
export function registerInflight(scope) {
|
|
42
|
+
if (!INFLIGHT_LIMIT_DISABLED && globalInflightCount >= INFLIGHT_LIMIT) {
|
|
43
|
+
throw new ResourceExhaustedError(globalInflightCount, INFLIGHT_LIMIT);
|
|
44
|
+
}
|
|
45
|
+
globalInflightCount++;
|
|
46
|
+
noteInflightUp();
|
|
47
|
+
getState(scope).inflight++;
|
|
48
|
+
}
|
|
49
|
+
export function unregisterInflight(scope) {
|
|
50
|
+
globalInflightCount = Math.max(0, globalInflightCount - 1);
|
|
51
|
+
noteInflightDown();
|
|
52
|
+
const s = getState(scope);
|
|
53
|
+
s.inflight = Math.max(0, s.inflight - 1);
|
|
54
|
+
if (s.inflight === 0 && s.lastError === undefined && scope !== 'default') {
|
|
55
|
+
healthStates.delete(scope);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function enableWatchdog(thresholdMs = 60_000, hooks) {
|
|
59
|
+
const prevThreshold = watchdogThresholdMs;
|
|
60
|
+
watchdogThresholdMs = thresholdMs;
|
|
61
|
+
if (hooks)
|
|
62
|
+
watchdogHooks.add(hooks);
|
|
63
|
+
if (watchdogTimer && thresholdMs === prevThreshold)
|
|
64
|
+
return;
|
|
65
|
+
if (watchdogTimer) {
|
|
66
|
+
clearInterval(watchdogTimer);
|
|
67
|
+
watchdogTimer = undefined;
|
|
68
|
+
}
|
|
69
|
+
const intervalMs = Math.max(50, Math.floor(thresholdMs / 4));
|
|
70
|
+
watchdogTimer = setInterval(() => {
|
|
71
|
+
if (globalInflightCount === 0)
|
|
72
|
+
return;
|
|
73
|
+
if (inflightBusySince === undefined)
|
|
74
|
+
return;
|
|
75
|
+
const elapsed = Date.now() - inflightBusySince;
|
|
76
|
+
if (elapsed < watchdogThresholdMs)
|
|
77
|
+
return;
|
|
78
|
+
if (watchdogFiredForBusySince === inflightBusySince)
|
|
79
|
+
return;
|
|
80
|
+
watchdogFiredForBusySince = inflightBusySince;
|
|
81
|
+
const event = {
|
|
82
|
+
type: 'watchdog',
|
|
83
|
+
key: '<unknown>',
|
|
84
|
+
traceId: '<watchdog>',
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
elapsedMs: elapsed,
|
|
87
|
+
scope: '<process>',
|
|
88
|
+
};
|
|
89
|
+
for (const h of watchdogHooks) {
|
|
90
|
+
safeCall(h.onWatchdog, event);
|
|
91
|
+
}
|
|
92
|
+
}, intervalMs);
|
|
93
|
+
const t = watchdogTimer;
|
|
94
|
+
if (typeof t.unref === 'function')
|
|
95
|
+
t.unref();
|
|
96
|
+
}
|
|
97
|
+
export function registerWatchdogHooks(hooks) {
|
|
98
|
+
watchdogHooks.add(hooks);
|
|
99
|
+
}
|
|
100
|
+
export function unregisterWatchdogHooks(hooks) {
|
|
101
|
+
watchdogHooks.delete(hooks);
|
|
102
|
+
}
|
|
103
|
+
export function disableWatchdog() {
|
|
104
|
+
if (watchdogTimer) {
|
|
105
|
+
clearInterval(watchdogTimer);
|
|
106
|
+
watchdogTimer = undefined;
|
|
107
|
+
}
|
|
108
|
+
watchdogHooks.clear();
|
|
109
|
+
}
|
|
110
|
+
export function recordError(scope, code, message) {
|
|
111
|
+
const s = getState(scope);
|
|
112
|
+
s.lastError = { code, message, timestamp: Date.now() };
|
|
113
|
+
}
|
|
114
|
+
export function recordSuccess(scope) {
|
|
115
|
+
getState(scope).lastSuccessAt = Date.now();
|
|
116
|
+
}
|
|
117
|
+
export function createHealthCheck(store, options) {
|
|
118
|
+
const scope = options?.scope ?? resolveStoreScope(store) ?? 'default';
|
|
119
|
+
const probeIntervalMs = options?.probeIntervalMs;
|
|
120
|
+
let probeTimer;
|
|
121
|
+
if (probeIntervalMs && probeIntervalMs > 0) {
|
|
122
|
+
probeTimer = setInterval(() => {
|
|
123
|
+
const s = healthStates.get(scope);
|
|
124
|
+
if (s && s.inflight > 0) {
|
|
125
|
+
console.warn(`Actly: health probe detected ${s.inflight} in-flight calls in scope "${scope}" ` +
|
|
126
|
+
`at ${new Date().toISOString()}. If this persists, a fn may be hung.`);
|
|
127
|
+
}
|
|
128
|
+
}, probeIntervalMs);
|
|
129
|
+
const t = probeTimer;
|
|
130
|
+
if (typeof t.unref === 'function')
|
|
131
|
+
t.unref();
|
|
132
|
+
}
|
|
133
|
+
const checkFn = () => {
|
|
134
|
+
const s = healthStates.get(scope);
|
|
19
135
|
return {
|
|
20
136
|
storeSize: store.size(),
|
|
21
|
-
pendingInflight:
|
|
137
|
+
pendingInflight: s?.inflight ?? 0,
|
|
22
138
|
uptimeMs: Date.now() - startTime,
|
|
23
|
-
lastError:
|
|
24
|
-
lastSuccessAt:
|
|
139
|
+
lastError: s?.lastError,
|
|
140
|
+
lastSuccessAt: s?.lastSuccessAt,
|
|
25
141
|
};
|
|
26
142
|
};
|
|
143
|
+
checkFn.dispose = () => {
|
|
144
|
+
if (probeTimer !== undefined) {
|
|
145
|
+
clearInterval(probeTimer);
|
|
146
|
+
probeTimer = undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return checkFn;
|
|
27
150
|
}
|
|
28
|
-
//# sourceMappingURL=health.js.map
|
package/dist/core/shutdown.cjs
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerDrainable = registerDrainable;
|
|
4
4
|
exports.unregisterDrainable = unregisterDrainable;
|
|
5
5
|
exports.drain = drain;
|
|
6
|
+
exports.drainAll = drainAll;
|
|
6
7
|
const drainStates = new Map();
|
|
7
8
|
function getState(scope) {
|
|
8
9
|
let s = drainStates.get(scope);
|
|
@@ -22,16 +23,21 @@ function unregisterDrainable(scope = 'default') {
|
|
|
22
23
|
for (const r of s.resolvers)
|
|
23
24
|
r();
|
|
24
25
|
s.resolvers = [];
|
|
26
|
+
if (scope !== 'default') {
|
|
27
|
+
drainStates.delete(scope);
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Wait for all in-flight act() calls in this scope to settle.
|
|
29
|
-
* Returns true if all settled within timeoutMs, false if timed out.
|
|
30
|
-
*/
|
|
31
31
|
async function drain(timeoutMs, scope = 'default') {
|
|
32
|
-
const s =
|
|
33
|
-
if (s
|
|
32
|
+
const s = drainStates.get(scope);
|
|
33
|
+
if (!s)
|
|
34
|
+
return true;
|
|
35
|
+
if (s.inflight === 0) {
|
|
36
|
+
if (scope !== 'default' && s.resolvers.length === 0) {
|
|
37
|
+
drainStates.delete(scope);
|
|
38
|
+
}
|
|
34
39
|
return true;
|
|
40
|
+
}
|
|
35
41
|
return new Promise((resolve) => {
|
|
36
42
|
let resolved = false;
|
|
37
43
|
const resolver = () => {
|
|
@@ -39,7 +45,6 @@ async function drain(timeoutMs, scope = 'default') {
|
|
|
39
45
|
return;
|
|
40
46
|
resolved = true;
|
|
41
47
|
clearTimeout(timer);
|
|
42
|
-
// Remove this resolver from the array
|
|
43
48
|
const idx = s.resolvers.indexOf(resolver);
|
|
44
49
|
if (idx >= 0)
|
|
45
50
|
s.resolvers.splice(idx, 1);
|
|
@@ -49,7 +54,6 @@ async function drain(timeoutMs, scope = 'default') {
|
|
|
49
54
|
if (resolved)
|
|
50
55
|
return;
|
|
51
56
|
resolved = true;
|
|
52
|
-
// Remove this resolver from the array on timeout
|
|
53
57
|
const idx = s.resolvers.indexOf(resolver);
|
|
54
58
|
if (idx >= 0)
|
|
55
59
|
s.resolvers.splice(idx, 1);
|
|
@@ -58,3 +62,10 @@ async function drain(timeoutMs, scope = 'default') {
|
|
|
58
62
|
s.resolvers.push(resolver);
|
|
59
63
|
});
|
|
60
64
|
}
|
|
65
|
+
async function drainAll(timeoutMs) {
|
|
66
|
+
const scopes = Array.from(drainStates.keys());
|
|
67
|
+
if (scopes.length === 0)
|
|
68
|
+
return true;
|
|
69
|
+
const results = await Promise.all(scopes.map(scope => drain(timeoutMs, scope)));
|
|
70
|
+
return results.every(r => r === true);
|
|
71
|
+
}
|
package/dist/core/shutdown.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export declare function registerDrainable(scope?: string): void;
|
|
2
2
|
export declare function unregisterDrainable(scope?: string): void;
|
|
3
|
-
/**
|
|
4
|
-
* Wait for all in-flight act() calls in this scope to settle.
|
|
5
|
-
* Returns true if all settled within timeoutMs, false if timed out.
|
|
6
|
-
*/
|
|
7
3
|
export declare function drain(timeoutMs: number, scope?: string): Promise<boolean>;
|
|
4
|
+
export declare function drainAll(timeoutMs: number): Promise<boolean>;
|
|
8
5
|
//# sourceMappingURL=shutdown.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../src/core/shutdown.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../src/core/shutdown.ts"],"names":[],"mappings":"AAWA,wBAAgB,iBAAiB,CAAC,KAAK,SAAY,GAAG,IAAI,CAEzD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,SAAY,GAAG,IAAI,CAgB3D;AAUD,wBAAsB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAiClF;AAsBD,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAWlE"}
|