@evolu/common 8.7.0 → 8.9.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 (302) hide show
  1. package/dist/src/Array.d.ts +21 -21
  2. package/dist/src/Array.js +5 -5
  3. package/dist/src/Assert.d.ts +498 -141
  4. package/dist/src/Assert.d.ts.map +1 -1
  5. package/dist/src/Assert.js +648 -99
  6. package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
  7. package/dist/src/Binary.d.ts.map +1 -0
  8. package/dist/src/{Buffer.js → Binary.js} +206 -24
  9. package/dist/src/Console.d.ts +2 -10
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +6 -12
  12. package/dist/src/Crypto.d.ts +5 -5
  13. package/dist/src/Crypto.js +1 -1
  14. package/dist/src/Eq.d.ts +35 -40
  15. package/dist/src/Eq.d.ts.map +1 -1
  16. package/dist/src/Eq.js +51 -87
  17. package/dist/src/Function.d.ts +9 -14
  18. package/dist/src/Function.d.ts.map +1 -1
  19. package/dist/src/Function.js +7 -9
  20. package/dist/src/Http.d.ts +6 -7
  21. package/dist/src/Http.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +1 -0
  24. package/dist/src/LockManager.d.ts +1 -1
  25. package/dist/src/LockManager.js +1 -1
  26. package/dist/src/Lookup.d.ts +3 -2
  27. package/dist/src/Lookup.d.ts.map +1 -1
  28. package/dist/src/Lookup.js +4 -3
  29. package/dist/src/Number.d.ts +1 -1
  30. package/dist/src/Number.d.ts.map +1 -1
  31. package/dist/src/Number.js +1 -1
  32. package/dist/src/Object.d.ts +4 -4
  33. package/dist/src/Object.d.ts.map +1 -1
  34. package/dist/src/Object.js +5 -4
  35. package/dist/src/Option.d.ts +1 -2
  36. package/dist/src/Option.d.ts.map +1 -1
  37. package/dist/src/Option.js +1 -2
  38. package/dist/src/Platform.js +2 -0
  39. package/dist/src/Polyfills.d.ts +6 -0
  40. package/dist/src/Polyfills.d.ts.map +1 -1
  41. package/dist/src/Polyfills.js +10 -1
  42. package/dist/src/Random.d.ts +1 -1
  43. package/dist/src/Result.d.ts +49 -54
  44. package/dist/src/Result.d.ts.map +1 -1
  45. package/dist/src/Result.js +17 -22
  46. package/dist/src/Set.d.ts +6 -6
  47. package/dist/src/Set.js +2 -2
  48. package/dist/src/Sqlite.js +1 -1
  49. package/dist/src/String.js +3 -2
  50. package/dist/src/Task.d.ts +32 -32
  51. package/dist/src/Task.d.ts.map +1 -1
  52. package/dist/src/Task.js +20 -22
  53. package/dist/src/Test.d.ts +31 -3
  54. package/dist/src/Test.d.ts.map +1 -1
  55. package/dist/src/Test.js +46 -3
  56. package/dist/src/Time.d.ts +3 -11
  57. package/dist/src/Time.d.ts.map +1 -1
  58. package/dist/src/Time.js +10 -18
  59. package/dist/src/Type.d.ts +87 -85
  60. package/dist/src/Type.d.ts.map +1 -1
  61. package/dist/src/Type.js +70 -63
  62. package/dist/src/Types.d.ts +12 -12
  63. package/dist/src/Types.js +1 -1
  64. package/dist/src/WebSocket.d.ts +4 -7
  65. package/dist/src/WebSocket.d.ts.map +1 -1
  66. package/dist/src/Worker.d.ts +1 -1
  67. package/dist/src/Worker.js +7 -7
  68. package/dist/src/index.d.ts +1 -1
  69. package/dist/src/index.js +1 -1
  70. package/dist/src/intl/_en.d.ts.map +1 -1
  71. package/dist/src/intl/_en.js +1 -1
  72. package/dist/src/intl/ar.d.ts.map +1 -1
  73. package/dist/src/intl/ar.js +1 -1
  74. package/dist/src/intl/bn.d.ts.map +1 -1
  75. package/dist/src/intl/bn.js +1 -1
  76. package/dist/src/intl/ca.d.ts.map +1 -1
  77. package/dist/src/intl/ca.js +1 -1
  78. package/dist/src/intl/cs.d.ts.map +1 -1
  79. package/dist/src/intl/cs.js +1 -1
  80. package/dist/src/intl/da.d.ts.map +1 -1
  81. package/dist/src/intl/da.js +1 -1
  82. package/dist/src/intl/de.d.ts.map +1 -1
  83. package/dist/src/intl/de.js +1 -1
  84. package/dist/src/intl/el.d.ts.map +1 -1
  85. package/dist/src/intl/el.js +1 -1
  86. package/dist/src/intl/es.d.ts.map +1 -1
  87. package/dist/src/intl/es.js +1 -1
  88. package/dist/src/intl/fa.d.ts.map +1 -1
  89. package/dist/src/intl/fa.js +1 -1
  90. package/dist/src/intl/fi.d.ts.map +1 -1
  91. package/dist/src/intl/fi.js +1 -1
  92. package/dist/src/intl/fil.d.ts.map +1 -1
  93. package/dist/src/intl/fil.js +1 -1
  94. package/dist/src/intl/fr.d.ts.map +1 -1
  95. package/dist/src/intl/fr.js +1 -1
  96. package/dist/src/intl/he.d.ts.map +1 -1
  97. package/dist/src/intl/he.js +1 -1
  98. package/dist/src/intl/hi.d.ts.map +1 -1
  99. package/dist/src/intl/hi.js +1 -1
  100. package/dist/src/intl/hr.d.ts.map +1 -1
  101. package/dist/src/intl/hr.js +1 -1
  102. package/dist/src/intl/hu.d.ts.map +1 -1
  103. package/dist/src/intl/hu.js +1 -1
  104. package/dist/src/intl/id.d.ts.map +1 -1
  105. package/dist/src/intl/id.js +1 -1
  106. package/dist/src/intl/it.d.ts.map +1 -1
  107. package/dist/src/intl/it.js +1 -1
  108. package/dist/src/intl/ja.d.ts.map +1 -1
  109. package/dist/src/intl/ja.js +1 -1
  110. package/dist/src/intl/ko.d.ts.map +1 -1
  111. package/dist/src/intl/ko.js +1 -1
  112. package/dist/src/intl/ml.d.ts.map +1 -1
  113. package/dist/src/intl/ml.js +1 -1
  114. package/dist/src/intl/mr.d.ts.map +1 -1
  115. package/dist/src/intl/mr.js +1 -1
  116. package/dist/src/intl/ms.d.ts.map +1 -1
  117. package/dist/src/intl/ms.js +1 -1
  118. package/dist/src/intl/nb.d.ts.map +1 -1
  119. package/dist/src/intl/nb.js +1 -1
  120. package/dist/src/intl/nl.d.ts.map +1 -1
  121. package/dist/src/intl/nl.js +1 -1
  122. package/dist/src/intl/pa.d.ts.map +1 -1
  123. package/dist/src/intl/pa.js +1 -1
  124. package/dist/src/intl/pl.d.ts.map +1 -1
  125. package/dist/src/intl/pl.js +1 -1
  126. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  127. package/dist/src/intl/pt-BR.js +1 -1
  128. package/dist/src/intl/pt.d.ts.map +1 -1
  129. package/dist/src/intl/pt.js +1 -1
  130. package/dist/src/intl/ro.d.ts.map +1 -1
  131. package/dist/src/intl/ro.js +1 -1
  132. package/dist/src/intl/sk.d.ts.map +1 -1
  133. package/dist/src/intl/sk.js +1 -1
  134. package/dist/src/intl/sl.d.ts.map +1 -1
  135. package/dist/src/intl/sl.js +1 -1
  136. package/dist/src/intl/sv.d.ts.map +1 -1
  137. package/dist/src/intl/sv.js +1 -1
  138. package/dist/src/intl/sw.d.ts.map +1 -1
  139. package/dist/src/intl/sw.js +1 -1
  140. package/dist/src/intl/ta.d.ts.map +1 -1
  141. package/dist/src/intl/ta.js +1 -1
  142. package/dist/src/intl/te.d.ts.map +1 -1
  143. package/dist/src/intl/te.js +1 -1
  144. package/dist/src/intl/th.d.ts.map +1 -1
  145. package/dist/src/intl/th.js +1 -1
  146. package/dist/src/intl/tr.d.ts.map +1 -1
  147. package/dist/src/intl/tr.js +1 -1
  148. package/dist/src/intl/uk.d.ts.map +1 -1
  149. package/dist/src/intl/uk.js +1 -1
  150. package/dist/src/intl/ur.d.ts.map +1 -1
  151. package/dist/src/intl/ur.js +1 -1
  152. package/dist/src/intl/vi.d.ts.map +1 -1
  153. package/dist/src/intl/vi.js +1 -1
  154. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  155. package/dist/src/intl/zh-CN.js +1 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +1 -1
  158. package/dist/src/local-first/Evolu.d.ts +12 -12
  159. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  160. package/dist/src/local-first/Evolu.js +2 -2
  161. package/dist/src/local-first/Protocol.d.ts +14 -76
  162. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  163. package/dist/src/local-first/Protocol.js +29 -206
  164. package/dist/src/local-first/Query.d.ts +11 -11
  165. package/dist/src/local-first/Query.js +6 -6
  166. package/dist/src/local-first/Relay.d.ts +2 -2
  167. package/dist/src/local-first/Schema.d.ts +3 -5
  168. package/dist/src/local-first/Schema.d.ts.map +1 -1
  169. package/dist/src/local-first/Schema.js +2 -4
  170. package/dist/src/local-first/Shared.d.ts.map +1 -1
  171. package/dist/src/local-first/Shared.js +5 -5
  172. package/dist/src/local-first/Storage.js +4 -4
  173. package/dist/src/local-first/Timestamp.d.ts +9 -0
  174. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  175. package/dist/src/local-first/Timestamp.js +13 -10
  176. package/package.json +7 -4
  177. package/src/Array.test.ts +809 -0
  178. package/src/Array.ts +21 -21
  179. package/src/Assert.test.ts +1232 -0
  180. package/src/Assert.ts +1023 -227
  181. package/src/BigInt.test.ts +28 -0
  182. package/src/Binary.test.ts +1128 -0
  183. package/src/{Buffer.ts → Binary.ts} +273 -46
  184. package/src/Brand.test.ts +90 -0
  185. package/src/Cache.test.ts +183 -0
  186. package/src/Callbacks.test.ts +66 -0
  187. package/src/Console.test.ts +809 -0
  188. package/src/Console.ts +9 -17
  189. package/src/Crypto.test.ts +138 -0
  190. package/src/Crypto.ts +5 -5
  191. package/src/Eq.test.ts +333 -0
  192. package/src/Eq.ts +110 -176
  193. package/src/Error.test.ts +118 -0
  194. package/src/Function.test.ts +204 -0
  195. package/src/Function.ts +10 -15
  196. package/src/Http.test.ts +816 -0
  197. package/src/Http.ts +6 -7
  198. package/src/LeakDetector.test.ts +210 -0
  199. package/src/LeakDetector.ts +1 -0
  200. package/src/LockManager.ts +2 -2
  201. package/src/Lookup.test.ts +418 -0
  202. package/src/Lookup.ts +4 -3
  203. package/src/Microtask.test.ts +140 -0
  204. package/src/Number.test.ts +218 -0
  205. package/src/Number.ts +2 -2
  206. package/src/Object.test.ts +293 -0
  207. package/src/Object.ts +5 -4
  208. package/src/Option.test.ts +104 -0
  209. package/src/Option.ts +1 -2
  210. package/src/Order.test.ts +56 -0
  211. package/src/Platform.ts +2 -0
  212. package/src/Polyfills.ts +9 -4
  213. package/src/Random.test.ts +89 -0
  214. package/src/Random.ts +1 -1
  215. package/src/Ref.test.ts +139 -0
  216. package/src/RefCount.test.ts +184 -0
  217. package/src/Relation.test.ts +505 -0
  218. package/src/Resource.test.ts +4623 -0
  219. package/src/Result.test.ts +1829 -0
  220. package/src/Result.ts +49 -54
  221. package/src/Schedule.test.ts +1554 -0
  222. package/src/Set.test.ts +279 -0
  223. package/src/Set.ts +6 -6
  224. package/src/Sqlite.ts +1 -1
  225. package/src/Store.test.ts +288 -0
  226. package/src/String.test.ts +54 -0
  227. package/src/String.ts +3 -3
  228. package/src/Task.test.ts +10820 -0
  229. package/src/Task.ts +38 -43
  230. package/src/Test.test.ts +88 -0
  231. package/src/Test.ts +49 -3
  232. package/src/Time.test.ts +926 -0
  233. package/src/Time.ts +13 -21
  234. package/src/Type.base64.test.ts +88 -0
  235. package/src/Type.test.ts +20465 -0
  236. package/src/Type.ts +114 -111
  237. package/src/Types.test.ts +324 -0
  238. package/src/Types.ts +13 -13
  239. package/src/WebSocket.ts +4 -7
  240. package/src/Worker.test.ts +755 -0
  241. package/src/Worker.ts +13 -18
  242. package/src/index.ts +1 -1
  243. package/src/intl/_en.ts +1 -1
  244. package/src/intl/ar.ts +1 -1
  245. package/src/intl/bn.ts +1 -1
  246. package/src/intl/ca.ts +1 -1
  247. package/src/intl/cs.ts +1 -1
  248. package/src/intl/da.ts +1 -1
  249. package/src/intl/de.ts +1 -1
  250. package/src/intl/el.ts +1 -1
  251. package/src/intl/es.ts +1 -1
  252. package/src/intl/fa.ts +1 -1
  253. package/src/intl/fi.ts +1 -1
  254. package/src/intl/fil.ts +1 -1
  255. package/src/intl/fr.ts +1 -1
  256. package/src/intl/he.ts +1 -1
  257. package/src/intl/hi.ts +1 -1
  258. package/src/intl/hr.ts +1 -1
  259. package/src/intl/hu.ts +1 -1
  260. package/src/intl/id.ts +1 -1
  261. package/src/intl/intl.test.ts +3106 -0
  262. package/src/intl/it.ts +1 -1
  263. package/src/intl/ja.ts +1 -1
  264. package/src/intl/ko.ts +1 -1
  265. package/src/intl/ml.ts +1 -1
  266. package/src/intl/mr.ts +1 -1
  267. package/src/intl/ms.ts +1 -1
  268. package/src/intl/nb.ts +1 -1
  269. package/src/intl/nl.ts +1 -1
  270. package/src/intl/pa.ts +1 -1
  271. package/src/intl/pl.ts +1 -1
  272. package/src/intl/pt-BR.ts +1 -1
  273. package/src/intl/pt.ts +1 -1
  274. package/src/intl/ro.ts +1 -1
  275. package/src/intl/sk.ts +1 -1
  276. package/src/intl/sl.ts +1 -1
  277. package/src/intl/sv.ts +1 -1
  278. package/src/intl/sw.ts +1 -1
  279. package/src/intl/ta.ts +1 -1
  280. package/src/intl/te.ts +1 -1
  281. package/src/intl/th.ts +1 -1
  282. package/src/intl/tr.ts +1 -1
  283. package/src/intl/uk.ts +1 -1
  284. package/src/intl/ur.ts +1 -1
  285. package/src/intl/vi.ts +1 -1
  286. package/src/intl/zh-CN.ts +1 -1
  287. package/src/intl/zh-TW.ts +1 -1
  288. package/src/local-first/Evolu.test.ts +1921 -0
  289. package/src/local-first/Evolu.ts +18 -14
  290. package/src/local-first/Owner.test.ts +83 -0
  291. package/src/local-first/Protocol.test.ts +1037 -0
  292. package/src/local-first/Protocol.ts +58 -261
  293. package/src/local-first/Query.test.ts +349 -0
  294. package/src/local-first/Query.ts +11 -11
  295. package/src/local-first/Relay.ts +2 -2
  296. package/src/local-first/Schema.ts +3 -5
  297. package/src/local-first/Shared.test.ts +1828 -0
  298. package/src/local-first/Shared.ts +9 -8
  299. package/src/local-first/Storage.ts +4 -4
  300. package/src/local-first/Timestamp.test.ts +374 -0
  301. package/src/local-first/Timestamp.ts +22 -10
  302. package/dist/src/Buffer.d.ts.map +0 -1
