@evolu/common 8.1.0 → 8.3.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 (232) hide show
  1. package/dist/src/Function.d.ts +1 -0
  2. package/dist/src/Function.d.ts.map +1 -1
  3. package/dist/src/Http.d.ts +9 -0
  4. package/dist/src/Http.d.ts.map +1 -1
  5. package/dist/src/Number.d.ts +66 -6
  6. package/dist/src/Number.d.ts.map +1 -1
  7. package/dist/src/Number.js +11 -3
  8. package/dist/src/Object.d.ts +1 -0
  9. package/dist/src/Object.d.ts.map +1 -1
  10. package/dist/src/RefCount.d.ts +1 -0
  11. package/dist/src/RefCount.d.ts.map +1 -1
  12. package/dist/src/Relation.d.ts +2 -0
  13. package/dist/src/Relation.d.ts.map +1 -1
  14. package/dist/src/Resource.d.ts +1 -0
  15. package/dist/src/Resource.d.ts.map +1 -1
  16. package/dist/src/Result.d.ts +383 -548
  17. package/dist/src/Result.d.ts.map +1 -1
  18. package/dist/src/Result.js +296 -46
  19. package/dist/src/Schedule.d.ts +2 -2
  20. package/dist/src/Schedule.d.ts.map +1 -1
  21. package/dist/src/StackTrace.d.ts +1 -0
  22. package/dist/src/StackTrace.d.ts.map +1 -1
  23. package/dist/src/Task.d.ts +978 -1061
  24. package/dist/src/Task.d.ts.map +1 -1
  25. package/dist/src/Task.js +1010 -159
  26. package/dist/src/Time.d.ts +71 -34
  27. package/dist/src/Time.d.ts.map +1 -1
  28. package/dist/src/Time.js +77 -1
  29. package/dist/src/Type.d.ts +1740 -795
  30. package/dist/src/Type.d.ts.map +1 -1
  31. package/dist/src/Type.js +1634 -211
  32. package/dist/src/Types.d.ts +0 -24
  33. package/dist/src/Types.d.ts.map +1 -1
  34. package/dist/src/intl/_en.d.ts +126 -0
  35. package/dist/src/intl/_en.d.ts.map +1 -0
  36. package/dist/src/intl/_en.js +267 -0
  37. package/dist/src/intl/ar.d.ts +117 -0
  38. package/dist/src/intl/ar.d.ts.map +1 -0
  39. package/dist/src/intl/ar.js +245 -0
  40. package/dist/src/intl/bn.d.ts +117 -0
  41. package/dist/src/intl/bn.d.ts.map +1 -0
  42. package/dist/src/intl/bn.js +258 -0
  43. package/dist/src/intl/ca.d.ts +117 -0
  44. package/dist/src/intl/ca.d.ts.map +1 -0
  45. package/dist/src/intl/ca.js +258 -0
  46. package/dist/src/intl/cs.d.ts +30 -7
  47. package/dist/src/intl/cs.d.ts.map +1 -1
  48. package/dist/src/intl/cs.js +65 -28
  49. package/dist/src/intl/da.d.ts +117 -0
  50. package/dist/src/intl/da.d.ts.map +1 -0
  51. package/dist/src/intl/da.js +258 -0
  52. package/dist/src/intl/de.d.ts +117 -0
  53. package/dist/src/intl/de.d.ts.map +1 -0
  54. package/dist/src/intl/de.js +258 -0
  55. package/dist/src/intl/el.d.ts +117 -0
  56. package/dist/src/intl/el.d.ts.map +1 -0
  57. package/dist/src/intl/el.js +265 -0
  58. package/dist/src/intl/es.d.ts +117 -0
  59. package/dist/src/intl/es.d.ts.map +1 -0
  60. package/dist/src/intl/es.js +258 -0
  61. package/dist/src/intl/fa.d.ts +117 -0
  62. package/dist/src/intl/fa.d.ts.map +1 -0
  63. package/dist/src/intl/fa.js +258 -0
  64. package/dist/src/intl/fi.d.ts +117 -0
  65. package/dist/src/intl/fi.d.ts.map +1 -0
  66. package/dist/src/intl/fi.js +258 -0
  67. package/dist/src/intl/fil.d.ts +117 -0
  68. package/dist/src/intl/fil.d.ts.map +1 -0
  69. package/dist/src/intl/fil.js +258 -0
  70. package/dist/src/intl/fr.d.ts +117 -0
  71. package/dist/src/intl/fr.d.ts.map +1 -0
  72. package/dist/src/intl/fr.js +258 -0
  73. package/dist/src/intl/he.d.ts +117 -0
  74. package/dist/src/intl/he.d.ts.map +1 -0
  75. package/dist/src/intl/he.js +258 -0
  76. package/dist/src/intl/hi.d.ts +117 -0
  77. package/dist/src/intl/hi.d.ts.map +1 -0
  78. package/dist/src/intl/hi.js +258 -0
  79. package/dist/src/intl/hr.d.ts +117 -0
  80. package/dist/src/intl/hr.d.ts.map +1 -0
  81. package/dist/src/intl/hr.js +265 -0
  82. package/dist/src/intl/hu.d.ts +63 -0
  83. package/dist/src/intl/hu.d.ts.map +1 -0
  84. package/dist/src/intl/hu.js +204 -0
  85. package/dist/src/intl/id.d.ts +117 -0
  86. package/dist/src/intl/id.d.ts.map +1 -0
  87. package/dist/src/intl/id.js +258 -0
  88. package/dist/src/intl/index.d.ts +42 -0
  89. package/dist/src/intl/index.d.ts.map +1 -1
  90. package/dist/src/intl/index.js +42 -0
  91. package/dist/src/intl/it.d.ts +117 -0
  92. package/dist/src/intl/it.d.ts.map +1 -0
  93. package/dist/src/intl/it.js +258 -0
  94. package/dist/src/intl/ja.d.ts +117 -0
  95. package/dist/src/intl/ja.d.ts.map +1 -0
  96. package/dist/src/intl/ja.js +258 -0
  97. package/dist/src/intl/ko.d.ts +117 -0
  98. package/dist/src/intl/ko.d.ts.map +1 -0
  99. package/dist/src/intl/ko.js +258 -0
  100. package/dist/src/intl/ml.d.ts +117 -0
  101. package/dist/src/intl/ml.d.ts.map +1 -0
  102. package/dist/src/intl/ml.js +258 -0
  103. package/dist/src/intl/mr.d.ts +117 -0
  104. package/dist/src/intl/mr.d.ts.map +1 -0
  105. package/dist/src/intl/mr.js +258 -0
  106. package/dist/src/intl/ms.d.ts +117 -0
  107. package/dist/src/intl/ms.d.ts.map +1 -0
  108. package/dist/src/intl/ms.js +245 -0
  109. package/dist/src/intl/nb.d.ts +63 -0
  110. package/dist/src/intl/nb.d.ts.map +1 -0
  111. package/dist/src/intl/nb.js +191 -0
  112. package/dist/src/intl/nl.d.ts +117 -0
  113. package/dist/src/intl/nl.d.ts.map +1 -0
  114. package/dist/src/intl/nl.js +258 -0
  115. package/dist/src/intl/pa.d.ts +117 -0
  116. package/dist/src/intl/pa.d.ts.map +1 -0
  117. package/dist/src/intl/pa.js +258 -0
  118. package/dist/src/intl/pl.d.ts +117 -0
  119. package/dist/src/intl/pl.d.ts.map +1 -0
  120. package/dist/src/intl/pl.js +242 -0
  121. package/dist/src/intl/pt-BR.d.ts +117 -0
  122. package/dist/src/intl/pt-BR.d.ts.map +1 -0
  123. package/dist/src/intl/pt-BR.js +258 -0
  124. package/dist/src/intl/pt.d.ts +117 -0
  125. package/dist/src/intl/pt.d.ts.map +1 -0
  126. package/dist/src/intl/pt.js +246 -0
  127. package/dist/src/intl/ro.d.ts +117 -0
  128. package/dist/src/intl/ro.d.ts.map +1 -0
  129. package/dist/src/intl/ro.js +258 -0
  130. package/dist/src/intl/sk.d.ts +117 -0
  131. package/dist/src/intl/sk.d.ts.map +1 -0
  132. package/dist/src/intl/sk.js +244 -0
  133. package/dist/src/intl/sl.d.ts +117 -0
  134. package/dist/src/intl/sl.d.ts.map +1 -0
  135. package/dist/src/intl/sl.js +258 -0
  136. package/dist/src/intl/sv.d.ts +117 -0
  137. package/dist/src/intl/sv.d.ts.map +1 -0
  138. package/dist/src/intl/sv.js +258 -0
  139. package/dist/src/intl/sw.d.ts +63 -0
  140. package/dist/src/intl/sw.d.ts.map +1 -0
  141. package/dist/src/intl/sw.js +191 -0
  142. package/dist/src/intl/ta.d.ts +117 -0
  143. package/dist/src/intl/ta.d.ts.map +1 -0
  144. package/dist/src/intl/ta.js +258 -0
  145. package/dist/src/intl/te.d.ts +117 -0
  146. package/dist/src/intl/te.d.ts.map +1 -0
  147. package/dist/src/intl/te.js +258 -0
  148. package/dist/src/intl/th.d.ts +117 -0
  149. package/dist/src/intl/th.d.ts.map +1 -0
  150. package/dist/src/intl/th.js +258 -0
  151. package/dist/src/intl/tr.d.ts +117 -0
  152. package/dist/src/intl/tr.d.ts.map +1 -0
  153. package/dist/src/intl/tr.js +255 -0
  154. package/dist/src/intl/uk.d.ts +117 -0
  155. package/dist/src/intl/uk.d.ts.map +1 -0
  156. package/dist/src/intl/uk.js +258 -0
  157. package/dist/src/intl/ur.d.ts +117 -0
  158. package/dist/src/intl/ur.d.ts.map +1 -0
  159. package/dist/src/intl/ur.js +245 -0
  160. package/dist/src/intl/vi.d.ts +117 -0
  161. package/dist/src/intl/vi.d.ts.map +1 -0
  162. package/dist/src/intl/vi.js +255 -0
  163. package/dist/src/intl/zh-CN.d.ts +117 -0
  164. package/dist/src/intl/zh-CN.d.ts.map +1 -0
  165. package/dist/src/intl/zh-CN.js +258 -0
  166. package/dist/src/intl/zh-TW.d.ts +117 -0
  167. package/dist/src/intl/zh-TW.d.ts.map +1 -0
  168. package/dist/src/intl/zh-TW.js +258 -0
  169. package/dist/src/local-first/Owner.d.ts +156 -62
  170. package/dist/src/local-first/Owner.d.ts.map +1 -1
  171. package/dist/src/local-first/Owner.js +119 -20
  172. package/package.json +1 -1
  173. package/src/Function.ts +2 -0
  174. package/src/Http.ts +9 -0
  175. package/src/Number.ts +85 -14
  176. package/src/Object.ts +2 -0
  177. package/src/RefCount.ts +2 -0
  178. package/src/Relation.ts +4 -0
  179. package/src/Resource.ts +2 -0
  180. package/src/Result.ts +397 -560
  181. package/src/Schedule.ts +1 -1
  182. package/src/StackTrace.ts +2 -0
  183. package/src/Task.ts +1145 -1225
  184. package/src/Time.ts +110 -62
  185. package/src/Type.ts +2842 -835
  186. package/src/Types.ts +0 -49
  187. package/src/intl/_en.ts +497 -0
  188. package/src/intl/ar.ts +417 -0
  189. package/src/intl/bn.ts +486 -0
  190. package/src/intl/ca.ts +488 -0
  191. package/src/intl/cs.ts +131 -32
  192. package/src/intl/da.ts +488 -0
  193. package/src/intl/de.ts +488 -0
  194. package/src/intl/el.ts +494 -0
  195. package/src/intl/es.ts +488 -0
  196. package/src/intl/fa.ts +487 -0
  197. package/src/intl/fi.ts +487 -0
  198. package/src/intl/fil.ts +488 -0
  199. package/src/intl/fr.ts +488 -0
  200. package/src/intl/he.ts +486 -0
  201. package/src/intl/hi.ts +487 -0
  202. package/src/intl/hr.ts +494 -0
  203. package/src/intl/hu.ts +387 -0
  204. package/src/intl/id.ts +487 -0
  205. package/src/intl/index.ts +42 -0
  206. package/src/intl/it.ts +488 -0
  207. package/src/intl/ja.ts +488 -0
  208. package/src/intl/ko.ts +487 -0
  209. package/src/intl/ml.ts +487 -0
  210. package/src/intl/mr.ts +487 -0
  211. package/src/intl/ms.ts +410 -0
  212. package/src/intl/nb.ts +364 -0
  213. package/src/intl/nl.ts +488 -0
  214. package/src/intl/pa.ts +487 -0
  215. package/src/intl/pl.ts +450 -0
  216. package/src/intl/pt-BR.ts +487 -0
  217. package/src/intl/pt.ts +424 -0
  218. package/src/intl/ro.ts +488 -0
  219. package/src/intl/sk.ts +412 -0
  220. package/src/intl/sl.ts +487 -0
  221. package/src/intl/sv.ts +488 -0
  222. package/src/intl/sw.ts +324 -0
  223. package/src/intl/ta.ts +487 -0
  224. package/src/intl/te.ts +487 -0
  225. package/src/intl/th.ts +484 -0
  226. package/src/intl/tr.ts +487 -0
  227. package/src/intl/uk.ts +488 -0
  228. package/src/intl/ur.ts +410 -0
  229. package/src/intl/vi.ts +435 -0
  230. package/src/intl/zh-CN.ts +482 -0
  231. package/src/intl/zh-TW.ts +482 -0
  232. package/src/local-first/Owner.ts +156 -62
