@evolu/common 8.1.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) 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/Number.d.ts +66 -6
  6. package/dist/src/Number.d.ts.map +1 -1
  7. package/dist/src/Number.js +11 -3
  8. package/dist/src/Object.d.ts +1 -0
  9. package/dist/src/Object.d.ts.map +1 -1
  10. package/dist/src/RefCount.d.ts +1 -0
  11. package/dist/src/RefCount.d.ts.map +1 -1
  12. package/dist/src/Relation.d.ts +2 -0
  13. package/dist/src/Relation.d.ts.map +1 -1
  14. package/dist/src/Resource.d.ts +1 -0
  15. package/dist/src/Resource.d.ts.map +1 -1
  16. package/dist/src/Result.d.ts +383 -548
  17. package/dist/src/Result.d.ts.map +1 -1
  18. package/dist/src/Result.js +296 -46
  19. package/dist/src/Schedule.d.ts +2 -2
  20. package/dist/src/Schedule.d.ts.map +1 -1
  21. package/dist/src/StackTrace.d.ts +1 -0
  22. package/dist/src/StackTrace.d.ts.map +1 -1
  23. package/dist/src/Task.d.ts +978 -1061
  24. package/dist/src/Task.d.ts.map +1 -1
  25. package/dist/src/Task.js +1010 -159
  26. package/dist/src/Time.d.ts +71 -34
  27. package/dist/src/Time.d.ts.map +1 -1
  28. package/dist/src/Time.js +77 -1
  29. package/dist/src/Type.d.ts +1740 -795
  30. package/dist/src/Type.d.ts.map +1 -1
  31. package/dist/src/Type.js +1634 -211
  32. package/dist/src/Types.d.ts +0 -24
  33. package/dist/src/Types.d.ts.map +1 -1
  34. package/dist/src/intl/_en.d.ts +126 -0
  35. package/dist/src/intl/_en.d.ts.map +1 -0
  36. package/dist/src/intl/_en.js +267 -0
  37. package/dist/src/intl/ar.d.ts +117 -0
  38. package/dist/src/intl/ar.d.ts.map +1 -0
  39. package/dist/src/intl/ar.js +245 -0
  40. package/dist/src/intl/bn.d.ts +117 -0
  41. package/dist/src/intl/bn.d.ts.map +1 -0
  42. package/dist/src/intl/bn.js +258 -0
  43. package/dist/src/intl/ca.d.ts +117 -0
  44. package/dist/src/intl/ca.d.ts.map +1 -0
  45. package/dist/src/intl/ca.js +258 -0
  46. package/dist/src/intl/cs.d.ts +30 -7
  47. package/dist/src/intl/cs.d.ts.map +1 -1
  48. package/dist/src/intl/cs.js +65 -28
  49. package/dist/src/intl/da.d.ts +117 -0
  50. package/dist/src/intl/da.d.ts.map +1 -0
  51. package/dist/src/intl/da.js +258 -0
  52. package/dist/src/intl/de.d.ts +117 -0
  53. package/dist/src/intl/de.d.ts.map +1 -0
  54. package/dist/src/intl/de.js +258 -0
  55. package/dist/src/intl/el.d.ts +117 -0
  56. package/dist/src/intl/el.d.ts.map +1 -0
  57. package/dist/src/intl/el.js +265 -0
  58. package/dist/src/intl/es.d.ts +117 -0
  59. package/dist/src/intl/es.d.ts.map +1 -0
  60. package/dist/src/intl/es.js +258 -0
  61. package/dist/src/intl/fa.d.ts +117 -0
  62. package/dist/src/intl/fa.d.ts.map +1 -0
  63. package/dist/src/intl/fa.js +258 -0
  64. package/dist/src/intl/fi.d.ts +117 -0
  65. package/dist/src/intl/fi.d.ts.map +1 -0
  66. package/dist/src/intl/fi.js +258 -0
  67. package/dist/src/intl/fil.d.ts +117 -0
  68. package/dist/src/intl/fil.d.ts.map +1 -0
  69. package/dist/src/intl/fil.js +258 -0
  70. package/dist/src/intl/fr.d.ts +117 -0
  71. package/dist/src/intl/fr.d.ts.map +1 -0
  72. package/dist/src/intl/fr.js +258 -0
  73. package/dist/src/intl/he.d.ts +117 -0
  74. package/dist/src/intl/he.d.ts.map +1 -0
  75. package/dist/src/intl/he.js +258 -0
  76. package/dist/src/intl/hi.d.ts +117 -0
  77. package/dist/src/intl/hi.d.ts.map +1 -0
  78. package/dist/src/intl/hi.js +258 -0
  79. package/dist/src/intl/hr.d.ts +117 -0
  80. package/dist/src/intl/hr.d.ts.map +1 -0
  81. package/dist/src/intl/hr.js +265 -0
  82. package/dist/src/intl/hu.d.ts +63 -0
  83. package/dist/src/intl/hu.d.ts.map +1 -0
  84. package/dist/src/intl/hu.js +204 -0
  85. package/dist/src/intl/id.d.ts +117 -0
  86. package/dist/src/intl/id.d.ts.map +1 -0
  87. package/dist/src/intl/id.js +258 -0
  88. package/dist/src/intl/index.d.ts +42 -0
  89. package/dist/src/intl/index.d.ts.map +1 -1
  90. package/dist/src/intl/index.js +42 -0
  91. package/dist/src/intl/it.d.ts +117 -0
  92. package/dist/src/intl/it.d.ts.map +1 -0
  93. package/dist/src/intl/it.js +258 -0
  94. package/dist/src/intl/ja.d.ts +117 -0
  95. package/dist/src/intl/ja.d.ts.map +1 -0
  96. package/dist/src/intl/ja.js +258 -0
  97. package/dist/src/intl/ko.d.ts +117 -0
  98. package/dist/src/intl/ko.d.ts.map +1 -0
  99. package/dist/src/intl/ko.js +258 -0
  100. package/dist/src/intl/ml.d.ts +117 -0
  101. package/dist/src/intl/ml.d.ts.map +1 -0
  102. package/dist/src/intl/ml.js +258 -0
  103. package/dist/src/intl/mr.d.ts +117 -0
  104. package/dist/src/intl/mr.d.ts.map +1 -0
  105. package/dist/src/intl/mr.js +258 -0
  106. package/dist/src/intl/ms.d.ts +117 -0
  107. package/dist/src/intl/ms.d.ts.map +1 -0
  108. package/dist/src/intl/ms.js +245 -0
  109. package/dist/src/intl/nb.d.ts +63 -0
  110. package/dist/src/intl/nb.d.ts.map +1 -0
  111. package/dist/src/intl/nb.js +191 -0
  112. package/dist/src/intl/nl.d.ts +117 -0
  113. package/dist/src/intl/nl.d.ts.map +1 -0
  114. package/dist/src/intl/nl.js +258 -0
  115. package/dist/src/intl/pa.d.ts +117 -0
  116. package/dist/src/intl/pa.d.ts.map +1 -0
  117. package/dist/src/intl/pa.js +258 -0
  118. package/dist/src/intl/pl.d.ts +117 -0
  119. package/dist/src/intl/pl.d.ts.map +1 -0
  120. package/dist/src/intl/pl.js +242 -0
  121. package/dist/src/intl/pt-BR.d.ts +117 -0
  122. package/dist/src/intl/pt-BR.d.ts.map +1 -0
  123. package/dist/src/intl/pt-BR.js +258 -0
  124. package/dist/src/intl/pt.d.ts +117 -0
  125. package/dist/src/intl/pt.d.ts.map +1 -0
  126. package/dist/src/intl/pt.js +246 -0
  127. package/dist/src/intl/ro.d.ts +117 -0
  128. package/dist/src/intl/ro.d.ts.map +1 -0
  129. package/dist/src/intl/ro.js +258 -0
  130. package/dist/src/intl/sk.d.ts +117 -0
  131. package/dist/src/intl/sk.d.ts.map +1 -0
  132. package/dist/src/intl/sk.js +244 -0
  133. package/dist/src/intl/sl.d.ts +117 -0
  134. package/dist/src/intl/sl.d.ts.map +1 -0
  135. package/dist/src/intl/sl.js +258 -0
  136. package/dist/src/intl/sv.d.ts +117 -0
  137. package/dist/src/intl/sv.d.ts.map +1 -0
  138. package/dist/src/intl/sv.js +258 -0
  139. package/dist/src/intl/sw.d.ts +63 -0
  140. package/dist/src/intl/sw.d.ts.map +1 -0
  141. package/dist/src/intl/sw.js +191 -0
  142. package/dist/src/intl/ta.d.ts +117 -0
  143. package/dist/src/intl/ta.d.ts.map +1 -0
  144. package/dist/src/intl/ta.js +258 -0
  145. package/dist/src/intl/te.d.ts +117 -0
  146. package/dist/src/intl/te.d.ts.map +1 -0
  147. package/dist/src/intl/te.js +258 -0
  148. package/dist/src/intl/th.d.ts +117 -0
  149. package/dist/src/intl/th.d.ts.map +1 -0
  150. package/dist/src/intl/th.js +258 -0
  151. package/dist/src/intl/tr.d.ts +117 -0
  152. package/dist/src/intl/tr.d.ts.map +1 -0
  153. package/dist/src/intl/tr.js +255 -0
  154. package/dist/src/intl/uk.d.ts +117 -0
  155. package/dist/src/intl/uk.d.ts.map +1 -0
  156. package/dist/src/intl/uk.js +258 -0
  157. package/dist/src/intl/ur.d.ts +117 -0
  158. package/dist/src/intl/ur.d.ts.map +1 -0
  159. package/dist/src/intl/ur.js +245 -0
  160. package/dist/src/intl/vi.d.ts +117 -0
  161. package/dist/src/intl/vi.d.ts.map +1 -0
  162. package/dist/src/intl/vi.js +255 -0
  163. package/dist/src/intl/zh-CN.d.ts +117 -0
  164. package/dist/src/intl/zh-CN.d.ts.map +1 -0
  165. package/dist/src/intl/zh-CN.js +258 -0
  166. package/dist/src/intl/zh-TW.d.ts +117 -0
  167. package/dist/src/intl/zh-TW.d.ts.map +1 -0
  168. package/dist/src/intl/zh-TW.js +258 -0
  169. package/dist/src/local-first/Owner.d.ts +156 -62
  170. package/dist/src/local-first/Owner.d.ts.map +1 -1
  171. package/dist/src/local-first/Owner.js +119 -20
  172. package/package.json +1 -1
  173. package/src/Function.ts +2 -0
  174. package/src/Http.ts +9 -0
  175. package/src/Number.ts +85 -14
  176. package/src/Object.ts +2 -0
  177. package/src/RefCount.ts +2 -0
  178. package/src/Relation.ts +4 -0
  179. package/src/Resource.ts +2 -0
  180. package/src/Result.ts +397 -560
  181. package/src/Schedule.ts +1 -1
  182. package/src/StackTrace.ts +2 -0
  183. package/src/Task.ts +1145 -1225
  184. package/src/Time.ts +110 -62
  185. package/src/Type.ts +2842 -835
  186. package/src/Types.ts +0 -49
  187. package/src/intl/_en.ts +497 -0
  188. package/src/intl/ar.ts +417 -0
  189. package/src/intl/bn.ts +486 -0
  190. package/src/intl/ca.ts +488 -0
  191. package/src/intl/cs.ts +131 -32
  192. package/src/intl/da.ts +488 -0
  193. package/src/intl/de.ts +488 -0
  194. package/src/intl/el.ts +494 -0
  195. package/src/intl/es.ts +488 -0
  196. package/src/intl/fa.ts +487 -0
  197. package/src/intl/fi.ts +487 -0
  198. package/src/intl/fil.ts +488 -0
  199. package/src/intl/fr.ts +488 -0
  200. package/src/intl/he.ts +486 -0
  201. package/src/intl/hi.ts +487 -0
  202. package/src/intl/hr.ts +494 -0
  203. package/src/intl/hu.ts +387 -0
  204. package/src/intl/id.ts +487 -0
  205. package/src/intl/index.ts +42 -0
  206. package/src/intl/it.ts +488 -0
  207. package/src/intl/ja.ts +488 -0
  208. package/src/intl/ko.ts +487 -0
  209. package/src/intl/ml.ts +487 -0
  210. package/src/intl/mr.ts +487 -0
  211. package/src/intl/ms.ts +410 -0
  212. package/src/intl/nb.ts +364 -0
  213. package/src/intl/nl.ts +488 -0
  214. package/src/intl/pa.ts +487 -0
  215. package/src/intl/pl.ts +450 -0
  216. package/src/intl/pt-BR.ts +487 -0
  217. package/src/intl/pt.ts +424 -0
  218. package/src/intl/ro.ts +488 -0
  219. package/src/intl/sk.ts +412 -0
  220. package/src/intl/sl.ts +487 -0
  221. package/src/intl/sv.ts +488 -0
  222. package/src/intl/sw.ts +324 -0
  223. package/src/intl/ta.ts +487 -0
  224. package/src/intl/te.ts +487 -0
  225. package/src/intl/th.ts +484 -0
  226. package/src/intl/tr.ts +487 -0
  227. package/src/intl/uk.ts +488 -0
  228. package/src/intl/ur.ts +410 -0
  229. package/src/intl/vi.ts +435 -0
  230. package/src/intl/zh-CN.ts +482 -0
  231. package/src/intl/zh-TW.ts +482 -0
  232. package/src/local-first/Owner.ts +156 -62
