@demlik/tea 0.2.0 → 0.4.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 (196) hide show
  1. package/README.md +49 -0
  2. package/dist/agent/index.d.ts +16 -770
  3. package/dist/agent/index.js +18 -16
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +22 -36
  6. package/dist/authed-call/index.js.map +1 -1
  7. package/dist/await-terminal/index.d.ts +2 -2
  8. package/dist/await-terminal/index.js +2 -2
  9. package/dist/batch-window/index.d.ts +3 -3
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +5 -2
  13. package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
  14. package/dist/chunk-25LBDCBC.js.map +1 -0
  15. package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
  16. package/dist/chunk-2AHAR7CX.js.map +1 -0
  17. package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  18. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  19. package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
  20. package/dist/chunk-5W7CNAHB.js.map +1 -0
  21. package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
  22. package/dist/chunk-B5RAZVXW.js.map +1 -0
  23. package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
  24. package/dist/chunk-D4NJBQAH.js.map +1 -0
  25. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  26. package/dist/chunk-D736QUGF.js.map +1 -0
  27. package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
  28. package/dist/chunk-ET6QQNSQ.js.map +1 -0
  29. package/dist/chunk-EWJP3YYP.js +21 -0
  30. package/dist/chunk-EWJP3YYP.js.map +1 -0
  31. package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
  32. package/dist/chunk-FCKP6N7Y.js.map +1 -0
  33. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  34. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  35. package/dist/chunk-G5E74IJB.js +8 -0
  36. package/dist/chunk-G5E74IJB.js.map +1 -0
  37. package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
  38. package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
  39. package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
  40. package/dist/chunk-JXYYRKD6.js.map +1 -0
  41. package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
  42. package/dist/chunk-LJJV6ZD5.js.map +1 -0
  43. package/dist/chunk-LYLDCIPH.js +365 -0
  44. package/dist/chunk-LYLDCIPH.js.map +1 -0
  45. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  46. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  47. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  48. package/dist/chunk-MUUROGIH.js.map +1 -0
  49. package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
  50. package/dist/chunk-PBDYW2TU.js.map +1 -0
  51. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  52. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  53. package/dist/chunk-RMX4EOQK.js +26 -0
  54. package/dist/chunk-RMX4EOQK.js.map +1 -0
  55. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  56. package/dist/chunk-S2EGRPCM.js.map +1 -0
  57. package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
  58. package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
  59. package/dist/chunk-SSHDBOXF.js +71 -0
  60. package/dist/chunk-SSHDBOXF.js.map +1 -0
  61. package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
  62. package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
  63. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  64. package/dist/chunk-UF4MZSLT.js.map +1 -0
  65. package/dist/chunk-ZJ5AOYBH.js +8 -0
  66. package/dist/chunk-ZJ5AOYBH.js.map +1 -0
  67. package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
  68. package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
  69. package/dist/circuit-breaker/index.d.ts +1 -0
  70. package/dist/circuit-breaker/index.js +1 -1
  71. package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
  72. package/dist/deadline/index.d.ts +77 -11
  73. package/dist/deadline/index.js +3 -3
  74. package/dist/devtools/index.js +8 -28
  75. package/dist/devtools/index.js.map +1 -1
  76. package/dist/do/index.d.ts +52 -24
  77. package/dist/do/index.js +94 -92
  78. package/dist/do/index.js.map +1 -1
  79. package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
  80. package/dist/extension/index.d.ts +25 -7
  81. package/dist/extension/index.js +4 -3
  82. package/dist/extension/index.js.map +1 -1
  83. package/dist/extension/react.js +4 -3
  84. package/dist/extension/react.js.map +1 -1
  85. package/dist/extension/subs/index.d.ts +2 -2
  86. package/dist/extension/subs/index.js +7 -10
  87. package/dist/extension/subs/index.js.map +1 -1
  88. package/dist/fan-out/index.d.ts +10 -10
  89. package/dist/fan-out/index.js +3 -3
  90. package/dist/idempotent-intake/index.d.ts +2 -2
  91. package/dist/idempotent-intake/index.js +3 -3
  92. package/dist/index.d.ts +50 -489
  93. package/dist/index.js +2 -2
  94. package/dist/llm-call/index.d.ts +3 -2
  95. package/dist/llm-call/index.js +12 -10
  96. package/dist/machine-B_BF4jij.d.ts +804 -0
  97. package/dist/machine-viz/index.d.ts +1 -1
  98. package/dist/machine-viz/index.js +2 -2
  99. package/dist/mem/index.d.ts +3 -2
  100. package/dist/mem/index.js.map +1 -1
  101. package/dist/monitored-run/index.d.ts +54 -30
  102. package/dist/monitored-run/index.js +7 -7
  103. package/dist/node/index.d.ts +3 -2
  104. package/dist/node/index.js +2 -3
  105. package/dist/node/index.js.map +1 -1
  106. package/dist/paginated-walk/index.d.ts +2 -2
  107. package/dist/paginated-walk/index.js +11 -10
  108. package/dist/parity/index.d.ts +6 -5
  109. package/dist/parity/index.js +5 -4
  110. package/dist/parity/index.js.map +1 -1
  111. package/dist/pbt/arbitraries/index.d.ts +1 -1
  112. package/dist/pbt/index.d.ts +1 -1
  113. package/dist/pbt/index.js +3 -3
  114. package/dist/pbt/index.js.map +1 -1
  115. package/dist/pbt/runners/index.d.ts +1 -1
  116. package/dist/pbt/runners/index.js +3 -3
  117. package/dist/poller/index.d.ts +59 -17
  118. package/dist/poller/index.js +56 -20
  119. package/dist/poller/index.js.map +1 -1
  120. package/dist/prediction/index.d.ts +1 -1
  121. package/dist/prediction/index.js +2 -2
  122. package/dist/pure/index.d.ts +1 -1
  123. package/dist/pure/index.js +2 -2
  124. package/dist/rate-limit/index.d.ts +2 -2
  125. package/dist/rate-limit/index.js +1 -1
  126. package/dist/react/index.d.ts +10 -3
  127. package/dist/react/index.js +2 -2
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/reconciler/index.d.ts +2 -2
  130. package/dist/reconciler/index.js +13 -15
  131. package/dist/reconciler/index.js.map +1 -1
  132. package/dist/recorder/index.d.ts +20 -20
  133. package/dist/recorder/index.js +1 -1
  134. package/dist/resilient-call/index.d.ts +31 -6
  135. package/dist/resilient-call/index.js +10 -9
  136. package/dist/retry-backoff/index.d.ts +191 -14
  137. package/dist/retry-backoff/index.js +1 -1
  138. package/dist/retry-to-success/index.js +1 -1
  139. package/dist/runtime-types-CukV8kMX.d.ts +406 -0
  140. package/dist/saga/index.d.ts +3 -2
  141. package/dist/saga/index.js +2 -2
  142. package/dist/saga/index.js.map +1 -1
  143. package/dist/snapshot/index.d.ts +1 -1
  144. package/dist/snapshot/index.js +3 -3
  145. package/dist/subs/index.d.ts +372 -4
  146. package/dist/subs/index.js +4 -1
  147. package/dist/testing/index.d.ts +2 -2
  148. package/dist/testing/index.js +2 -2
  149. package/dist/throttled-input/index.d.ts +2 -2
  150. package/dist/throttled-input/index.js +14 -13
  151. package/dist/throttled-input/index.js.map +1 -1
  152. package/dist/token-refresh/index.d.ts +1 -1
  153. package/dist/token-refresh/index.js +3 -3
  154. package/dist/trace-replay/index.d.ts +12 -7
  155. package/dist/trace-replay/index.js +4 -3
  156. package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
  157. package/dist/with-deadline/index.d.ts +1 -1
  158. package/dist/with-deadline/index.js +2 -2
  159. package/dist/with-resilience/index.d.ts +21 -2
  160. package/dist/with-resilience/index.js +12 -23
  161. package/dist/with-resilience/index.js.map +1 -1
  162. package/dist/with-telemetry/index.d.ts +1 -1
  163. package/dist/with-telemetry/index.js +2 -2
  164. package/dist/work-queue/adapter.d.ts +2 -2
  165. package/dist/work-queue/adapter.js +2 -2
  166. package/dist/work-queue/index.d.ts +3 -2
  167. package/dist/work-queue/index.js +1 -1
  168. package/dist/work-queue/index.js.map +1 -1
  169. package/dist/work-queue/ops.d.ts +2 -2
  170. package/dist/work-queue/ops.js +1 -1
  171. package/dist/workflow/index.d.ts +51 -19
  172. package/dist/workflow/index.js +71 -52
  173. package/dist/workflow/index.js.map +1 -1
  174. package/package.json +4 -1
  175. package/dist/chunk-2YOE5PIS.js.map +0 -1
  176. package/dist/chunk-3545BOYL.js.map +0 -1
  177. package/dist/chunk-5VNWZADP.js.map +0 -1
  178. package/dist/chunk-6GIGQT6C.js.map +0 -1
  179. package/dist/chunk-6VDCTR7N.js.map +0 -1
  180. package/dist/chunk-DYRKDCJI.js.map +0 -1
  181. package/dist/chunk-EGWTT5XJ.js.map +0 -1
  182. package/dist/chunk-EMJTPHI7.js +0 -91
  183. package/dist/chunk-EMJTPHI7.js.map +0 -1
  184. package/dist/chunk-FOGL3TC7.js.map +0 -1
  185. package/dist/chunk-IK4S5DJN.js.map +0 -1
  186. package/dist/chunk-M6UZT7HS.js.map +0 -1
  187. package/dist/chunk-NSDFA6M6.js +0 -198
  188. package/dist/chunk-NSDFA6M6.js.map +0 -1
  189. package/dist/chunk-SC3FAFZN.js.map +0 -1
  190. package/dist/chunk-SKAMZKTE.js +0 -20
  191. package/dist/chunk-SKAMZKTE.js.map +0 -1
  192. package/dist/chunk-TGG26XMK.js.map +0 -1
  193. package/dist/chunk-UV6UWGEO.js.map +0 -1
  194. package/dist/chunk-UYWUVEP6.js.map +0 -1
  195. package/dist/chunk-VFGKTGGC.js.map +0 -1
  196. package/dist/chunk-ZDRN3YCE.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * @packageDocumentation