package/src/Result.ts CHANGED
@@ -1,34 +1,27 @@
1
1
  /**
2
- * Type-safe error handling with the Result type.
2
+ * ## Intro
3
3
  *
4
- * @module
5
- */
6
-
7
- import {
8
- emptyArray,
9
- isNonEmptyArray,
10
- type NonEmptyReadonlyArray,
11
- } from "./Array.ts";
12
- import { assert } from "./Assert.ts";
13
- import { exhaustiveCheck, type Thunk } from "./Function.ts";
14
- import { createMutableRecord, emptyRecord, isIterable } from "./Object.ts";
15
- import type { Typed } from "./Type.ts";
16
- import type { Awaitable } from "./Types.ts";
17
-
18
- /**
19
- * The problem with `throw` in JavaScript is that the caught value is always of
4
+ * Type-safe error handling with {@link Result}.
5
+ *
6
+ * The problem with exceptions in JavaScript is that a caught value is always of
20
7
  * unknown type. We can't be sure all errors have been handled because the
21
- * TypeScript compiler can't tell us what was thrown — we can't use
8
+ * TypeScript compiler can't tell us what might be thrown — we can't use
22
9
  * {@link exhaustiveCheck}.
23
10
  *
24
- * Languages like Rust and Haskell solve this with types like `Result` or
25
- * `Either` where errors are part of the return type. TypeScript can do the
26
- * same.
11
+ * Languages like Rust and Haskell model recoverable failures with types such as
12
+ * `Result` and `Either`, making errors part of the return type.
27
13
  *
28
- * ## Example
14
+ * TypeScript can express the same pattern with discriminated unions of plain
15
+ * objects. In Evolu, domain error interfaces extend {@link Typed} to avoid
16
+ * repeating the literal `type` discriminant.
29
17
  *
30
18
  * ```ts
31
- * import { err, exhaustiveCheck, type Result } from "@evolu/common";
19
+ * import {
20
+ * err,
21
+ * exhaustiveCheck,
22
+ * type Result,
23
+ * type Typed,
24
+ * } from "@evolu/common";
32
25
  *
33
26
  * // TypeScript can't know what was thrown.
34
27
  * try {
@@ -40,16 +33,12 @@ import type { Awaitable } from "./Types.ts";
40
33
  * // With Result, errors are part of the return type.
41
34
  * const doSomething = (): Result<
42
35
  * number,
43
- * NotFoundError | InvalidInputError
36
+ * InvalidInputError | NotFoundError
44
37
  * > => err({ type: "NotFound" });
45
38
  *
46
- * interface NotFoundError {
47
- * readonly type: "NotFound";
48
- * }
39
+ * interface InvalidInputError extends Typed<"InvalidInput"> {}
49
40
  *
50
- * interface InvalidInputError {
51
- * readonly type: "InvalidInput";
52
- * }
41
+ * interface NotFoundError extends Typed<"NotFound"> {}
53
42
  *
54
43
  * // With Result, the error type is known and exhaustiveCheck works.
55
44
  * // If we add another error type, TypeScript tells us it isn't handled.
@@ -86,41 +75,37 @@ import type { Awaitable } from "./Types.ts";
86
75
  * }
87
76
  * ```
88
77
  *
89
- * Use {@link trySync} and {@link tryAsync} to intentionally convert thrown or
90
- * rejected errors into typed, recoverable `Result` values.
91
- *
92
- * Do not wrap every throwing API in `Result`. If an error is unrecoverable and
93
- * the caller has no meaningful fallback, let it throw.
78
+ * Use {@link trySync} and {@link tryAsync} to intentionally convert thrown values
79
+ * and Promise rejections into typed, recoverable errors represented by
80
+ * `Result`.
94
81
  *
95
- * ## Composition
82
+ * Do not wrap every API that throws or rejects in `Result`. If an error is
83
+ * unrecoverable and the caller has no meaningful fallback, let it propagate.
96
84
  *
97
85
  * Since `Result` is a plain object, imperative code works naturally.
98
86
  *
99
87
  * ```ts
100
- * import { ok, type Result } from "@evolu/common";
88
+ * import { ok, type Result, type Typed } from "@evolu/common";
101
89
  *
102
90
  * interface User {
103
91
  * readonly id: string;
104
92
  * }
105
93
  *
106
- * interface UserNotFoundError {
107
- * readonly type: "UserNotFoundError";
108
- * }
94
+ * const getUser = (): Result<User, UserNotFoundError> =>
95
+ * ok({ id: "user-1" });
96
+ *
97
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {}
109
98
  *
110
99
  * interface Profile {
111
100
  * readonly userId: string;
112
101
  * }
113
102
  *
114
- * interface ProfileNotFoundError {
115
- * readonly type: "ProfileNotFoundError";
116
- * }
117
- *
118
- * const getUser = (): Result<User, UserNotFoundError> =>
119
- * ok({ id: "user-1" });
120
103
  * const getProfile = (
121
104
  * userId: string,
122
105
  * ): Result<Profile, ProfileNotFoundError> => ok({ userId });
123
106
  *
107
+ * interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
108
+ *
124
109
  * const getCurrentProfile = (): Result<
125
110
  * Profile,
126
111
  * UserNotFoundError | ProfileNotFoundError
@@ -138,211 +123,26 @@ import type { Awaitable } from "./Types.ts";
138
123
  * expectOk(profile, { userId: "user-1" });
139
124
  * ```
140
125
  *
141
- * {@link flatMapResult} is for a single local composition.
142
- *
143
- * ```ts
144
- * import { flatMapResult, ok, type Result } from "@evolu/common";
145
- *
146
- * interface User {
147
- * readonly id: string;
148
- * }
149
- *
150
- * interface UserNotFoundError {
151
- * readonly type: "UserNotFoundError";
152
- * }
153
- *
154
- * interface Profile {
155
- * readonly userId: string;
156
- * }
157
- *
158
- * interface ProfileNotFoundError {
159
- * readonly type: "ProfileNotFoundError";
160
- * }
161
- *
162
- * const getUser = (): Result<User, UserNotFoundError> =>
163
- * ok({ id: "user-1" });
164
- * const getProfile = (
165
- * userId: string,
166
- * ): Result<Profile, ProfileNotFoundError> => ok({ userId });
167
- *
168
- * const profile = flatMapResult(getUser(), (user) => getProfile(user.id));
169
- * expectTypeOf(profile).toEqualTypeOf<
170
- * Result<Profile, UserNotFoundError | ProfileNotFoundError>
171
- * >();
172
- * expectOk(profile, { userId: "user-1" });
173
- * ```
174
- *
175
- * Use {@link allResult} to collect the Ok values from a collection of Results,
176
- * or return the first Err.
177
- *
178
- * ```ts
179
- * import { allResult, err, ok, type Result } from "@evolu/common";
180
- *
181
- * interface User {
182
- * readonly id: string;
183
- * }
184
- *
185
- * interface Preferences {
186
- * readonly theme: "dark" | "light";
187
- * }
188
- *
189
- * interface LoadAccountError {
190
- * readonly type: "LoadAccountError";
191
- * readonly resource: "user" | "preferences";
192
- * }
193
- *
194
- * const user: Result<User, LoadAccountError> = ok({ id: "user-1" });
195
- * const preferences: Result<Preferences, LoadAccountError> = ok({
196
- * theme: "dark",
197
- * });
198
- * const account = allResult([user, preferences]);
199
- * expectTypeOf(account).toEqualTypeOf<
200
- * Result<readonly [User, Preferences], LoadAccountError>
201
- * >();
202
- * expectOk(account, [{ id: "user-1" }, { theme: "dark" }]);
203
- *
204
- * const userError: Result<never, LoadAccountError> = err({
205
- * type: "LoadAccountError",
206
- * resource: "user",
207
- * });
208
- * const preferencesError: Result<never, LoadAccountError> = err({
209
- * type: "LoadAccountError",
210
- * resource: "preferences",
211
- * });
212
- * const failedAccount = allResult([userError, preferencesError]);
213
- * expectErr(failedAccount, {
214
- * type: "LoadAccountError",
215
- * resource: "user",
216
- * });
217
- * ```
218
- *
219
- * `allResult` can also map collection values to Results before collecting their
220
- * Ok values. Mapping stops on the first Err.
221
- *
222
- * ```ts
223
- * import { allResult, err, ok, type Result } from "@evolu/common";
224
- *
225
- * interface OrderLine {
226
- * readonly productId: string;
227
- * readonly quantity: number;
228
- * }
229
- *
230
- * interface ProductUnavailableError {
231
- * readonly type: "ProductUnavailableError";
232
- * readonly productId: string;
233
- * }
234
- *
235
- * const priceByProductId = new Map([["book", 20]]);
236
- * const priceOrderLine = ({
237
- * productId,
238
- * quantity,
239
- * }: OrderLine): Result<number, ProductUnavailableError> => {
240
- * const price = priceByProductId.get(productId);
241
- * if (price == null) {
242
- * return err({ type: "ProductUnavailableError", productId });
243
- * }
244
- * return ok(price * quantity);
245
- * };
246
- *
247
- * const orderLines: ReadonlyArray<OrderLine> = [
248
- * { productId: "book", quantity: 2 },
249
- * { productId: "sold-out", quantity: 1 },
250
- * ];
251
- * const prices = allResult(orderLines, priceOrderLine);
252
- * expectTypeOf(prices).toEqualTypeOf<
253
- * Result<ReadonlyArray<number>, ProductUnavailableError>
254
- * >();
255
- * expectErr(prices, {
256
- * type: "ProductUnavailableError",
257
- * productId: "sold-out",
258
- * });
259
- * ```
126
+ * Note `user` and `profile` are named after their success values, not after the
127
+ * Result (`userResult`, `profileResult`).
260
128
  *
261
- * For side effects, or whenever the success values aren't needed, disable
262
- * collection with `{ collect: false }`. `allResult` then returns `Result<void,
263
- * E>` without storing the successful values and still stops on the first Err.
264
- *
265
- * ```ts
266
- * import { allResult, err, ok, type Result } from "@evolu/common";
267
- *
268
- * interface User {
269
- * readonly id: string;
270
- * }
271
- *
272
- * interface SaveUserError {
273
- * readonly type: "SaveUserError";
274
- * }
275
- *
276
- * interface SendWelcomeEmailError {
277
- * readonly type: "SendWelcomeEmailError";
278
- * }
279
- *
280
- * const saveUser = (_user: User): Result<number, SaveUserError> =>
281
- * err({ type: "SaveUserError" });
282
- *
283
- * let welcomeEmailSent = false;
284
- * const sendWelcomeEmail = (
285
- * _user: User,
286
- * ): Result<void, SendWelcomeEmailError> => {
287
- * welcomeEmailSent = true;
288
- * return ok();
289
- * };
290
- *
291
- * const registerUser = (
292
- * user: User,
293
- * ): Result<void, SaveUserError | SendWelcomeEmailError> =>
294
- * allResult(
295
- * [saveUser, sendWelcomeEmail],
296
- * (operation) => operation(user),
297
- * { collect: false },
298
- * );
299
- *
300
- * const result = registerUser({ id: "user-1" });
301
- * expectTypeOf(result).toEqualTypeOf<
302
- * Result<void, SaveUserError | SendWelcomeEmailError>
303
- * >();
304
- * expectErr(result, { type: "SaveUserError" });
305
- * expect(welcomeEmailSent).toBe(false);
306
- * ```
307
- *
308
- * For the first success, {@link anyResult} returns the first Ok or the last
309
- * error if all fail:
310
- *
311
- * ```ts
312
- * import { anyResult, err, ok, type Result } from "@evolu/common";
313
- *
314
- * interface CacheMissError {
315
- * readonly type: "CacheMissError";
316
- * }
317
- *
318
- * const getCachedPrice = (): Result<number, CacheMissError> =>
319
- * err({ type: "CacheMissError" });
320
- *
321
- * const prices = [getCachedPrice(), ok(20)] as const;
322
- * const price = anyResult(prices);
323
- * expectTypeOf(price).toEqualTypeOf<Result<number, CacheMissError>>();
324
- * expectOk(price, 20);
325
- * ```
326
- *
327
- * ## Naming convention
328
- *
329
- * Name a Result after its value (`user`, `config`), not after the wrapper
330
- * (`userResult`, `configResult`). If it has no success value, name it `result`.
331
- * For several such operations, use `allResult` with `{ collect: false }` as
332
- * shown above.
129
+ * If a Result has no success value, name it `result`. For several such
130
+ * operations, use {@link allResult} with `{ collect: false }`.
333
131
  *
334
132
  * ## Unrecoverable errors
335
133
  *
336
- * Some errors can't be handled locally they must propagate to the top level.
337
- * These are unrecoverable errors: expected (you know they can happen) but only
338
- * handleable at the app level.
134
+ * Unrecoverable errors are failures the application can't handle meaningfully.
135
+ * For example, a database failure is unrecoverable when the application has no
136
+ * useful fallback, retry, or degraded mode.
339
137
  *
340
- * Do not force these errors into `Result` just because the underlying API
341
- * throws. If the local caller cannot recover, let the error propagate to a
342
- * global handler or other app boundary.
138
+ * Do not turn such failures into `Result` merely because the underlying API
139
+ * throws or rejects. Let the exception or Promise rejection propagate to the
140
+ * top-level handler so the application stops instead of continuing in a
141
+ * potentially invalid state.
343
142
  *
344
- * In Evolu apps, the root Run reports defects and the platform lifecycle API
345
- * owns application shutdown. For example, `@evolu/nodejs` provides `runMain`.
143
+ * In Evolu apps, the root {@link Run} reports defects and the platform lifecycle
144
+ * API owns application shutdown. For example, `@evolu/nodejs` provides
145
+ * {@link @evolu/nodejs!runMain | runMain}.
346
146
  *
347
147
  * ## FAQ
348
148
  *
@@ -385,17 +185,56 @@ import type { Awaitable } from "./Types.ts";
385
185
  * Use `Result<void, E>` and return `ok()` (no argument). Don't return
386
186
  * `ok(true)`, `ok("success")`, or `ok("done")` — `ok()` already signals
387
187
  * success; redundant values add noise.
188
+ *
189
+ * @module
190
+ */
191
+ import {
192
+ emptyArray,
193
+ isNonEmptyArray,
194
+ type NonEmptyReadonlyArray,
195
+ } from "./Array.ts";
196
+ import { assert } from "./Assert.ts";
197
+ import { exhaustiveCheck, type Thunk } from "./Function.ts";
198
+ import { createMutableRecord, emptyRecord, isIterable } from "./Object.ts";
199
+ import type { Run } from "./Task.ts";
200
+ import type { Typed } from "./Type.ts";
201
+ import type { Awaitable } from "./Types.ts";
202
+
203
+ /**
204
+ * A discriminated success or failure value: either {@link Ok} or {@link Err}.
205
+ *
206
+ * See the {@link @evolu/common!Result | Result overview}.
207
+ *
208
+ * @group Core
388
209
  */
