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.
Files changed (139) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/MIGRATION.md +141 -0
  3. package/README.md +334 -428
  4. package/SECURITY.md +56 -0
  5. package/dist/core/act.cjs +630 -126
  6. package/dist/core/act.d.ts +6 -84
  7. package/dist/core/act.d.ts.map +1 -1
  8. package/dist/core/act.js +630 -129
  9. package/dist/core/executor.cjs +1 -26
  10. package/dist/core/executor.d.ts +2 -38
  11. package/dist/core/executor.d.ts.map +1 -1
  12. package/dist/core/executor.js +1 -27
  13. package/dist/core/health.cjs +163 -0
  14. package/dist/core/health.d.ts +33 -0
  15. package/dist/core/health.d.ts.map +1 -0
  16. package/dist/core/health.js +150 -0
  17. package/dist/core/shutdown.cjs +71 -0
  18. package/dist/core/shutdown.d.ts +5 -0
  19. package/dist/core/shutdown.d.ts.map +1 -0
  20. package/dist/core/shutdown.js +65 -0
  21. package/dist/core/tenant.cjs +113 -0
  22. package/dist/core/tenant.d.ts +24 -0
  23. package/dist/core/tenant.d.ts.map +1 -0
  24. package/dist/core/tenant.js +109 -0
  25. package/dist/errors.cjs +149 -0
  26. package/dist/errors.d.ts +94 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +134 -0
  29. package/dist/index.cjs +51 -8
  30. package/dist/index.d.ts +22 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +14 -6
  33. package/dist/observability.cjs +6 -0
  34. package/dist/observability.d.ts +86 -0
  35. package/dist/observability.d.ts.map +1 -0
  36. package/dist/observability.js +3 -0
  37. package/dist/policies/bulkhead.cjs +137 -0
  38. package/dist/policies/bulkhead.d.ts +3 -0
  39. package/dist/policies/bulkhead.d.ts.map +1 -0
  40. package/dist/policies/bulkhead.js +134 -0
  41. package/dist/policies/cache.cjs +84 -67
  42. package/dist/policies/cache.d.ts +0 -33
  43. package/dist/policies/cache.d.ts.map +1 -1
  44. package/dist/policies/cache.js +84 -68
  45. package/dist/policies/circuitBreaker.cjs +179 -0
  46. package/dist/policies/circuitBreaker.d.ts +3 -0
  47. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  48. package/dist/policies/circuitBreaker.js +176 -0
  49. package/dist/policies/dedupe.cjs +46 -64
  50. package/dist/policies/dedupe.d.ts +0 -41
  51. package/dist/policies/dedupe.d.ts.map +1 -1
  52. package/dist/policies/dedupe.js +46 -65
  53. package/dist/policies/noop.cjs +6 -0
  54. package/dist/policies/noop.d.ts +3 -0
  55. package/dist/policies/noop.d.ts.map +1 -0
  56. package/dist/policies/noop.js +3 -0
  57. package/dist/policies/rateLimit.cjs +45 -0
  58. package/dist/policies/rateLimit.d.ts +3 -0
  59. package/dist/policies/rateLimit.d.ts.map +1 -0
  60. package/dist/policies/rateLimit.js +42 -0
  61. package/dist/policies/retry.cjs +129 -57
  62. package/dist/policies/retry.d.ts +0 -27
  63. package/dist/policies/retry.d.ts.map +1 -1
  64. package/dist/policies/retry.js +129 -58
  65. package/dist/policies/timeout.cjs +47 -103
  66. package/dist/policies/timeout.d.ts +2 -46
  67. package/dist/policies/timeout.d.ts.map +1 -1
  68. package/dist/policies/timeout.js +46 -102
  69. package/dist/stores/base.cjs +0 -10
  70. package/dist/stores/base.d.ts +4 -67
  71. package/dist/stores/base.d.ts.map +1 -1
  72. package/dist/stores/base.js +0 -11
  73. package/dist/stores/memory.cjs +147 -87
  74. package/dist/stores/memory.d.ts +11 -59
  75. package/dist/stores/memory.d.ts.map +1 -1
  76. package/dist/stores/memory.js +146 -88
  77. package/dist/testing/index.cjs +56 -0
  78. package/dist/testing/index.d.ts +6 -0
  79. package/dist/testing/index.d.ts.map +1 -0
  80. package/dist/testing/index.js +52 -0
  81. package/dist/types/index.cjs +0 -1
  82. package/dist/types/index.d.ts +62 -179
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +0 -2
  85. package/dist/utils/abort.cjs +46 -73
  86. package/dist/utils/abort.d.ts +4 -51
  87. package/dist/utils/abort.d.ts.map +1 -1
  88. package/dist/utils/abort.js +46 -74
  89. package/dist/utils/abortPool.cjs +25 -0
  90. package/dist/utils/abortPool.d.ts +4 -0
  91. package/dist/utils/abortPool.d.ts.map +1 -0
  92. package/dist/utils/abortPool.js +20 -0
  93. package/dist/utils/backoff.cjs +8 -15
  94. package/dist/utils/backoff.d.ts +0 -11
  95. package/dist/utils/backoff.d.ts.map +1 -1
  96. package/dist/utils/backoff.js +8 -16
  97. package/dist/utils/decorator.cjs +44 -0
  98. package/dist/utils/decorator.d.ts +3 -0
  99. package/dist/utils/decorator.d.ts.map +1 -0
  100. package/dist/utils/decorator.js +41 -0
  101. package/dist/utils/key.cjs +45 -0
  102. package/dist/utils/key.d.ts +2 -0
  103. package/dist/utils/key.d.ts.map +1 -0
  104. package/dist/utils/key.js +42 -0
  105. package/dist/utils/limits.cjs +22 -0
  106. package/dist/utils/limits.d.ts +21 -0
  107. package/dist/utils/limits.d.ts.map +1 -0
  108. package/dist/utils/limits.js +19 -0
  109. package/dist/utils/safeCall.cjs +28 -0
  110. package/dist/utils/safeCall.d.ts +2 -0
  111. package/dist/utils/safeCall.d.ts.map +1 -0
  112. package/dist/utils/safeCall.js +25 -0
  113. package/dist/utils/sanitize.cjs +50 -0
  114. package/dist/utils/sanitize.d.ts +3 -0
  115. package/dist/utils/sanitize.d.ts.map +1 -0
  116. package/dist/utils/sanitize.js +46 -0
  117. package/dist/utils/validate.cjs +163 -26
  118. package/dist/utils/validate.d.ts +6 -8
  119. package/dist/utils/validate.d.ts.map +1 -1
  120. package/dist/utils/validate.js +158 -27
  121. package/package.json +42 -7
  122. package/dist/core/act.js.map +0 -1
  123. package/dist/core/executor.js.map +0 -1
  124. package/dist/index.js.map +0 -1
  125. package/dist/policies/cache.js.map +0 -1
  126. package/dist/policies/dedupe.js.map +0 -1
  127. package/dist/policies/retry.js.map +0 -1
  128. package/dist/policies/timeout.js.map +0 -1
  129. package/dist/state/store.cjs +0 -14
  130. package/dist/state/store.d.ts +0 -11
  131. package/dist/state/store.d.ts.map +0 -1
  132. package/dist/state/store.js +0 -11
  133. package/dist/state/store.js.map +0 -1
  134. package/dist/stores/base.js.map +0 -1
  135. package/dist/stores/memory.js.map +0 -1
  136. package/dist/types/index.js.map +0 -1
  137. package/dist/utils/abort.js.map +0 -1
  138. package/dist/utils/backoff.js.map +0 -1
  139. package/dist/utils/validate.js.map +0 -1
@@ -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
  }
@@ -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,EACR,MAAM,mBAAmB,CAAA;AAG1B;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,eAAoC,CAAA;AAEpE,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,GAAG,EAAO,MAAM,CAAA;IAChB,EAAE,EAAQ,KAAK,CAAC,CAAC,CAAC,CAAA;IAClB;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,KAAK,EAAK,aAAa,CAAA;IACvB,IAAI,EAAM,OAAO,CAAA;IACjB;;;;;;OAMG;IACH,MAAM,EAAI,WAAW,CAAA;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA+BpE"}
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"}
@@ -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
+ }