@evolu/common 8.2.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/dist/src/Function.d.ts +1 -0
  2. package/dist/src/Function.d.ts.map +1 -1
  3. package/dist/src/Http.d.ts +9 -0
  4. package/dist/src/Http.d.ts.map +1 -1
  5. package/dist/src/Object.d.ts +1 -0
  6. package/dist/src/Object.d.ts.map +1 -1
  7. package/dist/src/RefCount.d.ts +1 -0
  8. package/dist/src/RefCount.d.ts.map +1 -1
  9. package/dist/src/Relation.d.ts +2 -0
  10. package/dist/src/Relation.d.ts.map +1 -1
  11. package/dist/src/Resource.d.ts +1 -0
  12. package/dist/src/Resource.d.ts.map +1 -1
  13. package/dist/src/Result.d.ts +383 -548
  14. package/dist/src/Result.d.ts.map +1 -1
  15. package/dist/src/Result.js +296 -46
  16. package/dist/src/StackTrace.d.ts +1 -0
  17. package/dist/src/StackTrace.d.ts.map +1 -1
  18. package/dist/src/Task.d.ts +985 -1069
  19. package/dist/src/Task.d.ts.map +1 -1
  20. package/dist/src/Task.js +1007 -156
  21. package/dist/src/Time.d.ts +1 -0
  22. package/dist/src/Time.d.ts.map +1 -1
  23. package/dist/src/Type.d.ts +1285 -734
  24. package/dist/src/Type.d.ts.map +1 -1
  25. package/dist/src/Type.js +1057 -193
  26. package/dist/src/intl/_en.d.ts +126 -0
  27. package/dist/src/intl/_en.d.ts.map +1 -0
  28. package/dist/src/intl/_en.js +267 -0
  29. package/dist/src/intl/ar.d.ts +117 -0
  30. package/dist/src/intl/ar.d.ts.map +1 -0
  31. package/dist/src/intl/ar.js +245 -0
  32. package/dist/src/intl/bn.d.ts +117 -0
  33. package/dist/src/intl/bn.d.ts.map +1 -0
  34. package/dist/src/intl/bn.js +258 -0
  35. package/dist/src/intl/ca.d.ts +117 -0
  36. package/dist/src/intl/ca.d.ts.map +1 -0
  37. package/dist/src/intl/ca.js +258 -0
  38. package/dist/src/intl/cs.d.ts +20 -7
  39. package/dist/src/intl/cs.d.ts.map +1 -1
  40. package/dist/src/intl/cs.js +54 -27
  41. package/dist/src/intl/da.d.ts +117 -0
  42. package/dist/src/intl/da.d.ts.map +1 -0
  43. package/dist/src/intl/da.js +258 -0
  44. package/dist/src/intl/de.d.ts +117 -0
  45. package/dist/src/intl/de.d.ts.map +1 -0
  46. package/dist/src/intl/de.js +258 -0
  47. package/dist/src/intl/el.d.ts +117 -0
  48. package/dist/src/intl/el.d.ts.map +1 -0
  49. package/dist/src/intl/el.js +265 -0
  50. package/dist/src/intl/es.d.ts +117 -0
  51. package/dist/src/intl/es.d.ts.map +1 -0
  52. package/dist/src/intl/es.js +258 -0
  53. package/dist/src/intl/fa.d.ts +117 -0
  54. package/dist/src/intl/fa.d.ts.map +1 -0
  55. package/dist/src/intl/fa.js +258 -0
  56. package/dist/src/intl/fi.d.ts +117 -0
  57. package/dist/src/intl/fi.d.ts.map +1 -0
  58. package/dist/src/intl/fi.js +258 -0
  59. package/dist/src/intl/fil.d.ts +117 -0
  60. package/dist/src/intl/fil.d.ts.map +1 -0
  61. package/dist/src/intl/fil.js +258 -0
  62. package/dist/src/intl/fr.d.ts +117 -0
  63. package/dist/src/intl/fr.d.ts.map +1 -0
  64. package/dist/src/intl/fr.js +258 -0
  65. package/dist/src/intl/he.d.ts +117 -0
  66. package/dist/src/intl/he.d.ts.map +1 -0
  67. package/dist/src/intl/he.js +258 -0
  68. package/dist/src/intl/hi.d.ts +117 -0
  69. package/dist/src/intl/hi.d.ts.map +1 -0
  70. package/dist/src/intl/hi.js +258 -0
  71. package/dist/src/intl/hr.d.ts +117 -0
  72. package/dist/src/intl/hr.d.ts.map +1 -0
  73. package/dist/src/intl/hr.js +265 -0
  74. package/dist/src/intl/hu.d.ts +63 -0
  75. package/dist/src/intl/hu.d.ts.map +1 -0
  76. package/dist/src/intl/hu.js +204 -0
  77. package/dist/src/intl/id.d.ts +117 -0
  78. package/dist/src/intl/id.d.ts.map +1 -0
  79. package/dist/src/intl/id.js +258 -0
  80. package/dist/src/intl/index.d.ts +42 -0
  81. package/dist/src/intl/index.d.ts.map +1 -1
  82. package/dist/src/intl/index.js +42 -0
  83. package/dist/src/intl/it.d.ts +117 -0
  84. package/dist/src/intl/it.d.ts.map +1 -0
  85. package/dist/src/intl/it.js +258 -0
  86. package/dist/src/intl/ja.d.ts +117 -0
  87. package/dist/src/intl/ja.d.ts.map +1 -0
  88. package/dist/src/intl/ja.js +258 -0
  89. package/dist/src/intl/ko.d.ts +117 -0
  90. package/dist/src/intl/ko.d.ts.map +1 -0
  91. package/dist/src/intl/ko.js +258 -0
  92. package/dist/src/intl/ml.d.ts +117 -0
  93. package/dist/src/intl/ml.d.ts.map +1 -0
  94. package/dist/src/intl/ml.js +258 -0
  95. package/dist/src/intl/mr.d.ts +117 -0
  96. package/dist/src/intl/mr.d.ts.map +1 -0
  97. package/dist/src/intl/mr.js +258 -0
  98. package/dist/src/intl/ms.d.ts +117 -0
  99. package/dist/src/intl/ms.d.ts.map +1 -0
  100. package/dist/src/intl/ms.js +245 -0
  101. package/dist/src/intl/nb.d.ts +63 -0
  102. package/dist/src/intl/nb.d.ts.map +1 -0
  103. package/dist/src/intl/nb.js +191 -0
  104. package/dist/src/intl/nl.d.ts +117 -0
  105. package/dist/src/intl/nl.d.ts.map +1 -0
  106. package/dist/src/intl/nl.js +258 -0
  107. package/dist/src/intl/pa.d.ts +117 -0
  108. package/dist/src/intl/pa.d.ts.map +1 -0
  109. package/dist/src/intl/pa.js +258 -0
  110. package/dist/src/intl/pl.d.ts +117 -0
  111. package/dist/src/intl/pl.d.ts.map +1 -0
  112. package/dist/src/intl/pl.js +242 -0
  113. package/dist/src/intl/pt-BR.d.ts +117 -0
  114. package/dist/src/intl/pt-BR.d.ts.map +1 -0
  115. package/dist/src/intl/pt-BR.js +258 -0
  116. package/dist/src/intl/pt.d.ts +117 -0
  117. package/dist/src/intl/pt.d.ts.map +1 -0
  118. package/dist/src/intl/pt.js +246 -0
  119. package/dist/src/intl/ro.d.ts +117 -0
  120. package/dist/src/intl/ro.d.ts.map +1 -0
  121. package/dist/src/intl/ro.js +258 -0
  122. package/dist/src/intl/sk.d.ts +117 -0
  123. package/dist/src/intl/sk.d.ts.map +1 -0
  124. package/dist/src/intl/sk.js +244 -0
  125. package/dist/src/intl/sl.d.ts +117 -0
  126. package/dist/src/intl/sl.d.ts.map +1 -0
  127. package/dist/src/intl/sl.js +258 -0
  128. package/dist/src/intl/sv.d.ts +117 -0
  129. package/dist/src/intl/sv.d.ts.map +1 -0
  130. package/dist/src/intl/sv.js +258 -0
  131. package/dist/src/intl/sw.d.ts +63 -0
  132. package/dist/src/intl/sw.d.ts.map +1 -0
  133. package/dist/src/intl/sw.js +191 -0
  134. package/dist/src/intl/ta.d.ts +117 -0
  135. package/dist/src/intl/ta.d.ts.map +1 -0
  136. package/dist/src/intl/ta.js +258 -0
  137. package/dist/src/intl/te.d.ts +117 -0
  138. package/dist/src/intl/te.d.ts.map +1 -0
  139. package/dist/src/intl/te.js +258 -0
  140. package/dist/src/intl/th.d.ts +117 -0
  141. package/dist/src/intl/th.d.ts.map +1 -0
  142. package/dist/src/intl/th.js +258 -0
  143. package/dist/src/intl/tr.d.ts +117 -0
  144. package/dist/src/intl/tr.d.ts.map +1 -0
  145. package/dist/src/intl/tr.js +255 -0
  146. package/dist/src/intl/uk.d.ts +117 -0
  147. package/dist/src/intl/uk.d.ts.map +1 -0
  148. package/dist/src/intl/uk.js +258 -0
  149. package/dist/src/intl/ur.d.ts +117 -0
  150. package/dist/src/intl/ur.d.ts.map +1 -0
  151. package/dist/src/intl/ur.js +245 -0
  152. package/dist/src/intl/vi.d.ts +117 -0
  153. package/dist/src/intl/vi.d.ts.map +1 -0
  154. package/dist/src/intl/vi.js +255 -0
  155. package/dist/src/intl/zh-CN.d.ts +117 -0
  156. package/dist/src/intl/zh-CN.d.ts.map +1 -0
  157. package/dist/src/intl/zh-CN.js +258 -0
  158. package/dist/src/intl/zh-TW.d.ts +117 -0
  159. package/dist/src/intl/zh-TW.d.ts.map +1 -0
  160. package/dist/src/intl/zh-TW.js +258 -0
  161. package/dist/src/local-first/Owner.d.ts +156 -62
  162. package/dist/src/local-first/Owner.d.ts.map +1 -1
  163. package/dist/src/local-first/Owner.js +119 -20
  164. package/package.json +1 -1
  165. package/src/Function.ts +2 -0
  166. package/src/Http.ts +9 -0
  167. package/src/Object.ts +2 -0
  168. package/src/RefCount.ts +2 -0
  169. package/src/Relation.ts +4 -0
  170. package/src/Resource.ts +2 -0
  171. package/src/Result.ts +397 -560
  172. package/src/StackTrace.ts +2 -0
  173. package/src/Task.ts +1143 -1222
  174. package/src/Time.ts +2 -0
  175. package/src/Type.ts +1598 -712
  176. package/src/intl/_en.ts +497 -0
  177. package/src/intl/ar.ts +417 -0
  178. package/src/intl/bn.ts +486 -0
  179. package/src/intl/ca.ts +488 -0
  180. package/src/intl/cs.ts +95 -31
  181. package/src/intl/da.ts +488 -0
  182. package/src/intl/de.ts +488 -0
  183. package/src/intl/el.ts +494 -0
  184. package/src/intl/es.ts +488 -0
  185. package/src/intl/fa.ts +487 -0
  186. package/src/intl/fi.ts +487 -0
  187. package/src/intl/fil.ts +488 -0
  188. package/src/intl/fr.ts +488 -0
  189. package/src/intl/he.ts +486 -0
  190. package/src/intl/hi.ts +487 -0
  191. package/src/intl/hr.ts +494 -0
  192. package/src/intl/hu.ts +387 -0
  193. package/src/intl/id.ts +487 -0
  194. package/src/intl/index.ts +42 -0
  195. package/src/intl/it.ts +488 -0
  196. package/src/intl/ja.ts +488 -0
  197. package/src/intl/ko.ts +487 -0
  198. package/src/intl/ml.ts +487 -0
  199. package/src/intl/mr.ts +487 -0
  200. package/src/intl/ms.ts +410 -0
  201. package/src/intl/nb.ts +364 -0
  202. package/src/intl/nl.ts +488 -0
  203. package/src/intl/pa.ts +487 -0
  204. package/src/intl/pl.ts +450 -0
  205. package/src/intl/pt-BR.ts +487 -0
  206. package/src/intl/pt.ts +424 -0
  207. package/src/intl/ro.ts +488 -0
  208. package/src/intl/sk.ts +412 -0
  209. package/src/intl/sl.ts +487 -0
  210. package/src/intl/sv.ts +488 -0
  211. package/src/intl/sw.ts +324 -0
  212. package/src/intl/ta.ts +487 -0
  213. package/src/intl/te.ts +487 -0
  214. package/src/intl/th.ts +484 -0
  215. package/src/intl/tr.ts +487 -0
  216. package/src/intl/uk.ts +488 -0
  217. package/src/intl/ur.ts +410 -0
  218. package/src/intl/vi.ts +435 -0
  219. package/src/intl/zh-CN.ts +482 -0
  220. package/src/intl/zh-TW.ts +482 -0
  221. package/src/local-first/Owner.ts +156 -62
