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