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