@@ -1,192 +1,85 @@
1
1
  /**
2
- * JavaScript-native structured concurrency.
2
+ * ## Intro
3
3
  *
4
- * @module
5
- */
6
- import { type NonEmptyReadonlyArray } from "./Array.ts";
7
- import type { Brand } from "./Brand.ts";
8
- import { type ConsoleDep, type TestConsoleDep } from "./Console.ts";
9
- import { type RandomBytesDep } from "./Crypto.ts";
10
- import type { NativeFetchDep } from "./Http.ts";
11
- import { type LeakDetectorDep, type TestLeakDetectorDep } from "./LeakDetector.ts";
12
- import { type LookupOption } from "./Lookup.ts";
13
- import { type Option } from "./Option.ts";
14
- import { type RandomDep, type RandomLibDep } from "./Random.ts";
15
- import { type Ref } from "./Ref.ts";
16
- import { type Done, type Result } from "./Result.ts";
17
- import type { Schedule, ScheduleStep } from "./Schedule.ts";
18
- import { type Millis, type PositiveDuration, type TimeDep, type TestTimeDep } from "./Time.ts";
19
- import { type InferType, NonNegativeInt, PositiveInt, String, Unknown, UnknownResult, type Id, type Int1To100OrPositiveInt, type ObjectType, type RecordType, type Typed, type TypedType } from "./Type.ts";
20
- import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
21
- /**
22
4
  * JavaScript-native structured concurrency.
23
5
  *
24
- * A Task is a function that receives a {@link Run} and returns an
25
- * {@link Awaitable | awaitable} {@link Result}.
26
- *
27
- * Structured concurrency is a simple idea: asynchronous operations form a tree
28
- * where every child belongs to a parent. A parent waits for its children before
29
- * it completes, and abort follows the tree: aborting a parent requests abort of
30
- * all its descendants, while races and fail-fast operations also abort
31
- * remaining sibling branches. This prevents detached work and gives
32
- * cancellation, failure, and cleanup explicit ownership. The tree also makes
33
- * 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.
34
11
  *
35
12
  * With plain {@link AbortController} code, these guarantees depend on call-site
36
13
  * discipline: someone must remember the `finally` that aborts started work and
37
- * the await that waits for its cleanup. Run makes them structural — `run(task)`
38
- * registers every child before it starts, and the parent settles only after
39
- * child cleanup finishes, so the wait cannot be forgotten.
40
- *
41
- * Evolu keeps the programming model close to idiomatic JavaScript: Tasks are
42
- * ordinary functions, Fibers are Promise-backed handles, abort uses
43
- * {@link AbortSignal}, and lifetimes compose with `using` and `await using`. The
44
- * core abstractions are:
45
- *
46
- * - {@link Task}: a function that receives a {@link Run} and returns an awaitable
47
- * {@link Result}
48
- * - {@link Run}: a callable object that starts Tasks, owns their lifetimes,
49
- * provides dependencies, and exposes monitoring state
50
- * - {@link Fiber}: a Promise-backed handle returned by a Run when it starts a Task
51
- * - {@link AbortableFiber}: a Fiber with explicit abort and async disposal
52
- *
53
- * Calling `run(task)` creates a child Run for that Task. Calling `task(run)`
54
- * directly reuses the current Run and bypasses those child lifetime
55
- * boundaries.
56
- *
57
- * Tasks return domain success or failure as a Result. Abort is control flow: a
58
- * Fiber from `run(task)` rejects with {@link AbortError} when the Task is
59
- * aborted, while `run.abortable(task)` catches abort and returns it as an
60
- * {@link Err}. If the Task itself throws or rejects, that is a defect. A defect
61
- * panics the Run tree, and a Fiber rejects with AbortError whose reason is
62
- * {@link PanicAbortReason}; an AbortableFiber returns that AbortError as an
63
- * Err.
64
- *
65
- * The core is intentionally small: ordinary Task functions, a callable Run with
66
- * closed-over state, Promise-backed Fibers, AbortSignal propagation, and
67
- * JavaScript resource management. That minimal model still covers abort,
68
- * cleanup, panic, dependency injection, monitoring, concurrency, and resource
69
- * bracketing. The code is covered by carefully written, readable tests, so they
70
- * serve as documentation too.
71
- *
72
- * ## Example
73
- *
74
- * This intentionally naive wrapper is useful for learning Task dependencies,
75
- * Result errors, and native AbortSignal interop. Do not copy it as a production
76
- * fetch helper: a Response is not a plain value — its unread body is tied to
77
- * the request signal, which aborts when the Task settles. The returned Response
78
- * escapes the scope that keeps it alive. Evolu's {@link fetch} exists to close
79
- * this gap.
80
- *
81
- * `nativeFetch` is already a {@link RunDefaultDeps | default dependency}; this
82
- * example declares the same shape locally to demonstrate how Tasks declare
83
- * capabilities, and passes it to {@link createRun} to show that default
84
- * 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.
85
36
  *
86
37
  * ```ts
87
38
  * import {
88
- * AbortError,
89
39
  * createRun,
90
40
  * err,
91
41
  * ok,
92
- * type AbortableFiber,
93
42
  * type Result,
94
43
  * type Task,
44
+ * type Typed,
95
45
  * } from "@evolu/common";
96
46
  *
97
- * // A dependency - wraps native fetch for testability.
98
- * interface NativeFetchDep {
99
- * readonly nativeFetch: typeof globalThis.fetch;
47
+ * interface User {
48
+ * readonly id: string;
49
+ * readonly name: string;
100
50
  * }
101
51
  *
102
- * interface NaiveFetchError {
103
- * readonly type: "NaiveFetchError";
104
- * readonly error: unknown;
52
+ * interface Db {
53
+ * readonly usersById: ReadonlyMap<string, User>;
105
54
  * }
106
55
  *
107
- * // A naive Task wrapping native fetch - adds abortability.
108
- * const naiveFetch =
109
- * (url: string): Task<Response, NaiveFetchError, NativeFetchDep> =>
110
- * async ({ deps, signal }) => {
111
- * try {
112
- * const response = await deps.nativeFetch(url, { signal });
113
- * return ok(response);
114
- * } catch (error) {
115
- * if (AbortError.is(error)) throw error;
116
- * return err({ type: "NaiveFetchError", error });
117
- * }
118
- * };
119
- *
120
- * const nativeFetch: typeof globalThis.fetch = async (input, init) => {
121
- * if (String(input).endsWith("/123")) {
122
- * return new Response(null, { status: 204 });
123
- * }
124
- *
125
- * return new Promise<Response>((_resolve, reject) => {
126
- * const { signal } = init ?? {};
127
- * if (signal?.aborted) {
128
- * reject(signal.reason);
129
- * return;
130
- * }
131
- * signal?.addEventListener("abort", () => reject(signal.reason), {
132
- * once: true,
133
- * });
134
- * });
135
- * };
136
- *
137
- * // Provide dependencies at the composition root.
138
- * const deps: NativeFetchDep = {
139
- * nativeFetch,
140
- * };
141
- *
142
- * // Create a Run with those dependencies.
143
- * await using run = createRun(deps);
56
+ * interface DbDep {
57
+ * readonly db: Db;
58
+ * }
144
59
  *
145
- * // Running a Task returns a Fiber; awaiting it gives a Result.
146
- * const result = await run(naiveFetch("/users/123"));
147
- * expectTypeOf(result).toEqualTypeOf<Result<Response, NaiveFetchError>>();
148
- * assert(result.ok);
149
- * expect(result.value.status).toBe(204);
150
- *
151
- * // So what is naive about it? The Response ok value.
152
- * // Wrong: the Task settled, so its Run disposed and aborted `signal`.
153
- * // The Response body is a live resource tied to that signal. Whether
154
- * // this read fails immediately or appears to work depends on the
155
- * // runtime and on how much of the body was already buffered — it is
156
- * // timing-dependent behavior, not an API you can rely on.
157
- * // await result.value.json();
158
- *
159
- * // Abort works when native fetch rejects with signal.reason. Some hosts use
160
- * // their own abort error, which this naive wrapper does not normalize.
161
- * const fiber = run.abortable(naiveFetch("/users/456"));
162
- * fiber.abort();
163
- * const abortResult = await fiber;
164
- * expectTypeOf(fiber).toEqualTypeOf<
165
- * AbortableFiber<Response, NaiveFetchError, NativeFetchDep>
166
- * >();
167
- * assert(!abortResult.ok);
168
- * expect(AbortError.is(abortResult.error)).toBe(true);
169
- * ```
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
+ * };
170
66
  *
171
- * Evolu's {@link fetch} is not fancier than the naive wrapper; it is correctly
172
- * bounded. It consumes the Response body inside the Task, while the request
173
- * signal is still alive, and returns a plain value:
67
+ * // Typed declares the `type` discriminant without repeating the property.
68
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {
69
+ * readonly id: string;
70
+ * }
174
71
  *
175
- * ```ts
176
- * import {
177
- * createRun,
178
- * fetch,
179
- * type FetchError,
180
- * type Result,
181
- * } from "@evolu/common";
72
+ * const user: User = { id: "user-1", name: "Ada" };
182
73
  *
183
- * const nativeFetch: typeof globalThis.fetch = async () =>
184
- * new Response("readme");
185
- * 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
+ * });
186
79
  *
187
- * const text = await run(fetch("/readme.txt", "text"));
188
- * expectTypeOf(text).toEqualTypeOf<Result<string, FetchError>>();
189
- * expectOk(text, "readme");
80
+ * const result = await run(getUser(user.id));
81
+ * expectTypeOf(result).toEqualTypeOf<Result<User, UserNotFoundError>>();
82
+ * expectOk(result, user);
190
83
  * ```
191
84
  *
192
85
  * In composition roots, prefer the lifecycle API from the matching Evolu
@@ -198,76 +91,99 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
198
91
  *
199
92
  * ## Composition
200
93
  *
201
- * | Category | Helper | Description |
202
- * | ------------ | ------------------------- | -------------------------------------- |
203
- * | Collection | {@link all} | Return Ok values or stop on first Err |
204
- * | | {@link allSettled} | Return every Task Result |
205
- * | | {@link each} | Handle each Task Result |
206
- * | Interop | {@link callback} | Wrap callback APIs |
207
- * | | {@link fetch} | Native fetch with bounded Response use |
208
- * | Timing | {@link sleep} | Pause execution |
209
- * | | {@link timeout} | Time-bounded execution |
210
- * | Resilience | {@link retry} | Retry domain errors with a schedule |
211
- * | Repetition | {@link repeat} | Repeat successes with a schedule |
212
- * | Racing | {@link any} | First Ok wins |
213
- * | | {@link race} | First settled Result wins |
214
- * | | {@link firstN} | First n Ok values win |
215
- * | | {@link firstNSettled} | First n Results win |
216
- * | Scheduling | {@link prioritized} | Assign scheduler priority |
217
- * | | {@link yieldNow} | Yield to the host scheduler |
218
- * | Lifetime | {@link daemon} | Run under root ownership |
219
- * | Abortability | {@link unabortable} | Mask abort after a Task starts |
220
- * | | {@link unabortableMask} | Mask acquire/release and restore use |
221
- * | | {@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 |
222
115
  *
223
116
  * Helpers that process multiple Tasks run sequentially by default. Use a
224
117
  * `concurrency` option to run more than one Task at a time.
225
118
  *
119
+ * ### Sequential composition
120
+ *
226
121
  * For ordinary sequential composition, use imperative code:
227
122
  *
228
123
  * ```ts
229
- * 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";
230
132
  *
231
133
  * interface User {
232
134
  * readonly id: string;
233
135
  * readonly profileId: string;
234
136
  * }
137
+ *
235
138
  * interface Profile {
236
139
  * readonly id: string;
237
140
  * }
238
141
  *
239
- * const loadUser =
240
- * (id: string): Task<User, "UserError"> =>
142
+ * const getUser =
143
+ * (id: string): Task<User, UserNotFoundError> =>
241
144
  * () =>
242
- * ok({ id, profileId: "profile-1" });
243
- * const loadProfile =
244
- * (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> =>
245
155
  * () =>
246
- * 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
+ * }
247
163
  *
248
- * const loadUserWithProfile =
164
+ * const getUserWithProfile =
249
165
  * (
250
166
  * id: string,
251
167
  * ): Task<
252
168
  * { readonly user: User; readonly profile: Profile },
253
- * "UserError" | "ProfileError"
169
+ * UserNotFoundError | ProfileNotFoundError
254
170
  * > =>
255
171
  * async (run) => {
256
- * const user = await run(loadUser(id));
172
+ * const user = await run(getUser(id));
257
173
  * if (!user.ok) return user;
258
174
  *
259
- * const profile = await run(loadProfile(user.value.profileId));
175
+ * const profile = await run(getProfile(user.value.profileId));
260
176
  * if (!profile.ok) return profile;
261
177
  *
262
178
  * return ok({ user: user.value, profile: profile.value });
263
179
  * };
264
180
  *
265
181
  * await using run = createRun();
266
- * const result = await run(loadUserWithProfile("user-1"));
182
+ * const result = await run(getUserWithProfile("user-1"));
267
183
  * expectTypeOf(result).toEqualTypeOf<
268
184
  * Result<
269
185
  * { readonly user: User; readonly profile: Profile },
270
- * "UserError" | "ProfileError"
186
+ * UserNotFoundError | ProfileNotFoundError
271
187
  * >
272
188
  * >();
273
189
  * expectOk(result, {
@@ -279,33 +195,13 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
279
195
  * Evolu intentionally avoids pipe APIs, chainable methods, and generator-based
280
196
  * effect DSLs. Plain async/await with early returns is easier to read, review,
281
197
  * and debug, and it lets TypeScript narrow Result values through ordinary
282
- * control flow. For the tradeoffs behind that choice, see “Why not generators?”
283
- * in the {@link Result} FAQ.
198
+ * control flow.
284
199
  *
285
- * ### Building a Resilient Fetch Task
200
+ * ### Resilient fetch
286
201
  *
287
202
  * {@link fetch} with a body mode already returns a plain value, so resilience is
288
- * ordinary Task composition. Use {@link timeout} to limit how long a request may
289
- * run:
290
- *
291
- * ```ts
292
- * import {
293
- * fetch,
294
- * timeout,
295
- * type FetchError,
296
- * type Task,
297
- * type TimeoutError,
298
- * } from "@evolu/common";
299
- *
300
- * const fetchWithTimeout = (url: string) =>
301
- * timeout(fetch(url, "text"), "30s");
302
- *
303
- * expectTypeOf(fetchWithTimeout).returns.toEqualTypeOf<
304
- * Task<string, FetchError | TimeoutError>
305
- * >();
306
- * ```
307
- *
308
- * 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:
309
205
  *
310
206
  * ```ts
311
207
  * import {
@@ -322,12 +218,9 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
322
218
  * type TimeoutError,
323
219
  * } from "@evolu/common";
324
220
  *
325
- * const fetchWithTimeout = (url: string) =>
326
- * timeout(fetch(url, "text"), "30s");
327
- *
328
221
  * const fetchWithRetry = (url: string) =>
329
222
  * retry(
330
- * fetchWithTimeout(url),
223
+ * timeout(fetch(url, "text"), "30s"),
331
224
  * // A jittered, capped, limited exponential backoff.
332
225
  * jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
333
226
  * );
@@ -337,7 +230,9 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
337
230
  * >();
338
231
  * ```
339
232
  *
340
- * 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}:
341
236
  *
342
237
  * ```ts
343
238
  * import { all, createRun, ok, sleep, type Task } from "@evolu/common";
@@ -367,23 +262,21 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
367
262
  * expect(maxActiveRequests).toBe(2);
368
263
  * ```
369
264
  *
370
- * ## Concurrency Primitives
371
- *
372
265
  * Task helpers compose Tasks; concurrency primitives are stateful objects that
373
266
  * coordinate Tasks across call sites. Create them with their `createX`
374
267
  * factories and share them where coordination is needed.
375
268
  *
376
- * | Primitive | Description |
377
- * | ---------------------- | -------------------------------------- |
378
- * | {@link Deferred} | One-shot value resolved from outside |
379
- * | {@link Gate} | Block and release Tasks repeatedly |
380
- * | {@link Semaphore} | Limit concurrent Tasks with permits |
381
- * | {@link Mutex} | Run Tasks one at a time |
382
- * | {@link SemaphoreByKey} | Per-key permits with automatic cleanup |
383
- * | {@link MutexByKey} | Per-key one-at-a-time execution |
384
- * | {@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 |
385
278
  *
386
- * ## Dependency Injection
279
+ * ## Dependency injection
387
280
  *
388
281
  * Task DI is
389
282
  * {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}
@@ -397,7 +290,42 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
397
290
  * Use normal Task arguments for per-call values and `D` for capabilities,
398
291
  * resources, or services shared by all code running inside a Run.
399
292
  *
400
- * ### 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
401
329
  *
402
330
  * {@link createRun} provides default {@link RunDefaultDeps} available to all
403
331
  * Tasks without declaring `D`:
@@ -410,62 +338,10 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
410
338
  * - {@link ReportDefect} — defect reporting
411
339
  * - {@link Time} — current time
412
340
  *
413
- * For example, using Console:
414
- *
415
- * ```ts
416
- * import { createRun, ok, type Task } from "@evolu/common";
417
- *
418
- * const myTask: Task<void> = async (run) => {
419
- * const { console } = run.deps;
420
- * console.log("started");
421
- * return ok();
422
- * };
423
- *
424
- * expectTypeOf(myTask).toEqualTypeOf<Task<void>>();
425
- * await using run = createRun();
426
- * expectOk(await run(myTask), undefined);
427
- * ```
428
- *
429
- * Custom Console with formatted output:
430
- *
431
- * ```ts
432
- * import {
433
- * createConsoleArrayOutput,
434
- * createConsole,
435
- * createConsoleFormatter,
436
- * createRun,
437
- * type ConsoleEntry,
438
- * } from "@evolu/common";
439
- *
440
- * const entries: Array<ConsoleEntry> = [];
441
- * const formatter = createConsoleFormatter()({
442
- * timestampFormat: "absolute",
443
- * });
444
- * const deps = {
445
- * console: createConsole({
446
- * output: createConsoleArrayOutput(entries),
447
- * formatter,
448
- * }),
449
- * };
450
- *
451
- * await using run = createRun(deps);
452
- * const console = run.deps.console.child("main");
453
- *
454
- * console.log("started");
455
- * expect(console.name).toBe("main");
456
- * expect(entries).toHaveLength(1);
457
- * expect(entries[0]?.path).toEqual(["main"]);
458
- * assert(entries[0]);
459
- * const formattedArgs = formatter(entries[0]);
460
- * // 21:20:25.588 [main] started
461
- * expect(formattedArgs[0]).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3} \[main\]$/);
462
- * expect(formattedArgs[1]).toBe("started");
463
- * ```
464
- *
465
341
  * For testing, use {@link testCreateRun} to get deterministic, controllable
466
342
  * implementations of all RunDefaultDeps.
467
343
  *
468
- * ## Resource Management
344
+ * ## Resource management
469
345
  *
470
346
  * JavaScript provides standard
471
347
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
@@ -479,7 +355,7 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
479
355
  * - Closure-held state bounded by a reusable {@link DisposableRun}:
480
356
  * {@link DisposableRun.defer}
481
357
  *
482
- * ### Returning Resources from Tasks
358
+ * ### Returning resources from Tasks
483
359
  *
484
360
  * A Task that successfully returns a disposable resource transfers ownership of
485
361
  * a live resource to its caller. The resource must remain live after the Task
@@ -494,32 +370,33 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
494
370
  * `undefined` should represent valid absence, not failure.
495
371
  *
496
372
  * ```ts
497
- * import { createRun, ok, type Task } from "@evolu/common";
373
+ * import { createRun, ok, type Task, type Typed } from "@evolu/common";
498
374
  *
499
- * interface ConnectionError {
500
- * readonly type: "ConnectionError";
501
- * }
502
375
  * interface Socket extends AsyncDisposable {
503
376
  * readonly send: (message: string) => string;
504
377
  * }
378
+ *
505
379
  * interface Connection extends AsyncDisposable {
506
380
  * readonly send: (message: string) => string;
507
381
  * }
508
382
  *
509
383
  * let socketDisposed = false;
510
- * const openSocket: Task<Socket, ConnectionError> = () =>
384
+ * const openSocket: Task<Socket, ConnectionFailedError> = () =>
511
385
  * ok({
512
386
  * send: (message) => message,
513
387
  * [Symbol.asyncDispose]: async () => {
514
388
  * socketDisposed = true;
515
389
  * },
516
390
  * });
391
+ *
392
+ * interface ConnectionFailedError extends Typed<"ConnectionFailed"> {}
393
+ *
517
394
  * const handshake =
518
- * (_socket: Socket): Task<void, ConnectionError> =>
395
+ * (_socket: Socket): Task<void, ConnectionFailedError> =>
519
396
  * () =>
520
397
  * ok();
521
398
  *
522
- * const createConnection: Task<Connection, ConnectionError> = async (
399
+ * const createConnection: Task<Connection, ConnectionFailedError> = async (
523
400
  * run,
524
401
  * ) => {
525
402
  * await using disposer = new AsyncDisposableStack();
@@ -547,95 +424,15 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
547
424
  * expect(socketDisposed).toBe(true);
548
425
  * ```
549
426
  *
550
- * Factories that return disposable values and do not fail can be used directly
551
- * with {@link Run.ok} and `await using`:
552
- *
553
- * ```ts
554
- * import { createRun, ok, type Task } from "@evolu/common";
555
- *
556
- * interface Foo extends AsyncDisposable {
557
- * readonly value: string;
558
- * }
559
- *
560
- * let disposed = false;
561
- * const createFoo = (): Task<Foo> => () =>
562
- * ok({
563
- * value: "foo",
564
- * [Symbol.asyncDispose]: async () => {
565
- * disposed = true;
566
- * },
567
- * });
568
- *
569
- * await using run = createRun();
570
- * {
571
- * await using foo = await run.ok(createFoo());
572
- * expect(foo.value).toBe("foo");
573
- * }
574
- * expect(disposed).toBe(true);
575
- * ```
576
- *
577
- * Use {@link acquireUseRelease} when acquisition and release are separate
578
- * operations rather than a disposable value:
579
- *
580
- * ```ts
581
- * import {
582
- * acquireUseRelease,
583
- * createRun,
584
- * ok,
585
- * type Task,
586
- * } from "@evolu/common";
587
- *
588
- * interface User {
589
- * readonly id: string;
590
- * readonly name: string;
591
- * }
592
- *
593
- * interface Connection {
594
- * readonly loadUser: (id: string) => User;
595
- * }
596
- *
597
- * const openConnection: Task<Connection> = () =>
598
- * ok({ loadUser: (id) => ({ id, name: "Ada" }) });
599
- *
600
- * const loadUser =
601
- * (connection: Connection): Task<User> =>
602
- * () =>
603
- * ok(connection.loadUser("user-1"));
604
- *
605
- * let connectionClosed = false;
606
- * const closeConnection =
607
- * (_connection: Connection): Task<void> =>
608
- * () => {
609
- * connectionClosed = true;
610
- * return ok();
611
- * };
612
- *
613
- * const queryUser = acquireUseRelease(
614
- * openConnection,
615
- * loadUser,
616
- * closeConnection,
617
- * );
618
- *
619
- * await using run = createRun();
620
- * const result = await run(queryUser);
621
- * expectOk(result, { id: "user-1", name: "Ada" });
622
- * expect(connectionClosed).toBe(true);
623
- * ```
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.
624
430
  *
625
431
  * ## Awaitable
626
432
  *
627
- * ```ts
628
- * import type { Awaitable as EvoluAwaitable } from "@evolu/common";
629
- *
630
- * type Awaitable<T> = T | PromiseLike<T>;
631
- *
632
- * expectTypeOf<Awaitable<number>>().toEqualTypeOf<
633
- * EvoluAwaitable<number>
634
- * >();
635
- * ```
636
- *
637
- * Even though {@link Task} returns {@link Awaitable}, allowing sync or async
638
- * 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.
639
436
  *
640
437
  * - **Sync** → {@link Result}, native `using` / `DisposableStack`
641
438
  * - **Async** → Task, Run, {@link Fiber}, `await using` / `AsyncDisposableStack`
@@ -644,39 +441,36 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
644
441
  * decomposition. Calling `run(task)` always creates a child Run by design. Use
645
442
  * ordinary promises when an async operation does not need its own Run.
646
443
  *
647
- * Benefits:
648
- *
649
- * - **No API ambiguity** Task is async, Result is sync
650
- * - **No Task overhead for sync code** plain sync functions can return Result
651
- *
652
- * While a unified sync/async API is technically possible — with
653
- * {@link isPromiseLike} detection and two-phase disposal (sync first, async if
654
- * needed, and a flag for callers) — Evolu prefers plain functions for sync code
655
- * because most operations involve I/O, which is inherently async, and when sync
656
- * is needed, it is for simplicity (ideally no dependencies) and performance
657
- * (zero abstraction overhead).
658
- *
659
- * Sync functions should be fast, so there is no need to monitor them. They
660
- * 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
661
455
  * {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
662
456
  * pattern where impure code gathers data, pure functions process it, and impure
663
- * code performs effects with the result. Sync functions taking deps often
664
- * indicate a design that could be improved. For example, a function taking
665
- * {@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}.
666
459
  *
667
- * Slow sync operations such as parsing large JSON, sorting millions of items,
668
- * or complex cryptography belong in workers. The async boundary to the worker
669
- * is a Task with full Run lifetime control: timeout, abort, cleanup, and
670
- * monitoring. The sync code inside the worker needs no monitoring; the async
671
- * 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.
672
464
  *
673
465
  * ## Glossary
674
466
  *
675
- * - **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.
676
469
  * - **Outcome** — a Fiber's settlement: resolution with the Task {@link Result},
677
- * or rejection with {@link AbortError}. Defects are not outcomes; they are
678
- * reported through {@link ReportDefectDep} whether or not the Fiber is
679
- * 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}.
680
474
  * - **Create** — construct a new value or a resource.
681
475
  * - **Acquire** — obtain a usable resource. Acquisition may create a new
682
476
  * resource, borrow one, open one, or take a lease/lock.
@@ -684,7 +478,7 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
684
478
  * pairs with acquire and need not mean disposal; examples include unlock,
685
479
  * logout, or returning a pooled resource.
686
480
  * - **Dispose / disposal** — owner-driven resource finalization via JavaScript
687
- * management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
481
+ * resource management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
688
482
  * `AsyncDisposableStack`).
689
483
  *
690
484
  * ## FAQ
@@ -722,12 +516,11 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
722
516
  *
723
517
  * ### What should Task code do with defects?
724
518
  *
725
- * Nothing. Once a defect happens, it is too late: the root Run panics, running
726
- * Tasks are aborted, and the Run tree shuts down. If a defect is recoverable,
727
- * wrap the code that throws or rejects with {@link trySync} or {@link tryAsync}
728
- * so it becomes a typed {@link Result} error. Do not wrap every defect;
729
- * unrecoverable defects should remain defects because an {@link Err} would not
730
- * 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.
731
524
  *
732
525
  * ### Why does a defect panic the whole Run tree?
733
526
  *
@@ -770,7 +563,9 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
770
563
  *
771
564
  * Task follows the opposite approach: Tasks are ordinary async functions, Run
772
565
  * owns lifetimes and scoped context, {@link Result} carries expected domain
773
- * 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.
774
569
  *
775
570
  * ### Are recursive Tasks stack-safe?
776
571
  *
@@ -790,33 +585,32 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
790
585
  * readonly children: ReadonlyArray<TreeNode>;
791
586
  * }
792
587
  *
793
- * const visited: Array<string> = [];
794
- * const visit = (node: TreeNode): void => {
795
- * visited.push(node.value);
796
- * };
797
- *
798
588
  * const visitTree =
799
- * (root: TreeNode): Task<void> =>
589
+ * (root: TreeNode): Task<ReadonlyArray<string>> =>
800
590
  * () => {
801
591
  * const remaining = [root];
592
+ * const visited: Array<string> = [];
802
593
  *
803
594
  * while (remaining.length > 0) {
804
595
  * const node = remaining.pop();
805
596
  * if (!node) continue;
806
- * visit(node);
597
+ * visited.push(node.value);
807
598
  * for (const child of node.children) remaining.push(child);
808
599
  * }
809
600
  *
810
- * return ok();
601
+ * return ok(visited);
811
602
  * };
812
603
  *
813
- * const tree: TreeNode = {
814
- * value: "root",
815
- * children: [{ value: "child", children: [] }],
816
- * };
817
604
  * await using run = createRun();
818
- * expectOk(await run(visitTree(tree)), undefined);
819
- * 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
+ * );
820
614
  * ```
821
615
  *
822
616
  * Task favors direct native execution, `async`/`await`, and native tooling over
@@ -825,35 +619,12 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
825
619
  * periodically await {@link yieldNow} for cooperative scheduling, and move
826
620
  * CPU-bound work to a worker.
827
621
  *
828
- * ### Why does Task use explicit Result handling?
829
- *
830
- * Task uses native TypeScript control flow so each async boundary and error
831
- * propagation point is visible.
832
- *
833
- * ```ts
834
- * import { createRun, ok, type Task } from "@evolu/common";
835
- *
836
- * const loadUser: Task<string, "LoadUserError"> = () => ok("Ada");
837
- * const greetUser: Task<string, "LoadUserError"> = async (run) => {
838
- * const user = await run(loadUser);
839
- * if (!user.ok) return user;
840
- * return ok(`Hello, ${user.value}`);
841
- * };
842
- *
843
- * await using run = createRun();
844
- * expectOk(await run(greetUser), "Hello, Ada");
845
- * ```
846
- *
847
- * This is slightly more verbose than fluent or generator-based syntax, but it's
848
- * simple to read, easy to debug, friendly to TypeScript narrowing, and works
849
- * well with generated code.
622
+ * ### Should a Task be called directly?
850
623
  *
851
- * ### Can a Task be called directly?
852
- *
853
- * Yes. A direct call, `task(run)`, uses the current Run instead of creating a
854
- * child Run, so it bypasses child lifetime tracking, scheduling metadata, and
855
- * child disposal boundaries. It is reserved for Task internals that explicitly
856
- * 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)`.
857
628
  *
858
629
  * ### Where are fork and join?
859
630
  *
@@ -872,6 +643,32 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
872
643
  * emitted by TypeScript; the polyfills provide the runtime resource-management
873
644
  * globals.
874
645
  *
646
+ * @module
647
+ */
648
+ import { type NonEmptyReadonlyArray } from "./Array.ts";
649
+ import type { Brand } from "./Brand.ts";
650
+ import { type ConsoleDep, type TestConsoleDep } from "./Console.ts";
651
+ import { type RandomBytesDep } from "./Crypto.ts";
652
+ import type { NativeFetchDep } from "./Http.ts";
653
+ import { type LeakDetectorDep, type TestLeakDetectorDep } from "./LeakDetector.ts";
654
+ import { type LookupOption } from "./Lookup.ts";
655
+ import { type Int1To100OrPositiveInt } from "./Number.ts";
656
+ import { type Option } from "./Option.ts";
657
+ import { type RandomDep, type RandomLibDep } from "./Random.ts";
658
+ import { type Ref } from "./Ref.ts";
659
+ import { type Done, type Result } from "./Result.ts";
660
+ import type { Schedule, ScheduleStep } from "./Schedule.ts";
661
+ import { type Millis, type PositiveDuration, type TimeDep, type TestTimeDep } from "./Time.ts";
662
+ import { type InferType, NonNegativeInt, PositiveInt, String, Unknown, UnknownResult, type Id, type ObjectType, type RecordType, type Typed, type TypedType } from "./Type.ts";
663
+ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
664
+ /**
665
+ * An operation run by {@link Run} that returns a {@link Result} synchronously or
666
+ * asynchronously and declares its dependencies through `D`.
667
+ *
668
+ * Its return type is {@link Awaitable}.
669
+ *
670
+ * See the {@link @evolu/common!Task | Task overview}.
671
+ *
875
672
  * @group Core
876
673
  */
