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/utils/abort.cjs
CHANGED
|
@@ -1,51 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// ─── AbortSignal helpers ──────────────────────────────────────────────────────
|
|
3
|
-
//
|
|
4
|
-
// Contract: zero listener accumulation on long-lived AbortSignals.
|
|
5
|
-
//
|
|
6
|
-
// Every `addEventListener('abort', ...)` MUST be paired with a
|
|
7
|
-
// `removeEventListener` on the success path, OR use `AbortSignal.any()`
|
|
8
|
-
// (Node 20+ native) which handles cleanup automatically.
|
|
9
|
-
//
|
|
10
|
-
// Long-lived signals (server shutdown, request pool) must not accumulate
|
|
11
|
-
// one listener per `act()` call — that would cause
|
|
12
|
-
// `MaxListenersExceededWarning` and unbounded closure retention.
|
|
13
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
3
|
exports.anySignal = anySignal;
|
|
15
4
|
exports.raceAbort = raceAbort;
|
|
16
5
|
exports.sleep = sleep;
|
|
17
6
|
exports.isAbortError = isAbortError;
|
|
18
7
|
exports.linkSignal = linkSignal;
|
|
19
|
-
|
|
20
|
-
* Compose multiple AbortSignals into one. Aborts when ANY input aborts,
|
|
21
|
-
* with the same reason.
|
|
22
|
-
*
|
|
23
|
-
* Uses native `AbortSignal.any()` on Node 20+ (zero allocations, no
|
|
24
|
-
* listener leaks — the runtime owns the lifecycle). Falls back to a
|
|
25
|
-
* polyfill that explicitly removes listeners on first abort.
|
|
26
|
-
*
|
|
27
|
-
* # Listener safety
|
|
28
|
-
*
|
|
29
|
-
* The polyfill registers `{ once: true }` listeners on each input and
|
|
30
|
-
* manually removes the others when one fires. After settlement, the
|
|
31
|
-
* composite signal holds zero references to the inputs — they may be GC'd.
|
|
32
|
-
*/
|
|
8
|
+
const NATIVE_ANY = AbortSignal.any;
|
|
33
9
|
function anySignal(signals) {
|
|
34
|
-
// Filter out undefined / null defensively.
|
|
35
10
|
const filtered = signals.filter((s) => s != null);
|
|
36
|
-
if (filtered.length === 0)
|
|
37
|
-
// No inputs → never-aborting signal. Use a fresh controller so callers
|
|
38
|
-
// get a real AbortSignal, not a hand-rolled mock.
|
|
11
|
+
if (filtered.length === 0)
|
|
39
12
|
return new AbortController().signal;
|
|
40
|
-
}
|
|
41
13
|
if (filtered.length === 1)
|
|
42
14
|
return filtered[0];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (typeof native === 'function') {
|
|
46
|
-
return native.call(AbortSignal, filtered);
|
|
47
|
-
}
|
|
48
|
-
// Polyfill: only used when native is missing. Listener-safe.
|
|
15
|
+
if (NATIVE_ANY)
|
|
16
|
+
return NATIVE_ANY.call(AbortSignal, filtered);
|
|
49
17
|
const controller = new AbortController();
|
|
50
18
|
const listeners = [];
|
|
51
19
|
for (const signal of filtered) {
|
|
@@ -55,41 +23,20 @@ function anySignal(signals) {
|
|
|
55
23
|
}
|
|
56
24
|
const onAbort = () => {
|
|
57
25
|
controller.abort(signal.reason);
|
|
58
|
-
// Remove all other listeners — they hold closures over `signal`
|
|
59
|
-
// and would otherwise keep the inputs alive past settlement.
|
|
60
26
|
for (const off of listeners)
|
|
61
27
|
off();
|
|
62
28
|
};
|
|
63
29
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
64
30
|
listeners.push(() => signal.removeEventListener('abort', onAbort));
|
|
65
31
|
}
|
|
32
|
+
if (controller.signal.aborted) {
|
|
33
|
+
for (const off of listeners)
|
|
34
|
+
off();
|
|
35
|
+
}
|
|
66
36
|
return controller.signal;
|
|
67
37
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Race a promise against an AbortSignal.
|
|
70
|
-
*
|
|
71
|
-
* - If the signal is already aborted, rejects immediately with `signal.reason`.
|
|
72
|
-
* - If the signal aborts while the promise is pending, rejects with `signal.reason`.
|
|
73
|
-
* - If the promise settles first, returns its value (or rejects with its error).
|
|
74
|
-
*
|
|
75
|
-
* # Listener safety
|
|
76
|
-
*
|
|
77
|
-
* On success path, the abort listener is explicitly removed. A
|
|
78
|
-
* `{ once: true }` listener would stay attached on long-lived signals,
|
|
79
|
-
* leaking one closure per call.
|
|
80
|
-
*
|
|
81
|
-
* # Mark-as-handled
|
|
82
|
-
*
|
|
83
|
-
* When the signal aborts first, the original `promise` may still settle
|
|
84
|
-
* later (success or failure). We attach a no-op `.catch` to it so V8
|
|
85
|
-
* doesn't emit an `unhandledRejection` warning. The error is NOT
|
|
86
|
-
* swallowed from the caller — the caller already received `signal.reason`.
|
|
87
|
-
*/
|
|
88
38
|
function raceAbort(promise, signal) {
|
|
89
39
|
if (signal.aborted) {
|
|
90
|
-
// Mark the promise as handled — its eventual rejection won't surface
|
|
91
|
-
// as an unhandled rejection. Its eventual success is dropped silently,
|
|
92
|
-
// which is correct: the caller has already moved on.
|
|
93
40
|
promise.catch(() => { });
|
|
94
41
|
return Promise.reject(signal.reason);
|
|
95
42
|
}
|
|
@@ -102,8 +49,6 @@ function raceAbort(promise, signal) {
|
|
|
102
49
|
signal.removeEventListener('abort', onAbort);
|
|
103
50
|
reject(signal.reason);
|
|
104
51
|
};
|
|
105
|
-
// Not using `{ once: true }` — we want explicit removal on success
|
|
106
|
-
// path so the listener doesn't linger on a long-lived signal.
|
|
107
52
|
signal.addEventListener('abort', onAbort);
|
|
108
53
|
promise.then((value) => {
|
|
109
54
|
if (settled)
|
|
@@ -120,20 +65,7 @@ function raceAbort(promise, signal) {
|
|
|
120
65
|
});
|
|
121
66
|
});
|
|
122
67
|
}
|
|
123
|
-
|
|
124
|
-
* Sleep for `ms` milliseconds, but abort early if `signal` fires.
|
|
125
|
-
*
|
|
126
|
-
* Resolves normally on timer expiry. Rejects with `signal.reason` if the
|
|
127
|
-
* signal aborts before the timer fires. If the signal is already aborted
|
|
128
|
-
* when called, rejects synchronously (in microtask).
|
|
129
|
-
*
|
|
130
|
-
* # Timer hygiene
|
|
131
|
-
*
|
|
132
|
-
* The internal `setTimeout` is `unref`'d on Node so it doesn't keep the
|
|
133
|
-
* event loop alive solely for this sleep. On browsers there is no
|
|
134
|
-
* equivalent — the timer is short-lived enough not to matter.
|
|
135
|
-
*/
|
|
136
|
-
function sleep(ms, signal) {
|
|
68
|
+
function sleep(ms, signal, opts) {
|
|
137
69
|
if (ms <= 0) {
|
|
138
70
|
if (signal?.aborted)
|
|
139
71
|
return Promise.reject(signal.reason);
|
|
@@ -146,12 +78,9 @@ function sleep(ms, signal) {
|
|
|
146
78
|
signal?.removeEventListener('abort', onAbort);
|
|
147
79
|
resolve();
|
|
148
80
|
}, ms);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// the process while a retry delay was pending — silently dropping the
|
|
153
|
-
// operation. The timer is short-lived (cleared in onAbort or after ms),
|
|
154
|
-
// so the cost of keeping the loop alive is bounded.
|
|
81
|
+
if (opts?.unref && typeof timer.unref === 'function') {
|
|
82
|
+
timer.unref();
|
|
83
|
+
}
|
|
155
84
|
const onAbort = () => {
|
|
156
85
|
clearTimeout(timer);
|
|
157
86
|
reject(signal.reason);
|
|
@@ -159,20 +88,14 @@ function sleep(ms, signal) {
|
|
|
159
88
|
signal?.addEventListener('abort', onAbort);
|
|
160
89
|
});
|
|
161
90
|
}
|
|
162
|
-
/**
|
|
163
|
-
* True if `err` is an `AbortError` (DOMException name or Error name).
|
|
164
|
-
*
|
|
165
|
-
* The default `shouldRetry` predicate uses this to skip retrying on
|
|
166
|
-
* cancellations — if the caller aborted, retrying would just abort again.
|
|
167
|
-
*/
|
|
168
91
|
function isAbortError(err) {
|
|
169
92
|
if (err == null || typeof err !== 'object')
|
|
170
93
|
return false;
|
|
94
|
+
if (err.code === 'ACTLY_ABORT')
|
|
95
|
+
return true;
|
|
171
96
|
const name = err.name;
|
|
172
97
|
if (name === 'AbortError')
|
|
173
98
|
return true;
|
|
174
|
-
// DOMException with name 'TimeoutError' is what AbortSignal.timeout throws.
|
|
175
|
-
// Distinguish from our own TimeoutError class by checking for DOMException.
|
|
176
99
|
if (name === 'TimeoutError' &&
|
|
177
100
|
err instanceof Error &&
|
|
178
101
|
typeof DOMException !== 'undefined' &&
|
|
@@ -181,26 +104,6 @@ function isAbortError(err) {
|
|
|
181
104
|
}
|
|
182
105
|
return false;
|
|
183
106
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Link a parent signal to a child controller: when the parent aborts, the
|
|
186
|
-
* child is aborted with the same reason.
|
|
187
|
-
*
|
|
188
|
-
* # Contract
|
|
189
|
-
*
|
|
190
|
-
* Returns an `unlink()` function that removes the listener. Callers MUST
|
|
191
|
-
* call `unlink()` on success path — otherwise the listener stays attached
|
|
192
|
-
* to the parent forever, leaking one closure per call.
|
|
193
|
-
*
|
|
194
|
-
* If the parent is already aborted, the child is aborted synchronously
|
|
195
|
-
* and `unlink` is a no-op.
|
|
196
|
-
*
|
|
197
|
-
* # Prefer `anySignal()` instead
|
|
198
|
-
*
|
|
199
|
-
* For new code, prefer `anySignal([parent, timeoutSignal])` which uses
|
|
200
|
-
* the native Node 20+ implementation and handles cleanup automatically.
|
|
201
|
-
* This function exists for call sites that need an `AbortController`
|
|
202
|
-
* (not just a signal) — e.g. to layer additional abort sources.
|
|
203
|
-
*/
|
|
204
107
|
function linkSignal(parent, child) {
|
|
205
108
|
if (parent.aborted) {
|
|
206
109
|
child.abort(parent.reason);
|
package/dist/utils/abort.d.ts
CHANGED
|
@@ -1,79 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compose multiple AbortSignals into one. Aborts when ANY input aborts,
|
|
3
|
-
* with the same reason.
|
|
4
|
-
*
|
|
5
|
-
* Uses native `AbortSignal.any()` on Node 20+ (zero allocations, no
|
|
6
|
-
* listener leaks — the runtime owns the lifecycle). Falls back to a
|
|
7
|
-
* polyfill that explicitly removes listeners on first abort.
|
|
8
|
-
*
|
|
9
|
-
* # Listener safety
|
|
10
|
-
*
|
|
11
|
-
* The polyfill registers `{ once: true }` listeners on each input and
|
|
12
|
-
* manually removes the others when one fires. After settlement, the
|
|
13
|
-
* composite signal holds zero references to the inputs — they may be GC'd.
|
|
14
|
-
*/
|
|
15
1
|
export declare function anySignal(signals: ReadonlyArray<AbortSignal>): AbortSignal;
|
|
16
|
-
/**
|
|
17
|
-
* Race a promise against an AbortSignal.
|
|
18
|
-
*
|
|
19
|
-
* - If the signal is already aborted, rejects immediately with `signal.reason`.
|
|
20
|
-
* - If the signal aborts while the promise is pending, rejects with `signal.reason`.
|
|
21
|
-
* - If the promise settles first, returns its value (or rejects with its error).
|
|
22
|
-
*
|
|
23
|
-
* # Listener safety
|
|
24
|
-
*
|
|
25
|
-
* On success path, the abort listener is explicitly removed. A
|
|
26
|
-
* `{ once: true }` listener would stay attached on long-lived signals,
|
|
27
|
-
* leaking one closure per call.
|
|
28
|
-
*
|
|
29
|
-
* # Mark-as-handled
|
|
30
|
-
*
|
|
31
|
-
* When the signal aborts first, the original `promise` may still settle
|
|
32
|
-
* later (success or failure). We attach a no-op `.catch` to it so V8
|
|
33
|
-
* doesn't emit an `unhandledRejection` warning. The error is NOT
|
|
34
|
-
* swallowed from the caller — the caller already received `signal.reason`.
|
|
35
|
-
*/
|
|
36
2
|
export declare function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* Resolves normally on timer expiry. Rejects with `signal.reason` if the
|
|
41
|
-
* signal aborts before the timer fires. If the signal is already aborted
|
|
42
|
-
* when called, rejects synchronously (in microtask).
|
|
43
|
-
*
|
|
44
|
-
* # Timer hygiene
|
|
45
|
-
*
|
|
46
|
-
* The internal `setTimeout` is `unref`'d on Node so it doesn't keep the
|
|
47
|
-
* event loop alive solely for this sleep. On browsers there is no
|
|
48
|
-
* equivalent — the timer is short-lived enough not to matter.
|
|
49
|
-
*/
|
|
50
|
-
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* True if `err` is an `AbortError` (DOMException name or Error name).
|
|
53
|
-
*
|
|
54
|
-
* The default `shouldRetry` predicate uses this to skip retrying on
|
|
55
|
-
* cancellations — if the caller aborted, retrying would just abort again.
|
|
56
|
-
*/
|
|
3
|
+
export declare function sleep(ms: number, signal?: AbortSignal, opts?: {
|
|
4
|
+
unref?: boolean;
|
|
5
|
+
}): Promise<void>;
|
|
57
6
|
export declare function isAbortError(err: unknown): boolean;
|
|
58
|
-
/**
|
|
59
|
-
* Link a parent signal to a child controller: when the parent aborts, the
|
|
60
|
-
* child is aborted with the same reason.
|
|
61
|
-
*
|
|
62
|
-
* # Contract
|
|
63
|
-
*
|
|
64
|
-
* Returns an `unlink()` function that removes the listener. Callers MUST
|
|
65
|
-
* call `unlink()` on success path — otherwise the listener stays attached
|
|
66
|
-
* to the parent forever, leaking one closure per call.
|
|
67
|
-
*
|
|
68
|
-
* If the parent is already aborted, the child is aborted synchronously
|
|
69
|
-
* and `unlink` is a no-op.
|
|
70
|
-
*
|
|
71
|
-
* # Prefer `anySignal()` instead
|
|
72
|
-
*
|
|
73
|
-
* For new code, prefer `anySignal([parent, timeoutSignal])` which uses
|
|
74
|
-
* the native Node 20+ implementation and handles cleanup automatically.
|
|
75
|
-
* This function exists for call sites that need an `AbortController`
|
|
76
|
-
* (not just a signal) — e.g. to layer additional abort sources.
|
|
77
|
-
*/
|
|
78
7
|
export declare function linkSignal(parent: AbortSignal, child: AbortController): () => void;
|
|
79
8
|
//# sourceMappingURL=abort.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../../src/utils/abort.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../../src/utils/abort.ts"],"names":[],"mappings":"AAaA,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,WAAW,CAgC1E;AAOD,wBAAgB,SAAS,CAAC,CAAC,EACzB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,CAAC,CAAC,CAmCZ;AAWD,wBAAgB,KAAK,CACnB,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAQD,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAelD;AASD,wBAAgB,UAAU,CACxB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,eAAe,GACrB,MAAM,IAAI,CAQZ"}
|
package/dist/utils/abort.js
CHANGED
|
@@ -1,44 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Contract: zero listener accumulation on long-lived AbortSignals.
|
|
4
|
-
//
|
|
5
|
-
// Every `addEventListener('abort', ...)` MUST be paired with a
|
|
6
|
-
// `removeEventListener` on the success path, OR use `AbortSignal.any()`
|
|
7
|
-
// (Node 20+ native) which handles cleanup automatically.
|
|
8
|
-
//
|
|
9
|
-
// Long-lived signals (server shutdown, request pool) must not accumulate
|
|
10
|
-
// one listener per `act()` call — that would cause
|
|
11
|
-
// `MaxListenersExceededWarning` and unbounded closure retention.
|
|
12
|
-
/**
|
|
13
|
-
* Compose multiple AbortSignals into one. Aborts when ANY input aborts,
|
|
14
|
-
* with the same reason.
|
|
15
|
-
*
|
|
16
|
-
* Uses native `AbortSignal.any()` on Node 20+ (zero allocations, no
|
|
17
|
-
* listener leaks — the runtime owns the lifecycle). Falls back to a
|
|
18
|
-
* polyfill that explicitly removes listeners on first abort.
|
|
19
|
-
*
|
|
20
|
-
* # Listener safety
|
|
21
|
-
*
|
|
22
|
-
* The polyfill registers `{ once: true }` listeners on each input and
|
|
23
|
-
* manually removes the others when one fires. After settlement, the
|
|
24
|
-
* composite signal holds zero references to the inputs — they may be GC'd.
|
|
25
|
-
*/
|
|
1
|
+
const NATIVE_ANY = AbortSignal.any;
|
|
26
2
|
export function anySignal(signals) {
|
|
27
|
-
// Filter out undefined / null defensively.
|
|
28
3
|
const filtered = signals.filter((s) => s != null);
|
|
29
|
-
if (filtered.length === 0)
|
|
30
|
-
// No inputs → never-aborting signal. Use a fresh controller so callers
|
|
31
|
-
// get a real AbortSignal, not a hand-rolled mock.
|
|
4
|
+
if (filtered.length === 0)
|
|
32
5
|
return new AbortController().signal;
|
|
33
|
-
}
|
|
34
6
|
if (filtered.length === 1)
|
|
35
7
|
return filtered[0];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (typeof native === 'function') {
|
|
39
|
-
return native.call(AbortSignal, filtered);
|
|
40
|
-
}
|
|
41
|
-
// Polyfill: only used when native is missing. Listener-safe.
|
|
8
|
+
if (NATIVE_ANY)
|
|
9
|
+
return NATIVE_ANY.call(AbortSignal, filtered);
|
|
42
10
|
const controller = new AbortController();
|
|
43
11
|
const listeners = [];
|
|
44
12
|
for (const signal of filtered) {
|
|
@@ -48,41 +16,20 @@ export function anySignal(signals) {
|
|
|
48
16
|
}
|
|
49
17
|
const onAbort = () => {
|
|
50
18
|
controller.abort(signal.reason);
|
|
51
|
-
// Remove all other listeners — they hold closures over `signal`
|
|
52
|
-
// and would otherwise keep the inputs alive past settlement.
|
|
53
19
|
for (const off of listeners)
|
|
54
20
|
off();
|
|
55
21
|
};
|
|
56
22
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
57
23
|
listeners.push(() => signal.removeEventListener('abort', onAbort));
|
|
58
24
|
}
|
|
25
|
+
if (controller.signal.aborted) {
|
|
26
|
+
for (const off of listeners)
|
|
27
|
+
off();
|
|
28
|
+
}
|
|
59
29
|
return controller.signal;
|
|
60
30
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Race a promise against an AbortSignal.
|
|
63
|
-
*
|
|
64
|
-
* - If the signal is already aborted, rejects immediately with `signal.reason`.
|
|
65
|
-
* - If the signal aborts while the promise is pending, rejects with `signal.reason`.
|
|
66
|
-
* - If the promise settles first, returns its value (or rejects with its error).
|
|
67
|
-
*
|
|
68
|
-
* # Listener safety
|
|
69
|
-
*
|
|
70
|
-
* On success path, the abort listener is explicitly removed. A
|
|
71
|
-
* `{ once: true }` listener would stay attached on long-lived signals,
|
|
72
|
-
* leaking one closure per call.
|
|
73
|
-
*
|
|
74
|
-
* # Mark-as-handled
|
|
75
|
-
*
|
|
76
|
-
* When the signal aborts first, the original `promise` may still settle
|
|
77
|
-
* later (success or failure). We attach a no-op `.catch` to it so V8
|
|
78
|
-
* doesn't emit an `unhandledRejection` warning. The error is NOT
|
|
79
|
-
* swallowed from the caller — the caller already received `signal.reason`.
|
|
80
|
-
*/
|
|
81
31
|
export function raceAbort(promise, signal) {
|
|
82
32
|
if (signal.aborted) {
|
|
83
|
-
// Mark the promise as handled — its eventual rejection won't surface
|
|
84
|
-
// as an unhandled rejection. Its eventual success is dropped silently,
|
|
85
|
-
// which is correct: the caller has already moved on.
|
|
86
33
|
promise.catch(() => { });
|
|
87
34
|
return Promise.reject(signal.reason);
|
|
88
35
|
}
|
|
@@ -95,8 +42,6 @@ export function raceAbort(promise, signal) {
|
|
|
95
42
|
signal.removeEventListener('abort', onAbort);
|
|
96
43
|
reject(signal.reason);
|
|
97
44
|
};
|
|
98
|
-
// Not using `{ once: true }` — we want explicit removal on success
|
|
99
|
-
// path so the listener doesn't linger on a long-lived signal.
|
|
100
45
|
signal.addEventListener('abort', onAbort);
|
|
101
46
|
promise.then((value) => {
|
|
102
47
|
if (settled)
|
|
@@ -113,20 +58,7 @@ export function raceAbort(promise, signal) {
|
|
|
113
58
|
});
|
|
114
59
|
});
|
|
115
60
|
}
|
|
116
|
-
|
|
117
|
-
* Sleep for `ms` milliseconds, but abort early if `signal` fires.
|
|
118
|
-
*
|
|
119
|
-
* Resolves normally on timer expiry. Rejects with `signal.reason` if the
|
|
120
|
-
* signal aborts before the timer fires. If the signal is already aborted
|
|
121
|
-
* when called, rejects synchronously (in microtask).
|
|
122
|
-
*
|
|
123
|
-
* # Timer hygiene
|
|
124
|
-
*
|
|
125
|
-
* The internal `setTimeout` is `unref`'d on Node so it doesn't keep the
|
|
126
|
-
* event loop alive solely for this sleep. On browsers there is no
|
|
127
|
-
* equivalent — the timer is short-lived enough not to matter.
|
|
128
|
-
*/
|
|
129
|
-
export function sleep(ms, signal) {
|
|
61
|
+
export function sleep(ms, signal, opts) {
|
|
130
62
|
if (ms <= 0) {
|
|
131
63
|
if (signal?.aborted)
|
|
132
64
|
return Promise.reject(signal.reason);
|
|
@@ -139,12 +71,9 @@ export function sleep(ms, signal) {
|
|
|
139
71
|
signal?.removeEventListener('abort', onAbort);
|
|
140
72
|
resolve();
|
|
141
73
|
}, ms);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// the process while a retry delay was pending — silently dropping the
|
|
146
|
-
// operation. The timer is short-lived (cleared in onAbort or after ms),
|
|
147
|
-
// so the cost of keeping the loop alive is bounded.
|
|
74
|
+
if (opts?.unref && typeof timer.unref === 'function') {
|
|
75
|
+
timer.unref();
|
|
76
|
+
}
|
|
148
77
|
const onAbort = () => {
|
|
149
78
|
clearTimeout(timer);
|
|
150
79
|
reject(signal.reason);
|
|
@@ -152,20 +81,14 @@ export function sleep(ms, signal) {
|
|
|
152
81
|
signal?.addEventListener('abort', onAbort);
|
|
153
82
|
});
|
|
154
83
|
}
|
|
155
|
-
/**
|
|
156
|
-
* True if `err` is an `AbortError` (DOMException name or Error name).
|
|
157
|
-
*
|
|
158
|
-
* The default `shouldRetry` predicate uses this to skip retrying on
|
|
159
|
-
* cancellations — if the caller aborted, retrying would just abort again.
|
|
160
|
-
*/
|
|
161
84
|
export function isAbortError(err) {
|
|
162
85
|
if (err == null || typeof err !== 'object')
|
|
163
86
|
return false;
|
|
87
|
+
if (err.code === 'ACTLY_ABORT')
|
|
88
|
+
return true;
|
|
164
89
|
const name = err.name;
|
|
165
90
|
if (name === 'AbortError')
|
|
166
91
|
return true;
|
|
167
|
-
// DOMException with name 'TimeoutError' is what AbortSignal.timeout throws.
|
|
168
|
-
// Distinguish from our own TimeoutError class by checking for DOMException.
|
|
169
92
|
if (name === 'TimeoutError' &&
|
|
170
93
|
err instanceof Error &&
|
|
171
94
|
typeof DOMException !== 'undefined' &&
|
|
@@ -174,26 +97,6 @@ export function isAbortError(err) {
|
|
|
174
97
|
}
|
|
175
98
|
return false;
|
|
176
99
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Link a parent signal to a child controller: when the parent aborts, the
|
|
179
|
-
* child is aborted with the same reason.
|
|
180
|
-
*
|
|
181
|
-
* # Contract
|
|
182
|
-
*
|
|
183
|
-
* Returns an `unlink()` function that removes the listener. Callers MUST
|
|
184
|
-
* call `unlink()` on success path — otherwise the listener stays attached
|
|
185
|
-
* to the parent forever, leaking one closure per call.
|
|
186
|
-
*
|
|
187
|
-
* If the parent is already aborted, the child is aborted synchronously
|
|
188
|
-
* and `unlink` is a no-op.
|
|
189
|
-
*
|
|
190
|
-
* # Prefer `anySignal()` instead
|
|
191
|
-
*
|
|
192
|
-
* For new code, prefer `anySignal([parent, timeoutSignal])` which uses
|
|
193
|
-
* the native Node 20+ implementation and handles cleanup automatically.
|
|
194
|
-
* This function exists for call sites that need an `AbortController`
|
|
195
|
-
* (not just a signal) — e.g. to layer additional abort sources.
|
|
196
|
-
*/
|
|
197
100
|
export function linkSignal(parent, child) {
|
|
198
101
|
if (parent.aborted) {
|
|
199
102
|
child.abort(parent.reason);
|
|
@@ -203,4 +106,3 @@ export function linkSignal(parent, child) {
|
|
|
203
106
|
parent.addEventListener('abort', onAbort);
|
|
204
107
|
return () => parent.removeEventListener('abort', onAbort);
|
|
205
108
|
}
|
|
206
|
-
//# sourceMappingURL=abort.js.map
|
package/dist/utils/abortPool.cjs
CHANGED
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abortPool.d.ts","sourceRoot":"","sources":["../../src/utils/abortPool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"abortPool.d.ts","sourceRoot":"","sources":["../../src/utils/abortPool.ts"],"names":[],"mappings":"AAMA,wBAAgB,iBAAiB,IAAI,eAAe,CAOnD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,IAAI,CAK1D;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC"}
|
package/dist/utils/abortPool.js
CHANGED
|
@@ -3,9 +3,8 @@ const MAX_POOL_SIZE = 64;
|
|
|
3
3
|
export function acquireController() {
|
|
4
4
|
while (pool.length > 0) {
|
|
5
5
|
const c = pool.pop();
|
|
6
|
-
if (!c.signal.aborted)
|
|
6
|
+
if (!c.signal.aborted)
|
|
7
7
|
return c;
|
|
8
|
-
}
|
|
9
8
|
}
|
|
10
9
|
return new AbortController();
|
|
11
10
|
}
|
|
@@ -19,4 +18,3 @@ export function releaseController(c) {
|
|
|
19
18
|
export function poolSize() {
|
|
20
19
|
return pool.length;
|
|
21
20
|
}
|
|
22
|
-
//# sourceMappingURL=abortPool.js.map
|
package/dist/utils/backoff.cjs
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.computeDelay = computeDelay;
|
|
4
|
-
/**
|
|
5
|
-
* Compute the delay before the next retry attempt, applying backoff, cap, and
|
|
6
|
-
* jitter in that order.
|
|
7
|
-
*
|
|
8
|
-
* Order matters:
|
|
9
|
-
* 1. `backoff` grows the base delay geometrically/linearly.
|
|
10
|
-
* 2. `maxDelay` caps the result (prevents exponential blowup).
|
|
11
|
-
* 3. `jitter` randomises within `[0, delay]` (prevents thundering herd).
|
|
12
|
-
*
|
|
13
|
-
* Returns 0 if `delayMs` is 0 or undefined — skipping the sleep entirely.
|
|
14
|
-
*/
|
|
15
4
|
function computeDelay(attempt, opts) {
|
|
16
5
|
const base = opts.delayMs ?? 0;
|
|
17
6
|
if (base === 0)
|
|
18
7
|
return 0;
|
|
19
|
-
// Step 1: backoff
|
|
20
8
|
let delay;
|
|
21
9
|
switch (opts.backoff ?? 'none') {
|
|
22
10
|
case 'linear':
|
|
@@ -27,24 +15,18 @@ function computeDelay(attempt, opts) {
|
|
|
27
15
|
break;
|
|
28
16
|
default: delay = base;
|
|
29
17
|
}
|
|
30
|
-
// Step 2: cap (guard against Infinity and NaN before Math.min)
|
|
31
18
|
const max = opts.maxDelay ?? Number.POSITIVE_INFINITY;
|
|
32
19
|
if (!Number.isFinite(delay))
|
|
33
20
|
delay = max;
|
|
34
21
|
delay = Math.min(delay, max);
|
|
35
|
-
// Step 3: jitter
|
|
36
|
-
// All jitter variants produce a delay in [0, delay].
|
|
37
|
-
// The `decorrelated` formula `base + random() * (delay - base)` assumes
|
|
38
|
-
// delay >= base, which can be violated when maxDelay caps below base.
|
|
39
|
-
// Fix: clamp the final result to [0, delay] to guarantee the cap holds.
|
|
40
22
|
switch (opts.jitter ?? 'full') {
|
|
41
23
|
case 'none': return delay;
|
|
42
24
|
case 'full': return Math.random() * delay;
|
|
43
25
|
case 'equal': return delay / 2 + Math.random() * delay / 2;
|
|
44
26
|
case 'decorrelated': {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const lo =
|
|
27
|
+
if (delay < base)
|
|
28
|
+
return Math.random() * delay;
|
|
29
|
+
const lo = base;
|
|
48
30
|
const hi = delay;
|
|
49
31
|
const result = lo + Math.random() * (hi - lo);
|
|
50
32
|
return Math.max(0, Math.min(result, delay));
|
package/dist/utils/backoff.d.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
1
|
import type { RetryOptions } from '../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Compute the delay before the next retry attempt, applying backoff, cap, and
|
|
4
|
-
* jitter in that order.
|
|
5
|
-
*
|
|
6
|
-
* Order matters:
|
|
7
|
-
* 1. `backoff` grows the base delay geometrically/linearly.
|
|
8
|
-
* 2. `maxDelay` caps the result (prevents exponential blowup).
|
|
9
|
-
* 3. `jitter` randomises within `[0, delay]` (prevents thundering herd).
|
|
10
|
-
*
|
|
11
|
-
* Returns 0 if `delayMs` is 0 or undefined — skipping the sleep entirely.
|
|
12
|
-
*/
|
|
13
2
|
export declare function computeDelay(attempt: number, opts: RetryOptions): number;
|
|
14
3
|
//# sourceMappingURL=backoff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AASrD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAgCxE"}
|
package/dist/utils/backoff.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compute the delay before the next retry attempt, applying backoff, cap, and
|
|
3
|
-
* jitter in that order.
|
|
4
|
-
*
|
|
5
|
-
* Order matters:
|
|
6
|
-
* 1. `backoff` grows the base delay geometrically/linearly.
|
|
7
|
-
* 2. `maxDelay` caps the result (prevents exponential blowup).
|
|
8
|
-
* 3. `jitter` randomises within `[0, delay]` (prevents thundering herd).
|
|
9
|
-
*
|
|
10
|
-
* Returns 0 if `delayMs` is 0 or undefined — skipping the sleep entirely.
|
|
11
|
-
*/
|
|
12
1
|
export function computeDelay(attempt, opts) {
|
|
13
2
|
const base = opts.delayMs ?? 0;
|
|
14
3
|
if (base === 0)
|
|
15
4
|
return 0;
|
|
16
|
-
// Step 1: backoff
|
|
17
5
|
let delay;
|
|
18
6
|
switch (opts.backoff ?? 'none') {
|
|
19
7
|
case 'linear':
|
|
@@ -24,24 +12,18 @@ export function computeDelay(attempt, opts) {
|
|
|
24
12
|
break;
|
|
25
13
|
default: delay = base;
|
|
26
14
|
}
|
|
27
|
-
// Step 2: cap (guard against Infinity and NaN before Math.min)
|
|
28
15
|
const max = opts.maxDelay ?? Number.POSITIVE_INFINITY;
|
|
29
16
|
if (!Number.isFinite(delay))
|
|
30
17
|
delay = max;
|
|
31
18
|
delay = Math.min(delay, max);
|
|
32
|
-
// Step 3: jitter
|
|
33
|
-
// All jitter variants produce a delay in [0, delay].
|
|
34
|
-
// The `decorrelated` formula `base + random() * (delay - base)` assumes
|
|
35
|
-
// delay >= base, which can be violated when maxDelay caps below base.
|
|
36
|
-
// Fix: clamp the final result to [0, delay] to guarantee the cap holds.
|
|
37
19
|
switch (opts.jitter ?? 'full') {
|
|
38
20
|
case 'none': return delay;
|
|
39
21
|
case 'full': return Math.random() * delay;
|
|
40
22
|
case 'equal': return delay / 2 + Math.random() * delay / 2;
|
|
41
23
|
case 'decorrelated': {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const lo =
|
|
24
|
+
if (delay < base)
|
|
25
|
+
return Math.random() * delay;
|
|
26
|
+
const lo = base;
|
|
45
27
|
const hi = delay;
|
|
46
28
|
const result = lo + Math.random() * (hi - lo);
|
|
47
29
|
return Math.max(0, Math.min(result, delay));
|
|
@@ -49,4 +31,3 @@ export function computeDelay(attempt, opts) {
|
|
|
49
31
|
default: return delay;
|
|
50
32
|
}
|
|
51
33
|
}
|
|
52
|
-
//# sourceMappingURL=backoff.js.map
|