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.
Files changed (130) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +262 -232
  3. package/dist/core/act.cjs +505 -39
  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 +504 -40
  7. package/dist/core/act.js.map +1 -1
  8. package/dist/core/executor.cjs +21 -9
  9. package/dist/core/executor.d.ts +31 -6
  10. package/dist/core/executor.d.ts.map +1 -1
  11. package/dist/core/executor.js +21 -9
  12. package/dist/core/executor.js.map +1 -1
  13. package/dist/core/health.cjs +34 -0
  14. package/dist/core/health.d.ts +18 -0
  15. package/dist/core/health.d.ts.map +1 -0
  16. package/dist/core/health.js +28 -0
  17. package/dist/core/health.js.map +1 -0
  18. package/dist/core/shutdown.cjs +60 -0
  19. package/dist/core/shutdown.d.ts +8 -0
  20. package/dist/core/shutdown.d.ts.map +1 -0
  21. package/dist/core/shutdown.js +56 -0
  22. package/dist/core/shutdown.js.map +1 -0
  23. package/dist/core/tenant.cjs +71 -0
  24. package/dist/core/tenant.d.ts +29 -0
  25. package/dist/core/tenant.d.ts.map +1 -0
  26. package/dist/core/tenant.js +68 -0
  27. package/dist/core/tenant.js.map +1 -0
  28. package/dist/errors.cjs +182 -0
  29. package/dist/errors.d.ts +151 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +171 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/index.cjs +56 -6
  34. package/dist/index.d.ts +22 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +24 -4
  37. package/dist/index.js.map +1 -1
  38. package/dist/observability.cjs +45 -0
  39. package/dist/observability.d.ts +145 -0
  40. package/dist/observability.d.ts.map +1 -0
  41. package/dist/observability.js +43 -0
  42. package/dist/observability.js.map +1 -0
  43. package/dist/policies/bulkhead.cjs +97 -0
  44. package/dist/policies/bulkhead.d.ts +3 -0
  45. package/dist/policies/bulkhead.d.ts.map +1 -0
  46. package/dist/policies/bulkhead.js +95 -0
  47. package/dist/policies/bulkhead.js.map +1 -0
  48. package/dist/policies/cache.cjs +223 -22
  49. package/dist/policies/cache.d.ts +42 -7
  50. package/dist/policies/cache.d.ts.map +1 -1
  51. package/dist/policies/cache.js +223 -22
  52. package/dist/policies/cache.js.map +1 -1
  53. package/dist/policies/circuitBreaker.cjs +85 -0
  54. package/dist/policies/circuitBreaker.d.ts +3 -0
  55. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  56. package/dist/policies/circuitBreaker.js +83 -0
  57. package/dist/policies/circuitBreaker.js.map +1 -0
  58. package/dist/policies/dedupe.cjs +98 -21
  59. package/dist/policies/dedupe.d.ts +20 -13
  60. package/dist/policies/dedupe.d.ts.map +1 -1
  61. package/dist/policies/dedupe.js +98 -21
  62. package/dist/policies/dedupe.js.map +1 -1
  63. package/dist/policies/rateLimit.cjs +35 -0
  64. package/dist/policies/rateLimit.d.ts +3 -0
  65. package/dist/policies/rateLimit.d.ts.map +1 -0
  66. package/dist/policies/rateLimit.js +33 -0
  67. package/dist/policies/rateLimit.js.map +1 -0
  68. package/dist/policies/retry.cjs +103 -26
  69. package/dist/policies/retry.d.ts +25 -2
  70. package/dist/policies/retry.d.ts.map +1 -1
  71. package/dist/policies/retry.js +103 -26
  72. package/dist/policies/retry.js.map +1 -1
  73. package/dist/policies/timeout.cjs +78 -36
  74. package/dist/policies/timeout.d.ts +7 -18
  75. package/dist/policies/timeout.d.ts.map +1 -1
  76. package/dist/policies/timeout.js +84 -31
  77. package/dist/policies/timeout.js.map +1 -1
  78. package/dist/state/store.cjs +9 -2
  79. package/dist/state/store.d.ts +9 -0
  80. package/dist/state/store.d.ts.map +1 -1
  81. package/dist/state/store.js +9 -2
  82. package/dist/state/store.js.map +1 -1
  83. package/dist/stores/base.cjs +4 -4
  84. package/dist/stores/base.d.ts +32 -56
  85. package/dist/stores/base.d.ts.map +1 -1
  86. package/dist/stores/base.js +4 -4
  87. package/dist/stores/base.js.map +1 -1
  88. package/dist/stores/memory.cjs +161 -39
  89. package/dist/stores/memory.d.ts +64 -24
  90. package/dist/stores/memory.d.ts.map +1 -1
  91. package/dist/stores/memory.js +160 -39
  92. package/dist/stores/memory.js.map +1 -1
  93. package/dist/types/index.d.ts +315 -37
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/utils/abort.cjs +212 -0
  96. package/dist/utils/abort.d.ts +79 -0
  97. package/dist/utils/abort.d.ts.map +1 -0
  98. package/dist/utils/abort.js +206 -0
  99. package/dist/utils/abort.js.map +1 -0
  100. package/dist/utils/abortPool.cjs +26 -0
  101. package/dist/utils/abortPool.d.ts +4 -0
  102. package/dist/utils/abortPool.d.ts.map +1 -0
  103. package/dist/utils/abortPool.js +22 -0
  104. package/dist/utils/abortPool.js.map +1 -0
  105. package/dist/utils/backoff.cjs +54 -0
  106. package/dist/utils/backoff.d.ts +14 -0
  107. package/dist/utils/backoff.d.ts.map +1 -0
  108. package/dist/utils/backoff.js +52 -0
  109. package/dist/utils/backoff.js.map +1 -0
  110. package/dist/utils/key.cjs +77 -0
  111. package/dist/utils/key.d.ts +29 -0
  112. package/dist/utils/key.d.ts.map +1 -0
  113. package/dist/utils/key.js +75 -0
  114. package/dist/utils/key.js.map +1 -0
  115. package/dist/utils/limits.cjs +40 -0
  116. package/dist/utils/limits.d.ts +39 -0
  117. package/dist/utils/limits.d.ts.map +1 -0
  118. package/dist/utils/limits.js +38 -0
  119. package/dist/utils/limits.js.map +1 -0
  120. package/dist/utils/sanitize.cjs +29 -0
  121. package/dist/utils/sanitize.d.ts +3 -0
  122. package/dist/utils/sanitize.d.ts.map +1 -0
  123. package/dist/utils/sanitize.js +26 -0
  124. package/dist/utils/sanitize.js.map +1 -0
  125. package/dist/utils/validate.cjs +153 -0
  126. package/dist/utils/validate.d.ts +26 -0
  127. package/dist/utils/validate.d.ts.map +1 -0
  128. package/dist/utils/validate.js +141 -0
  129. package/dist/utils/validate.js.map +1 -0
  130. package/package.json +20 -37
@@ -1,51 +1,145 @@
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;
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
- /** Number of attempts made before final failure */
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
- /** Total number of attempts including the first call. Must be >= 1. */
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
- /** Base delay between attempts in ms. 0 = no delay. */
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
- /** Abort after this many ms */
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 ms */
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 timeout per attempt.
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 deadline fires.
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
- /** Everything a policy receives about the current run */
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> 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.
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. v1.1: alias for SyncStateStore to preserve the
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
- * A future major version may widen this to SyncStateStore | AsyncStateStore
117
- * once call sites have been audited. For now: additive, non-breaking.
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
- * 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
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,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,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
+ }