877
674
  export type Task<T, E = never, D = unknown> = (run: Run<D>) => Awaitable<Result<T, E>>;
@@ -882,13 +679,13 @@ export type Task<T, E = never, D = unknown> = (run: Run<D>) => Awaitable<Result<
882
679
  */
883
680
  export type AnyTask = Task<any, any, any>;
884
681
  /**
885
- * Extracts the Ok value type from a {@link Task}.
682
+ * Extracts the {@link Ok} value type from a {@link Task}.
886
683
  *
887
684
  * @group Type utilities
888
685
  */
889
686
  export type InferTaskOk<TTask extends AnyTask> = TTask extends Task<infer T, any, any> ? T : never;
890
687
  /**
891
- * Extracts the Result error type from a {@link Task}.
688
+ * Extracts the {@link Result} error type from a {@link Task}.
892
689
  *
893
690
  * @group Type utilities
894
691
  */
@@ -900,10 +697,11 @@ export type InferTaskErr<TTask extends AnyTask> = TTask extends Task<any, infer
900
697
  */
901
698
  export type InferTaskDeps<TTask extends AnyTask> = TTask extends Task<any, any, infer D> ? D : never;
902
699
  /**
903
- * A {@link Task} that can return a value, signal done, or return a Result error.
700
+ * A {@link Task} that can return a value, signal done, or return a {@link Result}
701
+ * error.
904
702
  *
905
- * Use for pull-based protocols where `Done<D>` signals normal completion rather
906
- * than an error.
703
+ * Use for pull-based protocols where {@link Done | Done<D>} signals normal
704
+ * completion rather than an error.
907
705
  *
908
706
  * @group Core
909
707
  */
@@ -918,8 +716,8 @@ export type InferTaskDone<TTask extends AnyTask> = InferTaskErr<TTask> extends i
918
716
  * A {@link Task} whose error type is not `never`.
919
717
  *
920
718
  * Used by {@link Run.orThrow} to accept only Tasks that can return a declared
921
- * {@link Err}. Tasks without declared Result errors should use {@link Run.ok}
922
- * instead.
719
+ * {@link Err}. Tasks without declared {@link Result} errors should use
720
+ * {@link Run.ok} instead.
923
721
  *
924
722
  * @group Type utilities
925
723
  */
