@evolu/common 8.2.0 → 8.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Function.d.ts +1 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Http.d.ts +9 -0
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/Object.d.ts +1 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts +1 -0
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +2 -0
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Resource.d.ts +1 -0
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Result.d.ts +383 -548
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +296 -46
- package/dist/src/StackTrace.d.ts +1 -0
- package/dist/src/StackTrace.d.ts.map +1 -1
- package/dist/src/Task.d.ts +985 -1069
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1007 -156
- package/dist/src/Time.d.ts +1 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Type.d.ts +1322 -793
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1192 -244
- package/dist/src/intl/_en.d.ts +126 -0
- package/dist/src/intl/_en.d.ts.map +1 -0
- package/dist/src/intl/_en.js +267 -0
- package/dist/src/intl/ar.d.ts +117 -0
- package/dist/src/intl/ar.d.ts.map +1 -0
- package/dist/src/intl/ar.js +245 -0
- package/dist/src/intl/bn.d.ts +117 -0
- package/dist/src/intl/bn.d.ts.map +1 -0
- package/dist/src/intl/bn.js +258 -0
- package/dist/src/intl/ca.d.ts +117 -0
- package/dist/src/intl/ca.d.ts.map +1 -0
- package/dist/src/intl/ca.js +258 -0
- package/dist/src/intl/cs.d.ts +20 -7
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +54 -27
- package/dist/src/intl/da.d.ts +117 -0
- package/dist/src/intl/da.d.ts.map +1 -0
- package/dist/src/intl/da.js +258 -0
- package/dist/src/intl/de.d.ts +117 -0
- package/dist/src/intl/de.d.ts.map +1 -0
- package/dist/src/intl/de.js +258 -0
- package/dist/src/intl/el.d.ts +117 -0
- package/dist/src/intl/el.d.ts.map +1 -0
- package/dist/src/intl/el.js +265 -0
- package/dist/src/intl/es.d.ts +117 -0
- package/dist/src/intl/es.d.ts.map +1 -0
- package/dist/src/intl/es.js +258 -0
- package/dist/src/intl/fa.d.ts +117 -0
- package/dist/src/intl/fa.d.ts.map +1 -0
- package/dist/src/intl/fa.js +258 -0
- package/dist/src/intl/fi.d.ts +117 -0
- package/dist/src/intl/fi.d.ts.map +1 -0
- package/dist/src/intl/fi.js +258 -0
- package/dist/src/intl/fil.d.ts +117 -0
- package/dist/src/intl/fil.d.ts.map +1 -0
- package/dist/src/intl/fil.js +258 -0
- package/dist/src/intl/fr.d.ts +117 -0
- package/dist/src/intl/fr.d.ts.map +1 -0
- package/dist/src/intl/fr.js +258 -0
- package/dist/src/intl/he.d.ts +117 -0
- package/dist/src/intl/he.d.ts.map +1 -0
- package/dist/src/intl/he.js +258 -0
- package/dist/src/intl/hi.d.ts +117 -0
- package/dist/src/intl/hi.d.ts.map +1 -0
- package/dist/src/intl/hi.js +258 -0
- package/dist/src/intl/hr.d.ts +117 -0
- package/dist/src/intl/hr.d.ts.map +1 -0
- package/dist/src/intl/hr.js +265 -0
- package/dist/src/intl/hu.d.ts +63 -0
- package/dist/src/intl/hu.d.ts.map +1 -0
- package/dist/src/intl/hu.js +204 -0
- package/dist/src/intl/id.d.ts +117 -0
- package/dist/src/intl/id.d.ts.map +1 -0
- package/dist/src/intl/id.js +258 -0
- package/dist/src/intl/index.d.ts +42 -0
- package/dist/src/intl/index.d.ts.map +1 -1
- package/dist/src/intl/index.js +42 -0
- package/dist/src/intl/it.d.ts +117 -0
- package/dist/src/intl/it.d.ts.map +1 -0
- package/dist/src/intl/it.js +258 -0
- package/dist/src/intl/ja.d.ts +117 -0
- package/dist/src/intl/ja.d.ts.map +1 -0
- package/dist/src/intl/ja.js +258 -0
- package/dist/src/intl/ko.d.ts +117 -0
- package/dist/src/intl/ko.d.ts.map +1 -0
- package/dist/src/intl/ko.js +258 -0
- package/dist/src/intl/ml.d.ts +117 -0
- package/dist/src/intl/ml.d.ts.map +1 -0
- package/dist/src/intl/ml.js +258 -0
- package/dist/src/intl/mr.d.ts +117 -0
- package/dist/src/intl/mr.d.ts.map +1 -0
- package/dist/src/intl/mr.js +258 -0
- package/dist/src/intl/ms.d.ts +117 -0
- package/dist/src/intl/ms.d.ts.map +1 -0
- package/dist/src/intl/ms.js +245 -0
- package/dist/src/intl/nb.d.ts +63 -0
- package/dist/src/intl/nb.d.ts.map +1 -0
- package/dist/src/intl/nb.js +191 -0
- package/dist/src/intl/nl.d.ts +117 -0
- package/dist/src/intl/nl.d.ts.map +1 -0
- package/dist/src/intl/nl.js +258 -0
- package/dist/src/intl/pa.d.ts +117 -0
- package/dist/src/intl/pa.d.ts.map +1 -0
- package/dist/src/intl/pa.js +258 -0
- package/dist/src/intl/pl.d.ts +117 -0
- package/dist/src/intl/pl.d.ts.map +1 -0
- package/dist/src/intl/pl.js +242 -0
- package/dist/src/intl/pt-BR.d.ts +117 -0
- package/dist/src/intl/pt-BR.d.ts.map +1 -0
- package/dist/src/intl/pt-BR.js +258 -0
- package/dist/src/intl/pt.d.ts +117 -0
- package/dist/src/intl/pt.d.ts.map +1 -0
- package/dist/src/intl/pt.js +246 -0
- package/dist/src/intl/ro.d.ts +117 -0
- package/dist/src/intl/ro.d.ts.map +1 -0
- package/dist/src/intl/ro.js +258 -0
- package/dist/src/intl/sk.d.ts +117 -0
- package/dist/src/intl/sk.d.ts.map +1 -0
- package/dist/src/intl/sk.js +244 -0
- package/dist/src/intl/sl.d.ts +117 -0
- package/dist/src/intl/sl.d.ts.map +1 -0
- package/dist/src/intl/sl.js +258 -0
- package/dist/src/intl/sv.d.ts +117 -0
- package/dist/src/intl/sv.d.ts.map +1 -0
- package/dist/src/intl/sv.js +258 -0
- package/dist/src/intl/sw.d.ts +63 -0
- package/dist/src/intl/sw.d.ts.map +1 -0
- package/dist/src/intl/sw.js +191 -0
- package/dist/src/intl/ta.d.ts +117 -0
- package/dist/src/intl/ta.d.ts.map +1 -0
- package/dist/src/intl/ta.js +258 -0
- package/dist/src/intl/te.d.ts +117 -0
- package/dist/src/intl/te.d.ts.map +1 -0
- package/dist/src/intl/te.js +258 -0
- package/dist/src/intl/th.d.ts +117 -0
- package/dist/src/intl/th.d.ts.map +1 -0
- package/dist/src/intl/th.js +258 -0
- package/dist/src/intl/tr.d.ts +117 -0
- package/dist/src/intl/tr.d.ts.map +1 -0
- package/dist/src/intl/tr.js +255 -0
- package/dist/src/intl/uk.d.ts +117 -0
- package/dist/src/intl/uk.d.ts.map +1 -0
- package/dist/src/intl/uk.js +258 -0
- package/dist/src/intl/ur.d.ts +117 -0
- package/dist/src/intl/ur.d.ts.map +1 -0
- package/dist/src/intl/ur.js +245 -0
- package/dist/src/intl/vi.d.ts +117 -0
- package/dist/src/intl/vi.d.ts.map +1 -0
- package/dist/src/intl/vi.js +255 -0
- package/dist/src/intl/zh-CN.d.ts +117 -0
- package/dist/src/intl/zh-CN.d.ts.map +1 -0
- package/dist/src/intl/zh-CN.js +258 -0
- package/dist/src/intl/zh-TW.d.ts +117 -0
- package/dist/src/intl/zh-TW.d.ts.map +1 -0
- package/dist/src/intl/zh-TW.js +258 -0
- package/dist/src/local-first/Owner.d.ts +156 -62
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +119 -20
- package/package.json +1 -1
- package/src/Function.ts +2 -0
- package/src/Http.ts +9 -0
- package/src/Object.ts +2 -0
- package/src/RefCount.ts +2 -0
- package/src/Relation.ts +4 -0
- package/src/Resource.ts +2 -0
- package/src/Result.ts +397 -560
- package/src/StackTrace.ts +2 -0
- package/src/Task.ts +1143 -1222
- package/src/Time.ts +2 -0
- package/src/Type.ts +1759 -794
- package/src/intl/_en.ts +497 -0
- package/src/intl/ar.ts +417 -0
- package/src/intl/bn.ts +486 -0
- package/src/intl/ca.ts +488 -0
- package/src/intl/cs.ts +95 -31
- package/src/intl/da.ts +488 -0
- package/src/intl/de.ts +488 -0
- package/src/intl/el.ts +494 -0
- package/src/intl/es.ts +488 -0
- package/src/intl/fa.ts +487 -0
- package/src/intl/fi.ts +487 -0
- package/src/intl/fil.ts +488 -0
- package/src/intl/fr.ts +488 -0
- package/src/intl/he.ts +486 -0
- package/src/intl/hi.ts +487 -0
- package/src/intl/hr.ts +494 -0
- package/src/intl/hu.ts +387 -0
- package/src/intl/id.ts +487 -0
- package/src/intl/index.ts +42 -0
- package/src/intl/it.ts +488 -0
- package/src/intl/ja.ts +488 -0
- package/src/intl/ko.ts +487 -0
- package/src/intl/ml.ts +487 -0
- package/src/intl/mr.ts +487 -0
- package/src/intl/ms.ts +410 -0
- package/src/intl/nb.ts +364 -0
- package/src/intl/nl.ts +488 -0
- package/src/intl/pa.ts +487 -0
- package/src/intl/pl.ts +450 -0
- package/src/intl/pt-BR.ts +487 -0
- package/src/intl/pt.ts +424 -0
- package/src/intl/ro.ts +488 -0
- package/src/intl/sk.ts +412 -0
- package/src/intl/sl.ts +487 -0
- package/src/intl/sv.ts +488 -0
- package/src/intl/sw.ts +324 -0
- package/src/intl/ta.ts +487 -0
- package/src/intl/te.ts +487 -0
- package/src/intl/th.ts +484 -0
- package/src/intl/tr.ts +487 -0
- package/src/intl/uk.ts +488 -0
- package/src/intl/ur.ts +410 -0
- package/src/intl/vi.ts +435 -0
- package/src/intl/zh-CN.ts +482 -0
- package/src/intl/zh-TW.ts +482 -0
- package/src/local-first/Owner.ts +156 -62
package/dist/src/Task.js
CHANGED
|
@@ -51,8 +51,650 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
/**
|
|
54
|
+
* ## Intro
|
|
55
|
+
*
|
|
54
56
|
* JavaScript-native structured concurrency.
|
|
55
57
|
*
|
|
58
|
+
* Structured concurrency makes ownership of asynchronous work explicit.
|
|
59
|
+
* Operations form a tree where every child belongs to a parent. A parent waits
|
|
60
|
+
* for its children before it completes, and abort follows the tree: aborting a
|
|
61
|
+
* parent requests abort of all its descendants. Races and fail-fast operations
|
|
62
|
+
* also abort their remaining sibling branches.
|
|
63
|
+
*
|
|
64
|
+
* With plain {@link AbortController} code, these guarantees depend on call-site
|
|
65
|
+
* discipline: someone must remember the `finally` that aborts started work and
|
|
66
|
+
* the await that waits for cleanup. {@link Run} makes both structural:
|
|
67
|
+
* `run(task)` registers every child before it starts, and the parent settles
|
|
68
|
+
* only after child cleanup finishes.
|
|
69
|
+
*
|
|
70
|
+
* Evolu models structured concurrency with ordinary JavaScript:
|
|
71
|
+
*
|
|
72
|
+
* - A {@link Task} describes an asynchronous operation and its dependencies.
|
|
73
|
+
* - A {@link Run} starts Tasks and owns their lifetimes.
|
|
74
|
+
* - A {@link Fiber} is the Promise-backed handle returned when a Run starts a
|
|
75
|
+
* Task.
|
|
76
|
+
* - An {@link AbortableFiber} adds explicit abort and async disposal.
|
|
77
|
+
*
|
|
78
|
+
* The runtime core is deliberately small: ordinary functions, a callable Run
|
|
79
|
+
* with closed-over state, Promise-backed Fibers, {@link AbortSignal}
|
|
80
|
+
* propagation, and JavaScript resource management. Together, these primitives
|
|
81
|
+
* provide abort, cleanup, defect handling, dependency injection, monitoring,
|
|
82
|
+
* concurrency, and resource bracketing.
|
|
83
|
+
*
|
|
84
|
+
* Tasks return domain success or failure as {@link Result}. Abort is control
|
|
85
|
+
* flow represented by {@link AbortError}. If a Task throws or rejects with
|
|
86
|
+
* anything else, that is a defect: the root Run reports it and shuts down its
|
|
87
|
+
* tree so code does not continue in a potentially invalid state.
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* import {
|
|
91
|
+
* createRun,
|
|
92
|
+
* err,
|
|
93
|
+
* ok,
|
|
94
|
+
* type Result,
|
|
95
|
+
* type Task,
|
|
96
|
+
* type Typed,
|
|
97
|
+
* } from "@evolu/common";
|
|
98
|
+
*
|
|
99
|
+
* interface User {
|
|
100
|
+
* readonly id: string;
|
|
101
|
+
* readonly name: string;
|
|
102
|
+
* }
|
|
103
|
+
*
|
|
104
|
+
* interface Db {
|
|
105
|
+
* readonly usersById: ReadonlyMap<string, User>;
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* interface DbDep {
|
|
109
|
+
* readonly db: Db;
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* const getUser =
|
|
113
|
+
* (id: string): Task<User, UserNotFoundError, DbDep> =>
|
|
114
|
+
* (run) => {
|
|
115
|
+
* const user = run.deps.db.usersById.get(id);
|
|
116
|
+
* return user ? ok(user) : err({ type: "UserNotFound", id });
|
|
117
|
+
* };
|
|
118
|
+
*
|
|
119
|
+
* // Typed declares the `type` discriminant without repeating the property.
|
|
120
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
121
|
+
* readonly id: string;
|
|
122
|
+
* }
|
|
123
|
+
*
|
|
124
|
+
* const user: User = { id: "user-1", name: "Ada" };
|
|
125
|
+
*
|
|
126
|
+
* // Provide dependencies at the composition root. `await using` disposes the
|
|
127
|
+
* // Run and waits for its child Tasks before leaving this scope.
|
|
128
|
+
* await using run = createRun({
|
|
129
|
+
* db: { usersById: new Map([[user.id, user]]) },
|
|
130
|
+
* });
|
|
131
|
+
*
|
|
132
|
+
* const result = await run(getUser(user.id));
|
|
133
|
+
* expectTypeOf(result).toEqualTypeOf<Result<User, UserNotFoundError>>();
|
|
134
|
+
* expectOk(result, user);
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* In composition roots, prefer the lifecycle API from the matching Evolu
|
|
138
|
+
* platform package:
|
|
139
|
+
*
|
|
140
|
+
* - Node.js: `@evolu/nodejs`
|
|
141
|
+
* - Web: `@evolu/web`
|
|
142
|
+
* - React Native: `@evolu/react-native`
|
|
143
|
+
*
|
|
144
|
+
* ## Composition
|
|
145
|
+
*
|
|
146
|
+
* | Category | Helper | Description |
|
|
147
|
+
* | ------------ | ------------------------- | ----------------------------------------------------- |
|
|
148
|
+
* | Collection | {@link all} | Return {@link Ok} values or stop on first {@link Err} |
|
|
149
|
+
* | | {@link allSettled} | Return every Task Result |
|
|
150
|
+
* | | {@link each} | Handle each Task Result |
|
|
151
|
+
* | Interop | {@link callback} | Wrap callback APIs |
|
|
152
|
+
* | | {@link fetch} | Native fetch with bounded Response use |
|
|
153
|
+
* | Timing | {@link sleep} | Pause execution |
|
|
154
|
+
* | | {@link timeout} | Time-bounded execution |
|
|
155
|
+
* | Resilience | {@link retry} | Retry domain errors with a schedule |
|
|
156
|
+
* | Repetition | {@link repeat} | Repeat successes with a schedule |
|
|
157
|
+
* | Racing | {@link any} | First Ok wins |
|
|
158
|
+
* | | {@link race} | First settled Result wins |
|
|
159
|
+
* | | {@link firstN} | First n Ok values win |
|
|
160
|
+
* | | {@link firstNSettled} | First n Results win |
|
|
161
|
+
* | Scheduling | {@link prioritized} | Assign scheduler priority |
|
|
162
|
+
* | | {@link yieldNow} | Yield to the host scheduler |
|
|
163
|
+
* | Lifetime | {@link daemon} | Run under root ownership |
|
|
164
|
+
* | | {@link acquireUseRelease} | Bracket acquire, use, and release |
|
|
165
|
+
* | Abortability | {@link unabortable} | Mask abort after a Task starts |
|
|
166
|
+
* | | {@link unabortableMask} | Mask abort and selectively restore it |
|
|
167
|
+
*
|
|
168
|
+
* Helpers that process multiple Tasks run sequentially by default. Use a
|
|
169
|
+
* `concurrency` option to run more than one Task at a time.
|
|
170
|
+
*
|
|
171
|
+
* ### Sequential composition
|
|
172
|
+
*
|
|
173
|
+
* For ordinary sequential composition, use imperative code:
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* import {
|
|
177
|
+
* createRun,
|
|
178
|
+
* err,
|
|
179
|
+
* ok,
|
|
180
|
+
* type Result,
|
|
181
|
+
* type Task,
|
|
182
|
+
* type Typed,
|
|
183
|
+
* } from "@evolu/common";
|
|
184
|
+
*
|
|
185
|
+
* interface User {
|
|
186
|
+
* readonly id: string;
|
|
187
|
+
* readonly profileId: string;
|
|
188
|
+
* }
|
|
189
|
+
*
|
|
190
|
+
* interface Profile {
|
|
191
|
+
* readonly id: string;
|
|
192
|
+
* }
|
|
193
|
+
*
|
|
194
|
+
* const getUser =
|
|
195
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
196
|
+
* () =>
|
|
197
|
+
* id === "user-1"
|
|
198
|
+
* ? ok({ id, profileId: "profile-1" })
|
|
199
|
+
* : err({ type: "UserNotFound", id });
|
|
200
|
+
*
|
|
201
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
202
|
+
* readonly id: string;
|
|
203
|
+
* }
|
|
204
|
+
*
|
|
205
|
+
* const getProfile =
|
|
206
|
+
* (id: string): Task<Profile, ProfileNotFoundError> =>
|
|
207
|
+
* () =>
|
|
208
|
+
* id === "profile-1"
|
|
209
|
+
* ? ok({ id })
|
|
210
|
+
* : err({ type: "ProfileNotFound", id });
|
|
211
|
+
*
|
|
212
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {
|
|
213
|
+
* readonly id: string;
|
|
214
|
+
* }
|
|
215
|
+
*
|
|
216
|
+
* const getUserWithProfile =
|
|
217
|
+
* (
|
|
218
|
+
* id: string,
|
|
219
|
+
* ): Task<
|
|
220
|
+
* { readonly user: User; readonly profile: Profile },
|
|
221
|
+
* UserNotFoundError | ProfileNotFoundError
|
|
222
|
+
* > =>
|
|
223
|
+
* async (run) => {
|
|
224
|
+
* const user = await run(getUser(id));
|
|
225
|
+
* if (!user.ok) return user;
|
|
226
|
+
*
|
|
227
|
+
* const profile = await run(getProfile(user.value.profileId));
|
|
228
|
+
* if (!profile.ok) return profile;
|
|
229
|
+
*
|
|
230
|
+
* return ok({ user: user.value, profile: profile.value });
|
|
231
|
+
* };
|
|
232
|
+
*
|
|
233
|
+
* await using run = createRun();
|
|
234
|
+
* const result = await run(getUserWithProfile("user-1"));
|
|
235
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
236
|
+
* Result<
|
|
237
|
+
* { readonly user: User; readonly profile: Profile },
|
|
238
|
+
* UserNotFoundError | ProfileNotFoundError
|
|
239
|
+
* >
|
|
240
|
+
* >();
|
|
241
|
+
* expectOk(result, {
|
|
242
|
+
* user: { id: "user-1", profileId: "profile-1" },
|
|
243
|
+
* profile: { id: "profile-1" },
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* Evolu intentionally avoids pipe APIs, chainable methods, and generator-based
|
|
248
|
+
* effect DSLs. Plain async/await with early returns is easier to read, review,
|
|
249
|
+
* and debug, and it lets TypeScript narrow Result values through ordinary
|
|
250
|
+
* control flow.
|
|
251
|
+
*
|
|
252
|
+
* ### Resilient fetch
|
|
253
|
+
*
|
|
254
|
+
* {@link fetch} with a body mode already returns a plain value, so resilience is
|
|
255
|
+
* ordinary Task composition. Combine {@link timeout} and {@link retry} to bound
|
|
256
|
+
* each attempt and retry recoverable domain failures:
|
|
257
|
+
*
|
|
258
|
+
* ```ts
|
|
259
|
+
* import {
|
|
260
|
+
* exponential,
|
|
261
|
+
* fetch,
|
|
262
|
+
* jitter,
|
|
263
|
+
* maxDelay,
|
|
264
|
+
* retry,
|
|
265
|
+
* take,
|
|
266
|
+
* timeout,
|
|
267
|
+
* type FetchError,
|
|
268
|
+
* type RetryTaskError,
|
|
269
|
+
* type Task,
|
|
270
|
+
* type TimeoutError,
|
|
271
|
+
* } from "@evolu/common";
|
|
272
|
+
*
|
|
273
|
+
* const fetchWithRetry = (url: string) =>
|
|
274
|
+
* retry(
|
|
275
|
+
* timeout(fetch(url, "text"), "30s"),
|
|
276
|
+
* // A jittered, capped, limited exponential backoff.
|
|
277
|
+
* jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
278
|
+
* );
|
|
279
|
+
*
|
|
280
|
+
* expectTypeOf(fetchWithRetry).returns.toEqualTypeOf<
|
|
281
|
+
* Task<string, RetryTaskError<FetchError | TimeoutError>>
|
|
282
|
+
* >();
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* ### Concurrent composition
|
|
286
|
+
*
|
|
287
|
+
* Run composed Tasks with a `concurrency` option and {@link all}:
|
|
288
|
+
*
|
|
289
|
+
* ```ts
|
|
290
|
+
* import { all, createRun, ok, sleep, type Task } from "@evolu/common";
|
|
291
|
+
*
|
|
292
|
+
* await using run = createRun();
|
|
293
|
+
*
|
|
294
|
+
* const urls = [
|
|
295
|
+
* "https://api.example.com/users",
|
|
296
|
+
* "https://api.example.com/posts",
|
|
297
|
+
* "https://api.example.com/comments",
|
|
298
|
+
* ];
|
|
299
|
+
* let activeRequests = 0;
|
|
300
|
+
* let maxActiveRequests = 0;
|
|
301
|
+
* const fetchUrl =
|
|
302
|
+
* (url: string): Task<string> =>
|
|
303
|
+
* async (run) => {
|
|
304
|
+
* activeRequests += 1;
|
|
305
|
+
* maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
|
|
306
|
+
* await run.ok(sleep("1ms"));
|
|
307
|
+
* activeRequests -= 1;
|
|
308
|
+
* return ok(url);
|
|
309
|
+
* };
|
|
310
|
+
*
|
|
311
|
+
* // At most 2 concurrent requests.
|
|
312
|
+
* const result = await run(all(urls, fetchUrl, { concurrency: 2 }));
|
|
313
|
+
* expectOk(result, urls);
|
|
314
|
+
* expect(maxActiveRequests).toBe(2);
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* Task helpers compose Tasks; concurrency primitives are stateful objects that
|
|
318
|
+
* coordinate Tasks across call sites. Create them with their `createX`
|
|
319
|
+
* factories and share them where coordination is needed.
|
|
320
|
+
*
|
|
321
|
+
* | Primitive | Description |
|
|
322
|
+
* | ---------------------- | -------------------------------------------- |
|
|
323
|
+
* | {@link Deferred} | One-shot value resolved from outside |
|
|
324
|
+
* | {@link Gate} | Block and release Tasks repeatedly |
|
|
325
|
+
* | {@link Semaphore} | Limit concurrent Tasks with permits |
|
|
326
|
+
* | {@link Mutex} | Run Tasks one at a time |
|
|
327
|
+
* | {@link SemaphoreByKey} | Per-key permits with automatic cleanup |
|
|
328
|
+
* | {@link MutexByKey} | Per-key one-at-a-time execution |
|
|
329
|
+
* | {@link MutexRef} | {@link Ref} with serialized Task transitions |
|
|
330
|
+
*
|
|
331
|
+
* ## Dependency injection
|
|
332
|
+
*
|
|
333
|
+
* Task DI is
|
|
334
|
+
* {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}
|
|
335
|
+
* applied to {@link Run}. A {@link Task} declares required capabilities with its
|
|
336
|
+
* `D` type parameter and reads them from {@link Run.deps}.
|
|
337
|
+
*
|
|
338
|
+
* {@link createRun} supplies dependencies to the root Run and its children. A
|
|
339
|
+
* Run can also start one child Task with runtime-created dependencies by
|
|
340
|
+
* calling `run(task, deps)`, where `deps` is checked as {@link RunCustomDeps}.
|
|
341
|
+
*
|
|
342
|
+
* Use normal Task arguments for per-call values and `D` for capabilities,
|
|
343
|
+
* resources, or services shared by all code running inside a Run.
|
|
344
|
+
*
|
|
345
|
+
* ```ts
|
|
346
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
347
|
+
*
|
|
348
|
+
* interface GreetingFormatter {
|
|
349
|
+
* readonly format: (name: string) => string;
|
|
350
|
+
* }
|
|
351
|
+
*
|
|
352
|
+
* interface GreetingFormatterDep {
|
|
353
|
+
* readonly greetingFormatter: GreetingFormatter;
|
|
354
|
+
* }
|
|
355
|
+
*
|
|
356
|
+
* const greet =
|
|
357
|
+
* (name: string): Task<string, never, GreetingFormatterDep> =>
|
|
358
|
+
* (run) =>
|
|
359
|
+
* ok(run.deps.greetingFormatter.format(name));
|
|
360
|
+
*
|
|
361
|
+
* const formal: GreetingFormatter = {
|
|
362
|
+
* format: (name) => `Hello, ${name}`,
|
|
363
|
+
* };
|
|
364
|
+
* const casual: GreetingFormatter = {
|
|
365
|
+
* format: (name) => `Hi, ${name}`,
|
|
366
|
+
* };
|
|
367
|
+
*
|
|
368
|
+
* await using run = createRun({ greetingFormatter: formal });
|
|
369
|
+
*
|
|
370
|
+
* // Root dependencies are inherited.
|
|
371
|
+
* expectOk(await run(greet("Ada")), "Hello, Ada");
|
|
372
|
+
*
|
|
373
|
+
* // Child-specific dependencies replace the root's custom dependencies.
|
|
374
|
+
* expectOk(
|
|
375
|
+
* await run(greet("Ada"), { greetingFormatter: casual }),
|
|
376
|
+
* "Hi, Ada",
|
|
377
|
+
* );
|
|
378
|
+
* ```
|
|
379
|
+
*
|
|
380
|
+
* ### Default dependencies
|
|
381
|
+
*
|
|
382
|
+
* {@link createRun} provides default {@link RunDefaultDeps} available to all
|
|
383
|
+
* Tasks without declaring `D`:
|
|
384
|
+
*
|
|
385
|
+
* - {@link Console} — logging with hierarchical context via `child()`
|
|
386
|
+
* - {@link LeakDetector} — development-time leaked-handle detection
|
|
387
|
+
* - {@link NativeFetch} — WHATWG-compatible native fetch
|
|
388
|
+
* - {@link Random} — random number generation
|
|
389
|
+
* - {@link RandomBytes} — cryptographic random bytes
|
|
390
|
+
* - {@link ReportDefect} — defect reporting
|
|
391
|
+
* - {@link Time} — current time
|
|
392
|
+
*
|
|
393
|
+
* For testing, use {@link testCreateRun} to get deterministic, controllable
|
|
394
|
+
* implementations of all RunDefaultDeps.
|
|
395
|
+
*
|
|
396
|
+
* ## Resource management
|
|
397
|
+
*
|
|
398
|
+
* JavaScript provides standard
|
|
399
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
|
|
400
|
+
* Evolu adds {@link DisposableRun.defer} for closure-held state owned by a
|
|
401
|
+
* reusable Run.
|
|
402
|
+
*
|
|
403
|
+
* Choose the ownership primitive by where the resource is reachable:
|
|
404
|
+
*
|
|
405
|
+
* - Synchronous stack frame: `using` or {@link DisposableStack}
|
|
406
|
+
* - Async Task stack frame: `await using` or {@link AsyncDisposableStack}
|
|
407
|
+
* - Closure-held state bounded by a reusable {@link DisposableRun}:
|
|
408
|
+
* {@link DisposableRun.defer}
|
|
409
|
+
*
|
|
410
|
+
* ### Returning resources from Tasks
|
|
411
|
+
*
|
|
412
|
+
* A Task that successfully returns a disposable resource transfers ownership of
|
|
413
|
+
* a live resource to its caller. The resource must remain live after the Task
|
|
414
|
+
* settles. Do not register its disposal with the creating Task's
|
|
415
|
+
* {@link DisposableRun.defer}, because that child Run is disposed when the Task
|
|
416
|
+
* settles.
|
|
417
|
+
*
|
|
418
|
+
* Use {@link AsyncDisposableStack} while creating a resource. On a Result error,
|
|
419
|
+
* abort, or defect, stack unwinding disposes partially created resources. On
|
|
420
|
+
* success, {@link AsyncDisposableStack.move} transfers ownership to the returned
|
|
421
|
+
* resource. A recoverable creation failure should be a typed Result error;
|
|
422
|
+
* `undefined` should represent valid absence, not failure.
|
|
423
|
+
*
|
|
424
|
+
* ```ts
|
|
425
|
+
* import { createRun, ok, type Task, type Typed } from "@evolu/common";
|
|
426
|
+
*
|
|
427
|
+
* interface Socket extends AsyncDisposable {
|
|
428
|
+
* readonly send: (message: string) => string;
|
|
429
|
+
* }
|
|
430
|
+
*
|
|
431
|
+
* interface Connection extends AsyncDisposable {
|
|
432
|
+
* readonly send: (message: string) => string;
|
|
433
|
+
* }
|
|
434
|
+
*
|
|
435
|
+
* let socketDisposed = false;
|
|
436
|
+
* const openSocket: Task<Socket, ConnectionFailedError> = () =>
|
|
437
|
+
* ok({
|
|
438
|
+
* send: (message) => message,
|
|
439
|
+
* [Symbol.asyncDispose]: async () => {
|
|
440
|
+
* socketDisposed = true;
|
|
441
|
+
* },
|
|
442
|
+
* });
|
|
443
|
+
*
|
|
444
|
+
* interface ConnectionFailedError extends Typed<"ConnectionFailed"> {}
|
|
445
|
+
*
|
|
446
|
+
* const handshake =
|
|
447
|
+
* (_socket: Socket): Task<void, ConnectionFailedError> =>
|
|
448
|
+
* () =>
|
|
449
|
+
* ok();
|
|
450
|
+
*
|
|
451
|
+
* const createConnection: Task<Connection, ConnectionFailedError> = async (
|
|
452
|
+
* run,
|
|
453
|
+
* ) => {
|
|
454
|
+
* await using disposer = new AsyncDisposableStack();
|
|
455
|
+
*
|
|
456
|
+
* const socketResult = await run(openSocket);
|
|
457
|
+
* if (!socketResult.ok) return socketResult;
|
|
458
|
+
* const socket = disposer.use(socketResult.value);
|
|
459
|
+
*
|
|
460
|
+
* const handshakeResult = await run(handshake(socket));
|
|
461
|
+
* if (!handshakeResult.ok) return handshakeResult;
|
|
462
|
+
*
|
|
463
|
+
* const disposables = disposer.move();
|
|
464
|
+
* return ok({
|
|
465
|
+
* send: (message) => socket.send(message),
|
|
466
|
+
* [Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
467
|
+
* });
|
|
468
|
+
* };
|
|
469
|
+
*
|
|
470
|
+
* await using run = createRun();
|
|
471
|
+
* const result = await run(createConnection);
|
|
472
|
+
* assert(result.ok);
|
|
473
|
+
* expect(socketDisposed).toBe(false);
|
|
474
|
+
* expect(result.value.send("hello")).toBe("hello");
|
|
475
|
+
* await result.value[Symbol.asyncDispose]();
|
|
476
|
+
* expect(socketDisposed).toBe(true);
|
|
477
|
+
* ```
|
|
478
|
+
*
|
|
479
|
+
* Use {@link Run.ok} with `await using` when an infallible Task returns a
|
|
480
|
+
* disposable value. Use {@link acquireUseRelease} when acquisition and release
|
|
481
|
+
* are separate operations rather than a disposable value.
|
|
482
|
+
*
|
|
483
|
+
* ## Awaitable
|
|
484
|
+
*
|
|
485
|
+
* A {@link Task} returns {@link Awaitable}, so its body may produce a
|
|
486
|
+
* {@link Result} immediately or asynchronously. {@link Run} is always async and
|
|
487
|
+
* returns a {@link Fiber}; callers use the same ownership model either way.
|
|
488
|
+
*
|
|
489
|
+
* - **Sync** → {@link Result}, native `using` / `DisposableStack`
|
|
490
|
+
* - **Async** → Task, Run, {@link Fiber}, `await using` / `AsyncDisposableStack`
|
|
491
|
+
*
|
|
492
|
+
* A Task is an async ownership boundary, not a general unit of program
|
|
493
|
+
* decomposition. Calling `run(task)` always creates a child Run by design. Use
|
|
494
|
+
* ordinary promises when an async operation does not need its own Run.
|
|
495
|
+
*
|
|
496
|
+
* A unified sync/async effect API is technically possible. It can detect
|
|
497
|
+
* Promise-like values with {@link isPromiseLike}, dispose synchronous resources
|
|
498
|
+
* first, continue with asynchronous disposal when necessary, and track whether
|
|
499
|
+
* callers must await the result. Evolu deliberately keeps the two models
|
|
500
|
+
* separate instead: plain functions and Result for synchronous code, Task and
|
|
501
|
+
* Run for asynchronous ownership. Most effects involve inherently asynchronous
|
|
502
|
+
* I/O, while synchronous code benefits from a simpler API and no Task
|
|
503
|
+
* overhead.
|
|
504
|
+
*
|
|
505
|
+
* Keep synchronous computation as plain functions returning Result. Prefer
|
|
506
|
+
* passing values rather than dependencies, following the
|
|
507
|
+
* {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
|
|
508
|
+
* pattern where impure code gathers data, pure functions process it, and impure
|
|
509
|
+
* code performs effects with the result. For example, a pure function can
|
|
510
|
+
* accept a {@link RandomNumber} value instead of depending on {@link Random}.
|
|
511
|
+
*
|
|
512
|
+
* Large CPU-bound operations, such as parsing large JSON, sorting millions of
|
|
513
|
+
* items, or complex cryptography, belong in a worker. Model the asynchronous
|
|
514
|
+
* call to that worker as a Task so Run can provide timeout, abort, cleanup, and
|
|
515
|
+
* monitoring.
|
|
516
|
+
*
|
|
517
|
+
* ## Glossary
|
|
518
|
+
*
|
|
519
|
+
* - **Defect** — a thrown or rejected value other than {@link AbortError}, rather
|
|
520
|
+
* than a declared {@link Result} error.
|
|
521
|
+
* - **Outcome** — a Fiber's settlement: resolution with the Task {@link Result},
|
|
522
|
+
* or rejection with {@link AbortError}. The original defect is reported
|
|
523
|
+
* through {@link ReportDefectDep} whether or not the Fiber is observed; the
|
|
524
|
+
* Fiber boundary represents the panic with AbortError whose reason is
|
|
525
|
+
* {@link PanicAbortReason}.
|
|
526
|
+
* - **Create** — construct a new value or a resource.
|
|
527
|
+
* - **Acquire** — obtain a usable resource. Acquisition may create a new
|
|
528
|
+
* resource, borrow one, open one, or take a lease/lock.
|
|
529
|
+
* - **Release** — relinquish a previously acquired resource or lease. Release
|
|
530
|
+
* pairs with acquire and need not mean disposal; examples include unlock,
|
|
531
|
+
* logout, or returning a pooled resource.
|
|
532
|
+
* - **Dispose / disposal** — owner-driven resource finalization via JavaScript
|
|
533
|
+
* resource management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
|
|
534
|
+
* `AsyncDisposableStack`).
|
|
535
|
+
*
|
|
536
|
+
* ## FAQ
|
|
537
|
+
*
|
|
538
|
+
* ### Why is AbortError not part of every Task error type?
|
|
539
|
+
*
|
|
540
|
+
* The `E` type parameter represents declared domain errors. Abort is
|
|
541
|
+
* structured-concurrency control flow, not a domain error. A direct `run(task)`
|
|
542
|
+
* rejects with {@link AbortError} when the Task observes abort. Use
|
|
543
|
+
* `run.abortable(task)` when abort should be handled as an ordinary
|
|
544
|
+
* {@link Result} error at the Fiber boundary, or `daemon(task)` when waiting for
|
|
545
|
+
* a Task should stop immediately after abort.
|
|
546
|
+
*
|
|
547
|
+
* ### Do I have to await every Fiber?
|
|
548
|
+
*
|
|
549
|
+
* No. Awaiting a Fiber is join: it makes the child outcome part of the current
|
|
550
|
+
* control flow. When the outcome does not matter — a fire-and-forget side
|
|
551
|
+
* effect — discard the Fiber explicitly with `void run(task)`.
|
|
552
|
+
*
|
|
553
|
+
* That is safe because the Run tree supervises every Fiber it creates. A
|
|
554
|
+
* discarded Fiber whose Task observes abort (for example during Run disposal)
|
|
555
|
+
* never surfaces as an unhandled rejection, and cleanup is not lost — disposal
|
|
556
|
+
* already aborts and awaits the child. Defects are different: they still panic
|
|
557
|
+
* the root Run and are reported through {@link ReportDefectDep}, so discarding a
|
|
558
|
+
* Fiber never hides bugs.
|
|
559
|
+
*
|
|
560
|
+
* Choose the boundary explicitly:
|
|
561
|
+
*
|
|
562
|
+
* - `void run(task)` — the outcome does not matter. Abort is silent; defects are
|
|
563
|
+
* still reported.
|
|
564
|
+
* - `await run(task)` — the continuation depends on the Result, so abort rejects
|
|
565
|
+
* into the awaiter and the boundary must handle it.
|
|
566
|
+
* - `run.abortable(task)` — abort is an expected outcome handled as a
|
|
567
|
+
* {@link Result} error.
|
|
568
|
+
*
|
|
569
|
+
* ### What should Task code do with defects?
|
|
570
|
+
*
|
|
571
|
+
* Nothing. Once a defect reaches the {@link Run}, it is too late: the root Run
|
|
572
|
+
* panics, running Tasks are aborted, and the Run tree shuts down. If an
|
|
573
|
+
* operation can throw or reject for a recoverable reason, wrap that operation
|
|
574
|
+
* with {@link trySync} or {@link tryAsync} so the failure becomes a typed
|
|
575
|
+
* {@link Result} error. Let unrecoverable failures propagate as defects.
|
|
576
|
+
*
|
|
577
|
+
* ### Why does a defect panic the whole Run tree?
|
|
578
|
+
*
|
|
579
|
+
* The obvious alternative is partial recovery: only the failing subtree shuts
|
|
580
|
+
* down or restarts while the rest keeps running. Erlang/OTP made this "let it
|
|
581
|
+
* crash" model with supervisors the benchmark for fault-tolerant runtime
|
|
582
|
+
* design.
|
|
583
|
+
*
|
|
584
|
+
* Erlang can recover partially because of process isolation: each process owns
|
|
585
|
+
* its heap, so a crashed process cannot leave another process's state
|
|
586
|
+
* corrupted. JavaScript Tasks share a heap. A defect may throw after partially
|
|
587
|
+
* updating shared state, and the Run cannot prove which invariants are still
|
|
588
|
+
* valid. A subtree panic would stop the failing Task while leaving any
|
|
589
|
+
* corrupted shared state available to surviving Tasks. Locks make it worse: a
|
|
590
|
+
* defect inside a critical section may leave protected invariants half-updated.
|
|
591
|
+
* In-process restart is not a reliable recovery boundary either, because the
|
|
592
|
+
* restarted code may still share the same module state, closures, caches, or
|
|
593
|
+
* resources.
|
|
594
|
+
*
|
|
595
|
+
* JavaScript does have a boundary with Erlang-like isolation: workers. A worker
|
|
596
|
+
* has its own heap and structured-clone messaging, so corruption cannot cross
|
|
597
|
+
* the boundary, and respawning a worker starts from clean state. A defect can
|
|
598
|
+
* panic the worker's Run tree, the worker boundary can be torn down, and the
|
|
599
|
+
* supervising side decides whether to respawn — {@link retry} with a
|
|
600
|
+
* {@link Schedule} around a "spawn worker, run until exit" Task is a one-for-one
|
|
601
|
+
* supervisor. Multiple root Runs that share no mutable state are a lighter
|
|
602
|
+
* alternative, but the share-nothing guarantee is then architectural discipline
|
|
603
|
+
* rather than enforced isolation, so keep it opt-in and rare.
|
|
604
|
+
*
|
|
605
|
+
* ### Why imperative code instead of monadic effect composition?
|
|
606
|
+
*
|
|
607
|
+
* Monads give pure functional languages a way to sequence effects while keeping
|
|
608
|
+
* functions pure. JavaScript already has native effect sequencing: loops, early
|
|
609
|
+
* returns, `try`/`finally`, exceptions, and `async`/`await`.
|
|
610
|
+
*
|
|
611
|
+
* A monadic effect wrapper moves that control flow into a library DSL. The
|
|
612
|
+
* wrapper type becomes viral, and ordinary debugging, profiling, stack traces,
|
|
613
|
+
* and TypeScript narrowing have to work through the DSL instead of the
|
|
614
|
+
* language.
|
|
615
|
+
*
|
|
616
|
+
* Task follows the opposite approach: Tasks are ordinary async functions, Run
|
|
617
|
+
* owns lifetimes and scoped context, {@link Result} carries expected domain
|
|
618
|
+
* errors, and defects keep real exceptions with real stacks. Result propagation
|
|
619
|
+
* is explicit at each async boundary, so TypeScript narrows it through ordinary
|
|
620
|
+
* control flow and readers can see where an error is handled or returned.
|
|
621
|
+
*
|
|
622
|
+
* ### Are recursive Tasks stack-safe?
|
|
623
|
+
*
|
|
624
|
+
* Tasks have native JavaScript stack behavior. A deeply recursive Task can
|
|
625
|
+
* exceed the call stack when each step starts the next step synchronously.
|
|
626
|
+
* `await` alone does not prevent this: JavaScript evaluates its operand before
|
|
627
|
+
* suspending, and `run(nextTask)` starts the child Task immediately.
|
|
628
|
+
*
|
|
629
|
+
* Implement deep recursive algorithms with a loop and an explicit worklist so
|
|
630
|
+
* each iteration reuses the same stack frame:
|
|
631
|
+
*
|
|
632
|
+
* ```ts
|
|
633
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
634
|
+
*
|
|
635
|
+
* interface TreeNode {
|
|
636
|
+
* readonly value: string;
|
|
637
|
+
* readonly children: ReadonlyArray<TreeNode>;
|
|
638
|
+
* }
|
|
639
|
+
*
|
|
640
|
+
* const visitTree =
|
|
641
|
+
* (root: TreeNode): Task<ReadonlyArray<string>> =>
|
|
642
|
+
* () => {
|
|
643
|
+
* const remaining = [root];
|
|
644
|
+
* const visited: Array<string> = [];
|
|
645
|
+
*
|
|
646
|
+
* while (remaining.length > 0) {
|
|
647
|
+
* const node = remaining.pop();
|
|
648
|
+
* if (!node) continue;
|
|
649
|
+
* visited.push(node.value);
|
|
650
|
+
* for (const child of node.children) remaining.push(child);
|
|
651
|
+
* }
|
|
652
|
+
*
|
|
653
|
+
* return ok(visited);
|
|
654
|
+
* };
|
|
655
|
+
*
|
|
656
|
+
* await using run = createRun();
|
|
657
|
+
* expectOk(
|
|
658
|
+
* await run(
|
|
659
|
+
* visitTree({
|
|
660
|
+
* value: "root",
|
|
661
|
+
* children: [{ value: "child", children: [] }],
|
|
662
|
+
* }),
|
|
663
|
+
* ),
|
|
664
|
+
* ["root", "child"],
|
|
665
|
+
* );
|
|
666
|
+
* ```
|
|
667
|
+
*
|
|
668
|
+
* Task favors direct native execution, `async`/`await`, and native tooling over
|
|
669
|
+
* interpreted control flow. The trade-off is no transparent stack safety or
|
|
670
|
+
* automatic scheduling fairness. Use loops or worklists for deep algorithms,
|
|
671
|
+
* periodically await {@link yieldNow} for cooperative scheduling, and move
|
|
672
|
+
* CPU-bound work to a worker.
|
|
673
|
+
*
|
|
674
|
+
* ### Should a Task be called directly?
|
|
675
|
+
*
|
|
676
|
+
* Only inside Task internals that explicitly require same-Run execution. A
|
|
677
|
+
* direct call, `task(run)`, uses the current Run instead of creating a child
|
|
678
|
+
* Run, so it bypasses child lifetime tracking, scheduling metadata, and child
|
|
679
|
+
* disposal boundaries. Application code should use `run(task)`.
|
|
680
|
+
*
|
|
681
|
+
* ### Where are fork and join?
|
|
682
|
+
*
|
|
683
|
+
* Calling `run(task)` is fork: it starts a child Task and returns a
|
|
684
|
+
* {@link Fiber}. Awaiting or returning that Fiber is join: it makes the child
|
|
685
|
+
* Result or rejection part of the parent Task control flow.
|
|
686
|
+
*
|
|
687
|
+
* ### What runtime features does Task require?
|
|
688
|
+
*
|
|
689
|
+
* Task uses modern JavaScript APIs such as `Promise.withResolvers`,
|
|
690
|
+
* `AbortSignal.throwIfAborted`, `Symbol.dispose`, `Symbol.asyncDispose`,
|
|
691
|
+
* `DisposableStack`, and `AsyncDisposableStack`. Evolu provides polyfills for
|
|
692
|
+
* supported runtimes that need them: call `installPolyfills` from
|
|
693
|
+
* `@evolu/common/polyfills`, or from the platform package such as
|
|
694
|
+
* `@evolu/react-native/polyfills`. The `using` and `await using` syntax is
|
|
695
|
+
* emitted by TypeScript; the polyfills provide the runtime resource-management
|
|
696
|
+
* globals.
|
|
697
|
+
*
|
|
56
698
|
* @module
|
|
57
699
|
*/
|
|
58
700
|
import { emptyArray, isNonEmptyArray, mapArray, } from "./Array.js";
|
|
@@ -74,7 +716,7 @@ import { emptySet } from "./Set.js";
|
|
|
74
716
|
import { createTime, PositiveMillis, testCreateTime, } from "./Time.js";
|
|
75
717
|
import { assertType, createId, maxPositiveInt, NonNegativeInt, object, onePositiveInt, PositiveInt, record, String, typed, Unknown, UnknownResult, } from "./Type.js";
|
|
76
718
|
/**
|
|
77
|
-
*
|
|
719
|
+
* Runtime Type for structured data explaining why a {@link Run} was aborted.
|
|
78
720
|
*
|
|
79
721
|
* A reason has a `type` discriminant and optional structured data, so abort
|
|
80
722
|
* causes can carry typed domain data. Well-known reasons are
|
|
@@ -86,11 +728,11 @@ import { assertType, createId, maxPositiveInt, NonNegativeInt, object, onePositi
|
|
|
86
728
|
export const AbortReason = /*#__PURE__*/ object({ type: String },
|
|
87
729
|
/*#__PURE__*/ record(String, Unknown));
|
|
88
730
|
/**
|
|
89
|
-
*
|
|
731
|
+
* Runtime Type for structured-concurrency abort control flow.
|
|
90
732
|
*
|
|
91
|
-
* AbortError is thrown to stop Task execution when a Run observes
|
|
92
|
-
* request. AbortableFiber catches AbortError and returns it as
|
|
93
|
-
* error, so abort can be handled as an ordinary Task outcome.
|
|
733
|
+
* AbortError is thrown to stop {@link Task} execution when a {@link Run} observes
|
|
734
|
+
* an abort request. {@link AbortableFiber} catches AbortError and returns it as
|
|
735
|
+
* a {@link Result} error, so abort can be handled as an ordinary Task outcome.
|
|
94
736
|
*
|
|
95
737
|
* The reason explains why the Run was aborted. It can be an explicit abort
|
|
96
738
|
* reason, {@link runDisposedAbortReason} for normal Run cleanup, or
|
|
@@ -124,9 +766,9 @@ export const createAbortError = (reason) => ({
|
|
|
124
766
|
/**
|
|
125
767
|
* Shared abort reason used for ordinary {@link Run} cleanup.
|
|
126
768
|
*
|
|
127
|
-
* Disposal requests abort so child
|
|
128
|
-
* settle. This reason distinguishes that cleanup path from explicit
|
|
129
|
-
* {@link PanicAbortReason}.
|
|
769
|
+
* Disposal requests abort so child {@link Task}s stop while the Run waits for
|
|
770
|
+
* them to settle. This reason distinguishes that cleanup path from explicit
|
|
771
|
+
* abort and {@link PanicAbortReason}.
|
|
130
772
|
*
|
|
131
773
|
* @group Run
|
|
132
774
|
*/
|
|
@@ -146,7 +788,7 @@ export const explicitAbortReason = {
|
|
|
146
788
|
/**
|
|
147
789
|
* Shared abort reason for tests that need a non-production abort reason.
|
|
148
790
|
*
|
|
149
|
-
* @group
|
|
791
|
+
* @group Testing
|
|
150
792
|
*/
|
|
151
793
|
export const testAbortReason = {
|
|
152
794
|
type: "TestAbortReason",
|
|
@@ -154,7 +796,7 @@ export const testAbortReason = {
|
|
|
154
796
|
/**
|
|
155
797
|
* Shared {@link AbortError} for tests, created from {@link testAbortReason}.
|
|
156
798
|
*
|
|
157
|
-
* @group
|
|
799
|
+
* @group Testing
|
|
158
800
|
*/
|
|
159
801
|
export const testAbortError = /*#__PURE__*/ createAbortError(testAbortReason);
|
|
160
802
|
/**
|
|
@@ -203,6 +845,28 @@ export const createRunDefaultDeps = () => {
|
|
|
203
845
|
/**
|
|
204
846
|
* Creates a root {@link DisposableRun}.
|
|
205
847
|
*
|
|
848
|
+
* Use at composition roots such as app, server, worker, or test entry points.
|
|
849
|
+
* The common factory is platform-agnostic; platform adapters can wrap it to add
|
|
850
|
+
* global error handling or shutdown integration.
|
|
851
|
+
*
|
|
852
|
+
* ### Example
|
|
853
|
+
*
|
|
854
|
+
* ```ts
|
|
855
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
856
|
+
*
|
|
857
|
+
* interface ConfigDep {
|
|
858
|
+
* readonly config: { readonly apiUrl: string };
|
|
859
|
+
* }
|
|
860
|
+
*
|
|
861
|
+
* const loadApiUrl: Task<string, never, ConfigDep> = (run) =>
|
|
862
|
+
* ok(run.deps.config.apiUrl);
|
|
863
|
+
*
|
|
864
|
+
* await using run = createRun({
|
|
865
|
+
* config: { apiUrl: "https://api.example.com" },
|
|
866
|
+
* });
|
|
867
|
+
* expectOk(await run(loadApiUrl), "https://api.example.com");
|
|
868
|
+
* ```
|
|
869
|
+
*
|
|
206
870
|
* @group Run
|
|
207
871
|
*/
|
|
208
872
|
export const createRun = (deps) => createRunInternal({
|
|
@@ -685,10 +1349,11 @@ const mapInput = (input, fn) => Array.isArray(input)
|
|
|
685
1349
|
* callbacks. Resolve with `ok(value)` or `err(error)`, or reject with a defect,
|
|
686
1350
|
* to complete the Task. Settlement is one-shot: the first `resolve` or `reject`
|
|
687
1351
|
* wins, and later settlement calls are ignored, matching Promise semantics.
|
|
688
|
-
* When `reject` wins settlement, the defect panics the Run tree and is
|
|
689
|
-
* at Fiber boundaries as {@link AbortError} with
|
|
690
|
-
* Rejecting AbortError is reserved for Task abort
|
|
691
|
-
* `run(task)` rejects with it, and `run.abortable(task)`
|
|
1352
|
+
* When `reject` wins settlement, the defect panics the {@link Run} tree and is
|
|
1353
|
+
* observed at {@link Fiber} boundaries as {@link AbortError} with
|
|
1354
|
+
* {@link PanicAbortReason}. Rejecting AbortError is reserved for Task abort
|
|
1355
|
+
* control flow: direct `run(task)` rejects with it, and `run.abortable(task)`
|
|
1356
|
+
* returns it as an {@link Err}.
|
|
692
1357
|
*
|
|
693
1358
|
* This helper is a callback bridge. If `reject` forwards an Error created in a
|
|
694
1359
|
* separate async chain, V8 cannot reconstruct the caller's zero-cost async
|
|
@@ -703,10 +1368,10 @@ const mapInput = (input, fn) => Array.isArray(input)
|
|
|
703
1368
|
* Optionally return a synchronous cleanup callback. It runs when the Task Run
|
|
704
1369
|
* signal aborts, including normal completion and explicit abort. The callback
|
|
705
1370
|
* must not throw. Cleanup defects panic the root Run; if the callback Task
|
|
706
|
-
* already settled, its Fiber keeps the original Result while the root
|
|
707
|
-
* reports the panic. Cleanup must be synchronous; returned promises are
|
|
708
|
-
* awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
|
|
709
|
-
* with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
1371
|
+
* already settled, its Fiber keeps the original {@link Result} while the root
|
|
1372
|
+
* still reports the panic. Cleanup must be synchronous; returned promises are
|
|
1373
|
+
* not awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
|
|
1374
|
+
* using` with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
710
1375
|
* explicitly.
|
|
711
1376
|
*
|
|
712
1377
|
* If setup can throw after acquiring any resource, use a local
|
|
@@ -756,7 +1421,16 @@ export const callback = (fn) => (run) => {
|
|
|
756
1421
|
/**
|
|
757
1422
|
* Pauses execution for a specified {@link PositiveDuration}.
|
|
758
1423
|
*
|
|
759
|
-
* Aborting the Task clears the scheduled timeout.
|
|
1424
|
+
* Aborting the {@link Task} clears the scheduled timeout.
|
|
1425
|
+
*
|
|
1426
|
+
* ### Example
|
|
1427
|
+
*
|
|
1428
|
+
* ```ts
|
|
1429
|
+
* import { createRun, sleep } from "@evolu/common";
|
|
1430
|
+
*
|
|
1431
|
+
* await using run = createRun();
|
|
1432
|
+
* expectOk(await run(sleep("1ms")), undefined);
|
|
1433
|
+
* ```
|
|
760
1434
|
*
|
|
761
1435
|
* @group Timing
|
|
762
1436
|
*/
|
|
@@ -764,19 +1438,6 @@ export const sleep = (duration) => callback(({ run: { deps }, resolve }) => {
|
|
|
764
1438
|
const id = deps.time.setTimeout(() => resolve(ok()), duration);
|
|
765
1439
|
return () => deps.time.clearTimeout(id);
|
|
766
1440
|
});
|
|
767
|
-
/**
|
|
768
|
-
* Typed error returned by {@link timeout} when a Task exceeds its duration.
|
|
769
|
-
*
|
|
770
|
-
* @group Timing
|
|
771
|
-
*/
|
|
772
|
-
export const TimeoutError =
|
|
773
|
-
/*#__PURE__*/ typed("TimeoutError");
|
|
774
|
-
/**
|
|
775
|
-
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
776
|
-
*
|
|
777
|
-
* @group Timing
|
|
778
|
-
*/
|
|
779
|
-
export const timeoutError = { type: "TimeoutError" };
|
|
780
1441
|
/**
|
|
781
1442
|
* Limits how long a {@link Task} may run.
|
|
782
1443
|
*
|
|
@@ -815,23 +1476,37 @@ export const timeout = (task, duration) => race([
|
|
|
815
1476
|
},
|
|
816
1477
|
]);
|
|
817
1478
|
/**
|
|
818
|
-
*
|
|
1479
|
+
* Runtime Type for the error returned by {@link timeout} when a {@link Task}
|
|
1480
|
+
* exceeds its duration.
|
|
1481
|
+
*
|
|
1482
|
+
* @group Timing
|
|
1483
|
+
*/
|
|
1484
|
+
export const TimeoutError =
|
|
1485
|
+
/*#__PURE__*/ typed("TimeoutError");
|
|
1486
|
+
/**
|
|
1487
|
+
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
1488
|
+
*
|
|
1489
|
+
* @group Timing
|
|
1490
|
+
*/
|
|
1491
|
+
export const timeoutError = { type: "TimeoutError" };
|
|
1492
|
+
/**
|
|
1493
|
+
* Retries a {@link Task} according to a {@link Schedule}.
|
|
819
1494
|
*
|
|
820
1495
|
* Use `retry` for failure recovery: it repeats after {@link Err} and wraps the
|
|
821
1496
|
* final domain error. Use {@link repeat} for success-driven loops: it repeats
|
|
822
|
-
* after {@link Ok} and returns the Task's natural Result.
|
|
1497
|
+
* after {@link Ok} and returns the Task's natural {@link Result}.
|
|
823
1498
|
*
|
|
824
1499
|
* {@link AbortError} passes through unchanged when returned as a Result error,
|
|
825
|
-
* such as from {@link daemon}. Abort from `run(task)` remains Fiber
|
|
826
|
-
* flow. Other errors are domain errors: retrying continues while the
|
|
827
|
-
* yields another delay and {@link RetryOptions.shouldRetry} returns
|
|
828
|
-
* retrying stops, `retry` returns {@link RetryError} with the final
|
|
829
|
-
* as `lastError`.
|
|
1500
|
+
* such as from {@link daemon}. Abort from `run(task)` remains {@link Fiber}
|
|
1501
|
+
* control flow. Other errors are domain errors: retrying continues while the
|
|
1502
|
+
* schedule yields another delay and {@link RetryOptions.shouldRetry} returns
|
|
1503
|
+
* `true`. When retrying stops, `retry` returns {@link RetryError} with the final
|
|
1504
|
+
* domain error as `lastError`.
|
|
830
1505
|
*
|
|
831
1506
|
* AbortError recognition is structural. Domain errors, especially values parsed
|
|
832
1507
|
* from untrusted input, must not use the reserved AbortError shape.
|
|
833
1508
|
*
|
|
834
|
-
* ###
|
|
1509
|
+
* ### Retrying failures
|
|
835
1510
|
*
|
|
836
1511
|
* ```ts
|
|
837
1512
|
* import {
|
|
@@ -842,51 +1517,58 @@ export const timeout = (task, duration) => race([
|
|
|
842
1517
|
* type Result,
|
|
843
1518
|
* type RetryTaskError,
|
|
844
1519
|
* type Task,
|
|
1520
|
+
* type Typed,
|
|
845
1521
|
* } from "@evolu/common";
|
|
846
1522
|
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
* interface FetchDataError {
|
|
850
|
-
* readonly type: "FetchDataError";
|
|
851
|
-
* }
|
|
1523
|
+
* const fetchData: Task<string, ServiceUnavailableError> = () =>
|
|
1524
|
+
* err({ type: "ServiceUnavailable" });
|
|
852
1525
|
*
|
|
853
|
-
*
|
|
854
|
-
* err({ type: "FetchDataError" });
|
|
1526
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
855
1527
|
*
|
|
856
1528
|
* const fetchWithRetry = retry(fetchData, recurs(2));
|
|
857
1529
|
*
|
|
1530
|
+
* await using run = createRun();
|
|
858
1531
|
* const result = await run(fetchWithRetry);
|
|
859
1532
|
* expectTypeOf(result).toEqualTypeOf<
|
|
860
|
-
* Result<string, RetryTaskError<
|
|
1533
|
+
* Result<string, RetryTaskError<ServiceUnavailableError>>
|
|
861
1534
|
* >();
|
|
862
1535
|
* expectErr(result, {
|
|
863
1536
|
* type: "RetryError",
|
|
864
1537
|
* attempts: 3,
|
|
865
|
-
* lastError: { type: "
|
|
1538
|
+
* lastError: { type: "ServiceUnavailable" },
|
|
866
1539
|
* });
|
|
867
1540
|
* ```
|
|
868
1541
|
*
|
|
869
|
-
* ###
|
|
1542
|
+
* ### Filtering retries
|
|
870
1543
|
*
|
|
871
1544
|
* ```ts
|
|
872
|
-
* import {
|
|
1545
|
+
* import {
|
|
1546
|
+
* createRun,
|
|
1547
|
+
* err,
|
|
1548
|
+
* recurs,
|
|
1549
|
+
* retry,
|
|
1550
|
+
* type Task,
|
|
1551
|
+
* type Typed,
|
|
1552
|
+
* } from "@evolu/common";
|
|
873
1553
|
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
1554
|
+
* const fetchData: Task<
|
|
1555
|
+
* string,
|
|
1556
|
+
* TemporaryFailureError | PermanentFailureError
|
|
1557
|
+
* > = () => err({ type: "PermanentFailure" });
|
|
1558
|
+
*
|
|
1559
|
+
* interface TemporaryFailureError extends Typed<"TemporaryFailure"> {}
|
|
877
1560
|
*
|
|
878
|
-
*
|
|
879
|
-
* err({ type: "FatalError" });
|
|
1561
|
+
* interface PermanentFailureError extends Typed<"PermanentFailure"> {}
|
|
880
1562
|
*
|
|
881
1563
|
* const fetchWithRetry = retry(fetchData, recurs(5), {
|
|
882
|
-
* shouldRetry: (error) => error.type !== "
|
|
1564
|
+
* shouldRetry: (error) => error.type !== "PermanentFailure",
|
|
883
1565
|
* });
|
|
884
1566
|
*
|
|
885
1567
|
* await using run = createRun();
|
|
886
1568
|
* expectErr(await run(fetchWithRetry), {
|
|
887
1569
|
* type: "RetryError",
|
|
888
1570
|
* attempts: 1,
|
|
889
|
-
* lastError: { type: "
|
|
1571
|
+
* lastError: { type: "PermanentFailure" },
|
|
890
1572
|
* });
|
|
891
1573
|
* ```
|
|
892
1574
|
*
|
|
@@ -920,13 +1602,14 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
|
|
|
920
1602
|
}
|
|
921
1603
|
};
|
|
922
1604
|
/**
|
|
923
|
-
* Repeats a Task according to a {@link Schedule}.
|
|
1605
|
+
* Repeats a {@link Task} according to a {@link Schedule}.
|
|
924
1606
|
*
|
|
925
1607
|
* Runs the Task once, then repeats while the Task returns {@link Ok}, the
|
|
926
1608
|
* schedule yields another delay, and {@link RepeatOptions.shouldRepeat} returns
|
|
927
|
-
* `true`. When repeating stops, `repeat` returns the last successful
|
|
928
|
-
* the Task returns {@link Err}, including {@link Done} from a
|
|
929
|
-
* `repeat` returns that error without scheduling another
|
|
1609
|
+
* `true`. When repeating stops, `repeat` returns the last successful
|
|
1610
|
+
* {@link Result}. If the Task returns {@link Err}, including {@link Done} from a
|
|
1611
|
+
* {@link NextTask}, `repeat` returns that error without scheduling another
|
|
1612
|
+
* attempt.
|
|
930
1613
|
*
|
|
931
1614
|
* Use `repeat` for success-driven loops such as polling or consuming a
|
|
932
1615
|
* NextTask: it repeats after Ok and returns the Task's natural Result. Use
|
|
@@ -935,7 +1618,7 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
|
|
|
935
1618
|
*
|
|
936
1619
|
* With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
|
|
937
1620
|
*
|
|
938
|
-
* ###
|
|
1621
|
+
* ### Repeating successes
|
|
939
1622
|
*
|
|
940
1623
|
* ```ts
|
|
941
1624
|
* import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
|
|
@@ -953,7 +1636,7 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
|
|
|
953
1636
|
* expect(attempts).toBe(4);
|
|
954
1637
|
* ```
|
|
955
1638
|
*
|
|
956
|
-
* ###
|
|
1639
|
+
* ### Stopping with Done
|
|
957
1640
|
*
|
|
958
1641
|
* ```ts
|
|
959
1642
|
* import {
|
|
@@ -966,8 +1649,6 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
|
|
|
966
1649
|
* type NextTask,
|
|
967
1650
|
* } from "@evolu/common";
|
|
968
1651
|
*
|
|
969
|
-
* await using run = createRun();
|
|
970
|
-
*
|
|
971
1652
|
* interface Item {
|
|
972
1653
|
* readonly id: string;
|
|
973
1654
|
* }
|
|
@@ -979,6 +1660,7 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
|
|
|
979
1660
|
* return item ? ok(item) : err(done());
|
|
980
1661
|
* };
|
|
981
1662
|
*
|
|
1663
|
+
* await using run = createRun();
|
|
982
1664
|
* const result = await run(repeat(processQueue, spaced("1ms")));
|
|
983
1665
|
* expectErr(result, done());
|
|
984
1666
|
* expect(queue).toEqual([]);
|
|
@@ -1006,22 +1688,22 @@ export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } =
|
|
|
1006
1688
|
}
|
|
1007
1689
|
};
|
|
1008
1690
|
/**
|
|
1009
|
-
* Runs
|
|
1691
|
+
* Runs {@link Task}s until one returns {@link Ok} or all return {@link Err}.
|
|
1010
1692
|
*
|
|
1011
|
-
* Use {@link race} to return the first settled Result instead, whether Ok
|
|
1012
|
-
* {@link Err}.
|
|
1693
|
+
* Use {@link race} to return the first settled {@link Result} instead, whether Ok
|
|
1694
|
+
* or {@link Err}.
|
|
1013
1695
|
*
|
|
1014
|
-
* Returns the first {@link Ok} Result.
|
|
1015
|
-
* returns Ok, returns the last Err by
|
|
1016
|
-
* order. Other Err results are discarded;
|
|
1017
|
-
* every error.
|
|
1696
|
+
* Returns the first {@link Ok} Result. Queued Tasks are not started, and other
|
|
1697
|
+
* running Tasks are aborted. If no Task returns Ok, returns the last Err by
|
|
1698
|
+
* input order, regardless of completion order. Other Err results are discarded;
|
|
1699
|
+
* use {@link allSettled} when you need every error.
|
|
1018
1700
|
*
|
|
1019
1701
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1020
1702
|
* at a time.
|
|
1021
1703
|
*
|
|
1022
1704
|
* Similar to
|
|
1023
1705
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
|
|
1024
|
-
* but
|
|
1706
|
+
* but runs Tasks, returns Result values, and stops after the first Ok.
|
|
1025
1707
|
*
|
|
1026
1708
|
* ### Example
|
|
1027
1709
|
*
|
|
@@ -1033,10 +1715,14 @@ export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } =
|
|
|
1033
1715
|
* ok,
|
|
1034
1716
|
* type Result,
|
|
1035
1717
|
* type Task,
|
|
1718
|
+
* type Typed,
|
|
1036
1719
|
* } from "@evolu/common";
|
|
1037
1720
|
*
|
|
1038
|
-
* const unavailable: Task<string,
|
|
1039
|
-
* err("
|
|
1721
|
+
* const unavailable: Task<string, ServiceUnavailableError> = () =>
|
|
1722
|
+
* err({ type: "ServiceUnavailable" });
|
|
1723
|
+
*
|
|
1724
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
1725
|
+
*
|
|
1040
1726
|
* let fallbackStarted = false;
|
|
1041
1727
|
* const fallback: Task<string> = () => {
|
|
1042
1728
|
* fallbackStarted = true;
|
|
@@ -1046,7 +1732,9 @@ export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } =
|
|
|
1046
1732
|
* await using run = createRun();
|
|
1047
1733
|
* const result = await run(any([unavailable, fallback]));
|
|
1048
1734
|
*
|
|
1049
|
-
* expectTypeOf(result).toEqualTypeOf<
|
|
1735
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
1736
|
+
* Result<string, ServiceUnavailableError>
|
|
1737
|
+
* >();
|
|
1050
1738
|
* expectOk(result, "fallback");
|
|
1051
1739
|
* expect(fallbackStarted).toBe(true);
|
|
1052
1740
|
* ```
|
|
@@ -1074,7 +1762,7 @@ export const any = (tasks, options) => async (run) => {
|
|
|
1074
1762
|
return lastErr;
|
|
1075
1763
|
};
|
|
1076
1764
|
/**
|
|
1077
|
-
* Runs
|
|
1765
|
+
* Runs {@link Task}s until the first Task settles.
|
|
1078
1766
|
*
|
|
1079
1767
|
* Returns the first Task {@link Result} to settle, whether {@link Ok} or
|
|
1080
1768
|
* {@link Err}.
|
|
@@ -1095,7 +1783,7 @@ export const any = (tasks, options) => async (run) => {
|
|
|
1095
1783
|
* arrays whose emptiness is only known at runtime, guard with
|
|
1096
1784
|
* {@link isNonEmptyArray}:
|
|
1097
1785
|
*
|
|
1098
|
-
* ###
|
|
1786
|
+
* ### Runtime arrays
|
|
1099
1787
|
*
|
|
1100
1788
|
* ```ts
|
|
1101
1789
|
* import {
|
|
@@ -1114,7 +1802,7 @@ export const any = (tasks, options) => async (run) => {
|
|
|
1114
1802
|
* }
|
|
1115
1803
|
* ```
|
|
1116
1804
|
*
|
|
1117
|
-
* ###
|
|
1805
|
+
* ### First settlement
|
|
1118
1806
|
*
|
|
1119
1807
|
* ```ts
|
|
1120
1808
|
* import {
|
|
@@ -1157,12 +1845,12 @@ export const race = (tasks) => async (run) => {
|
|
|
1157
1845
|
return firstResult;
|
|
1158
1846
|
};
|
|
1159
1847
|
/**
|
|
1160
|
-
* Runs
|
|
1848
|
+
* Runs {@link Task}s until `count` Tasks return {@link Ok} or all Tasks settle.
|
|
1161
1849
|
*
|
|
1162
1850
|
* Returns {@link Ok} with Ok values in settlement order, not input order.
|
|
1163
|
-
* {@link Err}
|
|
1164
|
-
*
|
|
1165
|
-
* the Ok values that did settle.
|
|
1851
|
+
* {@link Err} {@link Result}s are ignored. When `count` Ok values have settled,
|
|
1852
|
+
* queued Tasks are not started and remaining running Tasks are aborted. If
|
|
1853
|
+
* fewer than `count` Tasks return Ok, returns the Ok values that did settle.
|
|
1166
1854
|
*
|
|
1167
1855
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1168
1856
|
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
@@ -1178,17 +1866,23 @@ export const race = (tasks) => async (run) => {
|
|
|
1178
1866
|
* ok,
|
|
1179
1867
|
* sleep,
|
|
1180
1868
|
* type Task,
|
|
1869
|
+
* type Typed,
|
|
1181
1870
|
* } from "@evolu/common";
|
|
1182
1871
|
*
|
|
1183
1872
|
* let slowCompleted = false;
|
|
1184
|
-
* const slow: Task<string
|
|
1873
|
+
* const slow: Task<string> = async (run) => {
|
|
1185
1874
|
* await run.ok(sleep("10ms"));
|
|
1186
1875
|
* slowCompleted = true;
|
|
1187
1876
|
* return ok("slow");
|
|
1188
1877
|
* };
|
|
1878
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
1879
|
+
* err({ type: "ServiceUnavailable" });
|
|
1880
|
+
*
|
|
1881
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
1882
|
+
*
|
|
1189
1883
|
* const tasks = [
|
|
1190
1884
|
* slow,
|
|
1191
|
-
*
|
|
1885
|
+
* unavailable,
|
|
1192
1886
|
* () => ok("fast-1"),
|
|
1193
1887
|
* () => ok("fast-2"),
|
|
1194
1888
|
* ] as const;
|
|
@@ -1213,11 +1907,12 @@ export const firstN = (tasks, count, options) => async (run) => {
|
|
|
1213
1907
|
return ok(values);
|
|
1214
1908
|
};
|
|
1215
1909
|
/**
|
|
1216
|
-
* Runs
|
|
1910
|
+
* Runs {@link Task}s until `count` Tasks settle or all Tasks settle.
|
|
1217
1911
|
*
|
|
1218
1912
|
* Returns {@link Ok} with Task {@link Result}s in settlement order, not input
|
|
1219
|
-
* order. When `count` Results have settled,
|
|
1220
|
-
* fewer than `count` Tasks settle,
|
|
1913
|
+
* order. When `count` Results have settled, queued Tasks are not started and
|
|
1914
|
+
* remaining running Tasks are aborted. If fewer than `count` Tasks settle,
|
|
1915
|
+
* returns the Results that did settle.
|
|
1221
1916
|
*
|
|
1222
1917
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1223
1918
|
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
@@ -1233,25 +1928,27 @@ export const firstN = (tasks, count, options) => async (run) => {
|
|
|
1233
1928
|
* ok,
|
|
1234
1929
|
* sleep,
|
|
1235
1930
|
* type Task,
|
|
1931
|
+
* type Typed,
|
|
1236
1932
|
* } from "@evolu/common";
|
|
1237
1933
|
*
|
|
1238
1934
|
* let slowCompleted = false;
|
|
1239
|
-
* const slow: Task<string
|
|
1935
|
+
* const slow: Task<string> = async (run) => {
|
|
1240
1936
|
* await run.ok(sleep("10ms"));
|
|
1241
1937
|
* slowCompleted = true;
|
|
1242
1938
|
* return ok("slow");
|
|
1243
1939
|
* };
|
|
1244
|
-
* const
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1940
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
1941
|
+
* err({ type: "ServiceUnavailable" });
|
|
1942
|
+
*
|
|
1943
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
1944
|
+
*
|
|
1945
|
+
* const tasks = [slow, unavailable, () => ok("fast")] as const;
|
|
1249
1946
|
* await using run = createRun();
|
|
1250
1947
|
*
|
|
1251
1948
|
* // Err and Ok both count, and Results use settlement order.
|
|
1252
1949
|
* const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
|
|
1253
1950
|
* expectOk(result, [
|
|
1254
|
-
* { ok: false, error: "
|
|
1951
|
+
* { ok: false, error: { type: "ServiceUnavailable" } },
|
|
1255
1952
|
* { ok: true, value: "fast" },
|
|
1256
1953
|
* ]);
|
|
1257
1954
|
* expect(slowCompleted).toBe(false);
|
|
@@ -1269,26 +1966,27 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
|
|
|
1269
1966
|
return ok(results);
|
|
1270
1967
|
};
|
|
1271
1968
|
/**
|
|
1272
|
-
* Runs
|
|
1273
|
-
* {@link Result} as it settles.
|
|
1969
|
+
* Runs {@link Task}s under a concurrency limit and calls `onResult` for each
|
|
1970
|
+
* Task {@link Result} as it settles.
|
|
1274
1971
|
*
|
|
1275
|
-
* `onResult` receives the Result and the original input index; call
|
|
1276
|
-
* settlement order, not input order. Returning `continue` lets queued
|
|
1277
|
-
* start when capacity is available. Returning `stop` prevents queued
|
|
1278
|
-
* starting and aborts already-running Tasks through structured
|
|
1279
|
-
* `each` still waits for them to settle before
|
|
1972
|
+
* `onResult` receives the {@link Result} and the original input index; call
|
|
1973
|
+
* order is settlement order, not input order. Returning `continue` lets queued
|
|
1974
|
+
* Tasks start when capacity is available. Returning `stop` prevents queued
|
|
1975
|
+
* Tasks from starting and aborts already-running Tasks through structured
|
|
1976
|
+
* {@link Run} disposal — `each` still waits for them to settle before
|
|
1977
|
+
* returning.
|
|
1280
1978
|
*
|
|
1281
1979
|
* `each` is the scheduling primitive under the collection helpers. Each one is
|
|
1282
1980
|
* a small `onResult` policy:
|
|
1283
1981
|
*
|
|
1284
|
-
* | Helper | Policy
|
|
1285
|
-
* | --------------------- |
|
|
1286
|
-
* | {@link all} | Collect values, stop on the first Err |
|
|
1287
|
-
* | {@link allSettled} | Collect every Result, never stop
|
|
1288
|
-
* | {@link any} | Stop on the first Ok |
|
|
1289
|
-
* | {@link race} | Stop on the first settled Result
|
|
1290
|
-
* | {@link firstN} | Stop after n Ok values
|
|
1291
|
-
* | {@link firstNSettled} | Stop after n Results
|
|
1982
|
+
* | Helper | Policy |
|
|
1983
|
+
* | --------------------- | --------------------------------------------- |
|
|
1984
|
+
* | {@link all} | Collect values, stop on the first {@link Err} |
|
|
1985
|
+
* | {@link allSettled} | Collect every Result, never stop |
|
|
1986
|
+
* | {@link any} | Stop on the first {@link Ok} |
|
|
1987
|
+
* | {@link race} | Stop on the first settled Result |
|
|
1988
|
+
* | {@link firstN} | Stop after n Ok values |
|
|
1989
|
+
* | {@link firstNSettled} | Stop after n Results |
|
|
1292
1990
|
*
|
|
1293
1991
|
* Use `each` directly to build a collection policy the helpers don't cover. For
|
|
1294
1992
|
* example, keep the first successful value together with its original input
|
|
@@ -1304,6 +2002,7 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
|
|
|
1304
2002
|
* ok,
|
|
1305
2003
|
* sleep,
|
|
1306
2004
|
* type Task,
|
|
2005
|
+
* type Typed,
|
|
1307
2006
|
* } from "@evolu/common";
|
|
1308
2007
|
*
|
|
1309
2008
|
* let slowCompleted = false;
|
|
@@ -1312,11 +2011,12 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
|
|
|
1312
2011
|
* slowCompleted = true;
|
|
1313
2012
|
* return ok("slow");
|
|
1314
2013
|
* };
|
|
1315
|
-
* const
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
2014
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
2015
|
+
* err({ type: "ServiceUnavailable" });
|
|
2016
|
+
*
|
|
2017
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
2018
|
+
*
|
|
2019
|
+
* const tasks = [slow, unavailable, () => ok("fast")] as const;
|
|
1320
2020
|
* let first: readonly [string, number] | undefined;
|
|
1321
2021
|
* await using run = createRun();
|
|
1322
2022
|
* const result = await run(
|
|
@@ -1350,7 +2050,7 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
|
|
|
1350
2050
|
* building on `each` preserves diagnostics that a hand-rolled scheduling loop
|
|
1351
2051
|
* typically loses.
|
|
1352
2052
|
*
|
|
1353
|
-
* @group
|
|
2053
|
+
* @group Collection
|
|
1354
2054
|
*/
|
|
1355
2055
|
export const each = (tasks, onResult, options = {}) => async (run) => {
|
|
1356
2056
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
@@ -1445,9 +2145,9 @@ export const prioritized = (priority, task) => withTaskMeta({ priority })(task);
|
|
|
1445
2145
|
* Yields execution to the host scheduler.
|
|
1446
2146
|
*
|
|
1447
2147
|
* Uses native `scheduler.yield()` when available, `setImmediate` when
|
|
1448
|
-
* available, and `setTimeout` elsewhere. Because this is a Task, `await
|
|
1449
|
-
* run(yieldNow)` is an explicit abortable checkpoint and is visible in
|
|
1450
|
-
* monitoring.
|
|
2148
|
+
* available, and `setTimeout` elsewhere. Because this is a {@link Task}, `await
|
|
2149
|
+
* run(yieldNow)` is an explicit abortable checkpoint and is visible in
|
|
2150
|
+
* {@link Run} monitoring.
|
|
1451
2151
|
*
|
|
1452
2152
|
* For example, call it periodically in a long-running synchronous loop to let
|
|
1453
2153
|
* the host process rendering, input, and other scheduled work.
|
|
@@ -1519,12 +2219,13 @@ export const yieldNow = async (run) => {
|
|
|
1519
2219
|
* interface ServerDep {
|
|
1520
2220
|
* readonly port: number;
|
|
1521
2221
|
* }
|
|
2222
|
+
*
|
|
1522
2223
|
* interface Server extends AsyncDisposable {}
|
|
1523
2224
|
*
|
|
1524
2225
|
* const serverStarted = Promise.withResolvers<void>();
|
|
1525
2226
|
* let serverStopped = false;
|
|
1526
|
-
* const startServer: Task<Server, never, ServerDep> = (
|
|
1527
|
-
* expect(deps.port).toBe(3000);
|
|
2227
|
+
* const startServer: Task<Server, never, ServerDep> = (run) => {
|
|
2228
|
+
* expect(run.deps.port).toBe(3000);
|
|
1528
2229
|
* serverStarted.resolve();
|
|
1529
2230
|
* return ok({
|
|
1530
2231
|
* [Symbol.asyncDispose]: async () => {
|
|
@@ -1588,7 +2289,7 @@ export const waitForAbort = async (run) => {
|
|
|
1588
2289
|
* Do not wrap a Task that keeps using a resource the caller may release after
|
|
1589
2290
|
* this wrapper returns, unless the Task reliably observes abort before using
|
|
1590
2291
|
* that resource. The daemon Task can continue after the caller stops waiting.
|
|
1591
|
-
* Later domain
|
|
2292
|
+
* Later domain {@link Err} results from the daemon Task are discarded after the
|
|
1592
2293
|
* caller stops waiting. Defects from the daemon Task remain visible to the root
|
|
1593
2294
|
* Run: if it later throws or rejects, the root Run still panics and reports the
|
|
1594
2295
|
* defect.
|
|
@@ -1598,10 +2299,10 @@ export const waitForAbort = async (run) => {
|
|
|
1598
2299
|
* to settle, keeping cleanup and late defects inside the caller's lifetime. A
|
|
1599
2300
|
* Task that ignores abort can keep them waiting.
|
|
1600
2301
|
*
|
|
1601
|
-
* `run.abortable(task)` returns an owned child Fiber and requests abort
|
|
1602
|
-
* that Fiber; `daemon(task)` starts a daemon child and stops waiting
|
|
1603
|
-
* current Run aborts. {@link unabortable} masks abort for a Task that
|
|
1604
|
-
* finish once started; daemon lets a Task outlive the caller.
|
|
2302
|
+
* `run.abortable(task)` returns an owned child {@link Fiber} and requests abort
|
|
2303
|
+
* through that Fiber; `daemon(task)` starts a daemon child and stops waiting
|
|
2304
|
+
* when the current Run aborts. {@link unabortable} masks abort for a Task that
|
|
2305
|
+
* must finish once started; daemon lets a Task outlive the caller.
|
|
1605
2306
|
*
|
|
1606
2307
|
* Because the Task starts with {@link Run.daemon}, a recorded abort request
|
|
1607
2308
|
* returns AbortError before the Task starts — including a request masked by
|
|
@@ -1648,15 +2349,10 @@ export const waitForAbort = async (run) => {
|
|
|
1648
2349
|
* import { createRun, ok, type Result, type Task } from "@evolu/common";
|
|
1649
2350
|
*
|
|
1650
2351
|
* type ResultValue = string;
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
* }
|
|
1654
|
-
* const createPromiseReturningResult = (): Promise<
|
|
1655
|
-
* Result<ResultValue, MyError>
|
|
1656
|
-
* > => Promise.resolve(ok("value"));
|
|
2352
|
+
* const createPromiseReturningResult = (): Promise<Result<ResultValue>> =>
|
|
2353
|
+
* Promise.resolve(ok("value"));
|
|
1657
2354
|
*
|
|
1658
|
-
* const task: Task<ResultValue
|
|
1659
|
-
* createPromiseReturningResult();
|
|
2355
|
+
* const task: Task<ResultValue> = () => createPromiseReturningResult();
|
|
1660
2356
|
*
|
|
1661
2357
|
* await using run = createRun();
|
|
1662
2358
|
* expectOk(await run(task), "value");
|
|
@@ -1669,12 +2365,9 @@ export const waitForAbort = async (run) => {
|
|
|
1669
2365
|
* import { ok, type Result, type Task } from "@evolu/common";
|
|
1670
2366
|
*
|
|
1671
2367
|
* type ResultValue = string;
|
|
1672
|
-
* interface MyError {
|
|
1673
|
-
* readonly type: "MyError";
|
|
1674
|
-
* }
|
|
1675
2368
|
* let promiseStarted = false;
|
|
1676
2369
|
* const createPromiseReturningResult = (): Promise<
|
|
1677
|
-
* Result<ResultValue
|
|
2370
|
+
* Result<ResultValue>
|
|
1678
2371
|
* > => {
|
|
1679
2372
|
* promiseStarted = true;
|
|
1680
2373
|
* return Promise.resolve(ok("value"));
|
|
@@ -1682,10 +2375,10 @@ export const waitForAbort = async (run) => {
|
|
|
1682
2375
|
*
|
|
1683
2376
|
* // Wrong: the Promise starts now, before a Run starts the Task.
|
|
1684
2377
|
* const promise = createPromiseReturningResult();
|
|
1685
|
-
* const task: Task<ResultValue
|
|
2378
|
+
* const task: Task<ResultValue> = () => promise;
|
|
1686
2379
|
*
|
|
1687
2380
|
* expect(promiseStarted).toBe(true);
|
|
1688
|
-
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue
|
|
2381
|
+
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue>>();
|
|
1689
2382
|
* ```
|
|
1690
2383
|
*
|
|
1691
2384
|
* @group Lifetime
|
|
@@ -1723,8 +2416,8 @@ export const daemon = (task) => async (run) => {
|
|
|
1723
2416
|
* Abort requests are masked while the Task runs, so `run.signal.aborted`
|
|
1724
2417
|
* remains false inside the Task. This does not force the Task to start after an
|
|
1725
2418
|
* abort request has already reached its Run; unabortable means the Task is not
|
|
1726
|
-
* interrupted once it has started. Disposing the enclosing Run still
|
|
1727
|
-
* the Task to settle.
|
|
2419
|
+
* interrupted once it has started. Disposing the enclosing {@link Run} still
|
|
2420
|
+
* waits for the Task to settle.
|
|
1728
2421
|
*
|
|
1729
2422
|
* Apply at most one abort behavior helper to a Task: do not wrap the same Task
|
|
1730
2423
|
* with both unabortable and restore, or apply either helper more than once.
|
|
@@ -1736,10 +2429,10 @@ export const daemon = (task) => async (run) => {
|
|
|
1736
2429
|
*
|
|
1737
2430
|
* const commitStarted = Promise.withResolvers<void>();
|
|
1738
2431
|
* const finishCommit = Promise.withResolvers<void>();
|
|
1739
|
-
* const commit: Task<string> = unabortable(async (
|
|
2432
|
+
* const commit: Task<string> = unabortable(async (run) => {
|
|
1740
2433
|
* commitStarted.resolve();
|
|
1741
2434
|
* await finishCommit.promise;
|
|
1742
|
-
* expect(signal.aborted).toBe(false);
|
|
2435
|
+
* expect(run.signal.aborted).toBe(false);
|
|
1743
2436
|
* return ok("committed");
|
|
1744
2437
|
* });
|
|
1745
2438
|
*
|
|
@@ -1758,8 +2451,8 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
1758
2451
|
abortBehavior: "unabortable",
|
|
1759
2452
|
});
|
|
1760
2453
|
/**
|
|
1761
|
-
* Like {@link unabortable}, but provides `restore` for child
|
|
1762
|
-
* run with the previous abort mask.
|
|
2454
|
+
* Like {@link unabortable}, but provides `restore` for child {@link Task}s that
|
|
2455
|
+
* should run with the previous abort mask.
|
|
1763
2456
|
*
|
|
1764
2457
|
* Use this for acquire/use/release flows where acquire and release must finish
|
|
1765
2458
|
* once started, while use should remain abortable. Child Tasks inherit the mask
|
|
@@ -1787,8 +2480,6 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
1787
2480
|
* type Task,
|
|
1788
2481
|
* } from "@evolu/common";
|
|
1789
2482
|
*
|
|
1790
|
-
* await using run = createRun();
|
|
1791
|
-
*
|
|
1792
2483
|
* interface Resource {
|
|
1793
2484
|
* readonly id: string;
|
|
1794
2485
|
* }
|
|
@@ -1805,13 +2496,14 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
1805
2496
|
* let released = false;
|
|
1806
2497
|
* const release =
|
|
1807
2498
|
* (_resource: Resource): Task<void> =>
|
|
1808
|
-
* (
|
|
2499
|
+
* (run) => {
|
|
1809
2500
|
* // Release inherits the mask even after abort was requested.
|
|
1810
|
-
* expect(signal.aborted).toBe(false);
|
|
2501
|
+
* expect(run.signal.aborted).toBe(false);
|
|
1811
2502
|
* released = true;
|
|
1812
2503
|
* return ok();
|
|
1813
2504
|
* };
|
|
1814
2505
|
*
|
|
2506
|
+
* await using run = createRun();
|
|
1815
2507
|
* const fiber = run.abortable(
|
|
1816
2508
|
* unabortableMask((restore) => async (run) => {
|
|
1817
2509
|
* // Acquire with abort masked.
|
|
@@ -1880,6 +2572,7 @@ export const unabortableMask = (fn) => unabortable((run) => {
|
|
|
1880
2572
|
* err,
|
|
1881
2573
|
* ok,
|
|
1882
2574
|
* type Task,
|
|
2575
|
+
* type Typed,
|
|
1883
2576
|
* } from "@evolu/common";
|
|
1884
2577
|
*
|
|
1885
2578
|
* interface Connection {
|
|
@@ -1890,9 +2583,14 @@ export const unabortableMask = (fn) => unabortable((run) => {
|
|
|
1890
2583
|
* const openConnection: Task<Connection> = () =>
|
|
1891
2584
|
* ok({ user: "Ada", isAvailable: false });
|
|
1892
2585
|
* const loadUser =
|
|
1893
|
-
* (connection: Connection): Task<string,
|
|
2586
|
+
* (connection: Connection): Task<string, UserUnavailableError> =>
|
|
1894
2587
|
* () =>
|
|
1895
|
-
* connection.isAvailable
|
|
2588
|
+
* connection.isAvailable
|
|
2589
|
+
* ? ok(connection.user)
|
|
2590
|
+
* : err({ type: "UserUnavailable" });
|
|
2591
|
+
*
|
|
2592
|
+
* interface UserUnavailableError extends Typed<"UserUnavailable"> {}
|
|
2593
|
+
*
|
|
1896
2594
|
* let connectionClosed = false;
|
|
1897
2595
|
* const closeConnection =
|
|
1898
2596
|
* (_connection: Connection): Task<void> =>
|
|
@@ -1908,12 +2606,12 @@ export const unabortableMask = (fn) => unabortable((run) => {
|
|
|
1908
2606
|
* );
|
|
1909
2607
|
*
|
|
1910
2608
|
* await using run = createRun();
|
|
1911
|
-
* expectErr(await run(queryUser), "
|
|
2609
|
+
* expectErr(await run(queryUser), { type: "UserUnavailable" });
|
|
1912
2610
|
* // Release still runs when use returns a domain error.
|
|
1913
2611
|
* expect(connectionClosed).toBe(true);
|
|
1914
2612
|
* ```
|
|
1915
2613
|
*
|
|
1916
|
-
* @group
|
|
2614
|
+
* @group Lifetime
|
|
1917
2615
|
*/
|
|
1918
2616
|
export const acquireUseRelease = (acquire, use, release) => unabortableMask((restore) => async (run) => {
|
|
1919
2617
|
const resourceResult = await run(acquire);
|
|
@@ -1930,6 +2628,48 @@ export const acquireUseRelease = (acquire, use, release) => unabortableMask((res
|
|
|
1930
2628
|
/**
|
|
1931
2629
|
* Creates a {@link Deferred}.
|
|
1932
2630
|
*
|
|
2631
|
+
* ### Example
|
|
2632
|
+
*
|
|
2633
|
+
* ```ts
|
|
2634
|
+
* import {
|
|
2635
|
+
* createDeferred,
|
|
2636
|
+
* createRun,
|
|
2637
|
+
* ok,
|
|
2638
|
+
* type Result,
|
|
2639
|
+
* } from "@evolu/common";
|
|
2640
|
+
*
|
|
2641
|
+
* await using run = createRun();
|
|
2642
|
+
* const deferred = createDeferred<string>();
|
|
2643
|
+
*
|
|
2644
|
+
* const fiber = run(deferred.task);
|
|
2645
|
+
* expect(deferred.resolve(ok("ready"))).toBe(true);
|
|
2646
|
+
*
|
|
2647
|
+
* const result = await fiber;
|
|
2648
|
+
* expectTypeOf(result).toEqualTypeOf<Result<string>>();
|
|
2649
|
+
* expectOk(result, "ready");
|
|
2650
|
+
*
|
|
2651
|
+
* // A Deferred is one-shot: later resolutions are ignored, and future
|
|
2652
|
+
* // waiters receive the original Result.
|
|
2653
|
+
* expect(deferred.resolve(ok("late"))).toBe(false);
|
|
2654
|
+
* expectOk(await run(deferred.task), "ready");
|
|
2655
|
+
* ```
|
|
2656
|
+
*
|
|
2657
|
+
* ### Aborting a waiter
|
|
2658
|
+
*
|
|
2659
|
+
* ```ts
|
|
2660
|
+
* import { AbortError, createDeferred, createRun } from "@evolu/common";
|
|
2661
|
+
*
|
|
2662
|
+
* await using run = createRun();
|
|
2663
|
+
* const deferred = createDeferred<string>();
|
|
2664
|
+
*
|
|
2665
|
+
* const fiber = run.abortable(deferred.task);
|
|
2666
|
+
* fiber.abort({ type: "NoLongerNeeded" });
|
|
2667
|
+
*
|
|
2668
|
+
* const result = await fiber;
|
|
2669
|
+
* assert(!result.ok);
|
|
2670
|
+
* expect(AbortError.is(result.error)).toBe(true);
|
|
2671
|
+
* ```
|
|
2672
|
+
*
|
|
1933
2673
|
* @group Concurrency primitives
|
|
1934
2674
|
*/
|
|
1935
2675
|
export const createDeferred = () => {
|
|
@@ -1960,6 +2700,34 @@ export const createDeferred = () => {
|
|
|
1960
2700
|
/**
|
|
1961
2701
|
* Creates a {@link Gate}.
|
|
1962
2702
|
*
|
|
2703
|
+
* ### Example
|
|
2704
|
+
*
|
|
2705
|
+
* ```ts
|
|
2706
|
+
* import { createGate, createRun, ok, type Task } from "@evolu/common";
|
|
2707
|
+
*
|
|
2708
|
+
* await using run = createRun();
|
|
2709
|
+
* const networkGate = createGate();
|
|
2710
|
+
*
|
|
2711
|
+
* const uploadedItems: Array<string> = [];
|
|
2712
|
+
* const syncOnce =
|
|
2713
|
+
* (item: string): Task<void> =>
|
|
2714
|
+
* async (run) => {
|
|
2715
|
+
* await run.ok(networkGate.wait);
|
|
2716
|
+
* uploadedItems.push(item);
|
|
2717
|
+
* return ok();
|
|
2718
|
+
* };
|
|
2719
|
+
*
|
|
2720
|
+
* const first = run(syncOnce("first"));
|
|
2721
|
+
* const second = run(syncOnce("second"));
|
|
2722
|
+
* expect(uploadedItems).toEqual([]);
|
|
2723
|
+
*
|
|
2724
|
+
* networkGate.open();
|
|
2725
|
+
* expectOk(await first, undefined);
|
|
2726
|
+
* expectOk(await second, undefined);
|
|
2727
|
+
* expect(uploadedItems).toEqual(["first", "second"]);
|
|
2728
|
+
* expect(networkGate.isOpen()).toBe(true);
|
|
2729
|
+
* ```
|
|
2730
|
+
*
|
|
1963
2731
|
* @group Concurrency primitives
|
|
1964
2732
|
*/
|
|
1965
2733
|
export const createGate = ({ isOpen = false, } = {}) => {
|
|
@@ -1997,6 +2765,45 @@ export const createGate = ({ isOpen = false, } = {}) => {
|
|
|
1997
2765
|
/**
|
|
1998
2766
|
* Creates a {@link Semaphore}.
|
|
1999
2767
|
*
|
|
2768
|
+
* ### Example
|
|
2769
|
+
*
|
|
2770
|
+
* ```ts
|
|
2771
|
+
* import {
|
|
2772
|
+
* createRun,
|
|
2773
|
+
* createSemaphore,
|
|
2774
|
+
* getOk,
|
|
2775
|
+
* ok,
|
|
2776
|
+
* sleep,
|
|
2777
|
+
* type Task,
|
|
2778
|
+
* } from "@evolu/common";
|
|
2779
|
+
*
|
|
2780
|
+
* await using run = createRun();
|
|
2781
|
+
*
|
|
2782
|
+
* const semaphore = createSemaphore(2);
|
|
2783
|
+
* let activeSaves = 0;
|
|
2784
|
+
* let maxActiveSaves = 0;
|
|
2785
|
+
*
|
|
2786
|
+
* const saveUser =
|
|
2787
|
+
* (id: string): Task<string> =>
|
|
2788
|
+
* async (run) => {
|
|
2789
|
+
* activeSaves += 1;
|
|
2790
|
+
* maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
|
|
2791
|
+
* await run.ok(sleep("10ms"));
|
|
2792
|
+
* activeSaves -= 1;
|
|
2793
|
+
* return ok(`saved:${id}`);
|
|
2794
|
+
* };
|
|
2795
|
+
*
|
|
2796
|
+
* const results = await Promise.all([
|
|
2797
|
+
* run(semaphore.withPermit(saveUser("1"))),
|
|
2798
|
+
* run(semaphore.withPermit(saveUser("2"))),
|
|
2799
|
+
* run(semaphore.withPermit(saveUser("3"))),
|
|
2800
|
+
* ]);
|
|
2801
|
+
*
|
|
2802
|
+
* const savedUsers = results.map(getOk);
|
|
2803
|
+
* expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
|
|
2804
|
+
* expect(maxActiveSaves).toBe(2);
|
|
2805
|
+
* ```
|
|
2806
|
+
*
|
|
2000
2807
|
* @group Concurrency primitives
|
|
2001
2808
|
*/
|
|
2002
2809
|
export const createSemaphore = (initialPermits, { policy = "fifo", } = {}) => {
|
|
@@ -2112,6 +2919,37 @@ export const createSemaphore = (initialPermits, { policy = "fifo", } = {}) => {
|
|
|
2112
2919
|
/**
|
|
2113
2920
|
* Creates a {@link Mutex}.
|
|
2114
2921
|
*
|
|
2922
|
+
* ### Example
|
|
2923
|
+
*
|
|
2924
|
+
* ```ts
|
|
2925
|
+
* import {
|
|
2926
|
+
* createMutex,
|
|
2927
|
+
* createRun,
|
|
2928
|
+
* ok,
|
|
2929
|
+
* sleep,
|
|
2930
|
+
* type Task,
|
|
2931
|
+
* } from "@evolu/common";
|
|
2932
|
+
*
|
|
2933
|
+
* const mutex = createMutex();
|
|
2934
|
+
* let balance = 0;
|
|
2935
|
+
* const deposit = (amount: number): Task<void> =>
|
|
2936
|
+
* mutex.withLock(async (run) => {
|
|
2937
|
+
* const currentBalance = balance;
|
|
2938
|
+
* await run.ok(sleep("1ms"));
|
|
2939
|
+
* balance = currentBalance + amount;
|
|
2940
|
+
* return ok();
|
|
2941
|
+
* });
|
|
2942
|
+
*
|
|
2943
|
+
* await using run = createRun();
|
|
2944
|
+
* const [first, second] = await Promise.all([
|
|
2945
|
+
* run(deposit(2)),
|
|
2946
|
+
* run(deposit(3)),
|
|
2947
|
+
* ]);
|
|
2948
|
+
* expectOk(first, undefined);
|
|
2949
|
+
* expectOk(second, undefined);
|
|
2950
|
+
* expect(balance).toBe(5);
|
|
2951
|
+
* ```
|
|
2952
|
+
*
|
|
2115
2953
|
* @group Concurrency primitives
|
|
2116
2954
|
*/
|
|
2117
2955
|
export const createMutex = () => {
|
|
@@ -2163,6 +3001,19 @@ export function createMutexByKey({ lookup = identity, } = {}) {
|
|
|
2163
3001
|
/**
|
|
2164
3002
|
* Creates a {@link MutexRef}.
|
|
2165
3003
|
*
|
|
3004
|
+
* ### Example
|
|
3005
|
+
*
|
|
3006
|
+
* ```ts
|
|
3007
|
+
* import { createMutexRef, createRun, ok } from "@evolu/common";
|
|
3008
|
+
*
|
|
3009
|
+
* const counter = createMutexRef(0);
|
|
3010
|
+
* const increment = counter.updateAndGet((value) => () => ok(value + 1));
|
|
3011
|
+
*
|
|
3012
|
+
* await using run = createRun();
|
|
3013
|
+
* expectOk(await run(increment), 1);
|
|
3014
|
+
* expectOk(await run(counter.get), 1);
|
|
3015
|
+
* ```
|
|
3016
|
+
*
|
|
2166
3017
|
* @group Concurrency primitives
|
|
2167
3018
|
*/
|
|
2168
3019
|
export const createMutexRef = (initialValue) => {
|
|
@@ -2209,7 +3060,7 @@ export const createMutexRef = (initialValue) => {
|
|
|
2209
3060
|
snapshot: mutex.snapshot,
|
|
2210
3061
|
};
|
|
2211
3062
|
};
|
|
2212
|
-
// TODO:
|
|
3063
|
+
// TODO: Expand Run observability.
|
|
2213
3064
|
// - Structured logging with levels, inherited log annotations, JSON output,
|
|
2214
3065
|
// filtering, and pluggable log sinks.
|
|
2215
3066
|
// - Tracing spans with names, timing, parent-child relationships, attributes,
|