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