@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
package/src/Result.ts CHANGED
@@ -1,20 +1,19 @@
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
6
 
7
7
  import {
8
- arrayFrom,
9
8
  emptyArray,
10
9
  isNonEmptyArray,
11
10
  type NonEmptyReadonlyArray,
12
- } from "./Array.js";
13
- import { assert } from "./Assert.js";
14
- import type { Lazy } from "./Function.js";
15
- import { exhaustiveCheck } from "./Function.js";
16
- import { createRecord, emptyRecord, isIterable } from "./Object.js";
17
- import type { Typed } from "./Type.js";
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";
18
17
 
19
18
  /**
20
19
  * The problem with `throw` in JavaScript is that the caught value is always of
@@ -26,28 +25,48 @@ import type { Typed } from "./Type.js";
26
25
  * `Either` where errors are part of the return type. TypeScript can do the
27
26
  * same.
28
27
  *
28
+ * ## Example
29
+ *
29
30
  * ```ts
30
- * // With throw - caught value is unknown, can't use exhaustiveCheck
31
+ * import { err, exhaustiveCheck, type Result } from "@evolu/common";
32
+ *
33
+ * // TypeScript can't know what was thrown.
31
34
  * try {
32
- * doSomething();
35
+ * throw new Error("Not found");
33
36
  * } catch (error) {
34
- * // error is unknown - TypeScript can't help us here
37
+ * expectTypeOf(error).toEqualTypeOf<unknown>();
38
+ * }
39
+ *
40
+ * // With Result, errors are part of the return type.
41
+ * const doSomething = (): Result<
42
+ * number,
43
+ * NotFoundError | InvalidInputError
44
+ * > => err({ type: "NotFound" });
45
+ *
46
+ * interface NotFoundError {
47
+ * readonly type: "NotFound";
48
+ * }
49
+ *
50
+ * interface InvalidInputError {
51
+ * readonly type: "InvalidInput";
35
52
  * }
36
53
  *
37
- * // With Result - error type is known, exhaustiveCheck works
54
+ * // With Result, the error type is known and exhaustiveCheck works.
55
+ * // If we add another error type, TypeScript tells us it isn't handled.
38
56
  * const result = doSomething();
39
57
  * if (!result.ok) {
40
58
  * switch (result.error.type) {
41
59
  * case "NotFound":
42
- * // handle not found
60
+ * expect(result.error).toEqual({ type: "NotFound" });
43
61
  * break;
44
62
  * case "InvalidInput":
45
- * // handle invalid input
63
+ * expect(result.error).toEqual({ type: "InvalidInput" });
46
64
  * break;
47
65
  * default:
48
- * exhaustiveCheck(result.error); // TypeScript ensures all cases handled
66
+ * exhaustiveCheck(result.error);
49
67
  * }
50
68
  * }
69
+ * expectErr(result, { type: "NotFound" });
51
70
  * ```
52
71
  *
53
72
  * A `Result` is either {@link Ok} (success with a value) or {@link Err} (failure
@@ -67,143 +86,250 @@ import type { Typed } from "./Type.js";
67
86
  * }
68
87
  * ```
69
88
  *
70
- * ## Example
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.
94
+ *
95
+ * ## Composition
96
+ *
97
+ * Since `Result` is a plain object, imperative code works naturally.
71
98
  *
72
99
  * ```ts
73
- * // Typed<T> adds { type: T } for discriminated unions
74
- * interface ParseJsonError extends Typed<"ParseJsonError"> {
75
- * readonly message: string;
100
+ * import { ok, type Result } from "@evolu/common";
101
+ *
102
+ * interface User {
103
+ * readonly id: string;
76
104
  * }
77
105
  *
78
- * const parseJson = (value: string): Result<unknown, ParseJsonError> => {
79
- * try {
80
- * return ok(JSON.parse(value));
81
- * } catch (error) {
82
- * return err({ type: "ParseJsonError", message: String(error) });
83
- * }
84
- * };
106
+ * interface UserNotFoundError {
107
+ * readonly type: "UserNotFoundError";
108
+ * }
85
109
  *
86
- * const json = parseJson('{"name": "Alice"}');
87
- * if (!json.ok) return json; // short-circuit on error
110
+ * interface Profile {
111
+ * readonly userId: string;
112
+ * }
88
113
  *
89
- * // Now we have access to json.value (type: unknown)
90
- * console.log(json.value);
91
- * ```
114
+ * interface ProfileNotFoundError {
115
+ * readonly type: "ProfileNotFoundError";
116
+ * }
92
117
  *
93
- * The caller doesn't need `try/catch`, just `if (!json.ok)`, and the error is
94
- * `ParseJsonError`, not `unknown`.
118
+ * const getUser = (): Result<User, UserNotFoundError> =>
119
+ * ok({ id: "user-1" });
120
+ * const getProfile = (
121
+ * userId: string,
122
+ * ): Result<Profile, ProfileNotFoundError> => ok({ userId });
95
123
  *
96
- * Use this pattern when the caller can recover or choose a different flow.
97
- * Parsing unknown JSON is a good fit because invalid input is expected and
98
- * actionable.
124
+ * const getCurrentProfile = (): Result<
125
+ * Profile,
126
+ * UserNotFoundError | ProfileNotFoundError
127
+ * > => {
128
+ * const user = getUser();
129
+ * if (!user.ok) return user;
99
130
  *
100
- * To avoid `try/catch` inside `parseJson` too, use {@link trySync}:
131
+ * return getProfile(user.value.id);
132
+ * };
101
133
  *
102
- * ```ts
103
- * const parseJson = (value: string): Result<unknown, ParseJsonError> =>
104
- * trySync(
105
- * () => JSON.parse(value) as unknown,
106
- * (error) => ({ type: "ParseJsonError", message: String(error) }),
107
- * );
134
+ * const profile = getCurrentProfile();
135
+ * expectTypeOf(profile).toEqualTypeOf<
136
+ * Result<Profile, UserNotFoundError | ProfileNotFoundError>
137
+ * >();
138
+ * expectOk(profile, { userId: "user-1" });
108
139
  * ```
109
140
  *
110
- * `trySync` and {@link tryAsync} are for intentionally converting thrown errors
111
- * into typed, recoverable {@link Result} values.
141
+ * {@link flatMapResult} is for a single local composition.
112
142
  *
113
- * Do not wrap every throwing API in {@link Result}. If an error is unrecoverable
114
- * and the caller has no meaningful fallback, let it throw and handle it at the
115
- * app boundary.
143
+ * ```ts
144
+ * import { flatMapResult, ok, type Result } from "@evolu/common";
116
145
  *
117
- * Since `Result` is a plain object, imperative code works naturally:
146
+ * interface User {
147
+ * readonly id: string;
148
+ * }
118
149
  *
119
- * ### Stop on error, map on success
150
+ * interface UserNotFoundError {
151
+ * readonly type: "UserNotFoundError";
152
+ * }
120
153
  *
121
- * ```ts
122
- * const users = getActiveUsers();
123
- * if (!users.ok) return users;
124
- * const usernames = mapArray(users.value, (u) => u.username);
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" });
125
173
  * ```
126
174
  *
127
- * ### Iterate array, stop on first error
175
+ * Use {@link allResult} to collect the Ok values from a collection of Results,
176
+ * or return the first Err.
128
177
  *
129
178
  * ```ts
130
- * for (const user of users) {
131
- * const result = validateUser(user); // Result<ValidUser, ValidateUserError>
132
- * if (!result.ok) return result;
179
+ * import { allResult, err, ok, type Result } from "@evolu/common";
180
+ *
181
+ * interface User {
182
+ * readonly id: string;
133
183
  * }
134
- * ```
135
184
  *
136
- * ## Style
185
+ * interface Preferences {
186
+ * readonly theme: "dark" | "light";
187
+ * }
137
188
  *
138
- * Imperative code is the preferred way to compose sequential {@link Result}
139
- * operations.
189
+ * interface LoadAccountError {
190
+ * readonly type: "LoadAccountError";
191
+ * readonly resource: "user" | "preferences";
192
+ * }
140
193
  *
141
- * ```ts
142
- * const user = getUser();
143
- * if (!user.ok) return user;
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
+ * ```
144
218
  *
145
- * const profile = getProfile(user.value.id);
146
- * if (!profile.ok) return profile;
219
+ * `allResult` can also map collection values to Results before collecting their
220
+ * Ok values. Mapping stops on the first Err.
147
221
  *
148
- * return ok({ user: user.value, profile: profile.value });
149
- * ```
222
+ * ```ts
223
+ * import { allResult, err, ok, type Result } from "@evolu/common";
150
224
  *
151
- * This is an intentional style choice. Evolu does not provide helper
152
- * combinators for every sequential pattern because that would duplicate plain
153
- * control flow and create API ambiguity. Use helpers when they add semantics
154
- * over ordinary control flow, such as operating on collections of results.
155
- * While it may seem verbose, it is explicit, transparent, and avoids pipes and
156
- * nested helpers, which are harder to debug.
225
+ * interface OrderLine {
226
+ * readonly productId: string;
227
+ * readonly quantity: number;
228
+ * }
157
229
  *
158
- * ## Composition
230
+ * interface ProductUnavailableError {
231
+ * readonly type: "ProductUnavailableError";
232
+ * readonly productId: string;
233
+ * }
159
234
  *
160
- * Some patterns are common enough that deserve helpers. The previous example
161
- * can be written with {@link mapResult}:
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
+ * };
162
246
  *
163
- * ```ts
164
- * const result = mapResult(users, validateUser);
165
- * // Result<ValidUser[], ValidateUserError>
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
+ * });
166
259
  * ```
167
260
  *
168
- * For an array of results, {@link allResult} extracts all values or returns the
169
- * first error:
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.
170
264
  *
171
265
  * ```ts
172
- * const result = allResult(validationResults);
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);
173
306
  * ```
174
307
  *
175
308
  * For the first success, {@link anyResult} returns the first Ok or the last
176
309
  * error if all fail:
177
310
  *
178
311
  * ```ts
179
- * const result = anyResult(parserResults);
180
- * ```
312
+ * import { anyResult, err, ok, type Result } from "@evolu/common";
181
313
  *
182
- * ## Naming convention
314
+ * interface CacheMissError {
315
+ * readonly type: "CacheMissError";
316
+ * }
183
317
  *
184
- * - Result with a value: name it after the value (`user`, `config`)
185
- * - Result without a value: name it `result`
318
+ * const getCachedPrice = (): Result<number, CacheMissError> =>
319
+ * err({ type: "CacheMissError" });
186
320
  *
187
- * ```ts
188
- * const processUser = (): Result<
189
- * void,
190
- * GetUserError | SaveToDatabaseError | SendWelcomeEmailError
191
- * > => {
192
- * const user = getUser();
193
- * if (!user.ok) return user;
194
- *
195
- * const result = saveToDatabase(user.value);
196
- * if (!result.ok) return result;
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
+ * ```
197
326
  *
198
- * // To avoid a clash with the previous `result`, use a block scope.
199
- * {
200
- * const result = sendWelcomeEmail(user.value);
201
- * if (!result.ok) return result;
202
- * }
327
+ * ## Naming convention
203
328
  *
204
- * return ok();
205
- * };
206
- * ```
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.
207
333
  *
208
334
  * ## Unrecoverable errors
209
335
  *
@@ -211,16 +337,49 @@ import type { Typed } from "./Type.js";
211
337
  * These are unrecoverable errors: expected (you know they can happen) but only
212
338
  * handleable at the app level.
213
339
  *
214
- * Do not force these errors into {@link Result} just because the underlying API
340
+ * Do not force these errors into `Result` just because the underlying API
215
341
  * throws. If the local caller cannot recover, let the error propagate to a
216
342
  * global handler or other app boundary.
217
343
  *
218
- * In Evolu apps, that boundary is typically a platform `createRun` adapter such
219
- * as `@evolu/web`, `@evolu/nodejs`, or `@evolu/react-native`, which add
220
- * platform-specific global error handling.
344
+ * In Evolu apps, the root Run reports defects and the platform lifecycle API
345
+ * owns application shutdown. For example, `@evolu/nodejs` provides `runMain`.
221
346
  *
222
347
  * ## FAQ
223
348
  *
349
+ * ### Is Result expensive?
350
+ *
351
+ * Wrapping a value in Result creates one small plain object and is very cheap.
352
+ * Its cost is usually worth considering only when a performance-critical loop
353
+ * creates millions of Results. Use Result by default; if profiling identifies
354
+ * such a loop as a bottleneck, benchmark the complete workload before
355
+ * optimizing it.
356
+ *
357
+ * ### Why not generators?
358
+ *
359
+ * Generator-based APIs make sequential workflows more concise: `yield*`
360
+ * combines error propagation (roughly replacing `if (!result.ok) return
361
+ * result`) with unwrapping the success value. That doesn't come for free.
362
+ * Supporting direct `yield*` requires adding iterator behavior to every Result;
363
+ * otherwise every use needs an adapter. That makes Result more than plain
364
+ * structural data: after serialization, its iterator behavior must be restored
365
+ * before direct `yield*` works. Generator machinery also makes control flow,
366
+ * debugger stepping, and stack traces less direct while adding runtime
367
+ * overhead. Evolu instead keeps Result as plain data and makes error
368
+ * propagation explicit. With AI, explicit checks are cheap to write.
369
+ *
370
+ * Generators do not make accidental omission impossible: a function returning a
371
+ * lazy operation can be called without composing the returned value with
372
+ * `yield*`, leaving the operation out of the workflow, just as a
373
+ * Result-returning function can be called and its Result ignored. Dedicated
374
+ * tooling can detect these omissions, but that safety comes from the tooling,
375
+ * not generator syntax itself.
376
+ *
377
+ * The intended way to write Evolu code is with test-driven development (TDD).
378
+ * Tests document the intended behavior and serve as its runnable specification,
379
+ * including failure paths. AI makes writing and maintaining those tests cheap.
380
+ * {@link exhaustiveCheck} complements tests by having TypeScript report newly
381
+ * added error variants.
382
+ *
224
383
  * ### What if a function doesn't return a value on success?
225
384
  *
226
385
  * Use `Result<void, E>` and return `ok()` (no argument). Don't return
@@ -251,15 +410,28 @@ export interface Ok<out T> {
251
410
  * ### Example
252
411
  *
253
412
  * ```ts
413
+ * import { err, ok, type Result, type Typed } from "@evolu/common";
414
+ *
415
+ * interface User {
416
+ * readonly id: string;
417
+ * }
418
+ *
254
419
  * interface NotFoundError extends Typed<"NotFoundError"> {
255
420
  * readonly id: string;
256
421
  * }
257
422
  *
423
+ * const users = new Map<string, User>([["user-1", { id: "user-1" }]]);
258
424
  * const findUser = (id: string): Result<User, NotFoundError> => {
259
425
  * const user = users.get(id);
260
426
  * if (user == null) return err({ type: "NotFoundError", id });
261
427
  * return ok(user);
262
428
  * };
429
+ *
430
+ * expectOk(findUser("user-1"), { id: "user-1" });
431
+ * expectErr(findUser("missing"), {
432
+ * type: "NotFoundError",
433
+ * id: "missing",
434
+ * });
263
435
  * ```
264
436
  */
