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