2
3
  * @demlik/tea/retry-backoff — exponential backoff with jitter + cap, and the
3
4
  * retry-attempt state every fallible `interpret` handler folds over.
4
5
  *
@@ -20,6 +21,13 @@
20
21
  * const delayMs = nextDelayMs(retry, policy); // pure: rng injected at call site
21
22
  * return [{ ...state, retry }, [{ type: "ScheduleRetry", delayMs, into: TimerFiredRetry }]];
22
23
  *
24
+ * and the same wiring under a wall-clock outage budget, where the failure Msg
25
+ * carries the instant it was observed:
26
+ *
27
+ * // policy: { baseMs: 250, factor: 2, capMs: 4_000, maxElapsedMs: 1_050_000, jitter: "full" }
28
+ * const retry = recordFailure(state.retry, msg.error, msg.at);
29
+ * if (!shouldRetry(retry, policy, msg.at)) return [toGaveUp(state, retryElapsedMs(retry, msg.at)), []];
30
+ *
23
31
  * The reducer stays pure: `nextDelayMs` is called with the runtime's RNG only
24
32
  * at the emit boundary (or the caller defers the whole delay computation to the
25
33
  * `interpret` handler and passes `rng` there). Either way the substrate never
@@ -27,6 +35,42 @@
27
35
  *
