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/policies/retry.cjs
CHANGED
|
@@ -3,85 +3,157 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.retryPolicy = retryPolicy;
|
|
4
4
|
const backoff_js_1 = require("../utils/backoff.js");
|
|
5
5
|
const abort_js_1 = require("../utils/abort.js");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* Abort errors indicate the caller or a timeout cancelled the operation —
|
|
11
|
-
* retrying would just abort again on the next attempt, wasting delay budget.
|
|
12
|
-
*
|
|
13
|
-
* User-supplied `shouldRetry` overrides this entirely.
|
|
14
|
-
*/
|
|
6
|
+
const safeCall_js_1 = require("../utils/safeCall.js");
|
|
7
|
+
const errors_js_1 = require("../errors.js");
|
|
8
|
+
const limits_js_1 = require("../utils/limits.js");
|
|
15
9
|
function defaultShouldRetry(error, _attempt) {
|
|
16
|
-
|
|
10
|
+
if ((0, abort_js_1.isAbortError)(error))
|
|
11
|
+
return false;
|
|
12
|
+
if (typeof error === 'object' && error !== null) {
|
|
13
|
+
const code = error.code;
|
|
14
|
+
if (code === 'ACTLY_TIMEOUT' || code === 'ACTLY_TOTAL_TIMEOUT')
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
17
18
|
}
|
|
18
|
-
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
19
|
-
/**
|
|
20
|
-
* Retry `fn` up to `opts.attempts` times on retryable errors.
|
|
21
|
-
*
|
|
22
|
-
* Writes the live attempt count into `ctx.meta.attempts` so `act()` can
|
|
23
|
-
* report it in the final `ActResult`.
|
|
24
|
-
*
|
|
25
|
-
* # Signal awareness
|
|
26
|
-
*
|
|
27
|
-
* - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
|
|
28
|
-
* `totalTimeout` or caller) has aborted, throws the parent's reason
|
|
29
|
-
* immediately — no more attempts.
|
|
30
|
-
* - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
|
|
31
|
-
* aborts mid-delay, the sleep rejects immediately instead of blocking
|
|
32
|
-
* the loop until the timer would have elapsed.
|
|
33
|
-
*
|
|
34
|
-
* # `shouldRetry` invocation
|
|
35
|
-
*
|
|
36
|
-
* Called after EVERY failure, including the last attempt. This preserves the
|
|
37
|
-
* predicate's contract for observers / metrics that rely on it being called
|
|
38
|
-
* per-attempt. The return value is only consulted when `attempt < max`.
|
|
39
|
-
*
|
|
40
|
-
* # Default predicate
|
|
41
|
-
*
|
|
42
|
-
* If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
|
|
43
|
-
* every error EXCEPT abort errors (so timeouts and caller cancellations
|
|
44
|
-
* don't waste retry budget).
|
|
45
|
-
*/
|
|
46
19
|
function retryPolicy(opts) {
|
|
47
20
|
const max = Math.max(1, Math.floor(opts.attempts));
|
|
48
21
|
const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
|
|
22
|
+
const shouldRetryResult = opts.shouldRetryResult;
|
|
23
|
+
const dangerouslyUnref = opts.dangerouslyUnref === true;
|
|
24
|
+
const sleepOpts = dangerouslyUnref ? { unref: true } : undefined;
|
|
25
|
+
const backoffFn = opts.backoffFn;
|
|
49
26
|
return (fn, ctx) => async (parentSignal) => {
|
|
50
|
-
|
|
27
|
+
const backoffState = {};
|
|
28
|
+
let errors;
|
|
29
|
+
let retriedAtLeastOnce = false;
|
|
30
|
+
const obs = ctx.observability;
|
|
51
31
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
52
|
-
// Parent (totalTimeout or caller signal) already aborted — bail.
|
|
53
32
|
if (parentSignal.aborted)
|
|
54
33
|
throw parentSignal.reason;
|
|
55
34
|
ctx.meta.attempts = attempt;
|
|
35
|
+
const attemptStart = Date.now();
|
|
56
36
|
try {
|
|
57
|
-
|
|
37
|
+
if (obs) {
|
|
38
|
+
(0, safeCall_js_1.safeCall)(obs.hooks.onAttempt, {
|
|
39
|
+
type: 'attempt', key: ctx.key, traceId: obs.traceId,
|
|
40
|
+
timestamp: attemptStart, attempt,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const value = await fn(parentSignal);
|
|
44
|
+
if (shouldRetryResult) {
|
|
45
|
+
let accept;
|
|
46
|
+
try {
|
|
47
|
+
const raw = shouldRetryResult(value, attempt);
|
|
48
|
+
accept = typeof raw === 'boolean' ? raw : true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
if (accept)
|
|
54
|
+
return value;
|
|
55
|
+
const syntheticError = new Error(`Actly: shouldRetryResult returned false on attempt ${attempt}`);
|
|
56
|
+
if (errors === undefined)
|
|
57
|
+
errors = [];
|
|
58
|
+
if (errors.length < 10) {
|
|
59
|
+
errors.push(syntheticError);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
errors.shift();
|
|
63
|
+
errors.push(syntheticError);
|
|
64
|
+
}
|
|
65
|
+
if (attempt >= max) {
|
|
66
|
+
if (obs) {
|
|
67
|
+
(0, safeCall_js_1.safeCall)(obs.hooks.onRetry, {
|
|
68
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
69
|
+
timestamp: Date.now(), attempt, delayMs: 0,
|
|
70
|
+
error: syntheticError,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
retriedAtLeastOnce = true;
|
|
76
|
+
if (parentSignal.aborted)
|
|
77
|
+
throw parentSignal.reason;
|
|
78
|
+
let rawDelay;
|
|
79
|
+
try {
|
|
80
|
+
rawDelay = backoffFn
|
|
81
|
+
? backoffFn(attempt, syntheticError, backoffState)
|
|
82
|
+
: (0, backoff_js_1.computeDelay)(attempt, opts);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
rawDelay = (0, backoff_js_1.computeDelay)(attempt, opts);
|
|
86
|
+
}
|
|
87
|
+
const safeDelay = Number.isFinite(rawDelay) ? rawDelay : 0;
|
|
88
|
+
const delay = Math.min(Math.max(0, safeDelay), limits_js_1.LIMITS.MAX_RETRY_DELAY_MS);
|
|
89
|
+
if (obs) {
|
|
90
|
+
(0, safeCall_js_1.safeCall)(obs.hooks.onRetry, {
|
|
91
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
92
|
+
timestamp: Date.now(), attempt, delayMs: delay,
|
|
93
|
+
error: syntheticError,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (delay > 0)
|
|
97
|
+
await (0, abort_js_1.sleep)(delay, parentSignal, sleepOpts);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
58
101
|
}
|
|
59
102
|
catch (err) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
if (errors === undefined)
|
|
104
|
+
errors = [];
|
|
105
|
+
if (errors.length < 10) {
|
|
106
|
+
errors.push(err);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
errors.shift();
|
|
110
|
+
errors.push(err);
|
|
111
|
+
}
|
|
112
|
+
let retryable;
|
|
113
|
+
try {
|
|
114
|
+
retryable = shouldRetry(err, attempt);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
66
117
|
throw err;
|
|
67
118
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
119
|
+
if (attempt >= max) {
|
|
120
|
+
if (retriedAtLeastOnce) {
|
|
121
|
+
throw new errors_js_1.RetryExhaustedError({
|
|
122
|
+
key: ctx.key,
|
|
123
|
+
attempts: attempt,
|
|
124
|
+
lastError: err,
|
|
125
|
+
errors: errors ?? [],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
71
128
|
throw err;
|
|
72
129
|
}
|
|
73
|
-
|
|
130
|
+
if (!retryable)
|
|
131
|
+
throw err;
|
|
132
|
+
retriedAtLeastOnce = true;
|
|
74
133
|
if (parentSignal.aborted)
|
|
75
134
|
throw parentSignal.reason;
|
|
76
|
-
|
|
135
|
+
let rawDelay;
|
|
136
|
+
try {
|
|
137
|
+
rawDelay = backoffFn
|
|
138
|
+
? backoffFn(attempt, err, backoffState)
|
|
139
|
+
: (0, backoff_js_1.computeDelay)(attempt, opts);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
rawDelay = (0, backoff_js_1.computeDelay)(attempt, opts);
|
|
143
|
+
}
|
|
144
|
+
const safeDelay = Number.isFinite(rawDelay) ? rawDelay : 0;
|
|
145
|
+
const delay = Math.min(Math.max(0, safeDelay), limits_js_1.LIMITS.MAX_RETRY_DELAY_MS);
|
|
146
|
+
if (obs) {
|
|
147
|
+
(0, safeCall_js_1.safeCall)(obs.hooks.onRetry, {
|
|
148
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
149
|
+
timestamp: Date.now(), attempt, delayMs: delay, error: err,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
77
152
|
if (delay > 0) {
|
|
78
|
-
|
|
79
|
-
await (0, abort_js_1.sleep)(delay, parentSignal);
|
|
153
|
+
await (0, abort_js_1.sleep)(delay, parentSignal, sleepOpts);
|
|
80
154
|
}
|
|
81
155
|
}
|
|
82
156
|
}
|
|
83
|
-
|
|
84
|
-
// The cast satisfies the type checker without a `throw` after the loop.
|
|
85
|
-
throw lastErr;
|
|
157
|
+
throw new Error('Actly: retryPolicy reached unreachable state');
|
|
86
158
|
};
|
|
87
159
|
}
|
package/dist/policies/retry.d.ts
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
1
|
import type { PolicyApplier, RetryOptions } from '../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Retry `fn` up to `opts.attempts` times on retryable errors.
|
|
4
|
-
*
|
|
5
|
-
* Writes the live attempt count into `ctx.meta.attempts` so `act()` can
|
|
6
|
-
* report it in the final `ActResult`.
|
|
7
|
-
*
|
|
8
|
-
* # Signal awareness
|
|
9
|
-
*
|
|
10
|
-
* - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
|
|
11
|
-
* `totalTimeout` or caller) has aborted, throws the parent's reason
|
|
12
|
-
* immediately — no more attempts.
|
|
13
|
-
* - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
|
|
14
|
-
* aborts mid-delay, the sleep rejects immediately instead of blocking
|
|
15
|
-
* the loop until the timer would have elapsed.
|
|
16
|
-
*
|
|
17
|
-
* # `shouldRetry` invocation
|
|
18
|
-
*
|
|
19
|
-
* Called after EVERY failure, including the last attempt. This preserves the
|
|
20
|
-
* predicate's contract for observers / metrics that rely on it being called
|
|
21
|
-
* per-attempt. The return value is only consulted when `attempt < max`.
|
|
22
|
-
*
|
|
23
|
-
* # Default predicate
|
|
24
|
-
*
|
|
25
|
-
* If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
|
|
26
|
-
* every error EXCEPT abort errors (so timeouts and caller cancellations
|
|
27
|
-
* don't waste retry budget).
|
|
28
|
-
*/
|
|
29
2
|
export declare function retryPolicy<T>(opts: RetryOptions): PolicyApplier<T>;
|
|
30
3
|
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAsC1F,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CA4KnE"}
|
package/dist/policies/retry.js
CHANGED
|
@@ -1,85 +1,156 @@
|
|
|
1
1
|
import { computeDelay } from '../utils/backoff.js';
|
|
2
2
|
import { isAbortError, sleep } from '../utils/abort.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Abort errors indicate the caller or a timeout cancelled the operation —
|
|
8
|
-
* retrying would just abort again on the next attempt, wasting delay budget.
|
|
9
|
-
*
|
|
10
|
-
* User-supplied `shouldRetry` overrides this entirely.
|
|
11
|
-
*/
|
|
3
|
+
import { safeCall } from '../utils/safeCall.js';
|
|
4
|
+
import { RetryExhaustedError } from '../errors.js';
|
|
5
|
+
import { LIMITS } from '../utils/limits.js';
|
|
12
6
|
function defaultShouldRetry(error, _attempt) {
|
|
13
|
-
|
|
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;
|
|
14
15
|
}
|
|
15
|
-
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
16
|
-
/**
|
|
17
|
-
* Retry `fn` up to `opts.attempts` times on retryable errors.
|
|
18
|
-
*
|
|
19
|
-
* Writes the live attempt count into `ctx.meta.attempts` so `act()` can
|
|
20
|
-
* report it in the final `ActResult`.
|
|
21
|
-
*
|
|
22
|
-
* # Signal awareness
|
|
23
|
-
*
|
|
24
|
-
* - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
|
|
25
|
-
* `totalTimeout` or caller) has aborted, throws the parent's reason
|
|
26
|
-
* immediately — no more attempts.
|
|
27
|
-
* - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
|
|
28
|
-
* aborts mid-delay, the sleep rejects immediately instead of blocking
|
|
29
|
-
* the loop until the timer would have elapsed.
|
|
30
|
-
*
|
|
31
|
-
* # `shouldRetry` invocation
|
|
32
|
-
*
|
|
33
|
-
* Called after EVERY failure, including the last attempt. This preserves the
|
|
34
|
-
* predicate's contract for observers / metrics that rely on it being called
|
|
35
|
-
* per-attempt. The return value is only consulted when `attempt < max`.
|
|
36
|
-
*
|
|
37
|
-
* # Default predicate
|
|
38
|
-
*
|
|
39
|
-
* If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
|
|
40
|
-
* every error EXCEPT abort errors (so timeouts and caller cancellations
|
|
41
|
-
* don't waste retry budget).
|
|
42
|
-
*/
|
|
43
16
|
export function retryPolicy(opts) {
|
|
44
17
|
const max = Math.max(1, Math.floor(opts.attempts));
|
|
45
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;
|
|
46
23
|
return (fn, ctx) => async (parentSignal) => {
|
|
47
|
-
|
|
24
|
+
const backoffState = {};
|
|
25
|
+
let errors;
|
|
26
|
+
let retriedAtLeastOnce = false;
|
|
27
|
+
const obs = ctx.observability;
|
|
48
28
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
49
|
-
// Parent (totalTimeout or caller signal) already aborted — bail.
|
|
50
29
|
if (parentSignal.aborted)
|
|
51
30
|
throw parentSignal.reason;
|
|
52
31
|
ctx.meta.attempts = attempt;
|
|
32
|
+
const attemptStart = Date.now();
|
|
53
33
|
try {
|
|
54
|
-
|
|
34
|
+
if (obs) {
|
|
35
|
+
safeCall(obs.hooks.onAttempt, {
|
|
36
|
+
type: 'attempt', key: ctx.key, traceId: obs.traceId,
|
|
37
|
+
timestamp: attemptStart, attempt,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
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;
|
|
55
98
|
}
|
|
56
99
|
catch (err) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
109
|
+
let retryable;
|
|
110
|
+
try {
|
|
111
|
+
retryable = shouldRetry(err, attempt);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
63
114
|
throw err;
|
|
64
115
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
116
|
+
if (attempt >= max) {
|
|
117
|
+
if (retriedAtLeastOnce) {
|
|
118
|
+
throw new RetryExhaustedError({
|
|
119
|
+
key: ctx.key,
|
|
120
|
+
attempts: attempt,
|
|
121
|
+
lastError: err,
|
|
122
|
+
errors: errors ?? [],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
68
125
|
throw err;
|
|
69
126
|
}
|
|
70
|
-
|
|
127
|
+
if (!retryable)
|
|
128
|
+
throw err;
|
|
129
|
+
retriedAtLeastOnce = true;
|
|
71
130
|
if (parentSignal.aborted)
|
|
72
131
|
throw parentSignal.reason;
|
|
73
|
-
|
|
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);
|
|
143
|
+
if (obs) {
|
|
144
|
+
safeCall(obs.hooks.onRetry, {
|
|
145
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
146
|
+
timestamp: Date.now(), attempt, delayMs: delay, error: err,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
74
149
|
if (delay > 0) {
|
|
75
|
-
|
|
76
|
-
await sleep(delay, parentSignal);
|
|
150
|
+
await sleep(delay, parentSignal, sleepOpts);
|
|
77
151
|
}
|
|
78
152
|
}
|
|
79
153
|
}
|
|
80
|
-
|
|
81
|
-
// The cast satisfies the type checker without a `throw` after the loop.
|
|
82
|
-
throw lastErr;
|
|
154
|
+
throw new Error('Actly: retryPolicy reached unreachable state');
|
|
83
155
|
};
|
|
84
156
|
}
|
|
85
|
-
//# sourceMappingURL=retry.js.map
|
|
@@ -4,124 +4,68 @@ 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* if (!result.ok && result.error instanceof TimeoutError) {
|
|
15
|
-
* console.log(`attempt timed out after ${result.error.ms}ms`)
|
|
16
|
-
* }
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
class TimeoutError extends Error {
|
|
20
|
-
ms;
|
|
21
|
-
constructor(ms) {
|
|
22
|
-
super(`ACT timed out after ${ms}ms`);
|
|
23
|
-
this.name = 'TimeoutError';
|
|
24
|
-
this.ms = ms;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
exports.TimeoutError = TimeoutError;
|
|
28
|
-
/**
|
|
29
|
-
* Thrown when the operation-wide `totalTimeout` budget fires.
|
|
30
|
-
*
|
|
31
|
-
* Distinct from `TimeoutError` (per-attempt) so callers can `instanceof`-check
|
|
32
|
-
* which deadline fired:
|
|
33
|
-
*
|
|
34
|
-
* ```ts
|
|
35
|
-
* if (result.error instanceof TotalTimeoutError) {
|
|
36
|
-
* // whole operation budget exhausted
|
|
37
|
-
* } else if (result.error instanceof TimeoutError) {
|
|
38
|
-
* // last attempt's per-attempt deadline fired
|
|
39
|
-
* }
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
class TotalTimeoutError extends Error {
|
|
43
|
-
ms;
|
|
44
|
-
constructor(ms) {
|
|
45
|
-
super(`ACT total timeout exceeded after ${ms}ms`);
|
|
46
|
-
this.name = 'TotalTimeoutError';
|
|
47
|
-
this.ms = ms;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
exports.TotalTimeoutError = TotalTimeoutError;
|
|
51
|
-
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
52
|
-
/**
|
|
53
|
-
* Build a timeout policy that throws `ErrorCtor` on deadline.
|
|
54
|
-
*
|
|
55
|
-
* # Cancellation contract
|
|
56
|
-
*
|
|
57
|
-
* Each invocation:
|
|
58
|
-
* 1. Creates a fresh `AbortController` for this attempt.
|
|
59
|
-
* 2. Arms a `setTimeout` that aborts the controller with a fresh `ErrorCtor(ms)`.
|
|
60
|
-
* 3. Links the parent signal: if the parent aborts (e.g. `totalTimeout` or
|
|
61
|
-
* caller cancellation), the child aborts with the parent's reason.
|
|
62
|
-
* 4. Races `fn(childSignal)` against the abort event.
|
|
63
|
-
*
|
|
64
|
-
* The race is critical: it ensures `act()` returns promptly even if `fn`
|
|
65
|
-
* ignores the signal. The underlying `fn` may keep running in the background
|
|
66
|
-
* (resource leak), but the caller is unblocked. This is the best JavaScript
|
|
67
|
-
* can do without cooperation from `fn`.
|
|
68
|
-
*
|
|
69
|
-
* If `fn` cooperates (passes `signal` to `fetch`, `AbortController`, etc.),
|
|
70
|
-
* the underlying work is cancelled properly — no leak.
|
|
71
|
-
*
|
|
72
|
-
* # Error attribution
|
|
73
|
-
*
|
|
74
|
-
* If the per-attempt timer fires, we throw `ErrorCtor(ms)` regardless of
|
|
75
|
-
* what `fn` does. If the parent signal fires first, we throw the parent's
|
|
76
|
-
* reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
|
|
77
|
-
*/
|
|
78
|
-
function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
79
|
-
return (fn, _ctx) => async (parentSignal) => {
|
|
7
|
+
const safeCall_js_1 = require("../utils/safeCall.js");
|
|
8
|
+
const errors_js_1 = require("../errors.js");
|
|
9
|
+
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
|
|
10
|
+
Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return errors_js_1.TotalTimeoutError; } });
|
|
11
|
+
function makeTimeoutPolicy(opts, errorCtor, kind) {
|
|
12
|
+
const strategy = opts.strategy ?? 'race';
|
|
13
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
80
14
|
const controller = new AbortController();
|
|
81
|
-
const timerError = new
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
28
|
+
const unlink = (0, abort_js_1.linkSignal)(parentSignal, controller);
|
|
89
29
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
|
95
44
|
return await new Promise((resolve, reject) => {
|
|
96
45
|
if (controller.signal.aborted) {
|
|
97
46
|
reject(controller.signal.reason);
|
|
98
47
|
return;
|
|
99
48
|
}
|
|
100
|
-
|
|
101
|
-
|
|
49
|
+
const onAbort = () => reject(controller.signal.reason);
|
|
50
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
51
|
+
Promise.resolve(fn(controller.signal)).then((value) => {
|
|
52
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
53
|
+
resolve(value);
|
|
54
|
+
}, (error) => {
|
|
55
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
56
|
+
reject(error);
|
|
57
|
+
});
|
|
102
58
|
});
|
|
103
59
|
}
|
|
104
60
|
finally {
|
|
105
61
|
clearTimeout(timer);
|
|
62
|
+
unlink();
|
|
106
63
|
}
|
|
107
64
|
};
|
|
108
65
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
111
|
-
*
|
|
112
|
-
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
113
|
-
* own clock.
|
|
114
|
-
*/
|
|
115
66
|
function timeoutPolicy(opts) {
|
|
116
|
-
return makeTimeoutPolicy(opts, TimeoutError);
|
|
67
|
+
return makeTimeoutPolicy(opts, errors_js_1.TimeoutError, 'per-attempt');
|
|
117
68
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
120
|
-
* delays) against a hard budget that does NOT reset.
|
|
121
|
-
*
|
|
122
|
-
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
123
|
-
* policy runs and stops regardless of what the inner chain is doing.
|
|
124
|
-
*/
|
|
125
69
|
function totalTimeoutPolicy(opts) {
|
|
126
|
-
return makeTimeoutPolicy(opts, TotalTimeoutError);
|
|
70
|
+
return makeTimeoutPolicy(opts, errors_js_1.TotalTimeoutError, 'total');
|
|
127
71
|
}
|