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/types/index.d.ts
CHANGED
|
@@ -1,51 +1,145 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The async function ACT wraps.
|
|
3
|
+
*
|
|
4
|
+
* Receives an {@link AbortSignal} that fires when:
|
|
5
|
+
* - the caller aborts via `options.signal`,
|
|
6
|
+
* - the per-attempt {@link TimeoutOptions} fires,
|
|
7
|
+
* - the operation-wide {@link ActOptions.totalTimeout} fires.
|
|
8
|
+
*
|
|
9
|
+
* Cooperative cancellation: pass `signal` through to `fetch`, `AbortController`,
|
|
10
|
+
* database drivers, or any primitive that accepts one. If you ignore it, ACT
|
|
11
|
+
* will still return promptly (the outer promise rejects), but the underlying
|
|
12
|
+
* work will keep running in the background — leaking resources until it
|
|
13
|
+
* settles on its own.
|
|
14
|
+
*
|
|
15
|
+
* Backwards compatible: `() => Promise<T>` is assignable to this type, so
|
|
16
|
+
* existing call sites continue to compile and run. They simply forgo
|
|
17
|
+
* cancellation.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Cooperative
|
|
21
|
+
* act('user:42', async (signal) => {
|
|
22
|
+
* return fetch(`/api/users/42`, { signal })
|
|
23
|
+
* }, { timeout: { ms: 5_000 } })
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Legacy (still works, signal ignored)
|
|
27
|
+
* act('user:42', () => fetchUser(42))
|
|
28
|
+
*/
|
|
29
|
+
export type ActFn<T> = (signal: AbortSignal) => Promise<T> | T;
|
|
30
|
+
/** Where a successful result came from. */
|
|
4
31
|
export type ActSource = 'fresh' | 'cache';
|
|
5
32
|
export interface ActSuccess<T> {
|
|
6
33
|
ok: true;
|
|
7
34
|
value: T;
|
|
8
35
|
source: ActSource;
|
|
9
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Number of attempts made before success.
|
|
38
|
+
*
|
|
39
|
+
* - Fresh success on first try: `1`
|
|
40
|
+
* - Fresh success after N retries: `N`
|
|
41
|
+
* - Cache hit: `0` (no work was performed)
|
|
42
|
+
* - Dedupe joiner: mirrors the originator's attempt count
|
|
43
|
+
*/
|
|
10
44
|
attempts: number;
|
|
45
|
+
/**
|
|
46
|
+
* Trace ID for correlation across logs/metrics. Present when
|
|
47
|
+
* `options.observability` or `options.traceId` is set; `undefined` otherwise.
|
|
48
|
+
*/
|
|
49
|
+
traceId?: string;
|
|
50
|
+
/** Wall-clock duration of this act() call in ms. */
|
|
51
|
+
durationMs?: number;
|
|
11
52
|
}
|
|
12
53
|
export interface ActFailure {
|
|
13
54
|
ok: false;
|
|
14
55
|
error: unknown;
|
|
15
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Number of attempts made before final failure.
|
|
58
|
+
* For dedupe joiners: mirrors the originator's attempt count.
|
|
59
|
+
*/
|
|
16
60
|
attempts: number;
|
|
61
|
+
/**
|
|
62
|
+
* Trace ID for correlation across logs/metrics. Present when
|
|
63
|
+
* `options.observability` or `options.traceId` is set; `undefined` otherwise.
|
|
64
|
+
*/
|
|
65
|
+
traceId?: string;
|
|
66
|
+
/** Wall-clock duration of this act() call in ms. */
|
|
67
|
+
durationMs?: number;
|
|
17
68
|
}
|
|
18
69
|
export type ActResult<T> = ActSuccess<T> | ActFailure;
|
|
19
70
|
export interface RetryOptions {
|
|
20
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Total number of attempts including the first call.
|
|
73
|
+
* Must be an integer >= 1.
|
|
74
|
+
*
|
|
75
|
+
* `attempts: 1` is a no-op (equivalent to omitting `retry`); the policy
|
|
76
|
+
* is not added to the chain. This is intentional — adding a policy that
|
|
77
|
+
* never retries is pure overhead.
|
|
78
|
+
*/
|
|
21
79
|
attempts: number;
|
|
22
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Base delay between attempts in milliseconds. Defaults to 0 (no delay).
|
|
82
|
+
* Must be a non-negative finite number.
|
|
83
|
+
*/
|
|
23
84
|
delayMs?: number;
|
|
24
85
|
/**
|
|
25
86
|
* How the base delay grows per attempt:
|
|
26
|
-
* - 'none' -> always delayMs
|
|
27
|
-
* - 'linear' -> delayMs * attempt
|
|
28
|
-
* - 'exponential' -> delayMs * 2^(attempt-1)
|
|
87
|
+
* - `'none'` -> always `delayMs`
|
|
88
|
+
* - `'linear'` -> `delayMs * attempt`
|
|
89
|
+
* - `'exponential'` -> `delayMs * 2^(attempt-1)`
|
|
90
|
+
*
|
|
91
|
+
* The computed delay is then capped by {@link maxDelay} and jittered by
|
|
92
|
+
* {@link jitter} before being slept.
|
|
93
|
+
*
|
|
94
|
+
* Defaults to `'none'`.
|
|
29
95
|
*/
|
|
30
96
|
backoff?: 'none' | 'linear' | 'exponential';
|
|
97
|
+
/**
|
|
98
|
+
* Hard cap on the computed delay. Defaults to `Infinity`.
|
|
99
|
+
*
|
|
100
|
+
* Without a cap, `exponential` backoff with `delayMs: 1000` and
|
|
101
|
+
* `attempts: 10` would sleep 8.5 minutes between attempts 9 and 10
|
|
102
|
+
* (256 seconds). Set `maxDelay` to something sane (e.g. 30_000) to
|
|
103
|
+
* bound worst-case latency.
|
|
104
|
+
*/
|
|
105
|
+
maxDelay?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Jitter strategy applied to the (post-`maxDelay`) delay.
|
|
108
|
+
*
|
|
109
|
+
* - `'none'` -> no jitter, return delay as-is
|
|
110
|
+
* - `'full'` -> `random() * delay` (default; best for thundering-herd prevention)
|
|
111
|
+
* - `'equal'` -> `delay/2 + random() * delay/2`
|
|
112
|
+
* - `'decorrelated'` -> `base + random() * (delay - base)`
|
|
113
|
+
*
|
|
114
|
+
* Defaults to `'full'`. Jitter prevents synchronised retry storms when
|
|
115
|
+
* many callers fail at the same instant (e.g. after an upstream outage
|
|
116
|
+
* recovers) — without it, all callers retry on the same tick.
|
|
117
|
+
*/
|
|
118
|
+
jitter?: 'none' | 'full' | 'equal' | 'decorrelated';
|
|
31
119
|
/**
|
|
32
120
|
* Predicate called after each failure, before the next attempt.
|
|
33
|
-
* Return false to stop retrying immediately and surface the error.
|
|
121
|
+
* Return `false` to stop retrying immediately and surface the error.
|
|
122
|
+
*
|
|
123
|
+
* Called for every failure including the last attempt (so observers stay
|
|
124
|
+
* informed); the return value is only consulted when there are remaining
|
|
125
|
+
* attempts.
|
|
34
126
|
*
|
|
35
127
|
* Use this to skip retries for errors that are definitively non-recoverable
|
|
36
128
|
* (e.g. HTTP 4xx, AuthError, ValidationError).
|
|
37
129
|
*
|
|
130
|
+
* Default behaviour: retry on every error except `AbortError` (which
|
|
131
|
+
* indicates the caller or a timeout cancelled the operation).
|
|
132
|
+
*
|
|
38
133
|
* @param error The error thrown by the most recent attempt.
|
|
39
134
|
* @param attempt The 1-based number of the attempt that just failed.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* shouldRetry: (err, attempt) =>
|
|
43
|
-
* !(err instanceof HttpError && err.status < 500)
|
|
44
135
|
*/
|
|
45
136
|
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
46
137
|
}
|
|
47
138
|
export interface TimeoutOptions {
|
|
48
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Abort after this many milliseconds.
|
|
141
|
+
* Must be a positive finite number.
|
|
142
|
+
*/
|
|
49
143
|
ms: number;
|
|
50
144
|
}
|
|
51
145
|
export interface DedupeOptions {
|
|
@@ -54,75 +148,259 @@ export interface DedupeOptions {
|
|
|
54
148
|
* Opt-in: be explicit when you want this behaviour.
|
|
55
149
|
*/
|
|
56
150
|
enabled: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Safety-net TTL for the in-flight entry, in milliseconds.
|
|
153
|
+
*
|
|
154
|
+
* If the originator's promise does not settle within this window, the
|
|
155
|
+
* entry is removed from the store so subsequent callers can start fresh.
|
|
156
|
+
* Originator's promise continues in the background until it settles or
|
|
157
|
+
* an outer timeout fires.
|
|
158
|
+
*
|
|
159
|
+
* Default: `Infinity` (no safety net). Pair with `timeout` or
|
|
160
|
+
* `totalTimeout` for proper cancellation in production.
|
|
161
|
+
*/
|
|
162
|
+
inflightTtl?: number;
|
|
57
163
|
}
|
|
58
164
|
export interface CacheOptions {
|
|
59
|
-
/** Keep a successful result for this many
|
|
165
|
+
/** Keep a successful result for this many milliseconds. Must be > 0. */
|
|
60
166
|
ttl: number;
|
|
61
167
|
}
|
|
62
|
-
export interface ActOptions {
|
|
168
|
+
export interface ActOptions<T = unknown> {
|
|
63
169
|
retry?: RetryOptions;
|
|
170
|
+
/** Per-attempt deadline. Each retry gets a fresh clock. */
|
|
64
171
|
timeout?: TimeoutOptions;
|
|
65
172
|
/**
|
|
66
173
|
* Collapse concurrent calls with the same key into one in-flight Promise.
|
|
67
174
|
*
|
|
68
175
|
* Shorthand: `dedupe: true`
|
|
69
|
-
* Full form: `dedupe: { enabled: true }`
|
|
70
|
-
*
|
|
71
|
-
* Both are equivalent. The object form exists for forward compatibility.
|
|
176
|
+
* Full form: `dedupe: { enabled: true, inflightTtl: 30_000 }`
|
|
72
177
|
*/
|
|
73
178
|
dedupe?: boolean | DedupeOptions;
|
|
74
179
|
cache?: CacheOptions;
|
|
75
180
|
/**
|
|
76
181
|
* Hard budget over the ENTIRE operation — including all retry attempts,
|
|
77
|
-
* delays, and the
|
|
182
|
+
* delays, and the per-attempt timeout.
|
|
78
183
|
*
|
|
79
184
|
* Distinct from `timeout`, which resets the clock on every attempt.
|
|
80
185
|
* Use both together to express: "each attempt may take at most X ms,
|
|
81
186
|
* but the whole thing must finish within Y ms."
|
|
82
187
|
*
|
|
83
|
-
* Rejects with TotalTimeoutError if the
|
|
188
|
+
* Rejects with {@link TotalTimeoutError} if the budget fires.
|
|
84
189
|
*/
|
|
85
190
|
totalTimeout?: TimeoutOptions;
|
|
191
|
+
/**
|
|
192
|
+
* Caller-provided cancellation signal.
|
|
193
|
+
*
|
|
194
|
+
* When this signal aborts:
|
|
195
|
+
* - if the operation has not yet started, it rejects immediately with
|
|
196
|
+
* the signal's `reason`,
|
|
197
|
+
* - if it is in progress, the inner {@link ActFn} receives an aborted
|
|
198
|
+
* signal (cooperative cancellation),
|
|
199
|
+
* - if it has already settled, the result is returned as normal.
|
|
200
|
+
*
|
|
201
|
+
* Combined with `timeout` / `totalTimeout`, this gives you full control
|
|
202
|
+
* over cancellation from outside `act()`.
|
|
203
|
+
*/
|
|
204
|
+
signal?: AbortSignal;
|
|
205
|
+
/**
|
|
206
|
+
* Observability hooks. All optional. When omitted entirely (the
|
|
207
|
+
* common case), zero overhead is incurred on the hot path — no event
|
|
208
|
+
* objects are allocated, no function calls are made.
|
|
209
|
+
*
|
|
210
|
+
* When hooks ARE registered, events are allocated lazily — only when
|
|
211
|
+
* the corresponding event actually fires.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* await act('user:42', fn, {
|
|
216
|
+
* retry: { attempts: 3 },
|
|
217
|
+
* observability: {
|
|
218
|
+
* onAttempt: (e) => metrics.increment('act.attempt', { key: e.key, attempt: e.attempt }),
|
|
219
|
+
* onFinalFailure: (e) => logger.error({ key: e.key, traceId: e.traceId, failedBy: e.failedBy }, 'act failed'),
|
|
220
|
+
* onFinalSuccess: (e) => metrics.histogram('act.duration', e.durationMs),
|
|
221
|
+
* },
|
|
222
|
+
* })
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
observability?: ObservabilityHooks;
|
|
226
|
+
/**
|
|
227
|
+
* Trace ID for logs/metrics correlation. Auto-generated via crypto.randomUUID()
|
|
228
|
+
* when omitted. Appears on every observability event and on ActResult.traceId.
|
|
229
|
+
*/
|
|
230
|
+
traceId?: string;
|
|
231
|
+
/** Circuit breaker: trips open after N consecutive failures, blocks calls for a cooldown period. */
|
|
232
|
+
circuitBreaker?: CircuitBreakerOptions;
|
|
233
|
+
/** Bulkhead: limits concurrent in-flight calls per key. Excess callers queue or fail fast. */
|
|
234
|
+
bulkhead?: BulkheadOptions;
|
|
235
|
+
/** Rate limiter: limits calls per window per key. Excess callers fail with RateLimitError. */
|
|
236
|
+
rateLimit?: RateLimitOptions;
|
|
237
|
+
/** Hedge: sends a second fn call after delayMs if the first hasn't settled. Races them. */
|
|
238
|
+
hedge?: HedgeOptions;
|
|
239
|
+
/** Fallback: returns this value if all policies fail. Suppresses ActFailure. */
|
|
240
|
+
fallback?: FallbackOptions<T>;
|
|
241
|
+
/** Audit: logs every act() call with key, traceId, result, timestamp. */
|
|
242
|
+
audit?: AuditOptions;
|
|
243
|
+
}
|
|
244
|
+
export interface CircuitBreakerOptions {
|
|
245
|
+
/** Number of consecutive failures before the breaker opens. Must be >= 1. */
|
|
246
|
+
threshold: number;
|
|
247
|
+
/** How long to stay open before transitioning to half-open (ms). Must be > 0. */
|
|
248
|
+
cooldownMs: number;
|
|
249
|
+
/** Optional: reset failure count after this idle period (ms). Default: Infinity. */
|
|
250
|
+
resetTimeoutMs?: number;
|
|
251
|
+
}
|
|
252
|
+
export interface BulkheadOptions {
|
|
253
|
+
/** Max concurrent in-flight calls per key. Must be >= 1. */
|
|
254
|
+
maxConcurrent: number;
|
|
255
|
+
/** How long to queue before rejecting with BulkheadOverflowError (ms). Default: 0 (fail fast). */
|
|
256
|
+
queueTimeoutMs?: number;
|
|
257
|
+
}
|
|
258
|
+
export interface RateLimitOptions {
|
|
259
|
+
/** Max calls per window per key. Must be >= 1. */
|
|
260
|
+
maxCalls: number;
|
|
261
|
+
/** Window size in ms. Must be > 0. */
|
|
262
|
+
windowMs: number;
|
|
263
|
+
}
|
|
264
|
+
export interface HedgeOptions {
|
|
265
|
+
/** Delay before sending the second (hedge) call (ms). Must be > 0. */
|
|
266
|
+
delayMs: number;
|
|
267
|
+
}
|
|
268
|
+
export interface FallbackOptions<T> {
|
|
269
|
+
/** Value to return if all retries/policies fail. */
|
|
270
|
+
value: T | (() => T | Promise<T>);
|
|
271
|
+
}
|
|
272
|
+
export interface AuditOptions {
|
|
273
|
+
/** Called with audit entry after every act() call (success or failure). */
|
|
274
|
+
log: (entry: AuditEntry) => void;
|
|
275
|
+
}
|
|
276
|
+
export interface AuditEntry {
|
|
277
|
+
key: string;
|
|
278
|
+
traceId: string;
|
|
279
|
+
timestamp: number;
|
|
280
|
+
durationMs: number;
|
|
281
|
+
ok: boolean;
|
|
282
|
+
attempts: number;
|
|
283
|
+
failedBy?: string;
|
|
284
|
+
error?: unknown;
|
|
86
285
|
}
|
|
87
286
|
/**
|
|
88
287
|
* Mutable bag mutated in-place during execution.
|
|
89
288
|
* Policies annotate it; act() reads the final state to build ActResult.
|
|
289
|
+
*
|
|
290
|
+
* For dedupe joiners: the bag is copied from the originator's bag after the
|
|
291
|
+
* in-flight promise settles (success or failure), so `attempts` reflects
|
|
292
|
+
* the real effort, not the default `1`.
|
|
90
293
|
*/
|
|
91
294
|
export interface RunMeta {
|
|
92
295
|
attempts: number;
|
|
93
296
|
source: ActSource;
|
|
94
297
|
}
|
|
95
|
-
|
|
298
|
+
export interface ObservabilityContext {
|
|
299
|
+
traceId: string;
|
|
300
|
+
hooks: ObservabilityHooks;
|
|
301
|
+
joinerCounter: number;
|
|
302
|
+
}
|
|
303
|
+
/** User-supplied observability hooks. See `observability.ts` for full shape. */
|
|
304
|
+
export interface ObservabilityHooks {
|
|
305
|
+
onAttempt?: (event: {
|
|
306
|
+
readonly type: 'attempt';
|
|
307
|
+
readonly key: string;
|
|
308
|
+
readonly traceId: string;
|
|
309
|
+
readonly timestamp: number;
|
|
310
|
+
readonly attempt: number;
|
|
311
|
+
readonly durationMs?: number;
|
|
312
|
+
readonly error?: unknown;
|
|
313
|
+
}) => void;
|
|
314
|
+
onRetry?: (event: {
|
|
315
|
+
readonly type: 'retry';
|
|
316
|
+
readonly key: string;
|
|
317
|
+
readonly traceId: string;
|
|
318
|
+
readonly timestamp: number;
|
|
319
|
+
readonly attempt: number;
|
|
320
|
+
readonly delayMs: number;
|
|
321
|
+
readonly error: unknown;
|
|
322
|
+
}) => void;
|
|
323
|
+
onCacheHit?: (event: {
|
|
324
|
+
readonly type: 'cache-hit';
|
|
325
|
+
readonly key: string;
|
|
326
|
+
readonly traceId: string;
|
|
327
|
+
readonly timestamp: number;
|
|
328
|
+
readonly ageMs: number;
|
|
329
|
+
}) => void;
|
|
330
|
+
onCacheMiss?: (event: {
|
|
331
|
+
readonly type: 'cache-miss';
|
|
332
|
+
readonly key: string;
|
|
333
|
+
readonly traceId: string;
|
|
334
|
+
readonly timestamp: number;
|
|
335
|
+
}) => void;
|
|
336
|
+
onDedupeJoin?: (event: {
|
|
337
|
+
readonly type: 'dedupe-join';
|
|
338
|
+
readonly key: string;
|
|
339
|
+
readonly traceId: string;
|
|
340
|
+
readonly timestamp: number;
|
|
341
|
+
readonly joinerPosition: number;
|
|
342
|
+
}) => void;
|
|
343
|
+
onTimeout?: (event: {
|
|
344
|
+
readonly type: 'timeout';
|
|
345
|
+
readonly key: string;
|
|
346
|
+
readonly traceId: string;
|
|
347
|
+
readonly timestamp: number;
|
|
348
|
+
readonly kind: 'per-attempt' | 'total';
|
|
349
|
+
readonly ms: number;
|
|
350
|
+
}) => void;
|
|
351
|
+
onFinalSuccess?: (event: {
|
|
352
|
+
readonly type: 'final-success';
|
|
353
|
+
readonly key: string;
|
|
354
|
+
readonly traceId: string;
|
|
355
|
+
readonly timestamp: number;
|
|
356
|
+
readonly source: ActSource;
|
|
357
|
+
readonly attempts: number;
|
|
358
|
+
readonly durationMs: number;
|
|
359
|
+
}) => void;
|
|
360
|
+
onFinalFailure?: (event: {
|
|
361
|
+
readonly type: 'final-failure';
|
|
362
|
+
readonly key: string;
|
|
363
|
+
readonly traceId: string;
|
|
364
|
+
readonly timestamp: number;
|
|
365
|
+
readonly attempts: number;
|
|
366
|
+
readonly durationMs: number;
|
|
367
|
+
readonly failedBy: 'abort' | 'timeout' | 'total-timeout' | 'retry-exhausted' | 'fn-error' | 'validation';
|
|
368
|
+
readonly error: unknown;
|
|
369
|
+
}) => void;
|
|
370
|
+
}
|
|
371
|
+
/** Everything a policy receives about the current run. */
|
|
96
372
|
export interface PolicyContext {
|
|
97
373
|
key: string;
|
|
98
374
|
store: AnyStateStore;
|
|
99
375
|
meta: RunMeta;
|
|
376
|
+
/**
|
|
377
|
+
* Observability context. Present only when the caller supplied
|
|
378
|
+
* `options.observability` hooks. Policies check `ctx.observability != null`
|
|
379
|
+
* before allocating event objects — no overhead when absent.
|
|
380
|
+
*/
|
|
381
|
+
observability?: ObservabilityContext;
|
|
100
382
|
}
|
|
101
383
|
/**
|
|
102
384
|
* The ONLY shape the executor knows about policies.
|
|
103
385
|
*
|
|
104
|
-
* A policy wraps ActFn<T
|
|
105
|
-
*
|
|
106
|
-
*
|
|
386
|
+
* A policy wraps `ActFn<T>` and returns a new `ActFn<T>`. It may intercept
|
|
387
|
+
* before, after, or instead of the inner call. The executor never imports
|
|
388
|
+
* a concrete policy — only this type.
|
|
107
389
|
*/
|
|
108
390
|
export type PolicyApplier<T> = (fn: ActFn<T>, ctx: PolicyContext) => ActFn<T>;
|
|
109
391
|
import type { SyncStateStore, AsyncStateStore } from '../stores/base.js';
|
|
110
392
|
export type { SyncStateStore, AsyncStateStore };
|
|
111
393
|
/**
|
|
112
|
-
* Public store type.
|
|
113
|
-
* existing contract — every v1.0 consumer typed against StateStore
|
|
114
|
-
* continues to compile without changes.
|
|
394
|
+
* Public store type. Alias for `SyncStateStore` (backwards compat).
|
|
115
395
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
396
|
+
* Kept for backwards compatibility — consumers typed against
|
|
397
|
+
* `StateStore` continues to compile without changes. A future major version
|
|
398
|
+
* may widen this to `SyncStateStore | AsyncStateStore`.
|
|
118
399
|
*/
|
|
119
400
|
export type StateStore = SyncStateStore;
|
|
120
401
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* async-store path is fully documented and example adapters ship.
|
|
124
|
-
*
|
|
125
|
-
* @internal
|
|
402
|
+
* Union of sync and async stores. Used internally by `PolicyContext` and
|
|
403
|
+
* exported for consumers building custom policy chains or store adapters.
|
|
126
404
|
*/
|
|
127
405
|
export type AnyStateStore = SyncStateStore | AsyncStateStore;
|
|
128
406
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAE9D,2CAA2C;AAC3C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEzC,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,EAAE,EAAE,IAAI,CAAA;IACR,KAAK,EAAE,CAAC,CAAA;IACR,MAAM,EAAE,SAAS,CAAA;IACjB;;;;;;;OAOG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,KAAK,CAAA;IACT,KAAK,EAAE,OAAO,CAAA;IACd;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAA;AAIrD,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,QAAQ,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,aAAa,CAAA;IAE3C;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,cAAc,CAAA;IAEnD;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;CAC3D;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAA;CACX;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;IAEhB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,KAAK,CAAC,EAAS,YAAY,CAAA;IAC3B,2DAA2D;IAC3D,OAAO,CAAC,EAAO,cAAc,CAAA;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAQ,OAAO,GAAG,aAAa,CAAA;IACtC,KAAK,CAAC,EAAS,YAAY,CAAA;IAC3B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,cAAc,CAAA;IAE7B;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAQ,WAAW,CAAA;IAE1B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAA;IAElC;;;OAGG;IACH,OAAO,CAAC,EAAO,MAAM,CAAA;IAIrB,oGAAoG;IACpG,cAAc,CAAC,EAAE,qBAAqB,CAAA;IAEtC,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,eAAe,CAAA;IAE1B,8FAA8F;IAC9F,SAAS,CAAC,EAAE,gBAAgB,CAAA;IAE5B,2FAA2F;IAC3F,KAAK,CAAC,EAAE,YAAY,CAAA;IAEpB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAA;IAE7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,YAAY,CAAA;CACrB;AAID,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,aAAa,EAAE,MAAM,CAAA;IACrB,kGAAkG;IAClG,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,oDAAoD;IACpD,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,GAAG,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,EAAE,EAAE,OAAO,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAI,SAAS,CAAA;CACpB;AAID,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IAEf,KAAK,EAAE,kBAAkB,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,gFAAgF;AAChF,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IACvN,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAC9M,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAChK,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAC1I,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAC7K,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAClM,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACpO,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,eAAe,GAAG,iBAAiB,GAAG,UAAU,GAAG,YAAY,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;CAC5U;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAI,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,IAAI,EAAG,OAAO,CAAA;IACd;;;;OAIG;IACH,aAAa,CAAC,EAAE,oBAAoB,CAAA;CACrC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,aAAa,KAAK,KAAK,CAAC,CAAC,CAAC,CAAA;AAI7E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,CAAA;AAE/C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,CAAA;AAEvC;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,eAAe,CAAA"}
|
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.anySignal = anySignal;
|
|
15
|
+
exports.raceAbort = raceAbort;
|
|
16
|
+
exports.sleep = sleep;
|
|
17
|
+
exports.isAbortError = isAbortError;
|
|
18
|
+
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
|
+
*/
|
|
33
|
+
function anySignal(signals) {
|
|
34
|
+
// Filter out undefined / null defensively.
|
|
35
|
+
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.
|
|
39
|
+
return new AbortController().signal;
|
|
40
|
+
}
|
|
41
|
+
if (filtered.length === 1)
|
|
42
|
+
return filtered[0];
|
|
43
|
+
// Native path (Node 20+, modern browsers, Bun).
|
|
44
|
+
const native = AbortSignal.any;
|
|
45
|
+
if (typeof native === 'function') {
|
|
46
|
+
return native.call(AbortSignal, filtered);
|
|
47
|
+
}
|
|
48
|
+
// Polyfill: only used when native is missing. Listener-safe.
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const listeners = [];
|
|
51
|
+
for (const signal of filtered) {
|
|
52
|
+
if (signal.aborted) {
|
|
53
|
+
controller.abort(signal.reason);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
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
|
+
for (const off of listeners)
|
|
61
|
+
off();
|
|
62
|
+
};
|
|
63
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
64
|
+
listeners.push(() => signal.removeEventListener('abort', onAbort));
|
|
65
|
+
}
|
|
66
|
+
return controller.signal;
|
|
67
|
+
}
|
|
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
|
+
function raceAbort(promise, signal) {
|
|
89
|
+
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
|
+
promise.catch(() => { });
|
|
94
|
+
return Promise.reject(signal.reason);
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
let settled = false;
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
if (settled)
|
|
100
|
+
return;
|
|
101
|
+
settled = true;
|
|
102
|
+
signal.removeEventListener('abort', onAbort);
|
|
103
|
+
reject(signal.reason);
|
|
104
|
+
};
|
|
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
|
+
signal.addEventListener('abort', onAbort);
|
|
108
|
+
promise.then((value) => {
|
|
109
|
+
if (settled)
|
|
110
|
+
return;
|
|
111
|
+
settled = true;
|
|
112
|
+
signal.removeEventListener('abort', onAbort);
|
|
113
|
+
resolve(value);
|
|
114
|
+
}, (error) => {
|
|
115
|
+
if (settled)
|
|
116
|
+
return;
|
|
117
|
+
settled = true;
|
|
118
|
+
signal.removeEventListener('abort', onAbort);
|
|
119
|
+
reject(error);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
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) {
|
|
137
|
+
if (ms <= 0) {
|
|
138
|
+
if (signal?.aborted)
|
|
139
|
+
return Promise.reject(signal.reason);
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
}
|
|
142
|
+
if (signal?.aborted)
|
|
143
|
+
return Promise.reject(signal.reason);
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
signal?.removeEventListener('abort', onAbort);
|
|
147
|
+
resolve();
|
|
148
|
+
}, ms);
|
|
149
|
+
// NOTE: do NOT `unref()` this timer. Unlike InMemoryStore's autoCleanup
|
|
150
|
+
// interval (which is a background housekeeping task), `sleep()` IS the
|
|
151
|
+
// operation the caller is awaiting. If we unref'd it, Node could exit
|
|
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.
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
reject(signal.reason);
|
|
158
|
+
};
|
|
159
|
+
signal?.addEventListener('abort', onAbort);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
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
|
+
function isAbortError(err) {
|
|
169
|
+
if (err == null || typeof err !== 'object')
|
|
170
|
+
return false;
|
|
171
|
+
const name = err.name;
|
|
172
|
+
if (name === 'AbortError')
|
|
173
|
+
return true;
|
|
174
|
+
// DOMException with name 'TimeoutError' is what AbortSignal.timeout throws.
|
|
175
|
+
// Distinguish from our own TimeoutError class by checking for DOMException.
|
|
176
|
+
if (name === 'TimeoutError' &&
|
|
177
|
+
err instanceof Error &&
|
|
178
|
+
typeof DOMException !== 'undefined' &&
|
|
179
|
+
err instanceof DOMException) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
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
|
+
function linkSignal(parent, child) {
|
|
205
|
+
if (parent.aborted) {
|
|
206
|
+
child.abort(parent.reason);
|
|
207
|
+
return () => { };
|
|
208
|
+
}
|
|
209
|
+
const onAbort = () => child.abort(parent.reason);
|
|
210
|
+
parent.addEventListener('abort', onAbort);
|
|
211
|
+
return () => parent.removeEventListener('abort', onAbort);
|
|
212
|
+
}
|