package/src/Task.ts CHANGED
@@ -1,290 +1,85 @@
1
1
  /**
2
- * JavaScript-native structured concurrency.
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
- * A Task is a function that receives a {@link Run} and returns an
123
- * {@link Awaitable | awaitable} {@link Result}.
124
- *
125
- * Structured concurrency is a simple idea: asynchronous operations form a tree
126
- * where every child belongs to a parent. A parent waits for its children before
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 its cleanup. Run makes them structural — `run(task)`
136
- * registers every child before it starts, and the parent settles only after
137
- * child cleanup finishes, so the wait cannot be forgotten.
138
- *
139
- * Evolu keeps the programming model close to idiomatic JavaScript: Tasks are
140
- * ordinary functions, Fibers are Promise-backed handles, abort uses
141
- * {@link AbortSignal}, and lifetimes compose with `using` and `await using`. The
142
- * core abstractions are:
143
- *
144
- * - {@link Task}: a function that receives a {@link Run} and returns an awaitable
145
- * {@link Result}
146
- * - {@link Run}: a callable object that starts Tasks, owns their lifetimes,
147
- * provides dependencies, and exposes monitoring state
148
- * - {@link Fiber}: a Promise-backed handle returned by a Run when it starts a Task
149
- * - {@link AbortableFiber}: a Fiber with explicit abort and async disposal
150
- *
151
- * Calling `run(task)` creates a child Run for that Task. Calling `task(run)`
152
- * directly reuses the current Run and bypasses those child lifetime
153
- * boundaries.
154
- *
155
- * Tasks return domain success or failure as a Result. Abort is control flow: a
156
- * Fiber from `run(task)` rejects with {@link AbortError} when the Task is
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
- * // A dependency - wraps native fetch for testability.
196
- * interface NativeFetchDep {
197
- * readonly nativeFetch: typeof globalThis.fetch;
47
+ * interface User {
48
+ * readonly id: string;
49
+ * readonly name: string;
198
50
  * }
199
51
  *
200
- * interface NaiveFetchError {
201
- * readonly type: "NaiveFetchError";
202
- * readonly error: unknown;
52
+ * interface Db {
53
+ * readonly usersById: ReadonlyMap<string, User>;
203
54
  * }
204
55
  *
205
- * // A naive Task wrapping native fetch - adds abortability.
206
- * const naiveFetch =
207
- * (url: string): Task<Response, NaiveFetchError, NativeFetchDep> =>
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
- * // Create a Run with those dependencies.
241
- * await using run = createRun(deps);
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
- * // Running a Task returns a Fiber; awaiting it gives a Result.
244
- * const result = await run(naiveFetch("/users/123"));
245
- * expectTypeOf(result).toEqualTypeOf<Result<Response, NaiveFetchError>>();
246
- * assert(result.ok);
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
- * Evolu's {@link fetch} is not fancier than the naive wrapper; it is correctly
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
- * ```ts
274
- * import {
275
- * createRun,
276
- * fetch,
277
- * type FetchError,
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 text = await run(fetch("/readme.txt", "text"));
286
- * expectTypeOf(text).toEqualTypeOf<Result<string, FetchError>>();
287
- * expectOk(text, "readme");
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
- * | Abortability | {@link unabortable} | Mask abort after a Task starts |
318
- * | | {@link unabortableMask} | Mask acquire/release and restore use |
319
- * | | {@link acquireUseRelease} | Bracket acquire, use, and release |
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 { createRun, ok, type Result, type Task } from "@evolu/common";
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 loadUser =
338
- * (id: string): Task<User, "UserError"> =>
142
+ * const getUser =
143
+ * (id: string): Task<User, UserNotFoundError> =>
339
144
  * () =>
340
- * ok({ id, profileId: "profile-1" });
341
- * const loadProfile =
342
- * (id: string): Task<Profile, "ProfileError"> =>
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
- * ok({ id });
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 loadUserWithProfile =
164
+ * const getUserWithProfile =
347
165
  * (
348
166
  * id: string,
349
167
  * ): Task<
350
168
  * { readonly user: User; readonly profile: Profile },
351
- * "UserError" | "ProfileError"
169
+ * UserNotFoundError | ProfileNotFoundError
352
170
  * > =>
353
171
  * async (run) => {
354
- * const user = await run(loadUser(id));
172
+ * const user = await run(getUser(id));
355
173
  * if (!user.ok) return user;
356
174
  *
357
- * const profile = await run(loadProfile(user.value.profileId));
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(loadUserWithProfile("user-1"));
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
- * "UserError" | "ProfileError"
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. For the tradeoffs behind that choice, see “Why not generators?”
381
- * in the {@link Result} FAQ.
198
+ * control flow.
382
199
  *
383
- * ### Building a Resilient Fetch Task
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. Use {@link timeout} to limit how long a request may
387
- * run:
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
- * fetchWithTimeout(url),
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
- * Run composed Tasks with a concurrency limit and {@link all}:
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 Injection
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
- * ### Default Dependencies
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 Management
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 Resources from Tasks
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, ConnectionError> = () =>
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, ConnectionError> =>
395
+ * (_socket: Socket): Task<void, ConnectionFailedError> =>
617
396
  * () =>
618
397
  * ok();
619
398
  *
620
- * const createConnection: Task<Connection, ConnectionError> = async (
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
- * Factories that return disposable values and do not fail can be used directly
649
- * with {@link Run.ok} and `await using`:
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
- * ```ts
726
- * import type { Awaitable as EvoluAwaitable } from "@evolu/common";
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
- * Benefits:
746
- *
747
- * - **No API ambiguity** Task is async, Result is sync
748
- * - **No Task overhead for sync code** plain sync functions can return Result
749
- *
750
- * While a unified sync/async API is technically possible — with
751
- * {@link isPromiseLike} detection and two-phase disposal (sync first, async if
752
- * needed, and a flag for callers) — Evolu prefers plain functions for sync code
753
- * because most operations involve I/O, which is inherently async, and when sync
754
- * is needed, it is for simplicity (ideally no dependencies) and performance
755
- * (zero abstraction overhead).
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. Sync functions taking deps often
762
- * indicate a design that could be improved. For example, a function taking
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
- * Slow sync operations such as parsing large JSON, sorting millions of items,
766
- * or complex cryptography belong in workers. The async boundary to the worker
767
- * is a Task with full Run lifetime control: timeout, abort, cleanup, and
768
- * monitoring. The sync code inside the worker needs no monitoring; the async
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 exception or rejected Promise from a Task body.
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}. Defects are not outcomes; they are
776
- * reported through {@link ReportDefectDep} whether or not the Fiber is
777
- * observed.
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 happens, it is too late: the root Run panics, running
824
- * Tasks are aborted, and the Run tree shuts down. If a defect is recoverable,
825
- * wrap the code that throws or rejects with {@link trySync} or {@link tryAsync}
826
- * so it becomes a typed {@link Result} error. Do not wrap every defect;
827
- * unrecoverable defects should remain defects because an {@link Err} would not
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<void> =>
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
- * visit(node);
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(await run(visitTree(tree)), undefined);
917
- * expect(visited).toEqual(["root", "child"]);
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
- * ### Why does Task use explicit Result handling?
622
+ * ### Should a Task be called directly?
927
623
  *
928
- * Task uses native TypeScript control flow so each async boundary and error
929
- * propagation point is visible.
930
- *
931
- * ```ts
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
- * Task uses modern JavaScript APIs such as `Promise.withResolvers`,
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 error.
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 `Done<D>` signals normal completion rather
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 {@link Run.ok}
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 optional deps argument replaces the custom deps available to the Task.
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> = ({ deps }) => {
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, { db });
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 value.
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, "LoadUserError", DbDep> = async (run) => {
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, "LoadUserError", DbDep>
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 requests
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
- * ### Example
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
- * For a long-lived reusable {@link Run}, use {@link Run.create}.
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 Run's
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> = ({ deps }) =>
1374
- * ok(deps.db.users[0] ?? "Unknown");
1375
- * const saveUser: Task<void, never, DbDep> = ({ deps }) => {
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 Tasks and closure-held cleanup
1515
- * registered with {@link DisposableRun.defer}; disposing it shuts down both.
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 Tasks settle and are awaited by
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 Tasks, prevents new Tasks from starting, and waits for
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 Fiber
1599
- * resolves with the Task {@link Result} when the Task returns normally. A Fiber
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 resolves with
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 Tasks inherit their parent's mask. `unabortable` increments the
1746
- * mask for the wrapped Task, and `unabortableMask` provides `restore` to run
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
- * Typed object explaining why a {@link Run} was aborted.
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
- * Typed object representing structured-concurrency abort control flow.
1615
+ * Runtime Type for structured-concurrency abort control flow.
1774
1616
  *
1775
- * AbortError is thrown to stop Task execution when a Run observes an abort
1776
- * request. AbortableFiber catches AbortError and returns it as a {@link Result}
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 returned a
1818
- * Result; {@link Err} means the Run aborted with {@link AbortError}. Panic is
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 Core
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 Tasks stop while the Run waits for them to
1962
- * settle. This reason distinguishes that cleanup path from explicit abort and
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 Run
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 Run
1843
+ * @group Testing
1996
1844
  */
