@evolu/common 8.2.0 → 8.3.0

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