28
36
  * NOT a substrate primitive: it depends on nothing from `../index`. It is a
29
37
  * leaf utility consumers reach via the `@demlik/tea/retry-backoff` subpath.
38
+ *
39
+ * ── THE BOUND MAY BE A DURATION, NOT ONLY A COUNT ──────────────────────────
40
+ * `maxAttempts` was the module's only terminal bound, and a count is the wrong
41
+ * bound whenever this ladder nests inside somebody else's ladder. A downstream
42
+ * consumer proved it in production: 4 restarts over a 250ms→4s ladder is ~3.75s
43
+ * of backoff, which nested under a client's own ~2.5s ladder came to roughly
44
+ * SIXTEEN SECONDS of total patience — against a peer that waits ~17.5 MINUTES
45
+ * before it gives up on the other side. A routine deploy outran the budget and
46
+ * killed 170 runs in 7 days that the peer would have resumed from a checkpoint.
47
+ *
48
+ * The failure a count cannot express is that the QUESTION is temporal. "How
49
+ * long do we tolerate an unreachable peer" has an answer in seconds, usually
50
+ * derived from the peer's own give-up window; what a count of attempts costs in
51
+ * seconds depends on how long each attempt takes, which is the carrier's
52
+ * business and not the policy's. So a policy may now declare `maxElapsedMs` —
53
+ * a wall-clock outage budget measured from the FIRST failure of the streak —
54
+ * instead of, or in addition to, `maxAttempts`. When both are declared, retry
55
+ * continues only while EVERY declared bound still permits it.
56
+ *
57
+ * TIME IS AN INPUT, NOT AN AMBIENT READ (invariant 2). The streak's origin is
58
+ * supplied by the caller to `recordFailure`, and `shouldRetry` is told `nowMs`;
59
+ * this module still never reads a clock, exactly as it never reads the RNG.
60
+ * The types make the wiring the only wiring that compiles: a duration-bounded
61
+ * policy only satisfies the `shouldRetry` overload that demands a
62
+ * {@link TimedRetryState} *and* a `nowMs`, so "declared an outage budget, never
63
+ * fed it a clock" is a type error rather than a bound that quietly never fires.
64
+ *
65
+ * A policy with NO bound at all is likewise unrepresentable by accident — the
66
+ * {@link RetryBudget} union has no "neither" arm; forever-retry must be spelled
67
+ * `unbounded: true` ({@link UnboundedRetryPolicy}).
68
+ *
69
+ * Strengthens invariant 2 (transitions are pure — the clock enters as an
70
+ * argument, never as a `Date.now()` inside the fold) and invariant 5
71
+ * (composition is by reduction — the new bound is an additive arm on the policy
72
+ * union and an optional argument on the verbs, so every existing count-bounded
73
+ * call site keeps compiling untouched).
30
74
  */
