@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,1829 @@
1
+ import { describe, it, test } from "node:test";
2
+ import {
3
+ assertEqual,
4
+ assertErr,
5
+ assertFalse,
6
+ assertInstanceOf,
7
+ assertOk,
8
+ assertRejectsSame,
9
+ assertSame,
10
+ assertThrowsInstanceOf,
11
+ assertThrowsSame,
12
+ assertTrue,
13
+ } from "./Assert.ts";
14
+
15
+ import type { NonEmptyReadonlyArray } from "./Array.ts";
16
+ import type {
17
+ Done,
18
+ Err,
19
+ ExcludeDone,
20
+ InferDone,
21
+ InferErr,
22
+ InferOk,
23
+ NextResult,
24
+ OnlyDone,
25
+ Result,
26
+ } from "./Result.ts";
27
+ import {
28
+ allResult,
29
+ anyResult,
30
+ done,
31
+ err,
32
+ flatMapResult,
33
+ getOk,
34
+ getOrNull,
35
+ getOrThrow,
36
+ isErr,
37
+ isOk,
38
+ ok,
39
+ tryAsync,
40
+ trySync,
41
+ } from "./Result.ts";
42
+ import { parseStackTrace } from "./StackTrace.ts";
43
+ import { assertType } from "./Type.ts";
44
+
45
+ describe("InferOk and InferErr", () => {
46
+ it("infers Ok type", () => {
47
+ type MyResult = Result<string, { type: "MyError"; code: number }>;
48
+ assertType<InferOk<MyResult>, string>();
49
+ });
50
+
51
+ it("infers Err type", () => {
52
+ interface MyError {
53
+ readonly type: "MyError";
54
+ readonly code: number;
55
+ }
56
+ type MyResult = Result<string, MyError>;
57
+ assertType<InferErr<MyResult>, MyError>();
58
+ });
59
+
60
+ it("handles void Result", () => {
61
+ type VoidResult = Result<void, Error>;
62
+ assertType<InferOk<VoidResult>, void>();
63
+ assertType<InferErr<VoidResult>, Error>();
64
+ });
65
+
66
+ it("works at runtime", () => {
67
+ interface MyError {
68
+ readonly type: "MyError";
69
+ readonly code: number;
70
+ }
71
+ type MyResult = Result<string, MyError>;
72
+
73
+ const okValue: InferOk<MyResult> = "hello";
74
+ const errValue: InferErr<MyResult> = { type: "MyError", code: 404 };
75
+
76
+ assertEqual(okValue, "hello");
77
+ assertEqual(errValue, { type: "MyError", code: 404 });
78
+ });
79
+ });
80
+
81
+ describe("ok", () => {
82
+ it("creates Ok with a value", () => {
83
+ assertEqual(ok(42), { ok: true, value: 42 });
84
+ });
85
+
86
+ it("creates Ok<void> without arguments", () => {
87
+ assertEqual(ok(), { ok: true, value: undefined });
88
+ });
89
+
90
+ it("caches ok() and ok(undefined)", () => {
91
+ assertSame(ok(), ok());
92
+ assertSame(ok(undefined), ok());
93
+ });
94
+
95
+ it("rejects Ok<void> when Result expects a value", () => {
96
+ // @ts-expect-error Type 'Ok<void>' is not assignable to type 'Result<string, Error>'
97
+ const _result: Result<string, Error> = ok();
98
+ });
99
+
100
+ it("returns Result<T, never> for correct type inference", () => {
101
+ const result = ok(42);
102
+ assertType<typeof result, Result<number>>();
103
+ });
104
+
105
+ it("infers never for E when combining with err", () => {
106
+ interface MyError {
107
+ readonly type: "MyError";
108
+ }
109
+
110
+ const example = (fail: boolean): Result<number, MyError> => {
111
+ if (fail) return err({ type: "MyError" });
112
+ return ok(42);
113
+ };
114
+
115
+ {
116
+ const actual = example(false);
117
+ assertType<typeof actual, Result<number, MyError>>();
118
+ }
119
+ });
120
+ });
121
+
122
+ describe("err", () => {
123
+ it("creates Err with an error", () => {
124
+ assertEqual(err("error"), { ok: false, error: "error" });
125
+ });
126
+
127
+ it("returns Result<never, E> for correct type inference", () => {
128
+ const result = err("oops");
129
+ assertType<typeof result, Result<never, string>>();
130
+ });
131
+ });
132
+
133
+ describe("isOk and isErr", () => {
134
+ it("identifies Ok result", () => {
135
+ const result = ok(123);
136
+
137
+ assertTrue(isOk(result));
138
+ assertFalse(isErr(result));
139
+ });
140
+
141
+ it("isOk narrows an Ok result", () => {
142
+ const result = ok(123);
143
+
144
+ if (isOk(result)) {
145
+ assertType<typeof result.value, number>();
146
+ }
147
+ });
148
+
149
+ it("identifies Err result", () => {
150
+ const result = err({ type: "TestError" as const });
151
+
152
+ assertFalse(isOk(result));
153
+ assertTrue(isErr(result));
154
+ });
155
+
156
+ it("isErr narrows an Err result", () => {
157
+ const result = err({ type: "TestError" as const });
158
+
159
+ if (isErr(result)) {
160
+ assertType<
161
+ typeof result.error extends { readonly type: "TestError" }
162
+ ? true
163
+ : false,
164
+ true
165
+ >();
166
+ }
167
+ });
168
+ });
169
+
170
+ describe("getOrThrow", () => {
171
+ it("returns value for Ok", () => {
172
+ assertEqual(getOrThrow(ok(42)), 42);
173
+ });
174
+
175
+ it("throws for Err", () => {
176
+ const error = assertThrowsInstanceOf(() => getOrThrow(err("error")), Error);
177
+ assertTrue(error.message.includes("getOrThrow"));
178
+ });
179
+
180
+ it("includes primitive error as cause", () => {
181
+ const error = assertThrowsInstanceOf(() => getOrThrow(err("error")), Error);
182
+ assertEqual(error.cause, "error");
183
+ });
184
+
185
+ it("includes Error instance as cause", () => {
186
+ const original = new TypeError("boom");
187
+ const error = assertThrowsInstanceOf(
188
+ () => getOrThrow(err(original)),
189
+ Error,
190
+ );
191
+ assertSame(error.cause, original);
192
+ });
193
+ });
194
+
195
+ describe("getOrNull", () => {
196
+ it("returns value for Ok", () => {
197
+ assertEqual(getOrNull(ok(42)), 42);
198
+ });
199
+
200
+ it("returns null for Err", () => {
201
+ assertSame(getOrNull(err("error")), null);
202
+ });
203
+ });
204
+
205
+ describe("getOk", () => {
206
+ it("extracts value from Result with never error", () => {
207
+ const result = ok(42);
208
+ assertEqual(getOk(result), 42);
209
+ });
210
+
211
+ it("rejects Result with possible error type", () => {
212
+ type IsAssignable =
213
+ Result<number, string> extends Result<number> ? true : false;
214
+ assertType<IsAssignable, false>();
215
+ });
216
+
217
+ it("throws when invariant is violated at runtime", () => {
218
+ const invalid = err("fail") as unknown as Result<number>;
219
+ const error = assertThrowsInstanceOf(() => getOk(invalid), Error);
220
+ assertTrue(error.message.includes("Expected Ok result."));
221
+ });
222
+ });
223
+
224
+ describe("trySync", () => {
225
+ interface ParseError {
226
+ readonly type: "ParseError";
227
+ readonly message: string;
228
+ }
229
+
230
+ it("returns Ok on success", () => {
231
+ const result = trySync(
232
+ () => JSON.parse('{"key": "value"}') as unknown,
233
+ (error): ParseError => ({ type: "ParseError", message: String(error) }),
234
+ );
235
+
236
+ assertEqual(result, {
237
+ ok: true,
238
+ value: { key: "value" },
239
+ });
240
+ });
241
+
242
+ it("returns Err on exception", () => {
243
+ const result = trySync(
244
+ () => JSON.parse("{key: value}") as unknown,
245
+ (error): ParseError => ({ type: "ParseError", message: String(error) }),
246
+ );
247
+
248
+ assertErr(result);
249
+ const { message } = result.error;
250
+ assertTrue(message.includes("SyntaxError"));
251
+ assertEqual(result, {
252
+ ok: false,
253
+ error: { type: "ParseError", message },
254
+ });
255
+ });
256
+
257
+ it("returns Err with the exception when mapError is omitted", () => {
258
+ const failure = new Error("Something went wrong");
259
+ const result = trySync(() => {
260
+ throw failure;
261
+ });
262
+
263
+ assertType<typeof result, Result<never, unknown>>();
264
+ assertEqual(result, err(failure));
265
+ });
266
+
267
+ it("mapError may throw to escalate a failure", () => {
268
+ const failure = new Error("Something went wrong");
269
+ const escalated = new Error("Escalated");
270
+
271
+ assertThrowsSame(
272
+ () =>
273
+ trySync(
274
+ () => {
275
+ throw failure;
276
+ },
277
+ () => {
278
+ throw escalated;
279
+ },
280
+ ),
281
+ escalated,
282
+ );
283
+ });
284
+ });
285
+
286
+ describe("tryAsync", () => {
287
+ it("returns Ok on resolved promise", async () => {
288
+ const result = await tryAsync(
289
+ () => Promise.resolve(),
290
+ (error) => ({ type: "TestError", message: String(error) }),
291
+ );
292
+
293
+ assertEqual(result, ok());
294
+ });
295
+
296
+ it("returns Err on rejected promise", async () => {
297
+ const result = await tryAsync(
298
+ // oxlint-disable-next-line typescript/require-await
299
+ async () => {
300
+ throw new Error("Something went wrong");
301
+ },
302
+ (error) => ({ type: "TestError", message: String(error) }),
303
+ );
304
+
305
+ assertEqual(
306
+ result,
307
+ err({
308
+ type: "TestError",
309
+ message: "Error: Something went wrong",
310
+ }),
311
+ );
312
+ });
313
+
314
+ it("returns Err with the rejection when mapError is omitted", async () => {
315
+ const failure = new Error("Something went wrong");
316
+ const result = await tryAsync(
317
+ // oxlint-disable-next-line typescript/require-await
318
+ async () => {
319
+ throw failure;
320
+ },
321
+ );
322
+
323
+ assertType<typeof result, Result<never, unknown>>();
324
+ assertEqual(result, err(failure));
325
+ });
326
+
327
+ it("maps custom error properties", async () => {
328
+ const result = await tryAsync(
329
+ // oxlint-disable-next-line typescript/require-await
330
+ async () => {
331
+ throw new TypeError("Invalid type");
332
+ },
333
+ (error) => ({
334
+ type: "CustomError",
335
+ name: error instanceof Error ? error.name : "UnknownError",
336
+ message: String(error),
337
+ }),
338
+ );
339
+
340
+ assertEqual(
341
+ result,
342
+ err({
343
+ type: "CustomError",
344
+ name: "TypeError",
345
+ message: "TypeError: Invalid type",
346
+ }),
347
+ );
348
+ });
349
+
350
+ it("mapError may throw to escalate a failure", async () => {
351
+ const failure = new Error("Something went wrong");
352
+ const escalated = new Error("Escalated");
353
+
354
+ await assertRejectsSame(
355
+ tryAsync(
356
+ () => Promise.reject(failure),
357
+ () => {
358
+ throw escalated;
359
+ },
360
+ ),
361
+ escalated,
362
+ );
363
+ });
364
+
365
+ it("catches synchronous throws", async () => {
366
+ const result = await tryAsync(
367
+ () => {
368
+ throw new Error("Sync throw before promise");
369
+ },
370
+ (error) => ({ type: "TestError", message: String(error) }),
371
+ );
372
+
373
+ assertEqual(
374
+ result,
375
+ err({
376
+ type: "TestError",
377
+ message: "Error: Sync throw before promise",
378
+ }),
379
+ );
380
+ });
381
+
382
+ it("preserves its await boundary in rejected error stacks", async () => {
383
+ const result = await tryAsync(async () => {
384
+ await Promise.resolve();
385
+ throw new Error("Something went wrong");
386
+ });
387
+
388
+ assertErr(result);
389
+ assertInstanceOf(result.error, Error);
390
+
391
+ assertTrue(parseStackTrace(result.error.stack).files.includes("Result.ts"));
392
+ });
393
+ });
394
+
395
+ describe("NextResult", () => {
396
+ it("models success, failure, and done", () => {
397
+ type E = "E";
398
+
399
+ const a: NextResult<number, E, string> = ok(1);
400
+ const b: NextResult<number, E, string> = err(done("finished"));
401
+ const c: NextResult<number, E, string> = err<E>("E");
402
+
403
+ assertType<typeof a, NextResult<number, E, string>>();
404
+ assertFalse(b.ok);
405
+ assertFalse(c.ok);
406
+ });
407
+
408
+ it("extracts all type parameters", () => {
409
+ type MyNextResult = NextResult<number, string, { summary: string }>;
410
+
411
+ assertType<InferOk<MyNextResult>, number>();
412
+ assertType<InferErr<MyNextResult>, string | Done<{ summary: string }>>();
413
+ assertType<
414
+ InferDone<MyNextResult>,
415
+ {
416
+ summary: string;
417
+ }
418
+ >();
419
+ });
420
+
421
+ describe("done", () => {
422
+ it("creates Done with done value", () => {
423
+ assertEqual(done("finished"), {
424
+ type: "Done",
425
+ done: "finished",
426
+ });
427
+ });
428
+
429
+ it("creates Done<void> without arguments", () => {
430
+ assertEqual(done(), {
431
+ type: "Done",
432
+ done: undefined,
433
+ });
434
+ {
435
+ const actual = done();
436
+ assertType<typeof actual, Done<void>>();
437
+ }
438
+ });
439
+
440
+ it("preserves done type", () => {
441
+ const value = done({ count: 1 });
442
+ assertType<typeof value, Done<{ count: number }>>();
443
+ assertType<typeof value.done, { count: number }>();
444
+ });
445
+ });
446
+
447
+ describe("ExcludeDone and OnlyDone", () => {
448
+ it("ExcludeDone removes Done from a union", () => {
449
+ interface MyError {
450
+ readonly type: "MyError";
451
+ }
452
+ type E = MyError | Done<void>;
453
+ assertType<ExcludeDone<E>, MyError>();
454
+ });
455
+
456
+ it("OnlyDone keeps only Done from a union", () => {
457
+ interface MyError {
458
+ readonly type: "MyError";
459
+ }
460
+ type E = MyError | Done<"done">;
461
+ assertType<OnlyDone<E>, Done<"done">>();
462
+ });
463
+
464
+ it("OnlyDone returns never when there is no Done", () => {
465
+ type E = "E";
466
+ assertType<OnlyDone<E>, never>();
467
+ });
468
+ });
469
+
470
+ describe("InferDone", () => {
471
+ it("extracts Done type from NextResult with void done", () => {
472
+ type R = NextResult<number, string>;
473
+ assertType<InferDone<R>, void>();
474
+ });
475
+
476
+ it("extracts Done type from NextResult with complex done", () => {
477
+ type R = NextResult<
478
+ number,
479
+ string,
480
+ { count: number; items: Array<string> }
481
+ >;
482
+ assertType<
483
+ InferDone<R>,
484
+ {
485
+ count: number;
486
+ items: Array<string>;
487
+ }
488
+ >();
489
+ });
490
+
491
+ it("returns never for Result without Done", () => {
492
+ type R = Result<number, string>;
493
+ assertType<InferDone<R>, never>();
494
+ });
495
+
496
+ it("works with union errors containing Done", () => {
497
+ interface MyError {
498
+ readonly type: "MyError";
499
+ }
500
+ type R = Result<number, MyError | Done<string>>;
501
+ assertType<InferDone<R>, string>();
502
+ });
503
+ });
504
+ });
505
+
506
+ describe("flatMapResult", () => {
507
+ it("composes an Ok with another Result-returning operation", () => {
508
+ const result = flatMapResult(ok(21), (value) => ok(value * 2));
509
+
510
+ assertEqual(result, ok(42));
511
+ assertType<typeof result, Result<number>>();
512
+ });
513
+
514
+ it("returns the existing Err without calling the operation", () => {
515
+ interface FirstError {
516
+ readonly type: "FirstError";
517
+ }
518
+
519
+ interface SecondError {
520
+ readonly type: "SecondError";
521
+ }
522
+
523
+ const first = (): Result<number, FirstError> => err({ type: "FirstError" });
524
+ let called = false;
525
+ const result = flatMapResult(first(), (): Result<string, SecondError> => {
526
+ called = true;
527
+ return err({ type: "SecondError" });
528
+ });
529
+
530
+ assertEqual(result, err({ type: "FirstError" }));
531
+ assertFalse(called);
532
+ assertType<
533
+ typeof result extends Result<string, FirstError | SecondError>
534
+ ? true
535
+ : false,
536
+ true
537
+ >();
538
+ });
539
+
540
+ it("returns an error from the next operation", () => {
541
+ const result = flatMapResult(ok(42), () => err("fail"));
542
+
543
+ assertEqual(result, err("fail"));
544
+ });
545
+ });
546
+
547
+ describe("allResult", () => {
548
+ it("returns emptyArray for empty array", () => {
549
+ const result = allResult([]);
550
+ assertEqual(result, ok([]));
551
+ });
552
+
553
+ it("returns emptyRecord for empty record", () => {
554
+ const result = allResult({});
555
+ assertOk(result, {});
556
+ });
557
+
558
+ it("extracts all values from array of Ok results", () => {
559
+ const results = [ok(1), ok(2), ok(3)];
560
+ assertEqual(allResult(results), ok([1, 2, 3]));
561
+ });
562
+
563
+ it("returns first error from array", () => {
564
+ interface E1 {
565
+ readonly type: "E1";
566
+ }
567
+ interface E2 {
568
+ readonly type: "E2";
569
+ }
570
+ const results: NonEmptyReadonlyArray<Result<number, E1 | E2>> = [
571
+ ok(1),
572
+ err({ type: "E1" }),
573
+ err({ type: "E2" }),
574
+ ];
575
+ assertEqual(allResult(results), err({ type: "E1" }));
576
+ });
577
+
578
+ it("extracts all values from struct", () => {
579
+ const result = allResult({ a: ok(1), b: ok("two") });
580
+ assertOk(result, { a: 1, b: "two" });
581
+ });
582
+
583
+ it("returns first error from struct", () => {
584
+ const result = allResult({ a: ok(1), b: err("fail"), c: ok(3) });
585
+ assertEqual(result, err("fail"));
586
+ });
587
+
588
+ it("tuple preserves types", () => {
589
+ const result = allResult([ok(1), ok("two"), ok(true)]);
590
+ if (result.ok) {
591
+ {
592
+ const actual = result.value[0];
593
+ assertType<typeof actual, number>();
594
+ }
595
+ {
596
+ const actual = result.value[1];
597
+ assertType<typeof actual, string>();
598
+ }
599
+ {
600
+ const actual = result.value[2];
601
+ assertType<typeof actual, boolean>();
602
+ }
603
+ }
604
+ });
605
+
606
+ it("struct preserves types", () => {
607
+ const result = allResult({ a: ok(1), b: ok("two") });
608
+ if (result.ok) {
609
+ assertType<typeof result.value, { a: number; b: string }>();
610
+ }
611
+ });
612
+
613
+ it("non-empty arrays preserve types", () => {
614
+ const result = allResult([ok(1), ok(2)]);
615
+ if (result.ok) {
616
+ {
617
+ const actual = result.value[0];
618
+ assertType<typeof actual, number>();
619
+ }
620
+ {
621
+ const actual = result.value[1];
622
+ assertType<typeof actual, number>();
623
+ }
624
+ }
625
+ });
626
+
627
+ it("works with Iterable", () => {
628
+ const set = new Set([ok(1), ok(2), ok(3)]);
629
+ const result = allResult(set);
630
+ assertEqual(result, ok([1, 2, 3]));
631
+ });
632
+
633
+ it("returns an empty array for an empty non-array Iterable", () => {
634
+ const results = new Set<Result<number>>();
635
+ const result = allResult(results);
636
+
637
+ assertType<typeof result, Result<ReadonlyArray<number>>>();
638
+ assertEqual(result, ok([]));
639
+ });
640
+
641
+ it("stops consuming an Iterable on the first error", () => {
642
+ const consumedValues: Array<number> = [];
643
+ const createResults = function* (): Generator<Result<number, string>> {
644
+ consumedValues.push(1);
645
+ yield ok(1);
646
+ consumedValues.push(2);
647
+ yield err("fail");
648
+ consumedValues.push(3);
649
+ yield ok(3);
650
+ };
651
+
652
+ const result = allResult(createResults());
653
+
654
+ assertErr(result, "fail");
655
+ assertEqual(consumedValues, [1, 2]);
656
+ });
657
+
658
+ it("ignores inherited record properties", () => {
659
+ const results = Object.assign(
660
+ Object.create({ inherited: err("fail") }) as Record<
661
+ string,
662
+ Result<number, string>
663
+ >,
664
+ { own: ok(1) },
665
+ );
666
+
667
+ const result = allResult(results);
668
+
669
+ assertOk(result, { own: 1 });
670
+ });
671
+
672
+ it("does not collect Ok values", () => {
673
+ interface FirstError {
674
+ readonly type: "FirstError";
675
+ }
676
+ interface SecondError {
677
+ readonly type: "SecondError";
678
+ }
679
+
680
+ const first: Result<number, FirstError> = ok(1);
681
+ const second: Result<string, SecondError> = ok("two");
682
+
683
+ const result = allResult([first, second], { collect: false });
684
+
685
+ assertType<typeof result, Result<void, FirstError | SecondError>>();
686
+ assertOk(result, undefined);
687
+ });
688
+
689
+ it("returns the first Err without collecting Ok values", () => {
690
+ const result = allResult([ok(1), err("first"), err("second")], {
691
+ collect: false,
692
+ });
693
+
694
+ assertErr(result, "first");
695
+ });
696
+
697
+ it("does not collect Ok values from a record", () => {
698
+ const result = allResult(
699
+ { first: ok(1), second: err("fail") },
700
+ { collect: false },
701
+ );
702
+
703
+ assertType<typeof result, Result<void, string>>();
704
+ assertErr(result, "fail");
705
+ });
706
+ });
707
+
708
+ describe("allResult mapping overload", () => {
709
+ it("returns emptyArray for empty array", () => {
710
+ const result = allResult([], (x: number) => ok(x * 2));
711
+ assertEqual(result, ok([]));
712
+ });
713
+
714
+ it("returns emptyRecord for empty record", () => {
715
+ const result = allResult({}, (x: number) => ok(x * 2));
716
+ assertOk(result, {});
717
+ });
718
+
719
+ it("maps items and collects results", () => {
720
+ const result = allResult([1, 2, 3], (x) => ok(x * 2));
721
+ assertEqual(result, ok([2, 4, 6]));
722
+ });
723
+
724
+ it("returns first error", () => {
725
+ const result = allResult([1, 2, 3], (x) =>
726
+ x === 2 ? err("fail") : ok(x * 2),
727
+ );
728
+ assertEqual(result, err("fail"));
729
+ });
730
+
731
+ it("does not map values after the first error", () => {
732
+ const mappedValues: Array<number> = [];
733
+ const result = allResult([1, 2, 3], (value) => {
734
+ mappedValues.push(value);
735
+ return value === 2 ? err("fail") : ok(value * 2);
736
+ });
737
+
738
+ assertEqual(result, err("fail"));
739
+ assertEqual(mappedValues, [1, 2]);
740
+ });
741
+
742
+ it("infers heterogeneous mapper errors", () => {
743
+ interface FirstError {
744
+ readonly type: "FirstError";
745
+ }
746
+ interface SecondError {
747
+ readonly type: "SecondError";
748
+ }
749
+
750
+ const first = (): Result<void, FirstError> => ok();
751
+ const second = (): Result<void, SecondError> =>
752
+ err({ type: "SecondError" });
753
+ const result = allResult([first, second], (operation) => operation());
754
+
755
+ assertType<
756
+ typeof result,
757
+ Result<readonly [void, void], FirstError | SecondError>
758
+ >();
759
+ assertErr(result, { type: "SecondError" });
760
+ });
761
+
762
+ it("does not collect mapped Ok values", () => {
763
+ const result = allResult([1, 2, 3], (value) => ok(value * 2), {
764
+ collect: false,
765
+ });
766
+
767
+ assertType<typeof result, Result<void>>();
768
+ assertOk(result, undefined);
769
+ });
770
+
771
+ it("stops mapping without collecting on the first Err", () => {
772
+ interface FirstError {
773
+ readonly type: "FirstError";
774
+ }
775
+ interface SecondError {
776
+ readonly type: "SecondError";
777
+ }
778
+
779
+ const calls: Array<string> = [];
780
+ const first = (): Result<number, FirstError> => {
781
+ calls.push("first");
782
+ return ok(1);
783
+ };
784
+ const second = (): Result<number, SecondError> => {
785
+ calls.push("second");
786
+ return err({ type: "SecondError" });
787
+ };
788
+ const result = allResult([first, second], (operation) => operation(), {
789
+ collect: false,
790
+ });
791
+
792
+ assertType<typeof result, Result<void, FirstError | SecondError>>();
793
+ assertErr(result, { type: "SecondError" });
794
+ assertEqual(calls, ["first", "second"]);
795
+ });
796
+
797
+ it("maps struct and collects results", () => {
798
+ const result = allResult({ a: 1, b: 2 }, (x) => ok(x * 2));
799
+ assertOk(result, { a: 2, b: 4 });
800
+ });
801
+
802
+ it("maps a record without collecting Ok values", () => {
803
+ const result = allResult({ a: 1, b: 2 }, (x) => ok(x * 2), {
804
+ collect: false,
805
+ });
806
+
807
+ assertType<typeof result, Result<void>>();
808
+ assertOk(result, undefined);
809
+ });
810
+
811
+ it("returns first error from struct", () => {
812
+ const result = allResult({ a: 1, b: 2, c: 3 }, (x) =>
813
+ x === 2 ? err("fail") : ok(x * 2),
814
+ );
815
+ assertEqual(result, err("fail"));
816
+ });
817
+
818
+ it("struct preserves types", () => {
819
+ const result = allResult({ a: 1, b: 2 }, (x) => ok(String(x)));
820
+ if (result.ok) {
821
+ assertType<typeof result.value, Readonly<Record<"a" | "b", string>>>();
822
+ }
823
+ });
824
+
825
+ it("non-empty arrays preserve types", () => {
826
+ const result = allResult([1, 2, 3], (x) => ok(x * 2));
827
+ if (result.ok) {
828
+ {
829
+ const actual = result.value[0];
830
+ assertType<typeof actual, number>();
831
+ }
832
+ {
833
+ const actual = result.value[1];
834
+ assertType<typeof actual, number>();
835
+ }
836
+ {
837
+ const actual = result.value[2];
838
+ assertType<typeof actual, number>();
839
+ }
840
+ }
841
+ });
842
+
843
+ it("works with Iterable", () => {
844
+ const set = new Set([1, 2, 3]);
845
+ const result = allResult(set, (x) => ok(x * 2));
846
+ assertEqual(result, ok([2, 4, 6]));
847
+ });
848
+ });
849
+
850
+ describe("anyResult", () => {
851
+ it("returns first success", () => {
852
+ assertEqual(anyResult([err("a"), ok(42), err("b")]), ok(42));
853
+ });
854
+
855
+ it("returns last error when all fail", () => {
856
+ assertEqual(anyResult([err("a"), err("b"), err("c")]), err("c"));
857
+ });
858
+
859
+ it("returns first Ok even if it's first", () => {
860
+ assertEqual(anyResult([ok(1), ok(2), ok(3)]), ok(1));
861
+ });
862
+
863
+ it("preserves types", () => {
864
+ const result = anyResult([err({ type: "E1" as const }), ok(42)]);
865
+ if (result.ok) {
866
+ assertType<typeof result.value, number>();
867
+ }
868
+ });
869
+ });
870
+
871
+ test("example: parseJson with early return", () => {
872
+ interface ParseJsonError {
873
+ readonly type: "ParseJsonError";
874
+ readonly message: string;
875
+ }
876
+
877
+ const parseJson = (value: string): Result<unknown, ParseJsonError> => {
878
+ try {
879
+ return ok(JSON.parse(value));
880
+ } catch (error) {
881
+ return err({ type: "ParseJsonError", message: String(error) });
882
+ }
883
+ };
884
+
885
+ const json = parseJson('{"key": "value"}');
886
+
887
+ if (!json.ok) return undefined;
888
+
889
+ assertType<typeof json.value, unknown>();
890
+ return undefined;
891
+ });
892
+
893
+ // --- Result with resource management ---
894
+ //
895
+ // Result and resource management are orthogonal concerns:
896
+ // - Result answers: "Did the operation succeed?"
897
+ // - Disposable answers: "When do we clean up resources?"
898
+ //
899
+ // Pattern:
900
+ // 1. Call a function that returns Result<Resource, Error>
901
+ // 2. If !result.ok, return early → disposal happens automatically
902
+ // 3. If result.ok, add result.value to the stack → resource gets tracked
903
+
904
+ interface CreateResourceError {
905
+ readonly type: "CreateResourceError";
906
+ readonly reason: string;
907
+ }
908
+
909
+ interface Resource extends Disposable {
910
+ readonly id: string;
911
+ readonly isDisposed: () => boolean;
912
+ }
913
+
914
+ interface AsyncResource extends AsyncDisposable {
915
+ readonly id: string;
916
+ readonly isDisposed: () => boolean;
917
+ }
918
+
919
+ const createMockResource = (id: string): Resource => {
920
+ let disposed = false;
921
+ return {
922
+ id,
923
+ isDisposed: () => disposed,
924
+ [Symbol.dispose]: () => {
925
+ disposed = true;
926
+ },
927
+ };
928
+ };
929
+
930
+ const createMockAsyncResource = (id: string): AsyncResource => {
931
+ let disposed = false;
932
+ return {
933
+ id,
934
+ isDisposed: () => disposed,
935
+ [Symbol.asyncDispose]: async () => {
936
+ await Promise.resolve();
937
+ disposed = true;
938
+ },
939
+ };
940
+ };
941
+
942
+ const createResource = (
943
+ id: string,
944
+ shouldFail: boolean,
945
+ ): Result<Resource, CreateResourceError> => {
946
+ if (shouldFail) {
947
+ return err({
948
+ type: "CreateResourceError",
949
+ reason: `Failed to create ${id}`,
950
+ });
951
+ }
952
+ return ok(createMockResource(id));
953
+ };
954
+
955
+ const createAsyncResource = async (
956
+ id: string,
957
+ shouldFail: boolean,
958
+ ): Promise<Result<AsyncResource, CreateResourceError>> => {
959
+ await Promise.resolve();
960
+ if (shouldFail) {
961
+ return err({
962
+ type: "CreateResourceError",
963
+ reason: `Failed to create ${id}`,
964
+ });
965
+ }
966
+ return ok(createMockAsyncResource(id));
967
+ };
968
+
969
+ describe("Result with Resource management", () => {
970
+ describe("using keyword", () => {
971
+ it("disposes on success", () => {
972
+ const resource = createResource("db", false);
973
+ if (!resource.ok) throw new Error("Should not fail");
974
+
975
+ {
976
+ using _ = resource.value;
977
+ assertFalse(resource.value.isDisposed());
978
+ }
979
+
980
+ assertTrue(resource.value.isDisposed());
981
+ });
982
+
983
+ it("disposes on early return", () => {
984
+ let resource = null as Resource | null;
985
+
986
+ const process = (): Result<string, CreateResourceError> => {
987
+ const result = createResource("db", false);
988
+ if (!result.ok) return result;
989
+
990
+ resource = result.value;
991
+ using _ = resource;
992
+
993
+ return err({ type: "CreateResourceError", reason: "other failure" });
994
+ };
995
+
996
+ const result = process();
997
+ assertErr(result, {
998
+ type: "CreateResourceError",
999
+ reason: "other failure",
1000
+ });
1001
+ assertTrue(resource?.isDisposed());
1002
+ });
1003
+
1004
+ it("disposes on throw", () => {
1005
+ let resource = null as Resource | null;
1006
+
1007
+ const process = (): void => {
1008
+ const result = createResource("db", false);
1009
+ if (!result.ok) throw new Error("Should not fail");
1010
+
1011
+ resource = result.value;
1012
+ using _ = resource;
1013
+
1014
+ throw new Error("Unexpected!");
1015
+ };
1016
+
1017
+ const error = assertThrowsInstanceOf(() => {
1018
+ process();
1019
+ }, Error);
1020
+ assertTrue(error.message.includes("Unexpected!"));
1021
+ assertTrue(resource?.isDisposed());
1022
+ });
1023
+
1024
+ // Block scopes control resource lifetime (RAII pattern).
1025
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using#using_in_a_block
1026
+ it("disposes at block scope exit", () => {
1027
+ const log: Array<string> = [];
1028
+
1029
+ const createLock = (name: string): Disposable => ({
1030
+ [Symbol.dispose]: () => {
1031
+ log.push(`unlock:${name}`);
1032
+ },
1033
+ });
1034
+
1035
+ const process = (): void => {
1036
+ log.push("start");
1037
+
1038
+ {
1039
+ using _ = createLock("a");
1040
+ log.push("critical-section-a");
1041
+ // lock "a" released here
1042
+ }
1043
+
1044
+ log.push("between");
1045
+
1046
+ {
1047
+ using _ = createLock("b");
1048
+ log.push("critical-section-b");
1049
+ // lock "b" released here
1050
+ }
1051
+
1052
+ log.push("end");
1053
+ };
1054
+
1055
+ process();
1056
+ assertEqual(log, [
1057
+ "start",
1058
+ "critical-section-a",
1059
+ "unlock:a",
1060
+ "between",
1061
+ "critical-section-b",
1062
+ "unlock:b",
1063
+ "end",
1064
+ ]);
1065
+ });
1066
+ });
1067
+
1068
+ describe("DisposableStack", () => {
1069
+ it("disposes resources on successful completion", () => {
1070
+ const disposed: Array<string> = [];
1071
+
1072
+ const processResources = (): Result<string, CreateResourceError> => {
1073
+ using disposer = new DisposableStack();
1074
+
1075
+ const resource1 = createResource("db", false);
1076
+ if (!resource1.ok) return resource1;
1077
+ disposer.use(resource1.value);
1078
+ disposer.defer(() => {
1079
+ disposed.push("db");
1080
+ });
1081
+
1082
+ const resource2 = createResource("file", false);
1083
+ if (!resource2.ok) return resource2;
1084
+ disposer.use(resource2.value);
1085
+ disposer.defer(() => {
1086
+ disposed.push("file");
1087
+ });
1088
+
1089
+ return ok("processed");
1090
+ };
1091
+
1092
+ const result = processResources();
1093
+ assertOk(result, "processed");
1094
+ assertEqual(disposed, ["file", "db"]);
1095
+ });
1096
+
1097
+ it("disposes created resources when later creation fails", () => {
1098
+ const disposed: Array<string> = [];
1099
+
1100
+ const processResources = (): Result<string, CreateResourceError> => {
1101
+ using disposer = new DisposableStack();
1102
+
1103
+ const resource1 = createResource("db", false);
1104
+ if (!resource1.ok) return resource1;
1105
+ disposer.use(resource1.value);
1106
+ disposer.defer(() => {
1107
+ disposed.push("db");
1108
+ });
1109
+
1110
+ const resource2 = createResource("file", true);
1111
+ if (!resource2.ok) return resource2;
1112
+
1113
+ disposer.use(resource2.value);
1114
+ disposer.defer(() => {
1115
+ disposed.push("file");
1116
+ });
1117
+
1118
+ return ok("processed");
1119
+ };
1120
+
1121
+ const result = processResources();
1122
+ assertErr(result, {
1123
+ type: "CreateResourceError",
1124
+ reason: "Failed to create file",
1125
+ });
1126
+ assertEqual(disposed, ["db"]);
1127
+ });
1128
+
1129
+ it("disposes nothing when first creation fails", () => {
1130
+ const disposed: Array<string> = [];
1131
+
1132
+ const processResources = (): Result<string, CreateResourceError> => {
1133
+ using disposer = new DisposableStack();
1134
+
1135
+ const resource1 = createResource("db", true);
1136
+ if (!resource1.ok) return resource1;
1137
+ disposer.use(resource1.value);
1138
+ disposer.defer(() => {
1139
+ disposed.push("db");
1140
+ });
1141
+
1142
+ return ok("processed");
1143
+ };
1144
+
1145
+ const result = processResources();
1146
+ assertErr(result, {
1147
+ type: "CreateResourceError",
1148
+ reason: "Failed to create db",
1149
+ });
1150
+ assertEqual(disposed, []);
1151
+ });
1152
+
1153
+ it("works with adopt for non-disposable values", () => {
1154
+ let connectionClosed = false;
1155
+
1156
+ interface Connection {
1157
+ readonly query: (sql: string) => Array<string>;
1158
+ }
1159
+
1160
+ const openConnection = (
1161
+ shouldFail: boolean,
1162
+ ): Result<Connection, CreateResourceError> => {
1163
+ if (shouldFail) {
1164
+ return err({
1165
+ type: "CreateResourceError",
1166
+ reason: "Connection failed",
1167
+ });
1168
+ }
1169
+ return ok({
1170
+ query: (sql: string) => [`result for: ${sql}`],
1171
+ });
1172
+ };
1173
+
1174
+ const closeConnection = (_conn: Connection): void => {
1175
+ connectionClosed = true;
1176
+ };
1177
+
1178
+ const queryDatabase = (): Result<Array<string>, CreateResourceError> => {
1179
+ using disposer = new DisposableStack();
1180
+
1181
+ const conn = openConnection(false);
1182
+ if (!conn.ok) return conn;
1183
+
1184
+ disposer.adopt(conn.value, closeConnection);
1185
+
1186
+ return ok(conn.value.query("SELECT * FROM users"));
1187
+ };
1188
+
1189
+ const result = queryDatabase();
1190
+ assertOk(result, ["result for: SELECT * FROM users"]);
1191
+ assertTrue(connectionClosed);
1192
+ });
1193
+
1194
+ it("handles multiple resources with mixed success/failure", () => {
1195
+ const log: Array<string> = [];
1196
+
1197
+ interface ProcessingError {
1198
+ readonly type: "ProcessingError";
1199
+ readonly step: string;
1200
+ }
1201
+
1202
+ type MyError = CreateResourceError | ProcessingError;
1203
+
1204
+ const process = (): Result<void, MyError> => {
1205
+ using disposer = new DisposableStack();
1206
+
1207
+ const db = createResource("db", false);
1208
+ if (!db.ok) return db;
1209
+ disposer.use(db.value);
1210
+ disposer.defer(() => {
1211
+ log.push("cleanup:db");
1212
+ });
1213
+
1214
+ const cache = createResource("cache", false);
1215
+ if (!cache.ok) return cache;
1216
+ disposer.use(cache.value);
1217
+ disposer.defer(() => {
1218
+ log.push("cleanup:cache");
1219
+ });
1220
+
1221
+ log.push("work:step1");
1222
+
1223
+ const step2Result = err({
1224
+ type: "ProcessingError",
1225
+ step: "step2",
1226
+ }) as Result<void, ProcessingError>;
1227
+ if (!step2Result.ok) return step2Result;
1228
+
1229
+ log.push("work:step2");
1230
+ return ok();
1231
+ };
1232
+
1233
+ const result = process();
1234
+ assertErr(result, { type: "ProcessingError", step: "step2" });
1235
+ assertEqual(log, ["work:step1", "cleanup:cache", "cleanup:db"]);
1236
+ });
1237
+
1238
+ it("disposes resources even when unexpected error is thrown", () => {
1239
+ const disposed: Array<string> = [];
1240
+
1241
+ const processResources = (): Result<string, CreateResourceError> => {
1242
+ using disposer = new DisposableStack();
1243
+
1244
+ const resource1 = createResource("db", false);
1245
+ if (!resource1.ok) return resource1;
1246
+ disposer.use(resource1.value);
1247
+ disposer.defer(() => {
1248
+ disposed.push("db");
1249
+ });
1250
+
1251
+ // Simulate unexpected error (bug in code, not a Result error)
1252
+ throw new Error("Unexpected bug!");
1253
+
1254
+ // This code is unreachable but shows the pattern
1255
+ // return ok("processed");
1256
+ };
1257
+
1258
+ // The unexpected error propagates, but disposal still happens
1259
+ const error = assertThrowsInstanceOf(() => processResources(), Error);
1260
+ assertTrue(error.message.includes("Unexpected bug!"));
1261
+ assertEqual(disposed, ["db"]);
1262
+ });
1263
+
1264
+ it("transfers ownership with move()", () => {
1265
+ const disposed: Array<string> = [];
1266
+
1267
+ const createResources = (): Result<
1268
+ DisposableStack,
1269
+ CreateResourceError
1270
+ > => {
1271
+ using disposer = new DisposableStack();
1272
+
1273
+ const r1 = createResource("a", false);
1274
+ if (!r1.ok) return r1;
1275
+ disposer.use(r1.value);
1276
+ disposer.defer(() => {
1277
+ disposed.push("a");
1278
+ });
1279
+
1280
+ const r2 = createResource("b", false);
1281
+ if (!r2.ok) return r2;
1282
+ disposer.use(r2.value);
1283
+ disposer.defer(() => {
1284
+ disposed.push("b");
1285
+ });
1286
+
1287
+ return ok(disposer.move());
1288
+ };
1289
+
1290
+ interface TransferError {
1291
+ readonly type: "TransferError";
1292
+ }
1293
+
1294
+ const useResources = (): Result<
1295
+ void,
1296
+ CreateResourceError | TransferError
1297
+ > => {
1298
+ const resources = createResources();
1299
+ if (!resources.ok) return resources;
1300
+
1301
+ using _ = resources.value;
1302
+
1303
+ disposed.push("work");
1304
+
1305
+ return ok();
1306
+ };
1307
+
1308
+ const result = useResources();
1309
+ assertOk(result, undefined);
1310
+ assertEqual(disposed, ["work", "b", "a"]);
1311
+ });
1312
+ });
1313
+
1314
+ describe("AsyncDisposableStack", () => {
1315
+ it("disposes async resources on successful completion", async () => {
1316
+ const disposed: Array<string> = [];
1317
+
1318
+ const processResources = async (): Promise<
1319
+ Result<string, CreateResourceError>
1320
+ > => {
1321
+ await using disposer = new AsyncDisposableStack();
1322
+
1323
+ const resource1 = await createAsyncResource("db", false);
1324
+ if (!resource1.ok) return resource1;
1325
+ disposer.use(resource1.value);
1326
+ disposer.defer(async () => {
1327
+ await Promise.resolve();
1328
+ disposed.push("db");
1329
+ });
1330
+
1331
+ const resource2 = await createAsyncResource("file", false);
1332
+ if (!resource2.ok) return resource2;
1333
+ disposer.use(resource2.value);
1334
+ disposer.defer(async () => {
1335
+ await Promise.resolve();
1336
+ disposed.push("file");
1337
+ });
1338
+
1339
+ return ok("processed");
1340
+ };
1341
+
1342
+ const result = await processResources();
1343
+ assertOk(result, "processed");
1344
+ assertEqual(disposed, ["file", "db"]);
1345
+ });
1346
+
1347
+ it("disposes created async resources when later creation fails", async () => {
1348
+ const disposed: Array<string> = [];
1349
+
1350
+ const processResources = async (): Promise<
1351
+ Result<string, CreateResourceError>
1352
+ > => {
1353
+ await using disposer = new AsyncDisposableStack();
1354
+
1355
+ const resource1 = await createAsyncResource("db", false);
1356
+ if (!resource1.ok) return resource1;
1357
+ disposer.use(resource1.value);
1358
+ disposer.defer(async () => {
1359
+ await Promise.resolve();
1360
+ disposed.push("db");
1361
+ });
1362
+
1363
+ const resource2 = await createAsyncResource("file", true);
1364
+ if (!resource2.ok) return resource2;
1365
+ disposer.use(resource2.value);
1366
+ disposer.defer(async () => {
1367
+ await Promise.resolve();
1368
+ disposed.push("file");
1369
+ });
1370
+
1371
+ return ok("processed");
1372
+ };
1373
+
1374
+ const result = await processResources();
1375
+ assertErr(result, {
1376
+ type: "CreateResourceError",
1377
+ reason: "Failed to create file",
1378
+ });
1379
+ assertEqual(disposed, ["db"]);
1380
+ });
1381
+
1382
+ it("can mix sync and async resources", async () => {
1383
+ const disposed: Array<string> = [];
1384
+
1385
+ const processResources = async (): Promise<
1386
+ Result<string, CreateResourceError>
1387
+ > => {
1388
+ await using disposer = new AsyncDisposableStack();
1389
+
1390
+ const syncResource = createResource("sync", false);
1391
+ if (!syncResource.ok) return syncResource;
1392
+ disposer.use(syncResource.value);
1393
+ disposer.defer(() => {
1394
+ disposed.push("sync");
1395
+ });
1396
+
1397
+ const asyncResource = await createAsyncResource("async", false);
1398
+ if (!asyncResource.ok) return asyncResource;
1399
+ disposer.use(asyncResource.value);
1400
+ disposer.defer(async () => {
1401
+ await Promise.resolve();
1402
+ disposed.push("async");
1403
+ });
1404
+
1405
+ return ok("mixed");
1406
+ };
1407
+
1408
+ const result = await processResources();
1409
+ assertOk(result, "mixed");
1410
+ assertEqual(disposed, ["async", "sync"]);
1411
+ });
1412
+ });
1413
+ });
1414
+
1415
+ /**
1416
+ * Evolu uses plain Result objects and explicit checks instead of
1417
+ * generator-based composition.
1418
+ *
1419
+ * Generators make sequential workflows more concise because `yield*` combines
1420
+ * error propagation with unwrapping the success value. That concision adds
1421
+ * generator machinery and makes control flow less explicit. With AI coding
1422
+ * tools, writing `if (!result.ok) return result` is cheap, so saving those
1423
+ * lines is less important.
1424
+ *
1425
+ * Generators also do not prevent accidental omission. A lazy operation called
1426
+ * without `yield*` is left out of the workflow, just as a Result can be
1427
+ * ignored. Dedicated tooling can detect either mistake, while tests remain the
1428
+ * runnable specification for successful and failing paths.
1429
+ *
1430
+ * A historical Apple M1 microbenchmark used 500,000 iterations of a three-step
1431
+ * Result chain and produced these rough numbers:
1432
+ *
1433
+ * - Imperative: ~25 ms
1434
+ * - Generator with a wrapper: ~330 ms (~13x slower)
1435
+ * - Iterable Result with an inline generator: ~1200 ms (~48x slower)
1436
+ * - Iterable Result with a hoisted generator: ~990 ms (~40x slower)
1437
+ *
1438
+ * This artificial workload does not predict application performance. It only
1439
+ * demonstrates that generator composition has measurable runtime overhead in a
1440
+ * tight synchronous loop. The skipped test below preserves the experiment.
1441
+ */
1442
+ describe("design decisions", () => {
1443
+ describe("generators", () => {
1444
+ interface ParseError {
1445
+ readonly type: "ParseError";
1446
+ }
1447
+
1448
+ interface ValidationError {
1449
+ readonly type: "ValidationError";
1450
+ }
1451
+
1452
+ /** A generator that yields errors and returns a value on success. */
1453
+ type Gen<T, E> = Generator<Err<E>, T>;
1454
+
1455
+ /**
1456
+ * Converts a Result to a Gen for use with yield*.
1457
+ *
1458
+ * @yields {Err<E>} Err if the result is an error
1459
+ */
1460
+ // oxlint-disable-next-line eslint/func-style -- Generators require the function keyword.
1461
+ function* gen<T, E>(result: Result<T, E>): Gen<T, E> {
1462
+ if (result.ok) {
1463
+ return result.value;
1464
+ }
1465
+ yield result;
1466
+ // This line is never reached - the runner exits on first yielded Err
1467
+ throw new Error("Unreachable");
1468
+ }
1469
+
1470
+ /** Runs a Gen and returns the Result. */
1471
+ const runGen = <T, E>(gen: Gen<T, E>): Result<T, E> => {
1472
+ const next = gen.next();
1473
+ if (!next.done) {
1474
+ // Generator yielded an Err - force cleanup by calling return()
1475
+ // This triggers finally blocks and `using` disposal in the generator
1476
+ gen.return(undefined as T);
1477
+ return next.value;
1478
+ }
1479
+ return ok(next.value);
1480
+ };
1481
+
1482
+ const parse = (input: string): Result<number, ParseError> => {
1483
+ const n = parseInt(input, 10);
1484
+ return isNaN(n) ? err({ type: "ParseError" }) : ok(n);
1485
+ };
1486
+
1487
+ const validate = (n: number): Result<number, ValidationError> =>
1488
+ n > 0 ? ok(n) : err({ type: "ValidationError" });
1489
+
1490
+ const double = (n: number): Result<number> => ok(n * 2);
1491
+
1492
+ it("composes multiple Results with generators", () => {
1493
+ const program = function* (
1494
+ input: string,
1495
+ ): Gen<number, ParseError | ValidationError> {
1496
+ const parsed = yield* gen(parse(input));
1497
+ const validated = yield* gen(validate(parsed));
1498
+ const doubled = yield* gen(double(validated));
1499
+ return doubled;
1500
+ };
1501
+
1502
+ // Success case
1503
+ const success = runGen(program("21"));
1504
+ assertEqual(success, ok(42));
1505
+
1506
+ // Parse error
1507
+ const parseErr = runGen(program("not a number"));
1508
+ assertEqual(parseErr, err({ type: "ParseError" }));
1509
+
1510
+ // Validation error
1511
+ const validationErr = runGen(program("-5"));
1512
+ assertEqual(validationErr, err({ type: "ValidationError" }));
1513
+ });
1514
+
1515
+ it("is equivalent to imperative pattern", () => {
1516
+ // Generator version
1517
+ const withGenerator = (
1518
+ input: string,
1519
+ ): Result<number, ParseError | ValidationError> => {
1520
+ const program = function* (): Gen<
1521
+ number,
1522
+ ParseError | ValidationError
1523
+ > {
1524
+ const parsed = yield* gen(parse(input));
1525
+ const validated = yield* gen(validate(parsed));
1526
+ const doubled = yield* gen(double(validated));
1527
+ return doubled;
1528
+ };
1529
+ return runGen(program());
1530
+ };
1531
+
1532
+ // Imperative version
1533
+ const imperative = (
1534
+ input: string,
1535
+ ): Result<number, ParseError | ValidationError> => {
1536
+ const parsed = parse(input);
1537
+ if (!parsed.ok) return parsed;
1538
+
1539
+ const validated = validate(parsed.value);
1540
+ if (!validated.ok) return validated;
1541
+
1542
+ const doubled = double(validated.value);
1543
+ if (!doubled.ok) return doubled;
1544
+
1545
+ return ok(doubled.value);
1546
+ };
1547
+
1548
+ // Both produce identical results
1549
+ assertEqual(withGenerator("21"), imperative("21"));
1550
+ assertEqual(withGenerator("abc"), imperative("abc"));
1551
+ assertEqual(withGenerator("-5"), imperative("-5"));
1552
+ });
1553
+
1554
+ it("shows type inference works correctly", () => {
1555
+ const program = function* (): Gen<number, ParseError | ValidationError> {
1556
+ const a = yield* gen(parse("10"));
1557
+ const b = yield* gen(validate(a));
1558
+ return b * 2;
1559
+ };
1560
+
1561
+ const result = runGen(program());
1562
+
1563
+ assertType<typeof result, Result<number, ParseError | ValidationError>>();
1564
+ });
1565
+
1566
+ it.skip("generator vs imperative performance", () => {
1567
+ const ITERATIONS = 500_000;
1568
+
1569
+ const withGenerator = (input: string): Result<number, ParseError> =>
1570
+ runGen(
1571
+ (function* (): Gen<number, ParseError> {
1572
+ const a = yield* gen(parse(input));
1573
+ const b = yield* gen(parse(String(a + 1)));
1574
+ const c = yield* gen(parse(String(b + 1)));
1575
+ return c;
1576
+ })(),
1577
+ );
1578
+
1579
+ type IterableResult<T, E> =
1580
+ | {
1581
+ readonly ok: true;
1582
+ readonly value: T;
1583
+ [Symbol.iterator](): Gen<T, E>;
1584
+ }
1585
+ | {
1586
+ readonly ok: false;
1587
+ readonly error: E;
1588
+ [Symbol.iterator](): Gen<T, E>;
1589
+ };
1590
+
1591
+ const iterableOk = <T, E = never>(value: T): IterableResult<T, E> => ({
1592
+ ok: true,
1593
+ value,
1594
+ // oxlint-disable-next-line eslint/require-yield
1595
+ *[Symbol.iterator]() {
1596
+ return value;
1597
+ },
1598
+ });
1599
+
1600
+ const iterableErr = <E, T = never>(error: E): IterableResult<T, E> => ({
1601
+ ok: false,
1602
+ error,
1603
+ *[Symbol.iterator]() {
1604
+ yield { ok: false, error };
1605
+ throw new Error("Unreachable");
1606
+ },
1607
+ });
1608
+
1609
+ const parseIterable = (
1610
+ input: string,
1611
+ ): IterableResult<number, ParseError> => {
1612
+ const n = parseInt(input, 10);
1613
+ return isNaN(n) ? iterableErr({ type: "ParseError" }) : iterableOk(n);
1614
+ };
1615
+
1616
+ const withIterableIterator = (
1617
+ input: string,
1618
+ ): Result<number, ParseError> =>
1619
+ runGen(
1620
+ (function* (): Gen<number, ParseError> {
1621
+ const a = yield* parseIterable(input);
1622
+ const b = yield* parseIterable(String(a + 1));
1623
+ const c = yield* parseIterable(String(b + 1));
1624
+ return c;
1625
+ })(),
1626
+ );
1627
+
1628
+ const iterableProgram = function* (
1629
+ input: string,
1630
+ ): Gen<number, ParseError> {
1631
+ const a = yield* parseIterable(input);
1632
+ const b = yield* parseIterable(String(a + 1));
1633
+ const c = yield* parseIterable(String(b + 1));
1634
+ return c;
1635
+ };
1636
+ const withIterableIteratorHoisted = (
1637
+ input: string,
1638
+ ): Result<number, ParseError> => runGen(iterableProgram(input));
1639
+
1640
+ const imperative = (input: string): Result<number, ParseError> => {
1641
+ const a = parse(input);
1642
+ if (!a.ok) return a;
1643
+ const b = parse(String(a.value + 1));
1644
+ if (!b.ok) return b;
1645
+ const c = parse(String(b.value + 1));
1646
+ if (!c.ok) return c;
1647
+ return ok(c.value);
1648
+ };
1649
+
1650
+ const generatorStart = performance.now();
1651
+ for (let i = 0; i < ITERATIONS; i++) {
1652
+ withGenerator("1");
1653
+ }
1654
+ const generatorTime = performance.now() - generatorStart;
1655
+
1656
+ const iterableStart = performance.now();
1657
+ for (let i = 0; i < ITERATIONS; i++) {
1658
+ withIterableIterator("1");
1659
+ }
1660
+ const iterableTime = performance.now() - iterableStart;
1661
+
1662
+ const iterableHoistedStart = performance.now();
1663
+ for (let i = 0; i < ITERATIONS; i++) {
1664
+ withIterableIteratorHoisted("1");
1665
+ }
1666
+ const iterableHoistedTime = performance.now() - iterableHoistedStart;
1667
+
1668
+ const imperativeStart = performance.now();
1669
+ for (let i = 0; i < ITERATIONS; i++) {
1670
+ imperative("1");
1671
+ }
1672
+ const imperativeTime = performance.now() - imperativeStart;
1673
+
1674
+ /* oxlint-disable eslint/no-console -- This benchmark reports each measured implementation. */
1675
+ console.log(`Generator (wrapper): ${generatorTime.toFixed(2)} ms`);
1676
+ console.log(`Iterable (inline): ${iterableTime.toFixed(2)} ms`);
1677
+ console.log(`Iterable (hoisted): ${iterableHoistedTime.toFixed(2)} ms`);
1678
+ console.log(`Imperative: ${imperativeTime.toFixed(2)} ms`);
1679
+ console.log(
1680
+ `Generator wrapper is ${(generatorTime / imperativeTime).toFixed(1)}x slower`,
1681
+ );
1682
+ console.log(
1683
+ `Iterable inline is ${(iterableTime / imperativeTime).toFixed(1)}x slower`,
1684
+ );
1685
+ console.log(
1686
+ `Iterable hoisted is ${(iterableHoistedTime / imperativeTime).toFixed(1)}x slower`,
1687
+ );
1688
+ /* oxlint-enable eslint/no-console */
1689
+ });
1690
+
1691
+ it("does not run a lazy generator called without yield*", () => {
1692
+ let operationRun = false;
1693
+
1694
+ // oxlint-disable-next-line eslint/require-yield -- Generator bodies are lazy even without yield.
1695
+ const lazyOperation = function* (): Gen<number, never> {
1696
+ operationRun = true;
1697
+ return 1;
1698
+ };
1699
+
1700
+ const program = function* (): Gen<void, never> {
1701
+ lazyOperation();
1702
+ return yield* gen(ok());
1703
+ };
1704
+
1705
+ assertOk(runGen(program()), undefined);
1706
+ assertFalse(operationRun);
1707
+ });
1708
+
1709
+ it("disposes resources when generator exits early on error", () => {
1710
+ // This test demonstrates that runGen properly cleans up resources
1711
+ // by calling gen.return() when it encounters an error.
1712
+ // This triggers finally blocks and `using` disposal in the generator.
1713
+
1714
+ const disposed: Array<string> = [];
1715
+
1716
+ const createTestResource = (
1717
+ id: string,
1718
+ shouldFail: boolean,
1719
+ ): Result<Disposable, ParseError> => {
1720
+ if (shouldFail) return err({ type: "ParseError" });
1721
+ return ok({
1722
+ [Symbol.dispose]: () => {
1723
+ disposed.push(id);
1724
+ },
1725
+ });
1726
+ };
1727
+
1728
+ const program = function* (): Gen<string, ParseError> {
1729
+ using disposer = new DisposableStack();
1730
+
1731
+ const r1 = yield* gen(createTestResource("db", false));
1732
+ disposer.use(r1);
1733
+
1734
+ // This fails - generator yields Err and runGen calls gen.return()
1735
+ const r2 = yield* gen(createTestResource("file", true));
1736
+ disposer.use(r2);
1737
+
1738
+ return "done";
1739
+ };
1740
+
1741
+ const result = runGen(program());
1742
+
1743
+ assertErr(result, { type: "ParseError" });
1744
+ // Resources ARE disposed because runGen calls gen.return() on error
1745
+ assertEqual(disposed, ["db"]);
1746
+ });
1747
+
1748
+ it("disposes resources when generator completes successfully", () => {
1749
+ const disposed: Array<string> = [];
1750
+
1751
+ const createTestResource = (id: string): Result<Disposable, ParseError> =>
1752
+ ok({
1753
+ [Symbol.dispose]: () => {
1754
+ disposed.push(id);
1755
+ },
1756
+ });
1757
+
1758
+ const program = function* (): Gen<string, ParseError> {
1759
+ using disposer = new DisposableStack();
1760
+
1761
+ const r1 = yield* gen(createTestResource("db"));
1762
+ disposer.use(r1);
1763
+
1764
+ const r2 = yield* gen(createTestResource("file"));
1765
+ disposer.use(r2);
1766
+
1767
+ return "done";
1768
+ };
1769
+
1770
+ const result = runGen(program());
1771
+
1772
+ assertOk(result, "done");
1773
+ // Resources ARE disposed on successful completion
1774
+ assertEqual(disposed, ["file", "db"]);
1775
+ });
1776
+
1777
+ it("supports direct yield* with the iterator protocol", () => {
1778
+ type IterableResult<T, E> =
1779
+ | {
1780
+ readonly ok: true;
1781
+ readonly value: T;
1782
+ [Symbol.iterator](): Gen<T, E>;
1783
+ }
1784
+ | {
1785
+ readonly ok: false;
1786
+ readonly error: E;
1787
+ [Symbol.iterator](): Gen<T, E>;
1788
+ };
1789
+
1790
+ const iterableOk = <T, E = never>(value: T): IterableResult<T, E> => ({
1791
+ ok: true,
1792
+ value,
1793
+ // oxlint-disable-next-line eslint/require-yield
1794
+ *[Symbol.iterator]() {
1795
+ return value;
1796
+ },
1797
+ });
1798
+
1799
+ const iterableErr = <E, T = never>(error: E): IterableResult<T, E> => ({
1800
+ ok: false,
1801
+ error,
1802
+ *[Symbol.iterator]() {
1803
+ yield { ok: false, error };
1804
+ throw new Error("Unreachable");
1805
+ },
1806
+ });
1807
+
1808
+ const parse = (input: string): IterableResult<number, ParseError> => {
1809
+ const n = parseInt(input, 10);
1810
+ return isNaN(n) ? iterableErr({ type: "ParseError" }) : iterableOk(n);
1811
+ };
1812
+
1813
+ const validate = (n: number): IterableResult<number, ValidationError> =>
1814
+ n > 0 ? iterableOk(n) : iterableErr({ type: "ValidationError" });
1815
+
1816
+ const program = function* (
1817
+ input: string,
1818
+ ): Gen<number, ParseError | ValidationError> {
1819
+ const parsed = yield* parse(input);
1820
+ const validated = yield* validate(parsed);
1821
+ return validated * 2;
1822
+ };
1823
+
1824
+ assertEqual(runGen(program("21")), ok(42));
1825
+ assertEqual(runGen(program("abc")), err({ type: "ParseError" }));
1826
+ assertEqual(runGen(program("-5")), err({ type: "ValidationError" }));
1827
+ });
1828
+ });
1829
+ });