1997
1845
  export const testAbortError = /*#__PURE__*/ createAbortError(testAbortReason);
1998
1846
 
1999
1847
  /**
2000
- * A root-level abort caused by a defect.
1848
+ * Abort reason recorded when a defect panics the root {@link Run}.
2001
1849
  *
2002
- * Panic is a root-level abort caused by a defect: a thrown exception or
2003
- * rejected Promise. Recoverable domain errors belong in Result. Unexpected or
2004
- * unrecoverable conditions, such as storage engine errors the Task cannot
2005
- * usefully handle, may throw or reject. {@link AbortError} is abort control
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} reports the
2063
- * {@link AbortError} whose reason is {@link PanicAbortReason}, and event
2064
- * monitoring reports observer defects without panicking the Run. The original
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 Runs include platform-independent implementations for console, leak
2102
- * detection, native fetch, randomness, error reporting, time, and optional Run
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
- /** Creates a root Run with custom deps merged over {@link RunDefaultDeps}. */
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
- /** Provides a test {@link Run} with deterministic default dependencies. */
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 array.
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`. A
2864
- * platform `availableParallelism()` result is often a good limit for CPU-bound
2865
- * Tasks. For network or database Tasks, choose a limit based on the transport,
2866
- * server, connection pool, and rate limits.
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 Tasks.
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 Tasks until all return {@link Ok} or one returns {@link Err}.
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 the
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, SaveUserError> =>
2848
+ * (id: string): Task<number, SaveUserFailedError> =>
2960
2849
  * () => {
2961
2850
  * if (id === "missing") {
2962
- * return err({ type: "SaveUserError", userId: id });
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"], saveUser, {
2863
+ * all([saveUser("user-1"), saveUser("missing"), saveUser("user-3")], {
2969
2864
  * collect: false,
2970
2865
  * }),
2971
2866
  * );
2972
- * expectTypeOf(saveResult).toEqualTypeOf<Result<void, SaveUserError>>();
2867
+ * expectTypeOf(saveResult).toEqualTypeOf<
2868
+ * Result<void, SaveUserFailedError>
2869
+ * >();
2973
2870
  * expectErr(saveResult, {
2974
- * type: "SaveUserError",
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 Tasks and preserves its shape.
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 Tasks and preserves its keys.
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 its Tasks.
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 Tasks and returns every Task {@link Result}.
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
- * interface LoadError {
3247
- * readonly type: "LoadError";
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 [Result<string, LoadError>, Result<ReadonlyArray<string>>]
3199
+ * readonly [
3200
+ * Result<string, ProfileNotFoundError>,
3201
+ * Result<ReadonlyArray<string>>,
3202
+ * ]
3263
3203
  * >
3264
3204
  * >();
3265
3205
  * expectOk(results, [
3266
- * { ok: false, error: { type: "LoadError" } },
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
- * interface LoadError {
3299
- * readonly type: "LoadError";
3300
- * }
3239
+ *
3301
3240
  * const fetchUser: Task<User> = () => ok({ id: "user-1" });
3302
- * const fetchProfile: Task<string, LoadError> = () =>
3303
- * err({ type: "LoadError" });
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, LoadError>;
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: "LoadError" } },
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 Tasks and preserves its shape.
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
- * interface LoadError {
3346
- * readonly type: "LoadError";
3347
- * }
3287
+ *
3348
3288
  * const loadUser =
3349
- * (id: string): Task<User, LoadError> =>
3289
+ * (id: string): Task<User, UserNotFoundError> =>
3350
3290
  * () =>
3351
- * id === "missing" ? err({ type: "LoadError" }) : ok({ id });
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<readonly [Result<User, LoadError>, Result<User, LoadError>]>
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: "LoadError" } },
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 Tasks and preserves its keys.
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
- * interface LoadError {
3411
- * readonly type: "LoadError";
3412
- * }
3358
+ *
3413
3359
  * const loadUser =
3414
- * (id: string): Task<User, LoadError> =>
3360
+ * (id: string): Task<User, UserNotFoundError> =>
3415
3361
  * () =>
3416
- * id === "missing" ? err({ type: "LoadError" }) : ok({ id });
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, LoadError>;
3433
- * readonly reviewer: Result<User, LoadError>;
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: "LoadError" } },
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 observed
3546
- * at Fiber boundaries as {@link AbortError} with {@link PanicAbortReason}.
3547
- * Rejecting AbortError is reserved for Task abort control flow: direct
3548
- * `run(task)` rejects with it, and `run.abortable(task)` returns it as an Err.
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 still
3564
- * reports the panic. Cleanup must be synchronous; returned promises are not
3565
- * awaited. For async cleanup, prefer {@link acquireUseRelease}, `await using`
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 Run
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
- * Error returned by {@link retry} when retrying stops after a domain error.
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 control
3767
- * flow. Other errors are domain errors: retrying continues while the schedule
3768
- * yields another delay and {@link RetryOptions.shouldRetry} returns `true`. When
3769
- * retrying stops, `retry` returns {@link RetryError} with the final domain error
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
- * ### Example
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
- * await using run = createRun();
3789
- *
3790
- * interface FetchDataError {
3791
- * readonly type: "FetchDataError";
3792
- * }
3729
+ * const fetchData: Task<string, ServiceUnavailableError> = () =>
3730
+ * err({ type: "ServiceUnavailable" });
3793
3731
  *
3794
- * const fetchData: Task<string, FetchDataError> = () =>
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<FetchDataError>>
3739
+ * Result<string, RetryTaskError<ServiceUnavailableError>>
3802
3740
  * >();
3803
3741
  * expectErr(result, {
3804
3742
  * type: "RetryError",
3805
3743
  * attempts: 3,
3806
- * lastError: { type: "FetchDataError" },
3744
+ * lastError: { type: "ServiceUnavailable" },
3807
3745
  * });
3808
3746
  * ```