31
75
  /**
32
76
  * Jitter strategy applied to the computed backoff delay.
@@ -82,25 +126,107 @@ declare function asRng(fn: () => number, probe?: boolean): Rng;
82
126
  */
83
127
  declare const defaultRng: Rng;
84
128
  /**
85
- * A retry policy is pure configuration — the knobs of the backoff curve. It
86
- * carries no mutable state; `RetryState` does. One policy is shared across all
87
- * the attempts of one logical operation (often a module-scope const).
129
+ * The shape of the delay ladder, with no terminal bound attached — `baseMs`,
130
+ * `factor`, `capMs`, `jitter`. Split out from the policy because the curve and
131
+ * the bound answer different questions: the curve is "how fast do we come
132
+ * back", the bound is "when do we stop". `backoffDelay` needs only the curve.
88
133
  */
89
- interface RetryPolicy {
134
+ interface BackoffCurve {
90
135
  /** Delay for attempt 0, in milliseconds, before any jitter or cap. */
91
136
  readonly baseMs: number;
92
137
  /** Geometric growth factor. `2` doubles the delay each attempt. */
93
138
  readonly factor: number;
94
139
  /** Hard ceiling, in milliseconds. The capped exponential never exceeds this. */
95
140
  readonly capMs: number;
141
+ /** Jitter strategy applied to the capped exponential delay. */
142
+ readonly jitter: Jitter;
143
+ }
144
+ /**
145
+ * Bound by ATTEMPT COUNT — the original bound, and the right one when each
146
+ * attempt costs about the same and the caller is the outermost ladder.
147
+ *
148
+ * `maxElapsedMs: never` is what keeps this arm honest: a count-bounded policy
149
+ * is *declared* to have no outage budget, so it satisfies the clock-free
150
+ * `shouldRetry` overload. Add `maxElapsedMs` and it becomes a
151
+ * {@link DurationRetryPolicy}, which no longer does.
152
+ */
153
+ interface CountBound {
96
154
  /**
97
155
  * Maximum number of attempts. `shouldRetry` allows attempts `0 .. maxAttempts-1`,
98
156
  * so `maxAttempts: 3` means "try once, then retry at most twice" (attempts 0, 1, 2).
99
157
  */
100
158
  readonly maxAttempts: number;
101
- /** Jitter strategy applied to the capped exponential delay. */
102
- readonly jitter: Jitter;
159
+ readonly maxElapsedMs?: never;
160
+ readonly unbounded?: never;
161
+ }
162
+ /**
163
+ * Bound by WALL-CLOCK OUTAGE DURATION, optionally also by count.
164
+ *
165
+ * `maxElapsedMs` is measured from the FIRST failure of the current streak
166
+ * ({@link TimedRetryState.firstFailureAtMs}), not from the last attempt: the
167
+ * question it answers is "how long has the far side been unreachable", which is
168
+ * the question a nesting caller's own patience is denominated in. Derive it
169
+ * from the peer's give-up window rather than restating a guess — two sides
170
+ * holding different answers to one ordered fact is the defect this bound
171
+ * exists to prevent.
172
+ *
173
+ * `maxAttempts` stays available here, and when both are present retry continues
174
+ * only while BOTH still permit it (see {@link shouldRetry}). Adding a count to a
175
+ * duration budget is a belt-and-braces guard against a pathologically fast
176
+ * failure loop, not a substitute for the duration.
177
+ */
178
+ interface DurationBound {
179
+ /**
180
+ * Outage budget in wall-clock milliseconds. `shouldRetry` permits another
181
+ * attempt while `nowMs - firstFailureAtMs < maxElapsedMs`, however many
182
+ * attempts that takes.
183
+ */
184
+ readonly maxElapsedMs: number;
185
+ /** Optional secondary count bound; omitted means "no count bound". */
186
+ readonly maxAttempts?: number;
187
+ readonly unbounded?: never;
188
+ }
189
+ /**
190
+ * No bound at all — retry forever. Deliberately verbose and deliberately the
191
+ * only way to say it: {@link RetryBudget} has no "neither bound declared" arm,
192
+ * so an infinite retry loop is something a policy author must write down, never
193
+ * something a forgotten field produces.
194
+ */
195
+ interface Unbounded {
196
+ readonly unbounded: true;
197
+ readonly maxAttempts?: never;
198
+ readonly maxElapsedMs?: never;
103
199
  }
200
+ /**
201
+ * The terminal bound a policy declares: a count, a duration (optionally with a
202
+ * count), or an explicit opt-in to neither. Exactly one arm applies, and there
203
+ * is no arm for "I forgot".
204
+ */
205
+ type RetryBudget = CountBound | DurationBound | Unbounded;
206
+ /**
207
+ * A retry policy is pure configuration — the knobs of the backoff curve plus
208
+ * the bound that ends the retrying. It carries no mutable state; `RetryState`
209
+ * does. One policy is shared across all the attempts of one logical operation
210
+ * (often a module-scope const).
211
+ *
212
+ * `RetryPolicy` is the COUNT-bounded shape, unchanged since the module's first
213
+ * release: every existing `{ baseMs, factor, capMs, maxAttempts, jitter }`
214
+ * literal still satisfies it, `policy.maxAttempts` is still a `number` (not
215
+ * `number | undefined`), and `shouldRetry(state, policy)` still takes no clock.
216
+ * Reach for {@link DurationRetryPolicy} when the bound should be temporal, or
217
+ * {@link AnyRetryPolicy} when writing code generic over the bound.
218
+ */
219
+ type RetryPolicy = BackoffCurve & CountBound;
220
+ /** A curve bounded by wall-clock outage duration (and optionally by count). */
221
+ type DurationRetryPolicy = BackoffCurve & DurationBound;
222
+ /** A curve with the explicit opt-in to unbounded retry. */
223
+ type UnboundedRetryPolicy = BackoffCurve & Unbounded;
224
+ /**
225
+ * Any well-formed policy. The parameter type for code that must work whatever
226
+ * bound the caller declared — `shouldRetry`'s implementation, wrappers, and
227
+ * consumers that take a policy from configuration.
228
+ */
229
+ type AnyRetryPolicy = BackoffCurve & RetryBudget;
104
230
  /**
105
231
  * Sensible defaults: 100ms base, doubling, capped at 30s, up to 5 attempts,
106
232
  * full jitter (the herd-avoidance default). Tuned for network calls; override
@@ -128,7 +254,7 @@ declare const defaultRetryPolicy: RetryPolicy;
128
254
  * `baseMs`. `capMs` is also the floor of the clamp, so a misconfigured
129
255
  * `capMs < baseMs` still yields a delay within `[0, capMs]`.
130
256
  */
131
- declare function backoffDelay(attempt: number, policy: RetryPolicy, rng?: Rng): number;
257
+ declare function backoffDelay(attempt: number, policy: BackoffCurve, rng?: Rng): number;
132
258
  /**
133
259
  * Per-operation retry state: how many attempts have failed so far, and the most
134
260
  * recent error. `lastError` is `unknown` because the failure can be anything a
@@ -141,21 +267,72 @@ interface RetryState {
141
267
  /** The error from the most recent failure, if any. Carried, never interpreted. */
142
268
  readonly lastError?: unknown;
143
269
  }