@@ -0,0 +1,1554 @@
1
+ import { describe, it } from "node:test";
2
+ import {
3
+ assertEqual,
4
+ assertOk,
5
+ assertThrowsInstanceOf,
6
+ assertTrue,
7
+ } from "./Assert.ts";
8
+
9
+ import type { RandomNumber } from "./Random.ts";
10
+ import type { NextResult } from "./Result.ts";
11
+ import { done, err, ok } from "./Result.ts";
12
+ import type { Schedule } from "./Schedule.ts";
13
+ import {
14
+ addDelay,
15
+ always,
16
+ collectAllScheduleOutputs,
17
+ collectScheduleInputs,
18
+ collectUntilScheduleOutput,
19
+ collectWhileScheduleOutput,
20
+ compensate,
21
+ delayed,
22
+ delays,
23
+ during,
24
+ elapsed,
25
+ exponential,
26
+ fibonacci,
27
+ fixed,
28
+ foldSchedule,
29
+ forever,
30
+ fromDelay,
31
+ fromDelays,
32
+ intersectSchedules,
33
+ jitter,
34
+ linear,
35
+ mapSchedule,
36
+ maxDelay,
37
+ maxElapsed,
38
+ modifyDelay,
39
+ once,
40
+ passthrough,
41
+ recurs,
42
+ repetitions,
43
+ resetScheduleAfter,
44
+ retryStrategyAws,
45
+ sequenceSchedules,
46
+ spaced,
47
+ take,
48
+ tapScheduleInput,
49
+ tapScheduleOutput,
50
+ unfoldSchedule,
51
+ unionSchedules,
52
+ untilScheduleInput,
53
+ untilScheduleOutput,
54
+ whenInput,
55
+ whileScheduleInput,
56
+ whileScheduleOutput,
57
+ windowed,
58
+ } from "./Schedule.ts";
59
+ import { testCreateDeps } from "./Task.ts";
60
+ import {
61
+ maxMillis,
62
+ Millis,
63
+ minMillis,
64
+ PositiveMillis,
65
+ testCreateTime,
66
+ } from "./Time.ts";
67
+ import { type DateIso, NonNegativeInt, Ratio } from "./Type.ts";
68
+
69
+ // Helper to create scheduleDeps with controllable time
70
+ const createScheduleDeps = (startAt = 0) => {
71
+ const deps = testCreateDeps();
72
+ const time = testCreateTime({ startAt: Millis.orThrow(startAt) });
73
+ return { ...deps, time };
74
+ };
75
+
76
+ const createScheduleDepsWithNow = (...times: ReadonlyArray<number>) => {
77
+ const deps = testCreateDeps();
78
+ let index = 0;
79
+ const time = testCreateTime({ startAt: Millis.orThrow(times[0] ?? 0) });
80
+ function now(): Millis;
81
+ function now(type: "DateIso"): DateIso;
82
+ function now(type?: "DateIso"): Millis | DateIso {
83
+ if (type === "DateIso") return time.now(type);
84
+ return Millis.orThrow(times[Math.min(index++, times.length - 1)]);
85
+ }
86
+ return {
87
+ ...deps,
88
+ time: {
89
+ ...time,
90
+ now,
91
+ },
92
+ };
93
+ };
94
+
95
+ const createScheduleDepsWithRandom = (value: RandomNumber) => ({
96
+ ...createScheduleDeps(),
97
+ random: { next: () => value },
98
+ });
99
+
100
+ const expectOk = (
101
+ result: NextResult<readonly [unknown, Millis]>,
102
+ [output, delay]: readonly [unknown, number],
103
+ ): void => {
104
+ assertOk(result);
105
+ if (!result.ok) return;
106
+ assertEqual(result.value[0], output);
107
+ assertEqual(result.value[1], Millis.orThrow(delay));
108
+ };
109
+
110
+ const expectDone = (result: NextResult<readonly [unknown, Millis]>): void => {
111
+ assertEqual(result, err(done()));
112
+ };
113
+
114
+ describe("Schedule", () => {
115
+ it("Output is covariant (out)", () => {
116
+ const deps = createScheduleDeps();
117
+ // Substitutability by output: a schedule producing more info can be used where
118
+ // less info is required.
119
+ interface RetryInfo {
120
+ readonly attempt: number;
121
+ readonly delay: Millis;
122
+ }
123
+
124
+ // Create a schedule that outputs detailed info
125
+ const detailedSchedule: Schedule<RetryInfo> = () => {
126
+ let attempt = 0;
127
+ return () => {
128
+ attempt++;
129
+ const delay = Millis.orThrow(100 * attempt);
130
+ return ok([{ attempt, delay }, delay]);
131
+ };
132
+ };
133
+
134
+ // Create a schedule that outputs less detailed info
135
+ const lessDetailedSchedule: Schedule<{ readonly attempt: number }> = () => {
136
+ let attempt = 0;
137
+ return () => {
138
+ attempt++;
139
+ const delay = Millis.orThrow(100 * attempt);
140
+ return ok([{ attempt }, delay]);
141
+ };
142
+ };
143
+
144
+ // ✓ Allowed: detailed output → less detailed output
145
+ const useSchedule = (s: Schedule<{ readonly attempt: number }>) => {
146
+ const step = s(deps);
147
+ return step(undefined);
148
+ };
149
+ const result = useSchedule(detailedSchedule);
150
+ assertOk(result);
151
+ assertEqual(result.ok ? result.value[0].attempt : null, 1);
152
+
153
+ // ✗ Not assignable in the other direction (output is missing fields).
154
+ // This is a structural type error; that's the expected mechanism.
155
+ // @ts-expect-error - Output lacks "delay"
156
+ const _invalid: Schedule<RetryInfo> = lessDetailedSchedule;
157
+ });
158
+
159
+ it("Input is contravariant (in)", () => {
160
+ const deps = createScheduleDeps();
161
+ // Substitutability by input: a schedule that accepts broader inputs can be
162
+ // used where narrower inputs are provided.
163
+ interface HttpError {
164
+ readonly status: number;
165
+ readonly message: string;
166
+ }
167
+
168
+ interface RateLimitError extends HttpError {
169
+ readonly retryAfter: number;
170
+ }
171
+
172
+ // spaced("1s") accepts unknown input - it doesn't inspect input at all
173
+ const genericSchedule: Schedule<Millis> = spaced("1s");
174
+
175
+ // ✓ Allowed: broader input → narrower input
176
+ const retryWithSchedule = (
177
+ error: RateLimitError,
178
+ schedule: Schedule<Millis, RateLimitError>,
179
+ ) => {
180
+ const step = schedule(deps);
181
+ return step(error);
182
+ };
183
+
184
+ const error: RateLimitError = {
185
+ status: 429,
186
+ message: "Too many",
187
+ retryAfter: 60,
188
+ };
189
+ const result = retryWithSchedule(error, genericSchedule);
190
+ assertOk(result);
191
+ assertEqual(result.ok ? result.value[1] : null, 1000);
192
+
193
+ // ✗ Not assignable in the other direction (schedule expects RateLimitError).
194
+ const specificSchedule: Schedule<Millis, RateLimitError> = whenInput<
195
+ RateLimitError,
196
+ Millis
197
+ >(
198
+ (e) => e.retryAfter > 0,
199
+ spaced("5s"),
200
+ )(spaced("1s"));
201
+ // @ts-expect-error - Input is too narrow
202
+ const _invalid: Schedule<Millis, HttpError> = specificSchedule;
203
+ });
204
+ });
205
+
206
+ describe("forever", () => {
207
+ it("returns attempt count and 0 delay", () => {
208
+ const deps = createScheduleDeps();
209
+ const step = forever(deps);
210
+ expectOk(step(undefined), [0, 0]);
211
+ expectOk(step(undefined), [1, 0]);
212
+ expectOk(step(undefined), [2, 0]);
213
+ });
214
+ });
215
+
216
+ describe("once", () => {
217
+ it("runs exactly once", () => {
218
+ const deps = createScheduleDeps();
219
+ const step = once(deps);
220
+ expectOk(step(undefined), [0, 0]);
221
+ expectDone(step(undefined));
222
+ });
223
+ });
224
+
225
+ describe("recurs", () => {
226
+ it("limits repetitions", () => {
227
+ const deps = createScheduleDeps();
228
+ const step = recurs(3)(deps);
229
+ expectOk(step(undefined), [0, 0]);
230
+ expectOk(step(undefined), [1, 0]);
231
+ expectOk(step(undefined), [2, 0]);
232
+ expectDone(step(undefined));
233
+ });
234
+ });
235
+
236
+ describe("spaced", () => {
237
+ it("returns constant delay", () => {
238
+ const deps = createScheduleDeps();
239
+ const step = spaced("100ms")(deps);
240
+ expectOk(step(undefined), [100, 100]);
241
+ expectOk(step(undefined), [100, 100]);
242
+ expectOk(step(undefined), [100, 100]);
243
+ });
244
+ });
245
+
246
+ describe("exponential", () => {
247
+ it("grows by factor", () => {
248
+ const deps = createScheduleDeps();
249
+ const step = exponential("100ms")(deps);
250
+ expectOk(step(undefined), [100, 100]);
251
+ expectOk(step(undefined), [200, 200]);
252
+ expectOk(step(undefined), [400, 400]);
253
+ expectOk(step(undefined), [800, 800]);
254
+ });
255
+
256
+ it("with custom factor", () => {
257
+ const deps = createScheduleDeps();
258
+ const step = exponential("100ms", 3)(deps);
259
+ expectOk(step(undefined), [100, 100]);
260
+ expectOk(step(undefined), [300, 300]);
261
+ expectOk(step(undefined), [900, 900]);
262
+ });
263
+
264
+ it("with fractional factor rounds to millis", () => {
265
+ const deps = createScheduleDeps();
266
+ const step = exponential("100ms", 1.5)(deps);
267
+ expectOk(step(undefined), [100, 100]);
268
+ expectOk(step(undefined), [150, 150]);
269
+ expectOk(step(undefined), [225, 225]);
270
+ expectOk(step(undefined), [338, 338]);
271
+ });
272
+
273
+ it("states are independent (stateful)", () => {
274
+ const deps = createScheduleDeps();
275
+ const schedule = exponential("100ms");
276
+
277
+ const step1 = schedule(deps);
278
+ const step2 = schedule(deps);
279
+
280
+ // Each step has its own data
281
+ expectOk(step1(undefined), [100, 100]);
282
+ expectOk(step1(undefined), [200, 200]);
283
+
284
+ // step2 starts fresh
285
+ expectOk(step2(undefined), [100, 100]);
286
+ expectOk(step2(undefined), [200, 200]);
287
+
288
+ // step1 continues from its data
289
+ expectOk(step1(undefined), [400, 400]);
290
+ });
291
+
292
+ it("saturates at maxMillis instead of throwing on overflow", () => {
293
+ const deps = createScheduleDeps();
294
+ const step = exponential("100ms")(deps);
295
+ // Advance to attempt 43 where 100 * 2^42 > maxMillis
296
+ for (let i = 0; i < 42; i++) step(undefined);
297
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
298
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
299
+ });
300
+
301
+ it("keeps a zero base at zero after the power overflows", () => {
302
+ const step = exponential(minMillis)(createScheduleDeps());
303
+ for (let i = 0; i < 1025; i++) step(undefined);
304
+ assertEqual(step(undefined), ok([minMillis, minMillis]));
305
+ });
306
+
307
+ it("throws for invalid factor", () => {
308
+ const nanError = assertThrowsInstanceOf(
309
+ () => exponential("100ms", Number.NaN),
310
+ Error,
311
+ );
312
+ assertTrue(nanError.message.includes("Expected NonNegative."));
313
+ const infinityError = assertThrowsInstanceOf(
314
+ () => exponential("100ms", Number.POSITIVE_INFINITY),
315
+ Error,
316
+ );
317
+ assertTrue(infinityError.message.includes("Expected NonNegative."));
318
+ const negativeError = assertThrowsInstanceOf(
319
+ () => exponential("100ms", -1),
320
+ Error,
321
+ );
322
+ assertTrue(negativeError.message.includes("Expected NonNegative."));
323
+ });
324
+ });
325
+
326
+ describe("linear", () => {
327
+ it("grows linearly", () => {
328
+ const deps = createScheduleDeps();
329
+ const step = linear("100ms")(deps);
330
+ expectOk(step(undefined), [100, 100]);
331
+ expectOk(step(undefined), [200, 200]);
332
+ expectOk(step(undefined), [300, 300]);
333
+ expectOk(step(undefined), [400, 400]);
334
+ });
335
+
336
+ it("saturates at maxMillis instead of throwing on overflow", () => {
337
+ const deps = createScheduleDeps();
338
+ const step = linear(maxMillis)(deps);
339
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
340
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
341
+ });
342
+ });
343
+
344
+ describe("fibonacci", () => {
345
+ it("grows by fibonacci sequence", () => {
346
+ const deps = createScheduleDeps();
347
+ const step = fibonacci("100ms")(deps);
348
+ // F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(6)=8
349
+ // 100 * 1
350
+ expectOk(step(undefined), [100, 100]);
351
+ // 100 * 1
352
+ expectOk(step(undefined), [100, 100]);
353
+ // 100 * 2
354
+ expectOk(step(undefined), [200, 200]);
355
+ // 100 * 3
356
+ expectOk(step(undefined), [300, 300]);
357
+ // 100 * 5
358
+ expectOk(step(undefined), [500, 500]);
359
+ // 100 * 8
360
+ expectOk(step(undefined), [800, 800]);
361
+ });
362
+
363
+ it("saturates at maxMillis instead of throwing after max Fibonacci index", () => {
364
+ const deps = createScheduleDeps();
365
+ const step = fibonacci("1ms")(deps);
366
+ for (let i = 0; i < 78; i++) step(undefined);
367
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
368
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
369
+ });
370
+ });
371
+
372
+ describe("fixed", () => {
373
+ it("aligns to window boundaries", () => {
374
+ const deps = createScheduleDeps();
375
+ const step = fixed("10s")(deps);
376
+
377
+ // First execution at T=0, outputs count 0, wait 10s to align to first window
378
+ expectOk(step(undefined), [0, 10000]);
379
+
380
+ // Second at T=13 (3s into second window), outputs count 1
381
+ // Next window boundary is at T=20, so wait 7s
382
+ deps.time.advance("13s");
383
+ expectOk(step(undefined), [1, 7000]);
384
+
385
+ // Third at T=27 (7s into third window), outputs count 2
386
+ // Next window boundary is at T=30, so wait 3s
387
+ // now at 27s
388
+ deps.time.advance("14s");
389
+ expectOk(step(undefined), [2, 3000]);
390
+
391
+ // Fourth at T=35 (5s into fourth window), outputs count 3
392
+ // Next window boundary is at T=40, so wait 5s
393
+ // now at 35s
394
+ deps.time.advance("8s");
395
+ expectOk(step(undefined), [3, 5000]);
396
+ });
397
+
398
+ it("handles running behind (execution > interval)", () => {
399
+ const deps = createScheduleDeps();
400
+ const step = fixed("10s")(deps);
401
+
402
+ // First execution starts
403
+ expectOk(step(undefined), [0, 10000]);
404
+
405
+ // Execution took 25s, now at T=25 (should have run at T=10, T=20)
406
+ // Running behind: delay is 0, but next boundary is T=30
407
+ deps.time.advance("25s");
408
+ expectOk(step(undefined), [1, 0]);
409
+
410
+ // Now at T=28, not running behind anymore, align to T=30
411
+ deps.time.advance("3s");
412
+ expectOk(step(undefined), [2, 2000]);
413
+ });
414
+
415
+ it("catches up missed recurrences before realigning", () => {
416
+ const deps = createScheduleDeps();
417
+ const step = fixed("10s")(deps);
418
+
419
+ expectOk(step(undefined), [0, 10000]);
420
+ deps.time.advance("55s");
421
+ expectOk(step(undefined), [1, 0]);
422
+ expectOk(step(undefined), [2, 0]);
423
+ expectOk(step(undefined), [3, 0]);
424
+ expectOk(step(undefined), [4, 0]);
425
+ expectOk(step(undefined), [5, 5000]);
426
+ });
427
+
428
+ it("with zero interval", () => {
429
+ const deps = createScheduleDeps();
430
+ const step = fixed(minMillis)(deps);
431
+ expectOk(step(undefined), [0, 0]);
432
+ deps.time.advance("100ms");
433
+ expectOk(step(undefined), [1, 0]);
434
+ deps.time.advance("100ms");
435
+ expectOk(step(undefined), [2, 0]);
436
+ });
437
+ });
438
+
439
+ describe("windowed", () => {
440
+ it("skips missed recurrences before realigning", () => {
441
+ const deps = createScheduleDeps();
442
+ const step = windowed("10s")(deps);
443
+
444
+ expectOk(step(undefined), [0, 10000]);
445
+ deps.time.advance("55s");
446
+ expectOk(step(undefined), [1, 5000]);
447
+ });
448
+
449
+ it("waits until the next interval at an exact boundary", () => {
450
+ const deps = createScheduleDeps();
451
+ const step = windowed("100ms")(deps);
452
+
453
+ expectOk(step(undefined), [0, 100]);
454
+ deps.time.advance("100ms");
455
+ expectOk(step(undefined), [1, 100]);
456
+ });
457
+
458
+ it("with zero interval", () => {
459
+ const deps = createScheduleDeps();
460
+ const step = windowed(minMillis)(deps);
461
+ expectOk(step(undefined), [0, 0]);
462
+ deps.time.advance("100ms");
463
+ expectOk(step(undefined), [1, 0]);
464
+ deps.time.advance("100ms");
465
+ expectOk(step(undefined), [2, 0]);
466
+ });
467
+
468
+ it("aligns to window boundaries", () => {
469
+ const schedule = windowed("100ms");
470
+ const deps = createScheduleDeps();
471
+
472
+ // At elapsed=0, wait full 100ms to next boundary
473
+ const step = schedule(deps);
474
+ expectOk(step(undefined), [0, 100]);
475
+ // At elapsed=30, wait 70ms to next boundary
476
+ deps.time.advance("30ms");
477
+ expectOk(step(undefined), [1, 70]);
478
+ // At elapsed=150 (30 + 120 more), wait 50ms to next boundary (200ms)
479
+ deps.time.advance("120ms");
480
+ expectOk(step(undefined), [2, 50]);
481
+ });
482
+ });
483
+
484
+ describe("fromDelay", () => {
485
+ it("creates single-delay schedule", () => {
486
+ const deps = createScheduleDeps();
487
+ const schedule = fromDelay("500ms");
488
+ const step = schedule(deps);
489
+
490
+ expectOk(step(undefined), [500, 500]);
491
+ expectDone(step(undefined));
492
+ });
493
+ });
494
+
495
+ describe("fromDelays", () => {
496
+ it("creates sequence of delays", () => {
497
+ const deps = createScheduleDeps();
498
+ const schedule = fromDelays("100ms", "500ms", "2s");
499
+ const step = schedule(deps);
500
+
501
+ expectOk(step(undefined), [100, 100]);
502
+ expectOk(step(undefined), [500, 500]);
503
+ expectOk(step(undefined), [2000, 2000]);
504
+ expectDone(step(undefined));
505
+ });
506
+
507
+ it("stops immediately with no delays", () => {
508
+ const step = fromDelays()(createScheduleDeps());
509
+
510
+ expectDone(step(undefined));
511
+ });
512
+ });
513
+
514
+ describe("elapsed", () => {
515
+ it("outputs total elapsed time", () => {
516
+ const deps = createScheduleDeps();
517
+ const step = elapsed(deps);
518
+
519
+ expectOk(step(undefined), [0, 0]);
520
+ deps.time.advance("100ms");
521
+ expectOk(step(undefined), [100, 0]);
522
+ deps.time.advance("400ms");
523
+ expectOk(step(undefined), [500, 0]);
524
+ deps.time.advance("500ms");
525
+ expectOk(step(undefined), [1000, 0]);
526
+ });
527
+
528
+ it("returns zero elapsed time when time moves backwards", () => {
529
+ const deps = createScheduleDepsWithNow(100, 50);
530
+ const step = elapsed(deps);
531
+
532
+ expectOk(step(undefined), [0, 0]);
533
+ expectOk(step(undefined), [0, 0]);
534
+ });
535
+
536
+ it("combined with other schedule", () => {
537
+ const deps = createScheduleDeps();
538
+ const step = intersectSchedules(take(3)(spaced("100ms")), elapsed)(deps);
539
+
540
+ const result1 = step(undefined);
541
+ assertOk(result1);
542
+ // spaced output
543
+ assertEqual(result1.ok ? result1.value[0][0] : null, 100);
544
+ // elapsed output
545
+ assertEqual(result1.ok ? result1.value[0][1] : null, 0);
546
+ // max delay
547
+ assertEqual(result1.ok ? result1.value[1] : null, 100);
548
+
549
+ deps.time.advance("150ms");
550
+ const result2 = step(undefined);
551
+ assertOk(result2);
552
+ assertEqual(result2.ok ? result2.value[0][0] : null, 100);
553
+ assertEqual(result2.ok ? result2.value[0][1] : null, 150);
554
+
555
+ deps.time.advance("150ms");
556
+ const result3 = step(undefined);
557
+ assertOk(result3);
558
+ assertEqual(result3.ok ? result3.value[0][0] : null, 100);
559
+ assertEqual(result3.ok ? result3.value[0][1] : null, 300);
560
+
561
+ deps.time.advance("100ms");
562
+ // take(3) exhausted
563
+ expectDone(step(undefined));
564
+ });
565
+ });
566
+
567
+ describe("during", () => {
568
+ it("runs for specified duration then stops", () => {
569
+ const schedule = during("100ms");
570
+ const deps = createScheduleDeps();
571
+ const step = schedule(deps);
572
+
573
+ expectOk(step(undefined), [0, 0]);
574
+ deps.time.advance("50ms");
575
+ expectOk(step(undefined), [50, 0]);
576
+ deps.time.advance("50ms");
577
+ // exactly at limit
578
+ expectOk(step(undefined), [100, 0]);
579
+ deps.time.advance("1ms");
580
+ // over limit
581
+ expectDone(step(undefined));
582
+ });
583
+ });
584
+
585
+ describe("always", () => {
586
+ it("always outputs constant value", () => {
587
+ const deps = createScheduleDeps();
588
+ const step = take(3)(always("retry"))(deps);
589
+ expectOk(step(undefined), ["retry", 0]);
590
+ expectOk(step(undefined), ["retry", 0]);
591
+ expectOk(step(undefined), ["retry", 0]);
592
+ expectDone(step(undefined));
593
+ });
594
+ });
595
+
596
+ describe("unfoldSchedule", () => {
597
+ it("creates schedule from state function", () => {
598
+ const deps = createScheduleDeps();
599
+ // Simple counter
600
+ const step = take(4)(unfoldSchedule(0, (n) => n + 1))(deps);
601
+
602
+ expectOk(step(undefined), [0, 0]);
603
+ expectOk(step(undefined), [1, 0]);
604
+ expectOk(step(undefined), [2, 0]);
605
+ expectOk(step(undefined), [3, 0]);
606
+ expectDone(step(undefined));
607
+ });
608
+
609
+ it("with custom state transformation", () => {
610
+ const deps = createScheduleDeps();
611
+ // Multiply by 2 each time
612
+ const step = take(4)(unfoldSchedule(1, (n) => n * 2))(deps);
613
+
614
+ expectOk(step(undefined), [1, 0]);
615
+ expectOk(step(undefined), [2, 0]);
616
+ expectOk(step(undefined), [4, 0]);
617
+ expectOk(step(undefined), [8, 0]);
618
+ });
619
+
620
+ it("with object state", () => {
621
+ const deps = createScheduleDeps();
622
+ interface Phase {
623
+ readonly name: string;
624
+ readonly count: number;
625
+ }
626
+
627
+ const nextPhase = (phase: Phase): Phase => ({
628
+ name: phase.count < 2 ? "warmup" : "active",
629
+ count: phase.count + 1,
630
+ });
631
+
632
+ const step = take(4)(unfoldSchedule({ name: "init", count: 0 }, nextPhase))(
633
+ deps,
634
+ );
635
+
636
+ expectOk(step(undefined), [{ name: "init", count: 0 }, 0]);
637
+ expectOk(step(undefined), [{ name: "warmup", count: 1 }, 0]);
638
+ expectOk(step(undefined), [{ name: "warmup", count: 2 }, 0]);
639
+ expectOk(step(undefined), [{ name: "active", count: 3 }, 0]);
640
+ });
641
+ });
642
+
643
+ describe("take", () => {
644
+ it("limits attempts", () => {
645
+ const deps = createScheduleDeps();
646
+ const step = take(3)(exponential("100ms"))(deps);
647
+ expectOk(step(undefined), [100, 100]);
648
+ expectOk(step(undefined), [200, 200]);
649
+ expectOk(step(undefined), [400, 400]);
650
+ expectDone(step(undefined));
651
+ });
652
+
653
+ it("accepts literals from 0 to 100 or a validated NonNegativeInt", () => {
654
+ take(0);
655
+ take(100);
656
+ take(NonNegativeInt.orThrow(101));
657
+ });
658
+
659
+ it("requires validation for other numbers", () => {
660
+ // @ts-expect-error - Dynamic numbers require validation.
661
+ take(Math.random());
662
+ // @ts-expect-error - Literals above 100 require validation.
663
+ take(101);
664
+ // @ts-expect-error - Negative integers are invalid.
665
+ take(-1);
666
+ // @ts-expect-error - Fractions are invalid.
667
+ take(1.5);
668
+ });
669
+ });
670
+
671
+ describe("maxElapsed", () => {
672
+ it("stops after duration", () => {
673
+ const deps = createScheduleDeps();
674
+ const step = maxElapsed("250ms")(exponential("100ms"))(deps);
675
+ expectOk(step(undefined), [100, 100]);
676
+ deps.time.advance("100ms");
677
+ expectOk(step(undefined), [200, 200]);
678
+ // now at 250ms
679
+ deps.time.advance("150ms");
680
+ // elapsed >= 250
681
+ expectDone(step(undefined));
682
+ });
683
+
684
+ it("keeps terminal done when time moves backwards", () => {
685
+ const step = maxElapsed("250ms")(exponential("100ms"))(
686
+ createScheduleDepsWithNow(0, 250, 0),
687
+ );
688
+
689
+ expectOk(step(undefined), [100, 100]);
690
+ expectDone(step(undefined));
691
+ expectDone(step(undefined));
692
+ });
693
+ });
694
+
695
+ describe("maxDelay", () => {
696
+ it("caps delay", () => {
697
+ const deps = createScheduleDeps();
698
+ const step = maxDelay("300ms")(exponential("100ms"))(deps);
699
+ expectOk(step(undefined), [100, 100]);
700
+ expectOk(step(undefined), [200, 200]);
701
+ // capped
702
+ expectOk(step(undefined), [400, 300]);
703
+ // capped
704
+ expectOk(step(undefined), [800, 300]);
705
+ });
706
+
707
+ it("maxDelay preserves done", () => {
708
+ const deps = createScheduleDeps();
709
+ const step = maxDelay("300ms")(once)(deps);
710
+ expectOk(step(undefined), [0, 0]);
711
+ expectDone(step(undefined));
712
+ });
713
+ });
714
+
715
+ describe("jitter", () => {
716
+ it("0% preserves delay", () => {
717
+ const deps = createScheduleDeps();
718
+ const step = jitter("0%")(spaced("100ms"))(deps);
719
+
720
+ expectOk(step(undefined), [100, 100]);
721
+ });
722
+
723
+ it("accepts Ratio", () => {
724
+ jitter(Ratio.orThrow(1));
725
+ });
726
+
727
+ describe("below", () => {
728
+ it("defaults to 50% below jitter", () => {
729
+ const result = jitter()(spaced("100ms"))(
730
+ createScheduleDepsWithRandom(0.5 as RandomNumber),
731
+ )(undefined);
732
+ const explicitResult = jitter("50%", "below")(spaced("100ms"))(
733
+ createScheduleDepsWithRandom(0.5 as RandomNumber),
734
+ )(undefined);
735
+
736
+ assertEqual(result, explicitResult);
737
+ expectOk(result, [100, 75]);
738
+ });
739
+
740
+ it("50% applies equal jitter", () => {
741
+ expectOk(
742
+ jitter("50%")(spaced("100ms"))(
743
+ createScheduleDepsWithRandom(0 as RandomNumber),
744
+ )(undefined),
745
+ [100, 50],
746
+ );
747
+ expectOk(
748
+ jitter("50%", "below")(spaced("100ms"))(
749
+ createScheduleDepsWithRandom(0.5 as RandomNumber),
750
+ )(undefined),
751
+ [100, 75],
752
+ );
753
+ expectOk(
754
+ jitter("50%")(spaced("100ms"))(
755
+ createScheduleDepsWithRandom(0.999999999 as RandomNumber),
756
+ )(undefined),
757
+ [100, 100],
758
+ );
759
+ });
760
+
761
+ it("100% applies full jitter", () => {
762
+ expectOk(
763
+ jitter("100%")(spaced("100ms"))(
764
+ createScheduleDepsWithRandom(0 as RandomNumber),
765
+ )(undefined),
766
+ [100, 0],
767
+ );
768
+ expectOk(
769
+ jitter("100%")(spaced("100ms"))(
770
+ createScheduleDepsWithRandom(0.5 as RandomNumber),
771
+ )(undefined),
772
+ [100, 50],
773
+ );
774
+ expectOk(
775
+ jitter("100%")(spaced("100ms"))(
776
+ createScheduleDepsWithRandom(0.999999999 as RandomNumber),
777
+ )(undefined),
778
+ [100, 100],
779
+ );
780
+ });
781
+ });
782
+
783
+ describe("around", () => {
784
+ it("randomizes around the original delay", () => {
785
+ expectOk(
786
+ jitter("50%", "around")(spaced("100ms"))(
787
+ createScheduleDepsWithRandom(0 as RandomNumber),
788
+ )(undefined),
789
+ [100, 50],
790
+ );
791
+ expectOk(
792
+ jitter("50%", "around")(spaced("100ms"))(
793
+ createScheduleDepsWithRandom(0.5 as RandomNumber),
794
+ )(undefined),
795
+ [100, 100],
796
+ );
797
+ expectOk(
798
+ jitter("50%", "around")(spaced("100ms"))(
799
+ createScheduleDepsWithRandom(0.999999999 as RandomNumber),
800
+ )(undefined),
801
+ [100, 150],
802
+ );
803
+ });
804
+
805
+ it("saturates at maxMillis instead of throwing on overflow", () => {
806
+ const step = jitter("100%", "around")(spaced(maxMillis))(
807
+ createScheduleDepsWithRandom(0.999999999 as RandomNumber),
808
+ );
809
+ assertEqual(step(undefined), ok([maxMillis, maxMillis]));
810
+ });
811
+ });
812
+
813
+ it("jitter preserves done", () => {
814
+ const deps = createScheduleDeps();
815
+ const step = jitter("50%")(once)(deps);
816
+ expectOk(step(undefined), [0, 0]);
817
+ expectDone(step(undefined));
818
+ });
819
+ });
820
+
821
+ describe("delayed", () => {
822
+ it("replaces the first delay", () => {
823
+ const deps = createScheduleDeps();
824
+ const step = delayed("500ms")(exponential("100ms"))(deps);
825
+ // Initial delay
826
+ expectOk(step(undefined), [100, 500]);
827
+ // Normal delays
828
+ expectOk(step(undefined), [200, 200]);
829
+ expectOk(step(undefined), [400, 400]);
830
+ });
831
+
832
+ it("does not override termination", () => {
833
+ const deps = createScheduleDeps();
834
+ const step = delayed("500ms")(take(0)(forever))(deps);
835
+ expectDone(step(undefined));
836
+ });
837
+ });
838
+
839
+ describe("addDelay", () => {
840
+ it("adds fixed delay to schedule", () => {
841
+ const deps = createScheduleDeps();
842
+ const schedule = addDelay("500ms")(exponential("100ms"));
843
+ const step = schedule(deps);
844
+
845
+ const result1 = step(undefined);
846
+ assertOk(result1);
847
+ // 100 + 500
848
+ assertEqual(result1.ok ? result1.value[1] : null, 600);
849
+
850
+ const result2 = step(undefined);
851
+ assertOk(result2);
852
+ // 200 + 500
853
+ assertEqual(result2.ok ? result2.value[1] : null, 700);
854
+
855
+ const result3 = step(undefined);
856
+ assertOk(result3);
857
+ // 400 + 500
858
+ assertEqual(result3.ok ? result3.value[1] : null, 900);
859
+ });
860
+ });
861
+
862
+ describe("modifyDelay", () => {
863
+ it("transforms delays", () => {
864
+ const deps = createScheduleDeps();
865
+ // Double all delays
866
+ const step = modifyDelay((d) => d * 2)(exponential("100ms"))(deps);
867
+ expectOk(step(undefined), [100, 200]);
868
+ expectOk(step(undefined), [200, 400]);
869
+ expectOk(step(undefined), [400, 800]);
870
+ });
871
+
872
+ it("passes through termination", () => {
873
+ const deps = createScheduleDeps();
874
+ const step = modifyDelay(() => 123)(take(1)(forever))(deps);
875
+ expectOk(step(undefined), [0, 123]);
876
+ expectDone(step(undefined));
877
+ });
878
+
879
+ it("saturates at maxMillis instead of throwing on overflow", () => {
880
+ const deps = createScheduleDeps();
881
+ const step = modifyDelay(() => maxMillis + 1)(once)(deps);
882
+ assertEqual(step(undefined), ok([0, maxMillis]));
883
+ });
884
+
885
+ it("throws when the transform returns NaN", () => {
886
+ const step = modifyDelay(() => Number.NaN)(once)(createScheduleDeps());
887
+ const error = assertThrowsInstanceOf(() => step(undefined), Error);
888
+ assertTrue(error.message.includes("Expected NonNaN."));
889
+ });
890
+ });
891
+
892
+ describe("compensate", () => {
893
+ it("subtracts execution time, not previous sleep", () => {
894
+ const deps = createScheduleDeps();
895
+ const step = compensate(spaced("1s"))(deps);
896
+
897
+ expectOk(step(undefined), [1000, 1000]);
898
+
899
+ // Simulate: slept 1000ms, then execution took 200ms.
900
+ deps.time.advance("1.2s");
901
+ expectOk(step(undefined), [1000, 800]);
902
+ });
903
+
904
+ it("keeps full delay until previous delay elapses", () => {
905
+ const deps = createScheduleDeps();
906
+ const step = compensate(spaced("1s"))(deps);
907
+ // First attempt at T=0, no previous → full delay
908
+ expectOk(step(undefined), [1000, 1000]);
909
+ // Second at T=200, before the previous 1000ms delay could have elapsed
910
+ deps.time.advance("200ms");
911
+ expectOk(step(undefined), [1000, 1000]);
912
+ });
913
+
914
+ it("passes through termination", () => {
915
+ const deps = createScheduleDeps();
916
+ const step = compensate(take(1)(spaced("1s")))(deps);
917
+ expectOk(step(undefined), [1000, 1000]);
918
+ expectDone(step(undefined));
919
+ });
920
+
921
+ it("keeps full delay when time moves backwards", () => {
922
+ const deps = createScheduleDepsWithNow(100, 50);
923
+ const step = compensate(spaced("1s"))(deps);
924
+
925
+ expectOk(step(undefined), [1000, 1000]);
926
+ expectOk(step(undefined), [1000, 1000]);
927
+ });
928
+ });
929
+
930
+ describe("whileScheduleInput", () => {
931
+ it("continues while predicate is true", () => {
932
+ const deps = createScheduleDeps();
933
+ interface Error {
934
+ readonly type: "Transient" | "Fatal";
935
+ }
936
+ const schedule = whileScheduleInput<Error>((e) => e.type === "Transient")(
937
+ spaced("100ms"),
938
+ );
939
+ const step = schedule(deps);
940
+
941
+ expectOk(step({ type: "Transient" }), [100, 100]);
942
+ expectOk(step({ type: "Transient" }), [100, 100]);
943
+ expectDone(step({ type: "Fatal" }));
944
+ });
945
+
946
+ it("remains done after predicate fails", () => {
947
+ const deps = createScheduleDeps();
948
+ const step = whileScheduleInput((n: number) => n > 0)(forever)(deps);
949
+
950
+ expectOk(step(1), [0, 0]);
951
+ expectDone(step(0));
952
+ expectDone(step(1));
953
+ });
954
+ });
955
+
956
+ describe("untilScheduleInput", () => {
957
+ it("stops when predicate becomes true", () => {
958
+ const deps = createScheduleDeps();
959
+
960
+ interface Error {
961
+ readonly type: "Transient" | "Fatal";
962
+ }
963
+ const schedule = untilScheduleInput<Error>((e) => e.type === "Fatal")(
964
+ spaced("100ms"),
965
+ );
966
+ const step = schedule(deps);
967
+
968
+ expectOk(step({ type: "Transient" }), [100, 100]);
969
+ expectOk(step({ type: "Transient" }), [100, 100]);
970
+ expectDone(step({ type: "Fatal" }));
971
+ });
972
+ });
973
+
974
+ describe("whileScheduleOutput", () => {
975
+ it("continues while predicate is true", () => {
976
+ const deps = createScheduleDeps();
977
+ // Stop when delay exceeds 300ms
978
+ const step = whileScheduleOutput((delay: Millis) => delay <= 300)(
979
+ exponential("100ms"),
980
+ )(deps);
981
+ expectOk(step(undefined), [100, 100]);
982
+ expectOk(step(undefined), [200, 200]);
983
+ // 400 > 300
984
+ expectDone(step(undefined));
985
+ });
986
+
987
+ it("passes through termination", () => {
988
+ const deps = createScheduleDeps();
989
+ const step = whileScheduleOutput(() => true)(take(1)(forever))(deps);
990
+ expectOk(step(undefined), [0, 0]);
991
+ expectDone(step(undefined));
992
+ });
993
+ });
994
+
995
+ describe("untilScheduleOutput", () => {
996
+ it("stops when predicate becomes true", () => {
997
+ const deps = createScheduleDeps();
998
+ // Stop when delay reaches 400ms
999
+ const step = untilScheduleOutput((delay: Millis) => delay >= 400)(
1000
+ exponential("100ms"),
1001
+ )(deps);
1002
+ expectOk(step(undefined), [100, 100]);
1003
+ expectOk(step(undefined), [200, 200]);
1004
+ // 400 >= 400
1005
+ expectDone(step(undefined));
1006
+ });
1007
+
1008
+ it("passes through termination", () => {
1009
+ const deps = createScheduleDeps();
1010
+ const step = untilScheduleOutput(() => false)(take(1)(forever))(deps);
1011
+ expectOk(step(undefined), [0, 0]);
1012
+ expectDone(step(undefined));
1013
+ });
1014
+ });
1015
+
1016
+ describe("predicate filters", () => {
1017
+ it("other predicate stops are sticky", () => {
1018
+ const inputUntilStep = untilScheduleInput((n: number) => n <= 0)(forever)(
1019
+ createScheduleDeps(),
1020
+ );
1021
+ expectOk(inputUntilStep(1), [0, 0]);
1022
+ expectDone(inputUntilStep(0));
1023
+ expectDone(inputUntilStep(1));
1024
+
1025
+ const outputWhileStep = whileScheduleOutput((n: number) => n > 0)(
1026
+ unfoldSchedule(1, (n) => (n === 1 ? 0 : 1)),
1027
+ )(createScheduleDeps());
1028
+ expectOk(outputWhileStep(undefined), [1, 0]);
1029
+ expectDone(outputWhileStep(undefined));
1030
+ expectDone(outputWhileStep(undefined));
1031
+
1032
+ const outputUntilStep = untilScheduleOutput((n: number) => n <= 0)(
1033
+ unfoldSchedule(1, (n) => (n === 1 ? 0 : 1)),
1034
+ )(createScheduleDeps());
1035
+ expectOk(outputUntilStep(undefined), [1, 0]);
1036
+ expectDone(outputUntilStep(undefined));
1037
+ expectDone(outputUntilStep(undefined));
1038
+ });
1039
+ });
1040
+
1041
+ describe("resetScheduleAfter", () => {
1042
+ it("requires a positive millis duration", () => {
1043
+ resetScheduleAfter("1ms");
1044
+ resetScheduleAfter(PositiveMillis.orThrow(1));
1045
+ // @ts-expect-error - Zero Millis is not a positive duration.
1046
+ resetScheduleAfter(minMillis);
1047
+ });
1048
+
1049
+ it("resets a running schedule after inactivity", () => {
1050
+ const deps = createScheduleDeps();
1051
+ const step = resetScheduleAfter("1s")(exponential("100ms"))(deps);
1052
+
1053
+ expectOk(step(undefined), [100, 100]);
1054
+ deps.time.advance("100ms");
1055
+ expectOk(step(undefined), [200, 200]);
1056
+ deps.time.advance("1s");
1057
+ expectOk(step(undefined), [100, 100]);
1058
+ deps.time.advance("100ms");
1059
+ expectOk(step(undefined), [200, 200]);
1060
+ });
1061
+
1062
+ it("keeps terminal done after inactivity", () => {
1063
+ const deps = createScheduleDeps();
1064
+ const step = resetScheduleAfter("1s")(take(1)(spaced("100ms")))(deps);
1065
+
1066
+ expectOk(step(undefined), [100, 100]);
1067
+ expectDone(step(undefined));
1068
+ deps.time.advance("2s");
1069
+ expectDone(step(undefined));
1070
+ });
1071
+ });
1072
+
1073
+ describe("mapSchedule", () => {
1074
+ it("transforms output", () => {
1075
+ const deps = createScheduleDeps();
1076
+ const schedule = mapSchedule((delay: Millis) => ({
1077
+ delay,
1078
+ doubled: delay * 2,
1079
+ }))(exponential("100ms"));
1080
+ const step = schedule(deps);
1081
+
1082
+ const result1 = step(undefined);
1083
+ assertOk(result1);
1084
+ assertEqual(result1.ok ? result1.value[0] : null, {
1085
+ delay: 100,
1086
+ doubled: 200,
1087
+ });
1088
+ // delay unchanged
1089
+ assertEqual(result1.ok ? result1.value[1] : null, 100);
1090
+
1091
+ const result2 = step(undefined);
1092
+ assertOk(result2);
1093
+ assertEqual(result2.ok ? result2.value[0] : null, {
1094
+ delay: 200,
1095
+ doubled: 400,
1096
+ });
1097
+ assertEqual(result2.ok ? result2.value[1] : null, 200);
1098
+ });
1099
+
1100
+ it("passes through termination", () => {
1101
+ const deps = createScheduleDeps();
1102
+ const step = mapSchedule((n: number) => n + 1)(take(1)(forever))(deps);
1103
+ expectOk(step(undefined), [1, 0]);
1104
+ expectDone(step(undefined));
1105
+ });
1106
+ });
1107
+
1108
+ describe("passthrough", () => {
1109
+ it("as constructor outputs input directly", () => {
1110
+ const deps = createScheduleDeps();
1111
+
1112
+ interface MyError {
1113
+ readonly code: number;
1114
+ readonly message: string;
1115
+ }
1116
+
1117
+ const step = passthrough<MyError>()(deps);
1118
+ const error1: MyError = { code: 500, message: "Internal error" };
1119
+ const error2: MyError = { code: 429, message: "Rate limited" };
1120
+
1121
+ const result1 = step(error1);
1122
+ expectOk(result1, [error1, 0]);
1123
+
1124
+ const result2 = step(error2);
1125
+ expectOk(result2, [error2, 0]);
1126
+ });
1127
+
1128
+ it("as combinator preserves timing, replaces output", () => {
1129
+ const deps = createScheduleDeps();
1130
+
1131
+ interface MyError {
1132
+ readonly code: number;
1133
+ }
1134
+
1135
+ const step = passthrough(exponential("100ms"))(deps);
1136
+ const error: MyError = { code: 500 };
1137
+
1138
+ const result1 = step(error);
1139
+ // output is input, delay from exponential
1140
+ expectOk(result1, [error, 100]);
1141
+
1142
+ const result2 = step({ code: 429 });
1143
+ // exponential growth
1144
+ expectOk(result2, [{ code: 429 }, 200]);
1145
+
1146
+ const result3 = step({ code: 503 });
1147
+ expectOk(result3, [{ code: 503 }, 400]);
1148
+ });
1149
+
1150
+ it("combinator respects schedule termination", () => {
1151
+ const deps = createScheduleDeps();
1152
+ const step = passthrough(take(2)(spaced("100ms")))(deps);
1153
+
1154
+ expectOk(step("first"), ["first", 100]);
1155
+ expectOk(step("second"), ["second", 100]);
1156
+ // take(2) exhausted
1157
+ expectDone(step("third"));
1158
+ });
1159
+ });
1160
+
1161
+ describe("foldSchedule", () => {
1162
+ it("accumulates state across iterations", () => {
1163
+ const deps = createScheduleDeps();
1164
+
1165
+ // Track cumulative delay
1166
+ const schedule = foldSchedule(
1167
+ 0,
1168
+ (total: number, delay: Millis) => total + delay,
1169
+ )(take(4)(exponential("100ms")));
1170
+ const step = schedule(deps);
1171
+
1172
+ // 0 + 100
1173
+ expectOk(step(undefined), [100, 100]);
1174
+ // 100 + 200
1175
+ expectOk(step(undefined), [300, 200]);
1176
+ // 300 + 400
1177
+ expectOk(step(undefined), [700, 400]);
1178
+ // 700 + 800
1179
+ expectOk(step(undefined), [1500, 800]);
1180
+ expectDone(step(undefined));
1181
+ });
1182
+ });
1183
+
1184
+ describe("repetitions", () => {
1185
+ it("outputs count instead of original output", () => {
1186
+ const deps = createScheduleDeps();
1187
+
1188
+ const schedule = repetitions(exponential("100ms"));
1189
+ const step = schedule(deps);
1190
+
1191
+ expectOk(step(undefined), [0, 100]);
1192
+ expectOk(step(undefined), [1, 200]);
1193
+ expectOk(step(undefined), [2, 400]);
1194
+ });
1195
+ });
1196
+
1197
+ describe("delays", () => {
1198
+ it("outputs the delay instead of original output", () => {
1199
+ const deps = createScheduleDeps();
1200
+
1201
+ const schedule = delays(exponential("100ms"));
1202
+ const step = schedule(deps);
1203
+
1204
+ expectOk(step(undefined), [100, 100]);
1205
+ expectOk(step(undefined), [200, 200]);
1206
+ expectOk(step(undefined), [400, 400]);
1207
+ });
1208
+
1209
+ it("passes through termination", () => {
1210
+ const deps = createScheduleDeps();
1211
+ const step = delays(take(1)(spaced("100ms")))(deps);
1212
+ expectOk(step(undefined), [100, 100]);
1213
+ expectDone(step(undefined));
1214
+ });
1215
+ });
1216
+
1217
+ describe("collectAllScheduleOutputs", () => {
1218
+ it("accumulates outputs into array", () => {
1219
+ const deps = createScheduleDeps();
1220
+ const schedule = collectAllScheduleOutputs(take(3)(spaced("100ms")));
1221
+ const step = schedule(deps);
1222
+
1223
+ expectOk(step(undefined), [[100], 100]);
1224
+ expectOk(step(undefined), [[100, 100], 100]);
1225
+ expectOk(step(undefined), [[100, 100, 100], 100]);
1226
+ expectDone(step(undefined));
1227
+ });
1228
+
1229
+ it("returns a new snapshot without changing previous outputs", () => {
1230
+ const step = collectAllScheduleOutputs(take(2)(spaced("100ms")))(
1231
+ createScheduleDeps(),
1232
+ );
1233
+
1234
+ const first = step(undefined);
1235
+ expectOk(first, [[100], 100]);
1236
+ expectOk(step(undefined), [[100, 100], 100]);
1237
+ expectOk(first, [[100], 100]);
1238
+ });
1239
+ });
1240
+
1241
+ describe("collectScheduleInputs", () => {
1242
+ it("accumulates inputs into array", () => {
1243
+ const deps = createScheduleDeps();
1244
+ const schedule = collectScheduleInputs(take(3)(spaced("100ms")));
1245
+ const step = schedule(deps);
1246
+
1247
+ expectOk(step("a"), [["a"], 100]);
1248
+ expectOk(step("b"), [["a", "b"], 100]);
1249
+ expectOk(step("c"), [["a", "b", "c"], 100]);
1250
+ expectDone(step("d"));
1251
+ });
1252
+ });
1253
+
1254
+ describe("collectWhileScheduleOutput", () => {
1255
+ it("collects outputs while predicate is true", () => {
1256
+ const deps = createScheduleDeps();
1257
+ const schedule = collectWhileScheduleOutput((delay: Millis) => delay < 400)(
1258
+ exponential("100ms"),
1259
+ );
1260
+ const step = schedule(deps);
1261
+
1262
+ expectOk(step(undefined), [[100], 100]);
1263
+ expectOk(step(undefined), [[100, 200], 200]);
1264
+ // 400 >= 400, predicate fails, stops
1265
+ expectDone(step(undefined));
1266
+ });
1267
+ });
1268
+
1269
+ describe("collectUntilScheduleOutput", () => {
1270
+ it("collects outputs until predicate becomes true", () => {
1271
+ const deps = createScheduleDeps();
1272
+ const schedule = collectUntilScheduleOutput(
1273
+ (delay: Millis) => delay >= 400,
1274
+ )(exponential("100ms"));
1275
+ const step = schedule(deps);
1276
+
1277
+ expectOk(step(undefined), [[100], 100]);
1278
+ expectOk(step(undefined), [[100, 200], 200]);
1279
+ // 400 >= 400, predicate true, stops
1280
+ expectDone(step(undefined));
1281
+ });
1282
+ });
1283
+
1284
+ describe("sequenceSchedules", () => {
1285
+ it("runs schedules in order", () => {
1286
+ const deps = createScheduleDeps();
1287
+
1288
+ const step = sequenceSchedules(
1289
+ take(2)(exponential("100ms")),
1290
+ take(3)(spaced("500ms")),
1291
+ )(deps);
1292
+
1293
+ // First schedule runs
1294
+ expectOk(step(undefined), [100, 100]);
1295
+ expectOk(step(undefined), [200, 200]);
1296
+ // First exhausted, second takes over
1297
+ expectOk(step(undefined), [500, 500]);
1298
+ expectOk(step(undefined), [500, 500]);
1299
+ expectOk(step(undefined), [500, 500]);
1300
+ // Second exhausted
1301
+ expectDone(step(undefined));
1302
+ });
1303
+
1304
+ it("handles three schedules", () => {
1305
+ const deps = createScheduleDeps();
1306
+
1307
+ const step = sequenceSchedules(
1308
+ take(1)(spaced("100ms")),
1309
+ take(1)(spaced("200ms")),
1310
+ take(1)(spaced("300ms")),
1311
+ )(deps);
1312
+
1313
+ expectOk(step(undefined), [100, 100]);
1314
+ expectOk(step(undefined), [200, 200]);
1315
+ expectOk(step(undefined), [300, 300]);
1316
+ expectDone(step(undefined));
1317
+ });
1318
+
1319
+ it("stops immediately for empty list", () => {
1320
+ const deps = createScheduleDeps();
1321
+ const step = sequenceSchedules()(deps);
1322
+ expectDone(step(undefined));
1323
+ });
1324
+ });
1325
+
1326
+ describe("intersectSchedules", () => {
1327
+ it("continues while both want to continue", () => {
1328
+ const deps = createScheduleDeps();
1329
+
1330
+ const a = take(2)(spaced("100ms"));
1331
+ const b = take(4)(spaced("200ms"));
1332
+ const step = intersectSchedules(a, b)(deps);
1333
+
1334
+ // max(100, 200)
1335
+ expectOk(step(undefined), [[100, 200], 200]);
1336
+ // max(100, 200)
1337
+ expectOk(step(undefined), [[100, 200], 200]);
1338
+ // a stopped, so intersection stops
1339
+ expectDone(step(undefined));
1340
+ });
1341
+
1342
+ it("does not step children after the intersection stops", () => {
1343
+ let aCalls = 0;
1344
+ let bCalls = 0;
1345
+ const step = intersectSchedules(
1346
+ tapScheduleInput(() => {
1347
+ aCalls++;
1348
+ })(take(1)(spaced("100ms"))),
1349
+ tapScheduleInput(() => {
1350
+ bCalls++;
1351
+ })(spaced("200ms")),
1352
+ )(createScheduleDeps());
1353
+
1354
+ expectOk(step(undefined), [[100, 200], 200]);
1355
+ expectDone(step(undefined));
1356
+ expectDone(step(undefined));
1357
+ assertEqual([aCalls, bCalls], [2, 2]);
1358
+ });
1359
+ });
1360
+
1361
+ describe("unionSchedules", () => {
1362
+ it("continues while either wants to continue", () => {
1363
+ const deps = createScheduleDeps();
1364
+
1365
+ const a = take(2)(spaced("100ms"));
1366
+ const b = take(4)(spaced("200ms"));
1367
+ const step = unionSchedules(a, b)(deps);
1368
+
1369
+ // min(100, 200)
1370
+ expectOk(step(undefined), [100, 100]);
1371
+ // min(100, 200)
1372
+ expectOk(step(undefined), [100, 100]);
1373
+ // a stopped, b continues
1374
+ expectOk(step(undefined), [200, 200]);
1375
+ expectOk(step(undefined), [200, 200]);
1376
+ // both stopped
1377
+ expectDone(step(undefined));
1378
+ });
1379
+
1380
+ it("continues when second schedule stops first", () => {
1381
+ const deps = createScheduleDeps();
1382
+
1383
+ const a = take(2)(spaced("200ms"));
1384
+ const b = take(1)(spaced("100ms"));
1385
+ const step = unionSchedules(a, b)(deps);
1386
+
1387
+ expectOk(step(undefined), [100, 100]);
1388
+ expectOk(step(undefined), [200, 200]);
1389
+ expectDone(step(undefined));
1390
+ });
1391
+
1392
+ it("returns output from the shorter delay", () => {
1393
+ const deps = createScheduleDeps();
1394
+
1395
+ const a = mapSchedule(() => "a")(spaced("200ms"));
1396
+ const b = mapSchedule(() => "b")(spaced("100ms"));
1397
+ const step = unionSchedules(a, b)(deps);
1398
+
1399
+ expectOk(step(undefined), ["b", 100]);
1400
+ });
1401
+
1402
+ it("does not step completed children", () => {
1403
+ let aCalls = 0;
1404
+ let bCalls = 0;
1405
+ const step = unionSchedules(
1406
+ tapScheduleInput(() => {
1407
+ aCalls++;
1408
+ })(take(1)(spaced("100ms"))),
1409
+ tapScheduleInput(() => {
1410
+ bCalls++;
1411
+ })(take(3)(spaced("200ms"))),
1412
+ )(createScheduleDeps());
1413
+
1414
+ expectOk(step(undefined), [100, 100]);
1415
+ expectOk(step(undefined), [200, 200]);
1416
+ expectOk(step(undefined), [200, 200]);
1417
+ assertEqual([aCalls, bCalls], [2, 3]);
1418
+ expectDone(step(undefined));
1419
+ expectDone(step(undefined));
1420
+ assertEqual([aCalls, bCalls], [2, 4]);
1421
+ });
1422
+ });
1423
+
1424
+ describe("whenInput", () => {
1425
+ it("maintains independent branch state", () => {
1426
+ const step = whenInput(
1427
+ (useAlt: boolean) => useAlt,
1428
+ exponential("1s"),
1429
+ )(exponential("100ms"))(createScheduleDeps());
1430
+
1431
+ expectOk(step(false), [100, 100]);
1432
+ expectOk(step(true), [1000, 1000]);
1433
+ expectOk(step(false), [200, 200]);
1434
+ expectOk(step(true), [2000, 2000]);
1435
+ });
1436
+
1437
+ it("shares an outer attempt limit across branches", () => {
1438
+ const step = take(3)(
1439
+ whenInput(
1440
+ (useAlt: boolean) => useAlt,
1441
+ exponential("1s"),
1442
+ )(exponential("100ms")),
1443
+ )(createScheduleDeps());
1444
+
1445
+ expectOk(step(false), [100, 100]);
1446
+ expectOk(step(true), [1000, 1000]);
1447
+ expectOk(step(false), [200, 200]);
1448
+ expectDone(step(true));
1449
+ });
1450
+
1451
+ it("passes through termination from alt schedule", () => {
1452
+ const deps = createScheduleDeps();
1453
+
1454
+ const step = whenInput(() => true, take(1)(spaced("1s")))(spaced("100ms"))(
1455
+ deps,
1456
+ );
1457
+
1458
+ expectOk(step(undefined), [1000, 1000]);
1459
+ expectDone(step(undefined));
1460
+ });
1461
+
1462
+ it("keeps terminal done when the selected branch changes", () => {
1463
+ const step = whenInput(
1464
+ (useAlt: boolean) => useAlt,
1465
+ take(1)(spaced("1s")),
1466
+ )(exponential("100ms"))(createScheduleDeps());
1467
+
1468
+ expectOk(step(false), [100, 100]);
1469
+ expectOk(step(false), [200, 200]);
1470
+ expectOk(step(true), [1000, 1000]);
1471
+ expectDone(step(true));
1472
+ expectDone(step(false));
1473
+ });
1474
+ });
1475
+
1476
+ describe("tapScheduleOutput", () => {
1477
+ it("executes side effect without altering schedule", () => {
1478
+ const deps = createScheduleDeps();
1479
+
1480
+ const outputs: Array<Millis> = [];
1481
+ const step = tapScheduleOutput((delay: Millis) => {
1482
+ outputs.push(delay);
1483
+ })(take(3)(exponential("100ms")))(deps);
1484
+
1485
+ expectOk(step(undefined), [100, 100]);
1486
+ expectOk(step(undefined), [200, 200]);
1487
+ expectOk(step(undefined), [400, 400]);
1488
+ expectDone(step(undefined));
1489
+
1490
+ assertEqual(outputs, [100, 200, 400]);
1491
+ });
1492
+
1493
+ it("does not call effect when schedule stops", () => {
1494
+ const deps = createScheduleDeps();
1495
+
1496
+ const outputs: Array<number> = [];
1497
+ const step = tapScheduleOutput((n: number) => {
1498
+ outputs.push(n);
1499
+ })(take(2)(forever))(deps);
1500
+
1501
+ step(undefined);
1502
+ step(undefined);
1503
+ // Err(Done<void>)
1504
+ step(undefined);
1505
+
1506
+ assertEqual(outputs, [0, 1]);
1507
+ });
1508
+ });
1509
+
1510
+ describe("tapScheduleInput", () => {
1511
+ it("executes side effect on input without altering schedule", () => {
1512
+ const deps = createScheduleDeps();
1513
+
1514
+ const inputs: Array<string> = [];
1515
+ const step = tapScheduleInput((input: string) => {
1516
+ inputs.push(input);
1517
+ })(take(3)(spaced("100ms")))(deps);
1518
+
1519
+ expectOk(step("error1"), [100, 100]);
1520
+ expectOk(step("error2"), [100, 100]);
1521
+ expectOk(step("error3"), [100, 100]);
1522
+ expectDone(step("error4"));
1523
+
1524
+ assertEqual(inputs, ["error1", "error2", "error3", "error4"]);
1525
+ });
1526
+
1527
+ it("is called even when schedule stops", () => {
1528
+ const deps = createScheduleDeps();
1529
+
1530
+ const inputs: Array<string> = [];
1531
+ const step = tapScheduleInput((input: string) => {
1532
+ inputs.push(input);
1533
+ })(take(1)(forever))(deps);
1534
+
1535
+ step("first");
1536
+ // Err(Done<void>) but tap still runs
1537
+ step("second");
1538
+
1539
+ assertEqual(inputs, ["first", "second"]);
1540
+ });
1541
+ });
1542
+
1543
+ describe("retryStrategyAws", () => {
1544
+ it("uses AWS 2.1 ordinary-failure timing", () => {
1545
+ // RandomNumber is below 1; millisecond rounding reaches the 50ms upper bound.
1546
+ const step = retryStrategyAws(
1547
+ createScheduleDepsWithRandom(0.999999999 as RandomNumber),
1548
+ );
1549
+
1550
+ expectOk(step(undefined), [50, 50]);
1551
+ expectOk(step(undefined), [100, 100]);
1552
+ expectDone(step(undefined));
1553
+ });
1554
+ });