@evolu/common 8.1.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Function.d.ts +1 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Http.d.ts +9 -0
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/Number.d.ts +66 -6
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +11 -3
- 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/Schedule.d.ts +2 -2
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/StackTrace.d.ts +1 -0
- package/dist/src/StackTrace.d.ts.map +1 -1
- package/dist/src/Task.d.ts +978 -1061
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1010 -159
- package/dist/src/Time.d.ts +71 -34
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +77 -1
- package/dist/src/Type.d.ts +1740 -795
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1634 -211
- package/dist/src/Types.d.ts +0 -24
- package/dist/src/Types.d.ts.map +1 -1
- 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 +30 -7
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +65 -28
- 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/Number.ts +85 -14
- 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/Schedule.ts +1 -1
- package/src/StackTrace.ts +2 -0
- package/src/Task.ts +1145 -1225
- package/src/Time.ts +110 -62
- package/src/Type.ts +2842 -835
- package/src/Types.ts +0 -49
- 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 +131 -32
- package/src/intl/da.ts +488 -0
- package/src/intl/de.ts +488 -0
- package/src/intl/el.ts +494 -0
- package/src/intl/es.ts +488 -0
- package/src/intl/fa.ts +487 -0
- package/src/intl/fi.ts +487 -0
- package/src/intl/fil.ts +488 -0
- package/src/intl/fr.ts +488 -0
- package/src/intl/he.ts +486 -0
- package/src/intl/hi.ts +487 -0
- package/src/intl/hr.ts +494 -0
- package/src/intl/hu.ts +387 -0
- package/src/intl/id.ts +487 -0
- package/src/intl/index.ts +42 -0
- package/src/intl/it.ts +488 -0
- package/src/intl/ja.ts +488 -0
- package/src/intl/ko.ts +487 -0
- package/src/intl/ml.ts +487 -0
- package/src/intl/mr.ts +487 -0
- package/src/intl/ms.ts +410 -0
- package/src/intl/nb.ts +364 -0
- package/src/intl/nl.ts +488 -0
- package/src/intl/pa.ts +487 -0
- package/src/intl/pl.ts +450 -0
- package/src/intl/pt-BR.ts +487 -0
- package/src/intl/pt.ts +424 -0
- package/src/intl/ro.ts +488 -0
- package/src/intl/sk.ts +412 -0
- package/src/intl/sl.ts +487 -0
- package/src/intl/sv.ts +488 -0
- package/src/intl/sw.ts +324 -0
- package/src/intl/ta.ts +487 -0
- package/src/intl/te.ts +487 -0
- package/src/intl/th.ts +484 -0
- package/src/intl/tr.ts +487 -0
- package/src/intl/uk.ts +488 -0
- package/src/intl/ur.ts +410 -0
- package/src/intl/vi.ts +435 -0
- package/src/intl/zh-CN.ts +482 -0
- package/src/intl/zh-TW.ts +482 -0
- package/src/local-first/Owner.ts +156 -62
package/src/Task.ts
CHANGED
|
@@ -1,291 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ## Intro
|
|
3
3
|
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
emptyArray,
|
|
8
|
-
isNonEmptyArray,
|
|
9
|
-
mapArray,
|
|
10
|
-
type NonEmptyReadonlyArray,
|
|
11
|
-
} from "./Array.ts";
|
|
12
|
-
import {
|
|
13
|
-
assert,
|
|
14
|
-
assertNonEmptyReadonlyArray,
|
|
15
|
-
assertNonNullable,
|
|
16
|
-
assertNotDisposed,
|
|
17
|
-
} from "./Assert.ts";
|
|
18
|
-
import type { Brand } from "./Brand.ts";
|
|
19
|
-
import {
|
|
20
|
-
createConsole,
|
|
21
|
-
testCreateConsole,
|
|
22
|
-
type Console,
|
|
23
|
-
type ConsoleDep,
|
|
24
|
-
type TestConsole,
|
|
25
|
-
type TestConsoleDep,
|
|
26
|
-
} from "./Console.ts";
|
|
27
|
-
import {
|
|
28
|
-
createRandomBytes,
|
|
29
|
-
testCreateRandomBytes,
|
|
30
|
-
type RandomBytes,
|
|
31
|
-
type RandomBytesDep,
|
|
32
|
-
} from "./Crypto.ts";
|
|
33
|
-
import { eqArrayStrict } from "./Eq.ts";
|
|
34
|
-
import { constTrue, constVoid, identity } from "./Function.ts";
|
|
35
|
-
import type { fetch, NativeFetch, NativeFetchDep } from "./Http.ts";
|
|
36
|
-
import {
|
|
37
|
-
createLeakDetector,
|
|
38
|
-
noopLeakDetector,
|
|
39
|
-
testCreateLeakDetector,
|
|
40
|
-
type LeakDetector,
|
|
41
|
-
type LeakDetectorDep,
|
|
42
|
-
type TestLeakDetectorDep,
|
|
43
|
-
} from "./LeakDetector.ts";
|
|
44
|
-
import { createLookupMap, type Lookup, type LookupOption } from "./Lookup.ts";
|
|
45
|
-
import { decrement, increment, incrementPositiveInt } from "./Number.ts";
|
|
46
|
-
import {
|
|
47
|
-
emptyRecord,
|
|
48
|
-
mapObject,
|
|
49
|
-
objectFromEntries,
|
|
50
|
-
objectToEntries,
|
|
51
|
-
} from "./Object.ts";
|
|
52
|
-
import { none, some, type Option } from "./Option.ts";
|
|
53
|
-
import { isDev } from "./Platform.ts";
|
|
54
|
-
import {
|
|
55
|
-
createRandom,
|
|
56
|
-
testCreateRandom,
|
|
57
|
-
testCreateRandomLib,
|
|
58
|
-
type Random,
|
|
59
|
-
type RandomDep,
|
|
60
|
-
type RandomLibDep,
|
|
61
|
-
type RandomNumber,
|
|
62
|
-
} from "./Random.ts";
|
|
63
|
-
import { createRef, type Ref } from "./Ref.ts";
|
|
64
|
-
import {
|
|
65
|
-
err,
|
|
66
|
-
getOk,
|
|
67
|
-
getOrThrow,
|
|
68
|
-
ok,
|
|
69
|
-
type AnyResult,
|
|
70
|
-
type Done,
|
|
71
|
-
type Err,
|
|
72
|
-
type Ok,
|
|
73
|
-
type Result,
|
|
74
|
-
type tryAsync,
|
|
75
|
-
type trySync,
|
|
76
|
-
} from "./Result.ts";
|
|
77
|
-
import type { Schedule, ScheduleStep } from "./Schedule.ts";
|
|
78
|
-
import { emptySet } from "./Set.ts";
|
|
79
|
-
import {
|
|
80
|
-
createTime,
|
|
81
|
-
PositiveMillis,
|
|
82
|
-
testCreateTime,
|
|
83
|
-
type Millis,
|
|
84
|
-
type PositiveDuration,
|
|
85
|
-
type Time,
|
|
86
|
-
type TimeDep,
|
|
87
|
-
type TestTimeDep,
|
|
88
|
-
} from "./Time.ts";
|
|
89
|
-
import {
|
|
90
|
-
assertType,
|
|
91
|
-
createId,
|
|
92
|
-
type InferType,
|
|
93
|
-
maxPositiveInt,
|
|
94
|
-
NonNegativeInt,
|
|
95
|
-
object,
|
|
96
|
-
onePositiveInt,
|
|
97
|
-
PositiveInt,
|
|
98
|
-
record,
|
|
99
|
-
String,
|
|
100
|
-
typed,
|
|
101
|
-
Unknown,
|
|
102
|
-
UnknownResult,
|
|
103
|
-
type Id,
|
|
104
|
-
type Int1To100OrPositiveInt,
|
|
105
|
-
type ObjectType,
|
|
106
|
-
type RecordType,
|
|
107
|
-
type Typed,
|
|
108
|
-
type TypedType,
|
|
109
|
-
} from "./Type.ts";
|
|
110
|
-
import type {
|
|
111
|
-
Awaitable,
|
|
112
|
-
isPromiseLike,
|
|
113
|
-
Writable,
|
|
114
|
-
ParameterIntersection,
|
|
115
|
-
Predicate,
|
|
116
|
-
} from "./Types.ts";
|
|
117
|
-
|
|
118
|
-
// Core
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
4
|
* JavaScript-native structured concurrency.
|
|
122
5
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* it completes, and abort follows the tree: aborting a parent requests abort of
|
|
129
|
-
* all its descendants, while races and fail-fast operations also abort
|
|
130
|
-
* remaining sibling branches. This prevents detached work and gives
|
|
131
|
-
* cancellation, failure, and cleanup explicit ownership. The tree also makes
|
|
132
|
-
* running work and its ownership observable.
|
|
6
|
+
* Structured concurrency makes ownership of asynchronous work explicit.
|
|
7
|
+
* Operations form a tree where every child belongs to a parent. A parent waits
|
|
8
|
+
* for its children before it completes, and abort follows the tree: aborting a
|
|
9
|
+
* parent requests abort of all its descendants. Races and fail-fast operations
|
|
10
|
+
* also abort their remaining sibling branches.
|
|
133
11
|
*
|
|
134
12
|
* With plain {@link AbortController} code, these guarantees depend on call-site
|
|
135
13
|
* discipline: someone must remember the `finally` that aborts started work and
|
|
136
|
-
* the await that waits for
|
|
137
|
-
* registers every child before it starts, and the parent settles
|
|
138
|
-
* child cleanup finishes
|
|
139
|
-
*
|
|
140
|
-
* Evolu
|
|
141
|
-
*
|
|
142
|
-
* {@link
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* -
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* aborted, while `run.abortable(task)` catches abort and returns it as an
|
|
159
|
-
* {@link Err}. If the Task itself throws or rejects, that is a defect. A defect
|
|
160
|
-
* panics the Run tree, and a Fiber rejects with AbortError whose reason is
|
|
161
|
-
* {@link PanicAbortReason}; an AbortableFiber returns that AbortError as an
|
|
162
|
-
* Err.
|
|
163
|
-
*
|
|
164
|
-
* The core is intentionally small: ordinary Task functions, a callable Run with
|
|
165
|
-
* closed-over state, Promise-backed Fibers, AbortSignal propagation, and
|
|
166
|
-
* JavaScript resource management. That minimal model still covers abort,
|
|
167
|
-
* cleanup, panic, dependency injection, monitoring, concurrency, and resource
|
|
168
|
-
* bracketing. The code is covered by carefully written, readable tests, so they
|
|
169
|
-
* serve as documentation too.
|
|
170
|
-
*
|
|
171
|
-
* ## Example
|
|
172
|
-
*
|
|
173
|
-
* This intentionally naive wrapper is useful for learning Task dependencies,
|
|
174
|
-
* Result errors, and native AbortSignal interop. Do not copy it as a production
|
|
175
|
-
* fetch helper: a Response is not a plain value — its unread body is tied to
|
|
176
|
-
* the request signal, which aborts when the Task settles. The returned Response
|
|
177
|
-
* escapes the scope that keeps it alive. Evolu's {@link fetch} exists to close
|
|
178
|
-
* this gap.
|
|
179
|
-
*
|
|
180
|
-
* `nativeFetch` is already a {@link RunDefaultDeps | default dependency}; this
|
|
181
|
-
* example declares the same shape locally to demonstrate how Tasks declare
|
|
182
|
-
* capabilities, and passes it to {@link createRun} to show that default
|
|
183
|
-
* dependencies can be replaced like custom ones.
|
|
14
|
+
* the await that waits for cleanup. {@link Run} makes both structural:
|
|
15
|
+
* `run(task)` registers every child before it starts, and the parent settles
|
|
16
|
+
* only after child cleanup finishes.
|
|
17
|
+
*
|
|
18
|
+
* Evolu models structured concurrency with ordinary JavaScript:
|
|
19
|
+
*
|
|
20
|
+
* - A {@link Task} describes an asynchronous operation and its dependencies.
|
|
21
|
+
* - A {@link Run} starts Tasks and owns their lifetimes.
|
|
22
|
+
* - A {@link Fiber} is the Promise-backed handle returned when a Run starts a
|
|
23
|
+
* Task.
|
|
24
|
+
* - An {@link AbortableFiber} adds explicit abort and async disposal.
|
|
25
|
+
*
|
|
26
|
+
* The runtime core is deliberately small: ordinary functions, a callable Run
|
|
27
|
+
* with closed-over state, Promise-backed Fibers, {@link AbortSignal}
|
|
28
|
+
* propagation, and JavaScript resource management. Together, these primitives
|
|
29
|
+
* provide abort, cleanup, defect handling, dependency injection, monitoring,
|
|
30
|
+
* concurrency, and resource bracketing.
|
|
31
|
+
*
|
|
32
|
+
* Tasks return domain success or failure as {@link Result}. Abort is control
|
|
33
|
+
* flow represented by {@link AbortError}. If a Task throws or rejects with
|
|
34
|
+
* anything else, that is a defect: the root Run reports it and shuts down its
|
|
35
|
+
* tree so code does not continue in a potentially invalid state.
|
|
184
36
|
*
|
|
185
37
|
* ```ts
|
|
186
38
|
* import {
|
|
187
|
-
* AbortError,
|
|
188
39
|
* createRun,
|
|
189
40
|
* err,
|
|
190
41
|
* ok,
|
|
191
|
-
* type AbortableFiber,
|
|
192
42
|
* type Result,
|
|
193
43
|
* type Task,
|
|
44
|
+
* type Typed,
|
|
194
45
|
* } from "@evolu/common";
|
|
195
46
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* readonly
|
|
47
|
+
* interface User {
|
|
48
|
+
* readonly id: string;
|
|
49
|
+
* readonly name: string;
|
|
199
50
|
* }
|
|
200
51
|
*
|
|
201
|
-
* interface
|
|
202
|
-
* readonly
|
|
203
|
-
* readonly error: unknown;
|
|
52
|
+
* interface Db {
|
|
53
|
+
* readonly usersById: ReadonlyMap<string, User>;
|
|
204
54
|
* }
|
|
205
55
|
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* async ({ deps, signal }) => {
|
|
210
|
-
* try {
|
|
211
|
-
* const response = await deps.nativeFetch(url, { signal });
|
|
212
|
-
* return ok(response);
|
|
213
|
-
* } catch (error) {
|
|
214
|
-
* if (AbortError.is(error)) throw error;
|
|
215
|
-
* return err({ type: "NaiveFetchError", error });
|
|
216
|
-
* }
|
|
217
|
-
* };
|
|
218
|
-
*
|
|
219
|
-
* const nativeFetch: typeof globalThis.fetch = async (input, init) => {
|
|
220
|
-
* if (String(input).endsWith("/123")) {
|
|
221
|
-
* return new Response(null, { status: 204 });
|
|
222
|
-
* }
|
|
223
|
-
*
|
|
224
|
-
* return new Promise<Response>((_resolve, reject) => {
|
|
225
|
-
* const { signal } = init ?? {};
|
|
226
|
-
* if (signal?.aborted) {
|
|
227
|
-
* reject(signal.reason);
|
|
228
|
-
* return;
|
|
229
|
-
* }
|
|
230
|
-
* signal?.addEventListener("abort", () => reject(signal.reason), {
|
|
231
|
-
* once: true,
|
|
232
|
-
* });
|
|
233
|
-
* });
|
|
234
|
-
* };
|
|
235
|
-
*
|
|
236
|
-
* // Provide dependencies at the composition root.
|
|
237
|
-
* const deps: NativeFetchDep = {
|
|
238
|
-
* nativeFetch,
|
|
239
|
-
* };
|
|
56
|
+
* interface DbDep {
|
|
57
|
+
* readonly db: Db;
|
|
58
|
+
* }
|
|
240
59
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
60
|
+
* const getUser =
|
|
61
|
+
* (id: string): Task<User, UserNotFoundError, DbDep> =>
|
|
62
|
+
* (run) => {
|
|
63
|
+
* const user = run.deps.db.usersById.get(id);
|
|
64
|
+
* return user ? ok(user) : err({ type: "UserNotFound", id });
|
|
65
|
+
* };
|
|
243
66
|
*
|
|
244
|
-
* //
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* expect(result.value.status).toBe(204);
|
|
249
|
-
*
|
|
250
|
-
* // So what is naive about it? The Response ok value.
|
|
251
|
-
* // Wrong: the Task settled, so its Run disposed and aborted `signal`.
|
|
252
|
-
* // The Response body is a live resource tied to that signal. Whether
|
|
253
|
-
* // this read fails immediately or appears to work depends on the
|
|
254
|
-
* // runtime and on how much of the body was already buffered — it is
|
|
255
|
-
* // timing-dependent behavior, not an API you can rely on.
|
|
256
|
-
* // await result.value.json();
|
|
257
|
-
*
|
|
258
|
-
* // Abort works when native fetch rejects with signal.reason. Some hosts use
|
|
259
|
-
* // their own abort error, which this naive wrapper does not normalize.
|
|
260
|
-
* const fiber = run.abortable(naiveFetch("/users/456"));
|
|
261
|
-
* fiber.abort();
|
|
262
|
-
* const abortResult = await fiber;
|
|
263
|
-
* expectTypeOf(fiber).toEqualTypeOf<
|
|
264
|
-
* AbortableFiber<Response, NaiveFetchError, NativeFetchDep>
|
|
265
|
-
* >();
|
|
266
|
-
* assert(!abortResult.ok);
|
|
267
|
-
* expect(AbortError.is(abortResult.error)).toBe(true);
|
|
268
|
-
* ```
|
|
67
|
+
* // Typed declares the `type` discriminant without repeating the property.
|
|
68
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
69
|
+
* readonly id: string;
|
|
70
|
+
* }
|
|
269
71
|
*
|
|
270
|
-
*
|
|
271
|
-
* bounded. It consumes the Response body inside the Task, while the request
|
|
272
|
-
* signal is still alive, and returns a plain value:
|
|
72
|
+
* const user: User = { id: "user-1", name: "Ada" };
|
|
273
73
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* type Result,
|
|
280
|
-
* } from "@evolu/common";
|
|
281
|
-
*
|
|
282
|
-
* const nativeFetch: typeof globalThis.fetch = async () =>
|
|
283
|
-
* new Response("readme");
|
|
284
|
-
* await using run = createRun({ nativeFetch });
|
|
74
|
+
* // Provide dependencies at the composition root. `await using` disposes the
|
|
75
|
+
* // Run and waits for its child Tasks before leaving this scope.
|
|
76
|
+
* await using run = createRun({
|
|
77
|
+
* db: { usersById: new Map([[user.id, user]]) },
|
|
78
|
+
* });
|
|
285
79
|
*
|
|
286
|
-
* const
|
|
287
|
-
* expectTypeOf(
|
|
288
|
-
* expectOk(
|
|
80
|
+
* const result = await run(getUser(user.id));
|
|
81
|
+
* expectTypeOf(result).toEqualTypeOf<Result<User, UserNotFoundError>>();
|
|
82
|
+
* expectOk(result, user);
|
|
289
83
|
* ```
|
|
290
84
|
*
|
|
291
85
|
* In composition roots, prefer the lifecycle API from the matching Evolu
|
|
@@ -297,76 +91,99 @@ import type {
|
|
|
297
91
|
*
|
|
298
92
|
* ## Composition
|
|
299
93
|
*
|
|
300
|
-
* | Category | Helper | Description
|
|
301
|
-
* | ------------ | ------------------------- |
|
|
302
|
-
* | Collection | {@link all} | Return Ok values or stop on first Err
|
|
303
|
-
* | | {@link allSettled} | Return every Task Result
|
|
304
|
-
* | | {@link each} | Handle each Task Result
|
|
305
|
-
* | Interop | {@link callback} | Wrap callback APIs
|
|
306
|
-
* | | {@link fetch} | Native fetch with bounded Response use
|
|
307
|
-
* | Timing | {@link sleep} | Pause execution
|
|
308
|
-
* | | {@link timeout} | Time-bounded execution
|
|
309
|
-
* | Resilience | {@link retry} | Retry domain errors with a schedule
|
|
310
|
-
* | Repetition | {@link repeat} | Repeat successes with a schedule
|
|
311
|
-
* | Racing | {@link any} | First Ok wins
|
|
312
|
-
* | | {@link race} | First settled Result wins
|
|
313
|
-
* | | {@link firstN} | First n Ok values win
|
|
314
|
-
* | | {@link firstNSettled} | First n Results win
|
|
315
|
-
* | Scheduling | {@link prioritized} | Assign scheduler priority
|
|
316
|
-
* | | {@link yieldNow} | Yield to the host scheduler
|
|
317
|
-
* | Lifetime | {@link daemon} | Run under root ownership
|
|
318
|
-
* |
|
|
319
|
-
* |
|
|
320
|
-
* | | {@link
|
|
94
|
+
* | Category | Helper | Description |
|
|
95
|
+
* | ------------ | ------------------------- | ----------------------------------------------------- |
|
|
96
|
+
* | Collection | {@link all} | Return {@link Ok} values or stop on first {@link Err} |
|
|
97
|
+
* | | {@link allSettled} | Return every Task Result |
|
|
98
|
+
* | | {@link each} | Handle each Task Result |
|
|
99
|
+
* | Interop | {@link callback} | Wrap callback APIs |
|
|
100
|
+
* | | {@link fetch} | Native fetch with bounded Response use |
|
|
101
|
+
* | Timing | {@link sleep} | Pause execution |
|
|
102
|
+
* | | {@link timeout} | Time-bounded execution |
|
|
103
|
+
* | Resilience | {@link retry} | Retry domain errors with a schedule |
|
|
104
|
+
* | Repetition | {@link repeat} | Repeat successes with a schedule |
|
|
105
|
+
* | Racing | {@link any} | First Ok wins |
|
|
106
|
+
* | | {@link race} | First settled Result wins |
|
|
107
|
+
* | | {@link firstN} | First n Ok values win |
|
|
108
|
+
* | | {@link firstNSettled} | First n Results win |
|
|
109
|
+
* | Scheduling | {@link prioritized} | Assign scheduler priority |
|
|
110
|
+
* | | {@link yieldNow} | Yield to the host scheduler |
|
|
111
|
+
* | Lifetime | {@link daemon} | Run under root ownership |
|
|
112
|
+
* | | {@link acquireUseRelease} | Bracket acquire, use, and release |
|
|
113
|
+
* | Abortability | {@link unabortable} | Mask abort after a Task starts |
|
|
114
|
+
* | | {@link unabortableMask} | Mask abort and selectively restore it |
|
|
321
115
|
*
|
|
322
116
|
* Helpers that process multiple Tasks run sequentially by default. Use a
|
|
323
117
|
* `concurrency` option to run more than one Task at a time.
|
|
324
118
|
*
|
|
119
|
+
* ### Sequential composition
|
|
120
|
+
*
|
|
325
121
|
* For ordinary sequential composition, use imperative code:
|
|
326
122
|
*
|
|
327
123
|
* ```ts
|
|
328
|
-
* import {
|
|
124
|
+
* import {
|
|
125
|
+
* createRun,
|
|
126
|
+
* err,
|
|
127
|
+
* ok,
|
|
128
|
+
* type Result,
|
|
129
|
+
* type Task,
|
|
130
|
+
* type Typed,
|
|
131
|
+
* } from "@evolu/common";
|
|
329
132
|
*
|
|
330
133
|
* interface User {
|
|
331
134
|
* readonly id: string;
|
|
332
135
|
* readonly profileId: string;
|
|
333
136
|
* }
|
|
137
|
+
*
|
|
334
138
|
* interface Profile {
|
|
335
139
|
* readonly id: string;
|
|
336
140
|
* }
|
|
337
141
|
*
|
|
338
|
-
* const
|
|
339
|
-
* (id: string): Task<User,
|
|
142
|
+
* const getUser =
|
|
143
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
340
144
|
* () =>
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
145
|
+
* id === "user-1"
|
|
146
|
+
* ? ok({ id, profileId: "profile-1" })
|
|
147
|
+
* : err({ type: "UserNotFound", id });
|
|
148
|
+
*
|
|
149
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
150
|
+
* readonly id: string;
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* const getProfile =
|
|
154
|
+
* (id: string): Task<Profile, ProfileNotFoundError> =>
|
|
344
155
|
* () =>
|
|
345
|
-
*
|
|
156
|
+
* id === "profile-1"
|
|
157
|
+
* ? ok({ id })
|
|
158
|
+
* : err({ type: "ProfileNotFound", id });
|
|
159
|
+
*
|
|
160
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {
|
|
161
|
+
* readonly id: string;
|
|
162
|
+
* }
|
|
346
163
|
*
|
|
347
|
-
* const
|
|
164
|
+
* const getUserWithProfile =
|
|
348
165
|
* (
|
|
349
166
|
* id: string,
|
|
350
167
|
* ): Task<
|
|
351
168
|
* { readonly user: User; readonly profile: Profile },
|
|
352
|
-
*
|
|
169
|
+
* UserNotFoundError | ProfileNotFoundError
|
|
353
170
|
* > =>
|
|
354
171
|
* async (run) => {
|
|
355
|
-
* const user = await run(
|
|
172
|
+
* const user = await run(getUser(id));
|
|
356
173
|
* if (!user.ok) return user;
|
|
357
174
|
*
|
|
358
|
-
* const profile = await run(
|
|
175
|
+
* const profile = await run(getProfile(user.value.profileId));
|
|
359
176
|
* if (!profile.ok) return profile;
|
|
360
177
|
*
|
|
361
178
|
* return ok({ user: user.value, profile: profile.value });
|
|
362
179
|
* };
|
|
363
180
|
*
|
|
364
181
|
* await using run = createRun();
|
|
365
|
-
* const result = await run(
|
|
182
|
+
* const result = await run(getUserWithProfile("user-1"));
|
|
366
183
|
* expectTypeOf(result).toEqualTypeOf<
|
|
367
184
|
* Result<
|
|
368
185
|
* { readonly user: User; readonly profile: Profile },
|
|
369
|
-
*
|
|
186
|
+
* UserNotFoundError | ProfileNotFoundError
|
|
370
187
|
* >
|
|
371
188
|
* >();
|
|
372
189
|
* expectOk(result, {
|
|
@@ -378,33 +195,13 @@ import type {
|
|
|
378
195
|
* Evolu intentionally avoids pipe APIs, chainable methods, and generator-based
|
|
379
196
|
* effect DSLs. Plain async/await with early returns is easier to read, review,
|
|
380
197
|
* and debug, and it lets TypeScript narrow Result values through ordinary
|
|
381
|
-
* control flow.
|
|
382
|
-
* in the {@link Result} FAQ.
|
|
198
|
+
* control flow.
|
|
383
199
|
*
|
|
384
|
-
* ###
|
|
200
|
+
* ### Resilient fetch
|
|
385
201
|
*
|
|
386
202
|
* {@link fetch} with a body mode already returns a plain value, so resilience is
|
|
387
|
-
* ordinary Task composition.
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* ```ts
|
|
391
|
-
* import {
|
|
392
|
-
* fetch,
|
|
393
|
-
* timeout,
|
|
394
|
-
* type FetchError,
|
|
395
|
-
* type Task,
|
|
396
|
-
* type TimeoutError,
|
|
397
|
-
* } from "@evolu/common";
|
|
398
|
-
*
|
|
399
|
-
* const fetchWithTimeout = (url: string) =>
|
|
400
|
-
* timeout(fetch(url, "text"), "30s");
|
|
401
|
-
*
|
|
402
|
-
* expectTypeOf(fetchWithTimeout).returns.toEqualTypeOf<
|
|
403
|
-
* Task<string, FetchError | TimeoutError>
|
|
404
|
-
* >();
|
|
405
|
-
* ```
|
|
406
|
-
*
|
|
407
|
-
* Add {@link retry} for recoverable domain failures:
|
|
203
|
+
* ordinary Task composition. Combine {@link timeout} and {@link retry} to bound
|
|
204
|
+
* each attempt and retry recoverable domain failures:
|
|
408
205
|
*
|
|
409
206
|
* ```ts
|
|
410
207
|
* import {
|
|
@@ -421,12 +218,9 @@ import type {
|
|
|
421
218
|
* type TimeoutError,
|
|
422
219
|
* } from "@evolu/common";
|
|
423
220
|
*
|
|
424
|
-
* const fetchWithTimeout = (url: string) =>
|
|
425
|
-
* timeout(fetch(url, "text"), "30s");
|
|
426
|
-
*
|
|
427
221
|
* const fetchWithRetry = (url: string) =>
|
|
428
222
|
* retry(
|
|
429
|
-
*
|
|
223
|
+
* timeout(fetch(url, "text"), "30s"),
|
|
430
224
|
* // A jittered, capped, limited exponential backoff.
|
|
431
225
|
* jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
432
226
|
* );
|
|
@@ -436,7 +230,9 @@ import type {
|
|
|
436
230
|
* >();
|
|
437
231
|
* ```
|
|
438
232
|
*
|
|
439
|
-
*
|
|
233
|
+
* ### Concurrent composition
|
|
234
|
+
*
|
|
235
|
+
* Run composed Tasks with a `concurrency` option and {@link all}:
|
|
440
236
|
*
|
|
441
237
|
* ```ts
|
|
442
238
|
* import { all, createRun, ok, sleep, type Task } from "@evolu/common";
|
|
@@ -466,23 +262,21 @@ import type {
|
|
|
466
262
|
* expect(maxActiveRequests).toBe(2);
|
|
467
263
|
* ```
|
|
468
264
|
*
|
|
469
|
-
* ## Concurrency Primitives
|
|
470
|
-
*
|
|
471
265
|
* Task helpers compose Tasks; concurrency primitives are stateful objects that
|
|
472
266
|
* coordinate Tasks across call sites. Create them with their `createX`
|
|
473
267
|
* factories and share them where coordination is needed.
|
|
474
268
|
*
|
|
475
|
-
* | Primitive | Description
|
|
476
|
-
* | ---------------------- |
|
|
477
|
-
* | {@link Deferred} | One-shot value resolved from outside
|
|
478
|
-
* | {@link Gate} | Block and release Tasks repeatedly
|
|
479
|
-
* | {@link Semaphore} | Limit concurrent Tasks with permits
|
|
480
|
-
* | {@link Mutex} | Run Tasks one at a time
|
|
481
|
-
* | {@link SemaphoreByKey} | Per-key permits with automatic cleanup
|
|
482
|
-
* | {@link MutexByKey} | Per-key one-at-a-time execution
|
|
483
|
-
* | {@link MutexRef} | Ref with serialized Task transitions
|
|
269
|
+
* | Primitive | Description |
|
|
270
|
+
* | ---------------------- | -------------------------------------------- |
|
|
271
|
+
* | {@link Deferred} | One-shot value resolved from outside |
|
|
272
|
+
* | {@link Gate} | Block and release Tasks repeatedly |
|
|
273
|
+
* | {@link Semaphore} | Limit concurrent Tasks with permits |
|
|
274
|
+
* | {@link Mutex} | Run Tasks one at a time |
|
|
275
|
+
* | {@link SemaphoreByKey} | Per-key permits with automatic cleanup |
|
|
276
|
+
* | {@link MutexByKey} | Per-key one-at-a-time execution |
|
|
277
|
+
* | {@link MutexRef} | {@link Ref} with serialized Task transitions |
|
|
484
278
|
*
|
|
485
|
-
* ## Dependency
|
|
279
|
+
* ## Dependency injection
|
|
486
280
|
*
|
|
487
281
|
* Task DI is
|
|
488
282
|
* {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}
|
|
@@ -496,7 +290,42 @@ import type {
|
|
|
496
290
|
* Use normal Task arguments for per-call values and `D` for capabilities,
|
|
497
291
|
* resources, or services shared by all code running inside a Run.
|
|
498
292
|
*
|
|
499
|
-
*
|
|
293
|
+
* ```ts
|
|
294
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
295
|
+
*
|
|
296
|
+
* interface GreetingFormatter {
|
|
297
|
+
* readonly format: (name: string) => string;
|
|
298
|
+
* }
|
|
299
|
+
*
|
|
300
|
+
* interface GreetingFormatterDep {
|
|
301
|
+
* readonly greetingFormatter: GreetingFormatter;
|
|
302
|
+
* }
|
|
303
|
+
*
|
|
304
|
+
* const greet =
|
|
305
|
+
* (name: string): Task<string, never, GreetingFormatterDep> =>
|
|
306
|
+
* (run) =>
|
|
307
|
+
* ok(run.deps.greetingFormatter.format(name));
|
|
308
|
+
*
|
|
309
|
+
* const formal: GreetingFormatter = {
|
|
310
|
+
* format: (name) => `Hello, ${name}`,
|
|
311
|
+
* };
|
|
312
|
+
* const casual: GreetingFormatter = {
|
|
313
|
+
* format: (name) => `Hi, ${name}`,
|
|
314
|
+
* };
|
|
315
|
+
*
|
|
316
|
+
* await using run = createRun({ greetingFormatter: formal });
|
|
317
|
+
*
|
|
318
|
+
* // Root dependencies are inherited.
|
|
319
|
+
* expectOk(await run(greet("Ada")), "Hello, Ada");
|
|
320
|
+
*
|
|
321
|
+
* // Child-specific dependencies replace the root's custom dependencies.
|
|
322
|
+
* expectOk(
|
|
323
|
+
* await run(greet("Ada"), { greetingFormatter: casual }),
|
|
324
|
+
* "Hi, Ada",
|
|
325
|
+
* );
|
|
326
|
+
* ```
|
|
327
|
+
*
|
|
328
|
+
* ### Default dependencies
|
|
500
329
|
*
|
|
501
330
|
* {@link createRun} provides default {@link RunDefaultDeps} available to all
|
|
502
331
|
* Tasks without declaring `D`:
|
|
@@ -509,62 +338,10 @@ import type {
|
|
|
509
338
|
* - {@link ReportDefect} — defect reporting
|
|
510
339
|
* - {@link Time} — current time
|
|
511
340
|
*
|
|
512
|
-
* For example, using Console:
|
|
513
|
-
*
|
|
514
|
-
* ```ts
|
|
515
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
516
|
-
*
|
|
517
|
-
* const myTask: Task<void> = async (run) => {
|
|
518
|
-
* const { console } = run.deps;
|
|
519
|
-
* console.log("started");
|
|
520
|
-
* return ok();
|
|
521
|
-
* };
|
|
522
|
-
*
|
|
523
|
-
* expectTypeOf(myTask).toEqualTypeOf<Task<void>>();
|
|
524
|
-
* await using run = createRun();
|
|
525
|
-
* expectOk(await run(myTask), undefined);
|
|
526
|
-
* ```
|
|
527
|
-
*
|
|
528
|
-
* Custom Console with formatted output:
|
|
529
|
-
*
|
|
530
|
-
* ```ts
|
|
531
|
-
* import {
|
|
532
|
-
* createConsoleArrayOutput,
|
|
533
|
-
* createConsole,
|
|
534
|
-
* createConsoleFormatter,
|
|
535
|
-
* createRun,
|
|
536
|
-
* type ConsoleEntry,
|
|
537
|
-
* } from "@evolu/common";
|
|
538
|
-
*
|
|
539
|
-
* const entries: Array<ConsoleEntry> = [];
|
|
540
|
-
* const formatter = createConsoleFormatter()({
|
|
541
|
-
* timestampFormat: "absolute",
|
|
542
|
-
* });
|
|
543
|
-
* const deps = {
|
|
544
|
-
* console: createConsole({
|
|
545
|
-
* output: createConsoleArrayOutput(entries),
|
|
546
|
-
* formatter,
|
|
547
|
-
* }),
|
|
548
|
-
* };
|
|
549
|
-
*
|
|
550
|
-
* await using run = createRun(deps);
|
|
551
|
-
* const console = run.deps.console.child("main");
|
|
552
|
-
*
|
|
553
|
-
* console.log("started");
|
|
554
|
-
* expect(console.name).toBe("main");
|
|
555
|
-
* expect(entries).toHaveLength(1);
|
|
556
|
-
* expect(entries[0]?.path).toEqual(["main"]);
|
|
557
|
-
* assert(entries[0]);
|
|
558
|
-
* const formattedArgs = formatter(entries[0]);
|
|
559
|
-
* // 21:20:25.588 [main] started
|
|
560
|
-
* expect(formattedArgs[0]).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3} \[main\]$/);
|
|
561
|
-
* expect(formattedArgs[1]).toBe("started");
|
|
562
|
-
* ```
|
|
563
|
-
*
|
|
564
341
|
* For testing, use {@link testCreateRun} to get deterministic, controllable
|
|
565
342
|
* implementations of all RunDefaultDeps.
|
|
566
343
|
*
|
|
567
|
-
* ## Resource
|
|
344
|
+
* ## Resource management
|
|
568
345
|
*
|
|
569
346
|
* JavaScript provides standard
|
|
570
347
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
|
|
@@ -578,7 +355,7 @@ import type {
|
|
|
578
355
|
* - Closure-held state bounded by a reusable {@link DisposableRun}:
|
|
579
356
|
* {@link DisposableRun.defer}
|
|
580
357
|
*
|
|
581
|
-
* ### Returning
|
|
358
|
+
* ### Returning resources from Tasks
|
|
582
359
|
*
|
|
583
360
|
* A Task that successfully returns a disposable resource transfers ownership of
|
|
584
361
|
* a live resource to its caller. The resource must remain live after the Task
|
|
@@ -593,32 +370,33 @@ import type {
|
|
|
593
370
|
* `undefined` should represent valid absence, not failure.
|
|
594
371
|
*
|
|
595
372
|
* ```ts
|
|
596
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
373
|
+
* import { createRun, ok, type Task, type Typed } from "@evolu/common";
|
|
597
374
|
*
|
|
598
|
-
* interface ConnectionError {
|
|
599
|
-
* readonly type: "ConnectionError";
|
|
600
|
-
* }
|
|
601
375
|
* interface Socket extends AsyncDisposable {
|
|
602
376
|
* readonly send: (message: string) => string;
|
|
603
377
|
* }
|
|
378
|
+
*
|
|
604
379
|
* interface Connection extends AsyncDisposable {
|
|
605
380
|
* readonly send: (message: string) => string;
|
|
606
381
|
* }
|
|
607
382
|
*
|
|
608
383
|
* let socketDisposed = false;
|
|
609
|
-
* const openSocket: Task<Socket,
|
|
384
|
+
* const openSocket: Task<Socket, ConnectionFailedError> = () =>
|
|
610
385
|
* ok({
|
|
611
386
|
* send: (message) => message,
|
|
612
387
|
* [Symbol.asyncDispose]: async () => {
|
|
613
388
|
* socketDisposed = true;
|
|
614
389
|
* },
|
|
615
390
|
* });
|
|
391
|
+
*
|
|
392
|
+
* interface ConnectionFailedError extends Typed<"ConnectionFailed"> {}
|
|
393
|
+
*
|
|
616
394
|
* const handshake =
|
|
617
|
-
* (_socket: Socket): Task<void,
|
|
395
|
+
* (_socket: Socket): Task<void, ConnectionFailedError> =>
|
|
618
396
|
* () =>
|
|
619
397
|
* ok();
|
|
620
398
|
*
|
|
621
|
-
* const createConnection: Task<Connection,
|
|
399
|
+
* const createConnection: Task<Connection, ConnectionFailedError> = async (
|
|
622
400
|
* run,
|
|
623
401
|
* ) => {
|
|
624
402
|
* await using disposer = new AsyncDisposableStack();
|
|
@@ -646,95 +424,15 @@ import type {
|
|
|
646
424
|
* expect(socketDisposed).toBe(true);
|
|
647
425
|
* ```
|
|
648
426
|
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* ```ts
|
|
653
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
654
|
-
*
|
|
655
|
-
* interface Foo extends AsyncDisposable {
|
|
656
|
-
* readonly value: string;
|
|
657
|
-
* }
|
|
658
|
-
*
|
|
659
|
-
* let disposed = false;
|
|
660
|
-
* const createFoo = (): Task<Foo> => () =>
|
|
661
|
-
* ok({
|
|
662
|
-
* value: "foo",
|
|
663
|
-
* [Symbol.asyncDispose]: async () => {
|
|
664
|
-
* disposed = true;
|
|
665
|
-
* },
|
|
666
|
-
* });
|
|
667
|
-
*
|
|
668
|
-
* await using run = createRun();
|
|
669
|
-
* {
|
|
670
|
-
* await using foo = await run.ok(createFoo());
|
|
671
|
-
* expect(foo.value).toBe("foo");
|
|
672
|
-
* }
|
|
673
|
-
* expect(disposed).toBe(true);
|
|
674
|
-
* ```
|
|
675
|
-
*
|
|
676
|
-
* Use {@link acquireUseRelease} when acquisition and release are separate
|
|
677
|
-
* operations rather than a disposable value:
|
|
678
|
-
*
|
|
679
|
-
* ```ts
|
|
680
|
-
* import {
|
|
681
|
-
* acquireUseRelease,
|
|
682
|
-
* createRun,
|
|
683
|
-
* ok,
|
|
684
|
-
* type Task,
|
|
685
|
-
* } from "@evolu/common";
|
|
686
|
-
*
|
|
687
|
-
* interface User {
|
|
688
|
-
* readonly id: string;
|
|
689
|
-
* readonly name: string;
|
|
690
|
-
* }
|
|
691
|
-
*
|
|
692
|
-
* interface Connection {
|
|
693
|
-
* readonly loadUser: (id: string) => User;
|
|
694
|
-
* }
|
|
695
|
-
*
|
|
696
|
-
* const openConnection: Task<Connection> = () =>
|
|
697
|
-
* ok({ loadUser: (id) => ({ id, name: "Ada" }) });
|
|
698
|
-
*
|
|
699
|
-
* const loadUser =
|
|
700
|
-
* (connection: Connection): Task<User> =>
|
|
701
|
-
* () =>
|
|
702
|
-
* ok(connection.loadUser("user-1"));
|
|
703
|
-
*
|
|
704
|
-
* let connectionClosed = false;
|
|
705
|
-
* const closeConnection =
|
|
706
|
-
* (_connection: Connection): Task<void> =>
|
|
707
|
-
* () => {
|
|
708
|
-
* connectionClosed = true;
|
|
709
|
-
* return ok();
|
|
710
|
-
* };
|
|
711
|
-
*
|
|
712
|
-
* const queryUser = acquireUseRelease(
|
|
713
|
-
* openConnection,
|
|
714
|
-
* loadUser,
|
|
715
|
-
* closeConnection,
|
|
716
|
-
* );
|
|
717
|
-
*
|
|
718
|
-
* await using run = createRun();
|
|
719
|
-
* const result = await run(queryUser);
|
|
720
|
-
* expectOk(result, { id: "user-1", name: "Ada" });
|
|
721
|
-
* expect(connectionClosed).toBe(true);
|
|
722
|
-
* ```
|
|
427
|
+
* Use {@link Run.ok} with `await using` when an infallible Task returns a
|
|
428
|
+
* disposable value. Use {@link acquireUseRelease} when acquisition and release
|
|
429
|
+
* are separate operations rather than a disposable value.
|
|
723
430
|
*
|
|
724
431
|
* ## Awaitable
|
|
725
432
|
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* type Awaitable<T> = T | PromiseLike<T>;
|
|
730
|
-
*
|
|
731
|
-
* expectTypeOf<Awaitable<number>>().toEqualTypeOf<
|
|
732
|
-
* EvoluAwaitable<number>
|
|
733
|
-
* >();
|
|
734
|
-
* ```
|
|
735
|
-
*
|
|
736
|
-
* Even though {@link Task} returns {@link Awaitable}, allowing sync or async
|
|
737
|
-
* results, {@link Run} is always async. This is a deliberate design choice:
|
|
433
|
+
* A {@link Task} returns {@link Awaitable}, so its body may produce a
|
|
434
|
+
* {@link Result} immediately or asynchronously. {@link Run} is always async and
|
|
435
|
+
* returns a {@link Fiber}; callers use the same ownership model either way.
|
|
738
436
|
*
|
|
739
437
|
* - **Sync** → {@link Result}, native `using` / `DisposableStack`
|
|
740
438
|
* - **Async** → Task, Run, {@link Fiber}, `await using` / `AsyncDisposableStack`
|
|
@@ -743,39 +441,36 @@ import type {
|
|
|
743
441
|
* decomposition. Calling `run(task)` always creates a child Run by design. Use
|
|
744
442
|
* ordinary promises when an async operation does not need its own Run.
|
|
745
443
|
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
* Sync functions should be fast, so there is no need to monitor them. They
|
|
759
|
-
* should take values, not dependencies — following the
|
|
444
|
+
* A unified sync/async effect API is technically possible. It can detect
|
|
445
|
+
* Promise-like values with {@link isPromiseLike}, dispose synchronous resources
|
|
446
|
+
* first, continue with asynchronous disposal when necessary, and track whether
|
|
447
|
+
* callers must await the result. Evolu deliberately keeps the two models
|
|
448
|
+
* separate instead: plain functions and Result for synchronous code, Task and
|
|
449
|
+
* Run for asynchronous ownership. Most effects involve inherently asynchronous
|
|
450
|
+
* I/O, while synchronous code benefits from a simpler API and no Task
|
|
451
|
+
* overhead.
|
|
452
|
+
*
|
|
453
|
+
* Keep synchronous computation as plain functions returning Result. Prefer
|
|
454
|
+
* passing values rather than dependencies, following the
|
|
760
455
|
* {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
|
|
761
456
|
* pattern where impure code gathers data, pure functions process it, and impure
|
|
762
|
-
* code performs effects with the result.
|
|
763
|
-
*
|
|
764
|
-
* {@link Random} could instead accept {@link RandomNumber} as a value.
|
|
457
|
+
* code performs effects with the result. For example, a pure function can
|
|
458
|
+
* accept a {@link RandomNumber} value instead of depending on {@link Random}.
|
|
765
459
|
*
|
|
766
|
-
*
|
|
767
|
-
* or complex cryptography belong in
|
|
768
|
-
*
|
|
769
|
-
* monitoring.
|
|
770
|
-
* call to the worker provides it.
|
|
460
|
+
* Large CPU-bound operations, such as parsing large JSON, sorting millions of
|
|
461
|
+
* items, or complex cryptography, belong in a worker. Model the asynchronous
|
|
462
|
+
* call to that worker as a Task so Run can provide timeout, abort, cleanup, and
|
|
463
|
+
* monitoring.
|
|
771
464
|
*
|
|
772
465
|
* ## Glossary
|
|
773
466
|
*
|
|
774
|
-
* - **Defect** — a thrown
|
|
467
|
+
* - **Defect** — a thrown or rejected value other than {@link AbortError}, rather
|
|
468
|
+
* than a declared {@link Result} error.
|
|
775
469
|
* - **Outcome** — a Fiber's settlement: resolution with the Task {@link Result},
|
|
776
|
-
* or rejection with {@link AbortError}.
|
|
777
|
-
*
|
|
778
|
-
*
|
|
470
|
+
* or rejection with {@link AbortError}. The original defect is reported
|
|
471
|
+
* through {@link ReportDefectDep} whether or not the Fiber is observed; the
|
|
472
|
+
* Fiber boundary represents the panic with AbortError whose reason is
|
|
473
|
+
* {@link PanicAbortReason}.
|
|
779
474
|
* - **Create** — construct a new value or a resource.
|
|
780
475
|
* - **Acquire** — obtain a usable resource. Acquisition may create a new
|
|
781
476
|
* resource, borrow one, open one, or take a lease/lock.
|
|
@@ -783,7 +478,7 @@ import type {
|
|
|
783
478
|
* pairs with acquire and need not mean disposal; examples include unlock,
|
|
784
479
|
* logout, or returning a pooled resource.
|
|
785
480
|
* - **Dispose / disposal** — owner-driven resource finalization via JavaScript
|
|
786
|
-
* management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
|
|
481
|
+
* resource management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
|
|
787
482
|
* `AsyncDisposableStack`).
|
|
788
483
|
*
|
|
789
484
|
* ## FAQ
|
|
@@ -821,12 +516,11 @@ import type {
|
|
|
821
516
|
*
|
|
822
517
|
* ### What should Task code do with defects?
|
|
823
518
|
*
|
|
824
|
-
* Nothing. Once a defect
|
|
825
|
-
* Tasks are aborted, and the Run tree shuts down. If
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* be useful anyway.
|
|
519
|
+
* Nothing. Once a defect reaches the {@link Run}, it is too late: the root Run
|
|
520
|
+
* panics, running Tasks are aborted, and the Run tree shuts down. If an
|
|
521
|
+
* operation can throw or reject for a recoverable reason, wrap that operation
|
|
522
|
+
* with {@link trySync} or {@link tryAsync} so the failure becomes a typed
|
|
523
|
+
* {@link Result} error. Let unrecoverable failures propagate as defects.
|
|
830
524
|
*
|
|
831
525
|
* ### Why does a defect panic the whole Run tree?
|
|
832
526
|
*
|
|
@@ -869,7 +563,9 @@ import type {
|
|
|
869
563
|
*
|
|
870
564
|
* Task follows the opposite approach: Tasks are ordinary async functions, Run
|
|
871
565
|
* owns lifetimes and scoped context, {@link Result} carries expected domain
|
|
872
|
-
* errors, and defects keep real exceptions with real stacks.
|
|
566
|
+
* errors, and defects keep real exceptions with real stacks. Result propagation
|
|
567
|
+
* is explicit at each async boundary, so TypeScript narrows it through ordinary
|
|
568
|
+
* control flow and readers can see where an error is handled or returned.
|
|
873
569
|
*
|
|
874
570
|
* ### Are recursive Tasks stack-safe?
|
|
875
571
|
*
|
|
@@ -889,33 +585,32 @@ import type {
|
|
|
889
585
|
* readonly children: ReadonlyArray<TreeNode>;
|
|
890
586
|
* }
|
|
891
587
|
*
|
|
892
|
-
* const visited: Array<string> = [];
|
|
893
|
-
* const visit = (node: TreeNode): void => {
|
|
894
|
-
* visited.push(node.value);
|
|
895
|
-
* };
|
|
896
|
-
*
|
|
897
588
|
* const visitTree =
|
|
898
|
-
* (root: TreeNode): Task<
|
|
589
|
+
* (root: TreeNode): Task<ReadonlyArray<string>> =>
|
|
899
590
|
* () => {
|
|
900
591
|
* const remaining = [root];
|
|
592
|
+
* const visited: Array<string> = [];
|
|
901
593
|
*
|
|
902
594
|
* while (remaining.length > 0) {
|
|
903
595
|
* const node = remaining.pop();
|
|
904
596
|
* if (!node) continue;
|
|
905
|
-
*
|
|
597
|
+
* visited.push(node.value);
|
|
906
598
|
* for (const child of node.children) remaining.push(child);
|
|
907
599
|
* }
|
|
908
600
|
*
|
|
909
|
-
* return ok();
|
|
601
|
+
* return ok(visited);
|
|
910
602
|
* };
|
|
911
603
|
*
|
|
912
|
-
* const tree: TreeNode = {
|
|
913
|
-
* value: "root",
|
|
914
|
-
* children: [{ value: "child", children: [] }],
|
|
915
|
-
* };
|
|
916
604
|
* await using run = createRun();
|
|
917
|
-
* expectOk(
|
|
918
|
-
*
|
|
605
|
+
* expectOk(
|
|
606
|
+
* await run(
|
|
607
|
+
* visitTree({
|
|
608
|
+
* value: "root",
|
|
609
|
+
* children: [{ value: "child", children: [] }],
|
|
610
|
+
* }),
|
|
611
|
+
* ),
|
|
612
|
+
* ["root", "child"],
|
|
613
|
+
* );
|
|
919
614
|
* ```
|
|
920
615
|
*
|
|
921
616
|
* Task favors direct native execution, `async`/`await`, and native tooling over
|
|
@@ -924,35 +619,12 @@ import type {
|
|
|
924
619
|
* periodically await {@link yieldNow} for cooperative scheduling, and move
|
|
925
620
|
* CPU-bound work to a worker.
|
|
926
621
|
*
|
|
927
|
-
* ###
|
|
928
|
-
*
|
|
929
|
-
* Task uses native TypeScript control flow so each async boundary and error
|
|
930
|
-
* propagation point is visible.
|
|
622
|
+
* ### Should a Task be called directly?
|
|
931
623
|
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* const greetUser: Task<string, "LoadUserError"> = async (run) => {
|
|
937
|
-
* const user = await run(loadUser);
|
|
938
|
-
* if (!user.ok) return user;
|
|
939
|
-
* return ok(`Hello, ${user.value}`);
|
|
940
|
-
* };
|
|
941
|
-
*
|
|
942
|
-
* await using run = createRun();
|
|
943
|
-
* expectOk(await run(greetUser), "Hello, Ada");
|
|
944
|
-
* ```
|
|
945
|
-
*
|
|
946
|
-
* This is slightly more verbose than fluent or generator-based syntax, but it's
|
|
947
|
-
* simple to read, easy to debug, friendly to TypeScript narrowing, and works
|
|
948
|
-
* well with generated code.
|
|
949
|
-
*
|
|
950
|
-
* ### Can a Task be called directly?
|
|
951
|
-
*
|
|
952
|
-
* Yes. A direct call, `task(run)`, uses the current Run instead of creating a
|
|
953
|
-
* child Run, so it bypasses child lifetime tracking, scheduling metadata, and
|
|
954
|
-
* child disposal boundaries. It is reserved for Task internals that explicitly
|
|
955
|
-
* need same-Run execution; use `run(task)` in application code.
|
|
624
|
+
* Only inside Task internals that explicitly require same-Run execution. A
|
|
625
|
+
* direct call, `task(run)`, uses the current Run instead of creating a child
|
|
626
|
+
* Run, so it bypasses child lifetime tracking, scheduling metadata, and child
|
|
627
|
+
* disposal boundaries. Application code should use `run(task)`.
|
|
956
628
|
*
|
|
957
629
|
* ### Where are fork and join?
|
|
958
630
|
*
|
|
@@ -960,16 +632,139 @@ import type {
|
|
|
960
632
|
* {@link Fiber}. Awaiting or returning that Fiber is join: it makes the child
|
|
961
633
|
* Result or rejection part of the parent Task control flow.
|
|
962
634
|
*
|
|
963
|
-
* ### What runtime features does Task require?
|
|
635
|
+
* ### What runtime features does Task require?
|
|
636
|
+
*
|
|
637
|
+
* Task uses modern JavaScript APIs such as `Promise.withResolvers`,
|
|
638
|
+
* `AbortSignal.throwIfAborted`, `Symbol.dispose`, `Symbol.asyncDispose`,
|
|
639
|
+
* `DisposableStack`, and `AsyncDisposableStack`. Evolu provides polyfills for
|
|
640
|
+
* supported runtimes that need them: call `installPolyfills` from
|
|
641
|
+
* `@evolu/common/polyfills`, or from the platform package such as
|
|
642
|
+
* `@evolu/react-native/polyfills`. The `using` and `await using` syntax is
|
|
643
|
+
* emitted by TypeScript; the polyfills provide the runtime resource-management
|
|
644
|
+
* globals.
|
|
645
|
+
*
|
|
646
|
+
* @module
|
|
647
|
+
*/
|
|
648
|
+
import {
|
|
649
|
+
emptyArray,
|
|
650
|
+
isNonEmptyArray,
|
|
651
|
+
mapArray,
|
|
652
|
+
type NonEmptyReadonlyArray,
|
|
653
|
+
} from "./Array.ts";
|
|
654
|
+
import {
|
|
655
|
+
assert,
|
|
656
|
+
assertNonEmptyReadonlyArray,
|
|
657
|
+
assertNonNullable,
|
|
658
|
+
assertNotDisposed,
|
|
659
|
+
} from "./Assert.ts";
|
|
660
|
+
import type { Brand } from "./Brand.ts";
|
|
661
|
+
import {
|
|
662
|
+
createConsole,
|
|
663
|
+
testCreateConsole,
|
|
664
|
+
type Console,
|
|
665
|
+
type ConsoleDep,
|
|
666
|
+
type TestConsole,
|
|
667
|
+
type TestConsoleDep,
|
|
668
|
+
} from "./Console.ts";
|
|
669
|
+
import {
|
|
670
|
+
createRandomBytes,
|
|
671
|
+
testCreateRandomBytes,
|
|
672
|
+
type RandomBytes,
|
|
673
|
+
type RandomBytesDep,
|
|
674
|
+
} from "./Crypto.ts";
|
|
675
|
+
import { eqArrayStrict } from "./Eq.ts";
|
|
676
|
+
import { constTrue, constVoid, identity } from "./Function.ts";
|
|
677
|
+
import type { fetch, NativeFetch, NativeFetchDep } from "./Http.ts";
|
|
678
|
+
import {
|
|
679
|
+
createLeakDetector,
|
|
680
|
+
noopLeakDetector,
|
|
681
|
+
testCreateLeakDetector,
|
|
682
|
+
type LeakDetector,
|
|
683
|
+
type LeakDetectorDep,
|
|
684
|
+
type TestLeakDetectorDep,
|
|
685
|
+
} from "./LeakDetector.ts";
|
|
686
|
+
import { createLookupMap, type Lookup, type LookupOption } from "./Lookup.ts";
|
|
687
|
+
import { decrement, increment, type Int1To100OrPositiveInt } from "./Number.ts";
|
|
688
|
+
import {
|
|
689
|
+
emptyRecord,
|
|
690
|
+
mapObject,
|
|
691
|
+
objectFromEntries,
|
|
692
|
+
objectToEntries,
|
|
693
|
+
} from "./Object.ts";
|
|
694
|
+
import { none, some, type Option } from "./Option.ts";
|
|
695
|
+
import { isDev } from "./Platform.ts";
|
|
696
|
+
import {
|
|
697
|
+
createRandom,
|
|
698
|
+
testCreateRandom,
|
|
699
|
+
testCreateRandomLib,
|
|
700
|
+
type Random,
|
|
701
|
+
type RandomDep,
|
|
702
|
+
type RandomLibDep,
|
|
703
|
+
type RandomNumber,
|
|
704
|
+
} from "./Random.ts";
|
|
705
|
+
import { createRef, type Ref } from "./Ref.ts";
|
|
706
|
+
import {
|
|
707
|
+
err,
|
|
708
|
+
getOk,
|
|
709
|
+
getOrThrow,
|
|
710
|
+
ok,
|
|
711
|
+
type AnyResult,
|
|
712
|
+
type Done,
|
|
713
|
+
type Err,
|
|
714
|
+
type Ok,
|
|
715
|
+
type Result,
|
|
716
|
+
type tryAsync,
|
|
717
|
+
type trySync,
|
|
718
|
+
} from "./Result.ts";
|
|
719
|
+
import type { Schedule, ScheduleStep } from "./Schedule.ts";
|
|
720
|
+
import { emptySet } from "./Set.ts";
|
|
721
|
+
import {
|
|
722
|
+
createTime,
|
|
723
|
+
PositiveMillis,
|
|
724
|
+
testCreateTime,
|
|
725
|
+
type Millis,
|
|
726
|
+
type PositiveDuration,
|
|
727
|
+
type Time,
|
|
728
|
+
type TimeDep,
|
|
729
|
+
type TestTimeDep,
|
|
730
|
+
} from "./Time.ts";
|
|
731
|
+
import {
|
|
732
|
+
assertType,
|
|
733
|
+
createId,
|
|
734
|
+
type InferType,
|
|
735
|
+
maxPositiveInt,
|
|
736
|
+
NonNegativeInt,
|
|
737
|
+
object,
|
|
738
|
+
onePositiveInt,
|
|
739
|
+
PositiveInt,
|
|
740
|
+
record,
|
|
741
|
+
String,
|
|
742
|
+
typed,
|
|
743
|
+
Unknown,
|
|
744
|
+
UnknownResult,
|
|
745
|
+
type Id,
|
|
746
|
+
type ObjectType,
|
|
747
|
+
type RecordType,
|
|
748
|
+
type Typed,
|
|
749
|
+
type TypedType,
|
|
750
|
+
} from "./Type.ts";
|
|
751
|
+
import type {
|
|
752
|
+
Awaitable,
|
|
753
|
+
isPromiseLike,
|
|
754
|
+
Writable,
|
|
755
|
+
ParameterIntersection,
|
|
756
|
+
Predicate,
|
|
757
|
+
} from "./Types.ts";
|
|
758
|
+
|
|
759
|
+
// Core
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* An operation run by {@link Run} that returns a {@link Result} synchronously or
|
|
763
|
+
* asynchronously and declares its dependencies through `D`.
|
|
764
|
+
*
|
|
765
|
+
* Its return type is {@link Awaitable}.
|
|
964
766
|
*
|
|
965
|
-
*
|
|
966
|
-
* `AbortSignal.throwIfAborted`, `Symbol.dispose`, `Symbol.asyncDispose`,
|
|
967
|
-
* `DisposableStack`, and `AsyncDisposableStack`. Evolu provides polyfills for
|
|
968
|
-
* supported runtimes that need them: call `installPolyfills` from
|
|
969
|
-
* `@evolu/common/polyfills`, or from the platform package such as
|
|
970
|
-
* `@evolu/react-native/polyfills`. The `using` and `await using` syntax is
|
|
971
|
-
* emitted by TypeScript; the polyfills provide the runtime resource-management
|
|
972
|
-
* globals.
|
|
767
|
+
* See the {@link @evolu/common!Task | Task overview}.
|
|
973
768
|
*
|
|
974
769
|
* @group Core
|
|
975
770
|
*/
|
|
@@ -985,7 +780,7 @@ export type Task<T, E = never, D = unknown> = (
|
|
|
985
780
|
export type AnyTask = Task<any, any, any>;
|
|
986
781
|
|
|
987
782
|
/**
|
|
988
|
-
* Extracts the Ok value type from a {@link Task}.
|
|
783
|
+
* Extracts the {@link Ok} value type from a {@link Task}.
|
|
989
784
|
*
|
|
990
785
|
* @group Type utilities
|
|
991
786
|
*/
|
|
@@ -993,7 +788,7 @@ export type InferTaskOk<TTask extends AnyTask> =
|
|
|
993
788
|
TTask extends Task<infer T, any, any> ? T : never;
|
|
994
789
|
|
|
995
790
|
/**
|
|
996
|
-
* Extracts the Result error type from a {@link Task}.
|
|
791
|
+
* Extracts the {@link Result} error type from a {@link Task}.
|
|
997
792
|
*
|
|
998
793
|
* @group Type utilities
|
|
999
794
|
*/
|
|
@@ -1009,10 +804,11 @@ export type InferTaskDeps<TTask extends AnyTask> =
|
|
|
1009
804
|
TTask extends Task<any, any, infer D> ? D : never;
|
|
1010
805
|
|
|
1011
806
|
/**
|
|
1012
|
-
* A {@link Task} that can return a value, signal done, or return a Result
|
|
807
|
+
* A {@link Task} that can return a value, signal done, or return a {@link Result}
|
|
808
|
+
* error.
|
|
1013
809
|
*
|
|
1014
|
-
* Use for pull-based protocols where
|
|
1015
|
-
* than an error.
|
|
810
|
+
* Use for pull-based protocols where {@link Done | Done<D>} signals normal
|
|
811
|
+
* completion rather than an error.
|
|
1016
812
|
*
|
|
1017
813
|
* @group Core
|
|
1018
814
|
*/
|
|
@@ -1038,8 +834,8 @@ export type InferTaskDone<TTask extends AnyTask> =
|
|
|
1038
834
|
* A {@link Task} whose error type is not `never`.
|
|
1039
835
|
*
|
|
1040
836
|
* Used by {@link Run.orThrow} to accept only Tasks that can return a declared
|
|
1041
|
-
* {@link Err}. Tasks without declared Result errors should use
|
|
1042
|
-
* instead.
|
|
837
|
+
* {@link Err}. Tasks without declared {@link Result} errors should use
|
|
838
|
+
* {@link Run.ok} instead.
|
|
1043
839
|
*
|
|
1044
840
|
* @group Type utilities
|
|
1045
841
|
*/
|
|
@@ -1064,7 +860,7 @@ export type TaskWithError<TTask extends AnyTask> = TTask &
|
|
|
1064
860
|
* the child Fiber from the parent. If a parent Task returns before awaiting or
|
|
1065
861
|
* returning a child Fiber, cleanup still waits for the child. A child defect
|
|
1066
862
|
* during that cleanup panics and aborts the root Run, but the parent Fiber
|
|
1067
|
-
* keeps the Result already returned by the parent Task.
|
|
863
|
+
* keeps the {@link Result} already returned by the parent Task.
|
|
1068
864
|
*
|
|
1069
865
|
* Disposing a Run requests abort and prevents new child Tasks from starting.
|
|
1070
866
|
* Async disposal waits for current children to settle. Abort requests propagate
|
|
@@ -1103,9 +899,7 @@ export interface Run<D = unknown> {
|
|
|
1103
899
|
* defects are still reported. Use {@link Run.daemon} for work that should
|
|
1104
900
|
* outlive the current Task.
|
|
1105
901
|
*
|
|
1106
|
-
* The
|
|
1107
|
-
* Default deps ({@link RunDefaultDeps}) are inherited unless replaced with
|
|
1108
|
-
* assignable alternatives.
|
|
902
|
+
* The Task uses this Run's current dependencies.
|
|
1109
903
|
*
|
|
1110
904
|
* The Fiber rejects when the Task observes abort by throwing
|
|
1111
905
|
* {@link AbortError}. It also rejects with AbortError whose reason is
|
|
@@ -1125,26 +919,33 @@ export interface Run<D = unknown> {
|
|
|
1125
919
|
* interface Db {
|
|
1126
920
|
* readonly name: string;
|
|
1127
921
|
* }
|
|
922
|
+
*
|
|
1128
923
|
* interface DbDep {
|
|
1129
924
|
* readonly db: Db;
|
|
1130
925
|
* }
|
|
1131
926
|
*
|
|
1132
927
|
* const db: Db = { name: "main" };
|
|
1133
928
|
* const loadUser: Task<string> = () => ok("Ada");
|
|
1134
|
-
* const saveUser: Task<void, never, DbDep> = (
|
|
1135
|
-
* expect(deps.db).toBe(db);
|
|
929
|
+
* const saveUser: Task<void, never, DbDep> = (run) => {
|
|
930
|
+
* expect(run.deps.db).toBe(db);
|
|
1136
931
|
* return ok();
|
|
1137
932
|
* };
|
|
1138
933
|
*
|
|
1139
|
-
* await using run = createRun();
|
|
934
|
+
* await using run = createRun({ db });
|
|
1140
935
|
* const userResult = await run(loadUser);
|
|
1141
|
-
* const savedResult = await run(saveUser
|
|
936
|
+
* const savedResult = await run(saveUser);
|
|
1142
937
|
* expectOk(userResult, "Ada");
|
|
1143
938
|
* expectOk(savedResult, undefined);
|
|
1144
939
|
* ```
|
|
1145
940
|
*/
|
|
1146
941
|
<T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
|
|
1147
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Starts a {@link Task} with replacement custom dependencies.
|
|
945
|
+
*
|
|
946
|
+
* Default deps ({@link RunDefaultDeps}) are inherited unless replaced with
|
|
947
|
+
* assignable alternatives.
|
|
948
|
+
*/
|
|
1148
949
|
<T, E, Deps extends object>(
|
|
1149
950
|
task: Task<T, E, Deps>,
|
|
1150
951
|
deps: RunCustomDeps<Deps>,
|
|
@@ -1157,6 +958,20 @@ export interface Run<D = unknown> {
|
|
|
1157
958
|
* This is the Task equivalent of {@link getOrThrow}. Use it where a declared
|
|
1158
959
|
* Result error should crash the current flow instead of being handled
|
|
1159
960
|
* locally.
|
|
961
|
+
*
|
|
962
|
+
* ### Example
|
|
963
|
+
*
|
|
964
|
+
* ```ts
|
|
965
|
+
* import { createRun, ok, type Task, type Typed } from "@evolu/common";
|
|
966
|
+
*
|
|
967
|
+
* const loadConfig: Task<string, ConfigInvalidError> = () =>
|
|
968
|
+
* ok("config");
|
|
969
|
+
*
|
|
970
|
+
* interface ConfigInvalidError extends Typed<"ConfigInvalid"> {}
|
|
971
|
+
*
|
|
972
|
+
* await using run = createRun();
|
|
973
|
+
* expect(await run.orThrow(loadConfig)).toBe("config");
|
|
974
|
+
* ```
|
|
1160
975
|
*/
|
|
1161
976
|
readonly orThrow: {
|
|
1162
977
|
<TTask extends Task<any, any, D>>(
|
|
@@ -1169,9 +984,36 @@ export interface Run<D = unknown> {
|
|
|
1169
984
|
};
|
|
1170
985
|
|
|
1171
986
|
/**
|
|
1172
|
-
* Runs a {@link Task} whose error type is `never` and returns its Ok
|
|
987
|
+
* Runs a {@link Task} whose error type is `never` and returns its {@link Ok}
|
|
988
|
+
* value.
|
|
1173
989
|
*
|
|
1174
990
|
* This is the Task equivalent of {@link getOk}.
|
|
991
|
+
*
|
|
992
|
+
* ### Example
|
|
993
|
+
*
|
|
994
|
+
* ```ts
|
|
995
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
996
|
+
*
|
|
997
|
+
* interface Resource extends AsyncDisposable {
|
|
998
|
+
* readonly value: string;
|
|
999
|
+
* }
|
|
1000
|
+
*
|
|
1001
|
+
* let disposed = false;
|
|
1002
|
+
* const openResource: Task<Resource> = () =>
|
|
1003
|
+
* ok({
|
|
1004
|
+
* value: "resource",
|
|
1005
|
+
* [Symbol.asyncDispose]: async () => {
|
|
1006
|
+
* disposed = true;
|
|
1007
|
+
* },
|
|
1008
|
+
* });
|
|
1009
|
+
*
|
|
1010
|
+
* await using run = createRun();
|
|
1011
|
+
* {
|
|
1012
|
+
* await using resource = await run.ok(openResource);
|
|
1013
|
+
* expect(resource.value).toBe("resource");
|
|
1014
|
+
* }
|
|
1015
|
+
* expect(disposed).toBe(true);
|
|
1016
|
+
* ```
|
|
1175
1017
|
*/
|
|
1176
1018
|
readonly ok: {
|
|
1177
1019
|
<T>(task: Task<T, never, D>): Promise<T>;
|
|
@@ -1210,8 +1052,9 @@ export interface Run<D = unknown> {
|
|
|
1210
1052
|
* interface DbDep {
|
|
1211
1053
|
* readonly db: { readonly name: string };
|
|
1212
1054
|
* }
|
|
1055
|
+
*
|
|
1213
1056
|
* const db = { name: "main" };
|
|
1214
|
-
* const loadUser: Task<string,
|
|
1057
|
+
* const loadUser: Task<string, never, DbDep> = async (run) => {
|
|
1215
1058
|
* await run.ok(sleep("1s"));
|
|
1216
1059
|
* return ok(run.deps.db.name);
|
|
1217
1060
|
* };
|
|
@@ -1219,7 +1062,7 @@ export interface Run<D = unknown> {
|
|
|
1219
1062
|
* await using run = createRun();
|
|
1220
1063
|
* const fiber = run.abortable(loadUser, { db });
|
|
1221
1064
|
* expectTypeOf(fiber).toEqualTypeOf<
|
|
1222
|
-
* AbortableFiber<string,
|
|
1065
|
+
* AbortableFiber<string, never, DbDep>
|
|
1223
1066
|
* >();
|
|
1224
1067
|
* fiber.abort();
|
|
1225
1068
|
* const userResult = await fiber;
|
|
@@ -1241,8 +1084,8 @@ export interface Run<D = unknown> {
|
|
|
1241
1084
|
* Normal child Runs are disposed after their Task settles. Tasks started by
|
|
1242
1085
|
* `run.daemon` detach their lifetime from the current Task and attach to the
|
|
1243
1086
|
* root Run, so they keep running until they settle or the root Run is
|
|
1244
|
-
* disposed. Calling `.abort()` or async-disposing the returned Fiber
|
|
1245
|
-
* abort. Keep the returned Fiber for lifetime control.
|
|
1087
|
+
* disposed. Calling `.abort()` or async-disposing the returned {@link Fiber}
|
|
1088
|
+
* requests abort. Keep the returned Fiber for lifetime control.
|
|
1246
1089
|
*
|
|
1247
1090
|
* The daemon receives deps derived from the Run that starts it, not from the
|
|
1248
1091
|
* root Run: `deps` replace that Run's custom deps for the daemon Task, while
|
|
@@ -1259,7 +1102,9 @@ export interface Run<D = unknown> {
|
|
|
1259
1102
|
* un-aborted, because detached work must not spawn under a scope that is
|
|
1260
1103
|
* shutting down.
|
|
1261
1104
|
*
|
|
1262
|
-
*
|
|
1105
|
+
* For a long-lived reusable {@link Run}, use {@link Run.create}.
|
|
1106
|
+
*
|
|
1107
|
+
* ### Abort masks
|
|
1263
1108
|
*
|
|
1264
1109
|
* ```ts
|
|
1265
1110
|
* import { createRun, ok, unabortable, type Task } from "@evolu/common";
|
|
@@ -1287,9 +1132,7 @@ export interface Run<D = unknown> {
|
|
|
1287
1132
|
* expectOk(await run(syncParent), ["synced", "synced"]);
|
|
1288
1133
|
* ```
|
|
1289
1134
|
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
* ### Example
|
|
1135
|
+
* ### Aborting a daemon
|
|
1293
1136
|
*
|
|
1294
1137
|
* ```ts
|
|
1295
1138
|
* import {
|
|
@@ -1303,6 +1146,7 @@ export interface Run<D = unknown> {
|
|
|
1303
1146
|
* interface DbDep {
|
|
1304
1147
|
* readonly db: { readonly name: string };
|
|
1305
1148
|
* }
|
|
1149
|
+
*
|
|
1306
1150
|
* const db = { name: "main" };
|
|
1307
1151
|
* const syncUsers: Task<void, never, DbDep> = async (run) => {
|
|
1308
1152
|
* await run.ok(sleep("1s"));
|
|
@@ -1317,6 +1161,8 @@ export interface Run<D = unknown> {
|
|
|
1317
1161
|
* expect(AbortError.is(syncResult.error)).toBe(true);
|
|
1318
1162
|
* ```
|
|
1319
1163
|
*
|
|
1164
|
+
* ### Disposing a daemon
|
|
1165
|
+
*
|
|
1320
1166
|
* ```ts
|
|
1321
1167
|
* import { createRun, ok, waitForAbort, type Task } from "@evolu/common";
|
|
1322
1168
|
*
|
|
@@ -1347,11 +1193,11 @@ export interface Run<D = unknown> {
|
|
|
1347
1193
|
};
|
|
1348
1194
|
|
|
1349
1195
|
/**
|
|
1350
|
-
* Creates a {@link DisposableRun} attached to the root Run with this
|
|
1351
|
-
* deps.
|
|
1196
|
+
* Creates a {@link DisposableRun} attached to the root {@link Run} with this
|
|
1197
|
+
* Run's deps.
|
|
1352
1198
|
*
|
|
1353
1199
|
* Use it when you need a Run that can be reused across multiple operations.
|
|
1354
|
-
* For a single long-lived Task, use {@link Run.daemon}.
|
|
1200
|
+
* For a single long-lived {@link Task}, use {@link Run.daemon}.
|
|
1355
1201
|
*
|
|
1356
1202
|
* Use deps to replace the created Run's custom deps. Default deps
|
|
1357
1203
|
* ({@link RunDefaultDeps}) are inherited unless explicitly replaced with
|
|
@@ -1370,11 +1216,12 @@ export interface Run<D = unknown> {
|
|
|
1370
1216
|
* interface DbDep {
|
|
1371
1217
|
* readonly db: { readonly users: Array<string> };
|
|
1372
1218
|
* }
|
|
1219
|
+
*
|
|
1373
1220
|
* const db = { users: ["Ada"] };
|
|
1374
|
-
* const loadUser: Task<string, never, DbDep> = (
|
|
1375
|
-
* ok(deps.db.users[0] ?? "Unknown");
|
|
1376
|
-
* const saveUser: Task<void, never, DbDep> = (
|
|
1377
|
-
* deps.db.users.push("Grace");
|
|
1221
|
+
* const loadUser: Task<string, never, DbDep> = (run) =>
|
|
1222
|
+
* ok(run.deps.db.users[0] ?? "Unknown");
|
|
1223
|
+
* const saveUser: Task<void, never, DbDep> = (run) => {
|
|
1224
|
+
* run.deps.db.users.push("Grace");
|
|
1378
1225
|
* return ok();
|
|
1379
1226
|
* };
|
|
1380
1227
|
*
|
|
@@ -1398,7 +1245,7 @@ export interface Run<D = unknown> {
|
|
|
1398
1245
|
/** The parent {@link Run}, if this Run was created as a child. */
|
|
1399
1246
|
readonly parent: Run | null;
|
|
1400
1247
|
|
|
1401
|
-
/** Dependencies available to the Task, including {@link RunDefaultDeps}. */
|
|
1248
|
+
/** Dependencies available to the {@link Task}, including {@link RunDefaultDeps}. */
|
|
1402
1249
|
readonly deps: RunDefaultDeps & D;
|
|
1403
1250
|
|
|
1404
1251
|
/**
|
|
@@ -1481,7 +1328,7 @@ export interface Run<D = unknown> {
|
|
|
1481
1328
|
/**
|
|
1482
1329
|
* Callback for monitoring Run events emitted by this Run or descendants.
|
|
1483
1330
|
*
|
|
1484
|
-
* Event handlers are observers, not part of Task control flow. Handler
|
|
1331
|
+
* Event handlers are observers, not part of {@link Task} control flow. Handler
|
|
1485
1332
|
* defects are reported via {@link ReportDefectDep.reportDefect}; they do not
|
|
1486
1333
|
* panic the root Run or change Run state.
|
|
1487
1334
|
*
|
|
@@ -1512,44 +1359,13 @@ export type RunCustomDeps<D extends object> = D & {
|
|
|
1512
1359
|
*
|
|
1513
1360
|
* {@link createRun} creates a root DisposableRun. {@link Run.create} creates one
|
|
1514
1361
|
* attached to that root, typically to give a reusable resource its own
|
|
1515
|
-
* lifetime. A DisposableRun owns its child
|
|
1516
|
-
* registered with {@link DisposableRun.defer}; disposing it shuts down
|
|
1362
|
+
* lifetime. A DisposableRun owns its child {@link Task}s and closure-held
|
|
1363
|
+
* cleanup registered with {@link DisposableRun.defer}; disposing it shuts down
|
|
1364
|
+
* both.
|
|
1517
1365
|
*
|
|
1518
1366
|
* Sync disposal starts shutdown without waiting. Async disposal waits for child
|
|
1519
1367
|
* Tasks and registered cleanup to finish.
|
|
1520
1368
|
*
|
|
1521
|
-
* Use {@link createRun} at composition roots such as app, server, worker, or
|
|
1522
|
-
* test entry points. The common factory is platform-agnostic; platform adapters
|
|
1523
|
-
* can wrap it to add global error handling or shutdown integration.
|
|
1524
|
-
*
|
|
1525
|
-
* ### Example
|
|
1526
|
-
*
|
|
1527
|
-
* ```ts
|
|
1528
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
1529
|
-
*
|
|
1530
|
-
* await using run = createRun();
|
|
1531
|
-
* const loadData: Task<string> = () => ok("data");
|
|
1532
|
-
*
|
|
1533
|
-
* expectOk(await run(loadData), "data");
|
|
1534
|
-
* ```
|
|
1535
|
-
*
|
|
1536
|
-
* ### Example with custom dependencies
|
|
1537
|
-
*
|
|
1538
|
-
* ```ts
|
|
1539
|
-
* import { createRun, type DisposableRun } from "@evolu/common";
|
|
1540
|
-
*
|
|
1541
|
-
* interface ConfigDep {
|
|
1542
|
-
* readonly config: { readonly apiUrl: string };
|
|
1543
|
-
* }
|
|
1544
|
-
*
|
|
1545
|
-
* await using run = createRun<ConfigDep>({
|
|
1546
|
-
* config: { apiUrl: "https://api.example.com" },
|
|
1547
|
-
* });
|
|
1548
|
-
*
|
|
1549
|
-
* expectTypeOf(run).toEqualTypeOf<DisposableRun<ConfigDep>>();
|
|
1550
|
-
* expect(run.deps.config.apiUrl).toBe("https://api.example.com");
|
|
1551
|
-
* ```
|
|
1552
|
-
*
|
|
1553
1369
|
* @group Core
|
|
1554
1370
|
*/
|
|
1555
1371
|
export interface DisposableRun<D = unknown>
|
|
@@ -1557,8 +1373,8 @@ export interface DisposableRun<D = unknown>
|
|
|
1557
1373
|
/**
|
|
1558
1374
|
* Registers closure-held cleanup owned by this Run.
|
|
1559
1375
|
*
|
|
1560
|
-
* Finalizers run in LIFO order after child
|
|
1561
|
-
* async disposal. The Run is in `Aborted` state while they run and
|
|
1376
|
+
* Finalizers run in LIFO order after child {@link Task}s settle and are
|
|
1377
|
+
* awaited by async disposal. The Run is in `Aborted` state while they run and
|
|
1562
1378
|
* transitions to `Settled` afterward, so a finalizer cannot start Tasks on
|
|
1563
1379
|
* it. Use `using` for resources owned by a Task stack frame; use `defer` for
|
|
1564
1380
|
* closure-held state whose lifetime is bounded by a reusable DisposableRun.
|
|
@@ -1569,6 +1385,23 @@ export interface DisposableRun<D = unknown>
|
|
|
1569
1385
|
* with the same already-reported {@link AbortError}.
|
|
1570
1386
|
*
|
|
1571
1387
|
* Calling `defer` after disposal starts is a programmer error.
|
|
1388
|
+
*
|
|
1389
|
+
* ### Example
|
|
1390
|
+
*
|
|
1391
|
+
* ```ts
|
|
1392
|
+
* import { createRun } from "@evolu/common";
|
|
1393
|
+
*
|
|
1394
|
+
* let connectionClosed = false;
|
|
1395
|
+
* {
|
|
1396
|
+
* await using run = createRun();
|
|
1397
|
+
* run.defer(() => {
|
|
1398
|
+
* connectionClosed = true;
|
|
1399
|
+
* });
|
|
1400
|
+
*
|
|
1401
|
+
* expect(connectionClosed).toBe(false);
|
|
1402
|
+
* }
|
|
1403
|
+
* expect(connectionClosed).toBe(true);
|
|
1404
|
+
* ```
|
|
1572
1405
|
*/
|
|
1573
1406
|
readonly defer: (finalizer: () => Awaitable<void>) => void;
|
|
1574
1407
|
|
|
@@ -1586,8 +1419,8 @@ export interface DisposableRun<D = unknown>
|
|
|
1586
1419
|
* {@link ReportDefectDep}. The original defect is available as
|
|
1587
1420
|
* `abortError.reason.defect` for diagnostics. The first panic records the
|
|
1588
1421
|
* AbortError as the root Run's aborted exit and starts root disposal, which
|
|
1589
|
-
* aborts running
|
|
1590
|
-
* running Tasks to settle. Later panics still report and return their own
|
|
1422
|
+
* aborts running {@link Task}s, prevents new Tasks from starting, and waits
|
|
1423
|
+
* for running Tasks to settle. Later panics still report and return their own
|
|
1591
1424
|
* AbortError, but do not replace the root Run exit.
|
|
1592
1425
|
*/
|
|
1593
1426
|
readonly panic: (defect: unknown) => AbortError;
|
|
@@ -1596,8 +1429,8 @@ export interface DisposableRun<D = unknown>
|
|
|
1596
1429
|
/**
|
|
1597
1430
|
* A Promise-backed handle to a {@link Task} started by a {@link Run}.
|
|
1598
1431
|
*
|
|
1599
|
-
* Await a Fiber to use the Task Result in the current control flow. The
|
|
1600
|
-
* resolves with the Task
|
|
1432
|
+
* Await a Fiber to use the Task {@link Result} in the current control flow. The
|
|
1433
|
+
* Fiber resolves with the Task Result when the Task returns normally. A Fiber
|
|
1601
1434
|
* returned by `run(task)` rejects with {@link AbortError} when the Task observes
|
|
1602
1435
|
* abort or when a defect panics the Run tree. Panic uses
|
|
1603
1436
|
* {@link PanicAbortReason}; the original defect is available on the reason for
|
|
@@ -1654,6 +1487,7 @@ export interface DisposableRun<D = unknown>
|
|
|
1654
1487
|
export interface Fiber<T = unknown, E = unknown, D = unknown> extends Promise<
|
|
1655
1488
|
Result<T, E>
|
|
1656
1489
|
> {
|
|
1490
|
+
/** The child {@link Run} that executes this Fiber's {@link Task}. */
|
|
1657
1491
|
readonly run: Run<D>;
|
|
1658
1492
|
}
|
|
1659
1493
|
|
|
@@ -1665,7 +1499,7 @@ export interface Fiber<T = unknown, E = unknown, D = unknown> extends Promise<
|
|
|
1665
1499
|
export type AnyFiber = Fiber<any, any, any>;
|
|
1666
1500
|
|
|
1667
1501
|
/**
|
|
1668
|
-
* Extracts the Ok value type from a {@link Fiber}.
|
|
1502
|
+
* Extracts the {@link Ok} value type from a {@link Fiber}.
|
|
1669
1503
|
*
|
|
1670
1504
|
* @group Type utilities
|
|
1671
1505
|
*/
|
|
@@ -1673,7 +1507,7 @@ export type InferFiberOk<TFiber extends AnyFiber> =
|
|
|
1673
1507
|
TFiber extends Fiber<infer T, any, any> ? T : never;
|
|
1674
1508
|
|
|
1675
1509
|
/**
|
|
1676
|
-
* Extracts the Result error type from a {@link Fiber}.
|
|
1510
|
+
* Extracts the {@link Result} error type from a {@link Fiber}.
|
|
1677
1511
|
*
|
|
1678
1512
|
* @group Type utilities
|
|
1679
1513
|
*/
|
|
@@ -1692,8 +1526,8 @@ export type InferFiberDeps<TFiber extends AnyFiber> =
|
|
|
1692
1526
|
* A {@link Fiber} with explicit abort and async-disposal controls.
|
|
1693
1527
|
*
|
|
1694
1528
|
* Calling `.abort()` requests abort for the Fiber's child {@link Run}. If the
|
|
1695
|
-
* Task observes abort or a defect panics the Run tree, the Fiber
|
|
1696
|
-
* an {@link Err} containing the {@link AbortError}. Panic uses
|
|
1529
|
+
* {@link Task} observes abort or a defect panics the Run tree, the Fiber
|
|
1530
|
+
* resolves with an {@link Err} containing the {@link AbortError}. Panic uses
|
|
1697
1531
|
* {@link PanicAbortReason}; the original defect is available on the reason for
|
|
1698
1532
|
* diagnostics.
|
|
1699
1533
|
*
|
|
@@ -1732,6 +1566,7 @@ export type InferFiberDeps<TFiber extends AnyFiber> =
|
|
|
1732
1566
|
*/
|
|
1733
1567
|
export interface AbortableFiber<T = unknown, E = unknown, D = unknown>
|
|
1734
1568
|
extends Fiber<T, E | AbortError, D>, AsyncDisposable {
|
|
1569
|
+
/** Requests abort with an optional {@link AbortReason}. */
|
|
1735
1570
|
readonly abort: (reason?: AbortReason) => void;
|
|
1736
1571
|
}
|
|
1737
1572
|
|
|
@@ -1743,16 +1578,16 @@ export interface AbortableFiber<T = unknown, E = unknown, D = unknown>
|
|
|
1743
1578
|
* scopes. Abort requests are still recorded, but the Run's observed abort
|
|
1744
1579
|
* signal is aborted only when the mask is `0`.
|
|
1745
1580
|
*
|
|
1746
|
-
* Plain child
|
|
1747
|
-
* mask for the wrapped Task, and `unabortableMask` provides
|
|
1748
|
-
* selected child Tasks with the previous mask.
|
|
1581
|
+
* Plain child {@link Task}s inherit their parent's mask. `unabortable`
|
|
1582
|
+
* increments the mask for the wrapped Task, and `unabortableMask` provides
|
|
1583
|
+
* `restore` to run selected child Tasks with the previous mask.
|
|
1749
1584
|
*
|
|
1750
1585
|
* @group Abortability
|
|
1751
1586
|
*/
|
|
1752
1587
|
export type AbortMask = NonNegativeInt & Brand<"AbortMask">;
|
|
1753
1588
|
|
|
1754
1589
|
/**
|
|
1755
|
-
*
|
|
1590
|
+
* Runtime Type for structured data explaining why a {@link Run} was aborted.
|
|
1756
1591
|
*
|
|
1757
1592
|
* A reason has a `type` discriminant and optional structured data, so abort
|
|
1758
1593
|
* causes can carry typed domain data. Well-known reasons are
|
|
@@ -1768,14 +1603,20 @@ export const AbortReason: ObjectType<
|
|
|
1768
1603
|
{ type: String },
|
|
1769
1604
|
/*#__PURE__*/ record(String, Unknown),
|
|
1770
1605
|
);
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Structured data explaining why a {@link Run} was aborted.
|
|
1609
|
+
*
|
|
1610
|
+
* @group Core
|
|
1611
|
+
*/
|
|
1771
1612
|
export interface AbortReason extends InferType<typeof AbortReason> {}
|
|
1772
1613
|
|
|
1773
1614
|
/**
|
|
1774
|
-
*
|
|
1615
|
+
* Runtime Type for structured-concurrency abort control flow.
|
|
1775
1616
|
*
|
|
1776
|
-
* AbortError is thrown to stop Task execution when a Run observes
|
|
1777
|
-
* request. AbortableFiber catches AbortError and returns it as
|
|
1778
|
-
* error, so abort can be handled as an ordinary Task outcome.
|
|
1617
|
+
* AbortError is thrown to stop {@link Task} execution when a {@link Run} observes
|
|
1618
|
+
* an abort request. {@link AbortableFiber} catches AbortError and returns it as
|
|
1619
|
+
* a {@link Result} error, so abort can be handled as an ordinary Task outcome.
|
|
1779
1620
|
*
|
|
1780
1621
|
* The reason explains why the Run was aborted. It can be an explicit abort
|
|
1781
1622
|
* reason, {@link runDisposedAbortReason} for normal Run cleanup, or
|
|
@@ -1800,6 +1641,12 @@ export const AbortError: TypedType<
|
|
|
1800
1641
|
> = /*#__PURE__*/ typed("AbortError", {
|
|
1801
1642
|
reason: AbortReason,
|
|
1802
1643
|
});
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Structured-concurrency abort control-flow value.
|
|
1647
|
+
*
|
|
1648
|
+
* @group Core
|
|
1649
|
+
*/
|
|
1803
1650
|
export interface AbortError extends InferType<typeof AbortError> {}
|
|
1804
1651
|
|
|
1805
1652
|
/**
|
|
@@ -1815,9 +1662,9 @@ export const createAbortError = (reason: AbortReason): AbortError => ({
|
|
|
1815
1662
|
/**
|
|
1816
1663
|
* Final outcome recorded by a {@link Run}.
|
|
1817
1664
|
*
|
|
1818
|
-
* A Run exit is an outer {@link Result}. {@link Ok} means the Task
|
|
1819
|
-
* Result; {@link Err} means the Run aborted with {@link AbortError}.
|
|
1820
|
-
* recorded as an AbortError whose reason is {@link PanicAbortReason}.
|
|
1665
|
+
* A Run exit is an outer {@link Result}. {@link Ok} means the {@link Task}
|
|
1666
|
+
* returned a Result; {@link Err} means the Run aborted with {@link AbortError}.
|
|
1667
|
+
* Panic is recorded as an AbortError whose reason is {@link PanicAbortReason}.
|
|
1821
1668
|
*
|
|
1822
1669
|
* @group Core
|
|
1823
1670
|
*/
|
|
@@ -1882,7 +1729,7 @@ export interface RunStateSettled extends Typed<"Settled">, RunAbortState {
|
|
|
1882
1729
|
* same object reference. This lets UI and debugging tools compare snapshots by
|
|
1883
1730
|
* identity and skip unchanged branches.
|
|
1884
1731
|
*
|
|
1885
|
-
* @group
|
|
1732
|
+
* @group Monitoring
|
|
1886
1733
|
* @see {@link Run.snapshot}
|
|
1887
1734
|
*/
|
|
1888
1735
|
export interface RunSnapshot {
|
|
@@ -1908,7 +1755,7 @@ export type RunEventData =
|
|
|
1908
1755
|
RunEventDataChildAdded | RunEventDataChildRemoved | RunEventDataStateChanged;
|
|
1909
1756
|
|
|
1910
1757
|
/**
|
|
1911
|
-
* A child Run was added to the emitting Run.
|
|
1758
|
+
* A child {@link Run} was added to the emitting Run.
|
|
1912
1759
|
*
|
|
1913
1760
|
* @group Monitoring
|
|
1914
1761
|
*/
|
|
@@ -1918,7 +1765,7 @@ export interface RunEventDataChildAdded extends Typed<"ChildAdded"> {
|
|
|
1918
1765
|
}
|
|
1919
1766
|
|
|
1920
1767
|
/**
|
|
1921
|
-
* A child Run was removed from the emitting Run.
|
|
1768
|
+
* A child {@link Run} was removed from the emitting Run.
|
|
1922
1769
|
*
|
|
1923
1770
|
* @group Monitoring
|
|
1924
1771
|
*/
|
|
@@ -1959,9 +1806,9 @@ export interface RunEvent {
|
|
|
1959
1806
|
/**
|
|
1960
1807
|
* Shared abort reason used for ordinary {@link Run} cleanup.
|
|
1961
1808
|
*
|
|
1962
|
-
* Disposal requests abort so child
|
|
1963
|
-
* settle. This reason distinguishes that cleanup path from explicit
|
|
1964
|
-
* {@link PanicAbortReason}.
|
|
1809
|
+
* Disposal requests abort so child {@link Task}s stop while the Run waits for
|
|
1810
|
+
* them to settle. This reason distinguishes that cleanup path from explicit
|
|
1811
|
+
* abort and {@link PanicAbortReason}.
|
|
1965
1812
|
*
|
|
1966
1813
|
* @group Run
|
|
1967
1814
|
*/
|
|
@@ -1984,7 +1831,7 @@ export const explicitAbortReason = {
|
|
|
1984
1831
|
/**
|
|
1985
1832
|
* Shared abort reason for tests that need a non-production abort reason.
|
|
1986
1833
|
*
|
|
1987
|
-
* @group
|
|
1834
|
+
* @group Testing
|
|
1988
1835
|
*/
|
|
1989
1836
|
export const testAbortReason = {
|
|
1990
1837
|
type: "TestAbortReason",
|
|
@@ -1993,26 +1840,25 @@ export const testAbortReason = {
|
|
|
1993
1840
|
/**
|
|
1994
1841
|
* Shared {@link AbortError} for tests, created from {@link testAbortReason}.
|
|
1995
1842
|
*
|
|
1996
|
-
* @group
|
|
1843
|
+
* @group Testing
|
|
1997
1844
|
*/
|
|
1998
1845
|
export const testAbortError = /*#__PURE__*/ createAbortError(testAbortReason);
|
|
1999
1846
|
|
|
2000
1847
|
/**
|
|
2001
|
-
*
|
|
1848
|
+
* Abort reason recorded when a defect panics the root {@link Run}.
|
|
2002
1849
|
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
* flow, not a defect.
|
|
1850
|
+
* A defect is a thrown or rejected value other than {@link AbortError}.
|
|
1851
|
+
* Recoverable domain errors belong in {@link Result}. Bugs and unrecoverable
|
|
1852
|
+
* failures, such as storage engine errors the {@link Task} cannot usefully
|
|
1853
|
+
* handle, may throw or reject.
|
|
2008
1854
|
*
|
|
2009
1855
|
* {@link Run.onEvent} handler defects are different: event handlers are
|
|
2010
1856
|
* monitoring code, so their defects are reported globally but do not panic the
|
|
2011
1857
|
* root Run.
|
|
2012
1858
|
*
|
|
2013
|
-
* When Run observes a defect, it aborts the root Run and starts disposal
|
|
1859
|
+
* When {@link Run} observes a defect, it aborts the root Run and starts disposal
|
|
2014
1860
|
* immediately. This prevents later Tasks from starting after the defect. A
|
|
2015
|
-
* Fiber rejects with AbortError whose reason is PanicAbortReason; an
|
|
1861
|
+
* {@link Fiber} rejects with AbortError whose reason is PanicAbortReason; an
|
|
2016
1862
|
* {@link AbortableFiber} returns that AbortError as an {@link Err}.
|
|
2017
1863
|
*
|
|
2018
1864
|
* @group Core
|
|
@@ -2060,10 +1906,10 @@ export interface RunConfigDep {
|
|
|
2060
1906
|
/**
|
|
2061
1907
|
* Reports a defect.
|
|
2062
1908
|
*
|
|
2063
|
-
* Run uses this dependency in two cases: {@link DisposableRun.panic}
|
|
2064
|
-
* {@link AbortError} whose reason is {@link PanicAbortReason}, and
|
|
2065
|
-
* monitoring reports observer defects without panicking the Run. The
|
|
2066
|
-
* panic defect is available at `abortError.reason.defect`.
|
|
1909
|
+
* {@link Run} uses this dependency in two cases: {@link DisposableRun.panic}
|
|
1910
|
+
* reports the {@link AbortError} whose reason is {@link PanicAbortReason}, and
|
|
1911
|
+
* event monitoring reports observer defects without panicking the Run. The
|
|
1912
|
+
* original panic defect is available at `abortError.reason.defect`.
|
|
2067
1913
|
*
|
|
2068
1914
|
* @group Run
|
|
2069
1915
|
*/
|
|
@@ -2099,9 +1945,9 @@ export const reportDefectAfterMicrotask: ReportDefect = (defect) => {
|
|
|
2099
1945
|
/**
|
|
2100
1946
|
* Default dependencies provided by {@link createRun}.
|
|
2101
1947
|
*
|
|
2102
|
-
* Root
|
|
2103
|
-
* detection, native fetch, randomness, error reporting, time, and optional
|
|
2104
|
-
* monitoring configuration.
|
|
1948
|
+
* Root {@link Run}s include platform-independent implementations for console,
|
|
1949
|
+
* leak detection, native fetch, randomness, error reporting, time, and optional
|
|
1950
|
+
* Run monitoring configuration.
|
|
2105
1951
|
*
|
|
2106
1952
|
* The {@link LeakDetector} is enabled only in development builds; production
|
|
2107
1953
|
* uses a no-op implementation.
|
|
@@ -2141,16 +1987,41 @@ export const createRunDefaultDeps = (): RunDefaultDeps => {
|
|
|
2141
1987
|
* @group Run
|
|
2142
1988
|
*/
|
|
2143
1989
|
export interface CreateRun {
|
|
2144
|
-
/** Creates a root Run with only {@link RunDefaultDeps}. */
|
|
1990
|
+
/** Creates a root {@link Run} with only {@link RunDefaultDeps}. */
|
|
2145
1991
|
(): DisposableRun;
|
|
2146
1992
|
|
|
2147
|
-
/**
|
|
1993
|
+
/**
|
|
1994
|
+
* Creates a root {@link Run} with custom deps merged over
|
|
1995
|
+
* {@link RunDefaultDeps}.
|
|
1996
|
+
*/
|
|
2148
1997
|
<D extends object>(deps: RunCustomDeps<D>): DisposableRun<D>;
|
|
2149
1998
|
}
|
|
2150
1999
|
|
|
2151
2000
|
/**
|
|
2152
2001
|
* Creates a root {@link DisposableRun}.
|
|
2153
2002
|
*
|
|
2003
|
+
* Use at composition roots such as app, server, worker, or test entry points.
|
|
2004
|
+
* The common factory is platform-agnostic; platform adapters can wrap it to add
|
|
2005
|
+
* global error handling or shutdown integration.
|
|
2006
|
+
*
|
|
2007
|
+
* ### Example
|
|
2008
|
+
*
|
|
2009
|
+
* ```ts
|
|
2010
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
2011
|
+
*
|
|
2012
|
+
* interface ConfigDep {
|
|
2013
|
+
* readonly config: { readonly apiUrl: string };
|
|
2014
|
+
* }
|
|
2015
|
+
*
|
|
2016
|
+
* const loadApiUrl: Task<string, never, ConfigDep> = (run) =>
|
|
2017
|
+
* ok(run.deps.config.apiUrl);
|
|
2018
|
+
*
|
|
2019
|
+
* await using run = createRun({
|
|
2020
|
+
* config: { apiUrl: "https://api.example.com" },
|
|
2021
|
+
* });
|
|
2022
|
+
* expectOk(await run(loadApiUrl), "https://api.example.com");
|
|
2023
|
+
* ```
|
|
2024
|
+
*
|
|
2154
2025
|
* @group Run
|
|
2155
2026
|
*/
|
|
2156
2027
|
export const createRun: CreateRun = <D extends object>(
|
|
@@ -2190,7 +2061,11 @@ export type TestRunDefaultDeps = Omit<
|
|
|
2190
2061
|
TestTimeDep &
|
|
2191
2062
|
RandomLibDep;
|
|
2192
2063
|
|
|
2193
|
-
/**
|
|
2064
|
+
/**
|
|
2065
|
+
* Provides a test {@link Run} with deterministic default dependencies.
|
|
2066
|
+
*
|
|
2067
|
+
* @group Testing
|
|
2068
|
+
*/
|
|
2194
2069
|
export interface TestRunDep<D = unknown> {
|
|
2195
2070
|
readonly run: Run<TestRunDefaultDeps & D>;
|
|
2196
2071
|
}
|
|
@@ -2318,12 +2193,42 @@ export const testCreateDeps = (options?: {
|
|
|
2318
2193
|
/**
|
|
2319
2194
|
* Creates a root {@link DisposableRun} with {@link TestRunDefaultDeps}.
|
|
2320
2195
|
*
|
|
2196
|
+
* ### Example
|
|
2197
|
+
*
|
|
2198
|
+
* ```ts
|
|
2199
|
+
* import { ok, testCreateRun, type Task } from "@evolu/common";
|
|
2200
|
+
*
|
|
2201
|
+
* const readTime: Task<number> = (run) => ok(run.deps.time.now());
|
|
2202
|
+
*
|
|
2203
|
+
* await using run = testCreateRun();
|
|
2204
|
+
* expectOk(await run(readTime), 0);
|
|
2205
|
+
* ```
|
|
2206
|
+
*
|
|
2321
2207
|
* @group Testing
|
|
2322
2208
|
*/
|
|
2323
2209
|
export function testCreateRun(
|
|
2324
2210
|
deps?: TestRunDefaultDeps,
|
|
2325
2211
|
): DisposableRun<TestRunDefaultDeps>;
|
|
2326
2212
|
|
|
2213
|
+
/**
|
|
2214
|
+
* Merges custom dependencies into {@link TestRunDefaultDeps}.
|
|
2215
|
+
*
|
|
2216
|
+
* ### Example
|
|
2217
|
+
*
|
|
2218
|
+
* ```ts
|
|
2219
|
+
* import { ok, testCreateRun, type Task } from "@evolu/common";
|
|
2220
|
+
*
|
|
2221
|
+
* interface FeatureDep {
|
|
2222
|
+
* readonly feature: { readonly enabled: boolean };
|
|
2223
|
+
* }
|
|
2224
|
+
*
|
|
2225
|
+
* const isFeatureEnabled: Task<boolean, never, FeatureDep> = (run) =>
|
|
2226
|
+
* ok(run.deps.feature.enabled);
|
|
2227
|
+
*
|
|
2228
|
+
* await using run = testCreateRun({ feature: { enabled: true } });
|
|
2229
|
+
* expectOk(await run(isFeatureEnabled), true);
|
|
2230
|
+
* ```
|
|
2231
|
+
*/
|
|
2327
2232
|
export function testCreateRun<D extends object>(
|
|
2328
2233
|
deps: RunCustomDeps<D>,
|
|
2329
2234
|
): DisposableRun<TestRunDefaultDeps & D>;
|
|
@@ -2836,7 +2741,8 @@ const withTaskMeta =
|
|
|
2836
2741
|
export type TaskRecord = Readonly<Record<string, AnyTask>>;
|
|
2837
2742
|
|
|
2838
2743
|
/**
|
|
2839
|
-
* Extracts the dependency intersection required by a readonly Task
|
|
2744
|
+
* Extracts the dependency intersection required by a readonly {@link Task}
|
|
2745
|
+
* array.
|
|
2840
2746
|
*
|
|
2841
2747
|
* @group Type utilities
|
|
2842
2748
|
*/
|
|
@@ -2850,7 +2756,7 @@ export type InferTasksDeps<TTasks extends ReadonlyArray<AnyTask>> =
|
|
|
2850
2756
|
>;
|
|
2851
2757
|
|
|
2852
2758
|
/**
|
|
2853
|
-
* Extracts the dependency intersection required by a Task record.
|
|
2759
|
+
* Extracts the dependency intersection required by a {@link Task} record.
|
|
2854
2760
|
*
|
|
2855
2761
|
* @group Type utilities
|
|
2856
2762
|
*/
|
|
@@ -2859,12 +2765,13 @@ export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<
|
|
|
2859
2765
|
>;
|
|
2860
2766
|
|
|
2861
2767
|
/**
|
|
2862
|
-
* Options shared by Task collection helpers.
|
|
2768
|
+
* Options shared by {@link Task} collection helpers.
|
|
2863
2769
|
*
|
|
2864
|
-
* `concurrency` controls how many Tasks run at once. It defaults to `1`.
|
|
2865
|
-
*
|
|
2866
|
-
*
|
|
2867
|
-
*
|
|
2770
|
+
* `concurrency` controls how many Tasks run at once. It defaults to `1`. For
|
|
2771
|
+
* CPU-bound Tasks backed by workers or parallel native operations, a platform
|
|
2772
|
+
* `availableParallelism()` result is often a good limit. For network or
|
|
2773
|
+
* database Tasks, choose a limit based on the transport, server, connection
|
|
2774
|
+
* pool, and rate limits.
|
|
2868
2775
|
*
|
|
2869
2776
|
* Keep concurrency bounded. In rare cases where running every Task concurrently
|
|
2870
2777
|
* is safe, use {@link maxPositiveInt}.
|
|
@@ -2872,6 +2779,7 @@ export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<
|
|
|
2872
2779
|
* @group Collection
|
|
2873
2780
|
*/
|
|
2874
2781
|
export interface TaskCollectionOptions {
|
|
2782
|
+
/** Maximum number of {@link Task}s run concurrently. Defaults to `1`. */
|
|
2875
2783
|
readonly concurrency?: Int1To100OrPositiveInt;
|
|
2876
2784
|
}
|
|
2877
2785
|
|
|
@@ -2881,12 +2789,13 @@ export interface TaskCollectionOptions {
|
|
|
2881
2789
|
* @group Collection
|
|
2882
2790
|
*/
|
|
2883
2791
|
export interface AllOptions extends TaskCollectionOptions {
|
|
2884
|
-
/** Disables collecting Ok values. */
|
|
2792
|
+
/** Disables collecting {@link Ok} values. */
|
|
2885
2793
|
readonly collect: false;
|
|
2886
2794
|
}
|
|
2887
2795
|
|
|
2888
2796
|
/**
|
|
2889
|
-
* Maps a Task array or record to the Ok values produced by its
|
|
2797
|
+
* Maps a {@link Task} array or record to the {@link Ok} values produced by its
|
|
2798
|
+
* Tasks.
|
|
2890
2799
|
*
|
|
2891
2800
|
* The mapped type is homomorphic, so tuples preserve their shape and records
|
|
2892
2801
|
* preserve their keys.
|
|
@@ -2900,7 +2809,7 @@ export type InferTasksOk<TTasks> = {
|
|
|
2900
2809
|
};
|
|
2901
2810
|
|
|
2902
2811
|
/**
|
|
2903
|
-
* Runs
|
|
2812
|
+
* Runs {@link Task}s until all return {@link Ok} or one returns {@link Err}.
|
|
2904
2813
|
*
|
|
2905
2814
|
* Returns Ok with all values when every Task returns Ok. Stops on the first
|
|
2906
2815
|
* Err; remaining running Tasks are aborted. Sequential by default; pass a
|
|
@@ -2918,8 +2827,8 @@ export type InferTasksOk<TTasks> = {
|
|
|
2918
2827
|
*
|
|
2919
2828
|
* Similar to
|
|
2920
2829
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all | Promise.all},
|
|
2921
|
-
* but runs Tasks, returns Result values, and aborts remaining Tasks on
|
|
2922
|
-
* first Err.
|
|
2830
|
+
* but runs Tasks, returns {@link Result} values, and aborts remaining Tasks on
|
|
2831
|
+
* the first Err.
|
|
2923
2832
|
*
|
|
2924
2833
|
* ### Example
|
|
2925
2834
|
*
|
|
@@ -2931,48 +2840,35 @@ export type InferTasksOk<TTasks> = {
|
|
|
2931
2840
|
* ok,
|
|
2932
2841
|
* type Result,
|
|
2933
2842
|
* type Task,
|
|
2843
|
+
* type Typed,
|
|
2934
2844
|
* } from "@evolu/common";
|
|
2935
2845
|
*
|
|
2936
|
-
* interface User {
|
|
2937
|
-
* readonly id: string;
|
|
2938
|
-
* }
|
|
2939
|
-
* interface Post {
|
|
2940
|
-
* readonly id: string;
|
|
2941
|
-
* }
|
|
2942
|
-
* const fetchUser: Task<User> = () => ok({ id: "user-1" });
|
|
2943
|
-
* const fetchPosts: Task<ReadonlyArray<Post>> = () =>
|
|
2944
|
-
* ok([{ id: "post-1" }]);
|
|
2945
|
-
* await using run = createRun();
|
|
2946
|
-
*
|
|
2947
|
-
* const dashboard = await run(all([fetchUser, fetchPosts]));
|
|
2948
|
-
* expectTypeOf(dashboard).toEqualTypeOf<
|
|
2949
|
-
* Result<readonly [User, ReadonlyArray<Post>]>
|
|
2950
|
-
* >();
|
|
2951
|
-
* expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
|
|
2952
|
-
*
|
|
2953
|
-
* // Skip collecting Ok values when they aren't needed.
|
|
2954
|
-
* interface SaveUserError {
|
|
2955
|
-
* readonly type: "SaveUserError";
|
|
2956
|
-
* readonly userId: string;
|
|
2957
|
-
* }
|
|
2958
2846
|
* const savedUserIds: Array<string> = [];
|
|
2959
2847
|
* const saveUser =
|
|
2960
|
-
* (id: string): Task<number,
|
|
2848
|
+
* (id: string): Task<number, SaveUserFailedError> =>
|
|
2961
2849
|
* () => {
|
|
2962
2850
|
* if (id === "missing") {
|
|
2963
|
-
* return err({ type: "
|
|
2851
|
+
* return err({ type: "SaveUserFailed", userId: id });
|
|
2964
2852
|
* }
|
|
2965
2853
|
* savedUserIds.push(id);
|
|
2966
2854
|
* return ok(1);
|
|
2967
2855
|
* };
|
|
2856
|
+
*
|
|
2857
|
+
* interface SaveUserFailedError extends Typed<"SaveUserFailed"> {
|
|
2858
|
+
* readonly userId: string;
|
|
2859
|
+
* }
|
|
2860
|
+
*
|
|
2861
|
+
* await using run = createRun();
|
|
2968
2862
|
* const saveResult = await run(
|
|
2969
|
-
* all(["user-1", "missing", "user-3"],
|
|
2863
|
+
* all([saveUser("user-1"), saveUser("missing"), saveUser("user-3")], {
|
|
2970
2864
|
* collect: false,
|
|
2971
2865
|
* }),
|
|
2972
2866
|
* );
|
|
2973
|
-
* expectTypeOf(saveResult).toEqualTypeOf<
|
|
2867
|
+
* expectTypeOf(saveResult).toEqualTypeOf<
|
|
2868
|
+
* Result<void, SaveUserFailedError>
|
|
2869
|
+
* >();
|
|
2974
2870
|
* expectErr(saveResult, {
|
|
2975
|
-
* type: "
|
|
2871
|
+
* type: "SaveUserFailed",
|
|
2976
2872
|
* userId: "missing",
|
|
2977
2873
|
* });
|
|
2978
2874
|
* expect(savedUserIds).toEqual(["user-1"]);
|
|
@@ -2985,12 +2881,46 @@ export function all<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
2985
2881
|
options: AllOptions,
|
|
2986
2882
|
): Task<void, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
|
|
2987
2883
|
|
|
2988
|
-
/** Runs a Task record without collecting its Ok values. */
|
|
2884
|
+
/** Runs a {@link Task} record without collecting its {@link Ok} values. */
|
|
2989
2885
|
export function all<const TTasks extends TaskRecord>(
|
|
2990
2886
|
tasks: TTasks,
|
|
2991
2887
|
options: AllOptions,
|
|
2992
2888
|
): Task<void, InferTaskErr<TTasks[keyof TTasks]>, InferTaskRecordDeps<TTasks>>;
|
|
2993
2889
|
|
|
2890
|
+
/**
|
|
2891
|
+
* Runs a {@link Task} array and preserves its shape.
|
|
2892
|
+
*
|
|
2893
|
+
* ### Example
|
|
2894
|
+
*
|
|
2895
|
+
* ```ts
|
|
2896
|
+
* import {
|
|
2897
|
+
* all,
|
|
2898
|
+
* createRun,
|
|
2899
|
+
* ok,
|
|
2900
|
+
* type Result,
|
|
2901
|
+
* type Task,
|
|
2902
|
+
* } from "@evolu/common";
|
|
2903
|
+
*
|
|
2904
|
+
* interface User {
|
|
2905
|
+
* readonly id: string;
|
|
2906
|
+
* }
|
|
2907
|
+
*
|
|
2908
|
+
* interface Post {
|
|
2909
|
+
* readonly id: string;
|
|
2910
|
+
* }
|
|
2911
|
+
*
|
|
2912
|
+
* const fetchUser: Task<User> = () => ok({ id: "user-1" });
|
|
2913
|
+
* const fetchPosts: Task<ReadonlyArray<Post>> = () =>
|
|
2914
|
+
* ok([{ id: "post-1" }]);
|
|
2915
|
+
*
|
|
2916
|
+
* await using run = createRun();
|
|
2917
|
+
* const dashboard = await run(all([fetchUser, fetchPosts]));
|
|
2918
|
+
* expectTypeOf(dashboard).toEqualTypeOf<
|
|
2919
|
+
* Result<readonly [User, ReadonlyArray<Post>]>
|
|
2920
|
+
* >();
|
|
2921
|
+
* expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
|
|
2922
|
+
* ```
|
|
2923
|
+
*/
|
|
2994
2924
|
export function all<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
2995
2925
|
tasks: TTasks,
|
|
2996
2926
|
options?: TaskCollectionOptions,
|
|
@@ -3001,7 +2931,7 @@ export function all<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
3001
2931
|
>;
|
|
3002
2932
|
|
|
3003
2933
|
/**
|
|
3004
|
-
* Runs a Task record and preserves its keys.
|
|
2934
|
+
* Runs a {@link Task} record and preserves its keys.
|
|
3005
2935
|
*
|
|
3006
2936
|
* ### Example
|
|
3007
2937
|
*
|
|
@@ -3017,6 +2947,7 @@ export function all<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
3017
2947
|
* interface User {
|
|
3018
2948
|
* readonly id: string;
|
|
3019
2949
|
* }
|
|
2950
|
+
*
|
|
3020
2951
|
* interface Post {
|
|
3021
2952
|
* readonly id: string;
|
|
3022
2953
|
* }
|
|
@@ -3046,8 +2977,30 @@ export function all<const TTasks extends TaskRecord>(
|
|
|
3046
2977
|
InferTaskRecordDeps<TTasks>
|
|
3047
2978
|
>;
|
|
3048
2979
|
|
|
2980
|
+
/** Maps an array to {@link Task}s without collecting their {@link Ok} values. */
|
|
2981
|
+
export function all<
|
|
2982
|
+
const TValues extends ReadonlyArray<unknown>,
|
|
2983
|
+
TTask extends AnyTask,
|
|
2984
|
+
>(
|
|
2985
|
+
values: TValues,
|
|
2986
|
+
fn: (value: TValues[number], index: number) => TTask,
|
|
2987
|
+
options: AllOptions,
|
|
2988
|
+
): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2989
|
+
|
|
2990
|
+
/** Maps record values to {@link Task}s without collecting their {@link Ok} values. */
|
|
2991
|
+
export function all<
|
|
2992
|
+
const TValues extends Readonly<Record<string, unknown>>,
|
|
2993
|
+
TTask extends AnyTask,
|
|
2994
|
+
>(
|
|
2995
|
+
values: TValues,
|
|
2996
|
+
// eslint-disable-next-line @typescript-eslint/unified-signatures -- Separate array and record overloads keep callback parameter inference precise.
|
|
2997
|
+
fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
|
|
2998
|
+
options: AllOptions,
|
|
2999
|
+
): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
3000
|
+
|
|
3049
3001
|
/**
|
|
3050
|
-
* Maps an array to
|
|
3002
|
+
* Maps an array to {@link Task}s and collects their {@link Ok} values in the same
|
|
3003
|
+
* shape.
|
|
3051
3004
|
*
|
|
3052
3005
|
* ### Example
|
|
3053
3006
|
*
|
|
@@ -3063,6 +3016,7 @@ export function all<const TTasks extends TaskRecord>(
|
|
|
3063
3016
|
* interface User {
|
|
3064
3017
|
* readonly id: string;
|
|
3065
3018
|
* }
|
|
3019
|
+
*
|
|
3066
3020
|
* const loadUser =
|
|
3067
3021
|
* (id: string): Task<User> =>
|
|
3068
3022
|
* () =>
|
|
@@ -3084,26 +3038,6 @@ export function all<const TTasks extends TaskRecord>(
|
|
|
3084
3038
|
* expectOk(result, [{ id: "user-1" }, { id: "user-2" }]);
|
|
3085
3039
|
* ```
|
|
3086
3040
|
*/
|
|
3087
|
-
export function all<
|
|
3088
|
-
const TValues extends ReadonlyArray<unknown>,
|
|
3089
|
-
TTask extends AnyTask,
|
|
3090
|
-
>(
|
|
3091
|
-
values: TValues,
|
|
3092
|
-
fn: (value: TValues[number], index: number) => TTask,
|
|
3093
|
-
options: AllOptions,
|
|
3094
|
-
): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
3095
|
-
|
|
3096
|
-
/** Maps record values to Tasks without collecting their Ok values. */
|
|
3097
|
-
export function all<
|
|
3098
|
-
const TValues extends Readonly<Record<string, unknown>>,
|
|
3099
|
-
TTask extends AnyTask,
|
|
3100
|
-
>(
|
|
3101
|
-
values: TValues,
|
|
3102
|
-
// eslint-disable-next-line @typescript-eslint/unified-signatures -- Separate array and record overloads keep callback parameter inference precise.
|
|
3103
|
-
fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
|
|
3104
|
-
options: AllOptions,
|
|
3105
|
-
): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
3106
|
-
|
|
3107
3041
|
export function all<
|
|
3108
3042
|
const TValues extends ReadonlyArray<unknown>,
|
|
3109
3043
|
TTask extends AnyTask,
|
|
@@ -3118,7 +3052,7 @@ export function all<
|
|
|
3118
3052
|
>;
|
|
3119
3053
|
|
|
3120
3054
|
/**
|
|
3121
|
-
* Maps record values to
|
|
3055
|
+
* Maps record values to {@link Task}s and preserves the record's keys.
|
|
3122
3056
|
*
|
|
3123
3057
|
* ### Example
|
|
3124
3058
|
*
|
|
@@ -3134,6 +3068,7 @@ export function all<
|
|
|
3134
3068
|
* interface User {
|
|
3135
3069
|
* readonly id: string;
|
|
3136
3070
|
* }
|
|
3071
|
+
*
|
|
3137
3072
|
* const loadUser =
|
|
3138
3073
|
* (id: string): Task<User> =>
|
|
3139
3074
|
* () =>
|
|
@@ -3201,7 +3136,8 @@ export function all(
|
|
|
3201
3136
|
}
|
|
3202
3137
|
|
|
3203
3138
|
/**
|
|
3204
|
-
* Maps a Task array or record to the Result values produced by
|
|
3139
|
+
* Maps a {@link Task} array or record to the {@link Result} values produced by
|
|
3140
|
+
* its Tasks.
|
|
3205
3141
|
*
|
|
3206
3142
|
* The mapped type is homomorphic, so tuples preserve their shape and records
|
|
3207
3143
|
* preserve their keys.
|
|
@@ -3215,7 +3151,7 @@ export type InferTasksSettled<TTasks> = {
|
|
|
3215
3151
|
};
|
|
3216
3152
|
|
|
3217
3153
|
/**
|
|
3218
|
-
* Runs all
|
|
3154
|
+
* Runs all {@link Task}s and returns every Task {@link Result}.
|
|
3219
3155
|
*
|
|
3220
3156
|
* Unlike {@link all}, {@link Err} Results do not stop later Tasks.
|
|
3221
3157
|
*
|
|
@@ -3242,14 +3178,14 @@ export type InferTasksSettled<TTasks> = {
|
|
|
3242
3178
|
* ok,
|
|
3243
3179
|
* type Result,
|
|
3244
3180
|
* type Task,
|
|
3181
|
+
* type Typed,
|
|
3245
3182
|
* } from "@evolu/common";
|
|
3246
3183
|
*
|
|
3247
|
-
*
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3184
|
+
* const loadProfile: Task<string, ProfileNotFoundError> = () =>
|
|
3185
|
+
* err({ type: "ProfileNotFound" });
|
|
3186
|
+
*
|
|
3187
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
|
|
3250
3188
|
*
|
|
3251
|
-
* const loadProfile: Task<string, LoadError> = () =>
|
|
3252
|
-
* err({ type: "LoadError" });
|
|
3253
3189
|
* let activityLoaded = false;
|
|
3254
3190
|
* const loadActivity: Task<ReadonlyArray<string>> = () => {
|
|
3255
3191
|
* activityLoaded = true;
|
|
@@ -3260,11 +3196,14 @@ export type InferTasksSettled<TTasks> = {
|
|
|
3260
3196
|
* const results = await run(allSettled([loadProfile, loadActivity]));
|
|
3261
3197
|
* expectTypeOf(results).toEqualTypeOf<
|
|
3262
3198
|
* Result<
|
|
3263
|
-
* readonly [
|
|
3199
|
+
* readonly [
|
|
3200
|
+
* Result<string, ProfileNotFoundError>,
|
|
3201
|
+
* Result<ReadonlyArray<string>>,
|
|
3202
|
+
* ]
|
|
3264
3203
|
* >
|
|
3265
3204
|
* >();
|
|
3266
3205
|
* expectOk(results, [
|
|
3267
|
-
* { ok: false, error: { type: "
|
|
3206
|
+
* { ok: false, error: { type: "ProfileNotFound" } },
|
|
3268
3207
|
* { ok: true, value: ["signed-in"] },
|
|
3269
3208
|
* ]);
|
|
3270
3209
|
* // Unlike all, a later Task still runs after an Err.
|
|
@@ -3279,7 +3218,7 @@ export function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
3279
3218
|
): Task<InferTasksSettled<TTasks>, never, InferTasksDeps<TTasks>>;
|
|
3280
3219
|
|
|
3281
3220
|
/**
|
|
3282
|
-
* Runs a Task record and preserves its keys.
|
|
3221
|
+
* Runs a {@link Task} record and preserves its keys.
|
|
3283
3222
|
*
|
|
3284
3223
|
* ### Example
|
|
3285
3224
|
*
|
|
@@ -3291,17 +3230,18 @@ export function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
3291
3230
|
* ok,
|
|
3292
3231
|
* type Result,
|
|
3293
3232
|
* type Task,
|
|
3233
|
+
* type Typed,
|
|
3294
3234
|
* } from "@evolu/common";
|
|
3295
3235
|
*
|
|
3296
3236
|
* interface User {
|
|
3297
3237
|
* readonly id: string;
|
|
3298
3238
|
* }
|
|
3299
|
-
*
|
|
3300
|
-
* readonly type: "LoadError";
|
|
3301
|
-
* }
|
|
3239
|
+
*
|
|
3302
3240
|
* const fetchUser: Task<User> = () => ok({ id: "user-1" });
|
|
3303
|
-
* const fetchProfile: Task<string,
|
|
3304
|
-
* err({ type: "
|
|
3241
|
+
* const fetchProfile: Task<string, ProfileNotFoundError> = () =>
|
|
3242
|
+
* err({ type: "ProfileNotFound" });
|
|
3243
|
+
*
|
|
3244
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
|
|
3305
3245
|
*
|
|
3306
3246
|
* await using run = createRun();
|
|
3307
3247
|
* const results = await run(
|
|
@@ -3311,12 +3251,12 @@ export function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
3311
3251
|
* expectTypeOf(results).toEqualTypeOf<
|
|
3312
3252
|
* Result<{
|
|
3313
3253
|
* readonly user: Result<User>;
|
|
3314
|
-
* readonly profile: Result<string,
|
|
3254
|
+
* readonly profile: Result<string, ProfileNotFoundError>;
|
|
3315
3255
|
* }>
|
|
3316
3256
|
* >();
|
|
3317
3257
|
* expectOk(results, {
|
|
3318
3258
|
* user: { ok: true, value: { id: "user-1" } },
|
|
3319
|
-
* profile: { ok: false, error: { type: "
|
|
3259
|
+
* profile: { ok: false, error: { type: "ProfileNotFound" } },
|
|
3320
3260
|
* });
|
|
3321
3261
|
* ```
|
|
3322
3262
|
*/
|
|
@@ -3326,7 +3266,7 @@ export function allSettled<const TTasks extends TaskRecord>(
|
|
|
3326
3266
|
): Task<InferTasksSettled<TTasks>, never, InferTaskRecordDeps<TTasks>>;
|
|
3327
3267
|
|
|
3328
3268
|
/**
|
|
3329
|
-
* Maps an array to
|
|
3269
|
+
* Maps an array to {@link Task}s and preserves its shape.
|
|
3330
3270
|
*
|
|
3331
3271
|
* ### Example
|
|
3332
3272
|
*
|
|
@@ -3338,18 +3278,19 @@ export function allSettled<const TTasks extends TaskRecord>(
|
|
|
3338
3278
|
* ok,
|
|
3339
3279
|
* type Result,
|
|
3340
3280
|
* type Task,
|
|
3281
|
+
* type Typed,
|
|
3341
3282
|
* } from "@evolu/common";
|
|
3342
3283
|
*
|
|
3343
3284
|
* interface User {
|
|
3344
3285
|
* readonly id: string;
|
|
3345
3286
|
* }
|
|
3346
|
-
*
|
|
3347
|
-
* readonly type: "LoadError";
|
|
3348
|
-
* }
|
|
3287
|
+
*
|
|
3349
3288
|
* const loadUser =
|
|
3350
|
-
* (id: string): Task<User,
|
|
3289
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
3351
3290
|
* () =>
|
|
3352
|
-
* id === "missing" ? err({ type: "
|
|
3291
|
+
* id === "missing" ? err({ type: "UserNotFound" }) : ok({ id });
|
|
3292
|
+
*
|
|
3293
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {}
|
|
3353
3294
|
*
|
|
3354
3295
|
* const userIds = ["user-1", "missing"] as const;
|
|
3355
3296
|
* const indexes: Array<number> = [];
|
|
@@ -3364,11 +3305,16 @@ export function allSettled<const TTasks extends TaskRecord>(
|
|
|
3364
3305
|
* await using run = createRun();
|
|
3365
3306
|
* const results = await run(loadUsers);
|
|
3366
3307
|
* expectTypeOf(results).toEqualTypeOf<
|
|
3367
|
-
* Result<
|
|
3308
|
+
* Result<
|
|
3309
|
+
* readonly [
|
|
3310
|
+
* Result<User, UserNotFoundError>,
|
|
3311
|
+
* Result<User, UserNotFoundError>,
|
|
3312
|
+
* ]
|
|
3313
|
+
* >
|
|
3368
3314
|
* >();
|
|
3369
3315
|
* expectOk(results, [
|
|
3370
3316
|
* { ok: true, value: { id: "user-1" } },
|
|
3371
|
-
* { ok: false, error: { type: "
|
|
3317
|
+
* { ok: false, error: { type: "UserNotFound" } },
|
|
3372
3318
|
* ]);
|
|
3373
3319
|
* ```
|
|
3374
3320
|
*/
|
|
@@ -3391,7 +3337,7 @@ export function allSettled<
|
|
|
3391
3337
|
>;
|
|
3392
3338
|
|
|
3393
3339
|
/**
|
|
3394
|
-
* Maps record values to
|
|
3340
|
+
* Maps record values to {@link Task}s and preserves the record's keys.
|
|
3395
3341
|
*
|
|
3396
3342
|
* ### Example
|
|
3397
3343
|
*
|
|
@@ -3403,18 +3349,19 @@ export function allSettled<
|
|
|
3403
3349
|
* ok,
|
|
3404
3350
|
* type Result,
|
|
3405
3351
|
* type Task,
|
|
3352
|
+
* type Typed,
|
|
3406
3353
|
* } from "@evolu/common";
|
|
3407
3354
|
*
|
|
3408
3355
|
* interface User {
|
|
3409
3356
|
* readonly id: string;
|
|
3410
3357
|
* }
|
|
3411
|
-
*
|
|
3412
|
-
* readonly type: "LoadError";
|
|
3413
|
-
* }
|
|
3358
|
+
*
|
|
3414
3359
|
* const loadUser =
|
|
3415
|
-
* (id: string): Task<User,
|
|
3360
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
3416
3361
|
* () =>
|
|
3417
|
-
* id === "missing" ? err({ type: "
|
|
3362
|
+
* id === "missing" ? err({ type: "UserNotFound" }) : ok({ id });
|
|
3363
|
+
*
|
|
3364
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {}
|
|
3418
3365
|
*
|
|
3419
3366
|
* const userIdsByRole = { admin: "user-1", reviewer: "missing" } as const;
|
|
3420
3367
|
* const roles: Array<keyof typeof userIdsByRole> = [];
|
|
@@ -3430,13 +3377,13 @@ export function allSettled<
|
|
|
3430
3377
|
* const results = await run(loadUsersByRole);
|
|
3431
3378
|
* expectTypeOf(results).toEqualTypeOf<
|
|
3432
3379
|
* Result<{
|
|
3433
|
-
* readonly admin: Result<User,
|
|
3434
|
-
* readonly reviewer: Result<User,
|
|
3380
|
+
* readonly admin: Result<User, UserNotFoundError>;
|
|
3381
|
+
* readonly reviewer: Result<User, UserNotFoundError>;
|
|
3435
3382
|
* }>
|
|
3436
3383
|
* >();
|
|
3437
3384
|
* expectOk(results, {
|
|
3438
3385
|
* admin: { ok: true, value: { id: "user-1" } },
|
|
3439
|
-
* reviewer: { ok: false, error: { type: "
|
|
3386
|
+
* reviewer: { ok: false, error: { type: "UserNotFound" } },
|
|
3440
3387
|
* });
|
|
3441
3388
|
* ```
|
|
3442
3389
|
*/
|
|
@@ -3543,10 +3490,11 @@ const mapInput = (
|
|
|
3543
3490
|
* callbacks. Resolve with `ok(value)` or `err(error)`, or reject with a defect,
|
|
3544
3491
|
* to complete the Task. Settlement is one-shot: the first `resolve` or `reject`
|
|
3545
3492
|
* wins, and later settlement calls are ignored, matching Promise semantics.
|
|
3546
|
-
* When `reject` wins settlement, the defect panics the Run tree and is
|
|
3547
|
-
* at Fiber boundaries as {@link AbortError} with
|
|
3548
|
-
* Rejecting AbortError is reserved for Task abort
|
|
3549
|
-
* `run(task)` rejects with it, and `run.abortable(task)`
|
|
3493
|
+
* When `reject` wins settlement, the defect panics the {@link Run} tree and is
|
|
3494
|
+
* observed at {@link Fiber} boundaries as {@link AbortError} with
|
|
3495
|
+
* {@link PanicAbortReason}. Rejecting AbortError is reserved for Task abort
|
|
3496
|
+
* control flow: direct `run(task)` rejects with it, and `run.abortable(task)`
|
|
3497
|
+
* returns it as an {@link Err}.
|
|
3550
3498
|
*
|
|
3551
3499
|
* This helper is a callback bridge. If `reject` forwards an Error created in a
|
|
3552
3500
|
* separate async chain, V8 cannot reconstruct the caller's zero-cost async
|
|
@@ -3561,10 +3509,10 @@ const mapInput = (
|
|
|
3561
3509
|
* Optionally return a synchronous cleanup callback. It runs when the Task Run
|
|
3562
3510
|
* signal aborts, including normal completion and explicit abort. The callback
|
|
3563
3511
|
* must not throw. Cleanup defects panic the root Run; if the callback Task
|
|
3564
|
-
* already settled, its Fiber keeps the original Result while the root
|
|
3565
|
-
* reports the panic. Cleanup must be synchronous; returned promises are
|
|
3566
|
-
* awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
|
|
3567
|
-
* with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
3512
|
+
* already settled, its Fiber keeps the original {@link Result} while the root
|
|
3513
|
+
* still reports the panic. Cleanup must be synchronous; returned promises are
|
|
3514
|
+
* not awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
|
|
3515
|
+
* using` with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
3568
3516
|
* explicitly.
|
|
3569
3517
|
*
|
|
3570
3518
|
* If setup can throw after acquiring any resource, use a local
|
|
@@ -3625,7 +3573,16 @@ export const callback =
|
|
|
3625
3573
|
/**
|
|
3626
3574
|
* Pauses execution for a specified {@link PositiveDuration}.
|
|
3627
3575
|
*
|
|
3628
|
-
* Aborting the Task clears the scheduled timeout.
|
|
3576
|
+
* Aborting the {@link Task} clears the scheduled timeout.
|
|
3577
|
+
*
|
|
3578
|
+
* ### Example
|
|
3579
|
+
*
|
|
3580
|
+
* ```ts
|
|
3581
|
+
* import { createRun, sleep } from "@evolu/common";
|
|
3582
|
+
*
|
|
3583
|
+
* await using run = createRun();
|
|
3584
|
+
* expectOk(await run(sleep("1ms")), undefined);
|
|
3585
|
+
* ```
|
|
3629
3586
|
*
|
|
3630
3587
|
* @group Timing
|
|
3631
3588
|
*/
|
|
@@ -3635,22 +3592,6 @@ export const sleep = (duration: PositiveDuration): Task<void> =>
|
|
|
3635
3592
|
return () => deps.time.clearTimeout(id);
|
|
3636
3593
|
});
|
|
3637
3594
|
|
|
3638
|
-
/**
|
|
3639
|
-
* Typed error returned by {@link timeout} when a Task exceeds its duration.
|
|
3640
|
-
*
|
|
3641
|
-
* @group Timing
|
|
3642
|
-
*/
|
|
3643
|
-
export const TimeoutError: TypedType<"TimeoutError"> =
|
|
3644
|
-
/*#__PURE__*/ typed("TimeoutError");
|
|
3645
|
-
export interface TimeoutError extends InferType<typeof TimeoutError> {}
|
|
3646
|
-
|
|
3647
|
-
/**
|
|
3648
|
-
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
3649
|
-
*
|
|
3650
|
-
* @group Timing
|
|
3651
|
-
*/
|
|
3652
|
-
export const timeoutError: TimeoutError = { type: "TimeoutError" };
|
|
3653
|
-
|
|
3654
3595
|
/**
|
|
3655
3596
|
* Limits how long a {@link Task} may run.
|
|
3656
3597
|
*
|
|
@@ -3693,6 +3634,29 @@ export const timeout = <T, E, D = unknown>(
|
|
|
3693
3634
|
},
|
|
3694
3635
|
]);
|
|
3695
3636
|
|
|
3637
|
+
/**
|
|
3638
|
+
* Runtime Type for the error returned by {@link timeout} when a {@link Task}
|
|
3639
|
+
* exceeds its duration.
|
|
3640
|
+
*
|
|
3641
|
+
* @group Timing
|
|
3642
|
+
*/
|
|
3643
|
+
export const TimeoutError: TypedType<"TimeoutError"> =
|
|
3644
|
+
/*#__PURE__*/ typed("TimeoutError");
|
|
3645
|
+
|
|
3646
|
+
/**
|
|
3647
|
+
* Error returned by {@link timeout} when a {@link Task} exceeds its duration.
|
|
3648
|
+
*
|
|
3649
|
+
* @group Timing
|
|
3650
|
+
*/
|
|
3651
|
+
export interface TimeoutError extends InferType<typeof TimeoutError> {}
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
3655
|
+
*
|
|
3656
|
+
* @group Timing
|
|
3657
|
+
*/
|
|
3658
|
+
export const timeoutError: TimeoutError = { type: "TimeoutError" };
|
|
3659
|
+
|
|
3696
3660
|
/**
|
|
3697
3661
|
* Options for {@link retry}.
|
|
3698
3662
|
*
|
|
@@ -3705,7 +3669,7 @@ export interface RetryOptions<E, Output> {
|
|
|
3705
3669
|
* Returning `false` stops retrying and returns {@link RetryError}. The
|
|
3706
3670
|
* original error is stored as `lastError` instead of returned directly. The
|
|
3707
3671
|
* predicate must not throw: a thrown exception is a defect that panics the
|
|
3708
|
-
* Run tree.
|
|
3672
|
+
* {@link Run} tree.
|
|
3709
3673
|
*/
|
|
3710
3674
|
readonly shouldRetry?: Predicate<E>;
|
|
3711
3675
|
|
|
@@ -3715,8 +3679,8 @@ export interface RetryOptions<E, Output> {
|
|
|
3715
3679
|
* `attempt` is the failed attempt that caused this retry, not the upcoming
|
|
3716
3680
|
* attempt number. The callback runs after {@link RetryOptions.shouldRetry} and
|
|
3717
3681
|
* the {@link Schedule} approve a retry, and before the retry delay. The
|
|
3718
|
-
* callback must not throw: a thrown exception is a defect that panics the
|
|
3719
|
-
* tree.
|
|
3682
|
+
* callback must not throw: a thrown exception is a defect that panics the
|
|
3683
|
+
* {@link Run} tree.
|
|
3720
3684
|
*/
|
|
3721
3685
|
readonly onRetry?: (attempt: RetryAttempt<E, Output>) => void;
|
|
3722
3686
|
}
|
|
@@ -3732,48 +3696,23 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
|
|
|
3732
3696
|
}
|
|
3733
3697
|
|
|
3734
3698
|
/**
|
|
3735
|
-
*
|
|
3736
|
-
*
|
|
3737
|
-
* @group Resilience
|
|
3738
|
-
*/
|
|
3739
|
-
export interface RetryError<E> extends Typed<"RetryError"> {
|
|
3740
|
-
/** The final domain error that stopped retrying. */
|
|
3741
|
-
readonly lastError: E;
|
|
3742
|
-
/** The number of attempts that were started. */
|
|
3743
|
-
readonly attempts: PositiveInt;
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
/**
|
|
3747
|
-
* Error type returned by {@link retry}.
|
|
3748
|
-
*
|
|
3749
|
-
* @group Resilience
|
|
3750
|
-
*/
|
|
3751
|
-
export type RetryTaskError<E> =
|
|
3752
|
-
// Wrap all non-abort errors in one RetryError, preserving their union.
|
|
3753
|
-
| ([Exclude<E, AbortError>] extends [never]
|
|
3754
|
-
? never
|
|
3755
|
-
: RetryError<Exclude<E, AbortError>>)
|
|
3756
|
-
// AbortError is control flow, so retry returns it unchanged.
|
|
3757
|
-
| Extract<E, AbortError>;
|
|
3758
|
-
|
|
3759
|
-
/**
|
|
3760
|
-
* Retries a Task according to a {@link Schedule}.
|
|
3699
|
+
* Retries a {@link Task} according to a {@link Schedule}.
|
|
3761
3700
|
*
|
|
3762
3701
|
* Use `retry` for failure recovery: it repeats after {@link Err} and wraps the
|
|
3763
3702
|
* final domain error. Use {@link repeat} for success-driven loops: it repeats
|
|
3764
|
-
* after {@link Ok} and returns the Task's natural Result.
|
|
3703
|
+
* after {@link Ok} and returns the Task's natural {@link Result}.
|
|
3765
3704
|
*
|
|
3766
3705
|
* {@link AbortError} passes through unchanged when returned as a Result error,
|
|
3767
|
-
* such as from {@link daemon}. Abort from `run(task)` remains Fiber
|
|
3768
|
-
* flow. Other errors are domain errors: retrying continues while the
|
|
3769
|
-
* yields another delay and {@link RetryOptions.shouldRetry} returns
|
|
3770
|
-
* retrying stops, `retry` returns {@link RetryError} with the final
|
|
3771
|
-
* as `lastError`.
|
|
3706
|
+
* such as from {@link daemon}. Abort from `run(task)` remains {@link Fiber}
|
|
3707
|
+
* control flow. Other errors are domain errors: retrying continues while the
|
|
3708
|
+
* schedule yields another delay and {@link RetryOptions.shouldRetry} returns
|
|
3709
|
+
* `true`. When retrying stops, `retry` returns {@link RetryError} with the final
|
|
3710
|
+
* domain error as `lastError`.
|
|
3772
3711
|
*
|
|
3773
3712
|
* AbortError recognition is structural. Domain errors, especially values parsed
|
|
3774
3713
|
* from untrusted input, must not use the reserved AbortError shape.
|
|
3775
3714
|
*
|
|
3776
|
-
* ###
|
|
3715
|
+
* ### Retrying failures
|
|
3777
3716
|
*
|
|
3778
3717
|
* ```ts
|
|
3779
3718
|
* import {
|
|
@@ -3784,51 +3723,58 @@ export type RetryTaskError<E> =
|
|
|
3784
3723
|
* type Result,
|
|
3785
3724
|
* type RetryTaskError,
|
|
3786
3725
|
* type Task,
|
|
3726
|
+
* type Typed,
|
|
3787
3727
|
* } from "@evolu/common";
|
|
3788
3728
|
*
|
|
3789
|
-
*
|
|
3790
|
-
*
|
|
3791
|
-
* interface FetchDataError {
|
|
3792
|
-
* readonly type: "FetchDataError";
|
|
3793
|
-
* }
|
|
3729
|
+
* const fetchData: Task<string, ServiceUnavailableError> = () =>
|
|
3730
|
+
* err({ type: "ServiceUnavailable" });
|
|
3794
3731
|
*
|
|
3795
|
-
*
|
|
3796
|
-
* err({ type: "FetchDataError" });
|
|
3732
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
3797
3733
|
*
|
|
3798
3734
|
* const fetchWithRetry = retry(fetchData, recurs(2));
|
|
3799
3735
|
*
|
|
3736
|
+
* await using run = createRun();
|
|
3800
3737
|
* const result = await run(fetchWithRetry);
|
|
3801
3738
|
* expectTypeOf(result).toEqualTypeOf<
|
|
3802
|
-
* Result<string, RetryTaskError<
|
|
3739
|
+
* Result<string, RetryTaskError<ServiceUnavailableError>>
|
|
3803
3740
|
* >();
|
|
3804
3741
|
* expectErr(result, {
|
|
3805
3742
|
* type: "RetryError",
|
|
3806
3743
|
* attempts: 3,
|
|
3807
|
-
* lastError: { type: "
|
|
3744
|
+
* lastError: { type: "ServiceUnavailable" },
|
|
3808
3745
|
* });
|
|
3809
3746
|
* ```
|
|
3810
3747
|
*
|
|
3811
|
-
* ###
|
|
3748
|
+
* ### Filtering retries
|
|
3812
3749
|
*
|
|
3813
3750
|
* ```ts
|
|
3814
|
-
* import {
|
|
3751
|
+
* import {
|
|
3752
|
+
* createRun,
|
|
3753
|
+
* err,
|
|
3754
|
+
* recurs,
|
|
3755
|
+
* retry,
|
|
3756
|
+
* type Task,
|
|
3757
|
+
* type Typed,
|
|
3758
|
+
* } from "@evolu/common";
|
|
3815
3759
|
*
|
|
3816
|
-
*
|
|
3817
|
-
*
|
|
3818
|
-
*
|
|
3760
|
+
* const fetchData: Task<
|
|
3761
|
+
* string,
|
|
3762
|
+
* TemporaryFailureError | PermanentFailureError
|
|
3763
|
+
* > = () => err({ type: "PermanentFailure" });
|
|
3764
|
+
*
|
|
3765
|
+
* interface TemporaryFailureError extends Typed<"TemporaryFailure"> {}
|
|
3819
3766
|
*
|
|
3820
|
-
*
|
|
3821
|
-
* err({ type: "FatalError" });
|
|
3767
|
+
* interface PermanentFailureError extends Typed<"PermanentFailure"> {}
|
|
3822
3768
|
*
|
|
3823
3769
|
* const fetchWithRetry = retry(fetchData, recurs(5), {
|
|
3824
|
-
* shouldRetry: (error) => error.type !== "
|
|
3770
|
+
* shouldRetry: (error) => error.type !== "PermanentFailure",
|
|
3825
3771
|
* });
|
|
3826
3772
|
*
|
|
3827
3773
|
* await using run = createRun();
|
|
3828
3774
|
* expectErr(await run(fetchWithRetry), {
|
|
3829
3775
|
* type: "RetryError",
|
|
3830
3776
|
* attempts: 1,
|
|
3831
|
-
* lastError: { type: "
|
|
3777
|
+
* lastError: { type: "PermanentFailure" },
|
|
3832
3778
|
* });
|
|
3833
3779
|
* ```
|
|
3834
3780
|
*
|
|
@@ -3871,11 +3817,36 @@ export const retry =
|
|
|
3871
3817
|
|
|
3872
3818
|
const [output, delay] = next.value;
|
|
3873
3819
|
onRetry?.({ error, attempt, output, delay });
|
|
3874
|
-
attempt =
|
|
3820
|
+
attempt = PositiveInt.orThrow(increment(attempt));
|
|
3875
3821
|
if (delay > 0) await run.ok(sleep(PositiveMillis.orThrow(delay)));
|
|
3876
3822
|
}
|
|
3877
3823
|
};
|
|
3878
3824
|
|
|
3825
|
+
/**
|
|
3826
|
+
* Error returned by {@link retry} when retrying stops after a domain error.
|
|
3827
|
+
*
|
|
3828
|
+
* @group Resilience
|
|
3829
|
+
*/
|
|
3830
|
+
export interface RetryError<E> extends Typed<"RetryError"> {
|
|
3831
|
+
/** The final domain error that stopped retrying. */
|
|
3832
|
+
readonly lastError: E;
|
|
3833
|
+
/** The number of attempts that were started. */
|
|
3834
|
+
readonly attempts: PositiveInt;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
/**
|
|
3838
|
+
* Error type returned by {@link retry}.
|
|
3839
|
+
*
|
|
3840
|
+
* @group Resilience
|
|
3841
|
+
*/
|
|
3842
|
+
export type RetryTaskError<E> =
|
|
3843
|
+
// Wrap all non-abort errors in one RetryError, preserving their union.
|
|
3844
|
+
| ([Exclude<E, AbortError>] extends [never]
|
|
3845
|
+
? never
|
|
3846
|
+
: RetryError<Exclude<E, AbortError>>)
|
|
3847
|
+
// AbortError is control flow, so retry returns it unchanged.
|
|
3848
|
+
| Extract<E, AbortError>;
|
|
3849
|
+
|
|
3879
3850
|
/**
|
|
3880
3851
|
* Options for {@link repeat}.
|
|
3881
3852
|
*
|
|
@@ -3883,10 +3854,10 @@ export const retry =
|
|
|
3883
3854
|
*/
|
|
3884
3855
|
export interface RepeatOptions<T, Output> {
|
|
3885
3856
|
/**
|
|
3886
|
-
* Decides whether an Ok value should schedule another repeat.
|
|
3857
|
+
* Decides whether an {@link Ok} value should schedule another repeat.
|
|
3887
3858
|
*
|
|
3888
3859
|
* The predicate must not throw: a thrown exception is a defect that panics
|
|
3889
|
-
* the Run tree.
|
|
3860
|
+
* the {@link Run} tree.
|
|
3890
3861
|
*/
|
|
3891
3862
|
readonly shouldRepeat?: Predicate<T>;
|
|
3892
3863
|
|
|
@@ -3897,7 +3868,7 @@ export interface RepeatOptions<T, Output> {
|
|
|
3897
3868
|
* upcoming attempt number. The callback runs after
|
|
3898
3869
|
* {@link RepeatOptions.shouldRepeat} and the {@link Schedule} approve a repeat,
|
|
3899
3870
|
* and before the repeat delay. The callback must not throw: a thrown
|
|
3900
|
-
* exception is a defect that panics the Run tree.
|
|
3871
|
+
* exception is a defect that panics the {@link Run} tree.
|
|
3901
3872
|
*/
|
|
3902
3873
|
readonly onRepeat?: (attempt: RepeatAttempt<T, Output>) => void;
|
|
3903
3874
|
}
|
|
@@ -3908,18 +3879,19 @@ export interface RepeatOptions<T, Output> {
|
|
|
3908
3879
|
* @group Repetition
|
|
3909
3880
|
*/
|
|
3910
3881
|
export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
3911
|
-
/** The Ok value returned by the completed attempt. */
|
|
3882
|
+
/** The {@link Ok} value returned by the completed attempt. */
|
|
3912
3883
|
readonly value: T;
|
|
3913
3884
|
}
|
|
3914
3885
|
|
|
3915
3886
|
/**
|
|
3916
|
-
* Repeats a Task according to a {@link Schedule}.
|
|
3887
|
+
* Repeats a {@link Task} according to a {@link Schedule}.
|
|
3917
3888
|
*
|
|
3918
3889
|
* Runs the Task once, then repeats while the Task returns {@link Ok}, the
|
|
3919
3890
|
* schedule yields another delay, and {@link RepeatOptions.shouldRepeat} returns
|
|
3920
|
-
* `true`. When repeating stops, `repeat` returns the last successful
|
|
3921
|
-
* the Task returns {@link Err}, including {@link Done} from a
|
|
3922
|
-
* `repeat` returns that error without scheduling another
|
|
3891
|
+
* `true`. When repeating stops, `repeat` returns the last successful
|
|
3892
|
+
* {@link Result}. If the Task returns {@link Err}, including {@link Done} from a
|
|
3893
|
+
* {@link NextTask}, `repeat` returns that error without scheduling another
|
|
3894
|
+
* attempt.
|
|
3923
3895
|
*
|
|
3924
3896
|
* Use `repeat` for success-driven loops such as polling or consuming a
|
|
3925
3897
|
* NextTask: it repeats after Ok and returns the Task's natural Result. Use
|
|
@@ -3928,7 +3900,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
3928
3900
|
*
|
|
3929
3901
|
* With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
|
|
3930
3902
|
*
|
|
3931
|
-
* ###
|
|
3903
|
+
* ### Repeating successes
|
|
3932
3904
|
*
|
|
3933
3905
|
* ```ts
|
|
3934
3906
|
* import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
|
|
@@ -3946,7 +3918,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
3946
3918
|
* expect(attempts).toBe(4);
|
|
3947
3919
|
* ```
|
|
3948
3920
|
*
|
|
3949
|
-
* ###
|
|
3921
|
+
* ### Stopping with Done
|
|
3950
3922
|
*
|
|
3951
3923
|
* ```ts
|
|
3952
3924
|
* import {
|
|
@@ -3959,8 +3931,6 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
3959
3931
|
* type NextTask,
|
|
3960
3932
|
* } from "@evolu/common";
|
|
3961
3933
|
*
|
|
3962
|
-
* await using run = createRun();
|
|
3963
|
-
*
|
|
3964
3934
|
* interface Item {
|
|
3965
3935
|
* readonly id: string;
|
|
3966
3936
|
* }
|
|
@@ -3972,6 +3942,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
3972
3942
|
* return item ? ok(item) : err(done());
|
|
3973
3943
|
* };
|
|
3974
3944
|
*
|
|
3945
|
+
* await using run = createRun();
|
|
3975
3946
|
* const result = await run(repeat(processQueue, spaced("1ms")));
|
|
3976
3947
|
* expectErr(result, done());
|
|
3977
3948
|
* expect(queue).toEqual([]);
|
|
@@ -4000,36 +3971,37 @@ export const repeat =
|
|
|
4000
3971
|
|
|
4001
3972
|
const [output, delay] = next.value;
|
|
4002
3973
|
onRepeat?.({ value: result.value, attempt, output, delay });
|
|
4003
|
-
attempt =
|
|
3974
|
+
attempt = PositiveInt.orThrow(increment(attempt));
|
|
4004
3975
|
if (delay > 0) await run.ok(sleep(PositiveMillis.orThrow(delay)));
|
|
4005
3976
|
}
|
|
4006
3977
|
};
|
|
4007
3978
|
|
|
4008
3979
|
/**
|
|
4009
|
-
* Extracts the Result type produced by one Task in a non-empty
|
|
3980
|
+
* Extracts the {@link Result} type produced by one {@link Task} in a non-empty
|
|
3981
|
+
* Task array.
|
|
4010
3982
|
*
|
|
4011
|
-
* @
|
|
3983
|
+
* @group Type utilities
|
|
4012
3984
|
*/
|
|
4013
3985
|
export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> =
|
|
4014
3986
|
Result<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>>;
|
|
4015
3987
|
|
|
4016
3988
|
/**
|
|
4017
|
-
* Runs
|
|
3989
|
+
* Runs {@link Task}s until one returns {@link Ok} or all return {@link Err}.
|
|
4018
3990
|
*
|
|
4019
|
-
* Use {@link race} to return the first settled Result instead, whether Ok
|
|
4020
|
-
* {@link Err}.
|
|
3991
|
+
* Use {@link race} to return the first settled {@link Result} instead, whether Ok
|
|
3992
|
+
* or {@link Err}.
|
|
4021
3993
|
*
|
|
4022
|
-
* Returns the first {@link Ok} Result.
|
|
4023
|
-
* returns Ok, returns the last Err by
|
|
4024
|
-
* order. Other Err results are discarded;
|
|
4025
|
-
* every error.
|
|
3994
|
+
* Returns the first {@link Ok} Result. Queued Tasks are not started, and other
|
|
3995
|
+
* running Tasks are aborted. If no Task returns Ok, returns the last Err by
|
|
3996
|
+
* input order, regardless of completion order. Other Err results are discarded;
|
|
3997
|
+
* use {@link allSettled} when you need every error.
|
|
4026
3998
|
*
|
|
4027
3999
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
4028
4000
|
* at a time.
|
|
4029
4001
|
*
|
|
4030
4002
|
* Similar to
|
|
4031
4003
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
|
|
4032
|
-
* but
|
|
4004
|
+
* but runs Tasks, returns Result values, and stops after the first Ok.
|
|
4033
4005
|
*
|
|
4034
4006
|
* ### Example
|
|
4035
4007
|
*
|
|
@@ -4041,10 +4013,14 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> =
|
|
|
4041
4013
|
* ok,
|
|
4042
4014
|
* type Result,
|
|
4043
4015
|
* type Task,
|
|
4016
|
+
* type Typed,
|
|
4044
4017
|
* } from "@evolu/common";
|
|
4045
4018
|
*
|
|
4046
|
-
* const unavailable: Task<string,
|
|
4047
|
-
* err("
|
|
4019
|
+
* const unavailable: Task<string, ServiceUnavailableError> = () =>
|
|
4020
|
+
* err({ type: "ServiceUnavailable" });
|
|
4021
|
+
*
|
|
4022
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
4023
|
+
*
|
|
4048
4024
|
* let fallbackStarted = false;
|
|
4049
4025
|
* const fallback: Task<string> = () => {
|
|
4050
4026
|
* fallbackStarted = true;
|
|
@@ -4054,7 +4030,9 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> =
|
|
|
4054
4030
|
* await using run = createRun();
|
|
4055
4031
|
* const result = await run(any([unavailable, fallback]));
|
|
4056
4032
|
*
|
|
4057
|
-
* expectTypeOf(result).toEqualTypeOf<
|
|
4033
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
4034
|
+
* Result<string, ServiceUnavailableError>
|
|
4035
|
+
* >();
|
|
4058
4036
|
* expectOk(result, "fallback");
|
|
4059
4037
|
* expect(fallbackStarted).toBe(true);
|
|
4060
4038
|
* ```
|
|
@@ -4102,7 +4080,7 @@ export const any =
|
|
|
4102
4080
|
};
|
|
4103
4081
|
|
|
4104
4082
|
/**
|
|
4105
|
-
* Runs
|
|
4083
|
+
* Runs {@link Task}s until the first Task settles.
|
|
4106
4084
|
*
|
|
4107
4085
|
* Returns the first Task {@link Result} to settle, whether {@link Ok} or
|
|
4108
4086
|
* {@link Err}.
|
|
@@ -4123,7 +4101,7 @@ export const any =
|
|
|
4123
4101
|
* arrays whose emptiness is only known at runtime, guard with
|
|
4124
4102
|
* {@link isNonEmptyArray}:
|
|
4125
4103
|
*
|
|
4126
|
-
* ###
|
|
4104
|
+
* ### Runtime arrays
|
|
4127
4105
|
*
|
|
4128
4106
|
* ```ts
|
|
4129
4107
|
* import {
|
|
@@ -4142,7 +4120,7 @@ export const any =
|
|
|
4142
4120
|
* }
|
|
4143
4121
|
* ```
|
|
4144
4122
|
*
|
|
4145
|
-
* ###
|
|
4123
|
+
* ### First settlement
|
|
4146
4124
|
*
|
|
4147
4125
|
* ```ts
|
|
4148
4126
|
* import {
|
|
@@ -4203,12 +4181,12 @@ export const race =
|
|
|
4203
4181
|
};
|
|
4204
4182
|
|
|
4205
4183
|
/**
|
|
4206
|
-
* Runs
|
|
4184
|
+
* Runs {@link Task}s until `count` Tasks return {@link Ok} or all Tasks settle.
|
|
4207
4185
|
*
|
|
4208
4186
|
* Returns {@link Ok} with Ok values in settlement order, not input order.
|
|
4209
|
-
* {@link Err}
|
|
4210
|
-
*
|
|
4211
|
-
* the Ok values that did settle.
|
|
4187
|
+
* {@link Err} {@link Result}s are ignored. When `count` Ok values have settled,
|
|
4188
|
+
* queued Tasks are not started and remaining running Tasks are aborted. If
|
|
4189
|
+
* fewer than `count` Tasks return Ok, returns the Ok values that did settle.
|
|
4212
4190
|
*
|
|
4213
4191
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
4214
4192
|
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
@@ -4224,17 +4202,23 @@ export const race =
|
|
|
4224
4202
|
* ok,
|
|
4225
4203
|
* sleep,
|
|
4226
4204
|
* type Task,
|
|
4205
|
+
* type Typed,
|
|
4227
4206
|
* } from "@evolu/common";
|
|
4228
4207
|
*
|
|
4229
4208
|
* let slowCompleted = false;
|
|
4230
|
-
* const slow: Task<string
|
|
4209
|
+
* const slow: Task<string> = async (run) => {
|
|
4231
4210
|
* await run.ok(sleep("10ms"));
|
|
4232
4211
|
* slowCompleted = true;
|
|
4233
4212
|
* return ok("slow");
|
|
4234
4213
|
* };
|
|
4214
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
4215
|
+
* err({ type: "ServiceUnavailable" });
|
|
4216
|
+
*
|
|
4217
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
4218
|
+
*
|
|
4235
4219
|
* const tasks = [
|
|
4236
4220
|
* slow,
|
|
4237
|
-
*
|
|
4221
|
+
* unavailable,
|
|
4238
4222
|
* () => ok("fast-1"),
|
|
4239
4223
|
* () => ok("fast-2"),
|
|
4240
4224
|
* ] as const;
|
|
@@ -4276,11 +4260,12 @@ export const firstN =
|
|
|
4276
4260
|
};
|
|
4277
4261
|
|
|
4278
4262
|
/**
|
|
4279
|
-
* Runs
|
|
4263
|
+
* Runs {@link Task}s until `count` Tasks settle or all Tasks settle.
|
|
4280
4264
|
*
|
|
4281
4265
|
* Returns {@link Ok} with Task {@link Result}s in settlement order, not input
|
|
4282
|
-
* order. When `count` Results have settled,
|
|
4283
|
-
* fewer than `count` Tasks settle,
|
|
4266
|
+
* order. When `count` Results have settled, queued Tasks are not started and
|
|
4267
|
+
* remaining running Tasks are aborted. If fewer than `count` Tasks settle,
|
|
4268
|
+
* returns the Results that did settle.
|
|
4284
4269
|
*
|
|
4285
4270
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
4286
4271
|
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
@@ -4296,25 +4281,27 @@ export const firstN =
|
|
|
4296
4281
|
* ok,
|
|
4297
4282
|
* sleep,
|
|
4298
4283
|
* type Task,
|
|
4284
|
+
* type Typed,
|
|
4299
4285
|
* } from "@evolu/common";
|
|
4300
4286
|
*
|
|
4301
4287
|
* let slowCompleted = false;
|
|
4302
|
-
* const slow: Task<string
|
|
4288
|
+
* const slow: Task<string> = async (run) => {
|
|
4303
4289
|
* await run.ok(sleep("10ms"));
|
|
4304
4290
|
* slowCompleted = true;
|
|
4305
4291
|
* return ok("slow");
|
|
4306
4292
|
* };
|
|
4307
|
-
* const
|
|
4308
|
-
*
|
|
4309
|
-
*
|
|
4310
|
-
*
|
|
4311
|
-
*
|
|
4293
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
4294
|
+
* err({ type: "ServiceUnavailable" });
|
|
4295
|
+
*
|
|
4296
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
4297
|
+
*
|
|
4298
|
+
* const tasks = [slow, unavailable, () => ok("fast")] as const;
|
|
4312
4299
|
* await using run = createRun();
|
|
4313
4300
|
*
|
|
4314
4301
|
* // Err and Ok both count, and Results use settlement order.
|
|
4315
4302
|
* const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
|
|
4316
4303
|
* expectOk(result, [
|
|
4317
|
-
* { ok: false, error: "
|
|
4304
|
+
* { ok: false, error: { type: "ServiceUnavailable" } },
|
|
4318
4305
|
* { ok: true, value: "fast" },
|
|
4319
4306
|
* ]);
|
|
4320
4307
|
* expect(slowCompleted).toBe(false);
|
|
@@ -4352,21 +4339,21 @@ export const firstNSettled =
|
|
|
4352
4339
|
/**
|
|
4353
4340
|
* Decision returned by an {@link each} result handler.
|
|
4354
4341
|
*
|
|
4355
|
-
* `continue` allows queued
|
|
4342
|
+
* `continue` allows queued {@link Task}s to start when concurrency capacity is
|
|
4356
4343
|
* available. `stop` prevents queued Tasks from starting and aborts already
|
|
4357
|
-
* running Tasks through structured Run disposal.
|
|
4344
|
+
* running Tasks through structured {@link Run} disposal.
|
|
4358
4345
|
*
|
|
4359
|
-
* @group
|
|
4346
|
+
* @group Collection
|
|
4360
4347
|
*/
|
|
4361
4348
|
export type EachDecision = "continue" | "stop";
|
|
4362
4349
|
|
|
4363
4350
|
/**
|
|
4364
|
-
* Handles one settled Task Result from {@link each}.
|
|
4351
|
+
* Handles one settled {@link Task} {@link Result} from {@link each}.
|
|
4365
4352
|
*
|
|
4366
4353
|
* The index is the original input index. Callback order follows settlement
|
|
4367
4354
|
* order, not input order.
|
|
4368
4355
|
*
|
|
4369
|
-
* @group
|
|
4356
|
+
* @group Collection
|
|
4370
4357
|
*/
|
|
4371
4358
|
export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
|
|
4372
4359
|
result: InferTasksResult<TTasks>,
|
|
@@ -4374,26 +4361,27 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
|
|
|
4374
4361
|
) => EachDecision;
|
|
4375
4362
|
|
|
4376
4363
|
/**
|
|
4377
|
-
* Runs
|
|
4378
|
-
* {@link Result} as it settles.
|
|
4364
|
+
* Runs {@link Task}s under a concurrency limit and calls `onResult` for each
|
|
4365
|
+
* Task {@link Result} as it settles.
|
|
4379
4366
|
*
|
|
4380
|
-
* `onResult` receives the Result and the original input index; call
|
|
4381
|
-
* settlement order, not input order. Returning `continue` lets queued
|
|
4382
|
-
* start when capacity is available. Returning `stop` prevents queued
|
|
4383
|
-
* starting and aborts already-running Tasks through structured
|
|
4384
|
-
* `each` still waits for them to settle before
|
|
4367
|
+
* `onResult` receives the {@link Result} and the original input index; call
|
|
4368
|
+
* order is settlement order, not input order. Returning `continue` lets queued
|
|
4369
|
+
* Tasks start when capacity is available. Returning `stop` prevents queued
|
|
4370
|
+
* Tasks from starting and aborts already-running Tasks through structured
|
|
4371
|
+
* {@link Run} disposal — `each` still waits for them to settle before
|
|
4372
|
+
* returning.
|
|
4385
4373
|
*
|
|
4386
4374
|
* `each` is the scheduling primitive under the collection helpers. Each one is
|
|
4387
4375
|
* a small `onResult` policy:
|
|
4388
4376
|
*
|
|
4389
|
-
* | Helper | Policy
|
|
4390
|
-
* | --------------------- |
|
|
4391
|
-
* | {@link all} | Collect values, stop on the first Err |
|
|
4392
|
-
* | {@link allSettled} | Collect every Result, never stop
|
|
4393
|
-
* | {@link any} | Stop on the first Ok |
|
|
4394
|
-
* | {@link race} | Stop on the first settled Result
|
|
4395
|
-
* | {@link firstN} | Stop after n Ok values
|
|
4396
|
-
* | {@link firstNSettled} | Stop after n Results
|
|
4377
|
+
* | Helper | Policy |
|
|
4378
|
+
* | --------------------- | --------------------------------------------- |
|
|
4379
|
+
* | {@link all} | Collect values, stop on the first {@link Err} |
|
|
4380
|
+
* | {@link allSettled} | Collect every Result, never stop |
|
|
4381
|
+
* | {@link any} | Stop on the first {@link Ok} |
|
|
4382
|
+
* | {@link race} | Stop on the first settled Result |
|
|
4383
|
+
* | {@link firstN} | Stop after n Ok values |
|
|
4384
|
+
* | {@link firstNSettled} | Stop after n Results |
|
|
4397
4385
|
*
|
|
4398
4386
|
* Use `each` directly to build a collection policy the helpers don't cover. For
|
|
4399
4387
|
* example, keep the first successful value together with its original input
|
|
@@ -4409,6 +4397,7 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
|
|
|
4409
4397
|
* ok,
|
|
4410
4398
|
* sleep,
|
|
4411
4399
|
* type Task,
|
|
4400
|
+
* type Typed,
|
|
4412
4401
|
* } from "@evolu/common";
|
|
4413
4402
|
*
|
|
4414
4403
|
* let slowCompleted = false;
|
|
@@ -4417,11 +4406,12 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
|
|
|
4417
4406
|
* slowCompleted = true;
|
|
4418
4407
|
* return ok("slow");
|
|
4419
4408
|
* };
|
|
4420
|
-
* const
|
|
4421
|
-
*
|
|
4422
|
-
*
|
|
4423
|
-
*
|
|
4424
|
-
*
|
|
4409
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
4410
|
+
* err({ type: "ServiceUnavailable" });
|
|
4411
|
+
*
|
|
4412
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
4413
|
+
*
|
|
4414
|
+
* const tasks = [slow, unavailable, () => ok("fast")] as const;
|
|
4425
4415
|
* let first: readonly [string, number] | undefined;
|
|
4426
4416
|
* await using run = createRun();
|
|
4427
4417
|
* const result = await run(
|
|
@@ -4455,7 +4445,7 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
|
|
|
4455
4445
|
* building on `each` preserves diagnostics that a hand-rolled scheduling loop
|
|
4456
4446
|
* typically loses.
|
|
4457
4447
|
*
|
|
4458
|
-
* @group
|
|
4448
|
+
* @group Collection
|
|
4459
4449
|
*/
|
|
4460
4450
|
export const each =
|
|
4461
4451
|
<TTasks extends NonEmptyReadonlyArray<AnyTask>>(
|
|
@@ -4523,10 +4513,9 @@ export const each =
|
|
|
4523
4513
|
};
|
|
4524
4514
|
|
|
4525
4515
|
/**
|
|
4526
|
-
* Scheduler priority for
|
|
4516
|
+
* Scheduler priority for {@link Task}s started through a native scheduler.
|
|
4527
4517
|
*
|
|
4528
|
-
*
|
|
4529
|
-
* added in a future release.
|
|
4518
|
+
* A Task's priority is static while it runs.
|
|
4530
4519
|
*
|
|
4531
4520
|
* @group Scheduling
|
|
4532
4521
|
*/
|
|
@@ -4568,9 +4557,9 @@ export const prioritized = <T, E, D = unknown>(
|
|
|
4568
4557
|
* Yields execution to the host scheduler.
|
|
4569
4558
|
*
|
|
4570
4559
|
* Uses native `scheduler.yield()` when available, `setImmediate` when
|
|
4571
|
-
* available, and `setTimeout` elsewhere. Because this is a Task, `await
|
|
4572
|
-
* run(yieldNow)` is an explicit abortable checkpoint and is visible in
|
|
4573
|
-
* monitoring.
|
|
4560
|
+
* available, and `setTimeout` elsewhere. Because this is a {@link Task}, `await
|
|
4561
|
+
* run(yieldNow)` is an explicit abortable checkpoint and is visible in
|
|
4562
|
+
* {@link Run} monitoring.
|
|
4574
4563
|
*
|
|
4575
4564
|
* For example, call it periodically in a long-running synchronous loop to let
|
|
4576
4565
|
* the host process rendering, input, and other scheduled work.
|
|
@@ -4643,12 +4632,13 @@ export const yieldNow: Task<void> = async (run) => {
|
|
|
4643
4632
|
* interface ServerDep {
|
|
4644
4633
|
* readonly port: number;
|
|
4645
4634
|
* }
|
|
4635
|
+
*
|
|
4646
4636
|
* interface Server extends AsyncDisposable {}
|
|
4647
4637
|
*
|
|
4648
4638
|
* const serverStarted = Promise.withResolvers<void>();
|
|
4649
4639
|
* let serverStopped = false;
|
|
4650
|
-
* const startServer: Task<Server, never, ServerDep> = (
|
|
4651
|
-
* expect(deps.port).toBe(3000);
|
|
4640
|
+
* const startServer: Task<Server, never, ServerDep> = (run) => {
|
|
4641
|
+
* expect(run.deps.port).toBe(3000);
|
|
4652
4642
|
* serverStarted.resolve();
|
|
4653
4643
|
* return ok({
|
|
4654
4644
|
* [Symbol.asyncDispose]: async () => {
|
|
@@ -4703,7 +4693,7 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4703
4693
|
* Do not wrap a Task that keeps using a resource the caller may release after
|
|
4704
4694
|
* this wrapper returns, unless the Task reliably observes abort before using
|
|
4705
4695
|
* that resource. The daemon Task can continue after the caller stops waiting.
|
|
4706
|
-
* Later domain
|
|
4696
|
+
* Later domain {@link Err} results from the daemon Task are discarded after the
|
|
4707
4697
|
* caller stops waiting. Defects from the daemon Task remain visible to the root
|
|
4708
4698
|
* Run: if it later throws or rejects, the root Run still panics and reports the
|
|
4709
4699
|
* defect.
|
|
@@ -4713,10 +4703,10 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4713
4703
|
* to settle, keeping cleanup and late defects inside the caller's lifetime. A
|
|
4714
4704
|
* Task that ignores abort can keep them waiting.
|
|
4715
4705
|
*
|
|
4716
|
-
* `run.abortable(task)` returns an owned child Fiber and requests abort
|
|
4717
|
-
* that Fiber; `daemon(task)` starts a daemon child and stops waiting
|
|
4718
|
-
* current Run aborts. {@link unabortable} masks abort for a Task that
|
|
4719
|
-
* finish once started; daemon lets a Task outlive the caller.
|
|
4706
|
+
* `run.abortable(task)` returns an owned child {@link Fiber} and requests abort
|
|
4707
|
+
* through that Fiber; `daemon(task)` starts a daemon child and stops waiting
|
|
4708
|
+
* when the current Run aborts. {@link unabortable} masks abort for a Task that
|
|
4709
|
+
* must finish once started; daemon lets a Task outlive the caller.
|
|
4720
4710
|
*
|
|
4721
4711
|
* Because the Task starts with {@link Run.daemon}, a recorded abort request
|
|
4722
4712
|
* returns AbortError before the Task starts — including a request masked by
|
|
@@ -4763,15 +4753,10 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4763
4753
|
* import { createRun, ok, type Result, type Task } from "@evolu/common";
|
|
4764
4754
|
*
|
|
4765
4755
|
* type ResultValue = string;
|
|
4766
|
-
*
|
|
4767
|
-
*
|
|
4768
|
-
* }
|
|
4769
|
-
* const createPromiseReturningResult = (): Promise<
|
|
4770
|
-
* Result<ResultValue, MyError>
|
|
4771
|
-
* > => Promise.resolve(ok("value"));
|
|
4756
|
+
* const createPromiseReturningResult = (): Promise<Result<ResultValue>> =>
|
|
4757
|
+
* Promise.resolve(ok("value"));
|
|
4772
4758
|
*
|
|
4773
|
-
* const task: Task<ResultValue
|
|
4774
|
-
* createPromiseReturningResult();
|
|
4759
|
+
* const task: Task<ResultValue> = () => createPromiseReturningResult();
|
|
4775
4760
|
*
|
|
4776
4761
|
* await using run = createRun();
|
|
4777
4762
|
* expectOk(await run(task), "value");
|
|
@@ -4784,12 +4769,9 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4784
4769
|
* import { ok, type Result, type Task } from "@evolu/common";
|
|
4785
4770
|
*
|
|
4786
4771
|
* type ResultValue = string;
|
|
4787
|
-
* interface MyError {
|
|
4788
|
-
* readonly type: "MyError";
|
|
4789
|
-
* }
|
|
4790
4772
|
* let promiseStarted = false;
|
|
4791
4773
|
* const createPromiseReturningResult = (): Promise<
|
|
4792
|
-
* Result<ResultValue
|
|
4774
|
+
* Result<ResultValue>
|
|
4793
4775
|
* > => {
|
|
4794
4776
|
* promiseStarted = true;
|
|
4795
4777
|
* return Promise.resolve(ok("value"));
|
|
@@ -4797,10 +4779,10 @@ export const waitForAbort: Task<never> = async (run) => {
|
|
|
4797
4779
|
*
|
|
4798
4780
|
* // Wrong: the Promise starts now, before a Run starts the Task.
|
|
4799
4781
|
* const promise = createPromiseReturningResult();
|
|
4800
|
-
* const task: Task<ResultValue
|
|
4782
|
+
* const task: Task<ResultValue> = () => promise;
|
|
4801
4783
|
*
|
|
4802
4784
|
* expect(promiseStarted).toBe(true);
|
|
4803
|
-
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue
|
|
4785
|
+
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue>>();
|
|
4804
4786
|
* ```
|
|
4805
4787
|
*
|
|
4806
4788
|
* @group Lifetime
|
|
@@ -4829,8 +4811,8 @@ export const daemon =
|
|
|
4829
4811
|
* Abort requests are masked while the Task runs, so `run.signal.aborted`
|
|
4830
4812
|
* remains false inside the Task. This does not force the Task to start after an
|
|
4831
4813
|
* abort request has already reached its Run; unabortable means the Task is not
|
|
4832
|
-
* interrupted once it has started. Disposing the enclosing Run still
|
|
4833
|
-
* the Task to settle.
|
|
4814
|
+
* interrupted once it has started. Disposing the enclosing {@link Run} still
|
|
4815
|
+
* waits for the Task to settle.
|
|
4834
4816
|
*
|
|
4835
4817
|
* Apply at most one abort behavior helper to a Task: do not wrap the same Task
|
|
4836
4818
|
* with both unabortable and restore, or apply either helper more than once.
|
|
@@ -4842,10 +4824,10 @@ export const daemon =
|
|
|
4842
4824
|
*
|
|
4843
4825
|
* const commitStarted = Promise.withResolvers<void>();
|
|
4844
4826
|
* const finishCommit = Promise.withResolvers<void>();
|
|
4845
|
-
* const commit: Task<string> = unabortable(async (
|
|
4827
|
+
* const commit: Task<string> = unabortable(async (run) => {
|
|
4846
4828
|
* commitStarted.resolve();
|
|
4847
4829
|
* await finishCommit.promise;
|
|
4848
|
-
* expect(signal.aborted).toBe(false);
|
|
4830
|
+
* expect(run.signal.aborted).toBe(false);
|
|
4849
4831
|
* return ok("committed");
|
|
4850
4832
|
* });
|
|
4851
4833
|
*
|
|
@@ -4865,8 +4847,8 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
4865
4847
|
});
|
|
4866
4848
|
|
|
4867
4849
|
/**
|
|
4868
|
-
* Like {@link unabortable}, but provides `restore` for child
|
|
4869
|
-
* run with the previous abort mask.
|
|
4850
|
+
* Like {@link unabortable}, but provides `restore` for child {@link Task}s that
|
|
4851
|
+
* should run with the previous abort mask.
|
|
4870
4852
|
*
|
|
4871
4853
|
* Use this for acquire/use/release flows where acquire and release must finish
|
|
4872
4854
|
* once started, while use should remain abortable. Child Tasks inherit the mask
|
|
@@ -4894,8 +4876,6 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
4894
4876
|
* type Task,
|
|
4895
4877
|
* } from "@evolu/common";
|
|
4896
4878
|
*
|
|
4897
|
-
* await using run = createRun();
|
|
4898
|
-
*
|
|
4899
4879
|
* interface Resource {
|
|
4900
4880
|
* readonly id: string;
|
|
4901
4881
|
* }
|
|
@@ -4912,13 +4892,14 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
|
4912
4892
|
* let released = false;
|
|
4913
4893
|
* const release =
|
|
4914
4894
|
* (_resource: Resource): Task<void> =>
|
|
4915
|
-
* (
|
|
4895
|
+
* (run) => {
|
|
4916
4896
|
* // Release inherits the mask even after abort was requested.
|
|
4917
|
-
* expect(signal.aborted).toBe(false);
|
|
4897
|
+
* expect(run.signal.aborted).toBe(false);
|
|
4918
4898
|
* released = true;
|
|
4919
4899
|
* return ok();
|
|
4920
4900
|
* };
|
|
4921
4901
|
*
|
|
4902
|
+
* await using run = createRun();
|
|
4922
4903
|
* const fiber = run.abortable(
|
|
4923
4904
|
* unabortableMask((restore) => async (run) => {
|
|
4924
4905
|
* // Acquire with abort masked.
|
|
@@ -5004,6 +4985,7 @@ export const unabortableMask = <T, E, D = unknown>(
|
|
|
5004
4985
|
* err,
|
|
5005
4986
|
* ok,
|
|
5006
4987
|
* type Task,
|
|
4988
|
+
* type Typed,
|
|
5007
4989
|
* } from "@evolu/common";
|
|
5008
4990
|
*
|
|
5009
4991
|
* interface Connection {
|
|
@@ -5014,9 +4996,14 @@ export const unabortableMask = <T, E, D = unknown>(
|
|
|
5014
4996
|
* const openConnection: Task<Connection> = () =>
|
|
5015
4997
|
* ok({ user: "Ada", isAvailable: false });
|
|
5016
4998
|
* const loadUser =
|
|
5017
|
-
* (connection: Connection): Task<string,
|
|
4999
|
+
* (connection: Connection): Task<string, UserUnavailableError> =>
|
|
5018
5000
|
* () =>
|
|
5019
|
-
* connection.isAvailable
|
|
5001
|
+
* connection.isAvailable
|
|
5002
|
+
* ? ok(connection.user)
|
|
5003
|
+
* : err({ type: "UserUnavailable" });
|
|
5004
|
+
*
|
|
5005
|
+
* interface UserUnavailableError extends Typed<"UserUnavailable"> {}
|
|
5006
|
+
*
|
|
5020
5007
|
* let connectionClosed = false;
|
|
5021
5008
|
* const closeConnection =
|
|
5022
5009
|
* (_connection: Connection): Task<void> =>
|
|
@@ -5032,12 +5019,12 @@ export const unabortableMask = <T, E, D = unknown>(
|
|
|
5032
5019
|
* );
|
|
5033
5020
|
*
|
|
5034
5021
|
* await using run = createRun();
|
|
5035
|
-
* expectErr(await run(queryUser), "
|
|
5022
|
+
* expectErr(await run(queryUser), { type: "UserUnavailable" });
|
|
5036
5023
|
* // Release still runs when use returns a domain error.
|
|
5037
5024
|
* expect(connectionClosed).toBe(true);
|
|
5038
5025
|
* ```
|
|
5039
5026
|
*
|
|
5040
|
-
* @group
|
|
5027
|
+
* @group Lifetime
|
|
5041
5028
|
*/
|
|
5042
5029
|
export const acquireUseRelease = <
|
|
5043
5030
|
Resource,
|
|
@@ -5071,15 +5058,29 @@ export const acquireUseRelease = <
|
|
|
5071
5058
|
/**
|
|
5072
5059
|
* A one-shot value resolved from outside the waiting {@link Task}.
|
|
5073
5060
|
*
|
|
5074
|
-
* Use Deferred when Task code must wait for a Result completed by an
|
|
5075
|
-
* callback or another Task. Deferred is `Promise.withResolvers` with
|
|
5076
|
-
* semantics: each waiter uses its waiting Run lifetime, can abort
|
|
5061
|
+
* Use Deferred when Task code must wait for a {@link Result} completed by an
|
|
5062
|
+
* external callback or another Task. Deferred is `Promise.withResolvers` with
|
|
5063
|
+
* Task semantics: each waiter uses its waiting {@link Run} lifetime, can abort
|
|
5077
5064
|
* independently, appears in Run observability, and settles with Result-based
|
|
5078
5065
|
* errors.
|
|
5079
5066
|
*
|
|
5080
5067
|
* The Deferred resolves once. Later calls to {@link Deferred.resolve} return
|
|
5081
5068
|
* `false` and do not change the Result.
|
|
5082
5069
|
*
|
|
5070
|
+
* @group Concurrency primitives
|
|
5071
|
+
* @see {@link createDeferred}
|
|
5072
|
+
*/
|
|
5073
|
+
export interface Deferred<T, E = never> {
|
|
5074
|
+
/** Waits until {@link Deferred.resolve} resolves the Deferred. */
|
|
5075
|
+
readonly task: Task<T, E>;
|
|
5076
|
+
|
|
5077
|
+
/** Resolves the Deferred, returning whether this call completed it. */
|
|
5078
|
+
readonly resolve: (result: Result<T, E>) => boolean;
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
/**
|
|
5082
|
+
* Creates a {@link Deferred}.
|
|
5083
|
+
*
|
|
5083
5084
|
* ### Example
|
|
5084
5085
|
*
|
|
5085
5086
|
* ```ts
|
|
@@ -5106,7 +5107,7 @@ export const acquireUseRelease = <
|
|
|
5106
5107
|
* expectOk(await run(deferred.task), "ready");
|
|
5107
5108
|
* ```
|
|
5108
5109
|
*
|
|
5109
|
-
* ###
|
|
5110
|
+
* ### Aborting a waiter
|
|
5110
5111
|
*
|
|
5111
5112
|
* ```ts
|
|
5112
5113
|
* import { AbortError, createDeferred, createRun } from "@evolu/common";
|
|
@@ -5123,20 +5124,6 @@ export const acquireUseRelease = <
|
|
|
5123
5124
|
* ```
|
|
5124
5125
|
*
|
|
5125
5126
|
* @group Concurrency primitives
|
|
5126
|
-
* @see {@link createDeferred}
|
|
5127
|
-
*/
|
|
5128
|
-
export interface Deferred<T, E = never> {
|
|
5129
|
-
/** Waits until {@link Deferred.resolve} resolves the Deferred. */
|
|
5130
|
-
readonly task: Task<T, E>;
|
|
5131
|
-
|
|
5132
|
-
/** Resolves the Deferred, returning whether this call completed it. */
|
|
5133
|
-
readonly resolve: (result: Result<T, E>) => boolean;
|
|
5134
|
-
}
|
|
5135
|
-
|
|
5136
|
-
/**
|
|
5137
|
-
* Creates a {@link Deferred}.
|
|
5138
|
-
*
|
|
5139
|
-
* @group Concurrency primitives
|
|
5140
5127
|
*/
|
|
5141
5128
|
export const createDeferred = <T, E = never>(): Deferred<T, E> => {
|
|
5142
5129
|
let resolvedResult: Result<T, E> | undefined;
|
|
@@ -5166,19 +5153,42 @@ export const createDeferred = <T, E = never>(): Deferred<T, E> => {
|
|
|
5166
5153
|
};
|
|
5167
5154
|
|
|
5168
5155
|
/**
|
|
5169
|
-
* A reusable gate for blocking and releasing
|
|
5170
|
-
*
|
|
5171
|
-
* - **Closed**: Tasks wait.
|
|
5172
|
-
* - **Open**: Tasks proceed.
|
|
5173
|
-
*
|
|
5174
|
-
* Use it to pause execution based on a condition. Unlike a {@link Deferred},
|
|
5175
|
-
* which resolves once, a Gate can be opened and closed repeatedly. Opening
|
|
5176
|
-
* releases all current waiters and allows future waiters to pass immediately.
|
|
5177
|
-
* Closing only affects future waiters. Releasing allows current waiters to pass
|
|
5178
|
-
* while keeping future waiters blocked.
|
|
5179
|
-
*
|
|
5180
|
-
* {@link createGate} creates a closed Gate by default. Pass `isOpen: true` when
|
|
5181
|
-
* work should proceed immediately.
|
|
5156
|
+
* A reusable gate for blocking and releasing {@link Task}s.
|
|
5157
|
+
*
|
|
5158
|
+
* - **Closed**: Tasks wait.
|
|
5159
|
+
* - **Open**: Tasks proceed.
|
|
5160
|
+
*
|
|
5161
|
+
* Use it to pause execution based on a condition. Unlike a {@link Deferred},
|
|
5162
|
+
* which resolves once, a Gate can be opened and closed repeatedly. Opening
|
|
5163
|
+
* releases all current waiters and allows future waiters to pass immediately.
|
|
5164
|
+
* Closing only affects future waiters. Releasing allows current waiters to pass
|
|
5165
|
+
* while keeping future waiters blocked.
|
|
5166
|
+
*
|
|
5167
|
+
* {@link createGate} creates a closed Gate by default. Pass `isOpen: true` when
|
|
5168
|
+
* work should proceed immediately.
|
|
5169
|
+
*
|
|
5170
|
+
* @group Concurrency primitives
|
|
5171
|
+
* @see {@link createGate}
|
|
5172
|
+
*/
|
|
5173
|
+
export interface Gate {
|
|
5174
|
+
/** Waits while the gate is closed. */
|
|
5175
|
+
readonly wait: Task<void>;
|
|
5176
|
+
|
|
5177
|
+
/** Opens the gate, releasing all waiters. Returns false when already open. */
|
|
5178
|
+
readonly open: () => boolean;
|
|
5179
|
+
|
|
5180
|
+
/** Closes the gate. Returns false when already closed. */
|
|
5181
|
+
readonly close: () => boolean;
|
|
5182
|
+
|
|
5183
|
+
/** Releases the current closed wait cycle. Returns false when already open. */
|
|
5184
|
+
readonly release: () => boolean;
|
|
5185
|
+
|
|
5186
|
+
/** Returns whether the gate is open. */
|
|
5187
|
+
readonly isOpen: () => boolean;
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
/**
|
|
5191
|
+
* Creates a {@link Gate}.
|
|
5182
5192
|
*
|
|
5183
5193
|
* ### Example
|
|
5184
5194
|
*
|
|
@@ -5209,29 +5219,6 @@ export const createDeferred = <T, E = never>(): Deferred<T, E> => {
|
|
|
5209
5219
|
* ```
|
|
5210
5220
|
*
|
|
5211
5221
|
* @group Concurrency primitives
|
|
5212
|
-
* @see {@link createGate}
|
|
5213
|
-
*/
|
|
5214
|
-
export interface Gate {
|
|
5215
|
-
/** Waits while the gate is closed. */
|
|
5216
|
-
readonly wait: Task<void>;
|
|
5217
|
-
|
|
5218
|
-
/** Opens the gate, releasing all waiters. Returns false when already open. */
|
|
5219
|
-
readonly open: () => boolean;
|
|
5220
|
-
|
|
5221
|
-
/** Closes the gate. Returns false when already closed. */
|
|
5222
|
-
readonly close: () => boolean;
|
|
5223
|
-
|
|
5224
|
-
/** Releases the current closed wait cycle. Returns false when already open. */
|
|
5225
|
-
readonly release: () => boolean;
|
|
5226
|
-
|
|
5227
|
-
/** Returns whether the gate is open. */
|
|
5228
|
-
readonly isOpen: () => boolean;
|
|
5229
|
-
}
|
|
5230
|
-
|
|
5231
|
-
/**
|
|
5232
|
-
* Creates a {@link Gate}.
|
|
5233
|
-
*
|
|
5234
|
-
* @group Concurrency primitives
|
|
5235
5222
|
*/
|
|
5236
5223
|
export const createGate = ({
|
|
5237
5224
|
isOpen = false,
|
|
@@ -5268,7 +5255,7 @@ export const createGate = ({
|
|
|
5268
5255
|
};
|
|
5269
5256
|
|
|
5270
5257
|
/**
|
|
5271
|
-
* Coordinates concurrent
|
|
5258
|
+
* Coordinates concurrent {@link Task}s by acquiring and releasing permits.
|
|
5272
5259
|
*
|
|
5273
5260
|
* Use {@link Semaphore.withPermit} or {@link Semaphore.withPermits} to acquire
|
|
5274
5261
|
* permits for one Task and release them when it settles. Use
|
|
@@ -5287,45 +5274,6 @@ export const createGate = ({
|
|
|
5287
5274
|
* already holding permits consumes additional permits and can wait if not
|
|
5288
5275
|
* enough permits are available.
|
|
5289
5276
|
*
|
|
5290
|
-
* ### Example
|
|
5291
|
-
*
|
|
5292
|
-
* ```ts
|
|
5293
|
-
* import {
|
|
5294
|
-
* createRun,
|
|
5295
|
-
* createSemaphore,
|
|
5296
|
-
* getOk,
|
|
5297
|
-
* ok,
|
|
5298
|
-
* sleep,
|
|
5299
|
-
* type Task,
|
|
5300
|
-
* } from "@evolu/common";
|
|
5301
|
-
*
|
|
5302
|
-
* await using run = createRun();
|
|
5303
|
-
*
|
|
5304
|
-
* const semaphore = createSemaphore(2);
|
|
5305
|
-
* let activeSaves = 0;
|
|
5306
|
-
* let maxActiveSaves = 0;
|
|
5307
|
-
*
|
|
5308
|
-
* const saveUser =
|
|
5309
|
-
* (id: string): Task<string> =>
|
|
5310
|
-
* async (run) => {
|
|
5311
|
-
* activeSaves += 1;
|
|
5312
|
-
* maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
|
|
5313
|
-
* await run.ok(sleep("10ms"));
|
|
5314
|
-
* activeSaves -= 1;
|
|
5315
|
-
* return ok(`saved:${id}`);
|
|
5316
|
-
* };
|
|
5317
|
-
*
|
|
5318
|
-
* const results = await Promise.all([
|
|
5319
|
-
* run(semaphore.withPermit(saveUser("1"))),
|
|
5320
|
-
* run(semaphore.withPermit(saveUser("2"))),
|
|
5321
|
-
* run(semaphore.withPermit(saveUser("3"))),
|
|
5322
|
-
* ]);
|
|
5323
|
-
*
|
|
5324
|
-
* const savedUsers = results.map(getOk);
|
|
5325
|
-
* expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
|
|
5326
|
-
* expect(maxActiveSaves).toBe(2);
|
|
5327
|
-
* ```
|
|
5328
|
-
*
|
|
5329
5277
|
* @group Concurrency primitives
|
|
5330
5278
|
* @see {@link createSemaphore}
|
|
5331
5279
|
*/
|
|
@@ -5352,8 +5300,8 @@ export interface Semaphore {
|
|
|
5352
5300
|
/**
|
|
5353
5301
|
* Acquires permits and returns an owned {@link SemaphorePermit}.
|
|
5354
5302
|
*
|
|
5355
|
-
* The Task waits until enough permits are available. Dispose or
|
|
5356
|
-
* returned permit to make them available again.
|
|
5303
|
+
* The {@link Task} waits until enough permits are available. Dispose or
|
|
5304
|
+
* release the returned permit to make them available again.
|
|
5357
5305
|
*
|
|
5358
5306
|
* When the request exceeds the current total permit count, the Task remains
|
|
5359
5307
|
* pending until {@link Semaphore.resize} increases capacity or the Task is
|
|
@@ -5466,6 +5414,45 @@ export interface SemaphoreSnapshot {
|
|
|
5466
5414
|
/**
|
|
5467
5415
|
* Creates a {@link Semaphore}.
|
|
5468
5416
|
*
|
|
5417
|
+
* ### Example
|
|
5418
|
+
*
|
|
5419
|
+
* ```ts
|
|
5420
|
+
* import {
|
|
5421
|
+
* createRun,
|
|
5422
|
+
* createSemaphore,
|
|
5423
|
+
* getOk,
|
|
5424
|
+
* ok,
|
|
5425
|
+
* sleep,
|
|
5426
|
+
* type Task,
|
|
5427
|
+
* } from "@evolu/common";
|
|
5428
|
+
*
|
|
5429
|
+
* await using run = createRun();
|
|
5430
|
+
*
|
|
5431
|
+
* const semaphore = createSemaphore(2);
|
|
5432
|
+
* let activeSaves = 0;
|
|
5433
|
+
* let maxActiveSaves = 0;
|
|
5434
|
+
*
|
|
5435
|
+
* const saveUser =
|
|
5436
|
+
* (id: string): Task<string> =>
|
|
5437
|
+
* async (run) => {
|
|
5438
|
+
* activeSaves += 1;
|
|
5439
|
+
* maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
|
|
5440
|
+
* await run.ok(sleep("10ms"));
|
|
5441
|
+
* activeSaves -= 1;
|
|
5442
|
+
* return ok(`saved:${id}`);
|
|
5443
|
+
* };
|
|
5444
|
+
*
|
|
5445
|
+
* const results = await Promise.all([
|
|
5446
|
+
* run(semaphore.withPermit(saveUser("1"))),
|
|
5447
|
+
* run(semaphore.withPermit(saveUser("2"))),
|
|
5448
|
+
* run(semaphore.withPermit(saveUser("3"))),
|
|
5449
|
+
* ]);
|
|
5450
|
+
*
|
|
5451
|
+
* const savedUsers = results.map(getOk);
|
|
5452
|
+
* expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
|
|
5453
|
+
* expect(maxActiveSaves).toBe(2);
|
|
5454
|
+
* ```
|
|
5455
|
+
*
|
|
5469
5456
|
* @group Concurrency primitives
|
|
5470
5457
|
*/
|
|
5471
5458
|
export const createSemaphore = (
|
|
@@ -5610,11 +5597,24 @@ export const createSemaphore = (
|
|
|
5610
5597
|
};
|
|
5611
5598
|
|
|
5612
5599
|
/**
|
|
5613
|
-
* Runs
|
|
5600
|
+
* Runs {@link Task}s one at a time.
|
|
5614
5601
|
*
|
|
5615
5602
|
* `Mutex` is non-reentrant. A Task that tries to acquire the same Mutex while
|
|
5616
5603
|
* already holding it waits on itself and will not progress.
|
|
5617
5604
|
*
|
|
5605
|
+
* @group Concurrency primitives
|
|
5606
|
+
*/
|
|
5607
|
+
export interface Mutex {
|
|
5608
|
+
/** Runs a {@link Task} while holding the lock. */
|
|
5609
|
+
readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
5610
|
+
|
|
5611
|
+
/** Returns the current lock state for monitoring and debugging. */
|
|
5612
|
+
readonly snapshot: () => SemaphoreSnapshot;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
/**
|
|
5616
|
+
* Creates a {@link Mutex}.
|
|
5617
|
+
*
|
|
5618
5618
|
* ### Example
|
|
5619
5619
|
*
|
|
5620
5620
|
* ```ts
|
|
@@ -5648,19 +5648,6 @@ export const createSemaphore = (
|
|
|
5648
5648
|
*
|
|
5649
5649
|
* @group Concurrency primitives
|
|
5650
5650
|
*/
|
|
5651
|
-
export interface Mutex {
|
|
5652
|
-
/** Runs a {@link Task} while holding the lock. */
|
|
5653
|
-
readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
5654
|
-
|
|
5655
|
-
/** Returns the current lock state for monitoring and debugging. */
|
|
5656
|
-
readonly snapshot: () => SemaphoreSnapshot;
|
|
5657
|
-
}
|
|
5658
|
-
|
|
5659
|
-
/**
|
|
5660
|
-
* Creates a {@link Mutex}.
|
|
5661
|
-
*
|
|
5662
|
-
* @group Concurrency primitives
|
|
5663
|
-
*/
|
|
5664
5651
|
export const createMutex = (): Mutex => {
|
|
5665
5652
|
const semaphore = createSemaphore(1);
|
|
5666
5653
|
|
|
@@ -5671,7 +5658,7 @@ export const createMutex = (): Mutex => {
|
|
|
5671
5658
|
};
|
|
5672
5659
|
|
|
5673
5660
|
/**
|
|
5674
|
-
* Coordinates concurrent
|
|
5661
|
+
* Coordinates concurrent {@link Task}s independently for each key.
|
|
5675
5662
|
*
|
|
5676
5663
|
* `SemaphoreByKey` intentionally exposes only Task-scoped acquisition helpers,
|
|
5677
5664
|
* not the complete {@link Semaphore} API. Methods like {@link Semaphore.take} and
|
|
@@ -5684,54 +5671,6 @@ export const createMutex = (): Mutex => {
|
|
|
5684
5671
|
* ownership should be tied to one Task lifetime and idle keys can be forgotten
|
|
5685
5672
|
* automatically.
|
|
5686
5673
|
*
|
|
5687
|
-
* ### Example
|
|
5688
|
-
*
|
|
5689
|
-
* ```ts
|
|
5690
|
-
* import {
|
|
5691
|
-
* createGate,
|
|
5692
|
-
* createRun,
|
|
5693
|
-
* createSemaphoreByKey,
|
|
5694
|
-
* getOk,
|
|
5695
|
-
* ok,
|
|
5696
|
-
* type Task,
|
|
5697
|
-
* } from "@evolu/common";
|
|
5698
|
-
*
|
|
5699
|
-
* // Each host gets an independent two-download limit.
|
|
5700
|
-
* const downloadsByHost = createSemaphoreByKey<string>(2);
|
|
5701
|
-
* const finishDownloads = createGate();
|
|
5702
|
-
* const firstBatchStarted = Promise.withResolvers<void>();
|
|
5703
|
-
* const started: Array<string> = [];
|
|
5704
|
-
* const download = (host: string, file: string): Task<string> =>
|
|
5705
|
-
* downloadsByHost.withPermit(host, async (run) => {
|
|
5706
|
-
* started.push(`${host}/${file}`);
|
|
5707
|
-
* if (started.length === 3) firstBatchStarted.resolve();
|
|
5708
|
-
* await run.ok(finishDownloads.wait);
|
|
5709
|
-
* return ok(`${host}/${file}`);
|
|
5710
|
-
* });
|
|
5711
|
-
*
|
|
5712
|
-
* await using run = createRun();
|
|
5713
|
-
* const downloads = [
|
|
5714
|
-
* run(download("a.example", "1.json")),
|
|
5715
|
-
* run(download("a.example", "2.json")),
|
|
5716
|
-
* run(download("a.example", "3.json")),
|
|
5717
|
-
* run(download("b.example", "1.json")),
|
|
5718
|
-
* ];
|
|
5719
|
-
* await firstBatchStarted.promise;
|
|
5720
|
-
* expect(started).toEqual([
|
|
5721
|
-
* "a.example/1.json",
|
|
5722
|
-
* "a.example/2.json",
|
|
5723
|
-
* "b.example/1.json",
|
|
5724
|
-
* ]);
|
|
5725
|
-
*
|
|
5726
|
-
* finishDownloads.open();
|
|
5727
|
-
* expect((await Promise.all(downloads)).map(getOk)).toEqual([
|
|
5728
|
-
* "a.example/1.json",
|
|
5729
|
-
* "a.example/2.json",
|
|
5730
|
-
* "a.example/3.json",
|
|
5731
|
-
* "b.example/1.json",
|
|
5732
|
-
* ]);
|
|
5733
|
-
* ```
|
|
5734
|
-
*
|
|
5735
5674
|
* @group Concurrency primitives
|
|
5736
5675
|
*/
|
|
5737
5676
|
export interface SemaphoreByKey<K = unknown> {
|
|
@@ -5764,12 +5703,37 @@ export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<
|
|
|
5764
5703
|
/**
|
|
5765
5704
|
* Creates a {@link SemaphoreByKey}.
|
|
5766
5705
|
*
|
|
5706
|
+
* ### Example
|
|
5707
|
+
*
|
|
5708
|
+
* ```ts
|
|
5709
|
+
* import {
|
|
5710
|
+
* createRun,
|
|
5711
|
+
* createSemaphoreByKey,
|
|
5712
|
+
* ok,
|
|
5713
|
+
* type Task,
|
|
5714
|
+
* } from "@evolu/common";
|
|
5715
|
+
*
|
|
5716
|
+
* // Each host gets an independent two-download limit.
|
|
5717
|
+
* const downloadsByHost = createSemaphoreByKey<string>(2);
|
|
5718
|
+
* const download = (host: string, file: string): Task<string> =>
|
|
5719
|
+
* downloadsByHost.withPermit(host, () => ok(`${host}/${file}`));
|
|
5720
|
+
*
|
|
5721
|
+
* await using run = createRun();
|
|
5722
|
+
* expectOk(
|
|
5723
|
+
* await run(download("a.example", "index.json")),
|
|
5724
|
+
* "a.example/index.json",
|
|
5725
|
+
* );
|
|
5726
|
+
* expect(downloadsByHost.isIdle("a.example")).toBe(true);
|
|
5727
|
+
* ```
|
|
5728
|
+
*
|
|
5767
5729
|
* @group Concurrency primitives
|
|
5768
5730
|
*/
|
|
5769
5731
|
export function createSemaphoreByKey<K = unknown>(
|
|
5770
5732
|
initialPermits: Int1To100OrPositiveInt,
|
|
5771
5733
|
options?: CreateSemaphoreByKeyOptions<K, unknown>,
|
|
5772
5734
|
): SemaphoreByKey<K>;
|
|
5735
|
+
|
|
5736
|
+
/** Creates a {@link SemaphoreByKey} with custom logical key lookup. */
|
|
5773
5737
|
export function createSemaphoreByKey<K, L>(
|
|
5774
5738
|
initialPermits: Int1To100OrPositiveInt,
|
|
5775
5739
|
options: CreateSemaphoreByKeyOptions<K, L>,
|
|
@@ -5807,48 +5771,8 @@ export function createSemaphoreByKey<K, L = K>(
|
|
|
5807
5771
|
}
|
|
5808
5772
|
|
|
5809
5773
|
/**
|
|
5810
|
-
* Runs
|
|
5811
|
-
*
|
|
5812
|
-
* ### Example
|
|
5813
|
-
*
|
|
5814
|
-
* ```ts
|
|
5815
|
-
* import {
|
|
5816
|
-
* createGate,
|
|
5817
|
-
* createMutexByKey,
|
|
5818
|
-
* createRun,
|
|
5819
|
-
* ok,
|
|
5820
|
-
* type Task,
|
|
5821
|
-
* } from "@evolu/common";
|
|
5822
|
-
*
|
|
5823
|
-
* const accountLocks = createMutexByKey<string>();
|
|
5824
|
-
* const finishDeposits = createGate();
|
|
5825
|
-
* const firstBatchStarted = Promise.withResolvers<void>();
|
|
5826
|
-
* const started: Array<string> = [];
|
|
5827
|
-
* const balancesByAccount = new Map<string, number>();
|
|
5828
|
-
* const deposit = (account: string, amount: number): Task<number> =>
|
|
5829
|
-
* accountLocks.withLock(account, async (run) => {
|
|
5830
|
-
* started.push(account);
|
|
5831
|
-
* if (started.length === 2) firstBatchStarted.resolve();
|
|
5832
|
-
* await run.ok(finishDeposits.wait);
|
|
5833
|
-
* const balance = (balancesByAccount.get(account) ?? 0) + amount;
|
|
5834
|
-
* balancesByAccount.set(account, balance);
|
|
5835
|
-
* return ok(balance);
|
|
5836
|
-
* });
|
|
5837
|
-
*
|
|
5838
|
-
* await using run = createRun();
|
|
5839
|
-
* const first = run(deposit("checking", 2));
|
|
5840
|
-
* const second = run(deposit("checking", 3));
|
|
5841
|
-
* const third = run(deposit("savings", 4));
|
|
5842
|
-
* await firstBatchStarted.promise;
|
|
5843
|
-
* // Different accounts proceed together; the second checking deposit waits.
|
|
5844
|
-
* expect(started).toEqual(["checking", "savings"]);
|
|
5845
|
-
*
|
|
5846
|
-
* finishDeposits.open();
|
|
5847
|
-
* expectOk(await first, 2);
|
|
5848
|
-
* expectOk(await second, 5);
|
|
5849
|
-
* expectOk(await third, 4);
|
|
5850
|
-
* expect(started).toEqual(["checking", "savings", "checking"]);
|
|
5851
|
-
* ```
|
|
5774
|
+
* Runs {@link Task}s one at a time independently for each key, like
|
|
5775
|
+
* {@link Mutex}.
|
|
5852
5776
|
*
|
|
5853
5777
|
* @group Concurrency primitives
|
|
5854
5778
|
*/
|
|
@@ -5873,11 +5797,38 @@ export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {}
|
|
|
5873
5797
|
/**
|
|
5874
5798
|
* Creates a {@link MutexByKey}.
|
|
5875
5799
|
*
|
|
5800
|
+
* ### Example
|
|
5801
|
+
*
|
|
5802
|
+
* ```ts
|
|
5803
|
+
* import {
|
|
5804
|
+
* createMutexByKey,
|
|
5805
|
+
* createRun,
|
|
5806
|
+
* ok,
|
|
5807
|
+
* type Task,
|
|
5808
|
+
* } from "@evolu/common";
|
|
5809
|
+
*
|
|
5810
|
+
* const accountLocks = createMutexByKey<string>();
|
|
5811
|
+
* const balancesByAccount = new Map<string, number>();
|
|
5812
|
+
* const deposit = (account: string, amount: number): Task<number> =>
|
|
5813
|
+
* accountLocks.withLock(account, () => {
|
|
5814
|
+
* const balance = (balancesByAccount.get(account) ?? 0) + amount;
|
|
5815
|
+
* balancesByAccount.set(account, balance);
|
|
5816
|
+
* return ok(balance);
|
|
5817
|
+
* });
|
|
5818
|
+
*
|
|
5819
|
+
* await using run = createRun();
|
|
5820
|
+
* expectOk(await run(deposit("checking", 2)), 2);
|
|
5821
|
+
* expectOk(await run(deposit("checking", 3)), 5);
|
|
5822
|
+
* expect(accountLocks.isIdle("checking")).toBe(true);
|
|
5823
|
+
* ```
|
|
5824
|
+
*
|
|
5876
5825
|
* @group Concurrency primitives
|
|
5877
5826
|
*/
|
|
5878
5827
|
export function createMutexByKey<K = unknown>(
|
|
5879
5828
|
options?: CreateMutexByKeyOptions<K, unknown>,
|
|
5880
5829
|
): MutexByKey<K>;
|
|
5830
|
+
|
|
5831
|
+
/** Creates a {@link MutexByKey} with custom logical key lookup. */
|
|
5881
5832
|
export function createMutexByKey<K, L>(
|
|
5882
5833
|
options: CreateMutexByKeyOptions<K, L>,
|
|
5883
5834
|
): MutexByKey<K>;
|
|
@@ -5904,57 +5855,13 @@ export function createMutexByKey<K, L = K>({
|
|
|
5904
5855
|
* internal Mutex, so calling another method on the same MutexRef from inside
|
|
5905
5856
|
* one of them waits on itself and will not progress.
|
|
5906
5857
|
*
|
|
5907
|
-
* Use it for state whose transitions are
|
|
5908
|
-
* Plain Ref cannot express that — between a sync read and a
|
|
5909
|
-
* concurrent transition can interleave and get lost.
|
|
5910
|
-
*
|
|
5911
|
-
* `MutexRef` operations are Tasks and incur normal Run lifecycle overhead. Use
|
|
5912
|
-
* {@link Ref} instead for synchronous state transitions, especially on
|
|
5913
|
-
* allocation-sensitive hot paths.
|
|
5914
|
-
*
|
|
5915
|
-
* ### Example
|
|
5916
|
-
*
|
|
5917
|
-
* ```ts
|
|
5918
|
-
* import {
|
|
5919
|
-
* createGate,
|
|
5920
|
-
* createMutexRef,
|
|
5921
|
-
* createRun,
|
|
5922
|
-
* ok,
|
|
5923
|
-
* type Task,
|
|
5924
|
-
* } from "@evolu/common";
|
|
5925
|
-
*
|
|
5926
|
-
* await using run = createRun();
|
|
5927
|
-
*
|
|
5928
|
-
* const finishRefresh = createGate();
|
|
5929
|
-
* const refreshStarted = Promise.withResolvers<void>();
|
|
5930
|
-
* let fetchTokenCalls = 0;
|
|
5931
|
-
* const fetchToken: Task<string> = async (run) => {
|
|
5932
|
-
* fetchTokenCalls += 1;
|
|
5933
|
-
* refreshStarted.resolve();
|
|
5934
|
-
* await run.ok(finishRefresh.wait);
|
|
5935
|
-
* return ok("fresh-token");
|
|
5936
|
-
* };
|
|
5937
|
-
*
|
|
5938
|
-
* const tokenRef = createMutexRef<string | null>(null);
|
|
5939
|
-
*
|
|
5940
|
-
* // Concurrent callers never trigger duplicate refreshes: the first caller
|
|
5941
|
-
* // runs fetchToken under the lock; later callers reuse the stored token.
|
|
5942
|
-
* const getToken = tokenRef.updateAndGet((current) =>
|
|
5943
|
-
* current === null ? fetchToken : () => ok(current),
|
|
5944
|
-
* );
|
|
5858
|
+
* Use it for state whose transitions are {@link Task}s: atomic async
|
|
5859
|
+
* read-modify-write. Plain Ref cannot express that — between a sync read and a
|
|
5860
|
+
* later write, a concurrent transition can interleave and get lost.
|
|
5945
5861
|
*
|
|
5946
|
-
*
|
|
5947
|
-
*
|
|
5948
|
-
*
|
|
5949
|
-
* finishRefresh.open();
|
|
5950
|
-
*
|
|
5951
|
-
* const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
5952
|
-
* expectTypeOf(getToken).toEqualTypeOf<Task<string | null>>();
|
|
5953
|
-
* expectOk(firstResult, "fresh-token");
|
|
5954
|
-
* expectOk(secondResult, "fresh-token");
|
|
5955
|
-
* expect(fetchTokenCalls).toBe(1);
|
|
5956
|
-
* expectOk(await run(tokenRef.get), "fresh-token");
|
|
5957
|
-
* ```
|
|
5862
|
+
* `MutexRef` operations are Tasks and incur normal {@link Run} lifecycle
|
|
5863
|
+
* overhead. Use {@link Ref} instead for synchronous state transitions,
|
|
5864
|
+
* especially on allocation-sensitive hot paths.
|
|
5958
5865
|
*
|
|
5959
5866
|
* @group Concurrency primitives
|
|
5960
5867
|
* @see {@link createMutexRef}
|
|
@@ -5999,6 +5906,19 @@ export interface MutexRef<T> {
|
|
|
5999
5906
|
/**
|
|
6000
5907
|
* Creates a {@link MutexRef}.
|
|
6001
5908
|
*
|
|
5909
|
+
* ### Example
|
|
5910
|
+
*
|
|
5911
|
+
* ```ts
|
|
5912
|
+
* import { createMutexRef, createRun, ok } from "@evolu/common";
|
|
5913
|
+
*
|
|
5914
|
+
* const counter = createMutexRef(0);
|
|
5915
|
+
* const increment = counter.updateAndGet((value) => () => ok(value + 1));
|
|
5916
|
+
*
|
|
5917
|
+
* await using run = createRun();
|
|
5918
|
+
* expectOk(await run(increment), 1);
|
|
5919
|
+
* expectOk(await run(counter.get), 1);
|
|
5920
|
+
* ```
|
|
5921
|
+
*
|
|
6002
5922
|
* @group Concurrency primitives
|
|
6003
5923
|
*/
|
|
6004
5924
|
export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
|
|
@@ -6056,7 +5976,7 @@ export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
|
|
|
6056
5976
|
};
|
|
6057
5977
|
};
|
|
6058
5978
|
|
|
6059
|
-
// TODO:
|
|
5979
|
+
// TODO: Expand Run observability.
|
|
6060
5980
|
// - Structured logging with levels, inherited log annotations, JSON output,
|
|
6061
5981
|
// filtering, and pluggable log sinks.
|
|
6062
5982
|
// - Tracing spans with names, timing, parent-child relationships, attributes,
|