actly 1.1.5 → 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 +334 -428
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +630 -126
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +630 -129
- package/dist/core/executor.cjs +1 -26
- package/dist/core/executor.d.ts +2 -38
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +1 -27
- package/dist/core/health.cjs +163 -0
- package/dist/core/health.d.ts +33 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +150 -0
- package/dist/core/shutdown.cjs +71 -0
- package/dist/core/shutdown.d.ts +5 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +65 -0
- package/dist/core/tenant.cjs +113 -0
- package/dist/core/tenant.d.ts +24 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +109 -0
- package/dist/errors.cjs +149 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +134 -0
- package/dist/index.cjs +51 -8
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/observability.cjs +6 -0
- package/dist/observability.d.ts +86 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +3 -0
- package/dist/policies/bulkhead.cjs +137 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +134 -0
- package/dist/policies/cache.cjs +84 -67
- package/dist/policies/cache.d.ts +0 -33
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +84 -68
- package/dist/policies/circuitBreaker.cjs +179 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +176 -0
- package/dist/policies/dedupe.cjs +46 -64
- package/dist/policies/dedupe.d.ts +0 -41
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +46 -65
- 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 +45 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +42 -0
- package/dist/policies/retry.cjs +129 -57
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +129 -58
- package/dist/policies/timeout.cjs +47 -103
- package/dist/policies/timeout.d.ts +2 -46
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +46 -102
- 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 +147 -87
- package/dist/stores/memory.d.ts +11 -59
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +146 -88
- 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 +62 -179
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +46 -73
- package/dist/utils/abort.d.ts +4 -51
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +46 -74
- package/dist/utils/abortPool.cjs +25 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +20 -0
- package/dist/utils/backoff.cjs +8 -15
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +8 -16
- 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 +45 -0
- package/dist/utils/key.d.ts +2 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +42 -0
- package/dist/utils/limits.cjs +22 -0
- package/dist/utils/limits.d.ts +21 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +19 -0
- 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 +50 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +46 -0
- package/dist/utils/validate.cjs +163 -26
- package/dist/utils/validate.d.ts +6 -8
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +158 -27
- package/package.json +42 -7
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/dedupe.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/backoff.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,10 +15,8 @@ async function execute(input) {
|
|
|
38
15
|
key: input.key,
|
|
39
16
|
store: input.store,
|
|
40
17
|
meta: input.meta,
|
|
18
|
+
observability: input.observability,
|
|
41
19
|
};
|
|
42
|
-
// Build the call chain from inside out.
|
|
43
|
-
// reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
|
|
44
20
|
const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
|
|
45
|
-
// Call the outermost wrapper with the root signal.
|
|
46
21
|
return wrapped(input.signal);
|
|
47
22
|
}
|
package/dist/core/executor.d.ts
CHANGED
|
@@ -1,49 +1,13 @@
|
|
|
1
|
-
import type { ActFn, PolicyApplier, AnyStateStore, RunMeta } 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
|
-
*/
|
|
1
|
+
import type { ActFn, PolicyApplier, AnyStateStore, RunMeta, ObservabilityContext } from '../types/index.js';
|
|
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;
|
|
10
|
+
observability?: ObservabilityContext;
|
|
34
11
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Pure execution engine.
|
|
37
|
-
*
|
|
38
|
-
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
39
|
-
* — a type alias defined in `/types`. Policy implementations live in
|
|
40
|
-
* `/policies` and are wired in `core/act.ts`.
|
|
41
|
-
*
|
|
42
|
-
* # Public API
|
|
43
|
-
*
|
|
44
|
-
* Exported so consumers can build custom policy chains with explicit stores
|
|
45
|
-
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
46
|
-
* module-level default store is wrong).
|
|
47
|
-
*/
|
|
48
12
|
export declare function execute<T>(input: ExecutorInput<T>): Promise<T>;
|
|
49
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,
|
|
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,11 +11,8 @@ export async function execute(input) {
|
|
|
34
11
|
key: input.key,
|
|
35
12
|
store: input.store,
|
|
36
13
|
meta: input.meta,
|
|
14
|
+
observability: input.observability,
|
|
37
15
|
};
|
|
38
|
-
// Build the call chain from inside out.
|
|
39
|
-
// reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
|
|
40
16
|
const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
|
|
41
|
-
// Call the outermost wrapper with the root signal.
|
|
42
17
|
return wrapped(input.signal);
|
|
43
18
|
}
|
|
44
|
-
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerStoreScope = registerStoreScope;
|
|
4
|
+
exports.resolveStoreScope = resolveStoreScope;
|
|
5
|
+
exports.registerInflight = registerInflight;
|
|
6
|
+
exports.unregisterInflight = unregisterInflight;
|
|
7
|
+
exports.enableWatchdog = enableWatchdog;
|
|
8
|
+
exports.registerWatchdogHooks = registerWatchdogHooks;
|
|
9
|
+
exports.unregisterWatchdogHooks = unregisterWatchdogHooks;
|
|
10
|
+
exports.disableWatchdog = disableWatchdog;
|
|
11
|
+
exports.recordError = recordError;
|
|
12
|
+
exports.recordSuccess = recordSuccess;
|
|
13
|
+
exports.createHealthCheck = createHealthCheck;
|
|
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);
|
|
20
|
+
}
|
|
21
|
+
function resolveStoreScope(store) {
|
|
22
|
+
return storeScopes.get(store);
|
|
23
|
+
}
|
|
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
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function noteInflightDown() {
|
|
41
|
+
if (globalInflightCount === 0) {
|
|
42
|
+
inflightBusySince = undefined;
|
|
43
|
+
watchdogFiredForBusySince = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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);
|
|
148
|
+
return {
|
|
149
|
+
storeSize: store.size(),
|
|
150
|
+
pendingInflight: s?.inflight ?? 0,
|
|
151
|
+
uptimeMs: Date.now() - startTime,
|
|
152
|
+
lastError: s?.lastError,
|
|
153
|
+
lastSuccessAt: s?.lastSuccessAt,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
checkFn.dispose = () => {
|
|
157
|
+
if (probeTimer !== undefined) {
|
|
158
|
+
clearInterval(probeTimer);
|
|
159
|
+
probeTimer = undefined;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return checkFn;
|
|
163
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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;
|
|
6
|
+
export interface HealthStatus {
|
|
7
|
+
storeSize: number;
|
|
8
|
+
pendingInflight: number;
|
|
9
|
+
uptimeMs: number;
|
|
10
|
+
lastError?: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
};
|
|
15
|
+
lastSuccessAt?: number;
|
|
16
|
+
}
|
|
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;
|
|
33
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +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;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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
}
|
|
8
|
+
export function resolveStoreScope(store) {
|
|
9
|
+
return storeScopes.get(store);
|
|
10
|
+
}
|
|
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
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function noteInflightDown() {
|
|
28
|
+
if (globalInflightCount === 0) {
|
|
29
|
+
inflightBusySince = undefined;
|
|
30
|
+
watchdogFiredForBusySince = undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
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);
|
|
135
|
+
return {
|
|
136
|
+
storeSize: store.size(),
|
|
137
|
+
pendingInflight: s?.inflight ?? 0,
|
|
138
|
+
uptimeMs: Date.now() - startTime,
|
|
139
|
+
lastError: s?.lastError,
|
|
140
|
+
lastSuccessAt: s?.lastSuccessAt,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
checkFn.dispose = () => {
|
|
144
|
+
if (probeTimer !== undefined) {
|
|
145
|
+
clearInterval(probeTimer);
|
|
146
|
+
probeTimer = undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return checkFn;
|
|
150
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerDrainable = registerDrainable;
|
|
4
|
+
exports.unregisterDrainable = unregisterDrainable;
|
|
5
|
+
exports.drain = drain;
|
|
6
|
+
exports.drainAll = drainAll;
|
|
7
|
+
const drainStates = new Map();
|
|
8
|
+
function getState(scope) {
|
|
9
|
+
let s = drainStates.get(scope);
|
|
10
|
+
if (!s) {
|
|
11
|
+
s = { inflight: 0, resolvers: [] };
|
|
12
|
+
drainStates.set(scope, s);
|
|
13
|
+
}
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
16
|
+
function registerDrainable(scope = 'default') {
|
|
17
|
+
getState(scope).inflight++;
|
|
18
|
+
}
|
|
19
|
+
function unregisterDrainable(scope = 'default') {
|
|
20
|
+
const s = getState(scope);
|
|
21
|
+
s.inflight = Math.max(0, s.inflight - 1);
|
|
22
|
+
if (s.inflight === 0) {
|
|
23
|
+
for (const r of s.resolvers)
|
|
24
|
+
r();
|
|
25
|
+
s.resolvers = [];
|
|
26
|
+
if (scope !== 'default') {
|
|
27
|
+
drainStates.delete(scope);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function drain(timeoutMs, scope = 'default') {
|
|
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
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let resolved = false;
|
|
43
|
+
const resolver = () => {
|
|
44
|
+
if (resolved)
|
|
45
|
+
return;
|
|
46
|
+
resolved = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
const idx = s.resolvers.indexOf(resolver);
|
|
49
|
+
if (idx >= 0)
|
|
50
|
+
s.resolvers.splice(idx, 1);
|
|
51
|
+
resolve(true);
|
|
52
|
+
};
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
if (resolved)
|
|
55
|
+
return;
|
|
56
|
+
resolved = true;
|
|
57
|
+
const idx = s.resolvers.indexOf(resolver);
|
|
58
|
+
if (idx >= 0)
|
|
59
|
+
s.resolvers.splice(idx, 1);
|
|
60
|
+
resolve(false);
|
|
61
|
+
}, timeoutMs);
|
|
62
|
+
s.resolvers.push(resolver);
|
|
63
|
+
});
|
|
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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function registerDrainable(scope?: string): void;
|
|
2
|
+
export declare function unregisterDrainable(scope?: string): void;
|
|
3
|
+
export declare function drain(timeoutMs: number, scope?: string): Promise<boolean>;
|
|
4
|
+
export declare function drainAll(timeoutMs: number): Promise<boolean>;
|
|
5
|
+
//# sourceMappingURL=shutdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const drainStates = new Map();
|
|
2
|
+
function getState(scope) {
|
|
3
|
+
let s = drainStates.get(scope);
|
|
4
|
+
if (!s) {
|
|
5
|
+
s = { inflight: 0, resolvers: [] };
|
|
6
|
+
drainStates.set(scope, s);
|
|
7
|
+
}
|
|
8
|
+
return s;
|
|
9
|
+
}
|
|
10
|
+
export function registerDrainable(scope = 'default') {
|
|
11
|
+
getState(scope).inflight++;
|
|
12
|
+
}
|
|
13
|
+
export function unregisterDrainable(scope = 'default') {
|
|
14
|
+
const s = getState(scope);
|
|
15
|
+
s.inflight = Math.max(0, s.inflight - 1);
|
|
16
|
+
if (s.inflight === 0) {
|
|
17
|
+
for (const r of s.resolvers)
|
|
18
|
+
r();
|
|
19
|
+
s.resolvers = [];
|
|
20
|
+
if (scope !== 'default') {
|
|
21
|
+
drainStates.delete(scope);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function drain(timeoutMs, scope = 'default') {
|
|
26
|
+
const s = drainStates.get(scope);
|
|
27
|
+
if (!s)
|
|
28
|
+
return true;
|
|
29
|
+
if (s.inflight === 0) {
|
|
30
|
+
if (scope !== 'default' && s.resolvers.length === 0) {
|
|
31
|
+
drainStates.delete(scope);
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let resolved = false;
|
|
37
|
+
const resolver = () => {
|
|
38
|
+
if (resolved)
|
|
39
|
+
return;
|
|
40
|
+
resolved = true;
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
const idx = s.resolvers.indexOf(resolver);
|
|
43
|
+
if (idx >= 0)
|
|
44
|
+
s.resolvers.splice(idx, 1);
|
|
45
|
+
resolve(true);
|
|
46
|
+
};
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
if (resolved)
|
|
49
|
+
return;
|
|
50
|
+
resolved = true;
|
|
51
|
+
const idx = s.resolvers.indexOf(resolver);
|
|
52
|
+
if (idx >= 0)
|
|
53
|
+
s.resolvers.splice(idx, 1);
|
|
54
|
+
resolve(false);
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
s.resolvers.push(resolver);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function drainAll(timeoutMs) {
|
|
60
|
+
const scopes = Array.from(drainStates.keys());
|
|
61
|
+
if (scopes.length === 0)
|
|
62
|
+
return true;
|
|
63
|
+
const results = await Promise.all(scopes.map(scope => drain(timeoutMs, scope)));
|
|
64
|
+
return results.every(r => r === true);
|
|
65
|
+
}
|