@evolu/common 7.4.0 → 8.0.0-next.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 (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -0,0 +1,1471 @@
1
+ /**
2
+ * Composable scheduling strategies.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { fibonacciAt, FibonacciIndex, increment } from "./Number.js";
8
+ import type { RandomDep } from "./Random.js";
9
+ import { done, err, type NextResult, ok } from "./Result.js";
10
+ import type { repeat, RepeatAttempt, retry, RetryAttempt } from "./Task.js";
11
+ import {
12
+ type Duration,
13
+ durationToMillis,
14
+ Millis,
15
+ minMillis,
16
+ type TimeDep,
17
+ } from "./Time.js";
18
+ import { onePositiveInt, PositiveInt } from "./Type.js";
19
+ import type { Predicate } from "./Types.js";
20
+
21
+ /**
22
+ * Composable scheduling strategies for {@link retry}, {@link repeat}, rate
23
+ * limiting, and more.
24
+ *
25
+ * A Schedule uses the State pattern: calling `schedule(deps)` creates a step
26
+ * function with internal state captured in closures. Each call to `step(input)`
27
+ * advances that state and returns `Ok([Output, Millis])` or `Err(Done<void>)`
28
+ * to stop. Multiple calls to `schedule(deps)` create independent state
29
+ * instances.
30
+ *
31
+ * ### Example
32
+ *
33
+ * ```ts
34
+ * import {
35
+ * exponential,
36
+ * jitter,
37
+ * maxDelay,
38
+ * retry,
39
+ * take,
40
+ * } from "@evolu/common";
41
+ *
42
+ * const fetchWithRetry = retry(
43
+ * fetchData,
44
+ * // A jittered, capped, limited exponential backoff.
45
+ * jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
46
+ * );
47
+ * ```
48
+ *
49
+ * Or use a preset:
50
+ *
51
+ * ```ts
52
+ * import { retryStrategyAws, retry } from "@evolu/common";
53
+ *
54
+ * const fetchWithRetry = retry(fetchData, retryStrategyAws);
55
+ * ```
56
+ */
57
+ export type Schedule<out Output, in Input = unknown> = (
58
+ deps: ScheduleDeps,
59
+ ) => (input: Input) => NextResult<readonly [Output, Millis]>;
60
+
61
+ /**
62
+ * Dependencies provided to a {@link Schedule}.
63
+ *
64
+ * The executor provides these once, and the schedule uses what it needs.
65
+ */
66
+ export type ScheduleDeps = TimeDep & RandomDep;
67
+
68
+ /**
69
+ * Base interface for schedule-based task helpers.
70
+ *
71
+ * Used by {@link RetryAttempt}, {@link RepeatAttempt}, and future schedule-driven
72
+ * helpers.
73
+ *
74
+ * @group Composition
75
+ */
76
+ export interface ScheduleStep<Output> {
77
+ /** The current attempt. */
78
+ readonly attempt: PositiveInt;
79
+
80
+ /** Output from the {@link Schedule} step. */
81
+ readonly output: Output;
82
+
83
+ /** Delay before this step executes. */
84
+ readonly delay: Millis;
85
+ }
86
+
87
+ /**
88
+ * Internal per-step metrics computed from timestamps.
89
+ *
90
+ * The schedule computes this internally from deps.time.now().
91
+ */
92
+ interface ScheduleStepMetrics {
93
+ /** Current attempt number. */
94
+ readonly attempt: PositiveInt;
95
+ /** Milliseconds elapsed since the schedule started. */
96
+ readonly elapsed: Millis;
97
+ /** Milliseconds since the previous step. On first step, this is 0. */
98
+ readonly elapsedSincePrevious: Millis;
99
+ }
100
+
101
+ /**
102
+ * Creates an internal per-step metrics tracker.
103
+ *
104
+ * Each call updates internal state and returns computed metrics.
105
+ */
106
+ const createScheduleStepMetrics = (
107
+ deps: TimeDep,
108
+ ): (() => ScheduleStepMetrics) => {
109
+ let attempt = onePositiveInt;
110
+ let start: Millis | null = null;
111
+ let previous: Millis | null = null;
112
+
113
+ return () => {
114
+ const now = deps.time.now();
115
+ const currentAttempt = attempt;
116
+ attempt = increment(attempt) as PositiveInt;
117
+ start ??= now;
118
+ const elapsed = (now - start) as Millis;
119
+ const elapsedSincePrevious =
120
+ previous === null ? (0 as Millis) : ((now - previous) as Millis);
121
+ previous = now;
122
+ return { attempt: currentAttempt, elapsed, elapsedSincePrevious };
123
+ };
124
+ };
125
+
126
+ /**
127
+ * A schedule that never stops and has no delay.
128
+ *
129
+ * Outputs the attempt count (0, 1, 2, ...). Useful as a base for composition or
130
+ * for immediate retry without backoff.
131
+ *
132
+ * ### Example
133
+ *
134
+ * ```ts
135
+ * // Retry immediately, up to 5 times
136
+ * const immediate = take(5)(forever);
137
+ * ```
138
+ *
139
+ * @group Constructors
140
+ */
141
+ export const forever: Schedule<number> = () => {
142
+ let attempt = 0;
143
+ return () => ok([attempt++, minMillis]);
144
+ };
145
+
146
+ /**
147
+ * A schedule that runs exactly once with no delay.
148
+ *
149
+ * Convenience for `take(1)(forever)`. Useful for one-shot operations.
150
+ *
151
+ * ### Example
152
+ *
153
+ * ```ts
154
+ * // Execute once, no retry
155
+ * const oneShot = once;
156
+ * ```
157
+ *
158
+ * @group Constructors
159
+ */
160
+ export const once: Schedule<number> = () => {
161
+ let finished = false;
162
+ return () => {
163
+ if (finished) return err(done());
164
+ finished = true;
165
+ return ok([0, minMillis]);
166
+ };
167
+ };
168
+
169
+ /**
170
+ * A schedule that recurs a fixed number of times.
171
+ *
172
+ * Convenience for `take(n)(forever)`. Outputs the current repetition count (0,
173
+ * 1, 2, ..., n-1).
174
+ *
175
+ * ### Example
176
+ *
177
+ * ```ts
178
+ * // Retry up to 3 times (4 total attempts including initial)
179
+ * const retry = recurs(3);
180
+ * ```
181
+ *
182
+ * @group Constructors
183
+ */
184
+ export const recurs = (n: number): Schedule<number> => take(n)(forever);
185
+
186
+ /**
187
+ * Constant delay schedule.
188
+ *
189
+ * Always waits the same duration after each execution completes. Never stops —
190
+ * combine with {@link take} or {@link maxElapsed} to limit.
191
+ *
192
+ * ### Example
193
+ *
194
+ * ```ts
195
+ * // 1s, 1s, 1s, ... (polling)
196
+ * const poll = spaced("1s");
197
+ *
198
+ * // Retry 3 times with 500ms between each
199
+ * const retry = take(3)(spaced("500ms"));
200
+ *
201
+ * // Heartbeat schedule
202
+ * const heartbeat = spaced("30s");
203
+ * ```
204
+ *
205
+ * @group Constructors
206
+ */
207
+ export const spaced =
208
+ (duration: Duration): Schedule<Millis> =>
209
+ () => {
210
+ const ms = durationToMillis(duration);
211
+ return () => ok([ms, ms]);
212
+ };
213
+
214
+ /**
215
+ * Exponential backoff schedule.
216
+ *
217
+ * Computes delay as `base * factor^(attempt - 1)`:
218
+ *
219
+ * - Attempt 1: `base`
220
+ * - Attempt 2: `base * factor`
221
+ * - Attempt 3: `base * factor²`
222
+ * - ...
223
+ *
224
+ * Never stops — combine with {@link take} or {@link maxElapsed} to limit.
225
+ *
226
+ * ### Example
227
+ *
228
+ * ```ts
229
+ * // 100ms, 200ms, 400ms, 800ms, ...
230
+ * const exp = exponential("100ms");
231
+ *
232
+ * // 100ms, 150ms, 225ms, 338ms, ... (gentler growth)
233
+ * const gentle = exponential("100ms", 1.5);
234
+ * ```
235
+ *
236
+ * @group Constructors
237
+ */
238
+ export const exponential =
239
+ (base: Duration, factor = 2): Schedule<Millis> =>
240
+ () => {
241
+ const baseMs = durationToMillis(base);
242
+ let attempt = 0;
243
+ return () => {
244
+ attempt++;
245
+ const rawDelay = baseMs * Math.pow(factor, attempt - 1);
246
+ const delay = Millis.orThrow(Math.max(0, Math.round(rawDelay)));
247
+ return ok([delay, delay]);
248
+ };
249
+ };
250
+
251
+ /**
252
+ * Linear backoff schedule.
253
+ *
254
+ * Delay increases linearly: `base * attempt`:
255
+ *
256
+ * - Attempt 1: `base`
257
+ * - Attempt 2: `base * 2`
258
+ * - Attempt 3: `base * 3`
259
+ * - ...
260
+ *
261
+ * Never stops — combine with {@link take} or {@link maxElapsed} to limit.
262
+ *
263
+ * ### Example
264
+ *
265
+ * ```ts
266
+ * // 100ms, 200ms, 300ms, 400ms, ...
267
+ * const lin = linear("100ms");
268
+ * ```
269
+ *
270
+ * @group Constructors
271
+ */
272
+ export const linear =
273
+ (base: Duration): Schedule<Millis> =>
274
+ () => {
275
+ const ms = durationToMillis(base);
276
+ let attempt = 0;
277
+ return () => {
278
+ attempt++;
279
+ const delay = Millis.orThrow(ms * attempt);
280
+ return ok([delay, delay]);
281
+ };
282
+ };
283
+
284
+ /**
285
+ * Fibonacci backoff schedule.
286
+ *
287
+ * Delays follow the Fibonacci sequence, growing more slowly than exponential:
288
+ *
289
+ * - Attempt 1: `initial`
290
+ * - Attempt 2: `initial`
291
+ * - Attempt 3: `initial * 2`
292
+ * - Attempt 4: `initial * 3`
293
+ * - Attempt 5: `initial * 5`
294
+ * - ...
295
+ *
296
+ * Never stops — combine with {@link take} or {@link maxElapsed} to limit.
297
+ *
298
+ * ### Example
299
+ *
300
+ * ```ts
301
+ * // 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, ...
302
+ * const fib = fibonacci("100ms");
303
+ * ```
304
+ *
305
+ * @group Constructors
306
+ */
307
+ export const fibonacci =
308
+ (initial: Duration): Schedule<Millis> =>
309
+ () => {
310
+ const ms = durationToMillis(initial);
311
+ let index = 1;
312
+ return () => {
313
+ const delay = Millis.orThrow(
314
+ ms * fibonacciAt(FibonacciIndex.orThrow(index)),
315
+ );
316
+ index++;
317
+ return ok([delay, delay]);
318
+ };
319
+ };
320
+
321
+ /**
322
+ * Fixed interval schedule aligned to time windows.
323
+ *
324
+ * Recurs on a fixed interval, outputting the repetition count (0, 1, 2, ...).
325
+ * Unlike {@link spaced}, which waits a duration _after_ each execution, `fixed`
326
+ * maintains a consistent cadence from when the schedule started.
327
+ *
328
+ * If execution takes longer than the interval, the next execution happens
329
+ * immediately but subsequent runs still align to the original window
330
+ * boundaries. This prevents "pile-up" while maintaining predictable timing.
331
+ *
332
+ * ### Example
333
+ *
334
+ * ```ts
335
+ * // Health check every 5 seconds, aligned to windows
336
+ * const healthCheck = take(10)(fixed("5s"));
337
+ *
338
+ * // Cron-like behavior: run at consistent intervals
339
+ * const cronLike = fixed("1m");
340
+ * ```
341
+ *
342
+ * @group Constructors
343
+ */
344
+ export const fixed =
345
+ (interval: Duration): Schedule<number> =>
346
+ (deps) => {
347
+ const intervalMs = durationToMillis(interval);
348
+ const metrics = createScheduleStepMetrics(deps);
349
+ let count = 0;
350
+ return () => {
351
+ const { elapsed } = metrics();
352
+ // Which window should we be in based on count?
353
+ const expectedWindowEnd = (count + 1) * intervalMs;
354
+ const runningBehind = intervalMs > 0 && elapsed >= expectedWindowEnd;
355
+ // Time until next window boundary
356
+ const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
357
+ const boundary = intervalMs - remainder;
358
+ const delay = runningBehind ? 0 : boundary;
359
+ return ok([count++, delay as Millis]);
360
+ };
361
+ };
362
+
363
+ /**
364
+ * Divides the timeline into fixed windows and sleeps until the next boundary.
365
+ *
366
+ * Similar to {@link fixed}, but always sleeps until the next window boundary
367
+ * regardless of when the last execution started. Outputs the repetition count.
368
+ *
369
+ * Useful for aligning executions to regular intervals from the start time.
370
+ *
371
+ * ### Example
372
+ *
373
+ * ```ts
374
+ * // Execute at regular 5-second boundaries from start
375
+ * const aligned = windowed("5s");
376
+ * // If elapsed is 3s, waits 2s. If elapsed is 7s, waits 3s.
377
+ * ```
378
+ *
379
+ * @group Constructors
380
+ */
381
+ export const windowed =
382
+ (interval: Duration): Schedule<number> =>
383
+ (deps) => {
384
+ const intervalMs = durationToMillis(interval);
385
+ const metrics = createScheduleStepMetrics(deps);
386
+ let count = 0;
387
+ return () => {
388
+ const { elapsed } = metrics();
389
+ const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
390
+ const delay = intervalMs === 0 ? 0 : intervalMs - remainder;
391
+ return ok([count++, delay as Millis]);
392
+ };
393
+ };
394
+
395
+ /**
396
+ * A schedule that runs once with a single delay.
397
+ *
398
+ * Convenience for `take(1)(spaced(delay))`. Useful for simple one-shot delays.
399
+ *
400
+ * ### Example
401
+ *
402
+ * ```ts
403
+ * // Wait 1 second then stop
404
+ * const oneShot = fromDelay("1s");
405
+ * ```
406
+ *
407
+ * @group Constructors
408
+ */
409
+ export const fromDelay = (delay: Duration): Schedule<Millis> =>
410
+ take(1)(spaced(delay));
411
+
412
+ /**
413
+ * A schedule that runs through a sequence of delays.
414
+ *
415
+ * Convenience for sequencing single-delay schedules. Useful for predefined
416
+ * retry patterns.
417
+ *
418
+ * ### Example
419
+ *
420
+ * ```ts
421
+ * // Custom retry sequence: 100ms, 500ms, 2s
422
+ * const custom = fromDelays("100ms", "500ms", "2s");
423
+ * ```
424
+ *
425
+ * @group Constructors
426
+ */
427
+ export const fromDelays = (
428
+ ...delays: ReadonlyArray<Duration>
429
+ ): Schedule<Millis> => sequenceSchedules(...delays.map((d) => fromDelay(d)));
430
+
431
+ /**
432
+ * A schedule that outputs the total elapsed time since the schedule started.
433
+ *
434
+ * Never stops — combine with {@link take} or {@link maxElapsed} to limit. Useful
435
+ * for tracking how long a retry sequence has been running.
436
+ *
437
+ * ### Example
438
+ *
439
+ * ```ts
440
+ * // Track elapsed time alongside retries
441
+ * const withTiming = intersectSchedules(exponential("100ms"), elapsed);
442
+ * // Outputs: [[100, 0], [200, ~100], [400, ~300], ...]
443
+ *
444
+ * // Stop after 30 seconds of elapsed time
445
+ * const timeLimited = whileScheduleOutput((ms: Millis) => ms < 30000)(
446
+ * elapsed,
447
+ * );
448
+ * ```
449
+ *
450
+ * @group Constructors
451
+ */
452
+ export const elapsed: Schedule<Millis> = (deps) => {
453
+ const metrics = createScheduleStepMetrics(deps);
454
+ return () => ok([metrics().elapsed, minMillis]);
455
+ };
456
+
457
+ /**
458
+ * A schedule that runs for a specified duration then stops.
459
+ *
460
+ * Outputs the elapsed time. Useful for time-boxed operations or combining with
461
+ * other schedules to create time-limited variants.
462
+ *
463
+ * ### Example
464
+ *
465
+ * ```ts
466
+ * // Run for at most 30 seconds
467
+ * const timeLimited = during("30s");
468
+ *
469
+ * // Combine with exponential for time-boxed retry
470
+ * const timedRetry = intersectSchedules(
471
+ * exponential("100ms"),
472
+ * during("10s"),
473
+ * );
474
+ * ```
475
+ *
476
+ * @group Constructors
477
+ */
478
+ export const during = (duration: Duration): Schedule<Millis> =>
479
+ whileScheduleOutput((ms: Millis) => ms <= durationToMillis(duration))(
480
+ elapsed,
481
+ );
482
+
483
+ /**
484
+ * A schedule that always outputs a constant value.
485
+ *
486
+ * Never stops — combine with {@link take} or {@link maxElapsed} to limit.
487
+ *
488
+ * ### Example
489
+ *
490
+ * ```ts
491
+ * // Always output "retry"
492
+ * const labeled = always("retry");
493
+ *
494
+ * // Combine with timing
495
+ * const withLabel = intersectSchedules(
496
+ * exponential("100ms"),
497
+ * always("backoff"),
498
+ * );
499
+ * ```
500
+ *
501
+ * @group Constructors
502
+ */
503
+ export const always = <A>(value: A): Schedule<A> =>
504
+ mapSchedule(() => value)(forever);
505
+
506
+ /**
507
+ * Creates a schedule by unfolding a state.
508
+ *
509
+ * Each step outputs the current state and computes the next state using the
510
+ * provided function. Never stops — combine with {@link take} or
511
+ * {@link maxElapsed} to limit.
512
+ *
513
+ * ### Example
514
+ *
515
+ * ```ts
516
+ * // Counter: 0, 1, 2, 3, ...
517
+ * const counter = unfoldSchedule(0, (n) => n + 1);
518
+ *
519
+ * // Custom backoff: 100, 150, 225, 338, ... (×1.5 each time)
520
+ * const customBackoff = unfoldSchedule(100, (delay) =>
521
+ * Math.round(delay * 1.5),
522
+ * );
523
+ *
524
+ * // State machine
525
+ * type Phase = "init" | "warmup" | "active";
526
+ * const phases = unfoldSchedule<Phase>("init", (phase) => {
527
+ * switch (phase) {
528
+ * case "init":
529
+ * return "warmup";
530
+ * case "warmup":
531
+ * return "active";
532
+ * case "active":
533
+ * return "active";
534
+ * }
535
+ * });
536
+ * ```
537
+ *
538
+ * @group Constructors
539
+ */
540
+ export const unfoldSchedule =
541
+ <State>(initial: State, next: (state: State) => State): Schedule<State> =>
542
+ () => {
543
+ let state = initial;
544
+ return () => {
545
+ const current = state;
546
+ state = next(state);
547
+ return ok([current, minMillis]);
548
+ };
549
+ };
550
+
551
+ /**
552
+ * Limits a schedule to a maximum number of attempts.
553
+ *
554
+ * After `n` attempts, returns `Err(Done<void>)` (stop).
555
+ *
556
+ * ### Example
557
+ *
558
+ * ```ts
559
+ * // Exponential backoff, max 3 retries
560
+ * const limited = take(3)(exponential("100ms"));
561
+ * // Attempt 1: 100ms, Attempt 2: 200ms, Attempt 3: 400ms, Attempt 4: Err(Done<void>)
562
+ * ```
563
+ *
564
+ * @group Limiting
565
+ */
566
+ export const take =
567
+ (n: number) =>
568
+ <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
569
+ (deps) => {
570
+ const step = schedule(deps);
571
+ let attempt = 0;
572
+ return (input) => {
573
+ attempt++;
574
+ if (attempt > n) return err(done());
575
+ return step(input);
576
+ };
577
+ };
578
+
579
+ /**
580
+ * Limits schedule execution to a maximum elapsed time.
581
+ *
582
+ * After `duration` has elapsed since the schedule started, returns
583
+ * `Err(Done<void>)`.
584
+ *
585
+ * ### Example
586
+ *
587
+ * ```ts
588
+ * // Retry for at most 30 seconds
589
+ * const timeLimited = maxElapsed("30s")(exponential("1s"));
590
+ * ```
591
+ *
592
+ * @group Limiting
593
+ */
594
+ export const maxElapsed = (duration: Duration) => {
595
+ const maxMs = durationToMillis(duration);
596
+ return <Output, Input>(
597
+ schedule: Schedule<Output, Input>,
598
+ ): Schedule<Output, Input> =>
599
+ (deps) => {
600
+ const step = schedule(deps);
601
+ const metrics = createScheduleStepMetrics(deps);
602
+ return (input) => {
603
+ const { elapsed } = metrics();
604
+ return elapsed >= maxMs ? err(done()) : step(input);
605
+ };
606
+ };
607
+ };
608
+
609
+ /**
610
+ * Caps the delay to a maximum value.
611
+ *
612
+ * If the schedule returns a delay greater than `max`, returns `max` instead.
613
+ *
614
+ * ### Example
615
+ *
616
+ * ```ts
617
+ * // Exponential capped at 10 seconds
618
+ * const capped = maxDelay("10s")(exponential("1s"));
619
+ * // 1s, 2s, 4s, 8s, 10s, 10s, 10s, ...
620
+ * ```
621
+ *
622
+ * @group Limiting
623
+ */
624
+ export const maxDelay = (max: Duration) => {
625
+ const maxMs = durationToMillis(max);
626
+ return <Output, Input>(
627
+ schedule: Schedule<Output, Input>,
628
+ ): Schedule<Output, Input> =>
629
+ (deps) => {
630
+ const step = schedule(deps);
631
+ return (input) => {
632
+ const result = step(input);
633
+ if (!result.ok) return result;
634
+ const [output, delay] = result.value;
635
+ return ok([output, Millis.orThrow(Math.min(delay, maxMs))]);
636
+ };
637
+ };
638
+ };
639
+
640
+ /**
641
+ * Adds randomized jitter to delays.
642
+ *
643
+ * Jitter helps prevent "thundering herd" when many clients retry simultaneously
644
+ * after a service recovers. The delay is randomized within a range:
645
+ *
646
+ * - `factor = 0` — no jitter (original delay)
647
+ * - `factor = 0.5` — delay varies ±50% (e.g., 1s becomes 500ms-1500ms)
648
+ * - `factor = 1` — full jitter, delay varies 0-200% (e.g., 1s becomes 0-2s)
649
+ *
650
+ * ### Example
651
+ *
652
+ * ```ts
653
+ * // AWS-style full jitter
654
+ * const awsStyle = jitter(1)(exponential("1s"));
655
+ *
656
+ * // Conservative jitter (±25%)
657
+ * const conservative = jitter(0.25)(exponential("1s"));
658
+ * ```
659
+ *
660
+ * @group Delay
661
+ */
662
+ export const jitter =
663
+ (factor = 0.5) =>
664
+ <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
665
+ (deps) => {
666
+ const step = schedule(deps);
667
+ return (input) => {
668
+ const result = step(input);
669
+ if (!result.ok) return result;
670
+ const [output, delay] = result.value;
671
+ const jittered = delay * (1 - factor + deps.random.next() * 2 * factor);
672
+ return ok([output, Millis.orThrow(Math.max(0, Math.round(jittered)))]);
673
+ };
674
+ };
675
+
676
+ /**
677
+ * Adds an initial delay before the first attempt.
678
+ *
679
+ * Subsequent attempts use the schedule's normal delays.
680
+ *
681
+ * ### Example
682
+ *
683
+ * ```ts
684
+ * // Wait 1s before first attempt, then exponential backoff
685
+ * const withWarmup = delayed("1s")(exponential("100ms"));
686
+ * ```
687
+ *
688
+ * @group Delay
689
+ */
690
+ export const delayed = (initialDelay: Duration) => {
691
+ const initialMs = durationToMillis(initialDelay);
692
+ return <Output, Input>(
693
+ schedule: Schedule<Output, Input>,
694
+ ): Schedule<Output, Input> =>
695
+ (deps) => {
696
+ const step = schedule(deps);
697
+ let first = true;
698
+ return (input) => {
699
+ const result = step(input);
700
+ if (!result.ok) return result;
701
+ if (first) {
702
+ first = false;
703
+ return ok([result.value[0], initialMs]);
704
+ }
705
+ return result;
706
+ };
707
+ };
708
+ };
709
+
710
+ /**
711
+ * Adds a fixed delay to the schedule's existing delay.
712
+ *
713
+ * ### Example
714
+ *
715
+ * ```ts
716
+ * // Add 500ms to each delay
717
+ * const slower = addDelay("500ms")(exponential("100ms"));
718
+ * // Delays: 600ms, 700ms, 900ms, 1300ms, ...
719
+ * ```
720
+ *
721
+ * @group Delay
722
+ */
723
+ export const addDelay = (
724
+ extra: Duration,
725
+ ): (<Output, Input>(
726
+ schedule: Schedule<Output, Input>,
727
+ ) => Schedule<Output, Input>) => {
728
+ const extraMs = durationToMillis(extra);
729
+ return modifyDelay((d) => d + extraMs);
730
+ };
731
+
732
+ /**
733
+ * Transforms the delay of a schedule.
734
+ *
735
+ * More flexible than {@link maxDelay} — can implement any delay transformation.
736
+ *
737
+ * ### Example
738
+ *
739
+ * ```ts
740
+ * // Double all delays
741
+ * const slower = modifyDelay((d) => d * 2)(exponential("100ms"));
742
+ *
743
+ * // Cap at 10s (equivalent to maxDelay)
744
+ * const capped = modifyDelay((d) => Math.min(d, 10000))(exponential("1s"));
745
+ * ```
746
+ *
747
+ * @group Delay
748
+ */
749
+ export const modifyDelay =
750
+ (f: (delay: Millis) => number) =>
751
+ <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
752
+ (deps) => {
753
+ const step = schedule(deps);
754
+ return (input) => {
755
+ const result = step(input);
756
+ if (!result.ok) return result;
757
+ const [output, delay] = result.value;
758
+ return ok([output, Millis.orThrow(Math.max(0, Math.round(f(delay))))]);
759
+ };
760
+ };
761
+
762
+ /**
763
+ * Adjusts delay by subtracting execution time.
764
+ *
765
+ * A simple combinator that subtracts the previous execution time from the
766
+ * schedule's delay. If execution took longer than the delay, returns 0.
767
+ *
768
+ * For window-aligned scheduling, use {@link fixed} instead.
769
+ *
770
+ * ### Example
771
+ *
772
+ * ```ts
773
+ * // Poll every 5s, accounting for execution time
774
+ * const polling = compensate(spaced("5s"));
775
+ * // If poll takes 1s → wait 4s. If poll takes 6s → wait 0s.
776
+ * ```
777
+ *
778
+ * @group Delay
779
+ */
780
+ export const compensate =
781
+ <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
782
+ (deps) => {
783
+ const step = schedule(deps);
784
+ const metrics = createScheduleStepMetrics(deps);
785
+ return (input) => {
786
+ const { elapsedSincePrevious } = metrics();
787
+ const result = step(input);
788
+ if (!result.ok) return result;
789
+ const [output, delay] = result.value;
790
+ const adjusted = Math.max(0, delay - elapsedSincePrevious);
791
+ return ok([output, adjusted as Millis]);
792
+ };
793
+ };
794
+
795
+ /**
796
+ * Continues while the input satisfies a predicate.
797
+ *
798
+ * Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `false`.
799
+ * Useful for input-aware retry strategies, e.g., only retry certain error
800
+ * types.
801
+ *
802
+ * ### Example
803
+ *
804
+ * ```ts
805
+ * interface MyError extends Typed<"Transient" | "Fatal"> {}
806
+ *
807
+ * // Only retry transient errors
808
+ * const retryTransient = whileScheduleInput(
809
+ * (error: MyError) => error.type === "Transient",
810
+ * )(exponential("100ms"));
811
+ * ```
812
+ *
813
+ * @group Filtering
814
+ */
815
+ export const whileScheduleInput =
816
+ <Input>(predicate: Predicate<Input>) =>
817
+ <Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
818
+ (deps) => {
819
+ const step = schedule(deps);
820
+ return (input) => {
821
+ if (!predicate(input)) return err(done());
822
+ return step(input);
823
+ };
824
+ };
825
+
826
+ /**
827
+ * Continues until the input satisfies a predicate.
828
+ *
829
+ * Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
830
+ * Useful for stopping retry on specific error conditions.
831
+ *
832
+ * ### Example
833
+ *
834
+ * ```ts
835
+ * interface MyError extends Typed<"Transient" | "Fatal"> {}
836
+ *
837
+ * // Stop retrying on fatal errors
838
+ * const stopOnFatal = untilScheduleInput(
839
+ * (error: MyError) => error.type === "Fatal",
840
+ * )(exponential("100ms"));
841
+ * ```
842
+ *
843
+ * @group Filtering
844
+ */
845
+ export const untilScheduleInput =
846
+ <Input>(predicate: Predicate<Input>) =>
847
+ <Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
848
+ (deps) => {
849
+ const step = schedule(deps);
850
+ return (input) => {
851
+ if (predicate(input)) return err(done());
852
+ return step(input);
853
+ };
854
+ };
855
+
856
+ /**
857
+ * Continues while the output satisfies a predicate.
858
+ *
859
+ * Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `false`.
860
+ *
861
+ * ### Example
862
+ *
863
+ * ```ts
864
+ * // Continue while delay is under 5 seconds
865
+ * const capped = whileScheduleOutput((delay: Millis) => delay < 5000)(
866
+ * exponential("1s"),
867
+ * );
868
+ * ```
869
+ *
870
+ * @group Filtering
871
+ */
872
+ export const whileScheduleOutput =
873
+ <Output>(predicate: Predicate<Output>) =>
874
+ <Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
875
+ (deps) => {
876
+ const step = schedule(deps);
877
+ return (input) => {
878
+ const result = step(input);
879
+ if (!result.ok) return result;
880
+ if (!predicate(result.value[0])) return err(done());
881
+ return result;
882
+ };
883
+ };
884
+
885
+ /**
886
+ * Continues until the output satisfies a predicate.
887
+ *
888
+ * Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
889
+ *
890
+ * ### Example
891
+ *
892
+ * ```ts
893
+ * // Stop when delay reaches 1 second
894
+ * const limited = untilScheduleOutput((delay: Millis) => delay >= 1000)(
895
+ * exponential("100ms"),
896
+ * );
897
+ * ```
898
+ *
899
+ * @group Filtering
900
+ */
901
+ export const untilScheduleOutput =
902
+ <Output>(predicate: Predicate<Output>) =>
903
+ <Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
904
+ (deps) => {
905
+ const step = schedule(deps);
906
+ return (input) => {
907
+ const result = step(input);
908
+ if (!result.ok) return result;
909
+ if (predicate(result.value[0])) return err(done());
910
+ return result;
911
+ };
912
+ };
913
+
914
+ /**
915
+ * Resets the schedule after a period of inactivity.
916
+ *
917
+ * If `elapsedSincePrevious` is greater than or equal to `duration`, creates a
918
+ * fresh state. Useful for circuit breakers that should "forget" failures after
919
+ * idle time.
920
+ *
921
+ * ### Example
922
+ *
923
+ * ```ts
924
+ * // Reset retry count after 1 minute of success
925
+ * const circuitBreaker = resetScheduleAfter("1m")(
926
+ * take(5)(exponential("1s")),
927
+ * );
928
+ * ```
929
+ *
930
+ * @group State
931
+ */
932
+ export const resetScheduleAfter = (duration: Duration) => {
933
+ const resetMs = durationToMillis(duration);
934
+ return <Output, Input>(
935
+ schedule: Schedule<Output, Input>,
936
+ ): Schedule<Output, Input> =>
937
+ (deps) => {
938
+ let step = schedule(deps);
939
+ const metrics = createScheduleStepMetrics(deps);
940
+ return (input) => {
941
+ const { elapsedSincePrevious } = metrics();
942
+ if (elapsedSincePrevious >= resetMs) {
943
+ step = schedule(deps);
944
+ }
945
+ return step(input);
946
+ };
947
+ };
948
+ };
949
+
950
+ /**
951
+ * Transforms the output of a schedule.
952
+ *
953
+ * The delay (second tuple element) remains unchanged.
954
+ *
955
+ * ### Example
956
+ *
957
+ * ```ts
958
+ * import { exponential, mapSchedule } from "@evolu/common";
959
+ *
960
+ * const schedule = mapSchedule((delay) => ({
961
+ * delay,
962
+ * doubled: delay * 2,
963
+ * }))(exponential("100ms"));
964
+ * ```
965
+ *
966
+ * @group Transform
967
+ */
968
+ export const mapSchedule =
969
+ <A, B>(f: (a: A) => B) =>
970
+ <Input>(schedule: Schedule<A, Input>): Schedule<B, Input> =>
971
+ (deps) => {
972
+ const step = schedule(deps);
973
+ return (input) => {
974
+ const result = step(input);
975
+ if (!result.ok) return result;
976
+ const [output, delay] = result.value;
977
+ return ok([f(output), delay]);
978
+ };
979
+ };
980
+
981
+ /**
982
+ * Creates a schedule that outputs its input, or wraps an existing schedule to
983
+ * output input instead of the original output.
984
+ *
985
+ * When called with no arguments, creates a schedule that outputs its input
986
+ * directly (the "identity" schedule). When called with a schedule, wraps it to
987
+ * preserve timing behavior but replace output with input.
988
+ *
989
+ * ### Example
990
+ *
991
+ * ```ts
992
+ * import { exponential, passthrough } from "@evolu/common";
993
+ *
994
+ * interface MyError {
995
+ * readonly message: string;
996
+ * }
997
+ *
998
+ * // Constructor: output equals input
999
+ * const identity = passthrough<MyError>();
1000
+ *
1001
+ * // Combinator: preserve timing, replace output
1002
+ * const withInput = passthrough(exponential("100ms"));
1003
+ * ```
1004
+ *
1005
+ * @group Constructors
1006
+ */
1007
+ export function passthrough<A>(): Schedule<A, A>;
1008
+ /** @group Transform */
1009
+ export function passthrough<Output, Input>(
1010
+ schedule: Schedule<Output, Input>,
1011
+ ): Schedule<Input, Input>;
1012
+ export function passthrough<Output, Input>(
1013
+ schedule?: Schedule<Output, Input>,
1014
+ ): Schedule<Input, Input> {
1015
+ if (schedule === undefined) {
1016
+ return () => (input) => ok([input, minMillis]);
1017
+ }
1018
+ return (deps) => {
1019
+ const step = schedule(deps);
1020
+ return (input) => {
1021
+ const result = step(input);
1022
+ if (!result.ok) return result;
1023
+ return ok([input, result.value[1]]);
1024
+ };
1025
+ };
1026
+ }
1027
+
1028
+ /**
1029
+ * Folds over the outputs of a schedule, accumulating state.
1030
+ *
1031
+ * Each step outputs the accumulated value. Useful for tracking totals,
1032
+ * collecting outputs, or building up metadata across attempts.
1033
+ *
1034
+ * ### Example
1035
+ *
1036
+ * ```ts
1037
+ * // Track total delay spent
1038
+ * const withTotal = foldSchedule(
1039
+ * 0,
1040
+ * (total: number, delay: Millis) => total + delay,
1041
+ * )(exponential("100ms"));
1042
+ * // Outputs: 100, 300, 700, 1500, ... (cumulative)
1043
+ *
1044
+ * // Collect all outputs
1045
+ * const collected = foldSchedule([] as Millis[], (acc, delay: Millis) => [
1046
+ * ...acc,
1047
+ * delay,
1048
+ * ])(take(3)(spaced("1s")));
1049
+ * // Outputs: [1000], [1000, 1000], [1000, 1000, 1000]
1050
+ *
1051
+ * // Count attempts with custom output
1052
+ * const counted = foldSchedule(
1053
+ * { attempts: 0, lastDelay: 0 as Millis },
1054
+ * (acc, delay: Millis) => ({
1055
+ * attempts: acc.attempts + 1,
1056
+ * lastDelay: delay,
1057
+ * }),
1058
+ * )(exponential("100ms"));
1059
+ * ```
1060
+ *
1061
+ * @group Transform
1062
+ */
1063
+ export const foldSchedule =
1064
+ <Z, Output>(initial: Z, f: (acc: Z, output: Output) => Z) =>
1065
+ <Input>(schedule: Schedule<Output, Input>): Schedule<Z, Input> =>
1066
+ (deps) => {
1067
+ const step = schedule(deps);
1068
+ let acc = initial;
1069
+ return (input) => {
1070
+ const result = step(input);
1071
+ if (!result.ok) return result;
1072
+ const [output, delay] = result.value;
1073
+ acc = f(acc, output);
1074
+ return ok([acc, delay]);
1075
+ };
1076
+ };
1077
+
1078
+ /**
1079
+ * Wraps a schedule to output the number of repetitions instead of original
1080
+ * output.
1081
+ *
1082
+ * Outputs 0, 1, 2, ... while preserving the underlying schedule's timing and
1083
+ * termination behavior.
1084
+ *
1085
+ * ### Example
1086
+ *
1087
+ * ```ts
1088
+ * // Track how many retries occurred
1089
+ * const counted = repetitions(exponential("100ms"));
1090
+ * // Outputs: 0, 1, 2, ... with exponential delays
1091
+ * ```
1092
+ *
1093
+ * @group Transform
1094
+ */
1095
+ export const repetitions = <Output, Input>(
1096
+ schedule: Schedule<Output, Input>,
1097
+ ): Schedule<number, Input> => foldSchedule(-1, (n) => n + 1)(schedule);
1098
+
1099
+ /**
1100
+ * Outputs the delay between recurrences.
1101
+ *
1102
+ * Wraps a schedule to output its delay (in milliseconds) instead of the
1103
+ * original output. Useful for monitoring or logging delay patterns.
1104
+ *
1105
+ * ### Example
1106
+ *
1107
+ * ```ts
1108
+ * // Monitor exponential delays
1109
+ * const monitorDelays = delays(exponential("100ms"));
1110
+ * // Outputs: 100, 200, 400, 800, ... (the delays themselves)
1111
+ *
1112
+ * // Log delays for debugging
1113
+ * const logged = tapScheduleOutput(console.log)(
1114
+ * delays(exponential("100ms")),
1115
+ * );
1116
+ * ```
1117
+ *
1118
+ * @group Transform
1119
+ */
1120
+ export const delays =
1121
+ <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Millis, Input> =>
1122
+ (deps) => {
1123
+ const step = schedule(deps);
1124
+ return (input) => {
1125
+ const result = step(input);
1126
+ if (!result.ok) return result;
1127
+ const [, delay] = result.value;
1128
+ return ok([delay, delay]);
1129
+ };
1130
+ };
1131
+
1132
+ /**
1133
+ * Collects all outputs into an array.
1134
+ *
1135
+ * Each step outputs an array containing all outputs so far.
1136
+ *
1137
+ * ### Example
1138
+ *
1139
+ * ```ts
1140
+ * // Collect all delays
1141
+ * const collected = collectAllScheduleOutputs(take(3)(spaced("100ms")));
1142
+ * // Outputs: [100], [100, 100], [100, 100, 100]
1143
+ * ```
1144
+ *
1145
+ * @group Collection
1146
+ */
1147
+ export const collectAllScheduleOutputs = <Output, Input>(
1148
+ schedule: Schedule<Output, Input>,
1149
+ ): Schedule<ReadonlyArray<Output>, Input> =>
1150
+ foldSchedule<ReadonlyArray<Output>, Output>([], (acc, out) => [...acc, out])(
1151
+ schedule,
1152
+ );
1153
+
1154
+ /**
1155
+ * Collects all inputs into an array.
1156
+ *
1157
+ * Each step outputs an array containing all inputs received so far. Mirror of
1158
+ * {@link collectAllScheduleOutputs} but for inputs.
1159
+ *
1160
+ * ### Example
1161
+ *
1162
+ * ```ts
1163
+ * // Collect all errors during retry
1164
+ * const errorHistory = collectScheduleInputs(
1165
+ * take(3)(exponential("100ms")),
1166
+ * );
1167
+ * // After 3 retries, outputs array of all error inputs
1168
+ * ```
1169
+ *
1170
+ * @group Collection
1171
+ */
1172
+ export const collectScheduleInputs = <Output, Input>(
1173
+ schedule: Schedule<Output, Input>,
1174
+ ): Schedule<ReadonlyArray<Input>, Input> =>
1175
+ collectAllScheduleOutputs(passthrough(schedule));
1176
+
1177
+ /**
1178
+ * Collects outputs while a predicate is true.
1179
+ *
1180
+ * More flexible than {@link collectAllScheduleOutputs} — stops collecting when
1181
+ * the predicate returns false.
1182
+ *
1183
+ * ### Example
1184
+ *
1185
+ * ```ts
1186
+ * // Collect delays while under 1 second
1187
+ * const smallDelays = collectWhileScheduleOutput(
1188
+ * (delay: Millis) => delay < 1000,
1189
+ * )(exponential("100ms"));
1190
+ * // Outputs: [100], [100, 200], [100, 200, 400], [100, 200, 400, 800], stops
1191
+ * ```
1192
+ *
1193
+ * @group Collection
1194
+ */
1195
+ export const collectWhileScheduleOutput =
1196
+ <Output>(predicate: Predicate<Output>) =>
1197
+ <Input>(
1198
+ schedule: Schedule<Output, Input>,
1199
+ ): Schedule<ReadonlyArray<Output>, Input> =>
1200
+ collectAllScheduleOutputs(whileScheduleOutput(predicate)(schedule));
1201
+
1202
+ /**
1203
+ * Collects outputs until a predicate becomes true.
1204
+ *
1205
+ * Mirror of {@link collectWhileScheduleOutput} — stops collecting when the
1206
+ * predicate returns true (inclusive of the matching output).
1207
+ *
1208
+ * ### Example
1209
+ *
1210
+ * ```ts
1211
+ * // Collect delays until reaching 1 second
1212
+ * const untilLarge = collectUntilScheduleOutput(
1213
+ * (delay: Millis) => delay >= 1000,
1214
+ * )(exponential("100ms"));
1215
+ * // Outputs: [100], [100, 200], [100, 200, 400], [100, 200, 400, 800], stops
1216
+ * ```
1217
+ *
1218
+ * @group Collection
1219
+ */
1220
+ export const collectUntilScheduleOutput =
1221
+ <Output>(predicate: Predicate<Output>) =>
1222
+ <Input>(
1223
+ schedule: Schedule<Output, Input>,
1224
+ ): Schedule<ReadonlyArray<Output>, Input> =>
1225
+ collectAllScheduleOutputs(untilScheduleOutput(predicate)(schedule));
1226
+
1227
+ /**
1228
+ * Sequences schedules: runs each until it stops, then continues with the next.
1229
+ *
1230
+ * Useful for adaptive strategies that start aggressive and become more
1231
+ * conservative over time.
1232
+ *
1233
+ * ### Example
1234
+ *
1235
+ * ```ts
1236
+ * // Fast retries first, then slower, then final fallback
1237
+ * const adaptive = sequenceSchedules(
1238
+ * take(3)(exponential("100ms")),
1239
+ * take(5)(fixed("500ms")),
1240
+ * fixed("1s"),
1241
+ * );
1242
+ * // Runs: 100ms, 200ms, 400ms, then 500ms×5, then 1s forever
1243
+ * ```
1244
+ *
1245
+ * @group Composition
1246
+ */
1247
+ export const sequenceSchedules =
1248
+ <Output, Input>(
1249
+ ...schedules: ReadonlyArray<Schedule<Output, Input>>
1250
+ ): Schedule<Output, Input> =>
1251
+ (deps) => {
1252
+ let index = 0;
1253
+ type Step =
1254
+ | ((input: Input) => NextResult<readonly [Output, Millis]>)
1255
+ | null;
1256
+ let currentStep: Step = schedules.length > 0 ? schedules[0](deps) : null;
1257
+ return (input) => {
1258
+ while (currentStep !== null) {
1259
+ const result = currentStep(input);
1260
+ if (result.ok) return result;
1261
+
1262
+ // Current exhausted, try next.
1263
+ index++;
1264
+ currentStep = index < schedules.length ? schedules[index](deps) : null;
1265
+ }
1266
+ return err(done());
1267
+ };
1268
+ };
1269
+
1270
+ /**
1271
+ * Combines two schedules with AND semantics.
1272
+ *
1273
+ * Continues only while both schedules want to continue. Uses the maximum delay.
1274
+ *
1275
+ * ### Example
1276
+ *
1277
+ * ```ts
1278
+ * // Retry up to 5 times AND within 30 seconds (both conditions must be met)
1279
+ * const both = intersectSchedules(
1280
+ * take(5)(exponential("1s")),
1281
+ * maxElapsed("30s")(forever),
1282
+ * );
1283
+ * ```
1284
+ *
1285
+ * @group Composition
1286
+ */
1287
+ export const intersectSchedules =
1288
+ <OutputA, OutputB, Input>(
1289
+ a: Schedule<OutputA, Input>,
1290
+ b: Schedule<OutputB, Input>,
1291
+ ): Schedule<[OutputA, OutputB], Input> =>
1292
+ (deps) => {
1293
+ const stepA = a(deps);
1294
+ const stepB = b(deps);
1295
+ return (input) => {
1296
+ const resultA = stepA(input);
1297
+ const resultB = stepB(input);
1298
+ if (!resultA.ok || !resultB.ok) return err(done());
1299
+ const [outputA, delayA] = resultA.value;
1300
+ const [outputB, delayB] = resultB.value;
1301
+ return ok([[outputA, outputB], Millis.orThrow(Math.max(delayA, delayB))]);
1302
+ };
1303
+ };
1304
+
1305
+ /**
1306
+ * Combines two schedules with OR semantics.
1307
+ *
1308
+ * Continues while either schedule wants to continue. Uses the minimum delay.
1309
+ *
1310
+ * ### Example
1311
+ *
1312
+ * ```ts
1313
+ * // Retry up to 5 times OR up to 30 seconds, whichever is longer
1314
+ * const either = unionSchedules(
1315
+ * take(5)(exponential("1s")),
1316
+ * maxElapsed("30s")(forever),
1317
+ * );
1318
+ * ```
1319
+ *
1320
+ * @group Composition
1321
+ */
1322
+ export const unionSchedules =
1323
+ <OutputA, OutputB, Input>(
1324
+ a: Schedule<OutputA, Input>,
1325
+ b: Schedule<OutputB, Input>,
1326
+ ): Schedule<OutputA | OutputB, Input> =>
1327
+ (deps) => {
1328
+ const stepA = a(deps);
1329
+ const stepB = b(deps);
1330
+ return (input) => {
1331
+ const resultA = stepA(input);
1332
+ const resultB = stepB(input);
1333
+
1334
+ if (!resultA.ok && !resultB.ok) return err(done());
1335
+ if (!resultA.ok) return resultB;
1336
+ if (!resultB.ok) return resultA;
1337
+
1338
+ const [outputA, delayA] = resultA.value;
1339
+ const [outputB, delayB] = resultB.value;
1340
+ // Use minimum delay, output from the one with smaller delay
1341
+ const minDelay = Math.min(delayA, delayB) as Millis;
1342
+ return delayA <= delayB
1343
+ ? ok([outputA, minDelay])
1344
+ : ok([outputB, minDelay]);
1345
+ };
1346
+ };
1347
+
1348
+ /**
1349
+ * Selects between two schedules based on input.
1350
+ *
1351
+ * If {@link Predicate} returns `true`, uses `altSchedule`; otherwise uses the
1352
+ * base schedule. Useful for implementing error-aware backoff where certain
1353
+ * errors (e.g., throttling) use different delays.
1354
+ *
1355
+ * ### Example
1356
+ *
1357
+ * ```ts
1358
+ * interface MyError extends Typed<"Throttled" | "NetworkError"> {}
1359
+ *
1360
+ * const awsWithThrottling = whenInput<MyError, Millis>(
1361
+ * (error) => error.type === "Throttled",
1362
+ * exponential("1s"), // throttled: 1s base
1363
+ * )(exponential("100ms")); // normal: 100ms base
1364
+ * ```
1365
+ *
1366
+ * @group Composition
1367
+ */
1368
+ export const whenInput =
1369
+ <Input, Output>(
1370
+ predicate: Predicate<Input>,
1371
+ altSchedule: Schedule<Output, Input>,
1372
+ ) =>
1373
+ (schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
1374
+ (deps) => {
1375
+ const normalStep = schedule(deps);
1376
+ const altStep = altSchedule(deps);
1377
+ return (input) => {
1378
+ if (predicate(input)) return altStep(input);
1379
+ return normalStep(input);
1380
+ };
1381
+ };
1382
+
1383
+ /**
1384
+ * Executes a side effect for every output without altering the schedule.
1385
+ *
1386
+ * Useful for logging, monitoring, or debugging without changing schedule
1387
+ * behavior.
1388
+ *
1389
+ * ### Example
1390
+ *
1391
+ * ```ts
1392
+ * // Log each delay for debugging
1393
+ * const logged = tapScheduleOutput((delay: Millis) => {
1394
+ * console.log(`Next delay: ${delay}ms`);
1395
+ * })(exponential("100ms"));
1396
+ *
1397
+ * // Track metrics
1398
+ * const recorded: Array<Millis> = [];
1399
+ * const tracked = tapScheduleOutput((delay: Millis) => {
1400
+ * recorded.push(delay);
1401
+ * })(retryStrategyAws);
1402
+ * ```
1403
+ *
1404
+ * @group Side effects
1405
+ */
1406
+ export const tapScheduleOutput =
1407
+ <Output>(f: (output: Output) => void) =>
1408
+ <Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
1409
+ (deps) => {
1410
+ const step = schedule(deps);
1411
+ return (input) => {
1412
+ const result = step(input);
1413
+ if (!result.ok) return result;
1414
+ f(result.value[0]);
1415
+ return result;
1416
+ };
1417
+ };
1418
+
1419
+ /**
1420
+ * Executes a side effect for every input without altering the schedule.
1421
+ *
1422
+ * Useful for logging errors during retry or monitoring what values are being
1423
+ * processed.
1424
+ *
1425
+ * ### Example
1426
+ *
1427
+ * ```ts
1428
+ * interface MyError extends Typed<string> {}
1429
+ *
1430
+ * const retrySchedule: Schedule<Millis, MyError> = exponential("100ms");
1431
+ *
1432
+ * // Log each error during retry
1433
+ * const logged = tapScheduleInput((error: MyError) => {
1434
+ * console.log(`Retrying after error: ${error.type}`);
1435
+ * })(retrySchedule);
1436
+ *
1437
+ * // Track retry reasons
1438
+ * const reasons: Array<string> = [];
1439
+ * const tracked = tapScheduleInput((error: MyError) => {
1440
+ * reasons.push(error.type);
1441
+ * })(retrySchedule);
1442
+ * ```
1443
+ *
1444
+ * @group Side effects
1445
+ */
1446
+ export const tapScheduleInput =
1447
+ <Input>(f: (input: Input) => void) =>
1448
+ <Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
1449
+ (deps) => {
1450
+ const step = schedule(deps);
1451
+ return (input) => {
1452
+ f(input);
1453
+ return step(input);
1454
+ };
1455
+ };
1456
+
1457
+ /**
1458
+ * AWS standard retry strategy.
1459
+ *
1460
+ * Exponential backoff (100ms base), max 2 retries (3 total attempts), 20s cap,
1461
+ * full jitter.
1462
+ *
1463
+ * @group Retry Strategies
1464
+ * @see https://github.com/aws/aws-sdk-java-v2/blob/master/core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java
1465
+ * @see https://github.com/aws/aws-sdk-java-v2/blob/master/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java
1466
+ */
1467
+ export const retryStrategyAws: Schedule<Millis> = /*#__PURE__*/ jitter(1)(
1468
+ /*#__PURE__*/ maxDelay("20s")(
1469
+ /*#__PURE__*/ take(2)(/*#__PURE__*/ exponential("100ms")),
1470
+ ),
1471
+ );