@@ -942,7 +740,7 @@ export type TaskWithError<TTask extends AnyTask> = TTask & ([InferTaskErr<TTask>
942
740
  * the child Fiber from the parent. If a parent Task returns before awaiting or
943
741
  * returning a child Fiber, cleanup still waits for the child. A child defect
944
742
  * during that cleanup panics and aborts the root Run, but the parent Fiber
945
- * keeps the Result already returned by the parent Task.
743
+ * keeps the {@link Result} already returned by the parent Task.
946
744
  *
947
745
  * Disposing a Run requests abort and prevents new child Tasks from starting.
948
746
  * Async disposal waits for current children to settle. Abort requests propagate
@@ -981,9 +779,7 @@ export interface Run<D = unknown> {
981
779
  * defects are still reported. Use {@link Run.daemon} for work that should
982
780
  * outlive the current Task.
983
781
  *
984
- * The optional deps argument replaces the custom deps available to the Task.
985
- * Default deps ({@link RunDefaultDeps}) are inherited unless replaced with
986
- * assignable alternatives.
782
+ * The Task uses this Run's current dependencies.
987
783
  *
988
784
  * The Fiber rejects when the Task observes abort by throwing
989
785
  * {@link AbortError}. It also rejects with AbortError whose reason is
@@ -1003,25 +799,32 @@ export interface Run<D = unknown> {
1003
799
  * interface Db {
1004
800
  * readonly name: string;
1005
801
  * }
802
+ *
1006
803
  * interface DbDep {
1007
804
  * readonly db: Db;
1008
805
  * }
1009
806
  *
1010
807
  * const db: Db = { name: "main" };
1011
808
  * const loadUser: Task<string> = () => ok("Ada");
1012
- * const saveUser: Task<void, never, DbDep> = ({ deps }) => {
1013
- * expect(deps.db).toBe(db);
809
+ * const saveUser: Task<void, never, DbDep> = (run) => {
810
+ * expect(run.deps.db).toBe(db);
1014
811
  * return ok();
1015
812
  * };
1016
813
  *
1017
- * await using run = createRun();
814
+ * await using run = createRun({ db });
1018
815
  * const userResult = await run(loadUser);
1019
- * const savedResult = await run(saveUser, { db });
816
+ * const savedResult = await run(saveUser);
1020
817
  * expectOk(userResult, "Ada");
1021
818
  * expectOk(savedResult, undefined);
1022
819
  * ```
1023
820
  */
1024
821
  <T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
822
+ /**
823
+ * Starts a {@link Task} with replacement custom dependencies.
824
+ *
825
+ * Default deps ({@link RunDefaultDeps}) are inherited unless replaced with
826
+ * assignable alternatives.
827
+ */
1025
828
  <T, E, Deps extends object>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): Fiber<T, E, Deps>;
1026
829
  /**
1027
830
  * Runs a {@link Task} whose error type is not `never` and throws if the
@@ -1030,18 +833,59 @@ export interface Run<D = unknown> {
1030
833
  * This is the Task equivalent of {@link getOrThrow}. Use it where a declared
1031
834
  * Result error should crash the current flow instead of being handled
1032
835
  * locally.
836
+ *
837
+ * ### Example
838
+ *
839
+ * ```ts
840
+ * import { createRun, ok, type Task, type Typed } from "@evolu/common";
841
+ *
842
+ * const loadConfig: Task<string, ConfigInvalidError> = () =>
843
+ * ok("config");
844
+ *
845
+ * interface ConfigInvalidError extends Typed<"ConfigInvalid"> {}
846
+ *
847
+ * await using run = createRun();
848
+ * expect(await run.orThrow(loadConfig)).toBe("config");
849
+ * ```
1033
850
  */
1034
851
  readonly orThrow: {
1035
852
  <TTask extends Task<any, any, D>>(task: TaskWithError<TTask>): Promise<InferTaskOk<TTask>>;
1036
853
  <Deps extends object, TTask extends Task<any, any, Deps>>(task: TaskWithError<TTask>, deps: RunCustomDeps<Deps>): Promise<InferTaskOk<TTask>>;
1037
854
  };
1038
855
  /**
1039
- * Runs a {@link Task} whose error type is `never` and returns its Ok value.
856
+ * Runs a {@link Task} whose error type is `never` and returns its {@link Ok}
857
+ * value.
1040
858
  *
1041
859
  * This is the Task equivalent of {@link getOk}.
1042
- */
1043
- readonly ok: {
1044
- <T>(task: Task<T, never, D>): Promise<T>;
860
+ *
861
+ * ### Example
862
+ *
863
+ * ```ts
864
+ * import { createRun, ok, type Task } from "@evolu/common";
865
+ *
866
+ * interface Resource extends AsyncDisposable {
867
+ * readonly value: string;
868
+ * }
869
+ *
870
+ * let disposed = false;
871
+ * const openResource: Task<Resource> = () =>
872
+ * ok({
873
+ * value: "resource",
874
+ * [Symbol.asyncDispose]: async () => {
875
+ * disposed = true;
876
+ * },
877
+ * });
878
+ *
879
+ * await using run = createRun();
880
+ * {
881
+ * await using resource = await run.ok(openResource);
882
+ * expect(resource.value).toBe("resource");
883
+ * }
884
+ * expect(disposed).toBe(true);
885
+ * ```
886
+ */
887
+ readonly ok: {
888
+ <T>(task: Task<T, never, D>): Promise<T>;
1045
889
  <T, Deps extends object>(task: Task<T, never, Deps>, deps: RunCustomDeps<Deps>): Promise<T>;
1046
890
  };
1047
891
  /**
@@ -1073,8 +917,9 @@ export interface Run<D = unknown> {
1073
917
  * interface DbDep {
1074
918
  * readonly db: { readonly name: string };
1075
919
  * }
920
+ *
1076
921
  * const db = { name: "main" };
1077
- * const loadUser: Task<string, "LoadUserError", DbDep> = async (run) => {
922
+ * const loadUser: Task<string, never, DbDep> = async (run) => {
1078
923
  * await run.ok(sleep("1s"));
1079
924
  * return ok(run.deps.db.name);
1080
925
  * };
@@ -1082,7 +927,7 @@ export interface Run<D = unknown> {
1082
927
  * await using run = createRun();
1083
928
  * const fiber = run.abortable(loadUser, { db });
1084
929
  * expectTypeOf(fiber).toEqualTypeOf<
1085
- * AbortableFiber<string, "LoadUserError", DbDep>
930
+ * AbortableFiber<string, never, DbDep>
1086
931
  * >();
1087
932
  * fiber.abort();
1088
933
  * const userResult = await fiber;
@@ -1100,8 +945,8 @@ export interface Run<D = unknown> {
1100
945
  * Normal child Runs are disposed after their Task settles. Tasks started by
1101
946
  * `run.daemon` detach their lifetime from the current Task and attach to the
1102
947
  * root Run, so they keep running until they settle or the root Run is
1103
- * disposed. Calling `.abort()` or async-disposing the returned Fiber requests
1104
- * abort. Keep the returned Fiber for lifetime control.
948
+ * disposed. Calling `.abort()` or async-disposing the returned {@link Fiber}
949
+ * requests abort. Keep the returned Fiber for lifetime control.
1105
950
  *
1106
951
  * The daemon receives deps derived from the Run that starts it, not from the
1107
952
  * root Run: `deps` replace that Run's custom deps for the daemon Task, while
@@ -1118,7 +963,9 @@ export interface Run<D = unknown> {
1118
963
  * un-aborted, because detached work must not spawn under a scope that is
1119
964
  * shutting down.
1120
965
  *
1121
- * ### Example
966
+ * For a long-lived reusable {@link Run}, use {@link Run.create}.
967
+ *
968
+ * ### Abort masks
1122
969
  *
1123
970
  * ```ts
1124
971
  * import { createRun, ok, unabortable, type Task } from "@evolu/common";
@@ -1146,9 +993,7 @@ export interface Run<D = unknown> {
1146
993
  * expectOk(await run(syncParent), ["synced", "synced"]);
1147
994
  * ```
1148
995
  *
1149
- * For a long-lived reusable {@link Run}, use {@link Run.create}.
1150
- *
1151
- * ### Example
996
+ * ### Aborting a daemon
1152
997
  *
1153
998
  * ```ts
1154
999
  * import {
@@ -1162,6 +1007,7 @@ export interface Run<D = unknown> {
1162
1007
  * interface DbDep {
1163
1008
  * readonly db: { readonly name: string };
1164
1009
  * }
1010
+ *
1165
1011
  * const db = { name: "main" };
1166
1012
  * const syncUsers: Task<void, never, DbDep> = async (run) => {
1167
1013
  * await run.ok(sleep("1s"));
@@ -1176,6 +1022,8 @@ export interface Run<D = unknown> {
1176
1022
  * expect(AbortError.is(syncResult.error)).toBe(true);
1177
1023
  * ```
1178
1024
  *
1025
+ * ### Disposing a daemon
1026
+ *
1179
1027
  * ```ts
1180
1028
  * import { createRun, ok, waitForAbort, type Task } from "@evolu/common";
1181
1029
  *
@@ -1202,11 +1050,11 @@ export interface Run<D = unknown> {
1202
1050
  <T, E, Deps extends object>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): AbortableFiber<T, E, Deps>;
1203
1051
  };
1204
1052
  /**
1205
- * Creates a {@link DisposableRun} attached to the root Run with this Run's
1206
- * deps.
1053
+ * Creates a {@link DisposableRun} attached to the root {@link Run} with this
1054
+ * Run's deps.
1207
1055
  *
1208
1056
  * Use it when you need a Run that can be reused across multiple operations.
1209
- * For a single long-lived Task, use {@link Run.daemon}.
1057
+ * For a single long-lived {@link Task}, use {@link Run.daemon}.
1210
1058
  *
1211
1059
  * Use deps to replace the created Run's custom deps. Default deps
1212
1060
  * ({@link RunDefaultDeps}) are inherited unless explicitly replaced with
@@ -1225,11 +1073,12 @@ export interface Run<D = unknown> {
1225
1073
  * interface DbDep {
1226
1074
  * readonly db: { readonly users: Array<string> };
1227
1075
  * }
1076
+ *
1228
1077
  * const db = { users: ["Ada"] };
1229
- * const loadUser: Task<string, never, DbDep> = ({ deps }) =>
1230
- * ok(deps.db.users[0] ?? "Unknown");
1231
- * const saveUser: Task<void, never, DbDep> = ({ deps }) => {
1232
- * deps.db.users.push("Grace");
1078
+ * const loadUser: Task<string, never, DbDep> = (run) =>
1079
+ * ok(run.deps.db.users[0] ?? "Unknown");
1080
+ * const saveUser: Task<void, never, DbDep> = (run) => {
1081
+ * run.deps.db.users.push("Grace");
1233
1082
  * return ok();
1234
1083
  * };
1235
1084
  *
@@ -1250,7 +1099,7 @@ export interface Run<D = unknown> {
1250
1099
  readonly id: Id;
1251
1100
  /** The parent {@link Run}, if this Run was created as a child. */
1252
1101
  readonly parent: Run | null;
1253
- /** Dependencies available to the Task, including {@link RunDefaultDeps}. */
1102
+ /** Dependencies available to the {@link Task}, including {@link RunDefaultDeps}. */
1254
1103
  readonly deps: RunDefaultDeps & D;
1255
1104
  /**
1256
1105
  * Abort signal for the {@link Task}.
@@ -1326,7 +1175,7 @@ export interface Run<D = unknown> {
1326
1175
  /**
1327
1176
  * Callback for monitoring Run events emitted by this Run or descendants.
1328
1177
  *
1329
- * Event handlers are observers, not part of Task control flow. Handler
1178
+ * Event handlers are observers, not part of {@link Task} control flow. Handler
1330
1179
  * defects are reported via {@link ReportDefectDep.reportDefect}; they do not
1331
1180
  * panic the root Run or change Run state.
1332
1181
  *
@@ -1351,52 +1200,21 @@ export type RunCustomDeps<D extends object> = D & {
1351
1200
  *
1352
1201
  * {@link createRun} creates a root DisposableRun. {@link Run.create} creates one
1353
1202
  * attached to that root, typically to give a reusable resource its own
1354
- * lifetime. A DisposableRun owns its child Tasks and closure-held cleanup
1355
- * registered with {@link DisposableRun.defer}; disposing it shuts down both.
1203
+ * lifetime. A DisposableRun owns its child {@link Task}s and closure-held
1204
+ * cleanup registered with {@link DisposableRun.defer}; disposing it shuts down
1205
+ * both.
1356
1206
  *
1357
1207
  * Sync disposal starts shutdown without waiting. Async disposal waits for child
1358
1208
  * Tasks and registered cleanup to finish.
1359
1209
  *
1360
- * Use {@link createRun} at composition roots such as app, server, worker, or
1361
- * test entry points. The common factory is platform-agnostic; platform adapters
1362
- * can wrap it to add global error handling or shutdown integration.
1363
- *
1364
- * ### Example
1365
- *
1366
- * ```ts
1367
- * import { createRun, ok, type Task } from "@evolu/common";
1368
- *
1369
- * await using run = createRun();
1370
- * const loadData: Task<string> = () => ok("data");
1371
- *
1372
- * expectOk(await run(loadData), "data");
1373
- * ```
1374
- *
1375
- * ### Example with custom dependencies
1376
- *
1377
- * ```ts
1378
- * import { createRun, type DisposableRun } from "@evolu/common";
1379
- *
1380
- * interface ConfigDep {
1381
- * readonly config: { readonly apiUrl: string };
1382
- * }
1383
- *
1384
- * await using run = createRun<ConfigDep>({
1385
- * config: { apiUrl: "https://api.example.com" },
1386
- * });
1387
- *
1388
- * expectTypeOf(run).toEqualTypeOf<DisposableRun<ConfigDep>>();
1389
- * expect(run.deps.config.apiUrl).toBe("https://api.example.com");
1390
- * ```
1391
- *
1392
1210
  * @group Core
1393
1211
  */
1394
1212
  export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDisposable {
1395
1213
  /**
1396
1214
  * Registers closure-held cleanup owned by this Run.
1397
1215
  *
1398
- * Finalizers run in LIFO order after child Tasks settle and are awaited by
1399
- * async disposal. The Run is in `Aborted` state while they run and
1216
+ * Finalizers run in LIFO order after child {@link Task}s settle and are
1217
+ * awaited by async disposal. The Run is in `Aborted` state while they run and
1400
1218
  * transitions to `Settled` afterward, so a finalizer cannot start Tasks on
1401
1219
  * it. Use `using` for resources owned by a Task stack frame; use `defer` for
1402
1220
  * closure-held state whose lifetime is bounded by a reusable DisposableRun.
@@ -1407,6 +1225,23 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
1407
1225
  * with the same already-reported {@link AbortError}.
1408
1226
  *
1409
1227
  * Calling `defer` after disposal starts is a programmer error.
1228
+ *
1229
+ * ### Example
1230
+ *
1231
+ * ```ts
1232
+ * import { createRun } from "@evolu/common";
1233
+ *
1234
+ * let connectionClosed = false;
1235
+ * {
1236
+ * await using run = createRun();
1237
+ * run.defer(() => {
1238
+ * connectionClosed = true;
1239
+ * });
1240
+ *
1241
+ * expect(connectionClosed).toBe(false);
1242
+ * }
1243
+ * expect(connectionClosed).toBe(true);
1244
+ * ```
1410
1245
  */
1411
1246
  readonly defer: (finalizer: () => Awaitable<void>) => void;
1412
1247
  /**
@@ -1422,8 +1257,8 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
1422
1257
  * {@link ReportDefectDep}. The original defect is available as
1423
1258
  * `abortError.reason.defect` for diagnostics. The first panic records the
1424
1259
  * AbortError as the root Run's aborted exit and starts root disposal, which
1425
- * aborts running Tasks, prevents new Tasks from starting, and waits for
1426
- * running Tasks to settle. Later panics still report and return their own
1260
+ * aborts running {@link Task}s, prevents new Tasks from starting, and waits
1261
+ * for running Tasks to settle. Later panics still report and return their own
1427
1262
  * AbortError, but do not replace the root Run exit.
1428
1263
  */
1429
1264
  readonly panic: (defect: unknown) => AbortError;
@@ -1431,8 +1266,8 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
1431
1266
  /**
1432
1267
  * A Promise-backed handle to a {@link Task} started by a {@link Run}.
1433
1268
  *
1434
- * Await a Fiber to use the Task Result in the current control flow. The Fiber
1435
- * resolves with the Task {@link Result} when the Task returns normally. A Fiber
1269
+ * Await a Fiber to use the Task {@link Result} in the current control flow. The
1270
+ * Fiber resolves with the Task Result when the Task returns normally. A Fiber
1436
1271
  * returned by `run(task)` rejects with {@link AbortError} when the Task observes
1437
1272
  * abort or when a defect panics the Run tree. Panic uses
1438
1273
  * {@link PanicAbortReason}; the original defect is available on the reason for
@@ -1487,6 +1322,7 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
1487
1322
  * @group Core
1488
1323
  */
1489
1324
  export interface Fiber<T = unknown, E = unknown, D = unknown> extends Promise<Result<T, E>> {
1325
+ /** The child {@link Run} that executes this Fiber's {@link Task}. */
1490
1326
  readonly run: Run<D>;
1491
1327
  }
1492
1328
  /**
@@ -1496,13 +1332,13 @@ export interface Fiber<T = unknown, E = unknown, D = unknown> extends Promise<Re
1496
1332
  */
1497
1333
  export type AnyFiber = Fiber<any, any, any>;
1498
1334
  /**
1499
- * Extracts the Ok value type from a {@link Fiber}.
1335
+ * Extracts the {@link Ok} value type from a {@link Fiber}.
1500
1336
  *
1501
1337
  * @group Type utilities
1502
1338
  */
1503
1339
  export type InferFiberOk<TFiber extends AnyFiber> = TFiber extends Fiber<infer T, any, any> ? T : never;
1504
1340
  /**
1505
- * Extracts the Result error type from a {@link Fiber}.
1341
+ * Extracts the {@link Result} error type from a {@link Fiber}.
1506
1342
  *
1507
1343
  * @group Type utilities
1508
1344
  */
@@ -1517,8 +1353,8 @@ export type InferFiberDeps<TFiber extends AnyFiber> = TFiber extends Fiber<any,
1517
1353
  * A {@link Fiber} with explicit abort and async-disposal controls.
1518
1354
  *
1519
1355
  * Calling `.abort()` requests abort for the Fiber's child {@link Run}. If the
1520
- * Task observes abort or a defect panics the Run tree, the Fiber resolves with
1521
- * an {@link Err} containing the {@link AbortError}. Panic uses
1356
+ * {@link Task} observes abort or a defect panics the Run tree, the Fiber
1357
+ * resolves with an {@link Err} containing the {@link AbortError}. Panic uses
1522
1358
  * {@link PanicAbortReason}; the original defect is available on the reason for
1523
1359
  * diagnostics.
1524
1360
  *
@@ -1556,6 +1392,7 @@ export type InferFiberDeps<TFiber extends AnyFiber> = TFiber extends Fiber<any,
1556
1392
  * @group Core
1557
1393
  */
1558
1394
  export interface AbortableFiber<T = unknown, E = unknown, D = unknown> extends Fiber<T, E | AbortError, D>, AsyncDisposable {
1395
+ /** Requests abort with an optional {@link AbortReason}. */
1559
1396
  readonly abort: (reason?: AbortReason) => void;
1560
1397
  }
1561
1398
  /**
@@ -1566,15 +1403,15 @@ export interface AbortableFiber<T = unknown, E = unknown, D = unknown> extends F
1566
1403
  * scopes. Abort requests are still recorded, but the Run's observed abort
1567
1404
  * signal is aborted only when the mask is `0`.
1568
1405
  *
1569
- * Plain child Tasks inherit their parent's mask. `unabortable` increments the
1570
- * mask for the wrapped Task, and `unabortableMask` provides `restore` to run
1571
- * selected child Tasks with the previous mask.
1406
+ * Plain child {@link Task}s inherit their parent's mask. `unabortable`
1407
+ * increments the mask for the wrapped Task, and `unabortableMask` provides
1408
+ * `restore` to run selected child Tasks with the previous mask.
1572
1409
  *
1573
1410
  * @group Abortability
1574
1411
  */
1575
1412
  export type AbortMask = NonNegativeInt & Brand<"AbortMask">;
1576
1413
  /**
1577
- * Typed object explaining why a {@link Run} was aborted.
1414
+ * Runtime Type for structured data explaining why a {@link Run} was aborted.
1578
1415
  *
1579
1416
  * A reason has a `type` discriminant and optional structured data, so abort
1580
1417
  * causes can carry typed domain data. Well-known reasons are
@@ -1586,14 +1423,19 @@ export type AbortMask = NonNegativeInt & Brand<"AbortMask">;
1586
1423
  export declare const AbortReason: ObjectType<{
1587
1424
  readonly type: typeof String;
1588
1425
  }, RecordType<typeof String, typeof Unknown>>;
1426
+ /**
1427
+ * Structured data explaining why a {@link Run} was aborted.
1428
+ *
1429
+ * @group Core
1430
+ */
1589
1431
  export interface AbortReason extends InferType<typeof AbortReason> {
1590
1432
  }
1591
1433
  /**
1592
- * Typed object representing structured-concurrency abort control flow.
1434
+ * Runtime Type for structured-concurrency abort control flow.
1593
1435
  *
1594
- * AbortError is thrown to stop Task execution when a Run observes an abort
1595
- * request. AbortableFiber catches AbortError and returns it as a {@link Result}
1596
- * error, so abort can be handled as an ordinary Task outcome.
1436
+ * AbortError is thrown to stop {@link Task} execution when a {@link Run} observes
1437
+ * an abort request. {@link AbortableFiber} catches AbortError and returns it as
1438
+ * a {@link Result} error, so abort can be handled as an ordinary Task outcome.
1597
1439
  *
1598
1440
  * The reason explains why the Run was aborted. It can be an explicit abort
1599
1441
  * reason, {@link runDisposedAbortReason} for normal Run cleanup, or
@@ -1615,6 +1457,11 @@ export interface AbortReason extends InferType<typeof AbortReason> {
1615
1457
  export declare const AbortError: TypedType<"AbortError", {
1616
1458
  readonly reason: typeof AbortReason;
1617
1459
  }>;
1460
+ /**
1461
+ * Structured-concurrency abort control-flow value.
1462
+ *
1463
+ * @group Core
1464
+ */
1618
1465
  export interface AbortError extends InferType<typeof AbortError> {
1619
1466
  }
1620
1467
  /**
@@ -1626,9 +1473,9 @@ export declare const createAbortError: (reason: AbortReason) => AbortError;
1626
1473
  /**
1627
1474
  * Final outcome recorded by a {@link Run}.
1628
1475
  *
1629
- * A Run exit is an outer {@link Result}. {@link Ok} means the Task returned a
1630
- * Result; {@link Err} means the Run aborted with {@link AbortError}. Panic is
1631
- * recorded as an AbortError whose reason is {@link PanicAbortReason}.
1476
+ * A Run exit is an outer {@link Result}. {@link Ok} means the {@link Task}
1477
+ * returned a Result; {@link Err} means the Run aborted with {@link AbortError}.
1478
+ * Panic is recorded as an AbortError whose reason is {@link PanicAbortReason}.
1632
1479
  *
1633
1480
  * @group Core
1634
1481
  */
@@ -1688,7 +1535,7 @@ export interface RunStateSettled extends Typed<"Settled">, RunAbortState {
1688
1535
  * same object reference. This lets UI and debugging tools compare snapshots by
1689
1536
  * identity and skip unchanged branches.
1690
1537
  *
1691
- * @group Core
1538
+ * @group Monitoring
1692
1539
  * @see {@link Run.snapshot}
1693
1540
  */
1694
1541
  export interface RunSnapshot {
@@ -1708,7 +1555,7 @@ export interface RunSnapshot {
1708
1555
  */
1709
1556
  export type RunEventData = RunEventDataChildAdded | RunEventDataChildRemoved | RunEventDataStateChanged;
1710
1557
  /**
1711
- * A child Run was added to the emitting Run.
1558
+ * A child {@link Run} was added to the emitting Run.
1712
1559
  *
1713
1560
  * @group Monitoring
1714
1561
  */
@@ -1717,7 +1564,7 @@ export interface RunEventDataChildAdded extends Typed<"ChildAdded"> {
1717
1564
  readonly childId: Id;
1718
1565
  }
1719
1566
  /**
1720
- * A child Run was removed from the emitting Run.
1567
+ * A child {@link Run} was removed from the emitting Run.
1721
1568
  *
1722
1569
  * @group Monitoring
1723
1570
  */
@@ -1753,9 +1600,9 @@ export interface RunEvent {
1753
1600
  /**
1754
1601
  * Shared abort reason used for ordinary {@link Run} cleanup.
1755
1602
  *
1756
- * Disposal requests abort so child Tasks stop while the Run waits for them to
1757
- * settle. This reason distinguishes that cleanup path from explicit abort and
1758
- * {@link PanicAbortReason}.
1603
+ * Disposal requests abort so child {@link Task}s stop while the Run waits for
1604
+ * them to settle. This reason distinguishes that cleanup path from explicit
1605
+ * abort and {@link PanicAbortReason}.
1759
1606
  *
1760
1607
  * @group Run
1761
1608
  */
@@ -1774,7 +1621,7 @@ export declare const explicitAbortReason: {
1774
1621
  /**
1775
1622
  * Shared abort reason for tests that need a non-production abort reason.
1776
1623
  *
1777
- * @group Run
1624
+ * @group Testing
1778
1625
  */
1779
1626
  export declare const testAbortReason: {
1780
1627
  readonly type: "TestAbortReason";
@@ -1782,25 +1629,24 @@ export declare const testAbortReason: {
1782
1629
  /**
1783
1630
  * Shared {@link AbortError} for tests, created from {@link testAbortReason}.
1784
1631
  *
1785
- * @group Run
1632
+ * @group Testing
1786
1633
  */
1787
1634
  export declare const testAbortError: AbortError;
1788
1635
  /**
1789
- * A root-level abort caused by a defect.
1636
+ * Abort reason recorded when a defect panics the root {@link Run}.
1790
1637
  *
1791
- * Panic is a root-level abort caused by a defect: a thrown exception or
1792
- * rejected Promise. Recoverable domain errors belong in Result. Unexpected or
1793
- * unrecoverable conditions, such as storage engine errors the Task cannot
1794
- * usefully handle, may throw or reject. {@link AbortError} is abort control
1795
- * flow, not a defect.
1638
+ * A defect is a thrown or rejected value other than {@link AbortError}.
1639
+ * Recoverable domain errors belong in {@link Result}. Bugs and unrecoverable
1640
+ * failures, such as storage engine errors the {@link Task} cannot usefully
1641
+ * handle, may throw or reject.
1796
1642
  *
1797
1643
  * {@link Run.onEvent} handler defects are different: event handlers are
1798
1644
  * monitoring code, so their defects are reported globally but do not panic the
1799
1645
  * root Run.
1800
1646
  *
1801
- * When Run observes a defect, it aborts the root Run and starts disposal
1647
+ * When {@link Run} observes a defect, it aborts the root Run and starts disposal
1802
1648
  * immediately. This prevents later Tasks from starting after the defect. A
1803
- * Fiber rejects with AbortError whose reason is PanicAbortReason; an
1649
+ * {@link Fiber} rejects with AbortError whose reason is PanicAbortReason; an
1804
1650
  * {@link AbortableFiber} returns that AbortError as an {@link Err}.
1805
1651
  *
1806
1652
  * @group Core
@@ -1841,10 +1687,10 @@ export interface RunConfigDep {
1841
1687
  /**
1842
1688
  * Reports a defect.
1843
1689
  *
1844
- * Run uses this dependency in two cases: {@link DisposableRun.panic} reports the
1845
- * {@link AbortError} whose reason is {@link PanicAbortReason}, and event
1846
- * monitoring reports observer defects without panicking the Run. The original
1847
- * panic defect is available at `abortError.reason.defect`.
1690
+ * {@link Run} uses this dependency in two cases: {@link DisposableRun.panic}
1691
+ * reports the {@link AbortError} whose reason is {@link PanicAbortReason}, and
1692
+ * event monitoring reports observer defects without panicking the Run. The
1693
+ * original panic defect is available at `abortError.reason.defect`.
1848
1694
  *
1849
1695
  * @group Run
1850
1696
  */
@@ -1873,9 +1719,9 @@ export declare const reportDefectAfterMicrotask: ReportDefect;
1873
1719
  /**
1874
1720
  * Default dependencies provided by {@link createRun}.
1875
1721
  *
1876
- * Root Runs include platform-independent implementations for console, leak
1877
- * detection, native fetch, randomness, error reporting, time, and optional Run
1878
- * monitoring configuration.
1722
+ * Root {@link Run}s include platform-independent implementations for console,
1723
+ * leak detection, native fetch, randomness, error reporting, time, and optional
1724
+ * Run monitoring configuration.
1879
1725
  *
1880
1726
  * The {@link LeakDetector} is enabled only in development builds; production
1881
1727
  * uses a no-op implementation.
@@ -1895,14 +1741,39 @@ export declare const createRunDefaultDeps: () => RunDefaultDeps;
1895
1741
  * @group Run
1896
1742
  */
1897
1743
  export interface CreateRun {
1898
- /** Creates a root Run with only {@link RunDefaultDeps}. */
1744
+ /** Creates a root {@link Run} with only {@link RunDefaultDeps}. */
1899
1745
  (): DisposableRun;
1900
- /** Creates a root Run with custom deps merged over {@link RunDefaultDeps}. */
1746
+ /**
1747
+ * Creates a root {@link Run} with custom deps merged over
1748
+ * {@link RunDefaultDeps}.
1749
+ */
1901
1750
  <D extends object>(deps: RunCustomDeps<D>): DisposableRun<D>;
1902
1751
  }
1903
1752
  /**
1904
1753
  * Creates a root {@link DisposableRun}.
1905
1754
  *
1755
+ * Use at composition roots such as app, server, worker, or test entry points.
1756
+ * The common factory is platform-agnostic; platform adapters can wrap it to add
1757
+ * global error handling or shutdown integration.
1758
+ *
1759
+ * ### Example
1760
+ *
1761
+ * ```ts
1762
+ * import { createRun, ok, type Task } from "@evolu/common";
1763
+ *
1764
+ * interface ConfigDep {
1765
+ * readonly config: { readonly apiUrl: string };
1766
+ * }
1767
+ *
1768
+ * const loadApiUrl: Task<string, never, ConfigDep> = (run) =>
1769
+ * ok(run.deps.config.apiUrl);
1770
+ *
1771
+ * await using run = createRun({
1772
+ * config: { apiUrl: "https://api.example.com" },
1773
+ * });
1774
+ * expectOk(await run(loadApiUrl), "https://api.example.com");
1775
+ * ```
1776
+ *
1906
1777
  * @group Run
1907
1778
  */
1908
1779
  export declare const createRun: CreateRun;
@@ -1926,7 +1797,11 @@ export declare const createRun: CreateRun;
1926
1797
  * @group Testing
1927
1798
  */
1928
1799
  export type TestRunDefaultDeps = Omit<RunDefaultDeps, "console" | "leakDetector" | "reportDefect" | "time"> & TestConsoleDep & TestLeakDetectorDep & TestReportDefectDep & TestTimeDep & RandomLibDep;
1929
- /** Provides a test {@link Run} with deterministic default dependencies. */
1800
+ /**
1801
+ * Provides a test {@link Run} with deterministic default dependencies.
1802
+ *
1803
+ * @group Testing
1804
+ */
1930
1805
  export interface TestRunDep<D = unknown> {
1931
1806
  readonly run: Run<TestRunDefaultDeps & D>;
1932
1807
  }
@@ -1974,9 +1849,39 @@ export declare const testCreateDeps: (options?: {
1974
1849
  /**
1975
1850
  * Creates a root {@link DisposableRun} with {@link TestRunDefaultDeps}.
1976
1851
  *
1852
+ * ### Example
1853
+ *
1854
+ * ```ts
1855
+ * import { ok, testCreateRun, type Task } from "@evolu/common";
1856
+ *
1857
+ * const readTime: Task<number> = (run) => ok(run.deps.time.now());
1858
+ *
1859
+ * await using run = testCreateRun();
1860
+ * expectOk(await run(readTime), 0);
1861
+ * ```
1862
+ *
1977
1863
  * @group Testing
1978
1864
  */
1979
1865
  export declare function testCreateRun(deps?: TestRunDefaultDeps): DisposableRun<TestRunDefaultDeps>;
1866
+ /**
1867
+ * Merges custom dependencies into {@link TestRunDefaultDeps}.
1868
+ *
1869
+ * ### Example
1870
+ *
1871
+ * ```ts
1872
+ * import { ok, testCreateRun, type Task } from "@evolu/common";
1873
+ *
1874
+ * interface FeatureDep {
1875
+ * readonly feature: { readonly enabled: boolean };
1876
+ * }
1877
+ *
1878
+ * const isFeatureEnabled: Task<boolean, never, FeatureDep> = (run) =>
1879
+ * ok(run.deps.feature.enabled);
1880
+ *
1881
+ * await using run = testCreateRun({ feature: { enabled: true } });
1882
+ * expectOk(await run(isFeatureEnabled), true);
1883
+ * ```
1884
+ */
1980
1885
  export declare function testCreateRun<D extends object>(deps: RunCustomDeps<D>): DisposableRun<TestRunDefaultDeps & D>;
1981
1886
  /**
1982
1887
  * A readonly record whose values are {@link Task}s.
@@ -1985,24 +1890,26 @@ export declare function testCreateRun<D extends object>(deps: RunCustomDeps<D>):
1985
1890
  */
1986
1891
  export type TaskRecord = Readonly<Record<string, AnyTask>>;
1987
1892
  /**
1988
- * Extracts the dependency intersection required by a readonly Task array.
1893
+ * Extracts the dependency intersection required by a readonly {@link Task}
1894
+ * array.
1989
1895
  *
1990
1896
  * @group Type utilities
1991
1897
  */
1992
1898
  export type InferTasksDeps<TTasks extends ReadonlyArray<AnyTask>> = ParameterIntersection<TTasks[number] extends infer TTask ? TTask extends AnyTask ? (deps: InferTaskDeps<TTask>) => void : never : never>;
1993
1899
  /**
1994
- * Extracts the dependency intersection required by a Task record.
1900
+ * Extracts the dependency intersection required by a {@link Task} record.
1995
1901
  *
1996
1902
  * @group Type utilities
1997
1903
  */
1998
1904
  export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<ReadonlyArray<TTasks[keyof TTasks]>>;
1999
1905
  /**
2000
- * Options shared by Task collection helpers.
1906
+ * Options shared by {@link Task} collection helpers.
2001
1907
  *
2002
- * `concurrency` controls how many Tasks run at once. It defaults to `1`. A
2003
- * platform `availableParallelism()` result is often a good limit for CPU-bound
2004
- * Tasks. For network or database Tasks, choose a limit based on the transport,
2005
- * server, connection pool, and rate limits.
1908
+ * `concurrency` controls how many Tasks run at once. It defaults to `1`. For
1909
+ * CPU-bound Tasks backed by workers or parallel native operations, a platform
1910
+ * `availableParallelism()` result is often a good limit. For network or
1911
+ * database Tasks, choose a limit based on the transport, server, connection
1912
+ * pool, and rate limits.
2006
1913
  *
2007
1914
  * Keep concurrency bounded. In rare cases where running every Task concurrently
2008
1915
  * is safe, use {@link maxPositiveInt}.
@@ -2010,6 +1917,7 @@ export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<Read
2010
1917
  * @group Collection
2011
1918
  */
2012
1919
  export interface TaskCollectionOptions {
1920
+ /** Maximum number of {@link Task}s run concurrently. Defaults to `1`. */
2013
1921
  readonly concurrency?: Int1To100OrPositiveInt;
2014
1922
  }
2015
1923
  /**
@@ -2018,11 +1926,12 @@ export interface TaskCollectionOptions {
2018
1926
  * @group Collection
2019
1927
  */
2020
1928
  export interface AllOptions extends TaskCollectionOptions {
2021
- /** Disables collecting Ok values. */
1929
+ /** Disables collecting {@link Ok} values. */
2022
1930
  readonly collect: false;
2023
1931
  }
2024
1932
  /**
2025
- * Maps a Task array or record to the Ok values produced by its Tasks.
1933
+ * Maps a {@link Task} array or record to the {@link Ok} values produced by its
1934
+ * Tasks.
2026
1935
  *
2027
1936
  * The mapped type is homomorphic, so tuples preserve their shape and records
2028
1937
  * preserve their keys.
@@ -2033,7 +1942,7 @@ export type InferTasksOk<TTasks> = {
2033
1942
  readonly [K in keyof TTasks]: TTasks[K] extends AnyTask ? InferTaskOk<TTasks[K]> : never;
2034
1943
  };
2035
1944
  /**
2036
- * Runs Tasks until all return {@link Ok} or one returns {@link Err}.
1945
+ * Runs {@link Task}s until all return {@link Ok} or one returns {@link Err}.
2037
1946
  *
2038
1947
  * Returns Ok with all values when every Task returns Ok. Stops on the first
2039
1948
  * Err; remaining running Tasks are aborted. Sequential by default; pass a
@@ -2051,8 +1960,8 @@ export type InferTasksOk<TTasks> = {
2051
1960
  *
2052
1961
  * Similar to
2053
1962
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all | Promise.all},
2054
- * but runs Tasks, returns Result values, and aborts remaining Tasks on the
2055
- * first Err.
1963
+ * but runs Tasks, returns {@link Result} values, and aborts remaining Tasks on
1964
+ * the first Err.
2056
1965
  *
2057
1966
  * ### Example
2058
1967
  *
@@ -2064,48 +1973,35 @@ export type InferTasksOk<TTasks> = {
2064
1973
  * ok,
2065
1974
  * type Result,
2066
1975
  * type Task,
1976
+ * type Typed,
2067
1977
  * } from "@evolu/common";
2068
1978
  *
2069
- * interface User {
2070
- * readonly id: string;
2071
- * }
2072
- * interface Post {
2073
- * readonly id: string;
2074
- * }
2075
- * const fetchUser: Task<User> = () => ok({ id: "user-1" });
2076
- * const fetchPosts: Task<ReadonlyArray<Post>> = () =>
2077
- * ok([{ id: "post-1" }]);
2078
- * await using run = createRun();
2079
- *
2080
- * const dashboard = await run(all([fetchUser, fetchPosts]));
2081
- * expectTypeOf(dashboard).toEqualTypeOf<
2082
- * Result<readonly [User, ReadonlyArray<Post>]>
2083
- * >();
2084
- * expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
2085
- *
2086
- * // Skip collecting Ok values when they aren't needed.
2087
- * interface SaveUserError {
2088
- * readonly type: "SaveUserError";
2089
- * readonly userId: string;
2090
- * }
2091
1979
  * const savedUserIds: Array<string> = [];
2092
1980
  * const saveUser =
2093
- * (id: string): Task<number, SaveUserError> =>
1981
+ * (id: string): Task<number, SaveUserFailedError> =>
2094
1982
  * () => {
2095
1983
  * if (id === "missing") {
2096
- * return err({ type: "SaveUserError", userId: id });
1984
+ * return err({ type: "SaveUserFailed", userId: id });
2097
1985
  * }
2098
1986
  * savedUserIds.push(id);
2099
1987
  * return ok(1);
2100
1988
  * };
1989
+ *
1990
+ * interface SaveUserFailedError extends Typed<"SaveUserFailed"> {
1991
+ * readonly userId: string;
1992
+ * }
1993
+ *
1994
+ * await using run = createRun();
2101
1995
  * const saveResult = await run(
2102
- * all(["user-1", "missing", "user-3"], saveUser, {
1996
+ * all([saveUser("user-1"), saveUser("missing"), saveUser("user-3")], {
2103
1997
  * collect: false,
2104
1998
  * }),
2105
1999
  * );
2106
- * expectTypeOf(saveResult).toEqualTypeOf<Result<void, SaveUserError>>();
2000
+ * expectTypeOf(saveResult).toEqualTypeOf<
2001
+ * Result<void, SaveUserFailedError>
2002
+ * >();
2107
2003
  * expectErr(saveResult, {
2108
- * type: "SaveUserError",
2004
+ * type: "SaveUserFailed",
2109
2005
  * userId: "missing",
2110
2006
  * });
2111
2007
  * expect(savedUserIds).toEqual(["user-1"]);
@@ -2114,11 +2010,45 @@ export type InferTasksOk<TTasks> = {
2114
2010
  * @group Collection
2115
2011
  */
2116
2012
  export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks: TTasks, options: AllOptions): Task<void, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
2117
- /** Runs a Task record without collecting its Ok values. */
2013
+ /** Runs a {@link Task} record without collecting its {@link Ok} values. */
2118
2014
  export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, options: AllOptions): Task<void, InferTaskErr<TTasks[keyof TTasks]>, InferTaskRecordDeps<TTasks>>;
2015
+ /**
2016
+ * Runs a {@link Task} array and preserves its shape.
2017
+ *
2018
+ * ### Example
2019
+ *
2020
+ * ```ts
2021
+ * import {
2022
+ * all,
2023
+ * createRun,
2024
+ * ok,
2025
+ * type Result,
2026
+ * type Task,
2027
+ * } from "@evolu/common";
2028
+ *
2029
+ * interface User {
2030
+ * readonly id: string;
2031
+ * }
2032
+ *
2033
+ * interface Post {
2034
+ * readonly id: string;
2035
+ * }
2036
+ *
2037
+ * const fetchUser: Task<User> = () => ok({ id: "user-1" });
2038
+ * const fetchPosts: Task<ReadonlyArray<Post>> = () =>
2039
+ * ok([{ id: "post-1" }]);
2040
+ *
2041
+ * await using run = createRun();
2042
+ * const dashboard = await run(all([fetchUser, fetchPosts]));
2043
+ * expectTypeOf(dashboard).toEqualTypeOf<
2044
+ * Result<readonly [User, ReadonlyArray<Post>]>
2045
+ * >();
2046
+ * expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
2047
+ * ```
2048
+ */
2119
2049
  export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksOk<TTasks>, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
2120
2050
  /**
2121
- * Runs a Task record and preserves its keys.
2051
+ * Runs a {@link Task} record and preserves its keys.
2122
2052
  *
2123
2053
  * ### Example
2124
2054
  *
@@ -2134,6 +2064,7 @@ export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks:
2134
2064
  * interface User {
2135
2065
  * readonly id: string;
2136
2066
  * }
2067
+ *
2137
2068
  * interface Post {
2138
2069
  * readonly id: string;
2139
2070
  * }
@@ -2155,8 +2086,13 @@ export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks:
2155
2086
  * ```
2156
2087
  */
2157
2088
  export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksOk<TTasks>, InferTaskErr<TTasks[keyof TTasks]>, InferTaskRecordDeps<TTasks>>;
2089
+ /** Maps an array to {@link Task}s without collecting their {@link Ok} values. */
2090
+ export declare function all<const TValues extends ReadonlyArray<unknown>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[number], index: number) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
2091
+ /** Maps record values to {@link Task}s without collecting their {@link Ok} values. */
2092
+ export declare function all<const TValues extends Readonly<Record<string, unknown>>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
2158
2093
  /**
2159
- * Maps an array to Tasks and preserves its shape.
2094
+ * Maps an array to {@link Task}s and collects their {@link Ok} values in the same
2095
+ * shape.
2160
2096
  *
2161
2097
  * ### Example
2162
2098
  *
@@ -2172,6 +2108,7 @@ export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, opti
2172
2108
  * interface User {
2173
2109
  * readonly id: string;
2174
2110
  * }
2111
+ *
2175
2112
  * const loadUser =
2176
2113
  * (id: string): Task<User> =>
2177
2114
  * () =>
@@ -2193,14 +2130,11 @@ export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, opti
2193
2130
  * expectOk(result, [{ id: "user-1" }, { id: "user-2" }]);
2194
2131
  * ```
2195
2132
  */
2196
- export declare function all<const TValues extends ReadonlyArray<unknown>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[number], index: number) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
2197
- /** Maps record values to Tasks without collecting their Ok values. */
2198
- export declare function all<const TValues extends Readonly<Record<string, unknown>>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
2199
2133
  export declare function all<const TValues extends ReadonlyArray<unknown>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[number], index: number) => TTask, options?: TaskCollectionOptions): Task<{
2200
2134
  readonly [K in keyof TValues]: InferTaskOk<TTask>;
2201
2135
  }, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
2202
2136
  /**
2203
- * Maps record values to Tasks and preserves its keys.
2137
+ * Maps record values to {@link Task}s and preserves the record's keys.
2204
2138
  *
2205
2139
  * ### Example
2206
2140
  *
@@ -2216,6 +2150,7 @@ export declare function all<const TValues extends ReadonlyArray<unknown>, TTask
2216
2150
  * interface User {
2217
2151
  * readonly id: string;
2218
2152
  * }
2153
+ *
2219
2154
  * const loadUser =
2220
2155
  * (id: string): Task<User> =>
2221
2156
  * () =>
@@ -2249,7 +2184,8 @@ export declare function all<const TValues extends Readonly<Record<string, unknow
2249
2184
  readonly [K in keyof TValues]: InferTaskOk<TTask>;
2250
2185
  }, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
2251
2186
  /**
2252
- * Maps a Task array or record to the Result values produced by its Tasks.
2187
+ * Maps a {@link Task} array or record to the {@link Result} values produced by
2188
+ * its Tasks.
2253
2189
  *
2254
2190
  * The mapped type is homomorphic, so tuples preserve their shape and records
2255
2191
  * preserve their keys.
@@ -2260,7 +2196,7 @@ export type InferTasksSettled<TTasks> = {
2260
2196
  readonly [K in keyof TTasks]: TTasks[K] extends AnyTask ? Result<InferTaskOk<TTasks[K]>, InferTaskErr<TTasks[K]>> : never;
2261
2197
  };
2262
2198
  /**
2263
- * Runs all Tasks and returns every Task {@link Result}.
2199
+ * Runs all {@link Task}s and returns every Task {@link Result}.
2264
2200
  *
2265
2201
  * Unlike {@link all}, {@link Err} Results do not stop later Tasks.
2266
2202
  *
@@ -2287,14 +2223,14 @@ export type InferTasksSettled<TTasks> = {
2287
2223
  * ok,
2288
2224
  * type Result,
2289
2225
  * type Task,
2226
+ * type Typed,
2290
2227
  * } from "@evolu/common";
2291
2228
  *
2292
- * interface LoadError {
2293
- * readonly type: "LoadError";
2294
- * }
2229
+ * const loadProfile: Task<string, ProfileNotFoundError> = () =>
2230
+ * err({ type: "ProfileNotFound" });
2231
+ *
2232
+ * interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
2295
2233
  *
2296
- * const loadProfile: Task<string, LoadError> = () =>
2297
- * err({ type: "LoadError" });
2298
2234
  * let activityLoaded = false;
2299
2235
  * const loadActivity: Task<ReadonlyArray<string>> = () => {
2300
2236
  * activityLoaded = true;
@@ -2305,11 +2241,14 @@ export type InferTasksSettled<TTasks> = {
2305
2241
  * const results = await run(allSettled([loadProfile, loadActivity]));
2306
2242
  * expectTypeOf(results).toEqualTypeOf<
2307
2243
  * Result<
2308
- * readonly [Result<string, LoadError>, Result<ReadonlyArray<string>>]
2244
+ * readonly [
2245
+ * Result<string, ProfileNotFoundError>,
2246
+ * Result<ReadonlyArray<string>>,
2247
+ * ]
2309
2248
  * >
2310
2249
  * >();
2311
2250
  * expectOk(results, [
2312
- * { ok: false, error: { type: "LoadError" } },
2251
+ * { ok: false, error: { type: "ProfileNotFound" } },
2313
2252
  * { ok: true, value: ["signed-in"] },
2314
2253
  * ]);
2315
2254
  * // Unlike all, a later Task still runs after an Err.
@@ -2320,7 +2259,7 @@ export type InferTasksSettled<TTasks> = {
2320
2259
  */
2321
2260
  export declare function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksSettled<TTasks>, never, InferTasksDeps<TTasks>>;
2322
2261
  /**
2323
- * Runs a Task record and preserves its keys.
2262
+ * Runs a {@link Task} record and preserves its keys.
2324
2263
  *
2325
2264
  * ### Example
2326
2265
  *
@@ -2332,17 +2271,18 @@ export declare function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
2332
2271
  * ok,
2333
2272
  * type Result,
2334
2273
  * type Task,
2274
+ * type Typed,
2335
2275
  * } from "@evolu/common";
2336
2276
  *
2337
2277
  * interface User {
2338
2278
  * readonly id: string;
2339
2279
  * }
2340
- * interface LoadError {
2341
- * readonly type: "LoadError";
2342
- * }
2280
+ *
2343
2281
  * const fetchUser: Task<User> = () => ok({ id: "user-1" });
2344
- * const fetchProfile: Task<string, LoadError> = () =>
2345
- * err({ type: "LoadError" });
2282
+ * const fetchProfile: Task<string, ProfileNotFoundError> = () =>
2283
+ * err({ type: "ProfileNotFound" });
2284
+ *
2285
+ * interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
2346
2286
  *
2347
2287
  * await using run = createRun();
2348
2288
  * const results = await run(
@@ -2352,18 +2292,18 @@ export declare function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
2352
2292
  * expectTypeOf(results).toEqualTypeOf<
2353
2293
  * Result<{
2354
2294
  * readonly user: Result<User>;
2355
- * readonly profile: Result<string, LoadError>;
2295
+ * readonly profile: Result<string, ProfileNotFoundError>;
2356
2296
  * }>
2357
2297
  * >();
2358
2298
  * expectOk(results, {
2359
2299
  * user: { ok: true, value: { id: "user-1" } },
2360
- * profile: { ok: false, error: { type: "LoadError" } },
2300
+ * profile: { ok: false, error: { type: "ProfileNotFound" } },
2361
2301
  * });
2362
2302
  * ```
2363
2303
  */
2364
2304
  export declare function allSettled<const TTasks extends TaskRecord>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksSettled<TTasks>, never, InferTaskRecordDeps<TTasks>>;
2365
2305
  /**
2366
- * Maps an array to Tasks and preserves its shape.
2306
+ * Maps an array to {@link Task}s and preserves its shape.
2367
2307
  *
2368
2308
  * ### Example
2369
2309
  *
@@ -2375,18 +2315,19 @@ export declare function allSettled<const TTasks extends TaskRecord>(tasks: TTask
2375
2315
  * ok,
2376
2316
  * type Result,
2377
2317
  * type Task,
2318
+ * type Typed,
2378
2319
  * } from "@evolu/common";
2379
2320
  *
2380
2321
  * interface User {
2381
2322
  * readonly id: string;
2382
2323
  * }
2383
- * interface LoadError {
2384
- * readonly type: "LoadError";
2385
- * }
2324
+ *
2386
2325
  * const loadUser =
2387
- * (id: string): Task<User, LoadError> =>
2326
+ * (id: string): Task<User, UserNotFoundError> =>
2388
2327
  * () =>
2389
- * id === "missing" ? err({ type: "LoadError" }) : ok({ id });
2328
+ * id === "missing" ? err({ type: "UserNotFound" }) : ok({ id });
2329
+ *
2330
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {}
2390
2331
  *
2391
2332
  * const userIds = ["user-1", "missing"] as const;
2392
2333
  * const indexes: Array<number> = [];
@@ -2401,11 +2342,16 @@ export declare function allSettled<const TTasks extends TaskRecord>(tasks: TTask
2401
2342
  * await using run = createRun();
2402
2343
  * const results = await run(loadUsers);
2403
2344
  * expectTypeOf(results).toEqualTypeOf<
2404
- * Result<readonly [Result<User, LoadError>, Result<User, LoadError>]>
2345
+ * Result<
2346
+ * readonly [
2347
+ * Result<User, UserNotFoundError>,
2348
+ * Result<User, UserNotFoundError>,
2349
+ * ]
2350
+ * >
2405
2351
  * >();
2406
2352
  * expectOk(results, [
2407
2353
  * { ok: true, value: { id: "user-1" } },
2408
- * { ok: false, error: { type: "LoadError" } },
2354
+ * { ok: false, error: { type: "UserNotFound" } },
2409
2355
  * ]);
2410
2356
  * ```
2411
2357
  */
@@ -2413,7 +2359,7 @@ export declare function allSettled<const TValues extends ReadonlyArray<unknown>,
2413
2359
  readonly [K in keyof TValues]: Result<InferTaskOk<TTask>, InferTaskErr<TTask>>;
2414
2360
  }, never, InferTasksDeps<ReadonlyArray<TTask>>>;
2415
2361
  /**
2416
- * Maps record values to Tasks and preserves its keys.
2362
+ * Maps record values to {@link Task}s and preserves the record's keys.
2417
2363
  *
2418
2364
  * ### Example
2419
2365
  *
@@ -2425,18 +2371,19 @@ export declare function allSettled<const TValues extends ReadonlyArray<unknown>,
2425
2371
  * ok,
2426
2372
  * type Result,
2427
2373
  * type Task,
2374
+ * type Typed,
2428
2375
  * } from "@evolu/common";
2429
2376
  *
2430
2377
  * interface User {
2431
2378
  * readonly id: string;
2432
2379
  * }
2433
- * interface LoadError {
2434
- * readonly type: "LoadError";
2435
- * }
2380
+ *
2436
2381
  * const loadUser =
2437
- * (id: string): Task<User, LoadError> =>
2382
+ * (id: string): Task<User, UserNotFoundError> =>
2438
2383
  * () =>
2439
- * id === "missing" ? err({ type: "LoadError" }) : ok({ id });
2384
+ * id === "missing" ? err({ type: "UserNotFound" }) : ok({ id });
2385
+ *
2386
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {}
2440
2387
  *
2441
2388
  * const userIdsByRole = { admin: "user-1", reviewer: "missing" } as const;
2442
2389
  * const roles: Array<keyof typeof userIdsByRole> = [];
@@ -2452,13 +2399,13 @@ export declare function allSettled<const TValues extends ReadonlyArray<unknown>,
2452
2399
  * const results = await run(loadUsersByRole);
2453
2400
  * expectTypeOf(results).toEqualTypeOf<
2454
2401
  * Result<{
2455
- * readonly admin: Result<User, LoadError>;
2456
- * readonly reviewer: Result<User, LoadError>;
2402
+ * readonly admin: Result<User, UserNotFoundError>;
2403
+ * readonly reviewer: Result<User, UserNotFoundError>;
2457
2404
  * }>
2458
2405
  * >();
2459
2406
  * expectOk(results, {
2460
2407
  * admin: { ok: true, value: { id: "user-1" } },
2461
- * reviewer: { ok: false, error: { type: "LoadError" } },
2408
+ * reviewer: { ok: false, error: { type: "UserNotFound" } },
2462
2409
  * });
2463
2410
  * ```
2464
2411
  */
@@ -2472,10 +2419,11 @@ export declare function allSettled<const TValues extends Readonly<Record<string,
2472
2419
  * callbacks. Resolve with `ok(value)` or `err(error)`, or reject with a defect,
2473
2420
  * to complete the Task. Settlement is one-shot: the first `resolve` or `reject`
2474
2421
  * wins, and later settlement calls are ignored, matching Promise semantics.
2475
- * When `reject` wins settlement, the defect panics the Run tree and is observed
2476
- * at Fiber boundaries as {@link AbortError} with {@link PanicAbortReason}.
2477
- * Rejecting AbortError is reserved for Task abort control flow: direct
2478
- * `run(task)` rejects with it, and `run.abortable(task)` returns it as an Err.
2422
+ * When `reject` wins settlement, the defect panics the {@link Run} tree and is
2423
+ * observed at {@link Fiber} boundaries as {@link AbortError} with
2424
+ * {@link PanicAbortReason}. Rejecting AbortError is reserved for Task abort
2425
+ * control flow: direct `run(task)` rejects with it, and `run.abortable(task)`
2426
+ * returns it as an {@link Err}.
2479
2427
  *
2480
2428
  * This helper is a callback bridge. If `reject` forwards an Error created in a
2481
2429
  * separate async chain, V8 cannot reconstruct the caller's zero-cost async
@@ -2490,10 +2438,10 @@ export declare function allSettled<const TValues extends Readonly<Record<string,
2490
2438
  * Optionally return a synchronous cleanup callback. It runs when the Task Run
2491
2439
  * signal aborts, including normal completion and explicit abort. The callback
2492
2440
  * must not throw. Cleanup defects panic the root Run; if the callback Task
2493
- * already settled, its Fiber keeps the original Result while the root still
2494
- * reports the panic. Cleanup must be synchronous; returned promises are not
2495
- * awaited. For async cleanup, prefer {@link acquireUseRelease}, `await using`
2496
- * with {@link AsyncDisposableStack}, or a Task that owns the resource
2441
+ * already settled, its Fiber keeps the original {@link Result} while the root
2442
+ * still reports the panic. Cleanup must be synchronous; returned promises are
2443
+ * not awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
2444
+ * using` with {@link AsyncDisposableStack}, or a Task that owns the resource
2497
2445
  * explicitly.
2498
2446
  *
2499
2447
  * If setup can throw after acquiring any resource, use a local
@@ -2537,25 +2485,20 @@ export declare const callback: <T, E = never, D = unknown>(fn: (options: {
2537
2485
  /**
2538
2486
  * Pauses execution for a specified {@link PositiveDuration}.
2539
2487
  *
2540
- * Aborting the Task clears the scheduled timeout.
2488
+ * Aborting the {@link Task} clears the scheduled timeout.
2541
2489
  *
2542
- * @group Timing
2543
- */
2544
- export declare const sleep: (duration: PositiveDuration) => Task<void>;
2545
- /**
2546
- * Typed error returned by {@link timeout} when a Task exceeds its duration.
2490
+ * ### Example
2547
2491
  *
2548
- * @group Timing
2549
- */
2550
- export declare const TimeoutError: TypedType<"TimeoutError">;
2551
- export interface TimeoutError extends InferType<typeof TimeoutError> {
2552
- }
2553
- /**
2554
- * The {@link TimeoutError} instance returned by {@link timeout}.
2492
+ * ```ts
2493
+ * import { createRun, sleep } from "@evolu/common";
2494
+ *
2495
+ * await using run = createRun();
2496
+ * expectOk(await run(sleep("1ms")), undefined);
2497
+ * ```
2555
2498
  *
2556
2499
  * @group Timing
2557
2500
  */
2558
- export declare const timeoutError: TimeoutError;
2501
+ export declare const sleep: (duration: PositiveDuration) => Task<void>;
2559
2502
  /**
2560
2503
  * Limits how long a {@link Task} may run.
2561
2504
  *
@@ -2587,6 +2530,26 @@ export declare const timeoutError: TimeoutError;
2587
2530
  * @group Timing
2588
2531
  */
2589
2532
  export declare const timeout: <T, E, D = unknown>(task: Task<T, E, D>, duration: PositiveDuration) => Task<T, E | TimeoutError, D>;
2533
+ /**
2534
+ * Runtime Type for the error returned by {@link timeout} when a {@link Task}
2535
+ * exceeds its duration.
2536
+ *
2537
+ * @group Timing
2538
+ */
2539
+ export declare const TimeoutError: TypedType<"TimeoutError">;
2540
+ /**
2541
+ * Error returned by {@link timeout} when a {@link Task} exceeds its duration.
2542
+ *
2543
+ * @group Timing
2544
+ */
2545
+ export interface TimeoutError extends InferType<typeof TimeoutError> {
2546
+ }
2547
+ /**
2548
+ * The {@link TimeoutError} instance returned by {@link timeout}.
2549
+ *
2550
+ * @group Timing
2551
+ */
2552
+ export declare const timeoutError: TimeoutError;
2590
2553
  /**
2591
2554
  * Options for {@link retry}.
2592
2555
  *
@@ -2599,7 +2562,7 @@ export interface RetryOptions<E, Output> {
2599
2562
  * Returning `false` stops retrying and returns {@link RetryError}. The
2600
2563
  * original error is stored as `lastError` instead of returned directly. The
2601
2564
  * predicate must not throw: a thrown exception is a defect that panics the
2602
- * Run tree.
2565
+ * {@link Run} tree.
2603
2566
  */
2604
2567
  readonly shouldRetry?: Predicate<E>;
2605
2568
  /**
@@ -2608,8 +2571,8 @@ export interface RetryOptions<E, Output> {
2608
2571
  * `attempt` is the failed attempt that caused this retry, not the upcoming
2609
2572
  * attempt number. The callback runs after {@link RetryOptions.shouldRetry} and
2610
2573
  * the {@link Schedule} approve a retry, and before the retry delay. The
2611
- * callback must not throw: a thrown exception is a defect that panics the Run
2612
- * tree.
2574
+ * callback must not throw: a thrown exception is a defect that panics the
2575
+ * {@link Run} tree.
2613
2576
  */
2614
2577
  readonly onRetry?: (attempt: RetryAttempt<E, Output>) => void;
2615
2578
  }
@@ -2623,40 +2586,23 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
2623
2586
  readonly error: E;
2624
2587
  }
2625
2588
  /**
2626
- * Error returned by {@link retry} when retrying stops after a domain error.
2627
- *
2628
- * @group Resilience
2629
- */
2630
- export interface RetryError<E> extends Typed<"RetryError"> {
2631
- /** The final domain error that stopped retrying. */
2632
- readonly lastError: E;
2633
- /** The number of attempts that were started. */
2634
- readonly attempts: PositiveInt;
2635
- }
2636
- /**
2637
- * Error type returned by {@link retry}.
2638
- *
2639
- * @group Resilience
2640
- */
2641
- export type RetryTaskError<E> = ([Exclude<E, AbortError>] extends [never] ? never : RetryError<Exclude<E, AbortError>>) | Extract<E, AbortError>;
2642
- /**
2643
- * Retries a Task according to a {@link Schedule}.
2589
+ * Retries a {@link Task} according to a {@link Schedule}.
2644
2590
  *
2645
2591
  * Use `retry` for failure recovery: it repeats after {@link Err} and wraps the
2646
2592
  * final domain error. Use {@link repeat} for success-driven loops: it repeats
2647
- * after {@link Ok} and returns the Task's natural Result.
2593
+ * after {@link Ok} and returns the Task's natural {@link Result}.
2648
2594
  *
2649
2595
  * {@link AbortError} passes through unchanged when returned as a Result error,
2650
- * such as from {@link daemon}. Abort from `run(task)` remains Fiber control
2651
- * flow. Other errors are domain errors: retrying continues while the schedule
2652
- * yields another delay and {@link RetryOptions.shouldRetry} returns `true`. When
2653
- * retrying stops, `retry` returns {@link RetryError} with the final domain error
2654
- * as `lastError`.
2596
+ * such as from {@link daemon}. Abort from `run(task)` remains {@link Fiber}
2597
+ * control flow. Other errors are domain errors: retrying continues while the
2598
+ * schedule yields another delay and {@link RetryOptions.shouldRetry} returns
2599
+ * `true`. When retrying stops, `retry` returns {@link RetryError} with the final
2600
+ * domain error as `lastError`.
2655
2601
  *
2656
2602
  * AbortError recognition is structural. Domain errors, especially values parsed
2657
2603
  * from untrusted input, must not use the reserved AbortError shape.
2658
2604
  *
2659
- * ### Example
2605
+ * ### Retrying failures
2660
2606
  *
2661
2607
  * ```ts
2662
2608
  * import {
@@ -2667,57 +2613,81 @@ export type RetryTaskError<E> = ([Exclude<E, AbortError>] extends [never] ? neve
2667
2613
  * type Result,
2668
2614
  * type RetryTaskError,
2669
2615
  * type Task,
2616
+ * type Typed,
2670
2617
  * } from "@evolu/common";
2671
2618
  *
2672
- * await using run = createRun();
2673
- *
2674
- * interface FetchDataError {
2675
- * readonly type: "FetchDataError";
2676
- * }
2619
+ * const fetchData: Task<string, ServiceUnavailableError> = () =>
2620
+ * err({ type: "ServiceUnavailable" });
2677
2621
  *
2678
- * const fetchData: Task<string, FetchDataError> = () =>
2679
- * err({ type: "FetchDataError" });
2622
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
2680
2623
  *
2681
2624
  * const fetchWithRetry = retry(fetchData, recurs(2));
2682
2625
  *
2626
+ * await using run = createRun();
2683
2627
  * const result = await run(fetchWithRetry);
2684
2628
  * expectTypeOf(result).toEqualTypeOf<
2685
- * Result<string, RetryTaskError<FetchDataError>>
2629
+ * Result<string, RetryTaskError<ServiceUnavailableError>>
2686
2630
  * >();
2687
2631
  * expectErr(result, {
2688
2632
  * type: "RetryError",
2689
2633
  * attempts: 3,
2690
- * lastError: { type: "FetchDataError" },
2634
+ * lastError: { type: "ServiceUnavailable" },
2691
2635
  * });
2692
2636
  * ```
2693
2637
  *
2694
- * ### Example
2638
+ * ### Filtering retries
2695
2639
  *
2696
2640
  * ```ts
2697
- * import { createRun, err, recurs, retry, type Task } from "@evolu/common";
2641
+ * import {
2642
+ * createRun,
2643
+ * err,
2644
+ * recurs,
2645
+ * retry,
2646
+ * type Task,
2647
+ * type Typed,
2648
+ * } from "@evolu/common";
2698
2649
  *
2699
- * interface FetchDataError {
2700
- * readonly type: "RecoverableError" | "FatalError";
2701
- * }
2650
+ * const fetchData: Task<
2651
+ * string,
2652
+ * TemporaryFailureError | PermanentFailureError
2653
+ * > = () => err({ type: "PermanentFailure" });
2654
+ *
2655
+ * interface TemporaryFailureError extends Typed<"TemporaryFailure"> {}
2702
2656
  *
2703
- * const fetchData: Task<string, FetchDataError> = () =>
2704
- * err({ type: "FatalError" });
2657
+ * interface PermanentFailureError extends Typed<"PermanentFailure"> {}
2705
2658
  *
2706
2659
  * const fetchWithRetry = retry(fetchData, recurs(5), {
2707
- * shouldRetry: (error) => error.type !== "FatalError",
2660
+ * shouldRetry: (error) => error.type !== "PermanentFailure",
2708
2661
  * });
2709
2662
  *
2710
2663
  * await using run = createRun();
2711
2664
  * expectErr(await run(fetchWithRetry), {
2712
2665
  * type: "RetryError",
2713
2666
  * attempts: 1,
2714
- * lastError: { type: "FatalError" },
2667
+ * lastError: { type: "PermanentFailure" },
2715
2668
  * });
2716
2669
  * ```
2717
2670
  *
2718
2671
  * @group Resilience
2719
2672
  */
2720
2673
  export declare const retry: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, Exclude<E, AbortError>>, { shouldRetry, onRetry, }?: RetryOptions<Exclude<E, AbortError>, Output>) => Task<T, RetryTaskError<E>, D>;
2674
+ /**
2675
+ * Error returned by {@link retry} when retrying stops after a domain error.
2676
+ *
2677
+ * @group Resilience
2678
+ */
2679
+ export interface RetryError<E> extends Typed<"RetryError"> {
2680
+ /** The final domain error that stopped retrying. */
2681
+ readonly lastError: E;
2682
+ /** The number of attempts that were started. */
2683
+ readonly attempts: PositiveInt;
2684
+ }
2685
+ /**
2686
+ * Error type returned by {@link retry}.
2687
+ *
2688
+ * @group Resilience
2689
+ */
2690
+ export type RetryTaskError<E> = ([Exclude<E, AbortError>] extends [never] ? never : RetryError<Exclude<E, AbortError>>) | Extract<E, AbortError>;
2721
2691
  /**
2722
2692
  * Options for {@link repeat}.
2723
2693
  *
@@ -2725,10 +2695,10 @@ export declare const retry: <T, E, D = unknown, Output = unknown>(task: Task<T,
2725
2695
  */
2726
2696
  export interface RepeatOptions<T, Output> {
2727
2697
  /**
2728
- * Decides whether an Ok value should schedule another repeat.
2698
+ * Decides whether an {@link Ok} value should schedule another repeat.
2729
2699
  *
2730
2700
  * The predicate must not throw: a thrown exception is a defect that panics
2731
- * the Run tree.
2701
+ * the {@link Run} tree.
2732
2702
  */
2733
2703
  readonly shouldRepeat?: Predicate<T>;
2734
2704
  /**
@@ -2738,7 +2708,7 @@ export interface RepeatOptions<T, Output> {
2738
2708
  * upcoming attempt number. The callback runs after
2739
2709
  * {@link RepeatOptions.shouldRepeat} and the {@link Schedule} approve a repeat,
2740
2710
  * and before the repeat delay. The callback must not throw: a thrown
2741
- * exception is a defect that panics the Run tree.
2711
+ * exception is a defect that panics the {@link Run} tree.
2742
2712
  */
2743
2713
  readonly onRepeat?: (attempt: RepeatAttempt<T, Output>) => void;
2744
2714
  }
@@ -2748,17 +2718,18 @@ export interface RepeatOptions<T, Output> {
2748
2718
  * @group Repetition
2749
2719
  */
2750
2720
  export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
2751
- /** The Ok value returned by the completed attempt. */
2721
+ /** The {@link Ok} value returned by the completed attempt. */
2752
2722
  readonly value: T;
2753
2723
  }
2754
2724
  /**
2755
- * Repeats a Task according to a {@link Schedule}.
2725
+ * Repeats a {@link Task} according to a {@link Schedule}.
2756
2726
  *
2757
2727
  * Runs the Task once, then repeats while the Task returns {@link Ok}, the
2758
2728
  * schedule yields another delay, and {@link RepeatOptions.shouldRepeat} returns
2759
- * `true`. When repeating stops, `repeat` returns the last successful Result. If
2760
- * the Task returns {@link Err}, including {@link Done} from a {@link NextTask},
2761
- * `repeat` returns that error without scheduling another attempt.
2729
+ * `true`. When repeating stops, `repeat` returns the last successful
2730
+ * {@link Result}. If the Task returns {@link Err}, including {@link Done} from a
2731
+ * {@link NextTask}, `repeat` returns that error without scheduling another
2732
+ * attempt.
2762
2733
  *
2763
2734
  * Use `repeat` for success-driven loops such as polling or consuming a
2764
2735
  * NextTask: it repeats after Ok and returns the Task's natural Result. Use
@@ -2767,7 +2738,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
2767
2738
  *
2768
2739
  * With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
2769
2740
  *
2770
- * ### Example
2741
+ * ### Repeating successes
2771
2742
  *
2772
2743
  * ```ts
2773
2744
  * import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
@@ -2785,7 +2756,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
2785
2756
  * expect(attempts).toBe(4);
2786
2757
  * ```
2787
2758
  *
2788
- * ### Example
2759
+ * ### Stopping with Done
2789
2760
  *
2790
2761
  * ```ts
2791
2762
  * import {
@@ -2798,8 +2769,6 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
2798
2769
  * type NextTask,
2799
2770
  * } from "@evolu/common";
2800
2771
  *
2801
- * await using run = createRun();
2802
- *
2803
2772
  * interface Item {
2804
2773
  * readonly id: string;
2805
2774
  * }
@@ -2811,6 +2780,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
2811
2780
  * return item ? ok(item) : err(done());
2812
2781
  * };
2813
2782
  *
2783
+ * await using run = createRun();
2814
2784
  * const result = await run(repeat(processQueue, spaced("1ms")));
2815
2785
  * expectErr(result, done());
2816
2786
  * expect(queue).toEqual([]);
@@ -2820,28 +2790,29 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
2820
2790
  */
2821
2791
  export declare const repeat: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, T>, { shouldRepeat, onRepeat }?: RepeatOptions<T, Output>) => Task<T, E, D>;
2822
2792
  /**
2823
- * Extracts the Result type produced by one Task in a non-empty Task array.
2793
+ * Extracts the {@link Result} type produced by one {@link Task} in a non-empty
2794
+ * Task array.
2824
2795
  *
2825
- * @internal
2796
+ * @group Type utilities
2826
2797
  */
2827
2798
  export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Result<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>>;
2828
2799
  /**
2829
- * Runs Tasks until one returns {@link Ok} or all return {@link Err}.
2800
+ * Runs {@link Task}s until one returns {@link Ok} or all return {@link Err}.
2830
2801
  *
2831
- * Use {@link race} to return the first settled Result instead, whether Ok or
2832
- * {@link Err}.
2802
+ * Use {@link race} to return the first settled {@link Result} instead, whether Ok
2803
+ * or {@link Err}.
2833
2804
  *
2834
- * Returns the first {@link Ok} Result. Losing Tasks are aborted. If no Task
2835
- * returns Ok, returns the last Err by input order, regardless of completion
2836
- * order. Other Err results are discarded; use {@link allSettled} when you need
2837
- * every error.
2805
+ * Returns the first {@link Ok} Result. Queued Tasks are not started, and other
2806
+ * running Tasks are aborted. If no Task returns Ok, returns the last Err by
2807
+ * input order, regardless of completion order. Other Err results are discarded;
2808
+ * use {@link allSettled} when you need every error.
2838
2809
  *
2839
2810
  * Sequential by default; pass a `concurrency` option to run more than one Task
2840
2811
  * at a time.
2841
2812
  *
2842
2813
  * Similar to
2843
2814
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
2844
- * but races Tasks, returns Result values, and aborts losers.
2815
+ * but runs Tasks, returns Result values, and stops after the first Ok.
2845
2816
  *
2846
2817
  * ### Example
2847
2818
  *
@@ -2853,10 +2824,14 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Re
2853
2824
  * ok,
2854
2825
  * type Result,
2855
2826
  * type Task,
2827
+ * type Typed,
2856
2828
  * } from "@evolu/common";
2857
2829
  *
2858
- * const unavailable: Task<string, "Unavailable"> = () =>
2859
- * err("Unavailable");
2830
+ * const unavailable: Task<string, ServiceUnavailableError> = () =>
2831
+ * err({ type: "ServiceUnavailable" });
2832
+ *
2833
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
2834
+ *
2860
2835
  * let fallbackStarted = false;
2861
2836
  * const fallback: Task<string> = () => {
2862
2837
  * fallbackStarted = true;
@@ -2866,7 +2841,9 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Re
2866
2841
  * await using run = createRun();
2867
2842
  * const result = await run(any([unavailable, fallback]));
2868
2843
  *
2869
- * expectTypeOf(result).toEqualTypeOf<Result<string, "Unavailable">>();
2844
+ * expectTypeOf(result).toEqualTypeOf<
2845
+ * Result<string, ServiceUnavailableError>
2846
+ * >();
2870
2847
  * expectOk(result, "fallback");
2871
2848
  * expect(fallbackStarted).toBe(true);
2872
2849
  * ```
@@ -2875,7 +2852,7 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Re
2875
2852
  */
2876
2853
  export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks, options?: TaskCollectionOptions) => Task<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
2877
2854
  /**
2878
- * Runs Tasks until the first Task settles.
2855
+ * Runs {@link Task}s until the first Task settles.
2879
2856
  *
2880
2857
  * Returns the first Task {@link Result} to settle, whether {@link Ok} or
2881
2858
  * {@link Err}.
@@ -2896,7 +2873,7 @@ export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks:
2896
2873
  * arrays whose emptiness is only known at runtime, guard with
2897
2874
  * {@link isNonEmptyArray}:
2898
2875
  *
2899
- * ### Example
2876
+ * ### Runtime arrays
2900
2877
  *
2901
2878
  * ```ts
2902
2879
  * import {
@@ -2915,7 +2892,7 @@ export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks:
2915
2892
  * }
2916
2893
  * ```
2917
2894
  *
2918
- * ### Example
2895
+ * ### First settlement
2919
2896
  *
2920
2897
  * ```ts
2921
2898
  * import {
@@ -2949,12 +2926,12 @@ export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks:
2949
2926
  */
2950
2927
  export declare const race: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks) => Task<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
2951
2928
  /**
2952
- * Runs Tasks until `count` Tasks return {@link Ok} or all Tasks settle.
2929
+ * Runs {@link Task}s until `count` Tasks return {@link Ok} or all Tasks settle.
2953
2930
  *
2954
2931
  * Returns {@link Ok} with Ok values in settlement order, not input order.
2955
- * {@link Err} Results are ignored. When `count` Ok values have settled,
2956
- * remaining Tasks are aborted. If fewer than `count` Tasks return Ok, returns
2957
- * the Ok values that did settle.
2932
+ * {@link Err} {@link Result}s are ignored. When `count` Ok values have settled,
2933
+ * queued Tasks are not started and remaining running Tasks are aborted. If
2934
+ * fewer than `count` Tasks return Ok, returns the Ok values that did settle.
2958
2935
  *
2959
2936
  * Sequential by default; pass a `concurrency` option to run more than one Task
2960
2937
  * at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
@@ -2970,17 +2947,23 @@ export declare const race: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks
2970
2947
  * ok,
2971
2948
  * sleep,
2972
2949
  * type Task,
2950
+ * type Typed,
2973
2951
  * } from "@evolu/common";
2974
2952
  *
2975
2953
  * let slowCompleted = false;
2976
- * const slow: Task<string, "Failed"> = async (run) => {
2954
+ * const slow: Task<string> = async (run) => {
2977
2955
  * await run.ok(sleep("10ms"));
2978
2956
  * slowCompleted = true;
2979
2957
  * return ok("slow");
2980
2958
  * };
2959
+ * const unavailable: Task<never, ServiceUnavailableError> = () =>
2960
+ * err({ type: "ServiceUnavailable" });
2961
+ *
2962
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
2963
+ *
2981
2964
  * const tasks = [
2982
2965
  * slow,
2983
- * () => err("Failed" as const),
2966
+ * unavailable,
2984
2967
  * () => ok("fast-1"),
2985
2968
  * () => ok("fast-2"),
2986
2969
  * ] as const;
@@ -2996,11 +2979,12 @@ export declare const race: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks
2996
2979
  */
2997
2980
  export declare const firstN: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks, count: Int1To100OrPositiveInt, options?: TaskCollectionOptions) => Task<ReadonlyArray<InferTaskOk<TTasks[number]>>, never, InferTasksDeps<TTasks>>;
2998
2981
  /**
2999
- * Runs Tasks until `count` Tasks settle or all Tasks settle.
2982
+ * Runs {@link Task}s until `count` Tasks settle or all Tasks settle.
3000
2983
  *
3001
2984
  * Returns {@link Ok} with Task {@link Result}s in settlement order, not input
3002
- * order. When `count` Results have settled, remaining Tasks are aborted. If
3003
- * fewer than `count` Tasks settle, returns the Results that did settle.
2985
+ * order. When `count` Results have settled, queued Tasks are not started and
2986
+ * remaining running Tasks are aborted. If fewer than `count` Tasks settle,
2987
+ * returns the Results that did settle.
3004
2988
  *
3005
2989
  * Sequential by default; pass a `concurrency` option to run more than one Task
3006
2990
  * at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
@@ -3016,25 +3000,27 @@ export declare const firstN: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tas
3016
3000
  * ok,
3017
3001
  * sleep,
3018
3002
  * type Task,
3003
+ * type Typed,
3019
3004
  * } from "@evolu/common";
3020
3005
  *
3021
3006
  * let slowCompleted = false;
3022
- * const slow: Task<string, "Failed"> = async (run) => {
3007
+ * const slow: Task<string> = async (run) => {
3023
3008
  * await run.ok(sleep("10ms"));
3024
3009
  * slowCompleted = true;
3025
3010
  * return ok("slow");
3026
3011
  * };
3027
- * const tasks = [
3028
- * slow,
3029
- * () => err("Failed" as const),
3030
- * () => ok("fast"),
3031
- * ] as const;
3012
+ * const unavailable: Task<never, ServiceUnavailableError> = () =>
3013
+ * err({ type: "ServiceUnavailable" });
3014
+ *
3015
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
3016
+ *
3017
+ * const tasks = [slow, unavailable, () => ok("fast")] as const;
3032
3018
  * await using run = createRun();
3033
3019
  *
3034
3020
  * // Err and Ok both count, and Results use settlement order.
3035
3021
  * const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
3036
3022
  * expectOk(result, [
3037
- * { ok: false, error: "Failed" },
3023
+ * { ok: false, error: { type: "ServiceUnavailable" } },
3038
3024
  * { ok: true, value: "fast" },
3039
3025
  * ]);
3040
3026
  * expect(slowCompleted).toBe(false);
@@ -3046,43 +3032,44 @@ export declare const firstNSettled: <TTasks extends NonEmptyReadonlyArray<AnyTas
3046
3032
  /**
3047
3033
  * Decision returned by an {@link each} result handler.
3048
3034
  *
3049
- * `continue` allows queued Tasks to start when concurrency capacity is
3035
+ * `continue` allows queued {@link Task}s to start when concurrency capacity is
3050
3036
  * available. `stop` prevents queued Tasks from starting and aborts already
3051
- * running Tasks through structured Run disposal.
3037
+ * running Tasks through structured {@link Run} disposal.
3052
3038
  *
3053
- * @group Concurrency
3039
+ * @group Collection
3054
3040
  */
3055
3041
  export type EachDecision = "continue" | "stop";
3056
3042
  /**
3057
- * Handles one settled Task Result from {@link each}.
3043
+ * Handles one settled {@link Task} {@link Result} from {@link each}.
3058
3044
  *
3059
3045
  * The index is the original input index. Callback order follows settlement
3060
3046
  * order, not input order.
3061
3047
  *
3062
- * @group Concurrency
3048
+ * @group Collection
3063
3049
  */
3064
3050
  export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (result: InferTasksResult<TTasks>, index: number) => EachDecision;
3065
3051
  /**
3066
- * Runs Tasks under a concurrency limit and calls `onResult` for each Task
3067
- * {@link Result} as it settles.
3052
+ * Runs {@link Task}s under a concurrency limit and calls `onResult` for each
3053
+ * Task {@link Result} as it settles.
3068
3054
  *
3069
- * `onResult` receives the Result and the original input index; call order is
3070
- * settlement order, not input order. Returning `continue` lets queued Tasks
3071
- * start when capacity is available. Returning `stop` prevents queued Tasks from
3072
- * starting and aborts already-running Tasks through structured Run disposal —
3073
- * `each` still waits for them to settle before returning.
3055
+ * `onResult` receives the {@link Result} and the original input index; call
3056
+ * order is settlement order, not input order. Returning `continue` lets queued
3057
+ * Tasks start when capacity is available. Returning `stop` prevents queued
3058
+ * Tasks from starting and aborts already-running Tasks through structured
3059
+ * {@link Run} disposal — `each` still waits for them to settle before
3060
+ * returning.
3074
3061
  *
3075
3062
  * `each` is the scheduling primitive under the collection helpers. Each one is
3076
3063
  * a small `onResult` policy:
3077
3064
  *
3078
- * | Helper | Policy |
3079
- * | --------------------- | ------------------------------------- |
3080
- * | {@link all} | Collect values, stop on the first Err |
3081
- * | {@link allSettled} | Collect every Result, never stop |
3082
- * | {@link any} | Stop on the first Ok |
3083
- * | {@link race} | Stop on the first settled Result |
3084
- * | {@link firstN} | Stop after n Ok values |
3085
- * | {@link firstNSettled} | Stop after n Results |
3065
+ * | Helper | Policy |
3066
+ * | --------------------- | --------------------------------------------- |
3067
+ * | {@link all} | Collect values, stop on the first {@link Err} |
3068
+ * | {@link allSettled} | Collect every Result, never stop |
3069
+ * | {@link any} | Stop on the first {@link Ok} |
3070
+ * | {@link race} | Stop on the first settled Result |
3071
+ * | {@link firstN} | Stop after n Ok values |
3072
+ * | {@link firstNSettled} | Stop after n Results |
3086
3073
  *
3087
3074
  * Use `each` directly to build a collection policy the helpers don't cover. For
3088
3075
  * example, keep the first successful value together with its original input
@@ -3098,6 +3085,7 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (resul
3098
3085
  * ok,
3099
3086
  * sleep,
3100
3087
  * type Task,
3088
+ * type Typed,
3101
3089
  * } from "@evolu/common";
3102
3090
  *
3103
3091
  * let slowCompleted = false;
@@ -3106,11 +3094,12 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (resul
3106
3094
  * slowCompleted = true;
3107
3095
  * return ok("slow");
3108
3096
  * };
3109
- * const tasks = [
3110
- * slow,
3111
- * () => err("Unavailable" as const),
3112
- * () => ok("fast"),
3113
- * ] as const;
3097
+ * const unavailable: Task<never, ServiceUnavailableError> = () =>
3098
+ * err({ type: "ServiceUnavailable" });
3099
+ *
3100
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
3101
+ *
3102
+ * const tasks = [slow, unavailable, () => ok("fast")] as const;
3114
3103
  * let first: readonly [string, number] | undefined;
3115
3104
  * await using run = createRun();
3116
3105
  * const result = await run(
@@ -3144,14 +3133,13 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (resul
3144
3133
  * building on `each` preserves diagnostics that a hand-rolled scheduling loop
3145
3134
  * typically loses.
3146
3135
  *
3147
- * @group Concurrency
3136
+ * @group Collection
3148
3137
  */
3149
3138
  export declare const each: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks, onResult: EachCallback<TTasks>, options?: TaskCollectionOptions) => Task<void, never, InferTasksDeps<TTasks>>;
3150
3139
  /**
3151
- * Scheduler priority for Tasks started through a native scheduler.
3140
+ * Scheduler priority for {@link Task}s started through a native scheduler.
3152
3141
  *
3153
- * Only static priorities are supported. Mutable scheduler priorities will be
3154
- * added in a future release.
3142
+ * A Task's priority is static while it runs.
3155
3143
  *
3156
3144
  * @group Scheduling
3157
3145
  */
@@ -3188,9 +3176,9 @@ export declare const prioritized: <T, E, D = unknown>(priority: TaskPriority, ta
3188
3176
  * Yields execution to the host scheduler.
3189
3177
  *
3190
3178
  * Uses native `scheduler.yield()` when available, `setImmediate` when
3191
- * available, and `setTimeout` elsewhere. Because this is a Task, `await
3192
- * run(yieldNow)` is an explicit abortable checkpoint and is visible in Run
3193
- * monitoring.
3179
+ * available, and `setTimeout` elsewhere. Because this is a {@link Task}, `await
3180
+ * run(yieldNow)` is an explicit abortable checkpoint and is visible in
3181
+ * {@link Run} monitoring.
3194
3182
  *
3195
3183
  * For example, call it periodically in a long-running synchronous loop to let
3196
3184
  * the host process rendering, input, and other scheduled work.
@@ -3241,12 +3229,13 @@ export declare const yieldNow: Task<void>;
3241
3229
  * interface ServerDep {
3242
3230
  * readonly port: number;
3243
3231
  * }
3232
+ *
3244
3233
  * interface Server extends AsyncDisposable {}
3245
3234
  *
3246
3235
  * const serverStarted = Promise.withResolvers<void>();
3247
3236
  * let serverStopped = false;
3248
- * const startServer: Task<Server, never, ServerDep> = ({ deps }) => {
3249
- * expect(deps.port).toBe(3000);
3237
+ * const startServer: Task<Server, never, ServerDep> = (run) => {
3238
+ * expect(run.deps.port).toBe(3000);
3250
3239
  * serverStarted.resolve();
3251
3240
  * return ok({
3252
3241
  * [Symbol.asyncDispose]: async () => {
@@ -3296,7 +3285,7 @@ export declare const waitForAbort: Task<never>;
3296
3285
  * Do not wrap a Task that keeps using a resource the caller may release after
3297
3286
  * this wrapper returns, unless the Task reliably observes abort before using
3298
3287
  * that resource. The daemon Task can continue after the caller stops waiting.
3299
- * Later domain `Err` results from the daemon Task are discarded after the
3288
+ * Later domain {@link Err} results from the daemon Task are discarded after the
3300
3289
  * caller stops waiting. Defects from the daemon Task remain visible to the root
3301
3290
  * Run: if it later throws or rejects, the root Run still panics and reports the
3302
3291
  * defect.
@@ -3306,10 +3295,10 @@ export declare const waitForAbort: Task<never>;
3306
3295
  * to settle, keeping cleanup and late defects inside the caller's lifetime. A
3307
3296
  * Task that ignores abort can keep them waiting.
3308
3297
  *
3309
- * `run.abortable(task)` returns an owned child Fiber and requests abort through
3310
- * that Fiber; `daemon(task)` starts a daemon child and stops waiting when the
3311
- * current Run aborts. {@link unabortable} masks abort for a Task that must
3312
- * finish once started; daemon lets a Task outlive the caller.
3298
+ * `run.abortable(task)` returns an owned child {@link Fiber} and requests abort
3299
+ * through that Fiber; `daemon(task)` starts a daemon child and stops waiting
3300
+ * when the current Run aborts. {@link unabortable} masks abort for a Task that
3301
+ * must finish once started; daemon lets a Task outlive the caller.
3313
3302
  *
3314
3303
  * Because the Task starts with {@link Run.daemon}, a recorded abort request
3315
3304
  * returns AbortError before the Task starts — including a request masked by
@@ -3356,15 +3345,10 @@ export declare const waitForAbort: Task<never>;
3356
3345
  * import { createRun, ok, type Result, type Task } from "@evolu/common";
3357
3346
  *
3358
3347
  * type ResultValue = string;
3359
- * interface MyError {
3360
- * readonly type: "MyError";
3361
- * }
3362
- * const createPromiseReturningResult = (): Promise<
3363
- * Result<ResultValue, MyError>
3364
- * > => Promise.resolve(ok("value"));
3348
+ * const createPromiseReturningResult = (): Promise<Result<ResultValue>> =>
3349
+ * Promise.resolve(ok("value"));
3365
3350
  *
3366
- * const task: Task<ResultValue, MyError> = () =>
3367
- * createPromiseReturningResult();
3351
+ * const task: Task<ResultValue> = () => createPromiseReturningResult();
3368
3352
  *
3369
3353
  * await using run = createRun();
3370
3354
  * expectOk(await run(task), "value");
@@ -3377,12 +3361,9 @@ export declare const waitForAbort: Task<never>;
3377
3361
  * import { ok, type Result, type Task } from "@evolu/common";
3378
3362
  *
3379
3363
  * type ResultValue = string;
3380
- * interface MyError {
3381
- * readonly type: "MyError";
3382
- * }
3383
3364
  * let promiseStarted = false;
3384
3365
  * const createPromiseReturningResult = (): Promise<
3385
- * Result<ResultValue, MyError>
3366
+ * Result<ResultValue>
3386
3367
  * > => {
3387
3368
  * promiseStarted = true;
3388
3369
  * return Promise.resolve(ok("value"));
@@ -3390,10 +3371,10 @@ export declare const waitForAbort: Task<never>;
3390
3371
  *
3391
3372
  * // Wrong: the Promise starts now, before a Run starts the Task.
3392
3373
  * const promise = createPromiseReturningResult();
3393
- * const task: Task<ResultValue, MyError> = () => promise;
3374
+ * const task: Task<ResultValue> = () => promise;
3394
3375
  *
3395
3376
  * expect(promiseStarted).toBe(true);
3396
- * expectTypeOf(task).toEqualTypeOf<Task<ResultValue, MyError>>();
3377
+ * expectTypeOf(task).toEqualTypeOf<Task<ResultValue>>();
3397
3378
  * ```
3398
3379
  *
3399
3380
  * @group Lifetime
@@ -3405,8 +3386,8 @@ export declare const daemon: <T, E, D = unknown>(task: Task<T, E, D>) => Task<T,
3405
3386
  * Abort requests are masked while the Task runs, so `run.signal.aborted`
3406
3387
  * remains false inside the Task. This does not force the Task to start after an
3407
3388
  * abort request has already reached its Run; unabortable means the Task is not
3408
- * interrupted once it has started. Disposing the enclosing Run still waits for
3409
- * the Task to settle.
3389
+ * interrupted once it has started. Disposing the enclosing {@link Run} still
3390
+ * waits for the Task to settle.
3410
3391
  *
3411
3392
  * Apply at most one abort behavior helper to a Task: do not wrap the same Task
3412
3393
  * with both unabortable and restore, or apply either helper more than once.
@@ -3418,10 +3399,10 @@ export declare const daemon: <T, E, D = unknown>(task: Task<T, E, D>) => Task<T,
3418
3399
  *
3419
3400
  * const commitStarted = Promise.withResolvers<void>();
3420
3401
  * const finishCommit = Promise.withResolvers<void>();
3421
- * const commit: Task<string> = unabortable(async ({ signal }) => {
3402
+ * const commit: Task<string> = unabortable(async (run) => {
3422
3403
  * commitStarted.resolve();
3423
3404
  * await finishCommit.promise;
3424
- * expect(signal.aborted).toBe(false);
3405
+ * expect(run.signal.aborted).toBe(false);
3425
3406
  * return ok("committed");
3426
3407
  * });
3427
3408
  *
@@ -3438,8 +3419,8 @@ export declare const daemon: <T, E, D = unknown>(task: Task<T, E, D>) => Task<T,
3438
3419
  */
3439
3420
  export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
3440
3421
  /**
3441
- * Like {@link unabortable}, but provides `restore` for child Tasks that should
3442
- * run with the previous abort mask.
3422
+ * Like {@link unabortable}, but provides `restore` for child {@link Task}s that
3423
+ * should run with the previous abort mask.
3443
3424
  *
3444
3425
  * Use this for acquire/use/release flows where acquire and release must finish
3445
3426
  * once started, while use should remain abortable. Child Tasks inherit the mask
@@ -3467,8 +3448,6 @@ export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D
3467
3448
  * type Task,
3468
3449
  * } from "@evolu/common";
3469
3450
  *
3470
- * await using run = createRun();
3471
- *
3472
3451
  * interface Resource {
3473
3452
  * readonly id: string;
3474
3453
  * }
@@ -3485,13 +3464,14 @@ export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D
3485
3464
  * let released = false;
3486
3465
  * const release =
3487
3466
  * (_resource: Resource): Task<void> =>
3488
- * ({ signal }) => {
3467
+ * (run) => {
3489
3468
  * // Release inherits the mask even after abort was requested.
3490
- * expect(signal.aborted).toBe(false);
3469
+ * expect(run.signal.aborted).toBe(false);
3491
3470
  * released = true;
3492
3471
  * return ok();
3493
3472
  * };
3494
3473
  *
3474
+ * await using run = createRun();
3495
3475
  * const fiber = run.abortable(
3496
3476
  * unabortableMask((restore) => async (run) => {
3497
3477
  * // Acquire with abort masked.
@@ -3544,6 +3524,7 @@ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2,
3544
3524
  * err,
3545
3525
  * ok,
3546
3526
  * type Task,
3527
+ * type Typed,
3547
3528
  * } from "@evolu/common";
3548
3529
  *
3549
3530
  * interface Connection {
@@ -3554,9 +3535,14 @@ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2,
3554
3535
  * const openConnection: Task<Connection> = () =>
3555
3536
  * ok({ user: "Ada", isAvailable: false });
3556
3537
  * const loadUser =
3557
- * (connection: Connection): Task<string, "Unavailable"> =>
3538
+ * (connection: Connection): Task<string, UserUnavailableError> =>
3558
3539
  * () =>
3559
- * connection.isAvailable ? ok(connection.user) : err("Unavailable");
3540
+ * connection.isAvailable
3541
+ * ? ok(connection.user)
3542
+ * : err({ type: "UserUnavailable" });
3543
+ *
3544
+ * interface UserUnavailableError extends Typed<"UserUnavailable"> {}
3545
+ *
3560
3546
  * let connectionClosed = false;
3561
3547
  * const closeConnection =
3562
3548
  * (_connection: Connection): Task<void> =>
@@ -3572,26 +3558,38 @@ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2,
3572
3558
  * );
3573
3559
  *
3574
3560
  * await using run = createRun();
3575
- * expectErr(await run(queryUser), "Unavailable");
3561
+ * expectErr(await run(queryUser), { type: "UserUnavailable" });
3576
3562
  * // Release still runs when use returns a domain error.
3577
3563
  * expect(connectionClosed).toBe(true);
3578
3564
  * ```
3579
3565
  *
3580
- * @group Abortability
3566
+ * @group Lifetime
3581
3567
  */
3582
3568
  export declare const acquireUseRelease: <Resource, T, EAcquire, EUse, DAcquire = unknown, DUse = unknown, DRelease = unknown>(acquire: Task<Resource, EAcquire, DAcquire>, use: (resource: Resource) => Task<T, EUse, DUse>, release: (resource: Resource) => Task<void, never, DRelease>) => Task<T, EAcquire | EUse, DAcquire & DUse & DRelease>;
3583
3569
  /**
3584
3570
  * A one-shot value resolved from outside the waiting {@link Task}.
3585
3571
  *
3586
- * Use Deferred when Task code must wait for a Result completed by an external
3587
- * callback or another Task. Deferred is `Promise.withResolvers` with Task
3588
- * semantics: each waiter uses its waiting Run lifetime, can abort
3572
+ * Use Deferred when Task code must wait for a {@link Result} completed by an
3573
+ * external callback or another Task. Deferred is `Promise.withResolvers` with
3574
+ * Task semantics: each waiter uses its waiting {@link Run} lifetime, can abort
3589
3575
  * independently, appears in Run observability, and settles with Result-based
3590
3576
  * errors.
3591
3577
  *
3592
3578
  * The Deferred resolves once. Later calls to {@link Deferred.resolve} return
3593
3579
  * `false` and do not change the Result.
3594
3580
  *
3581
+ * @group Concurrency primitives
3582
+ * @see {@link createDeferred}
3583
+ */
3584
+ export interface Deferred<T, E = never> {
3585
+ /** Waits until {@link Deferred.resolve} resolves the Deferred. */
3586
+ readonly task: Task<T, E>;
3587
+ /** Resolves the Deferred, returning whether this call completed it. */
3588
+ readonly resolve: (result: Result<T, E>) => boolean;
3589
+ }
3590
+ /**
3591
+ * Creates a {@link Deferred}.
3592
+ *
3595
3593
  * ### Example
3596
3594
  *
3597
3595
  * ```ts
@@ -3618,7 +3616,7 @@ export declare const acquireUseRelease: <Resource, T, EAcquire, EUse, DAcquire =
3618
3616
  * expectOk(await run(deferred.task), "ready");
3619
3617
  * ```
3620
3618
  *
3621
- * ### Example
3619
+ * ### Aborting a waiter
3622
3620
  *
3623
3621
  * ```ts
3624
3622
  * import { AbortError, createDeferred, createRun } from "@evolu/common";
@@ -3635,22 +3633,10 @@ export declare const acquireUseRelease: <Resource, T, EAcquire, EUse, DAcquire =
3635
3633
  * ```
3636
3634
  *
3637
3635
  * @group Concurrency primitives
3638
- * @see {@link createDeferred}
3639
- */
3640
- export interface Deferred<T, E = never> {
3641
- /** Waits until {@link Deferred.resolve} resolves the Deferred. */
3642
- readonly task: Task<T, E>;
3643
- /** Resolves the Deferred, returning whether this call completed it. */
3644
- readonly resolve: (result: Result<T, E>) => boolean;
3645
- }
3646
- /**
3647
- * Creates a {@link Deferred}.
3648
- *
3649
- * @group Concurrency primitives
3650
3636
  */
3651
3637
  export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
3652
3638
  /**
3653
- * A reusable gate for blocking and releasing Tasks.
3639
+ * A reusable gate for blocking and releasing {@link Task}s.
3654
3640
  *
3655
3641
  * - **Closed**: Tasks wait.
3656
3642
  * - **Open**: Tasks proceed.
@@ -3664,6 +3650,24 @@ export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
3664
3650
  * {@link createGate} creates a closed Gate by default. Pass `isOpen: true` when
3665
3651
  * work should proceed immediately.
3666
3652
  *
3653
+ * @group Concurrency primitives
3654
+ * @see {@link createGate}
3655
+ */
3656
+ export interface Gate {
3657
+ /** Waits while the gate is closed. */
3658
+ readonly wait: Task<void>;
3659
+ /** Opens the gate, releasing all waiters. Returns false when already open. */
3660
+ readonly open: () => boolean;
3661
+ /** Closes the gate. Returns false when already closed. */
3662
+ readonly close: () => boolean;
3663
+ /** Releases the current closed wait cycle. Returns false when already open. */
3664
+ readonly release: () => boolean;
3665
+ /** Returns whether the gate is open. */
3666
+ readonly isOpen: () => boolean;
3667
+ }
3668
+ /**
3669
+ * Creates a {@link Gate}.
3670
+ *
3667
3671
  * ### Example
3668
3672
  *
3669
3673
  * ```ts
@@ -3693,30 +3697,12 @@ export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
3693
3697
  * ```
3694
3698
  *
3695
3699
  * @group Concurrency primitives
3696
- * @see {@link createGate}
3697
- */
3698
- export interface Gate {
3699
- /** Waits while the gate is closed. */
3700
- readonly wait: Task<void>;
3701
- /** Opens the gate, releasing all waiters. Returns false when already open. */
3702
- readonly open: () => boolean;
3703
- /** Closes the gate. Returns false when already closed. */
3704
- readonly close: () => boolean;
3705
- /** Releases the current closed wait cycle. Returns false when already open. */
3706
- readonly release: () => boolean;
3707
- /** Returns whether the gate is open. */
3708
- readonly isOpen: () => boolean;
3709
- }
3710
- /**
3711
- * Creates a {@link Gate}.
3712
- *
3713
- * @group Concurrency primitives
3714
3700
  */
3715
3701
  export declare const createGate: ({ isOpen, }?: {
3716
3702
  isOpen?: boolean;
3717
3703
  }) => Gate;
3718
3704
  /**
3719
- * Coordinates concurrent Tasks by acquiring and releasing permits.
3705
+ * Coordinates concurrent {@link Task}s by acquiring and releasing permits.
3720
3706
  *
3721
3707
  * Use {@link Semaphore.withPermit} or {@link Semaphore.withPermits} to acquire
3722
3708
  * permits for one Task and release them when it settles. Use
@@ -3735,45 +3721,6 @@ export declare const createGate: ({ isOpen, }?: {
3735
3721
  * already holding permits consumes additional permits and can wait if not
3736
3722
  * enough permits are available.
3737
3723
  *
3738
- * ### Example
3739
- *
3740
- * ```ts
3741
- * import {
3742
- * createRun,
3743
- * createSemaphore,
3744
- * getOk,
3745
- * ok,
3746
- * sleep,
3747
- * type Task,
3748
- * } from "@evolu/common";
3749
- *
3750
- * await using run = createRun();
3751
- *
3752
- * const semaphore = createSemaphore(2);
3753
- * let activeSaves = 0;
3754
- * let maxActiveSaves = 0;
3755
- *
3756
- * const saveUser =
3757
- * (id: string): Task<string> =>
3758
- * async (run) => {
3759
- * activeSaves += 1;
3760
- * maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
3761
- * await run.ok(sleep("10ms"));
3762
- * activeSaves -= 1;
3763
- * return ok(`saved:${id}`);
3764
- * };
3765
- *
3766
- * const results = await Promise.all([
3767
- * run(semaphore.withPermit(saveUser("1"))),
3768
- * run(semaphore.withPermit(saveUser("2"))),
3769
- * run(semaphore.withPermit(saveUser("3"))),
3770
- * ]);
3771
- *
3772
- * const savedUsers = results.map(getOk);
3773
- * expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
3774
- * expect(maxActiveSaves).toBe(2);
3775
- * ```
3776
- *
3777
3724
  * @group Concurrency primitives
3778
3725
  * @see {@link createSemaphore}
3779
3726
  */
@@ -3793,8 +3740,8 @@ export interface Semaphore {
3793
3740
  /**
3794
3741
  * Acquires permits and returns an owned {@link SemaphorePermit}.
3795
3742
  *
3796
- * The Task waits until enough permits are available. Dispose or release the
3797
- * returned permit to make them available again.
3743
+ * The {@link Task} waits until enough permits are available. Dispose or
3744
+ * release the returned permit to make them available again.
3798
3745
  *
3799
3746
  * When the request exceeds the current total permit count, the Task remains
3800
3747
  * pending until {@link Semaphore.resize} increases capacity or the Task is
@@ -3894,6 +3841,45 @@ export interface SemaphoreSnapshot {
3894
3841
  /**
3895
3842
  * Creates a {@link Semaphore}.
3896
3843
  *
3844
+ * ### Example
3845
+ *
3846
+ * ```ts
3847
+ * import {
3848
+ * createRun,
3849
+ * createSemaphore,
3850
+ * getOk,
3851
+ * ok,
3852
+ * sleep,
3853
+ * type Task,
3854
+ * } from "@evolu/common";
3855
+ *
3856
+ * await using run = createRun();
3857
+ *
3858
+ * const semaphore = createSemaphore(2);
3859
+ * let activeSaves = 0;
3860
+ * let maxActiveSaves = 0;
3861
+ *
3862
+ * const saveUser =
3863
+ * (id: string): Task<string> =>
3864
+ * async (run) => {
3865
+ * activeSaves += 1;
3866
+ * maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
3867
+ * await run.ok(sleep("10ms"));
3868
+ * activeSaves -= 1;
3869
+ * return ok(`saved:${id}`);
3870
+ * };
3871
+ *
3872
+ * const results = await Promise.all([
3873
+ * run(semaphore.withPermit(saveUser("1"))),
3874
+ * run(semaphore.withPermit(saveUser("2"))),
3875
+ * run(semaphore.withPermit(saveUser("3"))),
3876
+ * ]);
3877
+ *
3878
+ * const savedUsers = results.map(getOk);
3879
+ * expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
3880
+ * expect(maxActiveSaves).toBe(2);
3881
+ * ```
3882
+ *
3897
3883
  * @group Concurrency primitives
3898
3884
  */
3899
3885
  export declare const createSemaphore: (initialPermits: Int1To100OrPositiveInt, { policy, }?: {
@@ -3905,11 +3891,22 @@ export declare const createSemaphore: (initialPermits: Int1To100OrPositiveInt, {
3905
3891
  policy?: SemaphorePolicy;
3906
3892
  }) => Semaphore;
3907
3893
  /**
3908
- * Runs Tasks one at a time.
3894
+ * Runs {@link Task}s one at a time.
3909
3895
  *
3910
3896
  * `Mutex` is non-reentrant. A Task that tries to acquire the same Mutex while
3911
3897
  * already holding it waits on itself and will not progress.
3912
3898
  *
3899
+ * @group Concurrency primitives
3900
+ */
3901
+ export interface Mutex {
3902
+ /** Runs a {@link Task} while holding the lock. */
3903
+ readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
3904
+ /** Returns the current lock state for monitoring and debugging. */
3905
+ readonly snapshot: () => SemaphoreSnapshot;
3906
+ }
3907
+ /**
3908
+ * Creates a {@link Mutex}.
3909
+ *
3913
3910
  * ### Example
3914
3911
  *
3915
3912
  * ```ts
@@ -3943,20 +3940,9 @@ export declare const createSemaphore: (initialPermits: Int1To100OrPositiveInt, {
3943
3940
  *
3944
3941
  * @group Concurrency primitives
3945
3942
  */
3946
- export interface Mutex {
3947
- /** Runs a {@link Task} while holding the lock. */
3948
- readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
3949
- /** Returns the current lock state for monitoring and debugging. */
3950
- readonly snapshot: () => SemaphoreSnapshot;
3951
- }
3952
- /**
3953
- * Creates a {@link Mutex}.
3954
- *
3955
- * @group Concurrency primitives
3956
- */
3957
3943
  export declare const createMutex: () => Mutex;
3958
3944
  /**
3959
- * Coordinates concurrent Tasks independently for each key.
3945
+ * Coordinates concurrent {@link Task}s independently for each key.
3960
3946
  *
3961
3947
  * `SemaphoreByKey` intentionally exposes only Task-scoped acquisition helpers,
3962
3948
  * not the complete {@link Semaphore} API. Methods like {@link Semaphore.take} and
@@ -3969,54 +3955,6 @@ export declare const createMutex: () => Mutex;
3969
3955
  * ownership should be tied to one Task lifetime and idle keys can be forgotten
3970
3956
  * automatically.
3971
3957
  *
3972
- * ### Example
3973
- *
3974
- * ```ts
3975
- * import {
3976
- * createGate,
3977
- * createRun,
3978
- * createSemaphoreByKey,
3979
- * getOk,
3980
- * ok,
3981
- * type Task,
3982
- * } from "@evolu/common";
3983
- *
3984
- * // Each host gets an independent two-download limit.
3985
- * const downloadsByHost = createSemaphoreByKey<string>(2);
3986
- * const finishDownloads = createGate();
3987
- * const firstBatchStarted = Promise.withResolvers<void>();
3988
- * const started: Array<string> = [];
3989
- * const download = (host: string, file: string): Task<string> =>
3990
- * downloadsByHost.withPermit(host, async (run) => {
3991
- * started.push(`${host}/${file}`);
3992
- * if (started.length === 3) firstBatchStarted.resolve();
3993
- * await run.ok(finishDownloads.wait);
3994
- * return ok(`${host}/${file}`);
3995
- * });
3996
- *
3997
- * await using run = createRun();
3998
- * const downloads = [
3999
- * run(download("a.example", "1.json")),
4000
- * run(download("a.example", "2.json")),
4001
- * run(download("a.example", "3.json")),
4002
- * run(download("b.example", "1.json")),
4003
- * ];
4004
- * await firstBatchStarted.promise;
4005
- * expect(started).toEqual([
4006
- * "a.example/1.json",
4007
- * "a.example/2.json",
4008
- * "b.example/1.json",
4009
- * ]);
4010
- *
4011
- * finishDownloads.open();
4012
- * expect((await Promise.all(downloads)).map(getOk)).toEqual([
4013
- * "a.example/1.json",
4014
- * "a.example/2.json",
4015
- * "a.example/3.json",
4016
- * "b.example/1.json",
4017
- * ]);
4018
- * ```
4019
- *
4020
3958
  * @group Concurrency primitives
4021
3959
  */
4022
3960
  export interface SemaphoreByKey<K = unknown> {
@@ -4039,56 +3977,40 @@ export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<K, L
4039
3977
  /**
4040
3978
  * Creates a {@link SemaphoreByKey}.
4041
3979
  *
4042
- * @group Concurrency primitives
4043
- */
4044
- export declare function createSemaphoreByKey<K = unknown>(initialPermits: Int1To100OrPositiveInt, options?: CreateSemaphoreByKeyOptions<K, unknown>): SemaphoreByKey<K>;
4045
- export declare function createSemaphoreByKey<K, L>(initialPermits: Int1To100OrPositiveInt, options: CreateSemaphoreByKeyOptions<K, L>): SemaphoreByKey<K>;
4046
- /**
4047
- * Runs Tasks one at a time independently for each key, like {@link Mutex}.
4048
- *
4049
3980
  * ### Example
4050
3981
  *
4051
3982
  * ```ts
4052
3983
  * import {
4053
- * createGate,
4054
- * createMutexByKey,
4055
3984
  * createRun,
3985
+ * createSemaphoreByKey,
4056
3986
  * ok,
4057
3987
  * type Task,
4058
3988
  * } from "@evolu/common";
4059
3989
  *
4060
- * const accountLocks = createMutexByKey<string>();
4061
- * const finishDeposits = createGate();
4062
- * const firstBatchStarted = Promise.withResolvers<void>();
4063
- * const started: Array<string> = [];
4064
- * const balancesByAccount = new Map<string, number>();
4065
- * const deposit = (account: string, amount: number): Task<number> =>
4066
- * accountLocks.withLock(account, async (run) => {
4067
- * started.push(account);
4068
- * if (started.length === 2) firstBatchStarted.resolve();
4069
- * await run.ok(finishDeposits.wait);
4070
- * const balance = (balancesByAccount.get(account) ?? 0) + amount;
4071
- * balancesByAccount.set(account, balance);
4072
- * return ok(balance);
4073
- * });
3990
+ * // Each host gets an independent two-download limit.
3991
+ * const downloadsByHost = createSemaphoreByKey<string>(2);
3992
+ * const download = (host: string, file: string): Task<string> =>
3993
+ * downloadsByHost.withPermit(host, () => ok(`${host}/${file}`));
4074
3994
  *
4075
3995
  * await using run = createRun();
4076
- * const first = run(deposit("checking", 2));
4077
- * const second = run(deposit("checking", 3));
4078
- * const third = run(deposit("savings", 4));
4079
- * await firstBatchStarted.promise;
4080
- * // Different accounts proceed together; the second checking deposit waits.
4081
- * expect(started).toEqual(["checking", "savings"]);
4082
- *
4083
- * finishDeposits.open();
4084
- * expectOk(await first, 2);
4085
- * expectOk(await second, 5);
4086
- * expectOk(await third, 4);
4087
- * expect(started).toEqual(["checking", "savings", "checking"]);
3996
+ * expectOk(
3997
+ * await run(download("a.example", "index.json")),
3998
+ * "a.example/index.json",
3999
+ * );
4000
+ * expect(downloadsByHost.isIdle("a.example")).toBe(true);
4088
4001
  * ```
4089
4002
  *
4090
4003
  * @group Concurrency primitives
4091
4004
  */
4005
+ export declare function createSemaphoreByKey<K = unknown>(initialPermits: Int1To100OrPositiveInt, options?: CreateSemaphoreByKeyOptions<K, unknown>): SemaphoreByKey<K>;
4006
+ /** Creates a {@link SemaphoreByKey} with custom logical key lookup. */
4007
+ export declare function createSemaphoreByKey<K, L>(initialPermits: Int1To100OrPositiveInt, options: CreateSemaphoreByKeyOptions<K, L>): SemaphoreByKey<K>;
4008
+ /**
4009
+ * Runs {@link Task}s one at a time independently for each key, like
4010
+ * {@link Mutex}.
4011
+ *
4012
+ * @group Concurrency primitives
4013
+ */
4092
4014
  export interface MutexByKey<K = unknown> {
4093
4015
  /** Runs a {@link Task} while holding the lock for the key. */
4094
4016
  readonly withLock: <T, E, D>(key: K, task: Task<T, E, D>) => Task<T, E, D>;
@@ -4107,9 +4029,35 @@ export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {
4107
4029
  /**
4108
4030
  * Creates a {@link MutexByKey}.
4109
4031
  *
4032
+ * ### Example
4033
+ *
4034
+ * ```ts
4035
+ * import {
4036
+ * createMutexByKey,
4037
+ * createRun,
4038
+ * ok,
4039
+ * type Task,
4040
+ * } from "@evolu/common";
4041
+ *
4042
+ * const accountLocks = createMutexByKey<string>();
4043
+ * const balancesByAccount = new Map<string, number>();
4044
+ * const deposit = (account: string, amount: number): Task<number> =>
4045
+ * accountLocks.withLock(account, () => {
4046
+ * const balance = (balancesByAccount.get(account) ?? 0) + amount;
4047
+ * balancesByAccount.set(account, balance);
4048
+ * return ok(balance);
4049
+ * });
4050
+ *
4051
+ * await using run = createRun();
4052
+ * expectOk(await run(deposit("checking", 2)), 2);
4053
+ * expectOk(await run(deposit("checking", 3)), 5);
4054
+ * expect(accountLocks.isIdle("checking")).toBe(true);
4055
+ * ```
4056
+ *
4110
4057
  * @group Concurrency primitives
4111
4058
  */
4112
4059
  export declare function createMutexByKey<K = unknown>(options?: CreateMutexByKeyOptions<K, unknown>): MutexByKey<K>;
4060
+ /** Creates a {@link MutexByKey} with custom logical key lookup. */
4113
4061
  export declare function createMutexByKey<K, L>(options: CreateMutexByKeyOptions<K, L>): MutexByKey<K>;
4114
4062
  /**
4115
4063
  * {@link Ref} protected by a {@link Mutex}.
@@ -4122,57 +4070,13 @@ export declare function createMutexByKey<K, L>(options: CreateMutexByKeyOptions<
4122
4070
  * internal Mutex, so calling another method on the same MutexRef from inside
4123
4071
  * one of them waits on itself and will not progress.
4124
4072
  *
4125
- * Use it for state whose transitions are Tasks: atomic async read-modify-write.
4126
- * Plain Ref cannot express that — between a sync read and a later write, a
4127
- * concurrent transition can interleave and get lost.
4073
+ * Use it for state whose transitions are {@link Task}s: atomic async
4074
+ * read-modify-write. Plain Ref cannot express that — between a sync read and a
4075
+ * later write, a concurrent transition can interleave and get lost.
4128
4076
  *
4129
- * `MutexRef` operations are Tasks and incur normal Run lifecycle overhead. Use
4130
- * {@link Ref} instead for synchronous state transitions, especially on
4131
- * allocation-sensitive hot paths.
4132
- *
4133
- * ### Example
4134
- *
4135
- * ```ts
4136
- * import {
4137
- * createGate,
4138
- * createMutexRef,
4139
- * createRun,
4140
- * ok,
4141
- * type Task,
4142
- * } from "@evolu/common";
4143
- *
4144
- * await using run = createRun();
4145
- *
4146
- * const finishRefresh = createGate();
4147
- * const refreshStarted = Promise.withResolvers<void>();
4148
- * let fetchTokenCalls = 0;
4149
- * const fetchToken: Task<string> = async (run) => {
4150
- * fetchTokenCalls += 1;
4151
- * refreshStarted.resolve();
4152
- * await run.ok(finishRefresh.wait);
4153
- * return ok("fresh-token");
4154
- * };
4155
- *
4156
- * const tokenRef = createMutexRef<string | null>(null);
4157
- *
4158
- * // Concurrent callers never trigger duplicate refreshes: the first caller
4159
- * // runs fetchToken under the lock; later callers reuse the stored token.
4160
- * const getToken = tokenRef.updateAndGet((current) =>
4161
- * current === null ? fetchToken : () => ok(current),
4162
- * );
4163
- *
4164
- * const first = run(getToken);
4165
- * await refreshStarted.promise;
4166
- * const second = run(getToken);
4167
- * finishRefresh.open();
4168
- *
4169
- * const [firstResult, secondResult] = await Promise.all([first, second]);
4170
- * expectTypeOf(getToken).toEqualTypeOf<Task<string | null>>();
4171
- * expectOk(firstResult, "fresh-token");
4172
- * expectOk(secondResult, "fresh-token");
4173
- * expect(fetchTokenCalls).toBe(1);
4174
- * expectOk(await run(tokenRef.get), "fresh-token");
4175
- * ```
4077
+ * `MutexRef` operations are Tasks and incur normal {@link Run} lifecycle
4078
+ * overhead. Use {@link Ref} instead for synchronous state transitions,
4079
+ * especially on allocation-sensitive hot paths.
4176
4080
  *
4177
4081
  * @group Concurrency primitives
4178
4082
  * @see {@link createMutexRef}
@@ -4200,6 +4104,19 @@ export interface MutexRef<T> {
4200
4104
  /**
4201
4105
  * Creates a {@link MutexRef}.
4202
4106
  *
4107
+ * ### Example
4108
+ *
4109
+ * ```ts
4110
+ * import { createMutexRef, createRun, ok } from "@evolu/common";
4111
+ *
4112
+ * const counter = createMutexRef(0);
4113
+ * const increment = counter.updateAndGet((value) => () => ok(value + 1));
4114
+ *
4115
+ * await using run = createRun();
4116
+ * expectOk(await run(increment), 1);
4117
+ * expectOk(await run(counter.get), 1);
4118
+ * ```
4119
+ *
4203
4120
  * @group Concurrency primitives
4204
4121
  */
4205
4122
  export declare const createMutexRef: <T>(initialValue: T) => MutexRef<T>;