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