actly 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -0
- package/MIGRATION.md +141 -0
- package/README.md +326 -218
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +406 -220
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +405 -222
- package/dist/core/executor.cjs +0 -27
- package/dist/core/executor.d.ts +0 -41
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +0 -28
- package/dist/core/health.cjs +146 -17
- package/dist/core/health.d.ts +20 -5
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +140 -18
- package/dist/core/shutdown.cjs +19 -8
- package/dist/core/shutdown.d.ts +1 -4
- package/dist/core/shutdown.d.ts.map +1 -1
- package/dist/core/shutdown.js +18 -9
- package/dist/core/tenant.cjs +61 -19
- package/dist/core/tenant.d.ts +5 -10
- package/dist/core/tenant.d.ts.map +1 -1
- package/dist/core/tenant.js +61 -20
- package/dist/errors.cjs +70 -103
- package/dist/errors.d.ts +33 -90
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +66 -103
- package/dist/index.cjs +25 -22
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/observability.cjs +0 -39
- package/dist/observability.d.ts +19 -78
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +0 -40
- package/dist/policies/bulkhead.cjs +44 -4
- package/dist/policies/bulkhead.d.ts +0 -0
- package/dist/policies/bulkhead.d.ts.map +1 -1
- package/dist/policies/bulkhead.js +44 -5
- package/dist/policies/cache.cjs +22 -142
- package/dist/policies/cache.d.ts +0 -46
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +22 -143
- package/dist/policies/circuitBreaker.cjs +125 -31
- package/dist/policies/circuitBreaker.d.ts +0 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -1
- package/dist/policies/circuitBreaker.js +125 -32
- package/dist/policies/dedupe.cjs +19 -66
- package/dist/policies/dedupe.d.ts +0 -23
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +19 -67
- package/dist/policies/noop.cjs +6 -0
- package/dist/policies/noop.d.ts +3 -0
- package/dist/policies/noop.d.ts.map +1 -0
- package/dist/policies/noop.js +3 -0
- package/dist/policies/rateLimit.cjs +15 -5
- package/dist/policies/rateLimit.d.ts +0 -0
- package/dist/policies/rateLimit.d.ts.map +1 -1
- package/dist/policies/rateLimit.js +15 -6
- package/dist/policies/retry.cjs +100 -65
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +100 -66
- package/dist/policies/timeout.cjs +32 -60
- package/dist/policies/timeout.d.ts +0 -13
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +32 -71
- package/dist/stores/base.cjs +0 -10
- package/dist/stores/base.d.ts +4 -67
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +0 -11
- package/dist/stores/memory.cjs +75 -88
- package/dist/stores/memory.d.ts +7 -74
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +75 -89
- package/dist/testing/index.cjs +56 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +52 -0
- package/dist/types/index.cjs +0 -1
- package/dist/types/index.d.ts +17 -307
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +14 -111
- package/dist/utils/abort.d.ts +3 -74
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +14 -112
- package/dist/utils/abortPool.cjs +1 -2
- package/dist/utils/abortPool.d.ts +0 -0
- package/dist/utils/abortPool.d.ts.map +1 -1
- package/dist/utils/abortPool.js +1 -3
- package/dist/utils/backoff.cjs +3 -21
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +3 -22
- package/dist/utils/decorator.cjs +44 -0
- package/dist/utils/decorator.d.ts +3 -0
- package/dist/utils/decorator.d.ts.map +1 -0
- package/dist/utils/decorator.js +41 -0
- package/dist/utils/key.cjs +13 -45
- package/dist/utils/key.d.ts +0 -27
- package/dist/utils/key.d.ts.map +1 -1
- package/dist/utils/key.js +13 -46
- package/dist/utils/limits.cjs +9 -27
- package/dist/utils/limits.d.ts +9 -27
- package/dist/utils/limits.d.ts.map +1 -1
- package/dist/utils/limits.js +9 -28
- package/dist/utils/safeCall.cjs +28 -0
- package/dist/utils/safeCall.d.ts +2 -0
- package/dist/utils/safeCall.d.ts.map +1 -0
- package/dist/utils/safeCall.js +25 -0
- package/dist/utils/sanitize.cjs +22 -1
- package/dist/utils/sanitize.d.ts +0 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +22 -2
- package/dist/utils/validate.cjs +81 -18
- package/dist/utils/validate.d.ts +6 -19
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +81 -19
- package/package.json +38 -4
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/core/health.js.map +0 -1
- package/dist/core/shutdown.js.map +0 -1
- package/dist/core/tenant.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observability.js.map +0 -1
- package/dist/policies/bulkhead.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/circuitBreaker.js.map +0 -1
- package/dist/policies/dedupe.js.map +0 -1
- package/dist/policies/rateLimit.js.map +0 -1
- package/dist/policies/retry.js.map +0 -1
- package/dist/policies/timeout.js.map +0 -1
- package/dist/state/store.cjs +0 -14
- package/dist/state/store.d.ts +0 -11
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -11
- package/dist/state/store.js.map +0 -1
- package/dist/stores/base.js.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/abort.js.map +0 -1
- package/dist/utils/abortPool.js.map +0 -1
- package/dist/utils/backoff.js.map +0 -1
- package/dist/utils/key.js.map +0 -1
- package/dist/utils/limits.js.map +0 -1
- package/dist/utils/sanitize.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/policies/retry.js
CHANGED
|
@@ -1,122 +1,156 @@
|
|
|
1
1
|
import { computeDelay } from '../utils/backoff.js';
|
|
2
2
|
import { isAbortError, sleep } from '../utils/abort.js';
|
|
3
|
+
import { safeCall } from '../utils/safeCall.js';
|
|
3
4
|
import { RetryExhaustedError } from '../errors.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Default `shouldRetry`: retry on any error EXCEPT abort errors.
|
|
7
|
-
*
|
|
8
|
-
* Abort errors indicate the caller or a timeout cancelled the operation —
|
|
9
|
-
* retrying would just abort again on the next attempt, wasting delay budget.
|
|
10
|
-
*
|
|
11
|
-
* User-supplied `shouldRetry` overrides this entirely.
|
|
12
|
-
*/
|
|
5
|
+
import { LIMITS } from '../utils/limits.js';
|
|
13
6
|
function defaultShouldRetry(error, _attempt) {
|
|
14
|
-
|
|
7
|
+
if (isAbortError(error))
|
|
8
|
+
return false;
|
|
9
|
+
if (typeof error === 'object' && error !== null) {
|
|
10
|
+
const code = error.code;
|
|
11
|
+
if (code === 'ACTLY_TIMEOUT' || code === 'ACTLY_TOTAL_TIMEOUT')
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
15
|
}
|
|
16
|
-
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
17
|
-
/**
|
|
18
|
-
* Retry `fn` up to `opts.attempts` times on retryable errors.
|
|
19
|
-
*
|
|
20
|
-
* Writes the live attempt count into `ctx.meta.attempts` so `act()` can
|
|
21
|
-
* report it in the final `ActResult`.
|
|
22
|
-
*
|
|
23
|
-
* # Signal awareness
|
|
24
|
-
*
|
|
25
|
-
* - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
|
|
26
|
-
* `totalTimeout` or caller) has aborted, throws the parent's reason
|
|
27
|
-
* immediately — no more attempts.
|
|
28
|
-
* - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
|
|
29
|
-
* aborts mid-delay, the sleep rejects immediately instead of blocking
|
|
30
|
-
* the loop until the timer would have elapsed.
|
|
31
|
-
*
|
|
32
|
-
* # `shouldRetry` invocation
|
|
33
|
-
*
|
|
34
|
-
* Called after EVERY failure, including the last attempt. This preserves the
|
|
35
|
-
* predicate's contract for observers / metrics that rely on it being called
|
|
36
|
-
* per-attempt. The return value is only consulted when `attempt < max`.
|
|
37
|
-
*
|
|
38
|
-
* # Default predicate
|
|
39
|
-
*
|
|
40
|
-
* If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
|
|
41
|
-
* every error EXCEPT abort errors (so timeouts and caller cancellations
|
|
42
|
-
* don't waste retry budget).
|
|
43
|
-
*/
|
|
44
16
|
export function retryPolicy(opts) {
|
|
45
17
|
const max = Math.max(1, Math.floor(opts.attempts));
|
|
46
18
|
const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
|
|
19
|
+
const shouldRetryResult = opts.shouldRetryResult;
|
|
20
|
+
const dangerouslyUnref = opts.dangerouslyUnref === true;
|
|
21
|
+
const sleepOpts = dangerouslyUnref ? { unref: true } : undefined;
|
|
22
|
+
const backoffFn = opts.backoffFn;
|
|
47
23
|
return (fn, ctx) => async (parentSignal) => {
|
|
48
|
-
const
|
|
49
|
-
let
|
|
24
|
+
const backoffState = {};
|
|
25
|
+
let errors;
|
|
50
26
|
let retriedAtLeastOnce = false;
|
|
51
27
|
const obs = ctx.observability;
|
|
52
28
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
53
|
-
// Parent (totalTimeout or caller signal) already aborted — bail.
|
|
54
29
|
if (parentSignal.aborted)
|
|
55
30
|
throw parentSignal.reason;
|
|
56
31
|
ctx.meta.attempts = attempt;
|
|
57
32
|
const attemptStart = Date.now();
|
|
58
33
|
try {
|
|
59
|
-
// Emit onAttempt before each attempt. (We could emit
|
|
60
|
-
// after with duration/error, but before lets observers track
|
|
61
|
-
// in-flight attempts in real time.)
|
|
62
34
|
if (obs) {
|
|
63
|
-
obs.hooks.onAttempt
|
|
35
|
+
safeCall(obs.hooks.onAttempt, {
|
|
64
36
|
type: 'attempt', key: ctx.key, traceId: obs.traceId,
|
|
65
37
|
timestamp: attemptStart, attempt,
|
|
66
38
|
});
|
|
67
39
|
}
|
|
68
|
-
|
|
40
|
+
const value = await fn(parentSignal);
|
|
41
|
+
if (shouldRetryResult) {
|
|
42
|
+
let accept;
|
|
43
|
+
try {
|
|
44
|
+
const raw = shouldRetryResult(value, attempt);
|
|
45
|
+
accept = typeof raw === 'boolean' ? raw : true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (accept)
|
|
51
|
+
return value;
|
|
52
|
+
const syntheticError = new Error(`Actly: shouldRetryResult returned false on attempt ${attempt}`);
|
|
53
|
+
if (errors === undefined)
|
|
54
|
+
errors = [];
|
|
55
|
+
if (errors.length < 10) {
|
|
56
|
+
errors.push(syntheticError);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
errors.shift();
|
|
60
|
+
errors.push(syntheticError);
|
|
61
|
+
}
|
|
62
|
+
if (attempt >= max) {
|
|
63
|
+
if (obs) {
|
|
64
|
+
safeCall(obs.hooks.onRetry, {
|
|
65
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
66
|
+
timestamp: Date.now(), attempt, delayMs: 0,
|
|
67
|
+
error: syntheticError,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
retriedAtLeastOnce = true;
|
|
73
|
+
if (parentSignal.aborted)
|
|
74
|
+
throw parentSignal.reason;
|
|
75
|
+
let rawDelay;
|
|
76
|
+
try {
|
|
77
|
+
rawDelay = backoffFn
|
|
78
|
+
? backoffFn(attempt, syntheticError, backoffState)
|
|
79
|
+
: computeDelay(attempt, opts);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
rawDelay = computeDelay(attempt, opts);
|
|
83
|
+
}
|
|
84
|
+
const safeDelay = Number.isFinite(rawDelay) ? rawDelay : 0;
|
|
85
|
+
const delay = Math.min(Math.max(0, safeDelay), LIMITS.MAX_RETRY_DELAY_MS);
|
|
86
|
+
if (obs) {
|
|
87
|
+
safeCall(obs.hooks.onRetry, {
|
|
88
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
89
|
+
timestamp: Date.now(), attempt, delayMs: delay,
|
|
90
|
+
error: syntheticError,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (delay > 0)
|
|
94
|
+
await sleep(delay, parentSignal, sleepOpts);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
69
98
|
}
|
|
70
99
|
catch (err) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
if (errors === undefined)
|
|
101
|
+
errors = [];
|
|
102
|
+
if (errors.length < 10) {
|
|
103
|
+
errors.push(err);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
errors.shift();
|
|
107
|
+
errors.push(err);
|
|
108
|
+
}
|
|
75
109
|
let retryable;
|
|
76
110
|
try {
|
|
77
111
|
retryable = shouldRetry(err, attempt);
|
|
78
112
|
}
|
|
79
113
|
catch {
|
|
80
|
-
// Predicate threw — don't retry, surface original error
|
|
81
114
|
throw err;
|
|
82
115
|
}
|
|
83
116
|
if (attempt >= max) {
|
|
84
|
-
// Last attempt. If we retried at least once (predicate
|
|
85
|
-
// allowed retries), wrap in RetryExhaustedError for context.
|
|
86
117
|
if (retriedAtLeastOnce) {
|
|
87
118
|
throw new RetryExhaustedError({
|
|
88
119
|
key: ctx.key,
|
|
89
120
|
attempts: attempt,
|
|
90
121
|
lastError: err,
|
|
91
|
-
errors,
|
|
122
|
+
errors: errors ?? [],
|
|
92
123
|
});
|
|
93
124
|
}
|
|
94
125
|
throw err;
|
|
95
126
|
}
|
|
96
|
-
if (!retryable)
|
|
97
|
-
// Non-retryable error — bail immediately.
|
|
127
|
+
if (!retryable)
|
|
98
128
|
throw err;
|
|
99
|
-
}
|
|
100
|
-
// Mark that we retried at least once — affects final wrap.
|
|
101
129
|
retriedAtLeastOnce = true;
|
|
102
|
-
// Parent aborted mid-attempt — don't sleep, bail.
|
|
103
130
|
if (parentSignal.aborted)
|
|
104
131
|
throw parentSignal.reason;
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
let rawDelay;
|
|
133
|
+
try {
|
|
134
|
+
rawDelay = backoffFn
|
|
135
|
+
? backoffFn(attempt, err, backoffState)
|
|
136
|
+
: computeDelay(attempt, opts);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
rawDelay = computeDelay(attempt, opts);
|
|
140
|
+
}
|
|
141
|
+
const safeDelay = Number.isFinite(rawDelay) ? rawDelay : 0;
|
|
142
|
+
const delay = Math.min(Math.max(0, safeDelay), LIMITS.MAX_RETRY_DELAY_MS);
|
|
107
143
|
if (obs) {
|
|
108
|
-
obs.hooks.onRetry
|
|
144
|
+
safeCall(obs.hooks.onRetry, {
|
|
109
145
|
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
110
146
|
timestamp: Date.now(), attempt, delayMs: delay, error: err,
|
|
111
147
|
});
|
|
112
148
|
}
|
|
113
149
|
if (delay > 0) {
|
|
114
|
-
await sleep(delay, parentSignal);
|
|
150
|
+
await sleep(delay, parentSignal, sleepOpts);
|
|
115
151
|
}
|
|
116
152
|
}
|
|
117
153
|
}
|
|
118
|
-
|
|
119
|
-
throw lastErr;
|
|
154
|
+
throw new Error('Actly: retryPolicy reached unreachable state');
|
|
120
155
|
};
|
|
121
156
|
}
|
|
122
|
-
//# sourceMappingURL=retry.js.map
|
|
@@ -4,58 +4,43 @@ exports.TotalTimeoutError = exports.TimeoutError = void 0;
|
|
|
4
4
|
exports.timeoutPolicy = timeoutPolicy;
|
|
5
5
|
exports.totalTimeoutPolicy = totalTimeoutPolicy;
|
|
6
6
|
const abort_js_1 = require("../utils/abort.js");
|
|
7
|
+
const safeCall_js_1 = require("../utils/safeCall.js");
|
|
7
8
|
const errors_js_1 = require("../errors.js");
|
|
8
9
|
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
|
|
9
10
|
Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return errors_js_1.TotalTimeoutError; } });
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Build a timeout policy that throws `ErrorCtor` on deadline.
|
|
13
|
-
*
|
|
14
|
-
* # Cancellation contract
|
|
15
|
-
*
|
|
16
|
-
* Each invocation:
|
|
17
|
-
* 1. Creates a fresh `AbortController` for this attempt.
|
|
18
|
-
* 2. Arms a `setTimeout` that aborts the controller with a fresh `ErrorCtor(ms)`.
|
|
19
|
-
* 3. Links the parent signal: if the parent aborts (e.g. `totalTimeout` or
|
|
20
|
-
* caller cancellation), the child aborts with the parent's reason.
|
|
21
|
-
* 4. Races `fn(childSignal)` against the abort event.
|
|
22
|
-
*
|
|
23
|
-
* The race is critical: it ensures `act()` returns promptly even if `fn`
|
|
24
|
-
* ignores the signal. The underlying `fn` may keep running in the background
|
|
25
|
-
* (resource leak), but the caller is unblocked. This is the best JavaScript
|
|
26
|
-
* can do without cooperation from `fn`.
|
|
27
|
-
*
|
|
28
|
-
* If `fn` cooperates (passes `signal` to `fetch`, `AbortController`, etc.),
|
|
29
|
-
* the underlying work is cancelled properly — no leak.
|
|
30
|
-
*
|
|
31
|
-
* # Error attribution
|
|
32
|
-
*
|
|
33
|
-
* If the per-attempt timer fires, we throw `ErrorCtor(ms)` regardless of
|
|
34
|
-
* what `fn` does. If the parent signal fires first, we throw the parent's
|
|
35
|
-
* reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
|
|
36
|
-
*/
|
|
37
|
-
function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
11
|
+
function makeTimeoutPolicy(opts, errorCtor, kind) {
|
|
12
|
+
const strategy = opts.strategy ?? 'race';
|
|
38
13
|
return (fn, ctx) => async (parentSignal) => {
|
|
39
14
|
const controller = new AbortController();
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
15
|
+
const timerError = new errorCtor(opts.ms, { key: ctx.key });
|
|
16
|
+
const obs = ctx.observability;
|
|
17
|
+
let timedOut = false;
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
timedOut = true;
|
|
20
|
+
if (obs) {
|
|
21
|
+
(0, safeCall_js_1.safeCall)(obs.hooks.onTimeout, {
|
|
22
|
+
type: 'timeout', key: ctx.key, traceId: obs.traceId,
|
|
23
|
+
timestamp: Date.now(), kind, ms: opts.ms,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
controller.abort(timerError);
|
|
27
|
+
}, opts.ms);
|
|
52
28
|
const unlink = (0, abort_js_1.linkSignal)(parentSignal, controller);
|
|
53
29
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
30
|
+
if (strategy === 'cooperative') {
|
|
31
|
+
if (parentSignal.aborted) {
|
|
32
|
+
throw parentSignal.reason;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const value = await fn(controller.signal);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (timedOut)
|
|
40
|
+
throw timerError;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
59
44
|
return await new Promise((resolve, reject) => {
|
|
60
45
|
if (controller.signal.aborted) {
|
|
61
46
|
reject(controller.signal.reason);
|
|
@@ -78,22 +63,9 @@ function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
|
78
63
|
}
|
|
79
64
|
};
|
|
80
65
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
83
|
-
*
|
|
84
|
-
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
85
|
-
* own clock.
|
|
86
|
-
*/
|
|
87
66
|
function timeoutPolicy(opts) {
|
|
88
|
-
return makeTimeoutPolicy(opts, errors_js_1.TimeoutError);
|
|
67
|
+
return makeTimeoutPolicy(opts, errors_js_1.TimeoutError, 'per-attempt');
|
|
89
68
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
92
|
-
* delays) against a hard budget that does NOT reset.
|
|
93
|
-
*
|
|
94
|
-
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
95
|
-
* policy runs and stops regardless of what the inner chain is doing.
|
|
96
|
-
*/
|
|
97
69
|
function totalTimeoutPolicy(opts) {
|
|
98
|
-
return makeTimeoutPolicy(opts, errors_js_1.TotalTimeoutError);
|
|
70
|
+
return makeTimeoutPolicy(opts, errors_js_1.TotalTimeoutError, 'total');
|
|
99
71
|
}
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import type { PolicyApplier, TimeoutOptions } from '../types/index.js';
|
|
2
2
|
import { TimeoutError, TotalTimeoutError } from '../errors.js';
|
|
3
3
|
export { TimeoutError, TotalTimeoutError };
|
|
4
|
-
/**
|
|
5
|
-
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
6
|
-
*
|
|
7
|
-
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
8
|
-
* own clock.
|
|
9
|
-
*/
|
|
10
4
|
export declare function timeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
|
|
11
|
-
/**
|
|
12
|
-
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
13
|
-
* delays) against a hard budget that does NOT reset.
|
|
14
|
-
*
|
|
15
|
-
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
16
|
-
* policy runs and stops regardless of what the inner chain is doing.
|
|
17
|
-
*/
|
|
18
5
|
export declare function totalTimeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
|
|
19
6
|
//# sourceMappingURL=timeout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAG5F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAI9D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAA;AAyG1C,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAMD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
|
package/dist/policies/timeout.js
CHANGED
|
@@ -1,65 +1,40 @@
|
|
|
1
1
|
import { linkSignal } from '../utils/abort.js';
|
|
2
|
+
import { safeCall } from '../utils/safeCall.js';
|
|
2
3
|
import { TimeoutError, TotalTimeoutError } from '../errors.js';
|
|
3
|
-
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
4
|
-
//
|
|
5
|
-
// TimeoutError and TotalTimeoutError live in `src/errors.ts` and
|
|
6
|
-
// now extend `ActlyError` (which extends `Error`). Existing `instanceof
|
|
7
|
-
// Error` and `instanceof TimeoutError` checks continue to work; new
|
|
8
|
-
// `instanceof ActlyError` and `.code` field give consumers a stable
|
|
9
|
-
// discriminator for telemetry.
|
|
10
|
-
//
|
|
11
|
-
// Re-exported here for backwards compatibility with code that imports
|
|
12
|
-
// from `'actly/policies/timeout'` (the deep path).
|
|
13
4
|
export { TimeoutError, TotalTimeoutError };
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* Build a timeout policy that throws `ErrorCtor` on deadline.
|
|
17
|
-
*
|
|
18
|
-
* # Cancellation contract
|
|
19
|
-
*
|
|
20
|
-
* Each invocation:
|
|
21
|
-
* 1. Creates a fresh `AbortController` for this attempt.
|
|
22
|
-
* 2. Arms a `setTimeout` that aborts the controller with a fresh `ErrorCtor(ms)`.
|
|
23
|
-
* 3. Links the parent signal: if the parent aborts (e.g. `totalTimeout` or
|
|
24
|
-
* caller cancellation), the child aborts with the parent's reason.
|
|
25
|
-
* 4. Races `fn(childSignal)` against the abort event.
|
|
26
|
-
*
|
|
27
|
-
* The race is critical: it ensures `act()` returns promptly even if `fn`
|
|
28
|
-
* ignores the signal. The underlying `fn` may keep running in the background
|
|
29
|
-
* (resource leak), but the caller is unblocked. This is the best JavaScript
|
|
30
|
-
* can do without cooperation from `fn`.
|
|
31
|
-
*
|
|
32
|
-
* If `fn` cooperates (passes `signal` to `fetch`, `AbortController`, etc.),
|
|
33
|
-
* the underlying work is cancelled properly — no leak.
|
|
34
|
-
*
|
|
35
|
-
* # Error attribution
|
|
36
|
-
*
|
|
37
|
-
* If the per-attempt timer fires, we throw `ErrorCtor(ms)` regardless of
|
|
38
|
-
* what `fn` does. If the parent signal fires first, we throw the parent's
|
|
39
|
-
* reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
|
|
40
|
-
*/
|
|
41
|
-
function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
5
|
+
function makeTimeoutPolicy(opts, errorCtor, kind) {
|
|
6
|
+
const strategy = opts.strategy ?? 'race';
|
|
42
7
|
return (fn, ctx) => async (parentSignal) => {
|
|
43
8
|
const controller = new AbortController();
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
9
|
+
const timerError = new errorCtor(opts.ms, { key: ctx.key });
|
|
10
|
+
const obs = ctx.observability;
|
|
11
|
+
let timedOut = false;
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
timedOut = true;
|
|
14
|
+
if (obs) {
|
|
15
|
+
safeCall(obs.hooks.onTimeout, {
|
|
16
|
+
type: 'timeout', key: ctx.key, traceId: obs.traceId,
|
|
17
|
+
timestamp: Date.now(), kind, ms: opts.ms,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
controller.abort(timerError);
|
|
21
|
+
}, opts.ms);
|
|
56
22
|
const unlink = linkSignal(parentSignal, controller);
|
|
57
23
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
24
|
+
if (strategy === 'cooperative') {
|
|
25
|
+
if (parentSignal.aborted) {
|
|
26
|
+
throw parentSignal.reason;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const value = await fn(controller.signal);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (timedOut)
|
|
34
|
+
throw timerError;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
63
38
|
return await new Promise((resolve, reject) => {
|
|
64
39
|
if (controller.signal.aborted) {
|
|
65
40
|
reject(controller.signal.reason);
|
|
@@ -82,23 +57,9 @@ function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
|
82
57
|
}
|
|
83
58
|
};
|
|
84
59
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
87
|
-
*
|
|
88
|
-
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
89
|
-
* own clock.
|
|
90
|
-
*/
|
|
91
60
|
export function timeoutPolicy(opts) {
|
|
92
|
-
return makeTimeoutPolicy(opts, TimeoutError);
|
|
61
|
+
return makeTimeoutPolicy(opts, TimeoutError, 'per-attempt');
|
|
93
62
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
96
|
-
* delays) against a hard budget that does NOT reset.
|
|
97
|
-
*
|
|
98
|
-
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
99
|
-
* policy runs and stops regardless of what the inner chain is doing.
|
|
100
|
-
*/
|
|
101
63
|
export function totalTimeoutPolicy(opts) {
|
|
102
|
-
return makeTimeoutPolicy(opts, TotalTimeoutError);
|
|
64
|
+
return makeTimeoutPolicy(opts, TotalTimeoutError, 'total');
|
|
103
65
|
}
|
|
104
|
-
//# sourceMappingURL=timeout.js.map
|
package/dist/stores/base.cjs
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// ─── Store interfaces ─────────────────────────────────────────────────────────
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.isSyncStore = isSyncStore;
|
|
5
4
|
exports.isAsyncStore = isAsyncStore;
|
|
6
|
-
// ─── Type guards ──────────────────────────────────────────────────────────────
|
|
7
|
-
/**
|
|
8
|
-
* Narrows `AnyStateStore` to `SyncStateStore` via the `_sync` discriminant.
|
|
9
|
-
* Used by `execute()` and `cachePolicy` to branch between sync and async paths.
|
|
10
|
-
*/
|
|
11
5
|
function isSyncStore(store) {
|
|
12
6
|
return store._sync === true;
|
|
13
7
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Narrows `AnyStateStore` to `AsyncStateStore` via the `_sync` discriminant.
|
|
16
|
-
* Provided for symmetry; prefer `isSyncStore` for the common guard pattern.
|
|
17
|
-
*/
|
|
18
8
|
function isAsyncStore(store) {
|
|
19
9
|
return store._sync === false;
|
|
20
10
|
}
|
package/dist/stores/base.d.ts
CHANGED
|
@@ -1,88 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Synchronous key-value store. All operations complete in the same tick.
|
|
3
|
-
*
|
|
4
|
-
* # Why synchronous?
|
|
5
|
-
*
|
|
6
|
-
* `dedupePolicy` must read an in-flight Promise from the store and, if absent,
|
|
7
|
-
* write a new one — all within a single synchronous frame. If `get()` were
|
|
8
|
-
* async, two concurrent callers could both observe a miss before either
|
|
9
|
-
* write lands, defeating deduplication entirely. There is no lock primitive
|
|
10
|
-
* in JavaScript that can paper over this: the constraint is structural, not
|
|
11
|
-
* implementation-level.
|
|
12
|
-
*
|
|
13
|
-
* # The `_sync` discriminant
|
|
14
|
-
*
|
|
15
|
-
* `_sync: true` is a runtime tag that lets `execute()` enforce the dedupe
|
|
16
|
-
* constraint for plain-JS callers who bypass TypeScript. It is NOT part of
|
|
17
|
-
* the semantic contract and MUST NOT be used for anything beyond that guard.
|
|
18
|
-
* Implementations must set it as a `readonly` literal (`true as const`).
|
|
19
|
-
*
|
|
20
|
-
* # Lifecycle
|
|
21
|
-
*
|
|
22
|
-
* Implementations that hold background resources (timers, connections) should
|
|
23
|
-
* expose a `destroy()` method as a convention. See `InMemoryStore` for the
|
|
24
|
-
* reference pattern.
|
|
25
|
-
*/
|
|
26
1
|
export interface SyncStateStore {
|
|
27
|
-
/** Runtime discriminant. Must be `true as const`. */
|
|
28
2
|
readonly _sync: true;
|
|
29
3
|
get<T>(key: string): T | undefined;
|
|
30
4
|
set<T>(key: string, value: T, ttlMs?: number): void;
|
|
31
5
|
delete(key: string): void;
|
|
32
6
|
has(key: string): boolean;
|
|
33
|
-
|
|
34
|
-
* Remove all entries synchronously.
|
|
35
|
-
* After `clear()` returns, `size()` returns 0.
|
|
36
|
-
*/
|
|
7
|
+
deleteIfExists?(key: string): boolean;
|
|
37
8
|
clear(): void;
|
|
38
|
-
/**
|
|
39
|
-
* Return the count of live (non-expired) entries.
|
|
40
|
-
* Side-effect free: does not mutate the store. Implementations MAY evict
|
|
41
|
-
* expired entries opportunistically during this call, but MUST NOT have
|
|
42
|
-
* observable side effects beyond internal cleanup.
|
|
43
|
-
*/
|
|
44
9
|
size(): number;
|
|
10
|
+
destroy?(): void;
|
|
45
11
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Asynchronous key-value store. All operations return Promises.
|
|
48
|
-
*
|
|
49
|
-
* # Policy compatibility
|
|
50
|
-
*
|
|
51
|
-
* Compatible with `cachePolicy` only. Passing an `AsyncStateStore` to a
|
|
52
|
-
* policy chain that includes `dedupePolicy` is a TypeScript error and a
|
|
53
|
-
* runtime error — `execute()` throws at chain-build time. See
|
|
54
|
-
* `SyncStateStore` for why dedupe requires synchronous access.
|
|
55
|
-
*
|
|
56
|
-
* # TTL semantics
|
|
57
|
-
*
|
|
58
|
-
* The store is responsible for honouring `ttlMs`. Actly passes it as a hint.
|
|
59
|
-
* Implementations may delegate to a native TTL mechanism (e.g. Redis EXPIRE).
|
|
60
|
-
*
|
|
61
|
-
* # The `_sync` discriminant
|
|
62
|
-
*
|
|
63
|
-
* `_sync: false` mirrors the discriminant on `SyncStateStore`. Must be set
|
|
64
|
-
* as a `readonly` literal (`false as const`).
|
|
65
|
-
*/
|
|
66
12
|
export interface AsyncStateStore {
|
|
67
|
-
/** Runtime discriminant. Must be `false as const`. */
|
|
68
13
|
readonly _sync: false;
|
|
69
14
|
get<T>(key: string): Promise<T | undefined>;
|
|
70
15
|
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
71
16
|
delete(key: string): Promise<void>;
|
|
72
17
|
has(key: string): Promise<boolean>;
|
|
73
|
-
|
|
18
|
+
deleteIfExists?(key: string): Promise<boolean>;
|
|
74
19
|
clear(): Promise<void>;
|
|
75
|
-
/** Return the count of live (non-expired) entries. */
|
|
76
20
|
size(): Promise<number>;
|
|
21
|
+
destroy?(): void | Promise<void>;
|
|
77
22
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Narrows `AnyStateStore` to `SyncStateStore` via the `_sync` discriminant.
|
|
80
|
-
* Used by `execute()` and `cachePolicy` to branch between sync and async paths.
|
|
81
|
-
*/
|
|
82
23
|
export declare function isSyncStore(store: SyncStateStore | AsyncStateStore): store is SyncStateStore;
|
|
83
|
-
/**
|
|
84
|
-
* Narrows `AnyStateStore` to `AsyncStateStore` via the `_sync` discriminant.
|
|
85
|
-
* Provided for symmetry; prefer `isSyncStore` for the common guard pattern.
|
|
86
|
-
*/
|
|
87
24
|
export declare function isAsyncStore(store: SyncStateStore | AsyncStateStore): store is AsyncStateStore;
|
|
88
25
|
//# sourceMappingURL=base.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/stores/base.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/stores/base.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAE7B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;IAEpB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAA;IAClC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IASzB,cAAc,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IAGrC,KAAK,IAAI,IAAI,CAAA;IAMb,IAAI,IAAI,MAAM,CAAA;IAOd,OAAO,CAAC,IAAI,IAAI,CAAA;CACjB;AAcD,MAAM,WAAW,eAAe;IAE9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IAErB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IAC3C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAMlC,cAAc,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAG9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAGtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAMvB,OAAO,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC;AAQD,wBAAgB,WAAW,CACzB,KAAK,EAAE,cAAc,GAAG,eAAe,GACtC,KAAK,IAAI,cAAc,CAEzB;AAMD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,GAAG,eAAe,GACtC,KAAK,IAAI,eAAe,CAE1B"}
|
package/dist/stores/base.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
// ─── Store interfaces ─────────────────────────────────────────────────────────
|
|
2
|
-
// ─── Type guards ──────────────────────────────────────────────────────────────
|
|
3
|
-
/**
|
|
4
|
-
* Narrows `AnyStateStore` to `SyncStateStore` via the `_sync` discriminant.
|
|
5
|
-
* Used by `execute()` and `cachePolicy` to branch between sync and async paths.
|
|
6
|
-
*/
|
|
7
1
|
export function isSyncStore(store) {
|
|
8
2
|
return store._sync === true;
|
|
9
3
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Narrows `AnyStateStore` to `AsyncStateStore` via the `_sync` discriminant.
|
|
12
|
-
* Provided for symmetry; prefer `isSyncStore` for the common guard pattern.
|
|
13
|
-
*/
|
|
14
4
|
export function isAsyncStore(store) {
|
|
15
5
|
return store._sync === false;
|
|
16
6
|
}
|
|
17
|
-
//# sourceMappingURL=base.js.map
|