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/act.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HedgeTimeoutError = void 0;
|
|
3
4
|
exports.act = act;
|
|
4
5
|
exports.invalidate = invalidate;
|
|
5
6
|
exports.withStore = withStore;
|
|
@@ -8,214 +9,717 @@ const retry_js_1 = require("../policies/retry.js");
|
|
|
8
9
|
const timeout_js_1 = require("../policies/timeout.js");
|
|
9
10
|
const dedupe_js_1 = require("../policies/dedupe.js");
|
|
10
11
|
const cache_js_1 = require("../policies/cache.js");
|
|
12
|
+
const circuitBreaker_js_1 = require("../policies/circuitBreaker.js");
|
|
13
|
+
const bulkhead_js_1 = require("../policies/bulkhead.js");
|
|
14
|
+
const rateLimit_js_1 = require("../policies/rateLimit.js");
|
|
11
15
|
const memory_js_1 = require("../stores/memory.js");
|
|
12
16
|
const base_js_1 = require("../stores/base.js");
|
|
13
17
|
const abort_js_1 = require("../utils/abort.js");
|
|
18
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
19
|
+
const safeCall_js_1 = require("../utils/safeCall.js");
|
|
20
|
+
const health_js_1 = require("./health.js");
|
|
21
|
+
const shutdown_js_1 = require("./shutdown.js");
|
|
14
22
|
const validate_js_1 = require("../utils/validate.js");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// chain may include dedupePolicy, which mandates synchronous store access.
|
|
18
|
-
// For SSR isolation or per-test control, use `withStore()` or call
|
|
19
|
-
// `execute()` directly with an explicit store.
|
|
20
|
-
const defaultStore = new memory_js_1.InMemoryStore();
|
|
21
|
-
// Namespace prefixes used by policies. Kept here (not in policy files) so
|
|
22
|
-
// `invalidate()` can resolve cache keys without importing policy internals.
|
|
23
|
+
const errors_js_1 = require("../errors.js");
|
|
24
|
+
const defaultStore = (0, memory_js_1.createDefaultStore)();
|
|
23
25
|
const CACHE_NS = 'cache:';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
function monotonicNow() {
|
|
27
|
+
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
|
28
|
+
return performance.now();
|
|
29
|
+
}
|
|
30
|
+
return Date.now();
|
|
31
|
+
}
|
|
32
|
+
function generateTraceId() {
|
|
33
|
+
const crypto = globalThis.crypto;
|
|
34
|
+
if (crypto && typeof crypto.randomUUID === 'function') {
|
|
35
|
+
return crypto.randomUUID();
|
|
36
|
+
}
|
|
37
|
+
return `actly-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
38
|
+
}
|
|
39
|
+
function buildObservability(hooks, traceId) {
|
|
40
|
+
if (!hooks)
|
|
41
|
+
return undefined;
|
|
42
|
+
const hasAnyHook = !!hooks.onAttempt ||
|
|
43
|
+
!!hooks.onRetry ||
|
|
44
|
+
!!hooks.onCacheHit ||
|
|
45
|
+
!!hooks.onCacheMiss ||
|
|
46
|
+
!!hooks.onDedupeJoin ||
|
|
47
|
+
!!hooks.onTimeout ||
|
|
48
|
+
!!hooks.onFinalSuccess ||
|
|
49
|
+
!!hooks.onFinalFailure ||
|
|
50
|
+
!!hooks.onBackpressure ||
|
|
51
|
+
!!hooks.onWatchdog;
|
|
52
|
+
if (!hasAnyHook)
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
traceId: traceId ?? generateTraceId(),
|
|
56
|
+
hooks,
|
|
57
|
+
joinerCounter: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function classifyFailure(error) {
|
|
61
|
+
if (error == null || typeof error !== 'object')
|
|
62
|
+
return 'fn-error';
|
|
63
|
+
const code = error.code;
|
|
64
|
+
if (code === 'ACTLY_ABORT')
|
|
65
|
+
return 'abort';
|
|
66
|
+
if (code === 'ACTLY_TIMEOUT')
|
|
67
|
+
return 'timeout';
|
|
68
|
+
if (code === 'ACTLY_TOTAL_TIMEOUT')
|
|
69
|
+
return 'total-timeout';
|
|
70
|
+
if (code === 'ACTLY_RETRY_EXHAUSTED')
|
|
71
|
+
return 'retry-exhausted';
|
|
72
|
+
if (code === 'ACTLY_VALIDATION')
|
|
73
|
+
return 'validation';
|
|
74
|
+
if (code === 'ACTLY_CIRCUIT_OPEN')
|
|
75
|
+
return 'circuit-open';
|
|
76
|
+
if (code === 'ACTLY_BULKHEAD_FULL')
|
|
77
|
+
return 'bulkhead-full';
|
|
78
|
+
if (code === 'ACTLY_RATE_LIMIT')
|
|
79
|
+
return 'rate-limited';
|
|
80
|
+
if (code === 'ACTLY_RESOURCE_EXHAUSTED')
|
|
81
|
+
return 'resource-exhausted';
|
|
82
|
+
if (code === 'ACTLY_HEDGE_TIMEOUT')
|
|
83
|
+
return 'hedge-timeout';
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
const name = error.name;
|
|
86
|
+
if (name === 'AbortError')
|
|
87
|
+
return 'abort';
|
|
88
|
+
}
|
|
89
|
+
return 'fn-error';
|
|
90
|
+
}
|
|
28
91
|
function normalizeDedupe(opt) {
|
|
29
92
|
if (opt === true)
|
|
30
93
|
return { enabled: true };
|
|
31
94
|
if (opt && typeof opt === 'object' && opt.enabled) {
|
|
32
|
-
|
|
33
|
-
enabled: true,
|
|
34
|
-
|
|
35
|
-
};
|
|
95
|
+
if (opt.inflightTtl !== undefined && opt.inflightTtl !== 0 && !Number.isNaN(opt.inflightTtl)) {
|
|
96
|
+
return { enabled: true, inflightTtl: opt.inflightTtl };
|
|
97
|
+
}
|
|
98
|
+
return { enabled: true };
|
|
36
99
|
}
|
|
37
100
|
return undefined;
|
|
38
101
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Build the policy chain from `ActOptions`. The order is fixed and
|
|
41
|
-
* documented in `executor.ts`. Policies with no effect (e.g. `retry.attempts: 1`)
|
|
42
|
-
* are skipped — they would be pure overhead.
|
|
43
|
-
*/
|
|
44
102
|
function buildPolicies(options) {
|
|
45
103
|
const dedupe = normalizeDedupe(options.dedupe);
|
|
46
104
|
const policies = [];
|
|
47
|
-
|
|
48
|
-
|
|
105
|
+
if (options.rateLimit) {
|
|
106
|
+
policies.push((0, rateLimit_js_1.rateLimitPolicy)(options.rateLimit));
|
|
107
|
+
}
|
|
108
|
+
if (options.circuitBreaker) {
|
|
109
|
+
policies.push((0, circuitBreaker_js_1.circuitBreakerPolicy)(options.circuitBreaker));
|
|
110
|
+
}
|
|
49
111
|
if (options.totalTimeout && options.totalTimeout.ms > 0) {
|
|
50
112
|
policies.push((0, timeout_js_1.totalTimeoutPolicy)(options.totalTimeout));
|
|
51
113
|
}
|
|
52
|
-
// 1. Cache: a hit short-circuits everything below it.
|
|
53
114
|
if (options.cache && options.cache.ttl > 0) {
|
|
54
115
|
policies.push((0, cache_js_1.cachePolicy)(options.cache));
|
|
55
116
|
}
|
|
56
|
-
|
|
117
|
+
if (options.bulkhead) {
|
|
118
|
+
policies.push((0, bulkhead_js_1.bulkheadPolicy)(options.bulkhead));
|
|
119
|
+
}
|
|
57
120
|
if (dedupe) {
|
|
58
121
|
policies.push((0, dedupe_js_1.dedupePolicy)(dedupe));
|
|
59
122
|
}
|
|
60
|
-
// 3. Retry: owns the attempt loop.
|
|
61
|
-
// `attempts: 1` is a no-op — skip to avoid overhead.
|
|
62
123
|
if (options.retry && options.retry.attempts > 1) {
|
|
63
124
|
policies.push((0, retry_js_1.retryPolicy)(options.retry));
|
|
64
125
|
}
|
|
65
|
-
// 4. Innermost: per-attempt clock. Resets on every retry.
|
|
66
126
|
if (options.timeout && options.timeout.ms > 0) {
|
|
67
127
|
policies.push((0, timeout_js_1.timeoutPolicy)(options.timeout));
|
|
68
128
|
}
|
|
69
129
|
return policies;
|
|
70
130
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Build a root AbortController from `options.signal`.
|
|
73
|
-
*
|
|
74
|
-
* - If no user signal: returns a fresh controller that never aborts unless
|
|
75
|
-
* an outer timeout policy aborts it.
|
|
76
|
-
* - If user signal is already aborted: returns a controller that is already
|
|
77
|
-
* aborted with the user's reason (so the operation rejects immediately).
|
|
78
|
-
* - Otherwise: links the user signal to the controller.
|
|
79
|
-
*/
|
|
80
131
|
function buildRootSignal(userSignal) {
|
|
81
132
|
const controller = new AbortController();
|
|
82
|
-
if (userSignal)
|
|
83
|
-
(0, abort_js_1.linkSignal)(userSignal, controller);
|
|
84
|
-
|
|
133
|
+
if (userSignal) {
|
|
134
|
+
const unlink = (0, abort_js_1.linkSignal)(userSignal, controller);
|
|
135
|
+
return { controller, cleanup: unlink };
|
|
136
|
+
}
|
|
137
|
+
return { controller, cleanup: () => { } };
|
|
85
138
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Execute `fn` with the given reliability policies.
|
|
88
|
-
*
|
|
89
|
-
* @param key Stable identifier for this action. Scopes dedupe + cache.
|
|
90
|
-
* @param fn The async work to run. Receives an `AbortSignal` for
|
|
91
|
-
* cooperative cancellation (legacy `() => Promise<T>` is
|
|
92
|
-
* still accepted — the signal is simply ignored).
|
|
93
|
-
* @param options Which policies to apply and how. All fields are optional.
|
|
94
|
-
*
|
|
95
|
-
* @returns `ActResult<T>` — always resolves, never throws.
|
|
96
|
-
* Check `result.ok` before reading `result.value`.
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* // With cooperative cancellation
|
|
100
|
-
* const result = await act('user:42', async (signal) => {
|
|
101
|
-
* return fetch(`/api/users/42`, { signal })
|
|
102
|
-
* }, {
|
|
103
|
-
* retry: { attempts: 3, delayMs: 200, backoff: 'exponential' },
|
|
104
|
-
* timeout: { ms: 5_000 },
|
|
105
|
-
* totalTimeout: { ms: 12_000 },
|
|
106
|
-
* dedupe: true,
|
|
107
|
-
* cache: { ttl: 60_000 },
|
|
108
|
-
* })
|
|
109
|
-
*
|
|
110
|
-
* if (result.ok) {
|
|
111
|
-
* console.log(result.value, result.source, result.attempts)
|
|
112
|
-
* } else {
|
|
113
|
-
* console.error(result.error)
|
|
114
|
-
* }
|
|
115
|
-
*/
|
|
116
139
|
async function act(key, fn, options = {}) {
|
|
117
|
-
// Validate input upfront. Programmer errors throw — they should not be
|
|
118
|
-
// swallowed into an ActFailure because the caller's code is broken.
|
|
119
140
|
(0, validate_js_1.assertKey)(key);
|
|
120
141
|
(0, validate_js_1.assertOptions)(options);
|
|
142
|
+
const hasAnyOption = options.retry !== undefined ||
|
|
143
|
+
options.timeout !== undefined ||
|
|
144
|
+
options.totalTimeout !== undefined ||
|
|
145
|
+
options.dedupe !== undefined ||
|
|
146
|
+
options.cache !== undefined ||
|
|
147
|
+
options.signal !== undefined ||
|
|
148
|
+
options.observability !== undefined ||
|
|
149
|
+
options.traceId !== undefined ||
|
|
150
|
+
options.circuitBreaker !== undefined ||
|
|
151
|
+
options.bulkhead !== undefined ||
|
|
152
|
+
options.rateLimit !== undefined ||
|
|
153
|
+
options.hedge !== undefined ||
|
|
154
|
+
options.fallback !== undefined ||
|
|
155
|
+
options.audit !== undefined;
|
|
156
|
+
if (!hasAnyOption) {
|
|
157
|
+
const startedAt = monotonicNow();
|
|
158
|
+
try {
|
|
159
|
+
(0, health_js_1.registerInflight)('default');
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
const now = monotonicNow();
|
|
163
|
+
const durationMs = now - startedAt;
|
|
164
|
+
(0, health_js_1.recordError)('default', 'ACTLY_RESOURCE_EXHAUSTED', (0, sanitize_js_1.sanitizeErrorMessage)(e));
|
|
165
|
+
return { ok: false, error: e, attempts: 0, durationMs };
|
|
166
|
+
}
|
|
167
|
+
(0, shutdown_js_1.registerDrainable)('default');
|
|
168
|
+
try {
|
|
169
|
+
const value = await fn(new AbortController().signal);
|
|
170
|
+
(0, health_js_1.recordSuccess)('default');
|
|
171
|
+
return { ok: true, value, source: 'fresh', attempts: 1, durationMs: monotonicNow() - startedAt };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
(0, health_js_1.recordError)('default', 'fn-error', (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
175
|
+
return { ok: false, error, attempts: 1, durationMs: monotonicNow() - startedAt };
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
(0, health_js_1.unregisterInflight)('default');
|
|
179
|
+
(0, shutdown_js_1.unregisterDrainable)('default');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
121
182
|
const meta = { attempts: 1, source: 'fresh' };
|
|
122
|
-
const rootController = buildRootSignal(options.signal);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
const { controller: rootController, cleanup } = buildRootSignal(options.signal);
|
|
184
|
+
const observability = buildObservability(options.observability, options.traceId);
|
|
185
|
+
const effectiveTraceId = observability?.traceId ?? options.traceId;
|
|
186
|
+
const startedAt = monotonicNow();
|
|
187
|
+
const scope = 'default';
|
|
188
|
+
try {
|
|
189
|
+
(0, health_js_1.registerInflight)(scope);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
cleanup();
|
|
193
|
+
const now = monotonicNow();
|
|
194
|
+
const durationMs = now - startedAt;
|
|
195
|
+
(0, health_js_1.recordError)(scope, 'ACTLY_RESOURCE_EXHAUSTED', (0, sanitize_js_1.sanitizeErrorMessage)(e));
|
|
196
|
+
if (observability) {
|
|
197
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
198
|
+
type: 'final-failure',
|
|
199
|
+
key, traceId: observability.traceId, timestamp: Date.now(),
|
|
200
|
+
attempts: 0, durationMs,
|
|
201
|
+
failedBy: 'resource-exhausted', error: e,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (options.audit) {
|
|
205
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, {
|
|
206
|
+
key, traceId: effectiveTraceId ?? '', timestamp: Date.now(), durationMs,
|
|
207
|
+
ok: false, attempts: 0, failedBy: 'resource-exhausted',
|
|
208
|
+
error: (0, sanitize_js_1.sanitizeError)(e),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return { ok: false, error: e, attempts: 0, traceId: effectiveTraceId, durationMs };
|
|
212
|
+
}
|
|
213
|
+
(0, shutdown_js_1.registerDrainable)(scope);
|
|
126
214
|
if (rootController.signal.aborted) {
|
|
127
|
-
|
|
215
|
+
cleanup();
|
|
216
|
+
(0, health_js_1.unregisterInflight)(scope);
|
|
217
|
+
(0, shutdown_js_1.unregisterDrainable)(scope);
|
|
218
|
+
const error = rootController.signal.reason;
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const durationMs = monotonicNow() - startedAt;
|
|
221
|
+
if (observability) {
|
|
222
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
223
|
+
type: 'final-failure',
|
|
224
|
+
key, traceId: observability.traceId, timestamp: now,
|
|
225
|
+
attempts: 0, durationMs,
|
|
226
|
+
failedBy: 'abort', error,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (options.audit) {
|
|
230
|
+
const sanitizedError = (0, sanitize_js_1.sanitizeError)(error);
|
|
231
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: now, durationMs, ok: false, attempts: 0, failedBy: 'abort', error: sanitizedError });
|
|
232
|
+
}
|
|
233
|
+
(0, health_js_1.recordError)(scope, 'ACTLY_ABORT', (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
234
|
+
return { ok: false, error, attempts: 0, traceId: effectiveTraceId, durationMs };
|
|
128
235
|
}
|
|
129
|
-
|
|
236
|
+
let policies;
|
|
130
237
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
238
|
+
policies = buildPolicies(options);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
cleanup();
|
|
242
|
+
(0, health_js_1.unregisterInflight)(scope);
|
|
243
|
+
(0, shutdown_js_1.unregisterDrainable)(scope);
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const durationMs = monotonicNow() - startedAt;
|
|
246
|
+
(0, health_js_1.recordError)(scope, 'ACTLY_VALIDATION', (0, sanitize_js_1.sanitizeErrorMessage)(e));
|
|
247
|
+
if (observability) {
|
|
248
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
249
|
+
type: 'final-failure',
|
|
250
|
+
key, traceId: observability.traceId, timestamp: now,
|
|
251
|
+
attempts: 0, durationMs, failedBy: 'validation', error: e,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (options.audit) {
|
|
255
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, {
|
|
256
|
+
key, traceId: effectiveTraceId ?? '', timestamp: now, durationMs,
|
|
257
|
+
ok: false, attempts: 0, failedBy: 'validation',
|
|
258
|
+
error: (0, sanitize_js_1.sanitizeError)(e),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return { ok: false, error: e, attempts: 0, traceId: effectiveTraceId, durationMs };
|
|
262
|
+
}
|
|
263
|
+
const hasRetryPolicy = !!(options.retry && options.retry.attempts > 1);
|
|
264
|
+
if (observability && !hasRetryPolicy) {
|
|
265
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onAttempt, {
|
|
266
|
+
type: 'attempt', key, traceId: observability.traceId,
|
|
267
|
+
timestamp: Date.now(), attempt: 1,
|
|
138
268
|
});
|
|
139
|
-
|
|
269
|
+
}
|
|
270
|
+
const hedgePlacement = options.hedge?.placement ?? 'outside-retry';
|
|
271
|
+
const hedgeKeepLoser = options.hedge?.keepLoser ?? false;
|
|
272
|
+
const fnWithHedge = (options.hedge && hedgePlacement === 'inside-retry')
|
|
273
|
+
? wrapHedge(fn, options.hedge.delayMs, hedgeKeepLoser)
|
|
274
|
+
: fn;
|
|
275
|
+
const needsRaceAbort = options.signal !== undefined;
|
|
276
|
+
try {
|
|
277
|
+
let value;
|
|
278
|
+
if (options.hedge && hedgePlacement === 'outside-retry') {
|
|
279
|
+
const hedgeMeta = { attempts: 1, source: 'fresh' };
|
|
280
|
+
const chainFactory = (signal, m) => (0, executor_js_1.execute)({
|
|
281
|
+
key,
|
|
282
|
+
fn: fnWithHedge,
|
|
283
|
+
policies,
|
|
284
|
+
store: defaultStore,
|
|
285
|
+
meta: m,
|
|
286
|
+
signal,
|
|
287
|
+
observability,
|
|
288
|
+
});
|
|
289
|
+
const hedgeResult = needsRaceAbort
|
|
290
|
+
? await (0, abort_js_1.raceAbort)(Promise.resolve(runWithHedge(chainFactory, meta, hedgeMeta, rootController.signal, options.hedge.delayMs, hedgeKeepLoser)), rootController.signal)
|
|
291
|
+
: await runWithHedge(chainFactory, meta, hedgeMeta, rootController.signal, options.hedge.delayMs, hedgeKeepLoser);
|
|
292
|
+
meta.attempts = hedgeResult.winnerMeta.attempts;
|
|
293
|
+
meta.source = hedgeResult.winnerMeta.source;
|
|
294
|
+
value = hedgeResult.value;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
const execPromise = (0, executor_js_1.execute)({
|
|
298
|
+
key,
|
|
299
|
+
fn: fnWithHedge,
|
|
300
|
+
policies,
|
|
301
|
+
store: defaultStore,
|
|
302
|
+
meta,
|
|
303
|
+
signal: rootController.signal,
|
|
304
|
+
observability,
|
|
305
|
+
});
|
|
306
|
+
value = needsRaceAbort
|
|
307
|
+
? await (0, abort_js_1.raceAbort)(Promise.resolve(execPromise), rootController.signal)
|
|
308
|
+
: await execPromise;
|
|
309
|
+
}
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
const durationMs = monotonicNow() - startedAt;
|
|
312
|
+
(0, health_js_1.recordSuccess)(scope);
|
|
313
|
+
if (observability) {
|
|
314
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalSuccess, {
|
|
315
|
+
type: 'final-success',
|
|
316
|
+
key, traceId: observability.traceId, timestamp: now,
|
|
317
|
+
source: meta.source, attempts: meta.attempts, durationMs,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (options.audit) {
|
|
321
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: now, durationMs, ok: true, attempts: meta.attempts });
|
|
322
|
+
}
|
|
323
|
+
return { ok: true, value, source: meta.source, attempts: meta.attempts, traceId: effectiveTraceId, durationMs };
|
|
140
324
|
}
|
|
141
325
|
catch (error) {
|
|
142
|
-
|
|
326
|
+
const durationMs = monotonicNow() - startedAt;
|
|
327
|
+
const failedBy = rootController.signal.aborted ? 'abort' : classifyFailure(error);
|
|
328
|
+
let errorRecorded = false;
|
|
329
|
+
if (options.fallback) {
|
|
330
|
+
(0, health_js_1.recordError)(scope, failedBy, (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
331
|
+
errorRecorded = true;
|
|
332
|
+
try {
|
|
333
|
+
const fallbackValue = typeof options.fallback.value === 'function'
|
|
334
|
+
? await options.fallback.value()
|
|
335
|
+
: options.fallback.value;
|
|
336
|
+
if (observability) {
|
|
337
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalSuccess, {
|
|
338
|
+
type: 'final-success',
|
|
339
|
+
key, traceId: observability.traceId, timestamp: Date.now(),
|
|
340
|
+
source: meta.source, attempts: meta.attempts, durationMs,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (options.audit) {
|
|
344
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: Date.now(), durationMs, ok: true, attempts: meta.attempts });
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, value: fallbackValue, source: 'fresh', attempts: meta.attempts, traceId: effectiveTraceId, durationMs };
|
|
347
|
+
}
|
|
348
|
+
catch (fallbackErr) {
|
|
349
|
+
if (observability) {
|
|
350
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
351
|
+
type: 'final-failure',
|
|
352
|
+
key, traceId: observability.traceId, timestamp: Date.now(),
|
|
353
|
+
attempts: meta.attempts, durationMs,
|
|
354
|
+
failedBy: 'fn-error', error: fallbackErr,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
|
|
358
|
+
console.warn('Actly: fallback threw — error swallowed, surfacing original fn error.', fallbackErr);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const sanitizedError = options.audit ? (0, sanitize_js_1.sanitizeError)(error) : error;
|
|
363
|
+
if (!errorRecorded) {
|
|
364
|
+
(0, health_js_1.recordError)(scope, failedBy, (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
365
|
+
}
|
|
366
|
+
if (observability) {
|
|
367
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
368
|
+
type: 'final-failure',
|
|
369
|
+
key, traceId: observability.traceId, timestamp: Date.now(),
|
|
370
|
+
attempts: meta.attempts, durationMs,
|
|
371
|
+
failedBy, error,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (options.audit) {
|
|
375
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: Date.now(), durationMs, ok: false, attempts: meta.attempts, failedBy, error: sanitizedError });
|
|
376
|
+
}
|
|
377
|
+
return { ok: false, error, attempts: meta.attempts, traceId: effectiveTraceId, durationMs };
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
cleanup();
|
|
381
|
+
(0, health_js_1.unregisterInflight)(scope);
|
|
382
|
+
(0, shutdown_js_1.unregisterDrainable)(scope);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
var errors_js_2 = require("../errors.js");
|
|
386
|
+
Object.defineProperty(exports, "HedgeTimeoutError", { enumerable: true, get: function () { return errors_js_2.HedgeTimeoutError; } });
|
|
387
|
+
function wrapHedge(fn, delayMs, keepLoser) {
|
|
388
|
+
return async (parentSignal) => {
|
|
389
|
+
const primaryCtl = new AbortController();
|
|
390
|
+
const hedgeCtl = new AbortController();
|
|
391
|
+
const unlinkPrimary = (0, abort_js_1.linkSignal)(parentSignal, primaryCtl);
|
|
392
|
+
const unlinkHedge = (0, abort_js_1.linkSignal)(parentSignal, hedgeCtl);
|
|
393
|
+
let timer;
|
|
394
|
+
let primary;
|
|
395
|
+
let hedgePromise;
|
|
396
|
+
try {
|
|
397
|
+
primary = Promise.resolve(fn(primaryCtl.signal));
|
|
398
|
+
const hedgeTimeout = new Promise((_, reject) => {
|
|
399
|
+
timer = setTimeout(() => reject(new errors_js_1.HedgeTimeoutError()), delayMs);
|
|
400
|
+
});
|
|
401
|
+
try {
|
|
402
|
+
return await Promise.race([primary, hedgeTimeout]);
|
|
403
|
+
}
|
|
404
|
+
catch (e) {
|
|
405
|
+
if (!(e instanceof errors_js_1.HedgeTimeoutError)) {
|
|
406
|
+
if (!keepLoser)
|
|
407
|
+
hedgeCtl.abort(new Error('hedge cancelled: primary rejected'));
|
|
408
|
+
throw e;
|
|
409
|
+
}
|
|
410
|
+
hedgePromise = Promise.resolve(fn(hedgeCtl.signal));
|
|
411
|
+
primary.catch(() => { });
|
|
412
|
+
hedgePromise.catch(() => { });
|
|
413
|
+
try {
|
|
414
|
+
const primaryTagged = primary.then(v => ({ value: v, winner: 'primary' }));
|
|
415
|
+
const hedgeTagged = hedgePromise.then(v => ({ value: v, winner: 'hedge' }));
|
|
416
|
+
const winner = await Promise.race([primaryTagged, hedgeTagged]);
|
|
417
|
+
if (!keepLoser) {
|
|
418
|
+
if (winner.winner === 'primary') {
|
|
419
|
+
hedgeCtl.abort(new Error('hedge cancelled: loser'));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
primaryCtl.abort(new Error('hedge cancelled: loser'));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return winner.value;
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (!keepLoser) {
|
|
429
|
+
primaryCtl.abort(new Error('hedge cancelled: loser rejected'));
|
|
430
|
+
hedgeCtl.abort(new Error('hedge cancelled: loser rejected'));
|
|
431
|
+
}
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
if (timer)
|
|
438
|
+
clearTimeout(timer);
|
|
439
|
+
unlinkPrimary();
|
|
440
|
+
unlinkHedge();
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async function runWithHedge(chainFactory, primaryMeta, hedgeMeta, parentSignal, delayMs, keepLoser) {
|
|
445
|
+
const primaryCtl = new AbortController();
|
|
446
|
+
const hedgeCtl = new AbortController();
|
|
447
|
+
const unlinkPrimary = (0, abort_js_1.linkSignal)(parentSignal, primaryCtl);
|
|
448
|
+
const unlinkHedge = (0, abort_js_1.linkSignal)(parentSignal, hedgeCtl);
|
|
449
|
+
let timer;
|
|
450
|
+
try {
|
|
451
|
+
const primary = Promise.resolve(chainFactory(primaryCtl.signal, primaryMeta));
|
|
452
|
+
const hedgeTimeout = new Promise((_, reject) => {
|
|
453
|
+
timer = setTimeout(() => reject(new errors_js_1.HedgeTimeoutError()), delayMs);
|
|
454
|
+
});
|
|
455
|
+
try {
|
|
456
|
+
const value = await Promise.race([primary, hedgeTimeout]);
|
|
457
|
+
return { value, winnerMeta: primaryMeta };
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
if (!(e instanceof errors_js_1.HedgeTimeoutError)) {
|
|
461
|
+
if (!keepLoser)
|
|
462
|
+
hedgeCtl.abort(new Error('hedge cancelled: primary rejected'));
|
|
463
|
+
throw e;
|
|
464
|
+
}
|
|
465
|
+
const hedge = Promise.resolve(chainFactory(hedgeCtl.signal, hedgeMeta));
|
|
466
|
+
primary.catch(() => { });
|
|
467
|
+
hedge.catch(() => { });
|
|
468
|
+
const primaryTagged = primary.then(v => ({ value: v, winnerMeta: primaryMeta }));
|
|
469
|
+
const hedgeTagged = hedge.then(v => ({ value: v, winnerMeta: hedgeMeta }));
|
|
470
|
+
try {
|
|
471
|
+
const winner = await Promise.race([primaryTagged, hedgeTagged]);
|
|
472
|
+
if (!keepLoser) {
|
|
473
|
+
primaryCtl.abort(new Error('hedge cancelled: loser'));
|
|
474
|
+
hedgeCtl.abort(new Error('hedge cancelled: loser'));
|
|
475
|
+
}
|
|
476
|
+
return winner;
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
if (!keepLoser) {
|
|
480
|
+
primaryCtl.abort(new Error('hedge cancelled: loser rejected'));
|
|
481
|
+
hedgeCtl.abort(new Error('hedge cancelled: loser rejected'));
|
|
482
|
+
}
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
if (timer)
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
unlinkPrimary();
|
|
491
|
+
unlinkHedge();
|
|
143
492
|
}
|
|
144
493
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Invalidate the cached value for `key` on the default module-level store.
|
|
147
|
-
*
|
|
148
|
-
* Only clears the cache slot — does not affect in-flight dedupe entries
|
|
149
|
-
* (those will settle on their own). Returns `true` if a cache entry was
|
|
150
|
-
* removed, `false` otherwise.
|
|
151
|
-
*
|
|
152
|
-
* Useful when you know the underlying data has changed and you want the
|
|
153
|
-
* next `act()` call to re-run `fn` instead of serving stale cache:
|
|
154
|
-
*
|
|
155
|
-
* ```ts
|
|
156
|
-
* await act('user:42', () => fetchUser(42), { cache: { ttl: 60_000 } })
|
|
157
|
-
* // ... user updates their profile ...
|
|
158
|
-
* invalidate('user:42') // next call will re-fetch
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
494
|
function invalidate(key) {
|
|
495
|
+
(0, validate_js_1.assertKey)(key);
|
|
162
496
|
const cacheKey = CACHE_NS + key;
|
|
163
497
|
const existed = defaultStore.has(cacheKey);
|
|
164
498
|
defaultStore.delete(cacheKey);
|
|
165
499
|
return existed;
|
|
166
500
|
}
|
|
167
501
|
function withStore(store) {
|
|
502
|
+
const crypto = globalThis.crypto;
|
|
503
|
+
const scope = 'scoped:' + (crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2, 14));
|
|
504
|
+
(0, health_js_1.registerStoreScope)(store, scope);
|
|
168
505
|
const scopedAct = async (key, fn, options = {}) => {
|
|
169
506
|
(0, validate_js_1.assertKey)(key);
|
|
170
507
|
(0, validate_js_1.assertOptions)(options);
|
|
171
508
|
const meta = { attempts: 1, source: 'fresh' };
|
|
172
|
-
const rootController = buildRootSignal(options.signal);
|
|
509
|
+
const { controller: rootController, cleanup } = buildRootSignal(options.signal);
|
|
510
|
+
const observability = buildObservability(options.observability, options.traceId);
|
|
511
|
+
const effectiveTraceId = observability?.traceId ?? options.traceId;
|
|
512
|
+
const startedAt = monotonicNow();
|
|
513
|
+
try {
|
|
514
|
+
(0, health_js_1.registerInflight)(scope);
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
cleanup();
|
|
518
|
+
const now = monotonicNow();
|
|
519
|
+
const durationMs = now - startedAt;
|
|
520
|
+
(0, health_js_1.recordError)(scope, 'ACTLY_RESOURCE_EXHAUSTED', (0, sanitize_js_1.sanitizeErrorMessage)(e));
|
|
521
|
+
if (observability) {
|
|
522
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
523
|
+
type: 'final-failure', key, traceId: observability.traceId, timestamp: Date.now(),
|
|
524
|
+
attempts: 0, durationMs, failedBy: 'resource-exhausted', error: e,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (options.audit) {
|
|
528
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, {
|
|
529
|
+
key, traceId: effectiveTraceId ?? '', timestamp: Date.now(), durationMs,
|
|
530
|
+
ok: false, attempts: 0, failedBy: 'resource-exhausted',
|
|
531
|
+
error: (0, sanitize_js_1.sanitizeError)(e),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return { ok: false, error: e, attempts: 0, traceId: effectiveTraceId, durationMs };
|
|
535
|
+
}
|
|
536
|
+
(0, shutdown_js_1.registerDrainable)(scope);
|
|
173
537
|
if (rootController.signal.aborted) {
|
|
174
|
-
|
|
538
|
+
cleanup();
|
|
539
|
+
(0, health_js_1.unregisterInflight)(scope);
|
|
540
|
+
(0, shutdown_js_1.unregisterDrainable)(scope);
|
|
541
|
+
const error = rootController.signal.reason;
|
|
542
|
+
const now = Date.now();
|
|
543
|
+
const durationMs = now - startedAt;
|
|
544
|
+
(0, health_js_1.recordError)(scope, 'ACTLY_ABORT', (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
545
|
+
if (observability) {
|
|
546
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
547
|
+
type: 'final-failure', key, traceId: observability.traceId, timestamp: now,
|
|
548
|
+
attempts: 0, durationMs, failedBy: 'abort', error,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (options.audit) {
|
|
552
|
+
const sanitizedError = (0, sanitize_js_1.sanitizeError)(error);
|
|
553
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: now, durationMs, ok: false, attempts: 0, failedBy: 'abort', error: sanitizedError });
|
|
554
|
+
}
|
|
555
|
+
return { ok: false, error, attempts: 0, traceId: effectiveTraceId, durationMs };
|
|
175
556
|
}
|
|
176
|
-
|
|
557
|
+
let policies;
|
|
177
558
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
559
|
+
policies = buildPolicies(options);
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
cleanup();
|
|
563
|
+
(0, health_js_1.unregisterInflight)(scope);
|
|
564
|
+
(0, shutdown_js_1.unregisterDrainable)(scope);
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
const durationMs = monotonicNow() - startedAt;
|
|
567
|
+
(0, health_js_1.recordError)(scope, 'ACTLY_VALIDATION', (0, sanitize_js_1.sanitizeErrorMessage)(e));
|
|
568
|
+
if (observability) {
|
|
569
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
570
|
+
type: 'final-failure', key, traceId: observability.traceId, timestamp: now,
|
|
571
|
+
attempts: 0, durationMs, failedBy: 'validation', error: e,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
if (options.audit) {
|
|
575
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, {
|
|
576
|
+
key, traceId: effectiveTraceId ?? '', timestamp: now, durationMs,
|
|
577
|
+
ok: false, attempts: 0, failedBy: 'validation',
|
|
578
|
+
error: (0, sanitize_js_1.sanitizeError)(e),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return { ok: false, error: e, attempts: 0, traceId: effectiveTraceId, durationMs };
|
|
582
|
+
}
|
|
583
|
+
const hasRetryPolicy = !!(options.retry && options.retry.attempts > 1);
|
|
584
|
+
if (observability && !hasRetryPolicy) {
|
|
585
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onAttempt, {
|
|
586
|
+
type: 'attempt', key, traceId: observability.traceId,
|
|
587
|
+
timestamp: Date.now(), attempt: 1,
|
|
185
588
|
});
|
|
186
|
-
|
|
589
|
+
}
|
|
590
|
+
const hedgePlacement = options.hedge?.placement ?? 'outside-retry';
|
|
591
|
+
const hedgeKeepLoser = options.hedge?.keepLoser ?? false;
|
|
592
|
+
const fnWithHedge = (options.hedge && hedgePlacement === 'inside-retry')
|
|
593
|
+
? wrapHedge(fn, options.hedge.delayMs, hedgeKeepLoser)
|
|
594
|
+
: fn;
|
|
595
|
+
const needsRaceAbort = options.signal !== undefined;
|
|
596
|
+
try {
|
|
597
|
+
let value;
|
|
598
|
+
if (options.hedge && hedgePlacement === 'outside-retry') {
|
|
599
|
+
const hedgeMeta = { attempts: 1, source: 'fresh' };
|
|
600
|
+
const chainFactory = (signal, m) => (0, executor_js_1.execute)({
|
|
601
|
+
key,
|
|
602
|
+
fn: fnWithHedge,
|
|
603
|
+
policies,
|
|
604
|
+
store,
|
|
605
|
+
meta: m,
|
|
606
|
+
signal,
|
|
607
|
+
observability,
|
|
608
|
+
});
|
|
609
|
+
const hedgeResult = needsRaceAbort
|
|
610
|
+
? await (0, abort_js_1.raceAbort)(Promise.resolve(runWithHedge(chainFactory, meta, hedgeMeta, rootController.signal, options.hedge.delayMs, hedgeKeepLoser)), rootController.signal)
|
|
611
|
+
: await runWithHedge(chainFactory, meta, hedgeMeta, rootController.signal, options.hedge.delayMs, hedgeKeepLoser);
|
|
612
|
+
meta.attempts = hedgeResult.winnerMeta.attempts;
|
|
613
|
+
meta.source = hedgeResult.winnerMeta.source;
|
|
614
|
+
value = hedgeResult.value;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const execPromise = (0, executor_js_1.execute)({
|
|
618
|
+
key,
|
|
619
|
+
fn: fnWithHedge,
|
|
620
|
+
policies,
|
|
621
|
+
store,
|
|
622
|
+
meta,
|
|
623
|
+
signal: rootController.signal,
|
|
624
|
+
observability,
|
|
625
|
+
});
|
|
626
|
+
value = needsRaceAbort
|
|
627
|
+
? await (0, abort_js_1.raceAbort)(Promise.resolve(execPromise), rootController.signal)
|
|
628
|
+
: await execPromise;
|
|
629
|
+
}
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
const durationMs = monotonicNow() - startedAt;
|
|
632
|
+
(0, health_js_1.recordSuccess)(scope);
|
|
633
|
+
if (observability) {
|
|
634
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalSuccess, {
|
|
635
|
+
type: 'final-success', key, traceId: observability.traceId, timestamp: now,
|
|
636
|
+
source: meta.source, attempts: meta.attempts, durationMs,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (options.audit) {
|
|
640
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: now, durationMs, ok: true, attempts: meta.attempts });
|
|
641
|
+
}
|
|
642
|
+
return { ok: true, value, source: meta.source, attempts: meta.attempts, traceId: effectiveTraceId, durationMs };
|
|
187
643
|
}
|
|
188
644
|
catch (error) {
|
|
189
|
-
|
|
645
|
+
const now = Date.now();
|
|
646
|
+
const durationMs = monotonicNow() - startedAt;
|
|
647
|
+
const failedBy = rootController.signal.aborted ? 'abort' : classifyFailure(error);
|
|
648
|
+
let errorRecorded = false;
|
|
649
|
+
if (options.fallback) {
|
|
650
|
+
(0, health_js_1.recordError)(scope, failedBy, (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
651
|
+
errorRecorded = true;
|
|
652
|
+
try {
|
|
653
|
+
const fallbackValue = typeof options.fallback.value === 'function'
|
|
654
|
+
? await options.fallback.value()
|
|
655
|
+
: options.fallback.value;
|
|
656
|
+
if (observability) {
|
|
657
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalSuccess, {
|
|
658
|
+
type: 'final-success', key, traceId: observability.traceId, timestamp: Date.now(),
|
|
659
|
+
source: meta.source, attempts: meta.attempts, durationMs,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (options.audit) {
|
|
663
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: Date.now(), durationMs, ok: true, attempts: meta.attempts });
|
|
664
|
+
}
|
|
665
|
+
return { ok: true, value: fallbackValue, source: 'fresh', attempts: meta.attempts, traceId: effectiveTraceId, durationMs };
|
|
666
|
+
}
|
|
667
|
+
catch (fallbackErr) {
|
|
668
|
+
if (observability) {
|
|
669
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
670
|
+
type: 'final-failure', key, traceId: observability.traceId, timestamp: Date.now(),
|
|
671
|
+
attempts: meta.attempts, durationMs, failedBy: 'fn-error', error: fallbackErr,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
|
|
675
|
+
console.warn('Actly: fallback threw — error swallowed, surfacing original fn error.', fallbackErr);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!errorRecorded) {
|
|
680
|
+
(0, health_js_1.recordError)(scope, failedBy, (0, sanitize_js_1.sanitizeErrorMessage)(error));
|
|
681
|
+
}
|
|
682
|
+
if (observability) {
|
|
683
|
+
(0, safeCall_js_1.safeCall)(observability.hooks.onFinalFailure, {
|
|
684
|
+
type: 'final-failure', key, traceId: observability.traceId, timestamp: now,
|
|
685
|
+
attempts: meta.attempts, durationMs, failedBy, error,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
if (options.audit) {
|
|
689
|
+
const sanitizedError = (0, sanitize_js_1.sanitizeError)(error);
|
|
690
|
+
(0, safeCall_js_1.safeCall)(options.audit.log, { key, traceId: effectiveTraceId ?? '', timestamp: Date.now(), durationMs, ok: false, attempts: meta.attempts, failedBy, error: sanitizedError });
|
|
691
|
+
}
|
|
692
|
+
return { ok: false, error, attempts: meta.attempts, traceId: effectiveTraceId, durationMs };
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
cleanup();
|
|
696
|
+
(0, health_js_1.unregisterInflight)(scope);
|
|
697
|
+
(0, shutdown_js_1.unregisterDrainable)(scope);
|
|
190
698
|
}
|
|
191
699
|
};
|
|
192
|
-
// Build the `invalidate` implementation. The runtime branch on
|
|
193
|
-
// `isSyncStore` selects the correct path; the cast through `unknown`
|
|
194
|
-
// is required because TypeScript cannot narrow the union return type
|
|
195
|
-
// (`boolean | Promise<boolean>`) to match either overload signature
|
|
196
|
-
// individually. The overloads at the call site guarantee callers see
|
|
197
|
-
// the correct type.
|
|
198
700
|
const invalidateImpl = (key) => {
|
|
199
701
|
(0, validate_js_1.assertKey)(key);
|
|
200
702
|
const cacheKey = CACHE_NS + key;
|
|
201
703
|
if ((0, base_js_1.isSyncStore)(store)) {
|
|
704
|
+
if (typeof store.deleteIfExists === 'function') {
|
|
705
|
+
return store.deleteIfExists(cacheKey);
|
|
706
|
+
}
|
|
202
707
|
const existed = store.has(cacheKey);
|
|
203
708
|
store.delete(cacheKey);
|
|
204
709
|
return existed;
|
|
205
710
|
}
|
|
206
|
-
|
|
711
|
+
if (typeof store.deleteIfExists === 'function') {
|
|
712
|
+
return store.deleteIfExists(cacheKey);
|
|
713
|
+
}
|
|
207
714
|
return (async () => {
|
|
208
715
|
const existed = await store.has(cacheKey);
|
|
209
716
|
await store.delete(cacheKey);
|
|
210
717
|
return existed;
|
|
211
718
|
})();
|
|
212
719
|
};
|
|
213
|
-
// Attach `invalidate` and `store` to the function object. We use
|
|
214
|
-
// `Object.assign` rather than mutation so the types narrow cleanly at
|
|
215
|
-
// the call site. The cast through `unknown` is necessary because the
|
|
216
|
-
// implementation signature is wider than either overload.
|
|
217
720
|
return Object.assign(scopedAct, {
|
|
218
721
|
invalidate: invalidateImpl,
|
|
219
722
|
store,
|
|
723
|
+
scope,
|
|
220
724
|
});
|
|
221
725
|
}
|