actly 1.1.0 → 1.1.5

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.
Files changed (70) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +284 -52
  3. package/dist/core/act.cjs +183 -35
  4. package/dist/core/act.d.ts +81 -10
  5. package/dist/core/act.d.ts.map +1 -1
  6. package/dist/core/act.js +181 -35
  7. package/dist/core/act.js.map +1 -1
  8. package/dist/core/executor.cjs +19 -9
  9. package/dist/core/executor.d.ts +25 -5
  10. package/dist/core/executor.d.ts.map +1 -1
  11. package/dist/core/executor.js +19 -9
  12. package/dist/core/executor.js.map +1 -1
  13. package/dist/index.cjs +13 -3
  14. package/dist/index.d.ts +7 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/policies/cache.cjs +85 -21
  19. package/dist/policies/cache.d.ts +29 -7
  20. package/dist/policies/cache.d.ts.map +1 -1
  21. package/dist/policies/cache.js +85 -21
  22. package/dist/policies/cache.js.map +1 -1
  23. package/dist/policies/dedupe.cjs +68 -20
  24. package/dist/policies/dedupe.d.ts +38 -13
  25. package/dist/policies/dedupe.d.ts.map +1 -1
  26. package/dist/policies/dedupe.js +68 -20
  27. package/dist/policies/dedupe.js.map +1 -1
  28. package/dist/policies/retry.cjs +65 -25
  29. package/dist/policies/retry.d.ts +25 -2
  30. package/dist/policies/retry.d.ts.map +1 -1
  31. package/dist/policies/retry.js +65 -25
  32. package/dist/policies/retry.js.map +1 -1
  33. package/dist/policies/timeout.cjs +87 -17
  34. package/dist/policies/timeout.d.ts +28 -8
  35. package/dist/policies/timeout.d.ts.map +1 -1
  36. package/dist/policies/timeout.js +87 -17
  37. package/dist/policies/timeout.js.map +1 -1
  38. package/dist/state/store.cjs +9 -2
  39. package/dist/state/store.d.ts +9 -0
  40. package/dist/state/store.d.ts.map +1 -1
  41. package/dist/state/store.js +9 -2
  42. package/dist/state/store.js.map +1 -1
  43. package/dist/stores/base.cjs +4 -4
  44. package/dist/stores/base.d.ts +32 -56
  45. package/dist/stores/base.d.ts.map +1 -1
  46. package/dist/stores/base.js +4 -4
  47. package/dist/stores/base.js.map +1 -1
  48. package/dist/stores/memory.cjs +77 -28
  49. package/dist/stores/memory.d.ts +44 -23
  50. package/dist/stores/memory.d.ts.map +1 -1
  51. package/dist/stores/memory.js +77 -28
  52. package/dist/stores/memory.js.map +1 -1
  53. package/dist/types/index.d.ts +141 -36
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/utils/abort.cjs +142 -0
  56. package/dist/utils/abort.d.ts +55 -0
  57. package/dist/utils/abort.d.ts.map +1 -0
  58. package/dist/utils/abort.js +136 -0
  59. package/dist/utils/abort.js.map +1 -0
  60. package/dist/utils/backoff.cjs +43 -0
  61. package/dist/utils/backoff.d.ts +14 -0
  62. package/dist/utils/backoff.d.ts.map +1 -0
  63. package/dist/utils/backoff.js +41 -0
  64. package/dist/utils/backoff.js.map +1 -0
  65. package/dist/utils/validate.cjs +79 -0
  66. package/dist/utils/validate.d.ts +15 -0
  67. package/dist/utils/validate.d.ts.map +1 -0
  68. package/dist/utils/validate.js +72 -0
  69. package/dist/utils/validate.js.map +1 -0
  70. package/package.json +19 -37
@@ -1,51 +1,131 @@
1
- /** The async function ACT wraps */
2
- export type ActFn<T> = () => Promise<T>;
3
- /** Where a successful result came from */
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
- /** Number of attempts made before success */
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;
11
45
  }
12
46
  export interface ActFailure {
13
47
  ok: false;
14
48
  error: unknown;
15
- /** Number of attempts made before final failure */
49
+ /**
50
+ * Number of attempts made before final failure.
51
+ * For dedupe joiners: mirrors the originator's attempt count.
52
+ */
16
53
  attempts: number;
17
54
  }