389
210
  export type Result<T, E = never> = Ok<T> | Err<E>;
390
211
 
391
212
  /**
392
213
  * Shorthand for a {@link Result} with `any` type parameters.
393
214
  *
394
- * @group Utilities
215
+ * @group Core
395
216
  */
396
217
  export type AnyResult = Result<any, any>;
397
218
 
398
- /** A successful {@link Result}. */
219
+ /**
220
+ * A successful {@link Result}.
221
+ *
222
+ * ### Example
223
+ *
224
+ * ```ts
225
+ * import { ok, type Result } from "@evolu/common";
226
+ *
227
+ * const result = ok();
228
+ * const count = ok(42);
229
+ *
230
+ * expectTypeOf(result).toEqualTypeOf<Result<void>>();
231
+ * expectTypeOf(count).toEqualTypeOf<Result<number>>();
232
+ * expectOk(result, undefined);
233
+ * expectOk(count, 42);
234
+ * ```
235
+ *
236
+ * @group Core
237
+ */
399
238
  export interface Ok<out T> {
400
239
  readonly ok: true;
401
240
  readonly value: T;
@@ -405,7 +244,7 @@ export interface Ok<out T> {
405
244
  * An error {@link Result}.
406
245
  *
407
246
  * The `error` property can be any type that describes the error. For domain
408
- * errors, use a plain object with a `type` field for discrimination.
247
+ * errors, define a plain interface extending {@link Typed}.
409
248
  *
410
249
  * ### Example
411
250
  *
@@ -416,23 +255,25 @@ export interface Ok<out T> {
416
255
  * readonly id: string;
417
256
  * }
418
257
  *
419
- * interface NotFoundError extends Typed<"NotFoundError"> {
420
- * readonly id: string;
421
- * }
422
- *
423
258
  * const users = new Map<string, User>([["user-1", { id: "user-1" }]]);
424
259
  * const findUser = (id: string): Result<User, NotFoundError> => {
425
260
  * const user = users.get(id);
426
- * if (user == null) return err({ type: "NotFoundError", id });
261
+ * if (user == null) return err({ type: "NotFound", id });
427
262
  * return ok(user);
428
263
  * };
429
264
  *
265
+ * interface NotFoundError extends Typed<"NotFound"> {
266
+ * readonly id: string;
267
+ * }
268
+ *
430
269
  * expectOk(findUser("user-1"), { id: "user-1" });
431
270
  * expectErr(findUser("missing"), {
432
- * type: "NotFoundError",
271
+ * type: "NotFound",
433
272
  * id: "missing",
434
273
  * });
435
274
  * ```
275
+ *
276
+ * @group Core
436
277
  */
437
278
  export interface Err<out E> {
438
279
  readonly ok: false;
@@ -442,7 +283,7 @@ export interface Err<out E> {
442
283
  /**
443
284
  * Infers the success value type from a {@link Result}.
444
285
  *
445
- * @group Utilities
286
+ * @group Core
446
287
  */
447
288
  export type InferOk<R extends Result<any, any>> =
448
289
  R extends Ok<infer T> ? T : never;
@@ -450,7 +291,7 @@ export type InferOk<R extends Result<any, any>> =
450
291
  /**
451
292
  * Infers the error type from a {@link Result}.
452
293
  *
453
- * @group Utilities
294
+ * @group Core
454
295
  */
455
296
  export type InferErr<R extends Result<any, any>> =
456
297
  R extends Err<infer E> ? E : never;
@@ -462,19 +303,7 @@ export type InferErr<R extends Result<any, any>> =
462
303
  * producing a value.
463
304
  * - `ok(value)` creates a `Result<T, never>` containing the specified value.
464
305
  *
465
- * ### Example
466
- *
467
- * ```ts
468
- * import { ok, type Result } from "@evolu/common";
469
- *
470
- * const noValue = ok();
471
- * const success = ok(42);
472
- *
473
- * expectTypeOf(noValue).toEqualTypeOf<Result<void>>();
474
- * expectTypeOf(success).toEqualTypeOf<Result<number>>();
475
- * expectOk(noValue, undefined);
476
- * expectOk(success, 42);
477
- * ```
306
+ * @group Core
478
307
  */
479
308
  export function ok(): Result<void>;
480
309
  /** Creates an {@link Ok} result with a specified value. */
@@ -487,18 +316,75 @@ export function ok<T>(value?: T): Result<T> {
487
316
  /** Cache ok() and ok(undefined) to avoid repeated allocations. */
488
317
  const okVoid: Result<void> = { ok: true, value: undefined };
489
318
 
490
- /** Creates an {@link Err} result. */
319
+ /**
320
+ * Creates an {@link Err} result.
321
+ *
322
+ * @group Core
323
+ */
491
324
  export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
492
325
 
493
- /** Type guard for {@link Ok} results. */
326
+ /**
327
+ * Type guard for {@link Ok} results.
328
+ *
329
+ * ### Example
330
+ *
331
+ * ```ts
332
+ * import {
333
+ * isOk,
334
+ * ok,
335
+ * type Ok,
336
+ * type Result,
337
+ * type Typed,
338
+ * } from "@evolu/common";
339
+ *
340
+ * const getCount = (): Result<number, CountUnavailableError> => ok(2);
341
+ *
342
+ * interface CountUnavailableError extends Typed<"CountUnavailable"> {}
343
+ *
344
+ * const count = getCount();
345
+ * if (isOk(count)) {
346
+ * expectTypeOf(count).toEqualTypeOf<Ok<number>>();
347
+ * expect(count.value).toBe(2);
348
+ * }
349
+ * ```
350
+ *
351
+ * @group Guards
352
+ */
494
353
  export const isOk = <T, E>(result: Result<T, E>): result is Ok<T> => result.ok;
495
354
 
496
- /** Type guard for {@link Err} results. */
355
+ /**
356
+ * Type guard for {@link Err} results.
357
+ *
358
+ * ### Example
359
+ *
360
+ * ```ts
361
+ * import {
362
+ * err,
363
+ * isErr,
364
+ * type Err,
365
+ * type Result,
366
+ * type Typed,
367
+ * } from "@evolu/common";
368
+ *
369
+ * const getCount = (): Result<number, CountUnavailableError> =>
370
+ * err({ type: "CountUnavailable" });
371
+ *
372
+ * interface CountUnavailableError extends Typed<"CountUnavailable"> {}
373
+ *
374
+ * const count = getCount();
375
+ * if (isErr(count)) {
376
+ * expectTypeOf(count).toEqualTypeOf<Err<CountUnavailableError>>();
377
+ * expect(count.error).toEqual({ type: "CountUnavailable" });
378
+ * }
379
+ * ```
380
+ *
381
+ * @group Guards
382
+ */
497
383
  export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
498
384
  !result.ok;
499
385
 
500
386
  /**
501
- * Gets the value from an `Ok` {@link Result}, or throws if it is an `Err`.
387
+ * Gets the value from an {@link Ok}, or throws for an {@link Err}.
502
388
  *
503
389
  * Use this where failure should crash the current flow instead of being handled
504
390
  * locally.
@@ -506,8 +392,8 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
506
392
  * **When to use:**
507
393
  *
508
394
  * - Application startup or composition-root setup where errors must stop the
509
- * program immediately. In Evolu apps, the root Run reports the defect and the
510
- * platform lifecycle API handles shutdown.
395
+ * program immediately. In Evolu apps, the root {@link Run} reports the defect
396
+ * and the platform lifecycle API handles shutdown.
511
397
  * - Module-level constants
512
398
  * - Test setup with values that are expected to be valid
513
399
  *
@@ -517,33 +403,41 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
517
403
  * ### Example
518
404
  *
519
405
  * ```ts
520
- * import { err, getOrThrow, ok, type Result } from "@evolu/common";
406
+ * import {
407
+ * err,
408
+ * getOrThrow,
409
+ * ok,
410
+ * type Result,
411
+ * type Typed,
412
+ * } from "@evolu/common";
521
413
  *
522
414
  * interface Config {
523
415
  * readonly port: number;
524
416
  * }
525
- * interface InvalidConfigError {
526
- * readonly type: "InvalidConfigError";
527
- * }
417
+ *
528
418
  * const loadConfig = (): Result<Config, InvalidConfigError> =>
529
419
  * ok({ port: 3000 });
530
420
  *
421
+ * interface InvalidConfigError extends Typed<"InvalidConfig"> {}
422
+ *
531
423
  * // At app startup, crash if the config is invalid.
532
424
  * const config = getOrThrow(loadConfig());
533
425
  * expectTypeOf(config).toEqualTypeOf<Config>();
534
426
  * expect(config).toEqual({ port: 3000 });
535
427
  *
536
428
  * try {
537
- * getOrThrow(err({ type: "InvalidConfigError" }));
429
+ * getOrThrow(err({ type: "InvalidConfig" }));
538
430
  * assert.fail("Expected getOrThrow to throw");
539
431
  * } catch (error) {
540
432
  * assert(error instanceof Error);
541
433
  * expect(error.message).toBe("getOrThrow");
542
- * expect(error.cause).toEqual({ type: "InvalidConfigError" });
434
+ * expect(error.cause).toEqual({ type: "InvalidConfig" });
543
435
  * }
544
436
  * ```
545
437
  *
546
438
  * Throws: `Error` with the original error attached as `cause`.
439
+ *
440
+ * @group Unwrapping
547
441
  */
548
442
  export const getOrThrow = <T, E>(result: Result<T, E>): T => {
549
443
  if (result.ok) {
@@ -554,8 +448,7 @@ export const getOrThrow = <T, E>(result: Result<T, E>): T => {
554
448
  };
555
449
 
556
450
  /**
557
- * Gets the value from an `Ok` {@link Result}, or returns `null` if it is an
558
- * `Err`.
451
+ * Gets the value from an {@link Ok}, or returns `null` for an {@link Err}.
559
452
  *
560
453
  * **Intended usage:**
561
454
  *
@@ -566,16 +459,22 @@ export const getOrThrow = <T, E>(result: Result<T, E>): T => {
566
459
  * ### Example
567
460
  *
568
461
  * ```ts
569
- * import { err, getOrNull, ok, type Result } from "@evolu/common";
462
+ * import {
463
+ * err,
464
+ * getOrNull,
465
+ * ok,
466
+ * type Result,
467
+ * type Typed,
468
+ * } from "@evolu/common";
570
469
  *
571
470
  * interface User {
572
471
  * readonly id: string;
573
472
  * }
574
- * interface UserNotFoundError {
575
- * readonly type: "UserNotFoundError";
576
- * }
473
+ *
577
474
  * const findUser = (id: string): Result<User, UserNotFoundError> =>
578
- * id === "user-1" ? ok({ id }) : err({ type: "UserNotFoundError" });
475
+ * id === "user-1" ? ok({ id }) : err({ type: "UserNotFound" });
476
+ *
477
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {}
579
478
  *
580
479
  * // For APIs that expect T | null.
581
480
  * const user = getOrNull(findUser("user-1"));
@@ -585,6 +484,8 @@ export const getOrThrow = <T, E>(result: Result<T, E>): T => {
585
484
  * expect(user).toEqual({ id: "user-1" });
586
485
  * expect(missingUser).toBeNull();
587
486
  * ```
487
+ *
488
+ * @group Unwrapping
588
489
  */
589
490
  export const getOrNull = <T, E>(result: Result<T, E>): T | null =>
590
491
  result.ok ? result.value : null;
@@ -606,6 +507,8 @@ export const getOrNull = <T, E>(result: Result<T, E>): T | null =>
606
507
  * expectTypeOf(count).toEqualTypeOf<number>();
607
508
  * expect(count).toBe(2);
608
509
  * ```
510
+ *
511
+ * @group Unwrapping
609
512
  */
610
513
  export const getOk = <T>(result: Result<T>): T => {
611
514
  assert(result.ok, "Expected Ok result.");
@@ -615,49 +518,53 @@ export const getOk = <T>(result: Result<T>): T => {
615
518
  /**
616
519
  * Wraps a synchronous function that may throw, returning a {@link Result}.
617
520
  *
618
- * `mapError` converts the caught `unknown` value into a typed domain error.
521
+ * When provided, `mapError` converts the caught `unknown` value into a typed
522
+ * domain error.
619
523
  *
620
- * Some APIs use exceptions for both recoverable and unrecoverable failures. In
621
- * that case, convert only the failures the caller can recover from and rethrow
622
- * the rest. `trySync` propagates rethrown failures instead of converting them
623
- * to Err.
524
+ * Some APIs throw for both recoverable and unrecoverable errors. In that case,
525
+ * convert only the errors the caller can recover from and rethrow the rest. The
526
+ * `trySync` error mapper propagates rethrown values instead of converting them
527
+ * to {@link Err}.
624
528
  *
625
529
  * ### Example
626
530
  *
627
531
  * ```ts
628
- * import { trySync, type Result } from "@evolu/common";
629
- *
630
- * interface ReserveSeatError {
631
- * readonly type: "ReserveSeatError";
632
- * readonly seat: string;
633
- * }
532
+ * import { trySync, type Result, type Typed } from "@evolu/common";
634
533
  *
635
534
  * class LegacySeatUnavailableError extends Error {}
636
535
  *
637
536
  * const legacyReserveSeat = (seat: string): void => {
638
537
  * if (seat === "A1") throw new LegacySeatUnavailableError();
639
- * if (seat === "B1") throw new Error("Unexpected database error");
538
+ * if (seat === "B1") throw new Error("Database error");
640
539
  * };
641
540
  *
642
- * const reserveSeat = (seat: string): Result<void, ReserveSeatError> =>
541
+ * const reserveSeat = (seat: string): Result<void, SeatUnavailableError> =>
643
542
  * trySync(
644
543
  * () => legacyReserveSeat(seat),
645
544
  * (error) => {
646
545
  * if (error instanceof LegacySeatUnavailableError) {
647
- * return { type: "ReserveSeatError", seat };
546
+ * return { type: "SeatUnavailable", seat };
648
547
  * }
649
548
  * throw error;
650
549
  * },
651
550
  * );
652
551
  *
552
+ * interface SeatUnavailableError extends Typed<"SeatUnavailable"> {
553
+ * readonly seat: string;
554
+ * }
555
+ *
653
556
  * const result = reserveSeat("B2");
654
- * expectTypeOf(result).toEqualTypeOf<Result<void, ReserveSeatError>>();
557
+ * expectTypeOf(result).toEqualTypeOf<Result<void, SeatUnavailableError>>();
655
558
  * expectOk(result, undefined);
656
- * expectErr(reserveSeat("A1"), { type: "ReserveSeatError", seat: "A1" });
657
- * expect(() => reserveSeat("B1")).toThrow("Unexpected database error");
559
+ * expectErr(reserveSeat("A1"), { type: "SeatUnavailable", seat: "A1" });
560
+ * expect(() => reserveSeat("B1")).toThrow("Database error");
658
561
  * ```
562
+ *
563
+ * @group Exception interop
659
564
  */
660
565
  export function trySync<T>(fn: () => T): Result<T, unknown>;
566
+
567
+ /** Maps caught exceptions to a typed error. */
661
568
  export function trySync<T, E>(
662
569
  fn: () => T,
663
570
  mapError: (error: unknown) => E,
@@ -676,58 +583,60 @@ export function trySync<T, E>(
676
583
  /**
677
584
  * Wraps an async function that may throw or reject, returning a {@link Result}.
678
585
  *
679
- * `mapError` converts the caught `unknown` value into a typed domain error.
586
+ * When provided, `mapError` converts the caught `unknown` value into a typed
587
+ * domain error.
680
588
  *
681
- * Some APIs use exceptions for both recoverable and unrecoverable failures. In
682
- * that case, convert only the failures the caller can recover from and rethrow
683
- * the rest. `tryAsync` rejects with rethrown failures instead of converting
684
- * them to Err.
589
+ * Some async APIs throw or reject for both recoverable and unrecoverable
590
+ * errors. In that case, convert only the errors the caller can recover from and
591
+ * rethrow the rest. The `tryAsync` error mapper leaves rethrown values as
592
+ * Promise rejections instead of converting them to {@link Err}.
685
593
  *
686
594
  * ### Example
687
595
  *
688
596
  * ```ts
689
- * import { tryAsync, type Result } from "@evolu/common";
690
- *
691
- * interface ReserveSeatError {
692
- * readonly type: "ReserveSeatError";
693
- * readonly seat: string;
694
- * }
597
+ * import { tryAsync, type Result, type Typed } from "@evolu/common";
695
598
  *
696
599
  * class LegacySeatUnavailableError extends Error {}
697
600
  *
698
601
  * const legacyReserveSeat = async (seat: string): Promise<void> => {
699
602
  * if (seat === "A1") throw new LegacySeatUnavailableError();
700
- * if (seat === "B1") throw new Error("Unexpected database error");
603
+ * if (seat === "B1") throw new Error("Database error");
701
604
  * };
702
605
  *
703
606
  * const reserveSeat = (
704
607
  * seat: string,
705
- * ): Promise<Result<void, ReserveSeatError>> =>
608
+ * ): Promise<Result<void, SeatUnavailableError>> =>
706
609
  * tryAsync(
707
610
  * () => legacyReserveSeat(seat),
708
611
  * (error) => {
709
612
  * if (error instanceof LegacySeatUnavailableError) {
710
- * return { type: "ReserveSeatError", seat };
613
+ * return { type: "SeatUnavailable", seat };
711
614
  * }
712
615
  * throw error;
713
616
  * },
714
617
  * );
715
618
  *
619
+ * interface SeatUnavailableError extends Typed<"SeatUnavailable"> {
620
+ * readonly seat: string;
621
+ * }
622
+ *
716
623
  * const result = await reserveSeat("B2");
717
- * expectTypeOf(result).toEqualTypeOf<Result<void, ReserveSeatError>>();
624
+ * expectTypeOf(result).toEqualTypeOf<Result<void, SeatUnavailableError>>();
718
625
  * expectOk(result, undefined);
719
626
  * expectErr(await reserveSeat("A1"), {
720
- * type: "ReserveSeatError",
627
+ * type: "SeatUnavailable",
721
628
  * seat: "A1",
722
629
  * });
723
- * await expect(reserveSeat("B1")).rejects.toThrow(
724
- * "Unexpected database error",
725
- * );
630
+ * await expect(reserveSeat("B1")).rejects.toThrow("Database error");
726
631
  * ```
632
+ *
633
+ * @group Exception interop
727
634
  */
728
635
  export function tryAsync<T>(
729
636
  promiseThunk: Thunk<Awaitable<T>>,
730
637
  ): Promise<Result<T, unknown>>;
638
+
639
+ /** Maps thrown or rejected values to a typed error. */
731
640
  export function tryAsync<T, E>(
732
641
  promiseThunk: Thunk<Awaitable<T>>,
733
642
  mapError: (error: unknown) => E,
@@ -749,35 +658,43 @@ export async function tryAsync<T, E>(
749
658
  * The consumer requests the next value (e.g. via `next()`), and the producer
750
659
  * responds with one of:
751
660
  *
752
- * - `Ok<A>` — produced a value
753
- * - `Err<Done<D>>` — completed normally with a done value
754
- * - `Err<E>` — failed with an error
661
+ * - An {@link Ok} containing `A` — produced a value
662
+ * - An {@link Err} containing {@link Done} — completed normally with a done value
663
+ * - An {@link Err} containing `E` — failed with an error
755
664
  *
756
665
  * Inspired by JavaScript's `Iterator.next()`, which returns `{ value, done }`.
757
666
  *
758
667
  * ### Example
759
668
  *
760
669
  * ```ts
761
- * import { done, err, ok, type NextResult } from "@evolu/common";
762
- *
763
- * interface ReadError {
764
- * readonly type: "ReadError";
765
- * }
670
+ * import {
671
+ * done,
672
+ * err,
673
+ * ok,
674
+ * type NextResult,
675
+ * type Typed,
676
+ * } from "@evolu/common";
766
677
  *
767
- * const next = (index: number): NextResult<string, ReadError, number> => {
678
+ * const next = (
679
+ * index: number,
680
+ * ): NextResult<string, ReadFailedError, number> => {
768
681
  * if (index === 0) return ok("first");
769
- * if (index === 1) return err({ type: "ReadError" });
682
+ * if (index === 1) return err({ type: "ReadFailed" });
770
683
  * return err(done(index));
771
684
  * };
772
685
  *
686
+ * interface ReadFailedError extends Typed<"ReadFailed"> {}
687
+ *
773
688
  * const value = next(0);
774
689
  * expectTypeOf(value).toEqualTypeOf<
775
- * NextResult<string, ReadError, number>
690
+ * NextResult<string, ReadFailedError, number>
776
691
  * >();
777
692
  * expectOk(value, "first");
778
- * expectErr(next(1), { type: "ReadError" });
693
+ * expectErr(next(1), { type: "ReadFailed" });
779
694
  * expectErr(next(2), { type: "Done", done: 2 });
780
695
  * ```
696
+ *
697
+ * @group Pull
781
698
  */
782
699
  export type NextResult<A, E = never, D = void> = Result<A, E | Done<D>>;
783
700
 
@@ -789,16 +706,6 @@ export type NextResult<A, E = never, D = void> = Result<A, E | Done<D>>;
789
706
  *
790
707
  * Inspired by JavaScript's `IteratorResult` where `{ done: true }` signals
791
708
  * completion.
792
- */
793
- export interface Done<out D = unknown> extends Typed<"Done"> {
794
- readonly done: D;
795
- }
796
-
797
- /**
798
- * Constructs a {@link Done} value.
799
- *
800
- * - `done()` creates a `Done<void>` for protocols that don't need a done value.
801
- * - `done(value)` creates a `Done<D>` containing the specified value.
802
709
  *
803
710
  * ### Example
804
711
  *
@@ -812,6 +719,20 @@ export interface Done<out D = unknown> extends Typed<"Done"> {
812
719
  * expect(withoutValue).toEqual({ type: "Done", done: undefined });
813
720
  * expect(withValue).toEqual({ type: "Done", done: 42 });
814
721
  * ```
722
+ *
723
+ * @group Pull
724
+ */
725
+ export interface Done<out D = unknown> extends Typed<"Done"> {
726
+ readonly done: D;
727
+ }
728
+
729
+ /**
730
+ * Constructs a {@link Done} value.
731
+ *
732
+ * - `done()` creates a `Done<void>` for protocols that don't need a done value.
733
+ * - `done(value)` creates a `Done<D>` containing the specified value.
734
+ *
735
+ * @group Pull
815
736
  */
816
737
  export function done(): Done<void>;
817
738
  /** With a done value. */
@@ -832,13 +753,16 @@ export function done<D>(value?: D): Done<D> {
832
753
  * ### Example
833
754
  *
834
755
  * ```ts
835
- * import { type Done, type ExcludeDone } from "@evolu/common";
756
+ * import { type Done, type ExcludeDone, type Typed } from "@evolu/common";
757
+ *
758
+ * type Errors = ReadFailedError | Done<number>;
836
759
  *
837
- * type Errors = "ReadError" | Done<number>;
838
- * expectTypeOf<ExcludeDone<Errors>>().toEqualTypeOf<"ReadError">();
760
+ * interface ReadFailedError extends Typed<"ReadFailed"> {}
761
+ *
762
+ * expectTypeOf<ExcludeDone<Errors>>().toEqualTypeOf<ReadFailedError>();
839
763
  * ```
840
764
  *
841
- * @group Utilities
765
+ * @group Pull
842
766
  */
843
767
  export type ExcludeDone<E> = Exclude<E, Done<any>>;
844
768
 
@@ -851,13 +775,16 @@ export type ExcludeDone<E> = Exclude<E, Done<any>>;
851
775
  * ### Example
852
776
  *
853
777
  * ```ts
854
- * import { type Done, type OnlyDone } from "@evolu/common";
778
+ * import { type Done, type OnlyDone, type Typed } from "@evolu/common";
779
+ *
780
+ * type Errors = ReadFailedError | Done<number>;
781
+ *
782
+ * interface ReadFailedError extends Typed<"ReadFailed"> {}
855
783
  *
856
- * type Errors = "ReadError" | Done<number>;
857
784
  * expectTypeOf<OnlyDone<Errors>>().toEqualTypeOf<Done<number>>();
858
785
  * ```
859
786
  *
860
- * @group Utilities
787
+ * @group Pull
861
788
  */
862
789
  export type OnlyDone<E> = Extract<E, Done<any>>;
863
790
 
@@ -867,13 +794,20 @@ export type OnlyDone<E> = Extract<E, Done<any>>;
867
794
  * ### Example
868
795
  *
869
796
  * ```ts
870
- * import { type InferDone, type NextResult } from "@evolu/common";
797
+ * import {
798
+ * type InferDone,
799
+ * type NextResult,
800
+ * type Typed,
801
+ * } from "@evolu/common";
802
+ *
803
+ * type ReadResult = NextResult<string, ReadFailedError, number>;
804
+ *
805
+ * interface ReadFailedError extends Typed<"ReadFailed"> {}
871
806
  *
872
- * type ReadResult = NextResult<string, "ReadError", number>;
873
807
  * expectTypeOf<InferDone<ReadResult>>().toEqualTypeOf<number>();
874
808
  * ```
875
809
  *
876
- * @group Utilities
810
+ * @group Pull
877
811
  */
878
812
  export type InferDone<R extends Result<any, any>> =
879
813
  InferErr<R> extends infer Errors
@@ -894,44 +828,35 @@ export type InferDone<R extends Result<any, any>> =
894
828
  * ### Example
895
829
  *
896
830
  * ```ts
897
- * import { err, flatMapResult, ok, type Result } from "@evolu/common";
831
+ * import {
832
+ * flatMapResult,
833
+ * ok,
834
+ * type Result,
835
+ * type Typed,
836
+ * } from "@evolu/common";
898
837
  *
899
838
  * interface User {
900
839
  * readonly id: string;
901
840
  * }
902
- * interface UserError {
903
- * readonly type: "UserError";
904
- * }
841
+ *
842
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {}
905
843
  *
906
844
  * interface Profile {
907
845
  * readonly userId: string;
908
846
  * }
909
- * interface ProfileError {
910
- * readonly type: "ProfileError";
911
- * }
912
- * const getProfile = (userId: string): Result<Profile, ProfileError> =>
913
- * ok({ userId });
914
- *
915
- * let getProfileCalls = 0;
916
- * const getProfileForUser = (
917
- * user: User,
918
- * ): Result<Profile, ProfileError> => {
919
- * getProfileCalls++;
920
- * return getProfile(user.id);
921
- * };
922
847
  *
923
- * const user: Result<User, UserError> = ok({ id: "user-1" });
924
- * const profile = flatMapResult(user, getProfileForUser);
848
+ * const getProfile = (
849
+ * userId: string,
850
+ * ): Result<Profile, ProfileNotFoundError> => ok({ userId });
851
+ *
852
+ * interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
853
+ *
854
+ * const user: Result<User, UserNotFoundError> = ok({ id: "user-1" });
855
+ * const profile = flatMapResult(user, ({ id }) => getProfile(id));
925
856
  * expectTypeOf(profile).toEqualTypeOf<
926
- * Result<Profile, UserError | ProfileError>
857
+ * Result<Profile, UserNotFoundError | ProfileNotFoundError>
927
858
  * >();
928
859
  * expectOk(profile, { userId: "user-1" });
929
- * expect(getProfileCalls).toBe(1);
930
- *
931
- * const missingUser: Result<User, UserError> = err({ type: "UserError" });
932
- * const missingProfile = flatMapResult(missingUser, getProfileForUser);
933
- * expectErr(missingProfile, { type: "UserError" });
934
- * expect(getProfileCalls).toBe(1);
935
860
  * ```
936
861
  *
937
862
  * @group Composition
@@ -942,41 +867,36 @@ export const flatMapResult = <T, E, U, F>(
942
867
  ): Result<U, E | F> => (result.ok ? fn(result.value) : result);
943
868
 
944
869
  /**
945
- * Collects the Ok values from a collection of {@link Result}s, or maps
946
- * collection values to Results and collects their Ok values.
870
+ * Collects successful values from {@link Result}s.
947
871
  *
948
- * Returns the first error if any result fails.
872
+ * Stops processing the input at the first {@link Err}. Mapping overloads do not
873
+ * call the mapper for the remaining input values.
949
874
  *
950
- * Pass `{ collect: false }` to return `Result<void, E>` without storing the Ok
951
- * values in an output collection.
875
+ * Pass `{ collect: false }` to return `Result<void, E>` without storing the
876
+ * {@link Ok} values in an output collection.
952
877
  *
953
878
  * ### Example
954
879
  *
955
880
  * ```ts
956
- * import { allResult, err, ok, type Result } from "@evolu/common";
881
+ * import { allResult, ok, type Result, type Typed } from "@evolu/common";
957
882
  *
958
- * interface CountError {
959
- * readonly type: "CountError";
960
- * }
961
- * interface LabelError {
962
- * readonly type: "LabelError";
963
- * }
883
+ * const getCount = (): Result<number, CountUnavailableError> => ok(2);
884
+ *
885
+ * interface CountUnavailableError extends Typed<"CountUnavailable"> {}
886
+ *
887
+ * const getLabel = (): Result<string, LabelUnavailableError> =>
888
+ * ok("books");
964
889
  *
965
- * const getCount = (): Result<number, CountError> => ok(2);
966
- * const getLabel = (): Result<string, LabelError> => ok("books");
890
+ * interface LabelUnavailableError extends Typed<"LabelUnavailable"> {}
967
891
  *
968
892
  * const values = allResult([getCount(), getLabel()]);
969
893
  * expectTypeOf(values).toEqualTypeOf<
970
- * Result<readonly [number, string], CountError | LabelError>
894
+ * Result<
895
+ * readonly [number, string],
896
+ * CountUnavailableError | LabelUnavailableError
897
+ * >
971
898
  * >();
972
899
  * expectOk(values, [2, "books"]);
973
- *
974
- * const numbers = allResult([
975
- * ok(1),
976
- * err({ type: "CountError" }),
977
- * err({ type: "LabelError" }),
978
- * ]);
979
- * expectErr(numbers, { type: "CountError" });
980
900
  * ```
981
901
  *
982
902
  * @group Composition
@@ -1011,26 +931,15 @@ export function allResult<T extends Readonly<Record<string, AnyResult>>>(
1011
931
  /**
1012
932
  * For dynamic or generated result lists.
1013
933
  *
1014
- * Stops consuming the iterable on the first Err.
1015
- *
1016
934
  * ### Example
1017
935
  *
1018
936
  * ```ts
1019
- * import { allResult, err, ok, type Result } from "@evolu/common";
1020
- *
1021
- * interface LoadNumberError {
1022
- * readonly type: "LoadNumberError";
1023
- * }
937
+ * import { allResult, ok, type Result } from "@evolu/common";
1024
938
  *
1025
- * const results: ReadonlyArray<Result<number, LoadNumberError>> = [
1026
- * ok(1),
1027
- * err({ type: "LoadNumberError" }),
1028
- * ];
939
+ * const results: ReadonlyArray<Result<number>> = [ok(1), ok(2)];
1029
940
  * const numbers = allResult(results);
1030
- * expectTypeOf(numbers).toEqualTypeOf<
1031
- * Result<ReadonlyArray<number>, LoadNumberError>
1032
- * >();
1033
- * expectErr(numbers, { type: "LoadNumberError" });
941
+ * expectTypeOf(numbers).toEqualTypeOf<Result<ReadonlyArray<number>>>();
942
+ * expectOk(numbers, [1, 2]);
1034
943
  * ```
1035
944
  */
1036
945
  export function allResult<T, E>(
@@ -1062,7 +971,19 @@ export function allResult<T, E>(
1062
971
  results: NonEmptyReadonlyArray<Result<T, E>>,
1063
972
  ): Result<NonEmptyReadonlyArray<T>, E>;
1064
973
 
1065
- /** Does not collect the Ok values from a record of Results. */
974
+ /**
975
+ * Does not collect the Ok values from a record of Results.
976
+ *
977
+ * ### Example
978
+ *
979
+ * ```ts
980
+ * import { allResult, ok, type Result } from "@evolu/common";
981
+ *
982
+ * const result = allResult({ a: ok(1), b: ok(2) }, { collect: false });
983
+ * expectTypeOf(result).toEqualTypeOf<Result<void>>();
984
+ * expectOk(result, undefined);
985
+ * ```
986
+ */
1066
987
  export function allResult<T extends Readonly<Record<string, AnyResult>>>(
1067
988
  results: T,
1068
989
  options: { readonly collect: false },
@@ -1072,25 +993,17 @@ export function allResult<T extends Readonly<Record<string, AnyResult>>>(
1072
993
  * Does not collect the Ok values from an iterable of Results.
1073
994
  *
1074
995
  * Use this overload when the Results already exist and only their collective
1075
- * success or failure matters. To stop invoking operations after the first Err,
1076
- * use the mapping overload instead.
996
+ * success or failure matters.
1077
997
  *
1078
998
  * ### Example
1079
999
  *
1080
1000
  * ```ts
1081
- * import { allResult, err, ok, type Result } from "@evolu/common";
1082
- *
1083
- * interface SaveError {
1084
- * readonly type: "SaveError";
1085
- * }
1001
+ * import { allResult, ok, type Result } from "@evolu/common";
1086
1002
  *
1087
- * const results: ReadonlyArray<Result<number, SaveError>> = [
1088
- * ok(1),
1089
- * err({ type: "SaveError" }),
1090
- * ];
1003
+ * const results: ReadonlyArray<Result<number>> = [ok(1), ok(2)];
1091
1004
  * const result = allResult(results, { collect: false });
1092
- * expectTypeOf(result).toEqualTypeOf<Result<void, SaveError>>();
1093
- * expectErr(result, { type: "SaveError" });
1005
+ * expectTypeOf(result).toEqualTypeOf<Result<void>>();
1006
+ * expectOk(result, undefined);
1094
1007
  * ```
1095
1008
  */
1096
1009
  export function allResult<R extends AnyResult>(
@@ -1111,35 +1024,16 @@ export function allResult<R extends AnyResult>(
1111
1024
  * type Result,
1112
1025
  * } from "@evolu/common";
1113
1026
  *
1114
- * interface Order {
1115
- * readonly id: string;
1116
- * }
1117
- * interface Invoice {
1118
- * readonly orderId: string;
1119
- * }
1120
- * interface CreateInvoiceError {
1121
- * readonly type: "CreateInvoiceError";
1122
- * }
1123
- *
1124
- * const orders: NonEmptyReadonlyArray<Order> = [{ id: "order-1" }];
1125
- * const createInvoice = (
1126
- * order: Order,
1127
- * ): Result<Invoice, CreateInvoiceError> => ok({ orderId: order.id });
1128
- *
1129
- * const invoices = allResult(orders, createInvoice);
1130
- * expectTypeOf(invoices).toEqualTypeOf<
1131
- * Result<NonEmptyReadonlyArray<Invoice>, CreateInvoiceError>
1027
+ * const numbers: NonEmptyReadonlyArray<number> = [1, 2];
1028
+ * const doubled = allResult(numbers, (number) => ok(number * 2));
1029
+ * expectTypeOf(doubled).toEqualTypeOf<
1030
+ * Result<NonEmptyReadonlyArray<number>>
1132
1031
  * >();
1133
- * expectOk(invoices, [{ orderId: "order-1" }]);
1032
+ * expectOk(doubled, [2, 4]);
1134
1033
  *
1135
- * const invoiceTuple = allResult(
1136
- * [{ id: "order-1" }, { id: "order-2" }] as const,
1137
- * createInvoice,
1138
- * );
1139
- * expectTypeOf(invoiceTuple).toEqualTypeOf<
1140
- * Result<readonly [Invoice, Invoice], CreateInvoiceError>
1141
- * >();
1142
- * expectOk(invoiceTuple, [{ orderId: "order-1" }, { orderId: "order-2" }]);
1034
+ * const tuple = allResult([1, 2] as const, (number) => ok(number * 2));
1035
+ * expectTypeOf(tuple).toEqualTypeOf<Result<readonly [number, number]>>();
1036
+ * expectOk(tuple, [2, 4]);
1143
1037
  * ```
1144
1038
  */
1145
1039
  export function allResult<
@@ -1153,37 +1047,15 @@ export function allResult<
1153
1047
  /**
1154
1048
  * Maps a dynamic or generated value collection to Results.
1155
1049
  *
1156
- * Stops consuming and mapping the iterable on the first Err.
1157
- *
1158
1050
  * ### Example
1159
1051
  *
1160
1052
  * ```ts
1161
- * import { allResult, err, ok, type Result } from "@evolu/common";
1162
- *
1163
- * interface ProductNotFoundError {
1164
- * readonly type: "ProductNotFoundError";
1165
- * readonly productId: string;
1166
- * }
1167
- *
1168
- * const pricesByProductId = new Map([
1169
- * ["book", 20],
1170
- * ["pen", 5],
1171
- * ]);
1172
- * const getPrice = (
1173
- * productId: string,
1174
- * ): Result<number, ProductNotFoundError> => {
1175
- * const price = pricesByProductId.get(productId);
1176
- * return price == null
1177
- * ? err({ type: "ProductNotFoundError", productId })
1178
- * : ok(price);
1179
- * };
1053
+ * import { allResult, ok, type Result } from "@evolu/common";
1180
1054
  *
1181
- * const productIds: Iterable<string> = new Set(["book", "pen"]);
1182
- * const prices = allResult(productIds, getPrice);
1183
- * expectTypeOf(prices).toEqualTypeOf<
1184
- * Result<ReadonlyArray<number>, ProductNotFoundError>
1185
- * >();
1186
- * expectOk(prices, [20, 5]);
1055
+ * const numbers: Iterable<number> = new Set([1, 2]);
1056
+ * const doubled = allResult(numbers, (number) => ok(number * 2));
1057
+ * expectTypeOf(doubled).toEqualTypeOf<Result<ReadonlyArray<number>>>();
1058
+ * expectOk(doubled, [2, 4]);
1187
1059
  * ```
1188
1060
  */
1189
1061
  export function allResult<A, R extends AnyResult>(
@@ -1197,32 +1069,18 @@ export function allResult<A, R extends AnyResult>(
1197
1069
  * ### Example
1198
1070
  *
1199
1071
  * ```ts
1200
- * import { allResult, err, ok, type Result } from "@evolu/common";
1072
+ * import { allResult, ok, type Result } from "@evolu/common";
1201
1073
  *
1202
1074
  * interface User {
1203
1075
  * readonly id: string;
1204
1076
  * }
1205
1077
  *
1206
- * interface UserNotFoundError {
1207
- * readonly type: "UserNotFoundError";
1208
- * readonly userId: string;
1209
- * }
1210
- *
1211
- * const usersById = new Map<string, User>([
1212
- * ["user-1", { id: "user-1" }],
1213
- * ["user-2", { id: "user-2" }],
1214
- * ]);
1215
- * const findUser = (userId: string): Result<User, UserNotFoundError> => {
1216
- * const user = usersById.get(userId);
1217
- * return user == null
1218
- * ? err({ type: "UserNotFoundError", userId })
1219
- * : ok(user);
1220
- * };
1078
+ * const toUser = (id: string): Result<User> => ok({ id });
1221
1079
  *
1222
1080
  * const userIdsByRole = { owner: "user-1", reviewer: "user-2" } as const;
1223
- * const usersByRole = allResult(userIdsByRole, findUser);
1081
+ * const usersByRole = allResult(userIdsByRole, toUser);
1224
1082
  * expectTypeOf(usersByRole).toEqualTypeOf<
1225
- * Result<Readonly<Record<"owner" | "reviewer", User>>, UserNotFoundError>
1083
+ * Result<Readonly<Record<"owner" | "reviewer", User>>>
1226
1084
  * >();
1227
1085
  * expectOk(usersByRole, {
1228
1086
  * owner: { id: "user-1" },
@@ -1246,22 +1104,20 @@ export function allResult<A, R extends AnyResult, K extends string>(
1246
1104
  * ```ts
1247
1105
  * import { allResult, ok, type Result } from "@evolu/common";
1248
1106
  *
1249
- * interface UpdateItemError {
1250
- * readonly type: "UpdateItemError";
1251
- * }
1252
- *
1253
- * const updatedItemIds: Array<string> = [];
1254
- * const updateItem = (itemId: string): Result<number, UpdateItemError> => {
1255
- * updatedItemIds.push(itemId);
1256
- * return ok(1);
1257
- * };
1258
- *
1259
- * const result = allResult(["item-1", "item-2"], updateItem, {
1260
- * collect: false,
1261
- * });
1262
- * expectTypeOf(result).toEqualTypeOf<Result<void, UpdateItemError>>();
1107
+ * const visited: Array<number> = [];
1108
+ * const result = allResult(
1109
+ * [1, 2],
1110
+ * (number) => {
1111
+ * visited.push(number);
1112
+ * return ok(number * 2);
1113
+ * },
1114
+ * {
1115
+ * collect: false,
1116
+ * },
1117
+ * );
1118
+ * expectTypeOf(result).toEqualTypeOf<Result<void>>();
1263
1119
  * expectOk(result, undefined);
1264
- * expect(updatedItemIds).toEqual(["item-1", "item-2"]);
1120
+ * expect(visited).toEqual([1, 2]);
1265
1121
  * ```
1266
1122
  */
1267
1123
  export function allResult<A, R extends AnyResult>(
@@ -1270,7 +1126,21 @@ export function allResult<A, R extends AnyResult>(
1270
1126
  options: { readonly collect: false },
1271
1127
  ): Result<void, InferErr<R>>;
1272
1128
 
1273
- /** Maps record values to Results without collecting their Ok values. */
1129
+ /**
1130
+ * Maps record values to Results without collecting their Ok values.
1131
+ *
1132
+ * ### Example
1133
+ *
1134
+ * ```ts
1135
+ * import { allResult, ok, type Result } from "@evolu/common";
1136
+ *
1137
+ * const result = allResult({ a: 1, b: 2 }, (number) => ok(number * 2), {
1138
+ * collect: false,
1139
+ * });
1140
+ * expectTypeOf(result).toEqualTypeOf<Result<void>>();
1141
+ * expectOk(result, undefined);
1142
+ * ```
1143
+ */
1274
1144
  export function allResult<A, R extends AnyResult, K extends string>(
1275
1145
  values: Readonly<Record<K, A>>,
1276
1146
  fn: (value: A) => R,
@@ -1357,52 +1227,19 @@ export function allResult(
1357
1227
  * import {
1358
1228
  * anyResult,
1359
1229
  * err,
1360
- * isNonEmptyArray,
1361
1230
  * ok,
1362
1231
  * type Result,
1232
+ * type Typed,
1363
1233
  * } from "@evolu/common";
1364
1234
  *
1365
- * interface LookupError {
1366
- * readonly type: "LookupError";
1367
- * }
1368
- *
1369
- * const results: ReadonlyArray<Result<number, LookupError>> = [
1370
- * err({ type: "LookupError" }),
1371
- * ok(42),
1372
- * ];
1373
- * if (isNonEmptyArray(results)) {
1374
- * const number = anyResult(results);
1375
- * expectTypeOf(number).toEqualTypeOf<Result<number, LookupError>>();
1376
- * expectOk(number, 42);
1377
- * }
1378
- * ```
1379
- *
1380
- * A statically non-empty tuple needs no guard:
1381
- *
1382
- * ```ts
1383
- * import { anyResult, err, ok, type Result } from "@evolu/common";
1384
- *
1385
- * interface LookupError {
1386
- * readonly type: "LookupError";
1387
- * readonly source: string;
1388
- * }
1235
+ * const getCachedPrice = (): Result<number, CacheMissError> =>
1236
+ * err({ type: "CacheMiss" });
1389
1237
  *
1390
- * const lookupError = (source: string): Result<never, LookupError> =>
1391
- * err({ type: "LookupError", source });
1238
+ * interface CacheMissError extends Typed<"CacheMiss"> {}
1392
1239
  *
1393
- * const results = [
1394
- * lookupError("first"),
1395
- * ok(42),
1396
- * lookupError("second"),
1397
- * ] as const;
1398
- * const number = anyResult(results);
1399
- * expectTypeOf(number).toEqualTypeOf<Result<number, LookupError>>();
1240
+ * const number = anyResult([getCachedPrice(), ok(42)]);
1241
+ * expectTypeOf(number).toEqualTypeOf<Result<number, CacheMissError>>();
1400
1242
  * expectOk(number, 42);
1401
- *
1402
- * const allFailed = [lookupError("a"), lookupError("b")] as const;
1403
- * const result = anyResult(allFailed);
1404
- * expectTypeOf(result).toEqualTypeOf<Result<never, LookupError>>();
1405
- * expectErr(result, { type: "LookupError", source: "b" });
1406
1243
  * ```
1407
1244
  *
1408
1245
  * @group Composition