265
437
  export interface Err<out E> {
@@ -268,7 +440,7 @@ export interface Err<out E> {
268
440
  }
269
441
 
270
442
  /**
271
- * Extracts the value type from a {@link Result}.
443
+ * Infers the success value type from a {@link Result}.
272
444
  *
273
445
  * @group Utilities
274
446
  */
@@ -276,7 +448,7 @@ export type InferOk<R extends Result<any, any>> =
276
448
  R extends Ok<infer T> ? T : never;
277
449
 
278
450
  /**
279
- * Extracts the error type from a {@link Result}.
451
+ * Infers the error type from a {@link Result}.
280
452
  *
281
453
  * @group Utilities
282
454
  */
@@ -293,11 +465,15 @@ export type InferErr<R extends Result<any, any>> =
293
465
  * ### Example
294
466
  *
295
467
  * ```ts
296
- * const noValue = ok();
297
- * console.log(noValue); // { ok: true, value: undefined }
468
+ * import { ok, type Result } from "@evolu/common";
298
469
  *
470
+ * const noValue = ok();
299
471
  * const success = ok(42);
300
- * console.log(success); // { ok: true, value: 42 }
472
+ *
473
+ * expectTypeOf(noValue).toEqualTypeOf<Result<void>>();
474
+ * expectTypeOf(success).toEqualTypeOf<Result<number>>();
475
+ * expectOk(noValue, undefined);
476
+ * expectOk(success, 42);
301
477
  * ```
302
478
  */
303
479
  export function ok(): Result<void>;
@@ -322,7 +498,7 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
322
498
  !result.ok;
323
499
 
324
500
  /**
325
- * Returns the value from an `Ok` {@link Result}, or throws if it is an `Err`.
501
+ * Gets the value from an `Ok` {@link Result}, or throws if it is an `Err`.
326
502
  *
327
503
  * Use this where failure should crash the current flow instead of being handled
328
504
  * locally.
@@ -330,8 +506,8 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
330
506
  * **When to use:**
331
507
  *
332
508
  * - Application startup or composition-root setup where errors must stop the
333
- * program immediately. In Evolu apps, errors are handled by platform-specific
334
- * `createRun` adapters at the app boundary.
509
+ * program immediately. In Evolu apps, the root Run reports the defect and the
510
+ * platform lifecycle API handles shutdown.
335
511
  * - Module-level constants
336
512
  * - Test setup with values that are expected to be valid
337
513
  *
@@ -341,9 +517,30 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
341
517
  * ### Example
342
518
  *
343
519
  * ```ts
344
- * // At app startup, crash if config is invalid:
520
+ * import { err, getOrThrow, ok, type Result } from "@evolu/common";
521
+ *
522
+ * interface Config {
523
+ * readonly port: number;
524
+ * }
525
+ * interface InvalidConfigError {
526
+ * readonly type: "InvalidConfigError";
527
+ * }
528
+ * const loadConfig = (): Result<Config, InvalidConfigError> =>
529
+ * ok({ port: 3000 });
530
+ *
531
+ * // At app startup, crash if the config is invalid.
345
532
  * const config = getOrThrow(loadConfig());
346
- * // Safe to use config here
533
+ * expectTypeOf(config).toEqualTypeOf<Config>();
534
+ * expect(config).toEqual({ port: 3000 });
535
+ *
536
+ * try {
537
+ * getOrThrow(err({ type: "InvalidConfigError" }));
538
+ * assert.fail("Expected getOrThrow to throw");
539
+ * } catch (error) {
540
+ * assert(error instanceof Error);
541
+ * expect(error.message).toBe("getOrThrow");
542
+ * expect(error.cause).toEqual({ type: "InvalidConfigError" });
543
+ * }
347
544
  * ```
348
545
  *
349
546
  * Throws: `Error` with the original error attached as `cause`.
@@ -357,8 +554,8 @@ export const getOrThrow = <T, E>(result: Result<T, E>): T => {
357
554
  };
358
555
 
359
556
  /**
360
- * Extracts the value from a {@link Result} if it is an `Ok`, or returns `null`
361
- * if it is an `Err`.
557
+ * Gets the value from an `Ok` {@link Result}, or returns `null` if it is an
558
+ * `Err`.
362
559
  *
363
560
  * **Intended usage:**
364
561
  *
@@ -369,19 +566,46 @@ export const getOrThrow = <T, E>(result: Result<T, E>): T => {
369
566
  * ### Example
370
567
  *
371
568
  * ```ts
372
- * // For APIs that expect T | null
373
- * const user = getOrNull(findUser(id));
569
+ * import { err, getOrNull, ok, type Result } from "@evolu/common";
570
+ *
571
+ * interface User {
572
+ * readonly id: string;
573
+ * }
574
+ * interface UserNotFoundError {
575
+ * readonly type: "UserNotFoundError";
576
+ * }
577
+ * const findUser = (id: string): Result<User, UserNotFoundError> =>
578
+ * id === "user-1" ? ok({ id }) : err({ type: "UserNotFoundError" });
579
+ *
580
+ * // For APIs that expect T | null.
581
+ * const user = getOrNull(findUser("user-1"));
582
+ * const missingUser = getOrNull(findUser("missing"));
583
+ *
584
+ * expectTypeOf(user).toEqualTypeOf<User | null>();
585
+ * expect(user).toEqual({ id: "user-1" });
586
+ * expect(missingUser).toBeNull();
374
587
  * ```
375
588
  */
376
589
  export const getOrNull = <T, E>(result: Result<T, E>): T | null =>
377
590
  result.ok ? result.value : null;
378
591
 
379
592
  /**
380
- * Extracts the value from a {@link Result} whose error type is `never`.
593
+ * Gets the value from a {@link Result} whose error type is `never`.
381
594
  *
382
595
  * This is useful when the type system guarantees the result cannot fail (for
383
596
  * example `Result<T, never>`), avoiding impossible `if (!result.ok)` branches
384
597
  * at call sites.
598
+ *
599
+ * ### Example
600
+ *
601
+ * ```ts
602
+ * import { getOk, ok, type Result } from "@evolu/common";
603
+ *
604
+ * const getCount = (): Result<number> => ok(2);
605
+ * const count = getOk(getCount());
606
+ * expectTypeOf(count).toEqualTypeOf<number>();
607
+ * expect(count).toBe(2);
608
+ * ```
385
609
  */
386
610
  export const getOk = <T>(result: Result<T>): T => {
387
611
  assert(result.ok, "Expected Ok result.");
@@ -391,60 +615,133 @@ export const getOk = <T>(result: Result<T>): T => {
391
615
  /**
392
616
  * Wraps a synchronous function that may throw, returning a {@link Result}.
393
617
  *
394
- * Use this when the thrown value should become a typed, recoverable error for
395
- * the caller. Do not use it for failures that should terminate the current flow
396
- * and propagate to a global handler.
618
+ * `mapError` converts the caught `unknown` value into a typed domain error.
619
+ *
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.
397
624
  *
398
625
  * ### Example
399
626
  *
400
627
  * ```ts
401
- * const parseJson = (value: string): Result<unknown, ParseJsonError> =>
628
+ * import { trySync, type Result } from "@evolu/common";
629
+ *
630
+ * interface ReserveSeatError {
631
+ * readonly type: "ReserveSeatError";
632
+ * readonly seat: string;
633
+ * }
634
+ *
635
+ * class LegacySeatUnavailableError extends Error {}
636
+ *
637
+ * const legacyReserveSeat = (seat: string): void => {
638
+ * if (seat === "A1") throw new LegacySeatUnavailableError();
639
+ * if (seat === "B1") throw new Error("Unexpected database error");
640
+ * };
641
+ *
642
+ * const reserveSeat = (seat: string): Result<void, ReserveSeatError> =>
402
643
  * trySync(
403
- * () => JSON.parse(value) as unknown,
404
- * (error) => ({ type: "ParseJsonError", message: String(error) }),
644
+ * () => legacyReserveSeat(seat),
645
+ * (error) => {
646
+ * if (error instanceof LegacySeatUnavailableError) {
647
+ * return { type: "ReserveSeatError", seat };
648
+ * }
649
+ * throw error;
650
+ * },
405
651
  * );
652
+ *
653
+ * const result = reserveSeat("B2");
654
+ * expectTypeOf(result).toEqualTypeOf<Result<void, ReserveSeatError>>();
655
+ * expectOk(result, undefined);
656
+ * expectErr(reserveSeat("A1"), { type: "ReserveSeatError", seat: "A1" });
657
+ * expect(() => reserveSeat("B1")).toThrow("Unexpected database error");
406
658
  * ```
407
659
  */
408
- export const trySync = <T, E>(
660
+ export function trySync<T>(fn: () => T): Result<T, unknown>;
661
+ export function trySync<T, E>(
409
662
  fn: () => T,
410
663
  mapError: (error: unknown) => E,
411
- ): Result<T, E> => {
664
+ ): Result<T, E>;
665
+ export function trySync<T, E>(
666
+ fn: () => T,
667
+ mapError?: (error: unknown) => E,
668
+ ): Result<T, E | unknown> {
412
669
  try {
413
670
  return ok(fn());
414
671
  } catch (error) {
415
- return err(mapError(error));
672
+ return err(mapError ? mapError(error) : error);
416
673
  }
417
- };
674
+ }
418
675
 
419
676
  /**
420
- * Wraps an async function that may throw, returning a {@link Result}.
677
+ * Wraps an async function that may throw or reject, returning a {@link Result}.
678
+ *
679
+ * `mapError` converts the caught `unknown` value into a typed domain error.
421
680
  *
422
- * Use this when the rejection should become a typed, recoverable error for the
423
- * caller. Do not use it for failures that should terminate the current flow and
424
- * propagate to a global handler.
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.
425
685
  *
426
686
  * ### Example
427
687
  *
428
688
  * ```ts
429
- * const fetchJson = (url: string): Promise<Result<unknown, FetchError>> =>
689
+ * import { tryAsync, type Result } from "@evolu/common";
690
+ *
691
+ * interface ReserveSeatError {
692
+ * readonly type: "ReserveSeatError";
693
+ * readonly seat: string;
694
+ * }
695
+ *
696
+ * class LegacySeatUnavailableError extends Error {}
697
+ *
698
+ * const legacyReserveSeat = async (seat: string): Promise<void> => {
699
+ * if (seat === "A1") throw new LegacySeatUnavailableError();
700
+ * if (seat === "B1") throw new Error("Unexpected database error");
701
+ * };
702
+ *
703
+ * const reserveSeat = (
704
+ * seat: string,
705
+ * ): Promise<Result<void, ReserveSeatError>> =>
430
706
  * tryAsync(
431
- * async () => {
432
- * const response = await fetch(url);
433
- * if (!response.ok) throw new Error(`Status ${response.status}`);
434
- * return response.json();
707
+ * () => legacyReserveSeat(seat),
708
+ * (error) => {
709
+ * if (error instanceof LegacySeatUnavailableError) {
710
+ * return { type: "ReserveSeatError", seat };
711
+ * }
712
+ * throw error;
435
713
  * },
436
- * (error) => ({ type: "FetchError", message: String(error) }),
437
714
  * );
715
+ *
716
+ * const result = await reserveSeat("B2");
717
+ * expectTypeOf(result).toEqualTypeOf<Result<void, ReserveSeatError>>();
718
+ * expectOk(result, undefined);
719
+ * expectErr(await reserveSeat("A1"), {
720
+ * type: "ReserveSeatError",
721
+ * seat: "A1",
722
+ * });
723
+ * await expect(reserveSeat("B1")).rejects.toThrow(
724
+ * "Unexpected database error",
725
+ * );
438
726
  * ```
439
727
  */
440
- export const tryAsync = <T, E>(
441
- lazyPromise: Lazy<Promise<T>>,
728
+ export function tryAsync<T>(
729
+ promiseThunk: Thunk<Awaitable<T>>,
730
+ ): Promise<Result<T, unknown>>;
731
+ export function tryAsync<T, E>(
732
+ promiseThunk: Thunk<Awaitable<T>>,
442
733
  mapError: (error: unknown) => E,
443
- ): Promise<Result<T, E>> =>
444
- Promise.try(lazyPromise).then(
445
- (value) => ok(value),
446
- (error: unknown) => err(mapError(error)),
447
- );
734
+ ): Promise<Result<T, E>>;
735
+ export async function tryAsync<T, E>(
736
+ promiseThunk: Thunk<Awaitable<T>>,
737
+ mapError?: (error: unknown) => E,
738
+ ): Promise<Result<T, E | unknown>> {
739
+ try {
740
+ return ok(await promiseThunk());
741
+ } catch (error) {
742
+ return err(mapError ? mapError(error) : error);
743
+ }
744
+ }
448
745
 
449
746
  /**
450
747
  * A result for a pull-based protocol with three outcomes.
@@ -457,6 +754,30 @@ export const tryAsync = <T, E>(
457
754
  * - `Err<E>` — failed with an error
458
755
  *
459
756
  * Inspired by JavaScript's `Iterator.next()`, which returns `{ value, done }`.
757
+ *
758
+ * ### Example
759
+ *
760
+ * ```ts
761
+ * import { done, err, ok, type NextResult } from "@evolu/common";
762
+ *
763
+ * interface ReadError {
764
+ * readonly type: "ReadError";
765
+ * }
766
+ *
767
+ * const next = (index: number): NextResult<string, ReadError, number> => {
768
+ * if (index === 0) return ok("first");
769
+ * if (index === 1) return err({ type: "ReadError" });
770
+ * return err(done(index));
771
+ * };
772
+ *
773
+ * const value = next(0);
774
+ * expectTypeOf(value).toEqualTypeOf<
775
+ * NextResult<string, ReadError, number>
776
+ * >();
777
+ * expectOk(value, "first");
778
+ * expectErr(next(1), { type: "ReadError" });
779
+ * expectErr(next(2), { type: "Done", done: 2 });
780
+ * ```
460
781
  */
461
782
  export type NextResult<A, E = never, D = void> = Result<A, E | Done<D>>;
462
783
 
@@ -478,6 +799,19 @@ export interface Done<out D = unknown> extends Typed<"Done"> {
478
799
  *
479
800
  * - `done()` creates a `Done<void>` for protocols that don't need a done value.
480
801
  * - `done(value)` creates a `Done<D>` containing the specified value.
802
+ *
803
+ * ### Example
804
+ *
805
+ * ```ts
806
+ * import { done, type Done } from "@evolu/common";
807
+ *
808
+ * const withoutValue = done();
809
+ * const withValue = done(42);
810
+ * expectTypeOf(withoutValue).toEqualTypeOf<Done<void>>();
811
+ * expectTypeOf(withValue).toEqualTypeOf<Done<number>>();
812
+ * expect(withoutValue).toEqual({ type: "Done", done: undefined });
813
+ * expect(withValue).toEqual({ type: "Done", done: 42 });
814
+ * ```
481
815
  */
482
816
  export function done(): Done<void>;
483
817
  /** With a done value. */
@@ -495,6 +829,15 @@ export function done<D>(value?: D): Done<D> {
495
829
  * Useful for pull-based protocols where completion is encoded in the error
496
830
  * channel (for example {@link NextResult}).
497
831
  *
832
+ * ### Example
833
+ *
834
+ * ```ts
835
+ * import { type Done, type ExcludeDone } from "@evolu/common";
836
+ *
837
+ * type Errors = "ReadError" | Done<number>;
838
+ * expectTypeOf<ExcludeDone<Errors>>().toEqualTypeOf<"ReadError">();
839
+ * ```
840
+ *
498
841
  * @group Utilities
499
842
  */
500
843
  export type ExcludeDone<E> = Exclude<E, Done<any>>;
@@ -505,12 +848,30 @@ export type ExcludeDone<E> = Exclude<E, Done<any>>;
505
848
  * Useful for pull-based protocols where completion is encoded in the error
506
849
  * channel (for example {@link NextResult}).
507
850
  *
851
+ * ### Example
852
+ *
853
+ * ```ts
854
+ * import { type Done, type OnlyDone } from "@evolu/common";
855
+ *
856
+ * type Errors = "ReadError" | Done<number>;
857
+ * expectTypeOf<OnlyDone<Errors>>().toEqualTypeOf<Done<number>>();
858
+ * ```
859
+ *
508
860
  * @group Utilities
509
861
  */
510
862
  export type OnlyDone<E> = Extract<E, Done<any>>;
511
863
 
512
864
  /**
513
- * Extracts the done value type from a {@link NextResult}.
865
+ * Infers the done value type from a {@link NextResult}.
866
+ *
867
+ * ### Example
868
+ *
869
+ * ```ts
870
+ * import { type InferDone, type NextResult } from "@evolu/common";
871
+ *
872
+ * type ReadResult = NextResult<string, "ReadError", number>;
873
+ * expectTypeOf<InferDone<ReadResult>>().toEqualTypeOf<number>();
874
+ * ```
514
875
  *
515
876
  * @group Utilities
516
877
  */
@@ -522,20 +883,100 @@ export type InferDone<R extends Result<any, any>> =
522
883
  : never;
523
884
 
524
885
  /**
525
- * Extracts all values from an array of {@link Result}s.
886
+ * Composes a successful {@link Result} with another Result-returning operation.
887
+ *
888
+ * Returns the existing error without calling the operation when the Result has
889
+ * failed.
890
+ *
891
+ * Do not nest `flatMapResult`. For longer workflows, use explicit checks, which
892
+ * keep names, intermediate values, and control flow flat and easy to read.
893
+ *
894
+ * ### Example
895
+ *
896
+ * ```ts
897
+ * import { err, flatMapResult, ok, type Result } from "@evolu/common";
898
+ *
899
+ * interface User {
900
+ * readonly id: string;
901
+ * }
902
+ * interface UserError {
903
+ * readonly type: "UserError";
904
+ * }
905
+ *
906
+ * interface Profile {
907
+ * readonly userId: string;
908
+ * }
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
+ *
923
+ * const user: Result<User, UserError> = ok({ id: "user-1" });
924
+ * const profile = flatMapResult(user, getProfileForUser);
925
+ * expectTypeOf(profile).toEqualTypeOf<
926
+ * Result<Profile, UserError | ProfileError>
927
+ * >();
928
+ * 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
+ * ```
936
+ *
937
+ * @group Composition
938
+ */
939
+ export const flatMapResult = <T, E, U, F>(
940
+ result: Result<T, E>,
941
+ fn: (value: T) => Result<U, F>,
942
+ ): Result<U, E | F> => (result.ok ? fn(result.value) : result);
943
+
944
+ /**
945
+ * Collects the Ok values from a collection of {@link Result}s, or maps
946
+ * collection values to Results and collects their Ok values.
526
947
  *
527
948
  * Returns the first error if any result fails.
528
949
  *
950
+ * Pass `{ collect: false }` to return `Result<void, E>` without storing the Ok
951
+ * values in an output collection.
952
+ *
529
953
  * ### Example
530
954
  *
531
955
  * ```ts
532
- * const results = [ok(1), ok(2), ok(3)];
533
- * const all = allResult(results);
534
- * // ok([1, 2, 3])
956
+ * import { allResult, err, ok, type Result } from "@evolu/common";
957
+ *
958
+ * interface CountError {
959
+ * readonly type: "CountError";
960
+ * }
961
+ * interface LabelError {
962
+ * readonly type: "LabelError";
963
+ * }
964
+ *
965
+ * const getCount = (): Result<number, CountError> => ok(2);
966
+ * const getLabel = (): Result<string, LabelError> => ok("books");
535
967
  *
536
- * const withError = [ok(1), err("fail"), ok(3)];
537
- * const failed = allResult(withError);
538
- * // err("fail")
968
+ * const values = allResult([getCount(), getLabel()]);
969
+ * expectTypeOf(values).toEqualTypeOf<
970
+ * Result<readonly [number, string], CountError | LabelError>
971
+ * >();
972
+ * 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" });
539
980
  * ```
540
981
  *
541
982
  * @group Composition
@@ -545,11 +986,19 @@ export function allResult<
545
986
  >(results: T): Result<{ [K in keyof T]: InferOk<T[K]> }, InferErr<T[number]>>;
546
987
 
547
988
  /**
548
- * Returns object with same keys.
989
+ * Returns an object with the same keys.
990
+ *
991
+ * ### Example
549
992
  *
550
993
  * ```ts
551
- * const result = allResult({ a: ok(1), b: ok(2) });
552
- * // ok({ a: 1, b: 2 })
994
+ * import { allResult, ok, type Result } from "@evolu/common";
995
+ *
996
+ * const resultsByName = { a: ok(1), b: ok(2) } as const;
997
+ * const valuesByName = allResult(resultsByName);
998
+ * expectTypeOf(valuesByName).toEqualTypeOf<
999
+ * Result<{ readonly a: number; readonly b: number }>
1000
+ * >();
1001
+ * expectOk(valuesByName, { a: 1, b: 2 });
553
1002
  * ```
554
1003
  */
555
1004
  export function allResult<T extends Readonly<Record<string, AnyResult>>>(
@@ -562,10 +1011,26 @@ export function allResult<T extends Readonly<Record<string, AnyResult>>>(
562
1011
  /**
563
1012
  * For dynamic or generated result lists.
564
1013
  *
1014
+ * Stops consuming the iterable on the first Err.
1015
+ *
1016
+ * ### Example
1017
+ *
565
1018
  * ```ts
566
- * const results: ReadonlyArray<Result<number, Error>> = getResults();
567
- * const all = allResult(results);
568
- * // Result<ReadonlyArray<number>, Error>
1019
+ * import { allResult, err, ok, type Result } from "@evolu/common";
1020
+ *
1021
+ * interface LoadNumberError {
1022
+ * readonly type: "LoadNumberError";
1023
+ * }
1024
+ *
1025
+ * const results: ReadonlyArray<Result<number, LoadNumberError>> = [
1026
+ * ok(1),
1027
+ * err({ type: "LoadNumberError" }),
1028
+ * ];
1029
+ * const numbers = allResult(results);
1030
+ * expectTypeOf(numbers).toEqualTypeOf<
1031
+ * Result<ReadonlyArray<number>, LoadNumberError>
1032
+ * >();
1033
+ * expectErr(numbers, { type: "LoadNumberError" });
569
1034
  * ```
570
1035
  */
571
1036
  export function allResult<T, E>(
@@ -573,145 +1038,307 @@ export function allResult<T, E>(
573
1038
  ): Result<ReadonlyArray<T>, E>;
574
1039
 
575
1040
  /**
576
- * Guarantees non-empty result.
1041
+ * Preserves a non-empty array on success.
1042
+ *
1043
+ * ### Example
577
1044
  *
578
1045
  * ```ts
579
- * const results: NonEmptyReadonlyArray<Result<number, Error>> = [
580
- * ok(1),
581
- * ok(2),
582
- * ];
583
- * const all = allResult(results);
584
- * // Result<NonEmptyReadonlyArray<number>, Error>
1046
+ * import {
1047
+ * allResult,
1048
+ * ok,
1049
+ * type NonEmptyReadonlyArray,
1050
+ * type Result,
1051
+ * } from "@evolu/common";
1052
+ *
1053
+ * const results: NonEmptyReadonlyArray<Result<number>> = [ok(1), ok(2)];
1054
+ * const numbers = allResult(results);
1055
+ * expectTypeOf(numbers).toEqualTypeOf<
1056
+ * Result<NonEmptyReadonlyArray<number>>
1057
+ * >();
1058
+ * expectOk(numbers, [1, 2]);
585
1059
  * ```
586
1060
  */
587
1061
  export function allResult<T, E>(
588
1062
  results: NonEmptyReadonlyArray<Result<T, E>>,
589
1063
  ): Result<NonEmptyReadonlyArray<T>, E>;
590
1064
 
591
- export function allResult(
592
- input: Iterable<AnyResult> | Readonly<Record<string, AnyResult>>,
593
- ): AnyResult {
594
- if (isIterable(input)) {
595
- const array = arrayFrom(input);
596
- if (!isNonEmptyArray(array)) return ok(emptyArray);
597
-
598
- const length = array.length;
599
- const values = new Array<unknown>(length);
600
- for (let i = 0; i < length; i++) {
601
- const result = array[i];
602
- if (!result.ok) return result;
603
- values[i] = result.value;
604
- }
605
- return ok(values);
606
- }
607
-
608
- const length = Object.keys(input).length;
609
- if (length === 0) return ok(emptyRecord);
610
-
611
- const keys = new Array<string>(length);
612
- const results = new Array<AnyResult>(length);
613
- let index = 0;
614
- for (const key in input) {
615
- keys[index] = key;
616
- results[index] = (input as Record<string, AnyResult>)[key];
617
- index++;
618
- }
619
-
620
- const record = createRecord();
621
- for (let i = 0; i < length; i++) {
622
- const result = results[i];
623
- if (!result.ok) return result;
624
- record[keys[i]] = result.value;
625
- }
626
- return ok(record);
627
- }
1065
+ /** Does not collect the Ok values from a record of Results. */
1066
+ export function allResult<T extends Readonly<Record<string, AnyResult>>>(
1067
+ results: T,
1068
+ options: { readonly collect: false },
1069
+ ): Result<void, [keyof T] extends [never] ? never : InferErr<T[keyof T]>>;
628
1070
 
629
1071
  /**
630
- * Maps items to {@link Result}s and extracts all values.
1072
+ * Does not collect the Ok values from an iterable of Results.
631
1073
  *
632
- * Returns the first error if any result fails.
1074
+ * 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.
633
1077
  *
634
1078
  * ### Example
635
1079
  *
636
1080
  * ```ts
637
- * const users = [{ id: 1 }, { id: 2 }];
638
- * const result = mapResult(users, validateUser);
639
- * // Result<ReadonlyArray<ValidUser>, ValidateUserError>
1081
+ * import { allResult, err, ok, type Result } from "@evolu/common";
1082
+ *
1083
+ * interface SaveError {
1084
+ * readonly type: "SaveError";
1085
+ * }
1086
+ *
1087
+ * const results: ReadonlyArray<Result<number, SaveError>> = [
1088
+ * ok(1),
1089
+ * err({ type: "SaveError" }),
1090
+ * ];
1091
+ * const result = allResult(results, { collect: false });
1092
+ * expectTypeOf(result).toEqualTypeOf<Result<void, SaveError>>();
1093
+ * expectErr(result, { type: "SaveError" });
640
1094
  * ```
1095
+ */
1096
+ export function allResult<R extends AnyResult>(
1097
+ results: Iterable<R>,
1098
+ options: { readonly collect: false },
1099
+ ): Result<void, InferErr<R>>;
1100
+
1101
+ /**
1102
+ * Maps a non-empty array or tuple to Results and preserves its shape.
641
1103
  *
642
- * @group Composition
1104
+ * ### Example
1105
+ *
1106
+ * ```ts
1107
+ * import {
1108
+ * allResult,
1109
+ * ok,
1110
+ * type NonEmptyReadonlyArray,
1111
+ * type Result,
1112
+ * } from "@evolu/common";
1113
+ *
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>
1132
+ * >();
1133
+ * expectOk(invoices, [{ orderId: "order-1" }]);
1134
+ *
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" }]);
1143
+ * ```
643
1144
  */
644
- export function mapResult<
1145
+ export function allResult<
645
1146
  const A extends readonly [unknown, ...Array<unknown>],
646
- T,
647
- E,
1147
+ R extends AnyResult,
648
1148
  >(
649
- items: A,
650
- fn: (a: A[number]) => Result<T, E>,
651
- ): Result<{ [K in keyof A]: T }, E>;
1149
+ values: A,
1150
+ fn: (value: A[number]) => R,
1151
+ ): Result<{ [K in keyof A]: InferOk<R> }, InferErr<R>>;
652
1152
 
653
1153
  /**
654
- * For dynamic or generated item lists.
1154
+ * Maps a dynamic or generated value collection to Results.
1155
+ *
1156
+ * Stops consuming and mapping the iterable on the first Err.
1157
+ *
1158
+ * ### Example
655
1159
  *
656
1160
  * ```ts
657
- * const users = [{ id: 1 }, { id: 2 }];
658
- * const result = mapResult(users, validateUser);
659
- * // Result<ReadonlyArray<ValidUser>, ValidateUserError>
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
+ * };
1180
+ *
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]);
660
1187
  * ```
661
1188
  */
662
- export function mapResult<A, T, E>(
663
- items: Iterable<A>,
664
- fn: (a: A) => Result<T, E>,
665
- ): Result<ReadonlyArray<T>, E>;
1189
+ export function allResult<A, R extends AnyResult>(
1190
+ values: Iterable<A>,
1191
+ fn: (value: A) => R,
1192
+ ): Result<ReadonlyArray<InferOk<R>>, InferErr<R>>;
666
1193
 
667
1194
  /**
668
- * Returns object with same keys.
1195
+ * Maps record values to Results and preserves the record's keys.
1196
+ *
1197
+ * ### Example
669
1198
  *
670
1199
  * ```ts
671
- * const result = mapResult({ a: 1, b: 2 }, double);
672
- * // Result<{ a: number, b: number }, DoubleError>
1200
+ * import { allResult, err, ok, type Result } from "@evolu/common";
1201
+ *
1202
+ * interface User {
1203
+ * readonly id: string;
1204
+ * }
1205
+ *
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
+ * };
1221
+ *
1222
+ * const userIdsByRole = { owner: "user-1", reviewer: "user-2" } as const;
1223
+ * const usersByRole = allResult(userIdsByRole, findUser);
1224
+ * expectTypeOf(usersByRole).toEqualTypeOf<
1225
+ * Result<Readonly<Record<"owner" | "reviewer", User>>, UserNotFoundError>
1226
+ * >();
1227
+ * expectOk(usersByRole, {
1228
+ * owner: { id: "user-1" },
1229
+ * reviewer: { id: "user-2" },
1230
+ * });
673
1231
  * ```
674
1232
  */
675
- export function mapResult<A, T, E, K extends string>(
676
- items: Readonly<Record<K, A>>,
677
- fn: (a: A) => Result<T, E>,
678
- ): Result<Readonly<Record<K, T>>, E>;
1233
+ export function allResult<A, R extends AnyResult, K extends string>(
1234
+ values: Readonly<Record<K, A>>,
1235
+ fn: (value: A) => R,
1236
+ ): Result<Readonly<Record<K, InferOk<R>>>, InferErr<R>>;
679
1237
 
680
- export function mapResult(
1238
+ /**
1239
+ * Maps values to Results without collecting their Ok values.
1240
+ *
1241
+ * This avoids allocating the output collection even when the mapped operations
1242
+ * return success values.
1243
+ *
1244
+ * ### Example
1245
+ *
1246
+ * ```ts
1247
+ * import { allResult, ok, type Result } from "@evolu/common";
1248
+ *
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>>();
1263
+ * expectOk(result, undefined);
1264
+ * expect(updatedItemIds).toEqual(["item-1", "item-2"]);
1265
+ * ```
1266
+ */
1267
+ export function allResult<A, R extends AnyResult>(
1268
+ values: Iterable<A>,
1269
+ fn: (value: A) => R,
1270
+ options: { readonly collect: false },
1271
+ ): Result<void, InferErr<R>>;
1272
+
1273
+ /** Maps record values to Results without collecting their Ok values. */
1274
+ export function allResult<A, R extends AnyResult, K extends string>(
1275
+ values: Readonly<Record<K, A>>,
1276
+ fn: (value: A) => R,
1277
+ options: { readonly collect: false },
1278
+ ): Result<void, InferErr<R>>;
1279
+
1280
+ export function allResult(
681
1281
  input: Iterable<unknown> | Readonly<Record<string, unknown>>,
682
- fn: (a: unknown) => AnyResult,
1282
+ fnOrOptions?: ((value: unknown) => AnyResult) | { readonly collect: false },
1283
+ options?: { readonly collect: false },
683
1284
  ): AnyResult {
684
- if (isIterable(input)) {
685
- const array = arrayFrom(input);
686
- if (!isNonEmptyArray(array)) return ok(emptyArray);
1285
+ const fn = typeof fnOrOptions === "function" ? fnOrOptions : undefined;
1286
+ const collect =
1287
+ typeof fnOrOptions === "function"
1288
+ ? options?.collect !== false
1289
+ : fnOrOptions?.collect !== false;
1290
+
1291
+ if (!collect) {
1292
+ if (isIterable(input)) {
1293
+ for (const value of input) {
1294
+ const result = fn ? fn(value) : (value as AnyResult);
1295
+ if (!result.ok) return result;
1296
+ }
1297
+ return ok();
1298
+ }
1299
+
1300
+ const keys = Object.keys(input);
1301
+
1302
+ for (const key of keys) {
1303
+ const value = (input as Record<string, unknown>)[key];
1304
+ const result = fn ? fn(value) : (value as AnyResult);
1305
+ if (!result.ok) return result;
1306
+ }
1307
+ return ok();
1308
+ }
1309
+
1310
+ if (Array.isArray(input)) {
1311
+ const length = input.length;
1312
+ if (length === 0) return ok(emptyArray);
687
1313
 
688
- const length = array.length;
689
1314
  const values = new Array<unknown>(length);
690
1315
  for (let i = 0; i < length; i++) {
691
- const result = fn(array[i]);
1316
+ const result = fn ? fn(input[i]) : (input[i] as AnyResult);
692
1317
  if (!result.ok) return result;
693
1318
  values[i] = result.value;
694
1319
  }
695
1320
  return ok(values);
696
1321
  }
697
1322
 
698
- const length = Object.keys(input).length;
699
- if (length === 0) return ok(emptyRecord);
700
-
701
- const keys = new Array<string>(length);
702
- const items = new Array<unknown>(length);
703
- let index = 0;
704
- for (const key in input) {
705
- keys[index] = key;
706
- items[index] = (input as Record<string, unknown>)[key];
707
- index++;
1323
+ if (isIterable(input)) {
1324
+ const values: Array<unknown> = [];
1325
+ for (const value of input) {
1326
+ const result = fn ? fn(value) : (value as AnyResult);
1327
+ if (!result.ok) return result;
1328
+ values.push(result.value);
1329
+ }
1330
+ return ok(values.length === 0 ? emptyArray : values);
708
1331
  }
709
1332
 
710
- const record = createRecord();
711
- for (let i = 0; i < length; i++) {
712
- const result = fn(items[i]);
1333
+ const keys = Object.keys(input);
1334
+ if (keys.length === 0) return ok(emptyRecord);
1335
+
1336
+ const record = createMutableRecord();
1337
+ for (const key of keys) {
1338
+ const value = (input as Record<string, unknown>)[key];
1339
+ const result = fn ? fn(value) : (value as AnyResult);
713
1340
  if (!result.ok) return result;
714
- record[keys[i]] = result.value;
1341
+ record[key] = result.value;
715
1342
  }
716
1343
  return ok(record);
717
1344
  }
@@ -722,28 +1349,60 @@ export function mapResult(
722
1349
  * If all results fail, returns the last error.
723
1350
  *
724
1351
  * Requires a non-empty array — there's no "first success" with zero
725
- * participants. Use {@link isNonEmptyArray} to guard:
1352
+ * participants. Use {@link isNonEmptyArray} to guard.
1353
+ *
1354
+ * ### Example
726
1355
  *
727
1356
  * ```ts
1357
+ * import {
1358
+ * anyResult,
1359
+ * err,
1360
+ * isNonEmptyArray,
1361
+ * ok,
1362
+ * type Result,
1363
+ * } from "@evolu/common";
1364
+ *
1365
+ * interface LookupError {
1366
+ * readonly type: "LookupError";
1367
+ * }
1368
+ *
1369
+ * const results: ReadonlyArray<Result<number, LookupError>> = [
1370
+ * err({ type: "LookupError" }),
1371
+ * ok(42),
1372
+ * ];
728
1373
  * if (isNonEmptyArray(results)) {
729
- * const result = anyResult(results);
1374
+ * const number = anyResult(results);
1375
+ * expectTypeOf(number).toEqualTypeOf<Result<number, LookupError>>();
1376
+ * expectOk(number, 42);
730
1377
  * }
731
1378
  * ```
732
1379
  *
733
- * ### Example
1380
+ * A statically non-empty tuple needs no guard:
734
1381
  *
735
1382
  * ```ts
736
- * const results = [err("fail1"), ok(42), err("fail2")];
737
- * if (isNonEmptyArray(results)) {
738
- * const result = anyResult(results);
739
- * // ok(42)
740
- * }
1383
+ * import { anyResult, err, ok, type Result } from "@evolu/common";
741
1384
  *
742
- * const allFailed = [err("a"), err("b"), err("c")];
743
- * if (isNonEmptyArray(allFailed)) {
744
- * const result = anyResult(allFailed);
745
- * // err("c") — last error
1385
+ * interface LookupError {
1386
+ * readonly type: "LookupError";
1387
+ * readonly source: string;
746
1388
  * }
1389
+ *
1390
+ * const lookupError = (source: string): Result<never, LookupError> =>
1391
+ * err({ type: "LookupError", source });
1392
+ *
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>>();
1400
+ * 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" });
747
1406
  * ```
748
1407
  *
749
1408
  * @group Composition