18
55
  export type ActResult<T> = ActSuccess<T> | ActFailure;
19
56
  export interface RetryOptions {
20
- /** Total number of attempts including the first call. Must be >= 1. */
57
+ /**
58
+ * Total number of attempts including the first call.
59
+ * Must be an integer >= 1.
60
+ *
61
+ * `attempts: 1` is a no-op (equivalent to omitting `retry`); the policy
62
+ * is not added to the chain. This is intentional — adding a policy that
63
+ * never retries is pure overhead.
64
+ */
21
65
  attempts: number;
22
- /** Base delay between attempts in ms. 0 = no delay. */
66
+ /**
67
+ * Base delay between attempts in milliseconds. Defaults to 0 (no delay).
68
+ * Must be a non-negative finite number.
69
+ */
23
70
  delayMs?: number;
24
71
  /**
25
72
  * How the base delay grows per attempt:
26
- * - 'none' -> always delayMs
27
- * - 'linear' -> delayMs * attempt
28
- * - 'exponential' -> delayMs * 2^(attempt-1)
73
+ * - `'none'` -> always `delayMs`
74
+ * - `'linear'` -> `delayMs * attempt`
75
+ * - `'exponential'` -> `delayMs * 2^(attempt-1)`
76
+ *
77
+ * The computed delay is then capped by {@link maxDelay} and jittered by
78
+ * {@link jitter} before being slept.
79
+ *
80
+ * Defaults to `'none'`.
29
81
  */
30
82
  backoff?: 'none' | 'linear' | 'exponential';
83
+ /**
84
+ * Hard cap on the computed delay. Defaults to `Infinity`.
85
+ *
86
+ * Without a cap, `exponential` backoff with `delayMs: 1000` and
87
+ * `attempts: 10` would sleep 8.5 minutes between attempts 9 and 10
88
+ * (256 seconds). Set `maxDelay` to something sane (e.g. 30_000) to
89
+ * bound worst-case latency.
90
+ */
91
+ maxDelay?: number;
92
+ /**
93
+ * Jitter strategy applied to the (post-`maxDelay`) delay.
94
+ *
95
+ * - `'none'` -> no jitter, return delay as-is
96
+ * - `'full'` -> `random() * delay` (default; best for thundering-herd prevention)
97
+ * - `'equal'` -> `delay/2 + random() * delay/2`
98
+ * - `'decorrelated'` -> `base + random() * (delay - base)`
99
+ *
100
+ * Defaults to `'full'`. Jitter prevents synchronised retry storms when
101
+ * many callers fail at the same instant (e.g. after an upstream outage
102
+ * recovers) — without it, all callers retry on the same tick.
103
+ */
104
+ jitter?: 'none' | 'full' | 'equal' | 'decorrelated';
31
105
  /**
32
106
  * Predicate called after each failure, before the next attempt.
33
- * Return false to stop retrying immediately and surface the error.
107
+ * Return `false` to stop retrying immediately and surface the error.
108
+ *
109
+ * Called for every failure including the last attempt (so observers stay
110
+ * informed); the return value is only consulted when there are remaining
111
+ * attempts.
34
112
  *
35
113
  * Use this to skip retries for errors that are definitively non-recoverable
36
114
  * (e.g. HTTP 4xx, AuthError, ValidationError).
37
115
  *
116
+ * Default behaviour: retry on every error except `AbortError` (which
117
+ * indicates the caller or a timeout cancelled the operation).
118
+ *
38
119
  * @param error The error thrown by the most recent attempt.
39
120
  * @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
121
  */
45
122
  shouldRetry?: (error: unknown, attempt: number) => boolean;
46
123
  }
47
124
  export interface TimeoutOptions {
48
- /** Abort after this many ms */
125
+ /**
126
+ * Abort after this many milliseconds.
127
+ * Must be a positive finite number.
128
+ */
49
129
  ms: number;
50
130
  }