144
- /** The starting state: zero attempts, no error yet. */
270
+ /**
271
+ * A retry state that also knows WHEN its failure streak began — the origin a
272
+ * duration bound is measured from. Minted by passing the observation instant to
273
+ * {@link recordFailure}; there is no other way to obtain one, so a streak with
274
+ * an outage budget but no origin is not a value that can be written down.
275
+ *
276
+ * The origin is the FIRST failure of the streak and is preserved across
277
+ * subsequent failures. A success resets the streak the way it always did — by
278
+ * returning to `initRetry()` — which drops the origin with it, so an operation
279
+ * that fails occasionally never accumulates outage and pays nothing for a wide
280
+ * `maxElapsedMs`; only an unbroken run of failures grows toward the bound.
281
+ */
282
+ interface TimedRetryState extends RetryState {
283
+ /** When the streak's first failure was observed (caller's clock, epoch ms). */
284
+ readonly firstFailureAtMs: number;
285
+ }
286
+ /** The starting state: zero attempts, no error yet, no streak. */
145
287
  declare function initRetry(): RetryState;
146
288
  /**
147
289
  * Record a failed attempt. PURE — returns a *new* state with `attempt`
148
290
  * incremented and `lastError` set; the input `state` is never mutated.
291
+ *
292
+ * `atMs` is the instant the failure was OBSERVED, supplied by the caller (the
293
+ * `interpret` handler owns the clock; this module never reads one — invariant
294
+ * 2). Passing it starts the streak clock on the first failure and preserves the
295
+ * original origin on every failure after it, yielding a {@link TimedRetryState}
296
+ * that a duration-bounded policy can be evaluated against. Omit it and the
297
+ * count-bounded behaviour is bit-for-bit what it always was.
149
298
  */
