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,47 +1,124 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.retryPolicy = retryPolicy;
4
- // ─── Helpers ─────────────────────────────────────────────────────────────────
5
- function computeDelay(attempt, opts) {
6
- const base = opts.delayMs ?? 0;
7
- if (base === 0)
8
- return 0;
9
- switch (opts.backoff ?? 'none') {
10
- case 'linear': return base * attempt;
11
- case 'exponential': return base * 2 ** (attempt - 1);
12
- default: return base;
13
- }
4
+ const backoff_js_1 = require("../utils/backoff.js");
5
+ const abort_js_1 = require("../utils/abort.js");
6
+ const errors_js_1 = require("../errors.js");
7
+ // ─── Default predicate ────────────────────────────────────────────────────────
8
+ /**
9
+ * Default `shouldRetry`: retry on any error EXCEPT abort errors.
10
+ *
11
+ * Abort errors indicate the caller or a timeout cancelled the operation —
12
+ * retrying would just abort again on the next attempt, wasting delay budget.
13
+ *
14
+ * User-supplied `shouldRetry` overrides this entirely.
15
+ */
16
+ function defaultShouldRetry(error, _attempt) {
17
+ return !(0, abort_js_1.isAbortError)(error);
14
18
  }
15
- const sleep = (ms) => new Promise(r => setTimeout(r, ms));
16
- // ─── Policy ──────────────────────────────────────────────────────────────────
19
+ // ─── Policy ───────────────────────────────────────────────────────────────────
17
20
  /**
18
- * Retries fn up to opts.attempts times on any thrown error.
19
- * Writes the live attempt count into ctx.meta.attempts.
21
+ * Retry `fn` up to `opts.attempts` times on retryable errors.
22
+ *
23
+ * Writes the live attempt count into `ctx.meta.attempts` so `act()` can
24
+ * report it in the final `ActResult`.
25
+ *
26
+ * # Signal awareness
27
+ *
28
+ * - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
29
+ * `totalTimeout` or caller) has aborted, throws the parent's reason
30
+ * immediately — no more attempts.
31
+ * - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
32
+ * aborts mid-delay, the sleep rejects immediately instead of blocking
33
+ * the loop until the timer would have elapsed.
34
+ *
35
+ * # `shouldRetry` invocation
36
+ *
37
+ * Called after EVERY failure, including the last attempt. This preserves the
38
+ * predicate's contract for observers / metrics that rely on it being called
39
+ * per-attempt. The return value is only consulted when `attempt < max`.
40
+ *
41
+ * # Default predicate
42
+ *
43
+ * If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
44
+ * every error EXCEPT abort errors (so timeouts and caller cancellations
45
+ * don't waste retry budget).
20
46
  */
21
47
  function retryPolicy(opts) {
22
- const max = Math.max(1, opts.attempts);
23
- return (fn, ctx) => async () => {
48
+ const max = Math.max(1, Math.floor(opts.attempts));
49
+ const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
50
+ return (fn, ctx) => async (parentSignal) => {
51
+ const errors = [];
24
52
  let lastErr;
53
+ let retriedAtLeastOnce = false;
54
+ const obs = ctx.observability;
25
55
  for (let attempt = 1; attempt <= max; attempt++) {
56
+ // Parent (totalTimeout or caller signal) already aborted — bail.
57
+ if (parentSignal.aborted)
58
+ throw parentSignal.reason;
26
59
  ctx.meta.attempts = attempt;
60
+ const attemptStart = Date.now();
27
61
  try {
28
- return await fn();
62
+ // Emit onAttempt before each attempt. (We could emit
63
+ // after with duration/error, but before lets observers track
64
+ // in-flight attempts in real time.)
65
+ if (obs) {
66
+ obs.hooks.onAttempt?.({
67
+ type: 'attempt', key: ctx.key, traceId: obs.traceId,
68
+ timestamp: attemptStart, attempt,
69
+ });
70
+ }
71
+ return await fn(parentSignal);
29
72
  }
30
73
  catch (err) {
31
74
  lastErr = err;
32
- if (attempt < max) {
33
- // Non-retryable error bail immediately without consuming
34
- // remaining attempts. The error surfaces exactly as-is.
35
- if (opts.shouldRetry && !opts.shouldRetry(err, attempt)) {
36
- throw err;
75
+ errors.push(err);
76
+ // Invoke shouldRetry. If the predicate throws, treat as "don't retry"
77
+ // and surface the original fn error (not the predicate error).
78
+ let retryable;
79
+ try {
80
+ retryable = shouldRetry(err, attempt);
81
+ }
82
+ catch {
83
+ // Predicate threw — don't retry, surface original error
84
+ throw err;
85
+ }
86
+ if (attempt >= max) {
87
+ // Last attempt. If we retried at least once (predicate
88
+ // allowed retries), wrap in RetryExhaustedError for context.
89
+ if (retriedAtLeastOnce) {
90
+ throw new errors_js_1.RetryExhaustedError({
91
+ key: ctx.key,
92
+ attempts: attempt,
93
+ lastError: err,
94
+ errors,
95
+ });
37
96
  }
38
- const delay = computeDelay(attempt, opts);
39
- if (delay > 0)
40
- await sleep(delay);
97
+ throw err;
98
+ }
99
+ if (!retryable) {
100
+ // Non-retryable error — bail immediately.
101
+ throw err;
102
+ }
103
+ // Mark that we retried at least once — affects final wrap.
104
+ retriedAtLeastOnce = true;
105
+ // Parent aborted mid-attempt — don't sleep, bail.
106
+ if (parentSignal.aborted)
107
+ throw parentSignal.reason;
108
+ const delay = (0, backoff_js_1.computeDelay)(attempt, opts);
109
+ // Emit onRetry before the delay sleep.
110
+ if (obs) {
111
+ obs.hooks.onRetry?.({
112
+ type: 'retry', key: ctx.key, traceId: obs.traceId,
113
+ timestamp: Date.now(), attempt, delayMs: delay, error: err,
114
+ });
115
+ }
116
+ if (delay > 0) {
117
+ await (0, abort_js_1.sleep)(delay, parentSignal);
41
118
  }
42
119
  }
43
120
  }
44
- // Every attempt failed surface the last error upstream
121
+ // Unreachable: the loop either returns or throws on every iteration.
45
122
  throw lastErr;
46
123
  };
47
124
  }
@@ -1,7 +1,30 @@
1
1
  import type { PolicyApplier, RetryOptions } from '../types/index.js';
2
2
  /**
3
- * Retries fn up to opts.attempts times on any thrown error.
4
- * Writes the live attempt count into ctx.meta.attempts.
3
+ * Retry `fn` up to `opts.attempts` times on retryable errors.
4
+ *
5
+ * Writes the live attempt count into `ctx.meta.attempts` so `act()` can
6
+ * report it in the final `ActResult`.
7
+ *
8
+ * # Signal awareness
9
+ *
10
+ * - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
11
+ * `totalTimeout` or caller) has aborted, throws the parent's reason
12
+ * immediately — no more attempts.
13
+ * - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
14
+ * aborts mid-delay, the sleep rejects immediately instead of blocking
15
+ * the loop until the timer would have elapsed.
16
+ *
17
+ * # `shouldRetry` invocation
18
+ *
19
+ * Called after EVERY failure, including the last attempt. This preserves the
20
+ * predicate's contract for observers / metrics that rely on it being called
21
+ * per-attempt. The return value is only consulted when `attempt < max`.
22
+ *
23
+ * # Default predicate
24
+ *
25
+ * If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
26
+ * every error EXCEPT abort errors (so timeouts and caller cancellations
27
+ * don't waste retry budget).
5
28
  */
6
29
  export declare function retryPolicy<T>(opts: RetryOptions): PolicyApplier<T>;
7
30
  //# sourceMappingURL=retry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAmB1F;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CA8BnE"}
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAqB1F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAuFnE"}
@@ -1,44 +1,121 @@
1
- // ─── Helpers ─────────────────────────────────────────────────────────────────
2
- function computeDelay(attempt, opts) {
3
- const base = opts.delayMs ?? 0;
4
- if (base === 0)
5
- return 0;
6
- switch (opts.backoff ?? 'none') {
7
- case 'linear': return base * attempt;
8
- case 'exponential': return base * 2 ** (attempt - 1);
9
- default: return base;
10
- }
1
+ import { computeDelay } from '../utils/backoff.js';
2
+ import { isAbortError, sleep } from '../utils/abort.js';
3
+ import { RetryExhaustedError } from '../errors.js';
4
+ // ─── Default predicate ────────────────────────────────────────────────────────
5
+ /**
6
+ * Default `shouldRetry`: retry on any error EXCEPT abort errors.
7
+ *
8
+ * Abort errors indicate the caller or a timeout cancelled the operation —
9
+ * retrying would just abort again on the next attempt, wasting delay budget.
10
+ *
11
+ * User-supplied `shouldRetry` overrides this entirely.
12
+ */
13
+ function defaultShouldRetry(error, _attempt) {
14
+ return !isAbortError(error);
11
15
  }
12
- const sleep = (ms) => new Promise(r => setTimeout(r, ms));
13
- // ─── Policy ──────────────────────────────────────────────────────────────────
16
+ // ─── Policy ───────────────────────────────────────────────────────────────────
14
17
  /**
15
- * Retries fn up to opts.attempts times on any thrown error.
16
- * Writes the live attempt count into ctx.meta.attempts.
18
+ * Retry `fn` up to `opts.attempts` times on retryable errors.
19
+ *
20
+ * Writes the live attempt count into `ctx.meta.attempts` so `act()` can
21
+ * report it in the final `ActResult`.
22
+ *
23
+ * # Signal awareness
24
+ *
25
+ * - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
26
+ * `totalTimeout` or caller) has aborted, throws the parent's reason
27
+ * immediately — no more attempts.
28
+ * - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
29
+ * aborts mid-delay, the sleep rejects immediately instead of blocking
30
+ * the loop until the timer would have elapsed.
31
+ *
32
+ * # `shouldRetry` invocation
33
+ *
34
+ * Called after EVERY failure, including the last attempt. This preserves the
35
+ * predicate's contract for observers / metrics that rely on it being called
36
+ * per-attempt. The return value is only consulted when `attempt < max`.
37
+ *
38
+ * # Default predicate
39
+ *
40
+ * If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
41
+ * every error EXCEPT abort errors (so timeouts and caller cancellations
42
+ * don't waste retry budget).
17
43
  */
18
44
  export function retryPolicy(opts) {
19
- const max = Math.max(1, opts.attempts);
20
- return (fn, ctx) => async () => {
45
+ const max = Math.max(1, Math.floor(opts.attempts));
46
+ const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
47
+ return (fn, ctx) => async (parentSignal) => {
48
+ const errors = [];
21
49
  let lastErr;
50
+ let retriedAtLeastOnce = false;
51
+ const obs = ctx.observability;
22
52
  for (let attempt = 1; attempt <= max; attempt++) {
53
+ // Parent (totalTimeout or caller signal) already aborted — bail.
54
+ if (parentSignal.aborted)
55
+ throw parentSignal.reason;
23
56
  ctx.meta.attempts = attempt;
57
+ const attemptStart = Date.now();
24
58
  try {
25
- return await fn();
59
+ // Emit onAttempt before each attempt. (We could emit
60
+ // after with duration/error, but before lets observers track
61
+ // in-flight attempts in real time.)
62
+ if (obs) {
63
+ obs.hooks.onAttempt?.({
64
+ type: 'attempt', key: ctx.key, traceId: obs.traceId,
65
+ timestamp: attemptStart, attempt,
66
+ });
67
+ }
68
+ return await fn(parentSignal);
26
69
  }
27
70
  catch (err) {
28
71
  lastErr = err;
29
- if (attempt < max) {
30
- // Non-retryable error bail immediately without consuming
31
- // remaining attempts. The error surfaces exactly as-is.
32
- if (opts.shouldRetry && !opts.shouldRetry(err, attempt)) {
33
- throw err;
72
+ errors.push(err);
73
+ // Invoke shouldRetry. If the predicate throws, treat as "don't retry"
74
+ // and surface the original fn error (not the predicate error).
75
+ let retryable;
76
+ try {
77
+ retryable = shouldRetry(err, attempt);
78
+ }
79
+ catch {
80
+ // Predicate threw — don't retry, surface original error
81
+ throw err;
82
+ }
83
+ if (attempt >= max) {
84
+ // Last attempt. If we retried at least once (predicate
85
+ // allowed retries), wrap in RetryExhaustedError for context.
86
+ if (retriedAtLeastOnce) {
87
+ throw new RetryExhaustedError({
88
+ key: ctx.key,
89
+ attempts: attempt,
90
+ lastError: err,
91
+ errors,
92
+ });
34
93
  }
35
- const delay = computeDelay(attempt, opts);
36
- if (delay > 0)
37
- await sleep(delay);
94
+ throw err;
95
+ }
96
+ if (!retryable) {
97
+ // Non-retryable error — bail immediately.
98
+ throw err;
99
+ }
100
+ // Mark that we retried at least once — affects final wrap.
101
+ retriedAtLeastOnce = true;
102
+ // Parent aborted mid-attempt — don't sleep, bail.
103
+ if (parentSignal.aborted)
104
+ throw parentSignal.reason;
105
+ const delay = computeDelay(attempt, opts);
106
+ // Emit onRetry before the delay sleep.
107
+ if (obs) {
108
+ obs.hooks.onRetry?.({
109
+ type: 'retry', key: ctx.key, traceId: obs.traceId,
110
+ timestamp: Date.now(), attempt, delayMs: delay, error: err,
111
+ });
112
+ }
113
+ if (delay > 0) {
114
+ await sleep(delay, parentSignal);
38
115
  }
39
116
  }
40
117
  }
41
- // Every attempt failed surface the last error upstream
118
+ // Unreachable: the loop either returns or throws on every iteration.
42
119
  throw lastErr;
43
120
  };
44
121
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAEhF,SAAS,YAAY,CAAC,OAAe,EAAE,IAAkB;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAExB,QAAQ,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,CAAC;QAC/B,KAAK,QAAQ,CAAC,CAAM,OAAO,IAAI,GAAG,OAAO,CAAA;QACzC,KAAK,aAAa,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;QACpD,OAAO,CAAC,CAAY,OAAO,IAAI,CAAA;IACjC,CAAC;AACH,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AAEvE,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IAEtC,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,IAAI,OAAgB,CAAA;QAEpB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;YAChD,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;YAC3B,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAA;YACnB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,GAAG,CAAA;gBAEb,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;oBAClB,2DAA2D;oBAC3D,wDAAwD;oBACxD,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;wBACxD,MAAM,GAAG,CAAA;oBACX,CAAC;oBAED,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;oBACzC,IAAI,KAAK,GAAG,CAAC;wBAAE,MAAM,KAAK,CAAC,KAAK,CAAC,CAAA;gBACnC,CAAC;YACH,CAAC;QACH,CAAC;QAED,yDAAyD;QACzD,MAAM,OAAO,CAAA;IACf,CAAC,CAAA;AACL,CAAC"}
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"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"}
@@ -3,55 +3,97 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TotalTimeoutError = exports.TimeoutError = void 0;
4
4
  exports.timeoutPolicy = timeoutPolicy;
5
5
  exports.totalTimeoutPolicy = totalTimeoutPolicy;
6
- // ─── Errors ───────────────────────────────────────────────────────────────────
7
- class TimeoutError extends Error {
8
- constructor(ms) {
9
- super(`ACT timed out after ${ms}ms`);
10
- this.name = 'TimeoutError';
11
- this.ms = ms;
12
- }
13
- }
14
- exports.TimeoutError = TimeoutError;
6
+ const abort_js_1 = require("../utils/abort.js");
7
+ const errors_js_1 = require("../errors.js");
8
+ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
9
+ Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return errors_js_1.TotalTimeoutError; } });
10
+ // ─── Policy ───────────────────────────────────────────────────────────────────
15
11
  /**
16
- * Thrown when the total budget across all attempts is exceeded.
17
- * Distinct from TimeoutError (per-attempt) so callers can instanceof-check
18
- * which deadline fired.
12
+ * Build a timeout policy that throws `ErrorCtor` on deadline.
13
+ *
14
+ * # Cancellation contract
15
+ *
16
+ * Each invocation:
17
+ * 1. Creates a fresh `AbortController` for this attempt.
18
+ * 2. Arms a `setTimeout` that aborts the controller with a fresh `ErrorCtor(ms)`.
19
+ * 3. Links the parent signal: if the parent aborts (e.g. `totalTimeout` or
20
+ * caller cancellation), the child aborts with the parent's reason.
21
+ * 4. Races `fn(childSignal)` against the abort event.
22
+ *
23
+ * The race is critical: it ensures `act()` returns promptly even if `fn`
24
+ * ignores the signal. The underlying `fn` may keep running in the background
25
+ * (resource leak), but the caller is unblocked. This is the best JavaScript
26
+ * can do without cooperation from `fn`.
27
+ *
28
+ * If `fn` cooperates (passes `signal` to `fetch`, `AbortController`, etc.),
29
+ * the underlying work is cancelled properly — no leak.
30
+ *
31
+ * # Error attribution
32
+ *
33
+ * If the per-attempt timer fires, we throw `ErrorCtor(ms)` regardless of
34
+ * what `fn` does. If the parent signal fires first, we throw the parent's
35
+ * reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
19
36
  */
20
- class TotalTimeoutError extends Error {
21
- constructor(ms) {
22
- super(`ACT total timeout exceeded after ${ms}ms`);
23
- this.name = 'TotalTimeoutError';
24
- this.ms = ms;
25
- }
26
- }
27
- exports.TotalTimeoutError = TotalTimeoutError;
28
- // ─── Helpers ──────────────────────────────────────────────────────────────────
29
- /** Shared race-against-a-timer logic. ErrorCtor lets callers pick the error type. */
30
37
  function makeTimeoutPolicy(opts, ErrorCtor) {
31
- return (fn, _ctx) => () => new Promise((resolve, reject) => {
32
- const timer = setTimeout(() => reject(new ErrorCtor(opts.ms)), opts.ms);
33
- fn()
34
- .then((v) => { clearTimeout(timer); resolve(v); })
35
- .catch((e) => { clearTimeout(timer); reject(e); });
36
- });
38
+ return (fn, ctx) => async (parentSignal) => {
39
+ const controller = new AbortController();
40
+ // Pass key to the error ctor for better debugging context.
41
+ const timerError = new ErrorCtor(opts.ms, { key: ctx.key });
42
+ // Arm the per-attempt timer. The error object is allocated once so the
43
+ // stack trace points here (the policy frame), not at setTimeout's
44
+ // internal callback.
45
+ const timer = setTimeout(() => controller.abort(timerError), opts.ms);
46
+ // NOTE: do NOT `unref()` this timer. The timeout IS the operation
47
+ // the caller is awaiting. unref'ing would let Node exit the process
48
+ // while a timeout was pending — silently dropping the operation.
49
+ // The timer is cleared in the finally block below.
50
+ // Link parent → child. Capture the unlink function so we can clean
51
+ // up the listener on success path (contract).
52
+ const unlink = (0, abort_js_1.linkSignal)(parentSignal, controller);
53
+ try {
54
+ // Race fn against the abort event. If fn settles first, we get its
55
+ // result/error. If the signal aborts first, we reject with reason.
56
+ //
57
+ // We do NOT use AbortSignal.timeout() here because we need to throw
58
+ // our own ErrorCtor, not a DOMException named "TimeoutError".
59
+ return await new Promise((resolve, reject) => {
60
+ if (controller.signal.aborted) {
61
+ reject(controller.signal.reason);
62
+ return;
63
+ }
64
+ const onAbort = () => reject(controller.signal.reason);
65
+ controller.signal.addEventListener('abort', onAbort, { once: true });
66
+ Promise.resolve(fn(controller.signal)).then((value) => {
67
+ controller.signal.removeEventListener('abort', onAbort);
68
+ resolve(value);
69
+ }, (error) => {
70
+ controller.signal.removeEventListener('abort', onAbort);
71
+ reject(error);
72
+ });
73
+ });
74
+ }
75
+ finally {
76
+ clearTimeout(timer);
77
+ unlink();
78
+ }
79
+ };
37
80
  }
38
- // ─── Policies ─────────────────────────────────────────────────────────────────
39
81
  /**
40
- * Races fn against a hard deadline.
41
- * Rejects with TimeoutError if the deadline fires first.
82
+ * Per-attempt timeout. Races `fn` against a deadline that resets on retry.
42
83
  *
43
- * Place this INSIDE retryPolicy (closer to fn) so each attempt has its own clock.
84
+ * Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
85
+ * own clock.
44
86
  */
45
87
  function timeoutPolicy(opts) {
46
- return makeTimeoutPolicy(opts, TimeoutError);
88
+ return makeTimeoutPolicy(opts, errors_js_1.TimeoutError);
47
89
  }
48
90
  /**
49
- * Races the ENTIRE operation (all retry attempts + delays) against a budget.
50
- * Rejects with TotalTimeoutError if the budget is exhausted.
91
+ * Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
92
+ * delays) against a hard budget that does NOT reset.
51
93
  *
52
94
  * Place this as the OUTERMOST policy so the clock starts before any other
53
95
  * policy runs and stops regardless of what the inner chain is doing.
54
96
  */
55
97
  function totalTimeoutPolicy(opts) {
56
- return makeTimeoutPolicy(opts, TotalTimeoutError);
98
+ return makeTimeoutPolicy(opts, errors_js_1.TotalTimeoutError);
57
99
  }
@@ -1,27 +1,16 @@
1
1
  import type { PolicyApplier, TimeoutOptions } from '../types/index.js';
2
- export declare class TimeoutError extends Error {
3
- readonly ms: number;
4
- constructor(ms: number);
5
- }
2
+ import { TimeoutError, TotalTimeoutError } from '../errors.js';
3
+ export { TimeoutError, TotalTimeoutError };
6
4
  /**
7
- * Thrown when the total budget across all attempts is exceeded.
8
- * Distinct from TimeoutError (per-attempt) so callers can instanceof-check
9
- * which deadline fired.
10
- */
11
- export declare class TotalTimeoutError extends Error {
12
- readonly ms: number;
13
- constructor(ms: number);
14
- }
15
- /**
16
- * Races fn against a hard deadline.
17
- * Rejects with TimeoutError if the deadline fires first.
5
+ * Per-attempt timeout. Races `fn` against a deadline that resets on retry.
18
6
  *
19
- * Place this INSIDE retryPolicy (closer to fn) so each attempt has its own clock.
7
+ * Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
8
+ * own clock.
20
9
  */
21
10
  export declare function timeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
22
11
  /**
23
- * Races the ENTIRE operation (all retry attempts + delays) against a budget.
24
- * Rejects with TotalTimeoutError if the budget is exhausted.
12
+ * Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
13
+ * delays) against a hard budget that does NOT reset.
25
14
  *
26
15
  * Place this as the OUTERMOST policy so the clock starts before any other
27
16
  * policy runs and stops regardless of what the inner chain is doing.
@@ -1 +1 @@
1
- {"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAI5F,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;gBAEP,EAAE,EAAE,MAAM;CAKvB;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;gBAEP,EAAE,EAAE,MAAM;CAKvB;AAyBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
1
+ {"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAE5F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAa9D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAA;AAyF1C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}