51
131
  export interface DedupeOptions {
@@ -54,45 +134,74 @@ export interface DedupeOptions {
54
134
  * Opt-in: be explicit when you want this behaviour.
55
135
  */
56
136
  enabled: boolean;
137
+ /**
138
+ * Safety-net TTL for the in-flight entry, in milliseconds.
139
+ *
140
+ * If the originator's promise does not settle within this window, the
141
+ * entry is removed from the store so subsequent callers can start fresh.
142
+ * Originator's promise continues in the background until it settles or
143
+ * an outer timeout fires.
144
+ *
145
+ * Default: `Infinity` (no safety net). Pair with `timeout` or
146
+ * `totalTimeout` for proper cancellation in production.
147
+ */
148
+ inflightTtl?: number;
57
149
  }
58
150
  export interface CacheOptions {
59
- /** Keep a successful result for this many ms */
151
+ /** Keep a successful result for this many milliseconds. Must be > 0. */
60
152
  ttl: number;
61
153
  }
62
154
  export interface ActOptions {
63
155
  retry?: RetryOptions;
156
+ /** Per-attempt deadline. Each retry gets a fresh clock. */
64
157
  timeout?: TimeoutOptions;
65
158
  /**
66
159
  * Collapse concurrent calls with the same key into one in-flight Promise.
67
160
  *
68
161
  * Shorthand: `dedupe: true`
69
- * Full form: `dedupe: { enabled: true }`
70
- *
71
- * Both are equivalent. The object form exists for forward compatibility.
162
+ * Full form: `dedupe: { enabled: true, inflightTtl: 30_000 }`
72
163
  */
73
164
  dedupe?: boolean | DedupeOptions;
74
165
  cache?: CacheOptions;
75
166
  /**
76
167
  * Hard budget over the ENTIRE operation — including all retry attempts,
77
- * delays, and the timeout per attempt.
168
+ * delays, and the per-attempt timeout.
78
169
  *
79
170
  * Distinct from `timeout`, which resets the clock on every attempt.
80
171
  * Use both together to express: "each attempt may take at most X ms,
81
172
  * but the whole thing must finish within Y ms."
82
173
  *
83
- * Rejects with TotalTimeoutError if the deadline fires.
174
+ * Rejects with {@link TotalTimeoutError} if the budget fires.
84
175
  */
85
176
  totalTimeout?: TimeoutOptions;
177
+ /**
178
+ * Caller-provided cancellation signal.
179
+ *
180
+ * When this signal aborts:
181
+ * - if the operation has not yet started, it rejects immediately with
182
+ * the signal's `reason`,
183
+ * - if it is in progress, the inner {@link ActFn} receives an aborted
184
+ * signal (cooperative cancellation),
185
+ * - if it has already settled, the result is returned as normal.
186
+ *
187
+ * Combined with `timeout` / `totalTimeout`, this gives you full control
188
+ * over cancellation from outside `act()`.
189
+ */
190
+ signal?: AbortSignal;
86
191
  }
87
192
  /**
88
193
  * Mutable bag mutated in-place during execution.
89
194
  * Policies annotate it; act() reads the final state to build ActResult.
195
+ *
196
+ * For dedupe joiners: the bag is copied from the originator's bag after the
197
+ * in-flight promise settles (success or failure), so `attempts` reflects
198
+ * the real effort, not the default `1`.
90
199
  */
91
200
  export interface RunMeta {
92
201
  attempts: number;
93
202
  source: ActSource;
94
203
  }
95
- /** Everything a policy receives about the current run */
204
+ /** Everything a policy receives about the current run. */
96
205
  export interface PolicyContext {
97
206
  key: string;
98
207
  store: AnyStateStore;
@@ -101,28 +210,24 @@ export interface PolicyContext {
101
210
  /**
102
211
  * The ONLY shape the executor knows about policies.
103
212
  *
104
- * A policy wraps ActFn<T> and returns a new ActFn<T>.
105
- * It may intercept before, after, or instead of the inner call.
106
- * The executor never imports a concrete policy — only this type.
213
+ * A policy wraps `ActFn<T>` and returns a new `ActFn<T>`. It may intercept
214
+ * before, after, or instead of the inner call. The executor never imports
215
+ * a concrete policy — only this type.
107
216
  */
108
217
  export type PolicyApplier<T> = (fn: ActFn<T>, ctx: PolicyContext) => ActFn<T>;
109
218
  import type { SyncStateStore, AsyncStateStore } from '../stores/base.js';
110
219
  export type { SyncStateStore, AsyncStateStore };
111
220
  /**
112
- * Public store type. v1.1: alias for SyncStateStore to preserve the
113
- * existing contract — every v1.0 consumer typed against StateStore
114
- * continues to compile without changes.
221
+ * Public store type. v1.1+: alias for `SyncStateStore`.
115
222
  *
116
- * A future major version may widen this to SyncStateStore | AsyncStateStore
117
- * once call sites have been audited. For now: additive, non-breaking.
223
+ * Kept for backwards compatibility every v1.0 consumer typed against
224
+ * `StateStore` continues to compile without changes. A future major version
225
+ * may widen this to `SyncStateStore | AsyncStateStore`.
118
226
  */
119
227
  export type StateStore = SyncStateStore;
120
228
  /**
121
- * Accepted by policies that support both sync and async stores (e.g. cache).
122
- * Not part of the public surface yet internal use only until the
123
- * async-store path is fully documented and example adapters ship.
124
- *
125
- * @internal
229
+ * Union of sync and async stores. Used internally by `PolicyContext` and
230
+ * exported for consumers building custom policy chains or store adapters.
126
231
  */
127
232
  export type AnyStateStore = SyncStateStore | AsyncStateStore;
128
233
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,mCAAmC;AACnC,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CAAA;AAEvC,0CAA0C;AAC1C,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,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,KAAK,CAAA;IACT,KAAK,EAAE,OAAO,CAAA;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAA;AAIrD,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,aAAa,CAAA;IAC3C;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;CAC3D;AAED,MAAM,WAAW,cAAc;IAC7B,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAA;CACX;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAS,YAAY,CAAA;IAC3B,OAAO,CAAC,EAAO,cAAc,CAAA;IAC7B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAQ,OAAO,GAAG,aAAa,CAAA;IACtC,KAAK,CAAC,EAAS,YAAY,CAAA;IAC3B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,cAAc,CAAA;CAC9B;AAID;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAI,SAAS,CAAA;CACpB;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAI,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,IAAI,EAAG,OAAO,CAAA;CACf;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;AAO7E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,CAAA;AAE/C;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,CAAA;AAEvC;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // ─── AbortSignal helpers ──────────────────────────────────────────────────────
3
+ //
4
+ // Centralised utilities for composing AbortSignals. These exist because
5
+ // Node 18 lacks `AbortSignal.any` (added in Node 20) and we want to keep
6
+ // the `engines` floor at 18 for backwards compatibility with existing
7
+ // consumers.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.anySignal = anySignal;
10
+ exports.raceAbort = raceAbort;
11
+ exports.sleep = sleep;
12
+ exports.isAbortError = isAbortError;
13
+ exports.linkSignal = linkSignal;
14
+ /**
15
+ * Polyfill for `AbortSignal.any(signals)` (Node 20+).
16
+ *
17
+ * Returns a single signal that aborts when ANY of the input signals aborts,
18
+ * with the same reason. If any input is already aborted, the returned signal
19
+ * is aborted synchronously.
20
+ *
21
+ * Listener registration is `{ once: true }` — once any signal fires, we stop
22
+ * listening on the others. The composite signal cannot be "un-aborted".
23
+ */
24
+ function anySignal(signals) {
25
+ // Fast path: native implementation (Node 20+, modern browsers, Bun).
26
+ // The cast is safe — the runtime check guards the call.
27
+ const native = AbortSignal.any;
28
+ if (typeof native === 'function')
29
+ return native.call(AbortSignal, signals);
30
+ // Polyfill for Node 18.
31
+ const controller = new AbortController();
32
+ for (const signal of signals) {
33
+ if (signal.aborted) {
34
+ controller.abort(signal.reason);
35
+ break;
36
+ }
37
+ signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
38
+ }
39
+ return controller.signal;
40
+ }
41
+ /**
42
+ * Race a promise against an AbortSignal.
43
+ *
44
+ * - If the signal is already aborted, rejects immediately with `signal.reason`.
45
+ * - If the signal aborts while the promise is pending, rejects with `signal.reason`.
46
+ * - If the promise settles first, returns its value (or rejects with its error).
47
+ *
48
+ * The listener is registered with `{ once: true }` and never leaks: either
49
+ * the signal fires (listener auto-removed) or the promise settles (the
50
+ * signal will eventually be GC'd along with the listener).
51
+ *
52
+ * Used by `dedupePolicy` so joiners can cancel their own `await` even if the
53
+ * originator's `fn` is still running.
54
+ */
55
+ function raceAbort(promise, signal) {
56
+ if (signal.aborted)
57
+ return Promise.reject(signal.reason);
58
+ return new Promise((resolve, reject) => {
59
+ let settled = false;
60
+ const onAbort = () => {
61
+ if (settled)
62
+ return;
63
+ settled = true;
64
+ reject(signal.reason);
65
+ };
66
+ signal.addEventListener('abort', onAbort, { once: true });
67
+ promise.then((value) => {
68
+ if (settled)
69
+ return;
70
+ settled = true;
71
+ resolve(value);
72
+ }, (error) => {
73
+ if (settled)
74
+ return;
75
+ settled = true;
76
+ reject(error);
77
+ });
78
+ });
79
+ }
80
+ /**
81
+ * Sleep for `ms` milliseconds, but abort early if `signal` fires.
82
+ *
83
+ * Resolves normally on timer expiry. Rejects with `signal.reason` if the
84
+ * signal aborts before the timer fires. If the signal is already aborted
85
+ * when called, rejects synchronously (in microtask).
86
+ *
87
+ * Used by `retryPolicy` to make backoff delays interruptible: when an outer
88
+ * `totalTimeout` fires mid-delay, the delay rejects immediately instead of
89
+ * blocking the retry loop until the timer would have elapsed.
90
+ */
91
+ function sleep(ms, signal) {
92
+ if (ms <= 0) {
93
+ if (signal?.aborted)
94
+ return Promise.reject(signal.reason);
95
+ return Promise.resolve();
96
+ }
97
+ if (signal?.aborted)
98
+ return Promise.reject(signal.reason);
99
+ return new Promise((resolve, reject) => {
100
+ const timer = setTimeout(() => {
101
+ signal?.removeEventListener('abort', onAbort);
102
+ resolve();
103
+ }, ms);
104
+ const onAbort = () => {
105
+ clearTimeout(timer);
106
+ reject(signal.reason);
107
+ };
108
+ signal?.addEventListener('abort', onAbort, { once: true });
109
+ });
110
+ }
111
+ /**
112
+ * True if `err` is an `AbortError` (DOMException name or Error name).
113
+ *
114
+ * The default `shouldRetry` predicate uses this to skip retrying on
115
+ * cancellations — if the caller aborted, retrying would just abort again.
116
+ */
117
+ function isAbortError(err) {
118
+ if (err == null || typeof err !== 'object')
119
+ return false;
120
+ const name = err.name;
121
+ return name === 'AbortError' || name === 'TimeoutError' &&
122
+ err instanceof Error &&
123
+ // DOMException with name 'TimeoutError' is what AbortSignal.timeout throws.
124
+ // Distinguish from our own TimeoutError class by checking for DOMException.
125
+ typeof DOMException !== 'undefined' &&
126
+ err instanceof DOMException;
127
+ }
128
+ /**
129
+ * Link a parent signal to a child controller: when the parent aborts, the
130
+ * child is aborted with the same reason. No-op if the parent is already
131
+ * aborted (the caller should check `parent.aborted` separately if it cares
132
+ * about synchronous abort).
133
+ *
134
+ * The listener is `{ once: true }` — no leak.
135
+ */
136
+ function linkSignal(parent, child) {
137
+ if (parent.aborted) {
138
+ child.abort(parent.reason);
139
+ return;
140
+ }
141
+ parent.addEventListener('abort', () => child.abort(parent.reason), { once: true });
142
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Polyfill for `AbortSignal.any(signals)` (Node 20+).
3
+ *
4
+ * Returns a single signal that aborts when ANY of the input signals aborts,
5
+ * with the same reason. If any input is already aborted, the returned signal
6
+ * is aborted synchronously.
7
+ *
8
+ * Listener registration is `{ once: true }` — once any signal fires, we stop
9
+ * listening on the others. The composite signal cannot be "un-aborted".
10
+ */
11
+ export declare function anySignal(signals: ReadonlyArray<AbortSignal>): AbortSignal;
12
+ /**
13
+ * Race a promise against an AbortSignal.
14
+ *
15
+ * - If the signal is already aborted, rejects immediately with `signal.reason`.
16
+ * - If the signal aborts while the promise is pending, rejects with `signal.reason`.
17
+ * - If the promise settles first, returns its value (or rejects with its error).
18
+ *
19
+ * The listener is registered with `{ once: true }` and never leaks: either
20
+ * the signal fires (listener auto-removed) or the promise settles (the
21
+ * signal will eventually be GC'd along with the listener).
22
+ *
23
+ * Used by `dedupePolicy` so joiners can cancel their own `await` even if the
24
+ * originator's `fn` is still running.
25
+ */
26
+ export declare function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
27
+ /**
28
+ * Sleep for `ms` milliseconds, but abort early if `signal` fires.
29
+ *
30
+ * Resolves normally on timer expiry. Rejects with `signal.reason` if the
31
+ * signal aborts before the timer fires. If the signal is already aborted
32
+ * when called, rejects synchronously (in microtask).
33
+ *
34
+ * Used by `retryPolicy` to make backoff delays interruptible: when an outer
35
+ * `totalTimeout` fires mid-delay, the delay rejects immediately instead of
36
+ * blocking the retry loop until the timer would have elapsed.
37
+ */
38
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
39
+ /**
40
+ * True if `err` is an `AbortError` (DOMException name or Error name).
41
+ *
42
+ * The default `shouldRetry` predicate uses this to skip retrying on
43
+ * cancellations — if the caller aborted, retrying would just abort again.
44
+ */
45
+ export declare function isAbortError(err: unknown): boolean;
46
+ /**
47
+ * Link a parent signal to a child controller: when the parent aborts, the
48
+ * child is aborted with the same reason. No-op if the parent is already
49
+ * aborted (the caller should check `parent.aborted` separately if it cares
50
+ * about synchronous abort).
51
+ *
52
+ * The listener is `{ once: true }` — no leak.
53
+ */
54
+ export declare function linkSignal(parent: AbortSignal, child: AbortController): void;
55
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../../src/utils/abort.ts"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,WAAW,CAwB1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACzB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,CAAC,CAAC,CA2BZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBrE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CASlD;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,eAAe,GACrB,IAAI,CAUN"}
@@ -0,0 +1,136 @@
1
+ // ─── AbortSignal helpers ──────────────────────────────────────────────────────
2
+ //
3
+ // Centralised utilities for composing AbortSignals. These exist because
4
+ // Node 18 lacks `AbortSignal.any` (added in Node 20) and we want to keep
5
+ // the `engines` floor at 18 for backwards compatibility with existing
6
+ // consumers.
7
+ /**
8
+ * Polyfill for `AbortSignal.any(signals)` (Node 20+).
9
+ *
10
+ * Returns a single signal that aborts when ANY of the input signals aborts,
11
+ * with the same reason. If any input is already aborted, the returned signal
12
+ * is aborted synchronously.
13
+ *
14
+ * Listener registration is `{ once: true }` — once any signal fires, we stop
15
+ * listening on the others. The composite signal cannot be "un-aborted".
16
+ */
17
+ export function anySignal(signals) {
18
+ // Fast path: native implementation (Node 20+, modern browsers, Bun).
19
+ // The cast is safe — the runtime check guards the call.
20
+ const native = AbortSignal.any;
21
+ if (typeof native === 'function')
22
+ return native.call(AbortSignal, signals);
23
+ // Polyfill for Node 18.
24
+ const controller = new AbortController();
25
+ for (const signal of signals) {
26
+ if (signal.aborted) {
27
+ controller.abort(signal.reason);
28
+ break;
29
+ }
30
+ signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
31
+ }
32
+ return controller.signal;
33
+ }
34
+ /**
35
+ * Race a promise against an AbortSignal.
36
+ *
37
+ * - If the signal is already aborted, rejects immediately with `signal.reason`.
38
+ * - If the signal aborts while the promise is pending, rejects with `signal.reason`.
39
+ * - If the promise settles first, returns its value (or rejects with its error).
40
+ *
41
+ * The listener is registered with `{ once: true }` and never leaks: either
42
+ * the signal fires (listener auto-removed) or the promise settles (the
43
+ * signal will eventually be GC'd along with the listener).
44
+ *
45
+ * Used by `dedupePolicy` so joiners can cancel their own `await` even if the
46
+ * originator's `fn` is still running.
47
+ */
48
+ export function raceAbort(promise, signal) {
49
+ if (signal.aborted)
50
+ return Promise.reject(signal.reason);
51
+ return new Promise((resolve, reject) => {
52
+ let settled = false;
53
+ const onAbort = () => {
54
+ if (settled)
55
+ return;
56
+ settled = true;
57
+ reject(signal.reason);
58
+ };
59
+ signal.addEventListener('abort', onAbort, { once: true });
60
+ promise.then((value) => {
61
+ if (settled)
62
+ return;
63
+ settled = true;
64
+ resolve(value);
65
+ }, (error) => {
66
+ if (settled)
67
+ return;
68
+ settled = true;
69
+ reject(error);
70
+ });
71
+ });
72
+ }
73
+ /**
74
+ * Sleep for `ms` milliseconds, but abort early if `signal` fires.
75
+ *
76
+ * Resolves normally on timer expiry. Rejects with `signal.reason` if the
77
+ * signal aborts before the timer fires. If the signal is already aborted
78
+ * when called, rejects synchronously (in microtask).
79
+ *
80
+ * Used by `retryPolicy` to make backoff delays interruptible: when an outer
81
+ * `totalTimeout` fires mid-delay, the delay rejects immediately instead of
82
+ * blocking the retry loop until the timer would have elapsed.
83
+ */
84
+ export function sleep(ms, signal) {
85
+ if (ms <= 0) {
86
+ if (signal?.aborted)
87
+ return Promise.reject(signal.reason);
88
+ return Promise.resolve();
89
+ }
90
+ if (signal?.aborted)
91
+ return Promise.reject(signal.reason);
92
+ return new Promise((resolve, reject) => {
93
+ const timer = setTimeout(() => {
94
+ signal?.removeEventListener('abort', onAbort);
95
+ resolve();
96
+ }, ms);
97
+ const onAbort = () => {
98
+ clearTimeout(timer);
99
+ reject(signal.reason);
100
+ };
101
+ signal?.addEventListener('abort', onAbort, { once: true });
102
+ });
103
+ }
104
+ /**
105
+ * True if `err` is an `AbortError` (DOMException name or Error name).
106
+ *
107
+ * The default `shouldRetry` predicate uses this to skip retrying on
108
+ * cancellations — if the caller aborted, retrying would just abort again.
109
+ */
110
+ export function isAbortError(err) {
111
+ if (err == null || typeof err !== 'object')
112
+ return false;
113
+ const name = err.name;
114
+ return name === 'AbortError' || name === 'TimeoutError' &&
115
+ err instanceof Error &&
116
+ // DOMException with name 'TimeoutError' is what AbortSignal.timeout throws.
117
+ // Distinguish from our own TimeoutError class by checking for DOMException.
118
+ typeof DOMException !== 'undefined' &&
119
+ err instanceof DOMException;
120
+ }
121
+ /**
122
+ * Link a parent signal to a child controller: when the parent aborts, the
123
+ * child is aborted with the same reason. No-op if the parent is already
124
+ * aborted (the caller should check `parent.aborted` separately if it cares
125
+ * about synchronous abort).
126
+ *
127
+ * The listener is `{ once: true }` — no leak.
128
+ */
129
+ export function linkSignal(parent, child) {
130
+ if (parent.aborted) {
131
+ child.abort(parent.reason);
132
+ return;
133
+ }
134
+ parent.addEventListener('abort', () => child.abort(parent.reason), { once: true });
135
+ }
136
+ //# sourceMappingURL=abort.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.js","sourceRoot":"","sources":["../../src/utils/abort.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ function computeDelay(attempt, opts) {
16
+ const base = opts.delayMs ?? 0;
17
+ if (base === 0)
18
+ return 0;
19
+ // Step 1: backoff
20
+ let delay;
21
+ switch (opts.backoff ?? 'none') {
22
+ case 'linear':
23
+ delay = base * attempt;
24
+ break;
25
+ case 'exponential':
26
+ delay = base * 2 ** (attempt - 1);
27
+ break;
28
+ default: delay = base;
29
+ }
30
+ // Step 2: cap (guard against Infinity and NaN before Math.min)
31
+ const max = opts.maxDelay ?? Number.POSITIVE_INFINITY;
32
+ if (!Number.isFinite(delay))
33
+ delay = max;
34
+ delay = Math.min(delay, max);
35
+ // Step 3: jitter
36
+ switch (opts.jitter ?? 'full') {
37
+ case 'none': return delay;
38
+ case 'full': return Math.random() * delay;
39
+ case 'equal': return delay / 2 + Math.random() * delay / 2;
40
+ case 'decorrelated': return base + Math.random() * (delay - base);
41
+ default: return delay;
42
+ }
43
+ }