3809
3747
  *
3810
- * ### Example
3748
+ * ### Filtering retries
3811
3749
  *
3812
3750
  * ```ts
3813
- * import { createRun, err, recurs, retry, type Task } from "@evolu/common";
3751
+ * import {
3752
+ * createRun,
3753
+ * err,
3754
+ * recurs,
3755
+ * retry,
3756
+ * type Task,
3757
+ * type Typed,
3758
+ * } from "@evolu/common";
3814
3759
  *
3815
- * interface FetchDataError {
3816
- * readonly type: "RecoverableError" | "FatalError";
3817
- * }
3760
+ * const fetchData: Task<
3761
+ * string,
3762
+ * TemporaryFailureError | PermanentFailureError
3763
+ * > = () => err({ type: "PermanentFailure" });
3764
+ *
3765
+ * interface TemporaryFailureError extends Typed<"TemporaryFailure"> {}
3818
3766
  *
3819
- * const fetchData: Task<string, FetchDataError> = () =>
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 !== "FatalError",
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: "FatalError" },
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 Result. If
3920
- * the Task returns {@link Err}, including {@link Done} from a {@link NextTask},
3921
- * `repeat` returns that error without scheduling another attempt.
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
- * ### Example
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
- * ### Example
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 Task array.
3980
+ * Extracts the {@link Result} type produced by one {@link Task} in a non-empty
3981
+ * Task array.
4009
3982
  *
4010
- * @internal
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 Tasks until one returns {@link Ok} or all return {@link Err}.
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 or
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. Losing Tasks are aborted. If no Task
4022
- * returns Ok, returns the last Err by input order, regardless of completion
4023
- * order. Other Err results are discarded; use {@link allSettled} when you need
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 races Tasks, returns Result values, and aborts losers.
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, "Unavailable"> = () =>
4046
- * err("Unavailable");
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<Result<string, "Unavailable">>();
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 Tasks until the first Task settles.
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
- * ### Example
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
- * ### Example
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 Tasks until `count` Tasks return {@link Ok} or all Tasks settle.
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} Results are ignored. When `count` Ok values have settled,
4209
- * remaining Tasks are aborted. If fewer than `count` Tasks return Ok, returns
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, "Failed"> = async (run) => {
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
- * () => err("Failed" as const),
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 Tasks until `count` Tasks settle or all Tasks settle.
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, remaining Tasks are aborted. If
4282
- * fewer than `count` Tasks settle, returns the Results that did 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, "Failed"> = async (run) => {
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 tasks = [
4307
- * slow,
4308
- * () => err("Failed" as const),
4309
- * () => ok("fast"),
4310
- * ] as const;
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: "Failed" },
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 Tasks to start when concurrency capacity is
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 Concurrency
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 Concurrency
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 Tasks under a concurrency limit and calls `onResult` for each Task
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 order is
4380
- * settlement order, not input order. Returning `continue` lets queued Tasks
4381
- * start when capacity is available. Returning `stop` prevents queued Tasks from
4382
- * starting and aborts already-running Tasks through structured Run disposal —
4383
- * `each` still waits for them to settle before returning.
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 tasks = [
4420
- * slow,
4421
- * () => err("Unavailable" as const),
4422
- * () => ok("fast"),
4423
- * ] as const;
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 Concurrency
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 Tasks started through a native scheduler.
4516
+ * Scheduler priority for {@link Task}s started through a native scheduler.
4526
4517
  *
4527
- * Only static priorities are supported. Mutable scheduler priorities will be
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 Run
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> = ({ deps }) => {
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 `Err` results from the daemon Task are discarded after the
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 through
4716
- * that Fiber; `daemon(task)` starts a daemon child and stops waiting when the
4717
- * current Run aborts. {@link unabortable} masks abort for a Task that must
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
- * interface MyError {
4766
- * readonly type: "MyError";
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, MyError> = () =>
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, MyError>
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, MyError> = () => promise;
4782
+ * const task: Task<ResultValue> = () => promise;
4800
4783
  *
4801
4784
  * expect(promiseStarted).toBe(true);
4802
- * expectTypeOf(task).toEqualTypeOf<Task<ResultValue, MyError>>();
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 waits for
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 ({ signal }) => {
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 Tasks that should
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
- * ({ signal }) => {
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, "Unavailable"> =>
4999
+ * (connection: Connection): Task<string, UserUnavailableError> =>
5017
5000
  * () =>
5018
- * connection.isAvailable ? ok(connection.user) : err("Unavailable");
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), "Unavailable");
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 Abortability
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 external
5074
- * callback or another Task. Deferred is `Promise.withResolvers` with Task
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
- * ### Example
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 Tasks.
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 Tasks by acquiring and releasing permits.
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 release the
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 Tasks one at a time.
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 Tasks independently for each key.
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 Tasks one at a time independently for each key, like {@link Mutex}.
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 Tasks: atomic async read-modify-write.
5907
- * Plain Ref cannot express that — between a sync read and a later write, 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
- * const first = run(getToken);
5946
- * await refreshStarted.promise;
5947
- * const second = run(getToken);
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: Add Run observability after Task migration.
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,