@evolu/common 8.0.0-next.4 → 8.0.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 (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Type-safe error handling with Result types.
2
+ * Type-safe error handling with the Result type.
3
3
  *
4
4
  * @module
5
5
  */
6
- import { type NonEmptyReadonlyArray } from "./Array.js";
7
- import type { Lazy } from "./Function.js";
8
- import type { Typed } from "./Type.js";
6
+ import { type NonEmptyReadonlyArray } from "./Array.ts";
7
+ import { type Thunk } from "./Function.ts";
8
+ import type { Typed } from "./Type.ts";
9
+ import type { Awaitable } from "./Types.ts";
9
10
  /**
10
11
  * The problem with `throw` in JavaScript is that the caught value is always of
11
12
  * unknown type. We can't be sure all errors have been handled because the
@@ -16,28 +17,48 @@ import type { Typed } from "./Type.js";
16
17
  * `Either` where errors are part of the return type. TypeScript can do the
17
18
  * same.
18
19
  *
20
+ * ## Example
21
+ *
19
22
  * ```ts
20
- * // With throw - caught value is unknown, can't use exhaustiveCheck
23
+ * import { err, exhaustiveCheck, type Result } from "@evolu/common";
24
+ *
25
+ * // TypeScript can't know what was thrown.
21
26
  * try {
22
- * doSomething();
27
+ * throw new Error("Not found");
23
28
  * } catch (error) {
24
- * // error is unknown - TypeScript can't help us here
29
+ * expectTypeOf(error).toEqualTypeOf<unknown>();
25
30
  * }
26
31
  *
27
- * // With Result - error type is known, exhaustiveCheck works
32
+ * // With Result, errors are part of the return type.
33
+ * const doSomething = (): Result<
34
+ * number,
35
+ * NotFoundError | InvalidInputError
36
+ * > => err({ type: "NotFound" });
37
+ *
38
+ * interface NotFoundError {
39
+ * readonly type: "NotFound";
40
+ * }
41
+ *
42
+ * interface InvalidInputError {
43
+ * readonly type: "InvalidInput";
44
+ * }
45
+ *
46
+ * // With Result, the error type is known and exhaustiveCheck works.
47
+ * // If we add another error type, TypeScript tells us it isn't handled.
28
48
  * const result = doSomething();
29
49
  * if (!result.ok) {
30
50
  * switch (result.error.type) {
31
51
  * case "NotFound":
32
- * // handle not found
52
+ * expect(result.error).toEqual({ type: "NotFound" });
33
53
  * break;
34
54
  * case "InvalidInput":
35
- * // handle invalid input
55
+ * expect(result.error).toEqual({ type: "InvalidInput" });
36
56
  * break;
37
57
  * default:
38
- * exhaustiveCheck(result.error); // TypeScript ensures all cases handled
58
+ * exhaustiveCheck(result.error);
39
59
  * }
40
60
  * }
61
+ * expectErr(result, { type: "NotFound" });
41
62
  * ```
42
63
  *
43
64
  * A `Result` is either {@link Ok} (success with a value) or {@link Err} (failure
@@ -57,143 +78,250 @@ import type { Typed } from "./Type.js";
57
78
  * }
58
79
  * ```
59
80
  *
60
- * ## Example
81
+ * Use {@link trySync} and {@link tryAsync} to intentionally convert thrown or
82
+ * rejected errors into typed, recoverable `Result` values.
83
+ *
84
+ * Do not wrap every throwing API in `Result`. If an error is unrecoverable and
85
+ * the caller has no meaningful fallback, let it throw.
86
+ *
87
+ * ## Composition
88
+ *
89
+ * Since `Result` is a plain object, imperative code works naturally.
61
90
  *
62
91
  * ```ts
63
- * // Typed<T> adds { type: T } for discriminated unions
64
- * interface ParseJsonError extends Typed<"ParseJsonError"> {
65
- * readonly message: string;
92
+ * import { ok, type Result } from "@evolu/common";
93
+ *
94
+ * interface User {
95
+ * readonly id: string;
66
96
  * }
67
97
  *
68
- * const parseJson = (value: string): Result<unknown, ParseJsonError> => {
69
- * try {
70
- * return ok(JSON.parse(value));
71
- * } catch (error) {
72
- * return err({ type: "ParseJsonError", message: String(error) });
73
- * }
74
- * };
98
+ * interface UserNotFoundError {
99
+ * readonly type: "UserNotFoundError";
100
+ * }
75
101
  *
76
- * const json = parseJson('{"name": "Alice"}');
77
- * if (!json.ok) return json; // short-circuit on error
102
+ * interface Profile {
103
+ * readonly userId: string;
104
+ * }
78
105
  *
79
- * // Now we have access to json.value (type: unknown)
80
- * console.log(json.value);
81
- * ```
106
+ * interface ProfileNotFoundError {
107
+ * readonly type: "ProfileNotFoundError";
108
+ * }
82
109
  *
83
- * The caller doesn't need `try/catch`, just `if (!json.ok)`, and the error is
84
- * `ParseJsonError`, not `unknown`.
110
+ * const getUser = (): Result<User, UserNotFoundError> =>
111
+ * ok({ id: "user-1" });
112
+ * const getProfile = (
113
+ * userId: string,
114
+ * ): Result<Profile, ProfileNotFoundError> => ok({ userId });
85
115
  *
86
- * Use this pattern when the caller can recover or choose a different flow.
87
- * Parsing unknown JSON is a good fit because invalid input is expected and
88
- * actionable.
116
+ * const getCurrentProfile = (): Result<
117
+ * Profile,
118
+ * UserNotFoundError | ProfileNotFoundError
119
+ * > => {
120
+ * const user = getUser();
121
+ * if (!user.ok) return user;
89
122
  *
90
- * To avoid `try/catch` inside `parseJson` too, use {@link trySync}:
123
+ * return getProfile(user.value.id);
124
+ * };
91
125
  *
92
- * ```ts
93
- * const parseJson = (value: string): Result<unknown, ParseJsonError> =>
94
- * trySync(
95
- * () => JSON.parse(value) as unknown,
96
- * (error) => ({ type: "ParseJsonError", message: String(error) }),
97
- * );
126
+ * const profile = getCurrentProfile();
127
+ * expectTypeOf(profile).toEqualTypeOf<
128
+ * Result<Profile, UserNotFoundError | ProfileNotFoundError>
129
+ * >();
130
+ * expectOk(profile, { userId: "user-1" });
98
131
  * ```
99
132
  *
100
- * `trySync` and {@link tryAsync} are for intentionally converting thrown errors
101
- * into typed, recoverable {@link Result} values.
133
+ * {@link flatMapResult} is for a single local composition.
102
134
  *
103
- * Do not wrap every throwing API in {@link Result}. If an error is unrecoverable
104
- * and the caller has no meaningful fallback, let it throw and handle it at the
105
- * app boundary.
135
+ * ```ts
136
+ * import { flatMapResult, ok, type Result } from "@evolu/common";
106
137
  *
107
- * Since `Result` is a plain object, imperative code works naturally:
138
+ * interface User {
139
+ * readonly id: string;
140
+ * }
108
141
  *
109
- * ### Stop on error, map on success
142
+ * interface UserNotFoundError {
143
+ * readonly type: "UserNotFoundError";
144
+ * }
110
145
  *
111
- * ```ts
112
- * const users = getActiveUsers();
113
- * if (!users.ok) return users;
114
- * const usernames = mapArray(users.value, (u) => u.username);
146
+ * interface Profile {
147
+ * readonly userId: string;
148
+ * }
149
+ *
150
+ * interface ProfileNotFoundError {
151
+ * readonly type: "ProfileNotFoundError";
152
+ * }
153
+ *
154
+ * const getUser = (): Result<User, UserNotFoundError> =>
155
+ * ok({ id: "user-1" });
156
+ * const getProfile = (
157
+ * userId: string,
158
+ * ): Result<Profile, ProfileNotFoundError> => ok({ userId });
159
+ *
160
+ * const profile = flatMapResult(getUser(), (user) => getProfile(user.id));
161
+ * expectTypeOf(profile).toEqualTypeOf<
162
+ * Result<Profile, UserNotFoundError | ProfileNotFoundError>
163
+ * >();
164
+ * expectOk(profile, { userId: "user-1" });
115
165
  * ```
116
166
  *
117
- * ### Iterate array, stop on first error
167
+ * Use {@link allResult} to collect the Ok values from a collection of Results,
168
+ * or return the first Err.
118
169
  *
119
170
  * ```ts
120
- * for (const user of users) {
121
- * const result = validateUser(user); // Result<ValidUser, ValidateUserError>
122
- * if (!result.ok) return result;
171
+ * import { allResult, err, ok, type Result } from "@evolu/common";
172
+ *
173
+ * interface User {
174
+ * readonly id: string;
123
175
  * }
124
- * ```
125
176
  *
126
- * ## Style
177
+ * interface Preferences {
178
+ * readonly theme: "dark" | "light";
179
+ * }
127
180
  *
128
- * Imperative code is the preferred way to compose sequential {@link Result}
129
- * operations.
181
+ * interface LoadAccountError {
182
+ * readonly type: "LoadAccountError";
183
+ * readonly resource: "user" | "preferences";
184
+ * }
130
185
  *
131
- * ```ts
132
- * const user = getUser();
133
- * if (!user.ok) return user;
186
+ * const user: Result<User, LoadAccountError> = ok({ id: "user-1" });
187
+ * const preferences: Result<Preferences, LoadAccountError> = ok({
188
+ * theme: "dark",
189
+ * });
190
+ * const account = allResult([user, preferences]);
191
+ * expectTypeOf(account).toEqualTypeOf<
192
+ * Result<readonly [User, Preferences], LoadAccountError>
193
+ * >();
194
+ * expectOk(account, [{ id: "user-1" }, { theme: "dark" }]);
195
+ *
196
+ * const userError: Result<never, LoadAccountError> = err({
197
+ * type: "LoadAccountError",
198
+ * resource: "user",
199
+ * });
200
+ * const preferencesError: Result<never, LoadAccountError> = err({
201
+ * type: "LoadAccountError",
202
+ * resource: "preferences",
203
+ * });
204
+ * const failedAccount = allResult([userError, preferencesError]);
205
+ * expectErr(failedAccount, {
206
+ * type: "LoadAccountError",
207
+ * resource: "user",
208
+ * });
209
+ * ```
134
210
  *
135
- * const profile = getProfile(user.value.id);
136
- * if (!profile.ok) return profile;
211
+ * `allResult` can also map collection values to Results before collecting their
212
+ * Ok values. Mapping stops on the first Err.
137
213
  *
138
- * return ok({ user: user.value, profile: profile.value });
139
- * ```
214
+ * ```ts
215
+ * import { allResult, err, ok, type Result } from "@evolu/common";
140
216
  *
141
- * This is an intentional style choice. Evolu does not provide helper
142
- * combinators for every sequential pattern because that would duplicate plain
143
- * control flow and create API ambiguity. Use helpers when they add semantics
144
- * over ordinary control flow, such as operating on collections of results.
145
- * While it may seem verbose, it is explicit, transparent, and avoids pipes and
146
- * nested helpers, which are harder to debug.
217
+ * interface OrderLine {
218
+ * readonly productId: string;
219
+ * readonly quantity: number;
220
+ * }
147
221
  *
148
- * ## Composition
222
+ * interface ProductUnavailableError {
223
+ * readonly type: "ProductUnavailableError";
224
+ * readonly productId: string;
225
+ * }
149
226
  *
150
- * Some patterns are common enough that deserve helpers. The previous example
151
- * can be written with {@link mapResult}:
227
+ * const priceByProductId = new Map([["book", 20]]);
228
+ * const priceOrderLine = ({
229
+ * productId,
230
+ * quantity,
231
+ * }: OrderLine): Result<number, ProductUnavailableError> => {
232
+ * const price = priceByProductId.get(productId);
233
+ * if (price == null) {
234
+ * return err({ type: "ProductUnavailableError", productId });
235
+ * }
236
+ * return ok(price * quantity);
237
+ * };
152
238
  *
153
- * ```ts
154
- * const result = mapResult(users, validateUser);
155
- * // Result<ValidUser[], ValidateUserError>
239
+ * const orderLines: ReadonlyArray<OrderLine> = [
240
+ * { productId: "book", quantity: 2 },
241
+ * { productId: "sold-out", quantity: 1 },
242
+ * ];
243
+ * const prices = allResult(orderLines, priceOrderLine);
244
+ * expectTypeOf(prices).toEqualTypeOf<
245
+ * Result<ReadonlyArray<number>, ProductUnavailableError>
246
+ * >();
247
+ * expectErr(prices, {
248
+ * type: "ProductUnavailableError",
249
+ * productId: "sold-out",
250
+ * });
156
251
  * ```
157
252
  *
158
- * For an array of results, {@link allResult} extracts all values or returns the
159
- * first error:
253
+ * For side effects, or whenever the success values aren't needed, disable
254
+ * collection with `{ collect: false }`. `allResult` then returns `Result<void,
255
+ * E>` without storing the successful values and still stops on the first Err.
160
256
  *
161
257
  * ```ts
162
- * const result = allResult(validationResults);
258
+ * import { allResult, err, ok, type Result } from "@evolu/common";
259
+ *
260
+ * interface User {
261
+ * readonly id: string;
262
+ * }
263
+ *
264
+ * interface SaveUserError {
265
+ * readonly type: "SaveUserError";
266
+ * }
267
+ *
268
+ * interface SendWelcomeEmailError {
269
+ * readonly type: "SendWelcomeEmailError";
270
+ * }
271
+ *
272
+ * const saveUser = (_user: User): Result<number, SaveUserError> =>
273
+ * err({ type: "SaveUserError" });
274
+ *
275
+ * let welcomeEmailSent = false;
276
+ * const sendWelcomeEmail = (
277
+ * _user: User,
278
+ * ): Result<void, SendWelcomeEmailError> => {
279
+ * welcomeEmailSent = true;
280
+ * return ok();
281
+ * };
282
+ *
283
+ * const registerUser = (
284
+ * user: User,
285
+ * ): Result<void, SaveUserError | SendWelcomeEmailError> =>
286
+ * allResult(
287
+ * [saveUser, sendWelcomeEmail],
288
+ * (operation) => operation(user),
289
+ * { collect: false },
290
+ * );
291
+ *
292
+ * const result = registerUser({ id: "user-1" });
293
+ * expectTypeOf(result).toEqualTypeOf<
294
+ * Result<void, SaveUserError | SendWelcomeEmailError>
295
+ * >();
296
+ * expectErr(result, { type: "SaveUserError" });
297
+ * expect(welcomeEmailSent).toBe(false);
163
298
  * ```
164
299
  *
165
300
  * For the first success, {@link anyResult} returns the first Ok or the last
166
301
  * error if all fail:
167
302
  *
168
303
  * ```ts
169
- * const result = anyResult(parserResults);
170
- * ```
304
+ * import { anyResult, err, ok, type Result } from "@evolu/common";
171
305
  *
172
- * ## Naming convention
306
+ * interface CacheMissError {
307
+ * readonly type: "CacheMissError";
308
+ * }
173
309
  *
174
- * - Result with a value: name it after the value (`user`, `config`)
175
- * - Result without a value: name it `result`
310
+ * const getCachedPrice = (): Result<number, CacheMissError> =>
311
+ * err({ type: "CacheMissError" });
176
312
  *
177
- * ```ts
178
- * const processUser = (): Result<
179
- * void,
180
- * GetUserError | SaveToDatabaseError | SendWelcomeEmailError
181
- * > => {
182
- * const user = getUser();
183
- * if (!user.ok) return user;
184
- *
185
- * const result = saveToDatabase(user.value);
186
- * if (!result.ok) return result;
313
+ * const prices = [getCachedPrice(), ok(20)] as const;
314
+ * const price = anyResult(prices);
315
+ * expectTypeOf(price).toEqualTypeOf<Result<number, CacheMissError>>();
316
+ * expectOk(price, 20);
317
+ * ```
187
318
  *
188
- * // To avoid a clash with the previous `result`, use a block scope.
189
- * {
190
- * const result = sendWelcomeEmail(user.value);
191
- * if (!result.ok) return result;
192
- * }
319
+ * ## Naming convention
193
320
  *
194
- * return ok();
195
- * };
196
- * ```
321
+ * Name a Result after its value (`user`, `config`), not after the wrapper
322
+ * (`userResult`, `configResult`). If it has no success value, name it `result`.
323
+ * For several such operations, use `allResult` with `{ collect: false }` as
324
+ * shown above.
197
325
  *
198
326
  * ## Unrecoverable errors
199
327
  *
@@ -201,16 +329,49 @@ import type { Typed } from "./Type.js";
201
329
  * These are unrecoverable errors: expected (you know they can happen) but only
202
330
  * handleable at the app level.
203
331
  *
204
- * Do not force these errors into {@link Result} just because the underlying API
332
+ * Do not force these errors into `Result` just because the underlying API
205
333
  * throws. If the local caller cannot recover, let the error propagate to a
206
334
  * global handler or other app boundary.
207
335
  *
208
- * In Evolu apps, that boundary is typically a platform `createRun` adapter such
209
- * as `@evolu/web`, `@evolu/nodejs`, or `@evolu/react-native`, which add
210
- * platform-specific global error handling.
336
+ * In Evolu apps, the root Run reports defects and the platform lifecycle API
337
+ * owns application shutdown. For example, `@evolu/nodejs` provides `runMain`.
211
338
  *
212
339
  * ## FAQ
213
340
  *
341
+ * ### Is Result expensive?
342
+ *
343
+ * Wrapping a value in Result creates one small plain object and is very cheap.
344
+ * Its cost is usually worth considering only when a performance-critical loop
345
+ * creates millions of Results. Use Result by default; if profiling identifies
346
+ * such a loop as a bottleneck, benchmark the complete workload before
347
+ * optimizing it.
348
+ *
349
+ * ### Why not generators?
350
+ *
351
+ * Generator-based APIs make sequential workflows more concise: `yield*`
352
+ * combines error propagation (roughly replacing `if (!result.ok) return
353
+ * result`) with unwrapping the success value. That doesn't come for free.
354
+ * Supporting direct `yield*` requires adding iterator behavior to every Result;
355
+ * otherwise every use needs an adapter. That makes Result more than plain
356
+ * structural data: after serialization, its iterator behavior must be restored
357
+ * before direct `yield*` works. Generator machinery also makes control flow,
358
+ * debugger stepping, and stack traces less direct while adding runtime
359
+ * overhead. Evolu instead keeps Result as plain data and makes error
360
+ * propagation explicit. With AI, explicit checks are cheap to write.
361
+ *
362
+ * Generators do not make accidental omission impossible: a function returning a
363
+ * lazy operation can be called without composing the returned value with
364
+ * `yield*`, leaving the operation out of the workflow, just as a
365
+ * Result-returning function can be called and its Result ignored. Dedicated
366
+ * tooling can detect these omissions, but that safety comes from the tooling,
367
+ * not generator syntax itself.
368
+ *
369
+ * The intended way to write Evolu code is with test-driven development (TDD).
370
+ * Tests document the intended behavior and serve as its runnable specification,
371
+ * including failure paths. AI makes writing and maintaining those tests cheap.
372
+ * {@link exhaustiveCheck} complements tests by having TypeScript report newly
373
+ * added error variants.
374
+ *
214
375
  * ### What if a function doesn't return a value on success?
215
376
  *
216
377
  * Use `Result<void, E>` and return `ok()` (no argument). Don't return
@@ -238,15 +399,28 @@ export interface Ok<out T> {
238
399
  * ### Example
239
400
  *
240
401
  * ```ts
402
+ * import { err, ok, type Result, type Typed } from "@evolu/common";
403
+ *
404
+ * interface User {
405
+ * readonly id: string;
406
+ * }
407
+ *
241
408
  * interface NotFoundError extends Typed<"NotFoundError"> {
242
409
  * readonly id: string;
243
410
  * }
244
411
  *
412
+ * const users = new Map<string, User>([["user-1", { id: "user-1" }]]);
245
413
  * const findUser = (id: string): Result<User, NotFoundError> => {
246
414
  * const user = users.get(id);
247
415
  * if (user == null) return err({ type: "NotFoundError", id });
248
416
  * return ok(user);
249
417
  * };
418
+ *
419
+ * expectOk(findUser("user-1"), { id: "user-1" });
420
+ * expectErr(findUser("missing"), {
421
+ * type: "NotFoundError",
422
+ * id: "missing",
423
+ * });
250
424
  * ```
251
425
  */
252
426
  export interface Err<out E> {
@@ -254,13 +428,13 @@ export interface Err<out E> {
254
428
  readonly error: E;
255
429
  }
256
430
  /**
257
- * Extracts the value type from a {@link Result}.
431
+ * Infers the success value type from a {@link Result}.
258
432
  *
259
433
  * @group Utilities
260
434
  */
261
435
  export type InferOk<R extends Result<any, any>> = R extends Ok<infer T> ? T : never;
262
436
  /**
263
- * Extracts the error type from a {@link Result}.
437
+ * Infers the error type from a {@link Result}.
264
438
  *
265
439
  * @group Utilities
266
440
  */
@@ -275,11 +449,15 @@ export type InferErr<R extends Result<any, any>> = R extends Err<infer E> ? E :
275
449
  * ### Example
276
450
  *
277
451
  * ```ts
278
- * const noValue = ok();
279
- * console.log(noValue); // { ok: true, value: undefined }
452
+ * import { ok, type Result } from "@evolu/common";
280
453
  *
454
+ * const noValue = ok();
281
455
  * const success = ok(42);
282
- * console.log(success); // { ok: true, value: 42 }
456
+ *
457
+ * expectTypeOf(noValue).toEqualTypeOf<Result<void>>();
458
+ * expectTypeOf(success).toEqualTypeOf<Result<number>>();
459
+ * expectOk(noValue, undefined);
460
+ * expectOk(success, 42);
283
461
  * ```
284
462
  */
285
463
  export declare function ok(): Result<void>;
@@ -292,7 +470,7 @@ export declare const isOk: <T, E>(result: Result<T, E>) => result is Ok<T>;
292
470
  /** Type guard for {@link Err} results. */
293
471
  export declare const isErr: <T, E>(result: Result<T, E>) => result is Err<E>;
294
472
  /**
295
- * Returns the value from an `Ok` {@link Result}, or throws if it is an `Err`.
473
+ * Gets the value from an `Ok` {@link Result}, or throws if it is an `Err`.
296
474
  *
297
475
  * Use this where failure should crash the current flow instead of being handled
298
476
  * locally.
@@ -300,8 +478,8 @@ export declare const isErr: <T, E>(result: Result<T, E>) => result is Err<E>;
300
478
  * **When to use:**
301
479
  *
302
480
  * - Application startup or composition-root setup where errors must stop the
303
- * program immediately. In Evolu apps, errors are handled by platform-specific
304
- * `createRun` adapters at the app boundary.
481
+ * program immediately. In Evolu apps, the root Run reports the defect and the
482
+ * platform lifecycle API handles shutdown.
305
483
  * - Module-level constants
306
484
  * - Test setup with values that are expected to be valid
307
485
  *
@@ -311,17 +489,38 @@ export declare const isErr: <T, E>(result: Result<T, E>) => result is Err<E>;
311
489
  * ### Example
312
490
  *
313
491
  * ```ts
314
- * // At app startup, crash if config is invalid:
492
+ * import { err, getOrThrow, ok, type Result } from "@evolu/common";
493
+ *
494
+ * interface Config {
495
+ * readonly port: number;
496
+ * }
497
+ * interface InvalidConfigError {
498
+ * readonly type: "InvalidConfigError";
499
+ * }
500
+ * const loadConfig = (): Result<Config, InvalidConfigError> =>
501
+ * ok({ port: 3000 });
502
+ *
503
+ * // At app startup, crash if the config is invalid.
315
504
  * const config = getOrThrow(loadConfig());
316
- * // Safe to use config here
505
+ * expectTypeOf(config).toEqualTypeOf<Config>();
506
+ * expect(config).toEqual({ port: 3000 });
507
+ *
508
+ * try {
509
+ * getOrThrow(err({ type: "InvalidConfigError" }));
510
+ * assert.fail("Expected getOrThrow to throw");
511
+ * } catch (error) {
512
+ * assert(error instanceof Error);
513
+ * expect(error.message).toBe("getOrThrow");
514
+ * expect(error.cause).toEqual({ type: "InvalidConfigError" });
515
+ * }
317
516
  * ```
318
517
  *
319
518
  * Throws: `Error` with the original error attached as `cause`.
320
519
  */
321
520
  export declare const getOrThrow: <T, E>(result: Result<T, E>) => T;
322
521
  /**
323
- * Extracts the value from a {@link Result} if it is an `Ok`, or returns `null`
324
- * if it is an `Err`.
522
+ * Gets the value from an `Ok` {@link Result}, or returns `null` if it is an
523
+ * `Err`.
325
524
  *
326
525
  * **Intended usage:**
327
526
  *
@@ -332,59 +531,147 @@ export declare const getOrThrow: <T, E>(result: Result<T, E>) => T;
332
531
  * ### Example
333
532
  *
334
533
  * ```ts
335
- * // For APIs that expect T | null
336
- * const user = getOrNull(findUser(id));
534
+ * import { err, getOrNull, ok, type Result } from "@evolu/common";
535
+ *
536
+ * interface User {
537
+ * readonly id: string;
538
+ * }
539
+ * interface UserNotFoundError {
540
+ * readonly type: "UserNotFoundError";
541
+ * }
542
+ * const findUser = (id: string): Result<User, UserNotFoundError> =>
543
+ * id === "user-1" ? ok({ id }) : err({ type: "UserNotFoundError" });
544
+ *
545
+ * // For APIs that expect T | null.
546
+ * const user = getOrNull(findUser("user-1"));
547
+ * const missingUser = getOrNull(findUser("missing"));
548
+ *
549
+ * expectTypeOf(user).toEqualTypeOf<User | null>();
550
+ * expect(user).toEqual({ id: "user-1" });
551
+ * expect(missingUser).toBeNull();
337
552
  * ```
338
553
  */
339
554
  export declare const getOrNull: <T, E>(result: Result<T, E>) => T | null;
340
555
  /**
341
- * Extracts the value from a {@link Result} whose error type is `never`.
556
+ * Gets the value from a {@link Result} whose error type is `never`.
342
557
  *
343
558
  * This is useful when the type system guarantees the result cannot fail (for
344
559
  * example `Result<T, never>`), avoiding impossible `if (!result.ok)` branches
345
560
  * at call sites.
561
+ *
562
+ * ### Example
563
+ *
564
+ * ```ts
565
+ * import { getOk, ok, type Result } from "@evolu/common";
566
+ *
567
+ * const getCount = (): Result<number> => ok(2);
568
+ * const count = getOk(getCount());
569
+ * expectTypeOf(count).toEqualTypeOf<number>();
570
+ * expect(count).toBe(2);
571
+ * ```
346
572
  */
347
573
  export declare const getOk: <T>(result: Result<T>) => T;
348
574
  /**
349
575
  * Wraps a synchronous function that may throw, returning a {@link Result}.
350
576
  *
351
- * Use this when the thrown value should become a typed, recoverable error for
352
- * the caller. Do not use it for failures that should terminate the current flow
353
- * and propagate to a global handler.
577
+ * `mapError` converts the caught `unknown` value into a typed domain error.
578
+ *
579
+ * Some APIs use exceptions for both recoverable and unrecoverable failures. In
580
+ * that case, convert only the failures the caller can recover from and rethrow
581
+ * the rest. `trySync` propagates rethrown failures instead of converting them
582
+ * to Err.
354
583
  *
355
584
  * ### Example
356
585
  *
357
586
  * ```ts
358
- * const parseJson = (value: string): Result<unknown, ParseJsonError> =>
587
+ * import { trySync, type Result } from "@evolu/common";
588
+ *
589
+ * interface ReserveSeatError {
590
+ * readonly type: "ReserveSeatError";
591
+ * readonly seat: string;
592
+ * }
593
+ *
594
+ * class LegacySeatUnavailableError extends Error {}
595
+ *
596
+ * const legacyReserveSeat = (seat: string): void => {
597
+ * if (seat === "A1") throw new LegacySeatUnavailableError();
598
+ * if (seat === "B1") throw new Error("Unexpected database error");
599
+ * };
600
+ *
601
+ * const reserveSeat = (seat: string): Result<void, ReserveSeatError> =>
359
602
  * trySync(
360
- * () => JSON.parse(value) as unknown,
361
- * (error) => ({ type: "ParseJsonError", message: String(error) }),
603
+ * () => legacyReserveSeat(seat),
604
+ * (error) => {
605
+ * if (error instanceof LegacySeatUnavailableError) {
606
+ * return { type: "ReserveSeatError", seat };
607
+ * }
608
+ * throw error;
609
+ * },
362
610
  * );
611
+ *
612
+ * const result = reserveSeat("B2");
613
+ * expectTypeOf(result).toEqualTypeOf<Result<void, ReserveSeatError>>();
614
+ * expectOk(result, undefined);
615
+ * expectErr(reserveSeat("A1"), { type: "ReserveSeatError", seat: "A1" });
616
+ * expect(() => reserveSeat("B1")).toThrow("Unexpected database error");
363
617
  * ```
364
618
  */
365
- export declare const trySync: <T, E>(fn: () => T, mapError: (error: unknown) => E) => Result<T, E>;
619
+ export declare function trySync<T>(fn: () => T): Result<T, unknown>;
620
+ export declare function trySync<T, E>(fn: () => T, mapError: (error: unknown) => E): Result<T, E>;
366
621
  /**
367
- * Wraps an async function that may throw, returning a {@link Result}.
622
+ * Wraps an async function that may throw or reject, returning a {@link Result}.
623
+ *
624
+ * `mapError` converts the caught `unknown` value into a typed domain error.
368
625
  *
369
- * Use this when the rejection should become a typed, recoverable error for the
370
- * caller. Do not use it for failures that should terminate the current flow and
371
- * propagate to a global handler.
626
+ * Some APIs use exceptions for both recoverable and unrecoverable failures. In
627
+ * that case, convert only the failures the caller can recover from and rethrow
628
+ * the rest. `tryAsync` rejects with rethrown failures instead of converting
629
+ * them to Err.
372
630
  *
373
631
  * ### Example
374
632
  *
375
633
  * ```ts
376
- * const fetchJson = (url: string): Promise<Result<unknown, FetchError>> =>
634
+ * import { tryAsync, type Result } from "@evolu/common";
635
+ *
636
+ * interface ReserveSeatError {
637
+ * readonly type: "ReserveSeatError";
638
+ * readonly seat: string;
639
+ * }
640
+ *
641
+ * class LegacySeatUnavailableError extends Error {}
642
+ *
643
+ * const legacyReserveSeat = async (seat: string): Promise<void> => {
644
+ * if (seat === "A1") throw new LegacySeatUnavailableError();
645
+ * if (seat === "B1") throw new Error("Unexpected database error");
646
+ * };
647
+ *
648
+ * const reserveSeat = (
649
+ * seat: string,
650
+ * ): Promise<Result<void, ReserveSeatError>> =>
377
651
  * tryAsync(
378
- * async () => {
379
- * const response = await fetch(url);
380
- * if (!response.ok) throw new Error(`Status ${response.status}`);
381
- * return response.json();
652
+ * () => legacyReserveSeat(seat),
653
+ * (error) => {
654
+ * if (error instanceof LegacySeatUnavailableError) {
655
+ * return { type: "ReserveSeatError", seat };
656
+ * }
657
+ * throw error;
382
658
  * },
383
- * (error) => ({ type: "FetchError", message: String(error) }),
384
659
  * );
660
+ *
661
+ * const result = await reserveSeat("B2");
662
+ * expectTypeOf(result).toEqualTypeOf<Result<void, ReserveSeatError>>();
663
+ * expectOk(result, undefined);
664
+ * expectErr(await reserveSeat("A1"), {
665
+ * type: "ReserveSeatError",
666
+ * seat: "A1",
667
+ * });
668
+ * await expect(reserveSeat("B1")).rejects.toThrow(
669
+ * "Unexpected database error",
670
+ * );
385
671
  * ```
386
672
  */
387
- export declare const tryAsync: <T, E>(lazyPromise: Lazy<Promise<T>>, mapError: (error: unknown) => E) => Promise<Result<T, E>>;
673
+ export declare function tryAsync<T>(promiseThunk: Thunk<Awaitable<T>>): Promise<Result<T, unknown>>;
674
+ export declare function tryAsync<T, E>(promiseThunk: Thunk<Awaitable<T>>, mapError: (error: unknown) => E): Promise<Result<T, E>>;
388
675
  /**
389
676
  * A result for a pull-based protocol with three outcomes.
390
677
  *
@@ -396,6 +683,30 @@ export declare const tryAsync: <T, E>(lazyPromise: Lazy<Promise<T>>, mapError: (
396
683
  * - `Err<E>` — failed with an error
397
684
  *
398
685
  * Inspired by JavaScript's `Iterator.next()`, which returns `{ value, done }`.
686
+ *
687
+ * ### Example
688
+ *
689
+ * ```ts
690
+ * import { done, err, ok, type NextResult } from "@evolu/common";
691
+ *
692
+ * interface ReadError {
693
+ * readonly type: "ReadError";
694
+ * }
695
+ *
696
+ * const next = (index: number): NextResult<string, ReadError, number> => {
697
+ * if (index === 0) return ok("first");
698
+ * if (index === 1) return err({ type: "ReadError" });
699
+ * return err(done(index));
700
+ * };
701
+ *
702
+ * const value = next(0);
703
+ * expectTypeOf(value).toEqualTypeOf<
704
+ * NextResult<string, ReadError, number>
705
+ * >();
706
+ * expectOk(value, "first");
707
+ * expectErr(next(1), { type: "ReadError" });
708
+ * expectErr(next(2), { type: "Done", done: 2 });
709
+ * ```
399
710
  */
400
711
  export type NextResult<A, E = never, D = void> = Result<A, E | Done<D>>;
401
712
  /**
@@ -415,6 +726,19 @@ export interface Done<out D = unknown> extends Typed<"Done"> {
415
726
  *
416
727
  * - `done()` creates a `Done<void>` for protocols that don't need a done value.
417
728
  * - `done(value)` creates a `Done<D>` containing the specified value.
729
+ *
730
+ * ### Example
731
+ *
732
+ * ```ts
733
+ * import { done, type Done } from "@evolu/common";
734
+ *
735
+ * const withoutValue = done();
736
+ * const withValue = done(42);
737
+ * expectTypeOf(withoutValue).toEqualTypeOf<Done<void>>();
738
+ * expectTypeOf(withValue).toEqualTypeOf<Done<number>>();
739
+ * expect(withoutValue).toEqual({ type: "Done", done: undefined });
740
+ * expect(withValue).toEqual({ type: "Done", done: 42 });
741
+ * ```
418
742
  */
419
743
  export declare function done(): Done<void>;
420
744
  /** With a done value. */
@@ -425,6 +749,15 @@ export declare function done<D>(value: D): Done<D>;
425
749
  * Useful for pull-based protocols where completion is encoded in the error
426
750
  * channel (for example {@link NextResult}).
427
751
  *
752
+ * ### Example
753
+ *
754
+ * ```ts
755
+ * import { type Done, type ExcludeDone } from "@evolu/common";
756
+ *
757
+ * type Errors = "ReadError" | Done<number>;
758
+ * expectTypeOf<ExcludeDone<Errors>>().toEqualTypeOf<"ReadError">();
759
+ * ```
760
+ *
428
761
  * @group Utilities
429
762
  */
430
763
  export type ExcludeDone<E> = Exclude<E, Done<any>>;
@@ -434,30 +767,124 @@ export type ExcludeDone<E> = Exclude<E, Done<any>>;
434
767
  * Useful for pull-based protocols where completion is encoded in the error
435
768
  * channel (for example {@link NextResult}).
436
769
  *
770
+ * ### Example
771
+ *
772
+ * ```ts
773
+ * import { type Done, type OnlyDone } from "@evolu/common";
774
+ *
775
+ * type Errors = "ReadError" | Done<number>;
776
+ * expectTypeOf<OnlyDone<Errors>>().toEqualTypeOf<Done<number>>();
777
+ * ```
778
+ *
437
779
  * @group Utilities
438
780
  */
439
781
  export type OnlyDone<E> = Extract<E, Done<any>>;
440
782
  /**
441
- * Extracts the done value type from a {@link NextResult}.
783
+ * Infers the done value type from a {@link NextResult}.
784
+ *
785
+ * ### Example
786
+ *
787
+ * ```ts
788
+ * import { type InferDone, type NextResult } from "@evolu/common";
789
+ *
790
+ * type ReadResult = NextResult<string, "ReadError", number>;
791
+ * expectTypeOf<InferDone<ReadResult>>().toEqualTypeOf<number>();
792
+ * ```
442
793
  *
443
794
  * @group Utilities
444
795
  */
445
796
  export type InferDone<R extends Result<any, any>> = InferErr<R> extends infer Errors ? Errors extends Done<infer D> ? D : never : never;
446
797
  /**
447
- * Extracts all values from an array of {@link Result}s.
798
+ * Composes a successful {@link Result} with another Result-returning operation.
799
+ *
800
+ * Returns the existing error without calling the operation when the Result has
801
+ * failed.
802
+ *
803
+ * Do not nest `flatMapResult`. For longer workflows, use explicit checks, which
804
+ * keep names, intermediate values, and control flow flat and easy to read.
805
+ *
806
+ * ### Example
807
+ *
808
+ * ```ts
809
+ * import { err, flatMapResult, ok, type Result } from "@evolu/common";
810
+ *
811
+ * interface User {
812
+ * readonly id: string;
813
+ * }
814
+ * interface UserError {
815
+ * readonly type: "UserError";
816
+ * }
817
+ *
818
+ * interface Profile {
819
+ * readonly userId: string;
820
+ * }
821
+ * interface ProfileError {
822
+ * readonly type: "ProfileError";
823
+ * }
824
+ * const getProfile = (userId: string): Result<Profile, ProfileError> =>
825
+ * ok({ userId });
826
+ *
827
+ * let getProfileCalls = 0;
828
+ * const getProfileForUser = (
829
+ * user: User,
830
+ * ): Result<Profile, ProfileError> => {
831
+ * getProfileCalls++;
832
+ * return getProfile(user.id);
833
+ * };
834
+ *
835
+ * const user: Result<User, UserError> = ok({ id: "user-1" });
836
+ * const profile = flatMapResult(user, getProfileForUser);
837
+ * expectTypeOf(profile).toEqualTypeOf<
838
+ * Result<Profile, UserError | ProfileError>
839
+ * >();
840
+ * expectOk(profile, { userId: "user-1" });
841
+ * expect(getProfileCalls).toBe(1);
842
+ *
843
+ * const missingUser: Result<User, UserError> = err({ type: "UserError" });
844
+ * const missingProfile = flatMapResult(missingUser, getProfileForUser);
845
+ * expectErr(missingProfile, { type: "UserError" });
846
+ * expect(getProfileCalls).toBe(1);
847
+ * ```
848
+ *
849
+ * @group Composition
850
+ */
851
+ export declare const flatMapResult: <T, E, U, F>(result: Result<T, E>, fn: (value: T) => Result<U, F>) => Result<U, E | F>;
852
+ /**
853
+ * Collects the Ok values from a collection of {@link Result}s, or maps
854
+ * collection values to Results and collects their Ok values.
448
855
  *
449
856
  * Returns the first error if any result fails.
450
857
  *
858
+ * Pass `{ collect: false }` to return `Result<void, E>` without storing the Ok
859
+ * values in an output collection.
860
+ *
451
861
  * ### Example
452
862
  *
453
863
  * ```ts
454
- * const results = [ok(1), ok(2), ok(3)];
455
- * const all = allResult(results);
456
- * // ok([1, 2, 3])
864
+ * import { allResult, err, ok, type Result } from "@evolu/common";
865
+ *
866
+ * interface CountError {
867
+ * readonly type: "CountError";
868
+ * }
869
+ * interface LabelError {
870
+ * readonly type: "LabelError";
871
+ * }
872
+ *
873
+ * const getCount = (): Result<number, CountError> => ok(2);
874
+ * const getLabel = (): Result<string, LabelError> => ok("books");
457
875
  *
458
- * const withError = [ok(1), err("fail"), ok(3)];
459
- * const failed = allResult(withError);
460
- * // err("fail")
876
+ * const values = allResult([getCount(), getLabel()]);
877
+ * expectTypeOf(values).toEqualTypeOf<
878
+ * Result<readonly [number, string], CountError | LabelError>
879
+ * >();
880
+ * expectOk(values, [2, "books"]);
881
+ *
882
+ * const numbers = allResult([
883
+ * ok(1),
884
+ * err({ type: "CountError" }),
885
+ * err({ type: "LabelError" }),
886
+ * ]);
887
+ * expectErr(numbers, { type: "CountError" });
461
888
  * ```
462
889
  *
463
890
  * @group Composition
@@ -466,11 +893,19 @@ export declare function allResult<const T extends readonly [AnyResult, ...Readon
466
893
  [K in keyof T]: InferOk<T[K]>;
467
894
  }, InferErr<T[number]>>;
468
895
  /**
469
- * Returns object with same keys.
896
+ * Returns an object with the same keys.
897
+ *
898
+ * ### Example
470
899
  *
471
900
  * ```ts
472
- * const result = allResult({ a: ok(1), b: ok(2) });
473
- * // ok({ a: 1, b: 2 })
901
+ * import { allResult, ok, type Result } from "@evolu/common";
902
+ *
903
+ * const resultsByName = { a: ok(1), b: ok(2) } as const;
904
+ * const valuesByName = allResult(resultsByName);
905
+ * expectTypeOf(valuesByName).toEqualTypeOf<
906
+ * Result<{ readonly a: number; readonly b: number }>
907
+ * >();
908
+ * expectOk(valuesByName, { a: 1, b: 2 });
474
909
  * ```
475
910
  */
476
911
  export declare function allResult<T extends Readonly<Record<string, AnyResult>>>(results: T): Result<{
@@ -481,91 +916,303 @@ export declare function allResult<T extends Readonly<Record<string, AnyResult>>>
481
916
  /**
482
917
  * For dynamic or generated result lists.
483
918
  *
919
+ * Stops consuming the iterable on the first Err.
920
+ *
921
+ * ### Example
922
+ *
484
923
  * ```ts
485
- * const results: ReadonlyArray<Result<number, Error>> = getResults();
486
- * const all = allResult(results);
487
- * // Result<ReadonlyArray<number>, Error>
924
+ * import { allResult, err, ok, type Result } from "@evolu/common";
925
+ *
926
+ * interface LoadNumberError {
927
+ * readonly type: "LoadNumberError";
928
+ * }
929
+ *
930
+ * const results: ReadonlyArray<Result<number, LoadNumberError>> = [
931
+ * ok(1),
932
+ * err({ type: "LoadNumberError" }),
933
+ * ];
934
+ * const numbers = allResult(results);
935
+ * expectTypeOf(numbers).toEqualTypeOf<
936
+ * Result<ReadonlyArray<number>, LoadNumberError>
937
+ * >();
938
+ * expectErr(numbers, { type: "LoadNumberError" });
488
939
  * ```
489
940
  */
490
941
  export declare function allResult<T, E>(results: Iterable<Result<T, E>>): Result<ReadonlyArray<T>, E>;
491
942
  /**
492
- * Guarantees non-empty result.
943
+ * Preserves a non-empty array on success.
944
+ *
945
+ * ### Example
493
946
  *
494
947
  * ```ts
495
- * const results: NonEmptyReadonlyArray<Result<number, Error>> = [
496
- * ok(1),
497
- * ok(2),
498
- * ];
499
- * const all = allResult(results);
500
- * // Result<NonEmptyReadonlyArray<number>, Error>
948
+ * import {
949
+ * allResult,
950
+ * ok,
951
+ * type NonEmptyReadonlyArray,
952
+ * type Result,
953
+ * } from "@evolu/common";
954
+ *
955
+ * const results: NonEmptyReadonlyArray<Result<number>> = [ok(1), ok(2)];
956
+ * const numbers = allResult(results);
957
+ * expectTypeOf(numbers).toEqualTypeOf<
958
+ * Result<NonEmptyReadonlyArray<number>>
959
+ * >();
960
+ * expectOk(numbers, [1, 2]);
501
961
  * ```
502
962
  */
503
963
  export declare function allResult<T, E>(results: NonEmptyReadonlyArray<Result<T, E>>): Result<NonEmptyReadonlyArray<T>, E>;
964
+ /** Does not collect the Ok values from a record of Results. */
965
+ export declare function allResult<T extends Readonly<Record<string, AnyResult>>>(results: T, options: {
966
+ readonly collect: false;
967
+ }): Result<void, [keyof T] extends [never] ? never : InferErr<T[keyof T]>>;
504
968
  /**
505
- * Maps items to {@link Result}s and extracts all values.
969
+ * Does not collect the Ok values from an iterable of Results.
506
970
  *
507
- * Returns the first error if any result fails.
971
+ * Use this overload when the Results already exist and only their collective
972
+ * success or failure matters. To stop invoking operations after the first Err,
973
+ * use the mapping overload instead.
508
974
  *
509
975
  * ### Example
510
976
  *
511
977
  * ```ts
512
- * const users = [{ id: 1 }, { id: 2 }];
513
- * const result = mapResult(users, validateUser);
514
- * // Result<ReadonlyArray<ValidUser>, ValidateUserError>
978
+ * import { allResult, err, ok, type Result } from "@evolu/common";
979
+ *
980
+ * interface SaveError {
981
+ * readonly type: "SaveError";
982
+ * }
983
+ *
984
+ * const results: ReadonlyArray<Result<number, SaveError>> = [
985
+ * ok(1),
986
+ * err({ type: "SaveError" }),
987
+ * ];
988
+ * const result = allResult(results, { collect: false });
989
+ * expectTypeOf(result).toEqualTypeOf<Result<void, SaveError>>();
990
+ * expectErr(result, { type: "SaveError" });
515
991
  * ```
992
+ */
993
+ export declare function allResult<R extends AnyResult>(results: Iterable<R>, options: {
994
+ readonly collect: false;
995
+ }): Result<void, InferErr<R>>;
996
+ /**
997
+ * Maps a non-empty array or tuple to Results and preserves its shape.
516
998
  *
517
- * @group Composition
999
+ * ### Example
1000
+ *
1001
+ * ```ts
1002
+ * import {
1003
+ * allResult,
1004
+ * ok,
1005
+ * type NonEmptyReadonlyArray,
1006
+ * type Result,
1007
+ * } from "@evolu/common";
1008
+ *
1009
+ * interface Order {
1010
+ * readonly id: string;
1011
+ * }
1012
+ * interface Invoice {
1013
+ * readonly orderId: string;
1014
+ * }
1015
+ * interface CreateInvoiceError {
1016
+ * readonly type: "CreateInvoiceError";
1017
+ * }
1018
+ *
1019
+ * const orders: NonEmptyReadonlyArray<Order> = [{ id: "order-1" }];
1020
+ * const createInvoice = (
1021
+ * order: Order,
1022
+ * ): Result<Invoice, CreateInvoiceError> => ok({ orderId: order.id });
1023
+ *
1024
+ * const invoices = allResult(orders, createInvoice);
1025
+ * expectTypeOf(invoices).toEqualTypeOf<
1026
+ * Result<NonEmptyReadonlyArray<Invoice>, CreateInvoiceError>
1027
+ * >();
1028
+ * expectOk(invoices, [{ orderId: "order-1" }]);
1029
+ *
1030
+ * const invoiceTuple = allResult(
1031
+ * [{ id: "order-1" }, { id: "order-2" }] as const,
1032
+ * createInvoice,
1033
+ * );
1034
+ * expectTypeOf(invoiceTuple).toEqualTypeOf<
1035
+ * Result<readonly [Invoice, Invoice], CreateInvoiceError>
1036
+ * >();
1037
+ * expectOk(invoiceTuple, [{ orderId: "order-1" }, { orderId: "order-2" }]);
1038
+ * ```
1039
+ */
1040
+ export declare function allResult<const A extends readonly [unknown, ...Array<unknown>], R extends AnyResult>(values: A, fn: (value: A[number]) => R): Result<{
1041
+ [K in keyof A]: InferOk<R>;
1042
+ }, InferErr<R>>;
1043
+ /**
1044
+ * Maps a dynamic or generated value collection to Results.
1045
+ *
1046
+ * Stops consuming and mapping the iterable on the first Err.
1047
+ *
1048
+ * ### Example
1049
+ *
1050
+ * ```ts
1051
+ * import { allResult, err, ok, type Result } from "@evolu/common";
1052
+ *
1053
+ * interface ProductNotFoundError {
1054
+ * readonly type: "ProductNotFoundError";
1055
+ * readonly productId: string;
1056
+ * }
1057
+ *
1058
+ * const pricesByProductId = new Map([
1059
+ * ["book", 20],
1060
+ * ["pen", 5],
1061
+ * ]);
1062
+ * const getPrice = (
1063
+ * productId: string,
1064
+ * ): Result<number, ProductNotFoundError> => {
1065
+ * const price = pricesByProductId.get(productId);
1066
+ * return price == null
1067
+ * ? err({ type: "ProductNotFoundError", productId })
1068
+ * : ok(price);
1069
+ * };
1070
+ *
1071
+ * const productIds: Iterable<string> = new Set(["book", "pen"]);
1072
+ * const prices = allResult(productIds, getPrice);
1073
+ * expectTypeOf(prices).toEqualTypeOf<
1074
+ * Result<ReadonlyArray<number>, ProductNotFoundError>
1075
+ * >();
1076
+ * expectOk(prices, [20, 5]);
1077
+ * ```
518
1078
  */
519
- export declare function mapResult<const A extends readonly [unknown, ...Array<unknown>], T, E>(items: A, fn: (a: A[number]) => Result<T, E>): Result<{
520
- [K in keyof A]: T;
521
- }, E>;
1079
+ export declare function allResult<A, R extends AnyResult>(values: Iterable<A>, fn: (value: A) => R): Result<ReadonlyArray<InferOk<R>>, InferErr<R>>;
522
1080
  /**
523
- * For dynamic or generated item lists.
1081
+ * Maps record values to Results and preserves the record's keys.
1082
+ *
1083
+ * ### Example
524
1084
  *
525
1085
  * ```ts
526
- * const users = [{ id: 1 }, { id: 2 }];
527
- * const result = mapResult(users, validateUser);
528
- * // Result<ReadonlyArray<ValidUser>, ValidateUserError>
1086
+ * import { allResult, err, ok, type Result } from "@evolu/common";
1087
+ *
1088
+ * interface User {
1089
+ * readonly id: string;
1090
+ * }
1091
+ *
1092
+ * interface UserNotFoundError {
1093
+ * readonly type: "UserNotFoundError";
1094
+ * readonly userId: string;
1095
+ * }
1096
+ *
1097
+ * const usersById = new Map<string, User>([
1098
+ * ["user-1", { id: "user-1" }],
1099
+ * ["user-2", { id: "user-2" }],
1100
+ * ]);
1101
+ * const findUser = (userId: string): Result<User, UserNotFoundError> => {
1102
+ * const user = usersById.get(userId);
1103
+ * return user == null
1104
+ * ? err({ type: "UserNotFoundError", userId })
1105
+ * : ok(user);
1106
+ * };
1107
+ *
1108
+ * const userIdsByRole = { owner: "user-1", reviewer: "user-2" } as const;
1109
+ * const usersByRole = allResult(userIdsByRole, findUser);
1110
+ * expectTypeOf(usersByRole).toEqualTypeOf<
1111
+ * Result<Readonly<Record<"owner" | "reviewer", User>>, UserNotFoundError>
1112
+ * >();
1113
+ * expectOk(usersByRole, {
1114
+ * owner: { id: "user-1" },
1115
+ * reviewer: { id: "user-2" },
1116
+ * });
529
1117
  * ```
530
1118
  */
531
- export declare function mapResult<A, T, E>(items: Iterable<A>, fn: (a: A) => Result<T, E>): Result<ReadonlyArray<T>, E>;
1119
+ export declare function allResult<A, R extends AnyResult, K extends string>(values: Readonly<Record<K, A>>, fn: (value: A) => R): Result<Readonly<Record<K, InferOk<R>>>, InferErr<R>>;
532
1120
  /**
533
- * Returns object with same keys.
1121
+ * Maps values to Results without collecting their Ok values.
1122
+ *
1123
+ * This avoids allocating the output collection even when the mapped operations
1124
+ * return success values.
1125
+ *
1126
+ * ### Example
534
1127
  *
535
1128
  * ```ts
536
- * const result = mapResult({ a: 1, b: 2 }, double);
537
- * // Result<{ a: number, b: number }, DoubleError>
1129
+ * import { allResult, ok, type Result } from "@evolu/common";
1130
+ *
1131
+ * interface UpdateItemError {
1132
+ * readonly type: "UpdateItemError";
1133
+ * }
1134
+ *
1135
+ * const updatedItemIds: Array<string> = [];
1136
+ * const updateItem = (itemId: string): Result<number, UpdateItemError> => {
1137
+ * updatedItemIds.push(itemId);
1138
+ * return ok(1);
1139
+ * };
1140
+ *
1141
+ * const result = allResult(["item-1", "item-2"], updateItem, {
1142
+ * collect: false,
1143
+ * });
1144
+ * expectTypeOf(result).toEqualTypeOf<Result<void, UpdateItemError>>();
1145
+ * expectOk(result, undefined);
1146
+ * expect(updatedItemIds).toEqual(["item-1", "item-2"]);
538
1147
  * ```
539
1148
  */
540
- export declare function mapResult<A, T, E, K extends string>(items: Readonly<Record<K, A>>, fn: (a: A) => Result<T, E>): Result<Readonly<Record<K, T>>, E>;
1149
+ export declare function allResult<A, R extends AnyResult>(values: Iterable<A>, fn: (value: A) => R, options: {
1150
+ readonly collect: false;
1151
+ }): Result<void, InferErr<R>>;
1152
+ /** Maps record values to Results without collecting their Ok values. */
1153
+ export declare function allResult<A, R extends AnyResult, K extends string>(values: Readonly<Record<K, A>>, fn: (value: A) => R, options: {
1154
+ readonly collect: false;
1155
+ }): Result<void, InferErr<R>>;
541
1156
  /**
542
1157
  * Returns the first successful {@link Result}.
543
1158
  *
544
1159
  * If all results fail, returns the last error.
545
1160
  *
546
1161
  * Requires a non-empty array — there's no "first success" with zero
547
- * participants. Use {@link isNonEmptyArray} to guard:
1162
+ * participants. Use {@link isNonEmptyArray} to guard.
1163
+ *
1164
+ * ### Example
548
1165
  *
549
1166
  * ```ts
1167
+ * import {
1168
+ * anyResult,
1169
+ * err,
1170
+ * isNonEmptyArray,
1171
+ * ok,
1172
+ * type Result,
1173
+ * } from "@evolu/common";
1174
+ *
1175
+ * interface LookupError {
1176
+ * readonly type: "LookupError";
1177
+ * }
1178
+ *
1179
+ * const results: ReadonlyArray<Result<number, LookupError>> = [
1180
+ * err({ type: "LookupError" }),
1181
+ * ok(42),
1182
+ * ];
550
1183
  * if (isNonEmptyArray(results)) {
551
- * const result = anyResult(results);
1184
+ * const number = anyResult(results);
1185
+ * expectTypeOf(number).toEqualTypeOf<Result<number, LookupError>>();
1186
+ * expectOk(number, 42);
552
1187
  * }
553
1188
  * ```
554
1189
  *
555
- * ### Example
1190
+ * A statically non-empty tuple needs no guard:
556
1191
  *
557
1192
  * ```ts
558
- * const results = [err("fail1"), ok(42), err("fail2")];
559
- * if (isNonEmptyArray(results)) {
560
- * const result = anyResult(results);
561
- * // ok(42)
562
- * }
1193
+ * import { anyResult, err, ok, type Result } from "@evolu/common";
563
1194
  *
564
- * const allFailed = [err("a"), err("b"), err("c")];
565
- * if (isNonEmptyArray(allFailed)) {
566
- * const result = anyResult(allFailed);
567
- * // err("c") — last error
1195
+ * interface LookupError {
1196
+ * readonly type: "LookupError";
1197
+ * readonly source: string;
568
1198
  * }
1199
+ *
1200
+ * const lookupError = (source: string): Result<never, LookupError> =>
1201
+ * err({ type: "LookupError", source });
1202
+ *
1203
+ * const results = [
1204
+ * lookupError("first"),
1205
+ * ok(42),
1206
+ * lookupError("second"),
1207
+ * ] as const;
1208
+ * const number = anyResult(results);
1209
+ * expectTypeOf(number).toEqualTypeOf<Result<number, LookupError>>();
1210
+ * expectOk(number, 42);
1211
+ *
1212
+ * const allFailed = [lookupError("a"), lookupError("b")] as const;
1213
+ * const result = anyResult(allFailed);
1214
+ * expectTypeOf(result).toEqualTypeOf<Result<never, LookupError>>();
1215
+ * expectErr(result, { type: "LookupError", source: "b" });
569
1216
  * ```
570
1217
  *
571
1218
  * @group Composition