299
+ declare function recordFailure(state: TimedRetryState, error: unknown, atMs?: number): TimedRetryState;
300
+ declare function recordFailure(state: RetryState, error: unknown, atMs: number): TimedRetryState;
150
301
  declare function recordFailure(state: RetryState, error: unknown): RetryState;
302
+ /**
303
+ * How long the current failure streak has lasted, in milliseconds. PURE.
304
+ *
305
+ * The honest number for a give-up log — a count of attempts says nothing about
306
+ * how much patience it actually spent. Clamped at 0 so a caller's clock going
307
+ * backwards (NTP step, a rehydrated state whose origin is from a different
308
+ * host) reads as "no outage yet" rather than a negative duration.
309
+ */
310
+ declare function retryElapsedMs(state: TimedRetryState, nowMs: number): number;
151
311
  /**
152
312
  * Whether another attempt is permitted under `policy`. PURE.
153
313
  *
154
- * `attempt` is the number of failures recorded so far (= the index of the next
155
- * attempt). We may retry while that index is below `maxAttempts`, i.e. attempts
156
- * `0 .. maxAttempts-1` are allowed and the `maxAttempts`-th is refused.
314
+ * Every bound the policy DECLARES must still permit the attempt; a bound it
315
+ * does not declare is not consulted. Concretely:
316
+ *
317
+ * - `maxAttempts` — `attempt` is the number of failures recorded so far (= the
318
+ * index of the next attempt); we may retry while that index is below
319
+ * `maxAttempts`, i.e. attempts `0 .. maxAttempts-1` are allowed and the
320
+ * `maxAttempts`-th is refused. Unchanged.
321
+ * - `maxElapsedMs` — we may retry while `nowMs - firstFailureAtMs` is below
322
+ * the budget, *however many attempts that takes*. A streak's first failure
323
+ * measures a zero-length outage and is therefore always retried, unless the
324
+ * budget is itself `0` (the "no patience" setting).
325
+ * - `unbounded: true` — always permitted.
326
+ *
327
+ * The overloads are the wiring contract. A count-bounded or explicitly
328
+ * unbounded policy needs no clock, so the two-argument call every existing
329
+ * caller already writes keeps type-checking. A policy carrying `maxElapsedMs`
330
+ * matches only the second overload, which demands a {@link TimedRetryState} and
331
+ * a `nowMs` — so declaring an outage budget and then never feeding it a clock
332
+ * fails to compile instead of silently producing a bound that never fires.
157
333
  */
