actly 1.1.0 → 1.2.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/LICENSE +0 -0
- package/README.md +262 -232
- package/dist/core/act.cjs +505 -39
- package/dist/core/act.d.ts +81 -10
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +504 -40
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +21 -9
- package/dist/core/executor.d.ts +31 -6
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +21 -9
- package/dist/core/executor.js.map +1 -1
- package/dist/core/health.cjs +34 -0
- package/dist/core/health.d.ts +18 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +28 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/shutdown.cjs +60 -0
- package/dist/core/shutdown.d.ts +8 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +56 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/tenant.cjs +71 -0
- package/dist/core/tenant.d.ts +29 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +68 -0
- package/dist/core/tenant.js.map +1 -0
- package/dist/errors.cjs +182 -0
- package/dist/errors.d.ts +151 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +171 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +56 -6
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -4
- package/dist/index.js.map +1 -1
- package/dist/observability.cjs +45 -0
- package/dist/observability.d.ts +145 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +43 -0
- package/dist/observability.js.map +1 -0
- package/dist/policies/bulkhead.cjs +97 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +95 -0
- package/dist/policies/bulkhead.js.map +1 -0
- package/dist/policies/cache.cjs +223 -22
- package/dist/policies/cache.d.ts +42 -7
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +223 -22
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/circuitBreaker.cjs +85 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +83 -0
- package/dist/policies/circuitBreaker.js.map +1 -0
- package/dist/policies/dedupe.cjs +98 -21
- package/dist/policies/dedupe.d.ts +20 -13
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +98 -21
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/rateLimit.cjs +35 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +33 -0
- package/dist/policies/rateLimit.js.map +1 -0
- package/dist/policies/retry.cjs +103 -26
- package/dist/policies/retry.d.ts +25 -2
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +103 -26
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +78 -36
- package/dist/policies/timeout.d.ts +7 -18
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +84 -31
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +9 -2
- package/dist/state/store.d.ts +9 -0
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +9 -2
- package/dist/state/store.js.map +1 -1
- package/dist/stores/base.cjs +4 -4
- package/dist/stores/base.d.ts +32 -56
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +4 -4
- package/dist/stores/base.js.map +1 -1
- package/dist/stores/memory.cjs +161 -39
- package/dist/stores/memory.d.ts +64 -24
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +160 -39
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +315 -37
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +212 -0
- package/dist/utils/abort.d.ts +79 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +206 -0
- package/dist/utils/abort.js.map +1 -0
- package/dist/utils/abortPool.cjs +26 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +22 -0
- package/dist/utils/abortPool.js.map +1 -0
- package/dist/utils/backoff.cjs +54 -0
- package/dist/utils/backoff.d.ts +14 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +52 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/key.cjs +77 -0
- package/dist/utils/key.d.ts +29 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +75 -0
- package/dist/utils/key.js.map +1 -0
- package/dist/utils/limits.cjs +40 -0
- package/dist/utils/limits.d.ts +39 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +38 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/sanitize.cjs +29 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +26 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validate.cjs +153 -0
- package/dist/utils/validate.d.ts +26 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +141 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +20 -37
package/dist/policies/retry.cjs
CHANGED
|
@@ -1,47 +1,124 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.retryPolicy = retryPolicy;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
const backoff_js_1 = require("../utils/backoff.js");
|
|
5
|
+
const abort_js_1 = require("../utils/abort.js");
|
|
6
|
+
const errors_js_1 = require("../errors.js");
|
|
7
|
+
// ─── Default predicate ────────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Default `shouldRetry`: retry on any error EXCEPT abort errors.
|
|
10
|
+
*
|
|
11
|
+
* Abort errors indicate the caller or a timeout cancelled the operation —
|
|
12
|
+
* retrying would just abort again on the next attempt, wasting delay budget.
|
|
13
|
+
*
|
|
14
|
+
* User-supplied `shouldRetry` overrides this entirely.
|
|
15
|
+
*/
|
|
16
|
+
function defaultShouldRetry(error, _attempt) {
|
|
17
|
+
return !(0, abort_js_1.isAbortError)(error);
|
|
14
18
|
}
|
|
15
|
-
|
|
16
|
-
// ─── Policy ──────────────────────────────────────────────────────────────────
|
|
19
|
+
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* Retry `fn` up to `opts.attempts` times on retryable errors.
|
|
22
|
+
*
|
|
23
|
+
* Writes the live attempt count into `ctx.meta.attempts` so `act()` can
|
|
24
|
+
* report it in the final `ActResult`.
|
|
25
|
+
*
|
|
26
|
+
* # Signal awareness
|
|
27
|
+
*
|
|
28
|
+
* - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
|
|
29
|
+
* `totalTimeout` or caller) has aborted, throws the parent's reason
|
|
30
|
+
* immediately — no more attempts.
|
|
31
|
+
* - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
|
|
32
|
+
* aborts mid-delay, the sleep rejects immediately instead of blocking
|
|
33
|
+
* the loop until the timer would have elapsed.
|
|
34
|
+
*
|
|
35
|
+
* # `shouldRetry` invocation
|
|
36
|
+
*
|
|
37
|
+
* Called after EVERY failure, including the last attempt. This preserves the
|
|
38
|
+
* predicate's contract for observers / metrics that rely on it being called
|
|
39
|
+
* per-attempt. The return value is only consulted when `attempt < max`.
|
|
40
|
+
*
|
|
41
|
+
* # Default predicate
|
|
42
|
+
*
|
|
43
|
+
* If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
|
|
44
|
+
* every error EXCEPT abort errors (so timeouts and caller cancellations
|
|
45
|
+
* don't waste retry budget).
|
|
20
46
|
*/
|
|
21
47
|
function retryPolicy(opts) {
|
|
22
|
-
const max = Math.max(1, opts.attempts);
|
|
23
|
-
|
|
48
|
+
const max = Math.max(1, Math.floor(opts.attempts));
|
|
49
|
+
const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
|
|
50
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
51
|
+
const errors = [];
|
|
24
52
|
let lastErr;
|
|
53
|
+
let retriedAtLeastOnce = false;
|
|
54
|
+
const obs = ctx.observability;
|
|
25
55
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
56
|
+
// Parent (totalTimeout or caller signal) already aborted — bail.
|
|
57
|
+
if (parentSignal.aborted)
|
|
58
|
+
throw parentSignal.reason;
|
|
26
59
|
ctx.meta.attempts = attempt;
|
|
60
|
+
const attemptStart = Date.now();
|
|
27
61
|
try {
|
|
28
|
-
|
|
62
|
+
// Emit onAttempt before each attempt. (We could emit
|
|
63
|
+
// after with duration/error, but before lets observers track
|
|
64
|
+
// in-flight attempts in real time.)
|
|
65
|
+
if (obs) {
|
|
66
|
+
obs.hooks.onAttempt?.({
|
|
67
|
+
type: 'attempt', key: ctx.key, traceId: obs.traceId,
|
|
68
|
+
timestamp: attemptStart, attempt,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return await fn(parentSignal);
|
|
29
72
|
}
|
|
30
73
|
catch (err) {
|
|
31
74
|
lastErr = err;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
errors.push(err);
|
|
76
|
+
// Invoke shouldRetry. If the predicate throws, treat as "don't retry"
|
|
77
|
+
// and surface the original fn error (not the predicate error).
|
|
78
|
+
let retryable;
|
|
79
|
+
try {
|
|
80
|
+
retryable = shouldRetry(err, attempt);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Predicate threw — don't retry, surface original error
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
if (attempt >= max) {
|
|
87
|
+
// Last attempt. If we retried at least once (predicate
|
|
88
|
+
// allowed retries), wrap in RetryExhaustedError for context.
|
|
89
|
+
if (retriedAtLeastOnce) {
|
|
90
|
+
throw new errors_js_1.RetryExhaustedError({
|
|
91
|
+
key: ctx.key,
|
|
92
|
+
attempts: attempt,
|
|
93
|
+
lastError: err,
|
|
94
|
+
errors,
|
|
95
|
+
});
|
|
37
96
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
if (!retryable) {
|
|
100
|
+
// Non-retryable error — bail immediately.
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
// Mark that we retried at least once — affects final wrap.
|
|
104
|
+
retriedAtLeastOnce = true;
|
|
105
|
+
// Parent aborted mid-attempt — don't sleep, bail.
|
|
106
|
+
if (parentSignal.aborted)
|
|
107
|
+
throw parentSignal.reason;
|
|
108
|
+
const delay = (0, backoff_js_1.computeDelay)(attempt, opts);
|
|
109
|
+
// Emit onRetry before the delay sleep.
|
|
110
|
+
if (obs) {
|
|
111
|
+
obs.hooks.onRetry?.({
|
|
112
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
113
|
+
timestamp: Date.now(), attempt, delayMs: delay, error: err,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (delay > 0) {
|
|
117
|
+
await (0, abort_js_1.sleep)(delay, parentSignal);
|
|
41
118
|
}
|
|
42
119
|
}
|
|
43
120
|
}
|
|
44
|
-
//
|
|
121
|
+
// Unreachable: the loop either returns or throws on every iteration.
|
|
45
122
|
throw lastErr;
|
|
46
123
|
};
|
|
47
124
|
}
|
package/dist/policies/retry.d.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import type { PolicyApplier, RetryOptions } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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).
|
|
5
28
|
*/
|
|
6
29
|
export declare function retryPolicy<T>(opts: RetryOptions): PolicyApplier<T>;
|
|
7
30
|
//# 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;AAqB1F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAuFnE"}
|
package/dist/policies/retry.js
CHANGED
|
@@ -1,44 +1,121 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { computeDelay } from '../utils/backoff.js';
|
|
2
|
+
import { isAbortError, sleep } from '../utils/abort.js';
|
|
3
|
+
import { RetryExhaustedError } from '../errors.js';
|
|
4
|
+
// ─── Default predicate ────────────────────────────────────────────────────────
|
|
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
|
+
*/
|
|
13
|
+
function defaultShouldRetry(error, _attempt) {
|
|
14
|
+
return !isAbortError(error);
|
|
11
15
|
}
|
|
12
|
-
|
|
13
|
-
// ─── Policy ──────────────────────────────────────────────────────────────────
|
|
16
|
+
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
14
17
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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).
|
|
17
43
|
*/
|
|
18
44
|
export function retryPolicy(opts) {
|
|
19
|
-
const max = Math.max(1, opts.attempts);
|
|
20
|
-
|
|
45
|
+
const max = Math.max(1, Math.floor(opts.attempts));
|
|
46
|
+
const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
|
|
47
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
48
|
+
const errors = [];
|
|
21
49
|
let lastErr;
|
|
50
|
+
let retriedAtLeastOnce = false;
|
|
51
|
+
const obs = ctx.observability;
|
|
22
52
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
53
|
+
// Parent (totalTimeout or caller signal) already aborted — bail.
|
|
54
|
+
if (parentSignal.aborted)
|
|
55
|
+
throw parentSignal.reason;
|
|
23
56
|
ctx.meta.attempts = attempt;
|
|
57
|
+
const attemptStart = Date.now();
|
|
24
58
|
try {
|
|
25
|
-
|
|
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
|
+
if (obs) {
|
|
63
|
+
obs.hooks.onAttempt?.({
|
|
64
|
+
type: 'attempt', key: ctx.key, traceId: obs.traceId,
|
|
65
|
+
timestamp: attemptStart, attempt,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return await fn(parentSignal);
|
|
26
69
|
}
|
|
27
70
|
catch (err) {
|
|
28
71
|
lastErr = err;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
72
|
+
errors.push(err);
|
|
73
|
+
// Invoke shouldRetry. If the predicate throws, treat as "don't retry"
|
|
74
|
+
// and surface the original fn error (not the predicate error).
|
|
75
|
+
let retryable;
|
|
76
|
+
try {
|
|
77
|
+
retryable = shouldRetry(err, attempt);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Predicate threw — don't retry, surface original error
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
if (attempt >= max) {
|
|
84
|
+
// Last attempt. If we retried at least once (predicate
|
|
85
|
+
// allowed retries), wrap in RetryExhaustedError for context.
|
|
86
|
+
if (retriedAtLeastOnce) {
|
|
87
|
+
throw new RetryExhaustedError({
|
|
88
|
+
key: ctx.key,
|
|
89
|
+
attempts: attempt,
|
|
90
|
+
lastError: err,
|
|
91
|
+
errors,
|
|
92
|
+
});
|
|
34
93
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
if (!retryable) {
|
|
97
|
+
// Non-retryable error — bail immediately.
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
// Mark that we retried at least once — affects final wrap.
|
|
101
|
+
retriedAtLeastOnce = true;
|
|
102
|
+
// Parent aborted mid-attempt — don't sleep, bail.
|
|
103
|
+
if (parentSignal.aborted)
|
|
104
|
+
throw parentSignal.reason;
|
|
105
|
+
const delay = computeDelay(attempt, opts);
|
|
106
|
+
// Emit onRetry before the delay sleep.
|
|
107
|
+
if (obs) {
|
|
108
|
+
obs.hooks.onRetry?.({
|
|
109
|
+
type: 'retry', key: ctx.key, traceId: obs.traceId,
|
|
110
|
+
timestamp: Date.now(), attempt, delayMs: delay, error: err,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (delay > 0) {
|
|
114
|
+
await sleep(delay, parentSignal);
|
|
38
115
|
}
|
|
39
116
|
}
|
|
40
117
|
}
|
|
41
|
-
//
|
|
118
|
+
// Unreachable: the loop either returns or throws on every iteration.
|
|
42
119
|
throw lastErr;
|
|
43
120
|
};
|
|
44
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAElD,iFAAiF;AAEjF;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,KAAc,EAAE,QAAgB;IAC1D,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,iFAAiF;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IAClD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,kBAAkB,CAAA;IAE1D,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,EAAE,YAAyB,EAAE,EAAE;QAClC,MAAM,MAAM,GAAc,EAAE,CAAA;QAC5B,IAAI,OAAgB,CAAA;QACpB,IAAI,kBAAkB,GAAG,KAAK,CAAA;QAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAA;QAE7B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;YAChD,iEAAiE;YACjE,IAAI,YAAY,CAAC,OAAO;gBAAE,MAAM,YAAY,CAAC,MAAM,CAAA;YAEnD,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;YAC3B,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAE/B,IAAI,CAAC;gBACH,qDAAqD;gBACrD,6DAA6D;gBAC7D,oCAAoC;gBACpC,IAAI,GAAG,EAAE,CAAC;oBACR,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;wBACpB,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO;wBACnD,SAAS,EAAE,YAAY,EAAE,OAAO;qBACjC,CAAC,CAAA;gBACJ,CAAC;gBACD,OAAO,MAAM,EAAE,CAAC,YAAY,CAAC,CAAA;YAC/B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,GAAG,CAAA;gBACb,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAEhB,sEAAsE;gBACtE,+DAA+D;gBAC/D,IAAI,SAAkB,CAAA;gBACtB,IAAI,CAAC;oBACH,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;gBACvC,CAAC;gBAAC,MAAM,CAAC;oBACP,wDAAwD;oBACxD,MAAM,GAAG,CAAA;gBACX,CAAC;gBAED,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;oBACnB,uDAAuD;oBACvD,6DAA6D;oBAC7D,IAAI,kBAAkB,EAAE,CAAC;wBACvB,MAAM,IAAI,mBAAmB,CAAC;4BAC5B,GAAG,EAAE,GAAG,CAAC,GAAG;4BACZ,QAAQ,EAAE,OAAO;4BACjB,SAAS,EAAE,GAAG;4BACd,MAAM;yBACP,CAAC,CAAA;oBACJ,CAAC;oBACD,MAAM,GAAG,CAAA;gBACX,CAAC;gBAED,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,0CAA0C;oBAC1C,MAAM,GAAG,CAAA;gBACX,CAAC;gBAED,2DAA2D;gBAC3D,kBAAkB,GAAG,IAAI,CAAA;gBAEzB,kDAAkD;gBAClD,IAAI,YAAY,CAAC,OAAO;oBAAE,MAAM,YAAY,CAAC,MAAM,CAAA;gBAEnD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBAEzC,uCAAuC;gBACvC,IAAI,GAAG,EAAE,CAAC;oBACR,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;wBAClB,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO;wBACjD,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG;qBAC3D,CAAC,CAAA;gBACJ,CAAC;gBAED,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBACd,MAAM,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;gBAClC,CAAC;YACH,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,MAAM,OAAO,CAAA;IACf,CAAC,CAAA;AACL,CAAC"}
|
|
@@ -3,55 +3,97 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TotalTimeoutError = exports.TimeoutError = void 0;
|
|
4
4
|
exports.timeoutPolicy = timeoutPolicy;
|
|
5
5
|
exports.totalTimeoutPolicy = totalTimeoutPolicy;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.ms = ms;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
exports.TimeoutError = TimeoutError;
|
|
6
|
+
const abort_js_1 = require("../utils/abort.js");
|
|
7
|
+
const errors_js_1 = require("../errors.js");
|
|
8
|
+
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
|
|
9
|
+
Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return errors_js_1.TotalTimeoutError; } });
|
|
10
|
+
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
15
11
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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).
|
|
19
36
|
*/
|
|
20
|
-
class TotalTimeoutError extends Error {
|
|
21
|
-
constructor(ms) {
|
|
22
|
-
super(`ACT total timeout exceeded after ${ms}ms`);
|
|
23
|
-
this.name = 'TotalTimeoutError';
|
|
24
|
-
this.ms = ms;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
exports.TotalTimeoutError = TotalTimeoutError;
|
|
28
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
29
|
-
/** Shared race-against-a-timer logic. ErrorCtor lets callers pick the error type. */
|
|
30
37
|
function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
31
|
-
return (fn,
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
// Pass key to the error ctor for better debugging context.
|
|
41
|
+
const timerError = new ErrorCtor(opts.ms, { key: ctx.key });
|
|
42
|
+
// Arm the per-attempt timer. The error object is allocated once so the
|
|
43
|
+
// stack trace points here (the policy frame), not at setTimeout's
|
|
44
|
+
// internal callback.
|
|
45
|
+
const timer = setTimeout(() => controller.abort(timerError), opts.ms);
|
|
46
|
+
// NOTE: do NOT `unref()` this timer. The timeout IS the operation
|
|
47
|
+
// the caller is awaiting. unref'ing would let Node exit the process
|
|
48
|
+
// while a timeout was pending — silently dropping the operation.
|
|
49
|
+
// The timer is cleared in the finally block below.
|
|
50
|
+
// Link parent → child. Capture the unlink function so we can clean
|
|
51
|
+
// up the listener on success path (contract).
|
|
52
|
+
const unlink = (0, abort_js_1.linkSignal)(parentSignal, controller);
|
|
53
|
+
try {
|
|
54
|
+
// Race fn against the abort event. If fn settles first, we get its
|
|
55
|
+
// result/error. If the signal aborts first, we reject with reason.
|
|
56
|
+
//
|
|
57
|
+
// We do NOT use AbortSignal.timeout() here because we need to throw
|
|
58
|
+
// our own ErrorCtor, not a DOMException named "TimeoutError".
|
|
59
|
+
return await new Promise((resolve, reject) => {
|
|
60
|
+
if (controller.signal.aborted) {
|
|
61
|
+
reject(controller.signal.reason);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const onAbort = () => reject(controller.signal.reason);
|
|
65
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
66
|
+
Promise.resolve(fn(controller.signal)).then((value) => {
|
|
67
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
68
|
+
resolve(value);
|
|
69
|
+
}, (error) => {
|
|
70
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
71
|
+
reject(error);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
unlink();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
37
80
|
}
|
|
38
|
-
// ─── Policies ─────────────────────────────────────────────────────────────────
|
|
39
81
|
/**
|
|
40
|
-
* Races fn against a
|
|
41
|
-
* Rejects with TimeoutError if the deadline fires first.
|
|
82
|
+
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
42
83
|
*
|
|
43
|
-
* Place this INSIDE retryPolicy (closer to fn) so each attempt has its
|
|
84
|
+
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
85
|
+
* own clock.
|
|
44
86
|
*/
|
|
45
87
|
function timeoutPolicy(opts) {
|
|
46
|
-
return makeTimeoutPolicy(opts, TimeoutError);
|
|
88
|
+
return makeTimeoutPolicy(opts, errors_js_1.TimeoutError);
|
|
47
89
|
}
|
|
48
90
|
/**
|
|
49
|
-
* Races the ENTIRE
|
|
50
|
-
*
|
|
91
|
+
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
92
|
+
* delays) against a hard budget that does NOT reset.
|
|
51
93
|
*
|
|
52
94
|
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
53
95
|
* policy runs and stops regardless of what the inner chain is doing.
|
|
54
96
|
*/
|
|
55
97
|
function totalTimeoutPolicy(opts) {
|
|
56
|
-
return makeTimeoutPolicy(opts, TotalTimeoutError);
|
|
98
|
+
return makeTimeoutPolicy(opts, errors_js_1.TotalTimeoutError);
|
|
57
99
|
}
|
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import type { PolicyApplier, TimeoutOptions } from '../types/index.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
constructor(ms: number);
|
|
5
|
-
}
|
|
2
|
+
import { TimeoutError, TotalTimeoutError } from '../errors.js';
|
|
3
|
+
export { TimeoutError, TotalTimeoutError };
|
|
6
4
|
/**
|
|
7
|
-
*
|
|
8
|
-
* Distinct from TimeoutError (per-attempt) so callers can instanceof-check
|
|
9
|
-
* which deadline fired.
|
|
10
|
-
*/
|
|
11
|
-
export declare class TotalTimeoutError extends Error {
|
|
12
|
-
readonly ms: number;
|
|
13
|
-
constructor(ms: number);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Races fn against a hard deadline.
|
|
17
|
-
* Rejects with TimeoutError if the deadline fires first.
|
|
5
|
+
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
18
6
|
*
|
|
19
|
-
* Place this INSIDE retryPolicy (closer to fn) so each attempt has its
|
|
7
|
+
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
8
|
+
* own clock.
|
|
20
9
|
*/
|
|
21
10
|
export declare function timeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
|
|
22
11
|
/**
|
|
23
|
-
* Races the ENTIRE
|
|
24
|
-
*
|
|
12
|
+
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
13
|
+
* delays) against a hard budget that does NOT reset.
|
|
25
14
|
*
|
|
26
15
|
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
27
16
|
* policy runs and stops regardless of what the inner chain is doing.
|
|
@@ -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;AAE5F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAa9D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAA;AAyF1C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
|