158
- declare function shouldRetry(state: RetryState, policy: RetryPolicy): boolean;
334
+ declare function shouldRetry(state: RetryState, policy: BackoffCurve & (CountBound | Unbounded), nowMs?: number): boolean;
335
+ declare function shouldRetry(state: TimedRetryState, policy: AnyRetryPolicy, nowMs: number): boolean;
159
336
  /**
160
337
  * The delay to wait before the next attempt, given the current state. PURE.
161
338
  *
@@ -163,6 +340,6 @@ declare function shouldRetry(state: RetryState, policy: RetryPolicy): boolean;
163
340
  * attempt index, so the delay grows with each recorded failure. `rng` is
164
341
  * injected for determinism, same contract as `backoffDelay`.
165
342
  */
166
- declare function nextDelayMs(state: RetryState, policy: RetryPolicy, rng?: Rng): number;
343
+ declare function nextDelayMs(state: RetryState, policy: BackoffCurve, rng?: Rng): number;
167
344
 
168
- export { type Jitter, type RetryPolicy, type RetryState, type Rng, asRng, backoffDelay, defaultRetryPolicy, defaultRng, initRetry, nextDelayMs, recordFailure, shouldRetry };
345
+ export { type AnyRetryPolicy, type BackoffCurve, type CountBound, type DurationBound, type DurationRetryPolicy, type Jitter, type RetryBudget, type RetryPolicy, type RetryState, type Rng, type TimedRetryState, type Unbounded, type UnboundedRetryPolicy, asRng, backoffDelay, defaultRetryPolicy, defaultRng, initRetry, nextDelayMs, recordFailure, retryElapsedMs, shouldRetry };
@@ -1,3 +1,3 @@
1
- export { asRng, backoffDelay, defaultRetryPolicy, defaultRng, initRetry, nextDelayMs, recordFailure, shouldRetry } from '../chunk-5VNWZADP.js';
1
+ export { asRng, backoffDelay, defaultRetryPolicy, defaultRng, initRetry, nextDelayMs, recordFailure, retryElapsedMs, shouldRetry } from '../chunk-Q2UM7BXU.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { defaultRng, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-5VNWZADP.js';
1
+ import { defaultRng, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-Q2UM7BXU.js';
2
2
 
3
3
  // src/retry-to-success/index.ts
4
4
  var RetryExhaustedError = class extends Error {