@evolu/common 8.2.0 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1322 -793
  24. package/dist/src/Type.d.ts.map +1 -1
  25. package/dist/src/Type.js +1192 -244
  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 +1759 -794
  176. package/src/intl/_en.ts +497 -0
  177. package/src/intl/ar.ts +417 -0
  178. package/src/intl/bn.ts +486 -0
  179. package/src/intl/ca.ts +488 -0
  180. package/src/intl/cs.ts +95 -31
  181. package/src/intl/da.ts +488 -0
  182. package/src/intl/de.ts +488 -0
  183. package/src/intl/el.ts +494 -0
  184. package/src/intl/es.ts +488 -0
  185. package/src/intl/fa.ts +487 -0
  186. package/src/intl/fi.ts +487 -0
  187. package/src/intl/fil.ts +488 -0
  188. package/src/intl/fr.ts +488 -0
  189. package/src/intl/he.ts +486 -0
  190. package/src/intl/hi.ts +487 -0
  191. package/src/intl/hr.ts +494 -0
  192. package/src/intl/hu.ts +387 -0
  193. package/src/intl/id.ts +487 -0
  194. package/src/intl/index.ts +42 -0
  195. package/src/intl/it.ts +488 -0
  196. package/src/intl/ja.ts +488 -0
  197. package/src/intl/ko.ts +487 -0
  198. package/src/intl/ml.ts +487 -0
  199. package/src/intl/mr.ts +487 -0
  200. package/src/intl/ms.ts +410 -0
  201. package/src/intl/nb.ts +364 -0
  202. package/src/intl/nl.ts +488 -0
  203. package/src/intl/pa.ts +487 -0
  204. package/src/intl/pl.ts +450 -0
  205. package/src/intl/pt-BR.ts +487 -0
  206. package/src/intl/pt.ts +424 -0
  207. package/src/intl/ro.ts +488 -0
  208. package/src/intl/sk.ts +412 -0
  209. package/src/intl/sl.ts +487 -0
  210. package/src/intl/sv.ts +488 -0
  211. package/src/intl/sw.ts +324 -0
  212. package/src/intl/ta.ts +487 -0
  213. package/src/intl/te.ts +487 -0
  214. package/src/intl/th.ts +484 -0
  215. package/src/intl/tr.ts +487 -0
  216. package/src/intl/uk.ts +488 -0
  217. package/src/intl/ur.ts +410 -0
  218. package/src/intl/vi.ts +435 -0
  219. package/src/intl/zh-CN.ts +482 -0
  220. package/src/intl/zh-TW.ts +482 -0
  221. package/src/local-first/Owner.ts +156 -62
package/dist/src/Task.js CHANGED
@@ -51,8 +51,650 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
53
  /**
54
+ * ## Intro
55
+ *
54
56
  * JavaScript-native structured concurrency.
55
57
  *
58
+ * Structured concurrency makes ownership of asynchronous work explicit.
59
+ * Operations form a tree where every child belongs to a parent. A parent waits
60
+ * for its children before it completes, and abort follows the tree: aborting a
61
+ * parent requests abort of all its descendants. Races and fail-fast operations
62
+ * also abort their remaining sibling branches.
63
+ *
64
+ * With plain {@link AbortController} code, these guarantees depend on call-site
65
+ * discipline: someone must remember the `finally` that aborts started work and
66
+ * the await that waits for cleanup. {@link Run} makes both structural:
67
+ * `run(task)` registers every child before it starts, and the parent settles
68
+ * only after child cleanup finishes.
69
+ *
70
+ * Evolu models structured concurrency with ordinary JavaScript:
71
+ *
72
+ * - A {@link Task} describes an asynchronous operation and its dependencies.
73
+ * - A {@link Run} starts Tasks and owns their lifetimes.
74
+ * - A {@link Fiber} is the Promise-backed handle returned when a Run starts a
75
+ * Task.
76
+ * - An {@link AbortableFiber} adds explicit abort and async disposal.
77
+ *
78
+ * The runtime core is deliberately small: ordinary functions, a callable Run
79
+ * with closed-over state, Promise-backed Fibers, {@link AbortSignal}
80
+ * propagation, and JavaScript resource management. Together, these primitives
81
+ * provide abort, cleanup, defect handling, dependency injection, monitoring,
82
+ * concurrency, and resource bracketing.
83
+ *
84
+ * Tasks return domain success or failure as {@link Result}. Abort is control
85
+ * flow represented by {@link AbortError}. If a Task throws or rejects with
86
+ * anything else, that is a defect: the root Run reports it and shuts down its
87
+ * tree so code does not continue in a potentially invalid state.
88
+ *
89
+ * ```ts
90
+ * import {
91
+ * createRun,
92
+ * err,
93
+ * ok,
94
+ * type Result,
95
+ * type Task,
96
+ * type Typed,
97
+ * } from "@evolu/common";
98
+ *
99
+ * interface User {
100
+ * readonly id: string;
101
+ * readonly name: string;
102
+ * }
103
+ *
104
+ * interface Db {
105
+ * readonly usersById: ReadonlyMap<string, User>;
106
+ * }
107
+ *
108
+ * interface DbDep {
109
+ * readonly db: Db;
110
+ * }
111
+ *
112
+ * const getUser =
113
+ * (id: string): Task<User, UserNotFoundError, DbDep> =>
114
+ * (run) => {
115
+ * const user = run.deps.db.usersById.get(id);
116
+ * return user ? ok(user) : err({ type: "UserNotFound", id });
117
+ * };
118
+ *
119
+ * // Typed declares the `type` discriminant without repeating the property.
120
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {
121
+ * readonly id: string;
122
+ * }
123
+ *
124
+ * const user: User = { id: "user-1", name: "Ada" };
125
+ *
126
+ * // Provide dependencies at the composition root. `await using` disposes the
127
+ * // Run and waits for its child Tasks before leaving this scope.
128
+ * await using run = createRun({
129
+ * db: { usersById: new Map([[user.id, user]]) },
130
+ * });
131
+ *
132
+ * const result = await run(getUser(user.id));
133
+ * expectTypeOf(result).toEqualTypeOf<Result<User, UserNotFoundError>>();
134
+ * expectOk(result, user);
135
+ * ```
136
+ *
137
+ * In composition roots, prefer the lifecycle API from the matching Evolu
138
+ * platform package:
139
+ *
140
+ * - Node.js: `@evolu/nodejs`
141
+ * - Web: `@evolu/web`
142
+ * - React Native: `@evolu/react-native`
143
+ *
144
+ * ## Composition
145
+ *
146
+ * | Category | Helper | Description |
147
+ * | ------------ | ------------------------- | ----------------------------------------------------- |
148
+ * | Collection | {@link all} | Return {@link Ok} values or stop on first {@link Err} |
149
+ * | | {@link allSettled} | Return every Task Result |
150
+ * | | {@link each} | Handle each Task Result |
151
+ * | Interop | {@link callback} | Wrap callback APIs |
152
+ * | | {@link fetch} | Native fetch with bounded Response use |
153
+ * | Timing | {@link sleep} | Pause execution |
154
+ * | | {@link timeout} | Time-bounded execution |
155
+ * | Resilience | {@link retry} | Retry domain errors with a schedule |
156
+ * | Repetition | {@link repeat} | Repeat successes with a schedule |
157
+ * | Racing | {@link any} | First Ok wins |
158
+ * | | {@link race} | First settled Result wins |
159
+ * | | {@link firstN} | First n Ok values win |
160
+ * | | {@link firstNSettled} | First n Results win |
161
+ * | Scheduling | {@link prioritized} | Assign scheduler priority |
162
+ * | | {@link yieldNow} | Yield to the host scheduler |
163
+ * | Lifetime | {@link daemon} | Run under root ownership |
164
+ * | | {@link acquireUseRelease} | Bracket acquire, use, and release |
165
+ * | Abortability | {@link unabortable} | Mask abort after a Task starts |
166
+ * | | {@link unabortableMask} | Mask abort and selectively restore it |
167
+ *
168
+ * Helpers that process multiple Tasks run sequentially by default. Use a
169
+ * `concurrency` option to run more than one Task at a time.
170
+ *
171
+ * ### Sequential composition
172
+ *
173
+ * For ordinary sequential composition, use imperative code:
174
+ *
175
+ * ```ts
176
+ * import {
177
+ * createRun,
178
+ * err,
179
+ * ok,
180
+ * type Result,
181
+ * type Task,
182
+ * type Typed,
183
+ * } from "@evolu/common";
184
+ *
185
+ * interface User {
186
+ * readonly id: string;
187
+ * readonly profileId: string;
188
+ * }
189
+ *
190
+ * interface Profile {
191
+ * readonly id: string;
192
+ * }
193
+ *
194
+ * const getUser =
195
+ * (id: string): Task<User, UserNotFoundError> =>
196
+ * () =>
197
+ * id === "user-1"
198
+ * ? ok({ id, profileId: "profile-1" })
199
+ * : err({ type: "UserNotFound", id });
200
+ *
201
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {
202
+ * readonly id: string;
203
+ * }
204
+ *
205
+ * const getProfile =
206
+ * (id: string): Task<Profile, ProfileNotFoundError> =>
207
+ * () =>
208
+ * id === "profile-1"
209
+ * ? ok({ id })
210
+ * : err({ type: "ProfileNotFound", id });
211
+ *
212
+ * interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {
213
+ * readonly id: string;
214
+ * }
215
+ *
216
+ * const getUserWithProfile =
217
+ * (
218
+ * id: string,
219
+ * ): Task<
220
+ * { readonly user: User; readonly profile: Profile },
221
+ * UserNotFoundError | ProfileNotFoundError
222
+ * > =>
223
+ * async (run) => {
224
+ * const user = await run(getUser(id));
225
+ * if (!user.ok) return user;
226
+ *
227
+ * const profile = await run(getProfile(user.value.profileId));
228
+ * if (!profile.ok) return profile;
229
+ *
230
+ * return ok({ user: user.value, profile: profile.value });
231
+ * };
232
+ *
233
+ * await using run = createRun();
234
+ * const result = await run(getUserWithProfile("user-1"));
235
+ * expectTypeOf(result).toEqualTypeOf<
236
+ * Result<
237
+ * { readonly user: User; readonly profile: Profile },
238
+ * UserNotFoundError | ProfileNotFoundError
239
+ * >
240
+ * >();
241
+ * expectOk(result, {
242
+ * user: { id: "user-1", profileId: "profile-1" },
243
+ * profile: { id: "profile-1" },
244
+ * });
245
+ * ```
246
+ *
247
+ * Evolu intentionally avoids pipe APIs, chainable methods, and generator-based
248
+ * effect DSLs. Plain async/await with early returns is easier to read, review,
249
+ * and debug, and it lets TypeScript narrow Result values through ordinary
250
+ * control flow.
251
+ *
252
+ * ### Resilient fetch
253
+ *
254
+ * {@link fetch} with a body mode already returns a plain value, so resilience is
255
+ * ordinary Task composition. Combine {@link timeout} and {@link retry} to bound
256
+ * each attempt and retry recoverable domain failures:
257
+ *
258
+ * ```ts
259
+ * import {
260
+ * exponential,
261
+ * fetch,
262
+ * jitter,
263
+ * maxDelay,
264
+ * retry,
265
+ * take,
266
+ * timeout,
267
+ * type FetchError,
268
+ * type RetryTaskError,
269
+ * type Task,
270
+ * type TimeoutError,
271
+ * } from "@evolu/common";
272
+ *
273
+ * const fetchWithRetry = (url: string) =>
274
+ * retry(
275
+ * timeout(fetch(url, "text"), "30s"),
276
+ * // A jittered, capped, limited exponential backoff.
277
+ * jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
278
+ * );
279
+ *
280
+ * expectTypeOf(fetchWithRetry).returns.toEqualTypeOf<
281
+ * Task<string, RetryTaskError<FetchError | TimeoutError>>
282
+ * >();
283
+ * ```
284
+ *
285
+ * ### Concurrent composition
286
+ *
287
+ * Run composed Tasks with a `concurrency` option and {@link all}:
288
+ *
289
+ * ```ts
290
+ * import { all, createRun, ok, sleep, type Task } from "@evolu/common";
291
+ *
292
+ * await using run = createRun();
293
+ *
294
+ * const urls = [
295
+ * "https://api.example.com/users",
296
+ * "https://api.example.com/posts",
297
+ * "https://api.example.com/comments",
298
+ * ];
299
+ * let activeRequests = 0;
300
+ * let maxActiveRequests = 0;
301
+ * const fetchUrl =
302
+ * (url: string): Task<string> =>
303
+ * async (run) => {
304
+ * activeRequests += 1;
305
+ * maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
306
+ * await run.ok(sleep("1ms"));
307
+ * activeRequests -= 1;
308
+ * return ok(url);
309
+ * };
310
+ *
311
+ * // At most 2 concurrent requests.
312
+ * const result = await run(all(urls, fetchUrl, { concurrency: 2 }));
313
+ * expectOk(result, urls);
314
+ * expect(maxActiveRequests).toBe(2);
315
+ * ```
316
+ *
317
+ * Task helpers compose Tasks; concurrency primitives are stateful objects that
318
+ * coordinate Tasks across call sites. Create them with their `createX`
319
+ * factories and share them where coordination is needed.
320
+ *
321
+ * | Primitive | Description |
322
+ * | ---------------------- | -------------------------------------------- |
323
+ * | {@link Deferred} | One-shot value resolved from outside |
324
+ * | {@link Gate} | Block and release Tasks repeatedly |
325
+ * | {@link Semaphore} | Limit concurrent Tasks with permits |
326
+ * | {@link Mutex} | Run Tasks one at a time |
327
+ * | {@link SemaphoreByKey} | Per-key permits with automatic cleanup |
328
+ * | {@link MutexByKey} | Per-key one-at-a-time execution |
329
+ * | {@link MutexRef} | {@link Ref} with serialized Task transitions |
330
+ *
331
+ * ## Dependency injection
332
+ *
333
+ * Task DI is
334
+ * {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}
335
+ * applied to {@link Run}. A {@link Task} declares required capabilities with its
336
+ * `D` type parameter and reads them from {@link Run.deps}.
337
+ *
338
+ * {@link createRun} supplies dependencies to the root Run and its children. A
339
+ * Run can also start one child Task with runtime-created dependencies by
340
+ * calling `run(task, deps)`, where `deps` is checked as {@link RunCustomDeps}.
341
+ *
342
+ * Use normal Task arguments for per-call values and `D` for capabilities,
343
+ * resources, or services shared by all code running inside a Run.
344
+ *
345
+ * ```ts
346
+ * import { createRun, ok, type Task } from "@evolu/common";
347
+ *
348
+ * interface GreetingFormatter {
349
+ * readonly format: (name: string) => string;
350
+ * }
351
+ *
352
+ * interface GreetingFormatterDep {
353
+ * readonly greetingFormatter: GreetingFormatter;
354
+ * }
355
+ *
356
+ * const greet =
357
+ * (name: string): Task<string, never, GreetingFormatterDep> =>
358
+ * (run) =>
359
+ * ok(run.deps.greetingFormatter.format(name));
360
+ *
361
+ * const formal: GreetingFormatter = {
362
+ * format: (name) => `Hello, ${name}`,
363
+ * };
364
+ * const casual: GreetingFormatter = {
365
+ * format: (name) => `Hi, ${name}`,
366
+ * };
367
+ *
368
+ * await using run = createRun({ greetingFormatter: formal });
369
+ *
370
+ * // Root dependencies are inherited.
371
+ * expectOk(await run(greet("Ada")), "Hello, Ada");
372
+ *
373
+ * // Child-specific dependencies replace the root's custom dependencies.
374
+ * expectOk(
375
+ * await run(greet("Ada"), { greetingFormatter: casual }),
376
+ * "Hi, Ada",
377
+ * );
378
+ * ```
379
+ *
380
+ * ### Default dependencies
381
+ *
382
+ * {@link createRun} provides default {@link RunDefaultDeps} available to all
383
+ * Tasks without declaring `D`:
384
+ *
385
+ * - {@link Console} — logging with hierarchical context via `child()`
386
+ * - {@link LeakDetector} — development-time leaked-handle detection
387
+ * - {@link NativeFetch} — WHATWG-compatible native fetch
388
+ * - {@link Random} — random number generation
389
+ * - {@link RandomBytes} — cryptographic random bytes
390
+ * - {@link ReportDefect} — defect reporting
391
+ * - {@link Time} — current time
392
+ *
393
+ * For testing, use {@link testCreateRun} to get deterministic, controllable
394
+ * implementations of all RunDefaultDeps.
395
+ *
396
+ * ## Resource management
397
+ *
398
+ * JavaScript provides standard
399
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
400
+ * Evolu adds {@link DisposableRun.defer} for closure-held state owned by a
401
+ * reusable Run.
402
+ *
403
+ * Choose the ownership primitive by where the resource is reachable:
404
+ *
405
+ * - Synchronous stack frame: `using` or {@link DisposableStack}
406
+ * - Async Task stack frame: `await using` or {@link AsyncDisposableStack}
407
+ * - Closure-held state bounded by a reusable {@link DisposableRun}:
408
+ * {@link DisposableRun.defer}
409
+ *
410
+ * ### Returning resources from Tasks
411
+ *
412
+ * A Task that successfully returns a disposable resource transfers ownership of
413
+ * a live resource to its caller. The resource must remain live after the Task
414
+ * settles. Do not register its disposal with the creating Task's
415
+ * {@link DisposableRun.defer}, because that child Run is disposed when the Task
416
+ * settles.
417
+ *
418
+ * Use {@link AsyncDisposableStack} while creating a resource. On a Result error,
419
+ * abort, or defect, stack unwinding disposes partially created resources. On
420
+ * success, {@link AsyncDisposableStack.move} transfers ownership to the returned
421
+ * resource. A recoverable creation failure should be a typed Result error;
422
+ * `undefined` should represent valid absence, not failure.
423
+ *
424
+ * ```ts
425
+ * import { createRun, ok, type Task, type Typed } from "@evolu/common";
426
+ *
427
+ * interface Socket extends AsyncDisposable {
428
+ * readonly send: (message: string) => string;
429
+ * }
430
+ *
431
+ * interface Connection extends AsyncDisposable {
432
+ * readonly send: (message: string) => string;
433
+ * }
434
+ *
435
+ * let socketDisposed = false;
436
+ * const openSocket: Task<Socket, ConnectionFailedError> = () =>
437
+ * ok({
438
+ * send: (message) => message,
439
+ * [Symbol.asyncDispose]: async () => {
440
+ * socketDisposed = true;
441
+ * },
442
+ * });
443
+ *
444
+ * interface ConnectionFailedError extends Typed<"ConnectionFailed"> {}
445
+ *
446
+ * const handshake =
447
+ * (_socket: Socket): Task<void, ConnectionFailedError> =>
448
+ * () =>
449
+ * ok();
450
+ *
451
+ * const createConnection: Task<Connection, ConnectionFailedError> = async (
452
+ * run,
453
+ * ) => {
454
+ * await using disposer = new AsyncDisposableStack();
455
+ *
456
+ * const socketResult = await run(openSocket);
457
+ * if (!socketResult.ok) return socketResult;
458
+ * const socket = disposer.use(socketResult.value);
459
+ *
460
+ * const handshakeResult = await run(handshake(socket));
461
+ * if (!handshakeResult.ok) return handshakeResult;
462
+ *
463
+ * const disposables = disposer.move();
464
+ * return ok({
465
+ * send: (message) => socket.send(message),
466
+ * [Symbol.asyncDispose]: () => disposables.disposeAsync(),
467
+ * });
468
+ * };
469
+ *
470
+ * await using run = createRun();
471
+ * const result = await run(createConnection);
472
+ * assert(result.ok);
473
+ * expect(socketDisposed).toBe(false);
474
+ * expect(result.value.send("hello")).toBe("hello");
475
+ * await result.value[Symbol.asyncDispose]();
476
+ * expect(socketDisposed).toBe(true);
477
+ * ```
478
+ *
479
+ * Use {@link Run.ok} with `await using` when an infallible Task returns a
480
+ * disposable value. Use {@link acquireUseRelease} when acquisition and release
481
+ * are separate operations rather than a disposable value.
482
+ *
483
+ * ## Awaitable
484
+ *
485
+ * A {@link Task} returns {@link Awaitable}, so its body may produce a
486
+ * {@link Result} immediately or asynchronously. {@link Run} is always async and
487
+ * returns a {@link Fiber}; callers use the same ownership model either way.
488
+ *
489
+ * - **Sync** → {@link Result}, native `using` / `DisposableStack`
490
+ * - **Async** → Task, Run, {@link Fiber}, `await using` / `AsyncDisposableStack`
491
+ *
492
+ * A Task is an async ownership boundary, not a general unit of program
493
+ * decomposition. Calling `run(task)` always creates a child Run by design. Use
494
+ * ordinary promises when an async operation does not need its own Run.
495
+ *
496
+ * A unified sync/async effect API is technically possible. It can detect
497
+ * Promise-like values with {@link isPromiseLike}, dispose synchronous resources
498
+ * first, continue with asynchronous disposal when necessary, and track whether
499
+ * callers must await the result. Evolu deliberately keeps the two models
500
+ * separate instead: plain functions and Result for synchronous code, Task and
501
+ * Run for asynchronous ownership. Most effects involve inherently asynchronous
502
+ * I/O, while synchronous code benefits from a simpler API and no Task
503
+ * overhead.
504
+ *
505
+ * Keep synchronous computation as plain functions returning Result. Prefer
506
+ * passing values rather than dependencies, following the
507
+ * {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
508
+ * pattern where impure code gathers data, pure functions process it, and impure
509
+ * code performs effects with the result. For example, a pure function can
510
+ * accept a {@link RandomNumber} value instead of depending on {@link Random}.
511
+ *
512
+ * Large CPU-bound operations, such as parsing large JSON, sorting millions of
513
+ * items, or complex cryptography, belong in a worker. Model the asynchronous
514
+ * call to that worker as a Task so Run can provide timeout, abort, cleanup, and
515
+ * monitoring.
516
+ *
517
+ * ## Glossary
518
+ *
519
+ * - **Defect** — a thrown or rejected value other than {@link AbortError}, rather
520
+ * than a declared {@link Result} error.
521
+ * - **Outcome** — a Fiber's settlement: resolution with the Task {@link Result},
522
+ * or rejection with {@link AbortError}. The original defect is reported
523
+ * through {@link ReportDefectDep} whether or not the Fiber is observed; the
524
+ * Fiber boundary represents the panic with AbortError whose reason is
525
+ * {@link PanicAbortReason}.
526
+ * - **Create** — construct a new value or a resource.
527
+ * - **Acquire** — obtain a usable resource. Acquisition may create a new
528
+ * resource, borrow one, open one, or take a lease/lock.
529
+ * - **Release** — relinquish a previously acquired resource or lease. Release
530
+ * pairs with acquire and need not mean disposal; examples include unlock,
531
+ * logout, or returning a pooled resource.
532
+ * - **Dispose / disposal** — owner-driven resource finalization via JavaScript
533
+ * resource management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
534
+ * `AsyncDisposableStack`).
535
+ *
536
+ * ## FAQ
537
+ *
538
+ * ### Why is AbortError not part of every Task error type?
539
+ *
540
+ * The `E` type parameter represents declared domain errors. Abort is
541
+ * structured-concurrency control flow, not a domain error. A direct `run(task)`
542
+ * rejects with {@link AbortError} when the Task observes abort. Use
543
+ * `run.abortable(task)` when abort should be handled as an ordinary
544
+ * {@link Result} error at the Fiber boundary, or `daemon(task)` when waiting for
545
+ * a Task should stop immediately after abort.
546
+ *
547
+ * ### Do I have to await every Fiber?
548
+ *
549
+ * No. Awaiting a Fiber is join: it makes the child outcome part of the current
550
+ * control flow. When the outcome does not matter — a fire-and-forget side
551
+ * effect — discard the Fiber explicitly with `void run(task)`.
552
+ *
553
+ * That is safe because the Run tree supervises every Fiber it creates. A
554
+ * discarded Fiber whose Task observes abort (for example during Run disposal)
555
+ * never surfaces as an unhandled rejection, and cleanup is not lost — disposal
556
+ * already aborts and awaits the child. Defects are different: they still panic
557
+ * the root Run and are reported through {@link ReportDefectDep}, so discarding a
558
+ * Fiber never hides bugs.
559
+ *
560
+ * Choose the boundary explicitly:
561
+ *
562
+ * - `void run(task)` — the outcome does not matter. Abort is silent; defects are
563
+ * still reported.
564
+ * - `await run(task)` — the continuation depends on the Result, so abort rejects
565
+ * into the awaiter and the boundary must handle it.
566
+ * - `run.abortable(task)` — abort is an expected outcome handled as a
567
+ * {@link Result} error.
568
+ *
569
+ * ### What should Task code do with defects?
570
+ *
571
+ * Nothing. Once a defect reaches the {@link Run}, it is too late: the root Run
572
+ * panics, running Tasks are aborted, and the Run tree shuts down. If an
573
+ * operation can throw or reject for a recoverable reason, wrap that operation
574
+ * with {@link trySync} or {@link tryAsync} so the failure becomes a typed
575
+ * {@link Result} error. Let unrecoverable failures propagate as defects.
576
+ *
577
+ * ### Why does a defect panic the whole Run tree?
578
+ *
579
+ * The obvious alternative is partial recovery: only the failing subtree shuts
580
+ * down or restarts while the rest keeps running. Erlang/OTP made this "let it
581
+ * crash" model with supervisors the benchmark for fault-tolerant runtime
582
+ * design.
583
+ *
584
+ * Erlang can recover partially because of process isolation: each process owns
585
+ * its heap, so a crashed process cannot leave another process's state
586
+ * corrupted. JavaScript Tasks share a heap. A defect may throw after partially
587
+ * updating shared state, and the Run cannot prove which invariants are still
588
+ * valid. A subtree panic would stop the failing Task while leaving any
589
+ * corrupted shared state available to surviving Tasks. Locks make it worse: a
590
+ * defect inside a critical section may leave protected invariants half-updated.
591
+ * In-process restart is not a reliable recovery boundary either, because the
592
+ * restarted code may still share the same module state, closures, caches, or
593
+ * resources.
594
+ *
595
+ * JavaScript does have a boundary with Erlang-like isolation: workers. A worker
596
+ * has its own heap and structured-clone messaging, so corruption cannot cross
597
+ * the boundary, and respawning a worker starts from clean state. A defect can
598
+ * panic the worker's Run tree, the worker boundary can be torn down, and the
599
+ * supervising side decides whether to respawn — {@link retry} with a
600
+ * {@link Schedule} around a "spawn worker, run until exit" Task is a one-for-one
601
+ * supervisor. Multiple root Runs that share no mutable state are a lighter
602
+ * alternative, but the share-nothing guarantee is then architectural discipline
603
+ * rather than enforced isolation, so keep it opt-in and rare.
604
+ *
605
+ * ### Why imperative code instead of monadic effect composition?
606
+ *
607
+ * Monads give pure functional languages a way to sequence effects while keeping
608
+ * functions pure. JavaScript already has native effect sequencing: loops, early
609
+ * returns, `try`/`finally`, exceptions, and `async`/`await`.
610
+ *
611
+ * A monadic effect wrapper moves that control flow into a library DSL. The
612
+ * wrapper type becomes viral, and ordinary debugging, profiling, stack traces,
613
+ * and TypeScript narrowing have to work through the DSL instead of the
614
+ * language.
615
+ *
616
+ * Task follows the opposite approach: Tasks are ordinary async functions, Run
617
+ * owns lifetimes and scoped context, {@link Result} carries expected domain
618
+ * errors, and defects keep real exceptions with real stacks. Result propagation
619
+ * is explicit at each async boundary, so TypeScript narrows it through ordinary
620
+ * control flow and readers can see where an error is handled or returned.
621
+ *
622
+ * ### Are recursive Tasks stack-safe?
623
+ *
624
+ * Tasks have native JavaScript stack behavior. A deeply recursive Task can
625
+ * exceed the call stack when each step starts the next step synchronously.
626
+ * `await` alone does not prevent this: JavaScript evaluates its operand before
627
+ * suspending, and `run(nextTask)` starts the child Task immediately.
628
+ *
629
+ * Implement deep recursive algorithms with a loop and an explicit worklist so
630
+ * each iteration reuses the same stack frame:
631
+ *
632
+ * ```ts
633
+ * import { createRun, ok, type Task } from "@evolu/common";
634
+ *
635
+ * interface TreeNode {
636
+ * readonly value: string;
637
+ * readonly children: ReadonlyArray<TreeNode>;
638
+ * }
639
+ *
640
+ * const visitTree =
641
+ * (root: TreeNode): Task<ReadonlyArray<string>> =>
642
+ * () => {
643
+ * const remaining = [root];
644
+ * const visited: Array<string> = [];
645
+ *
646
+ * while (remaining.length > 0) {
647
+ * const node = remaining.pop();
648
+ * if (!node) continue;
649
+ * visited.push(node.value);
650
+ * for (const child of node.children) remaining.push(child);
651
+ * }
652
+ *
653
+ * return ok(visited);
654
+ * };
655
+ *
656
+ * await using run = createRun();
657
+ * expectOk(
658
+ * await run(
659
+ * visitTree({
660
+ * value: "root",
661
+ * children: [{ value: "child", children: [] }],
662
+ * }),
663
+ * ),
664
+ * ["root", "child"],
665
+ * );
666
+ * ```
667
+ *
668
+ * Task favors direct native execution, `async`/`await`, and native tooling over
669
+ * interpreted control flow. The trade-off is no transparent stack safety or
670
+ * automatic scheduling fairness. Use loops or worklists for deep algorithms,
671
+ * periodically await {@link yieldNow} for cooperative scheduling, and move
672
+ * CPU-bound work to a worker.
673
+ *
674
+ * ### Should a Task be called directly?
675
+ *
676
+ * Only inside Task internals that explicitly require same-Run execution. A
677
+ * direct call, `task(run)`, uses the current Run instead of creating a child
678
+ * Run, so it bypasses child lifetime tracking, scheduling metadata, and child
679
+ * disposal boundaries. Application code should use `run(task)`.
680
+ *
681
+ * ### Where are fork and join?
682
+ *
683
+ * Calling `run(task)` is fork: it starts a child Task and returns a
684
+ * {@link Fiber}. Awaiting or returning that Fiber is join: it makes the child
685
+ * Result or rejection part of the parent Task control flow.
686
+ *
687
+ * ### What runtime features does Task require?
688
+ *
689
+ * Task uses modern JavaScript APIs such as `Promise.withResolvers`,
690
+ * `AbortSignal.throwIfAborted`, `Symbol.dispose`, `Symbol.asyncDispose`,
691
+ * `DisposableStack`, and `AsyncDisposableStack`. Evolu provides polyfills for
692
+ * supported runtimes that need them: call `installPolyfills` from
693
+ * `@evolu/common/polyfills`, or from the platform package such as
694
+ * `@evolu/react-native/polyfills`. The `using` and `await using` syntax is
695
+ * emitted by TypeScript; the polyfills provide the runtime resource-management
696
+ * globals.
697
+ *
56
698
  * @module
57
699
  */
58
700
  import { emptyArray, isNonEmptyArray, mapArray, } from "./Array.js";
@@ -74,7 +716,7 @@ import { emptySet } from "./Set.js";
74
716
  import { createTime, PositiveMillis, testCreateTime, } from "./Time.js";
75
717
  import { assertType, createId, maxPositiveInt, NonNegativeInt, object, onePositiveInt, PositiveInt, record, String, typed, Unknown, UnknownResult, } from "./Type.js";
76
718
  /**
77
- * Typed object explaining why a {@link Run} was aborted.
719
+ * Runtime Type for structured data explaining why a {@link Run} was aborted.
78
720
  *
79
721
  * A reason has a `type` discriminant and optional structured data, so abort
80
722
  * causes can carry typed domain data. Well-known reasons are
@@ -86,11 +728,11 @@ import { assertType, createId, maxPositiveInt, NonNegativeInt, object, onePositi
86
728
  export const AbortReason = /*#__PURE__*/ object({ type: String },
87
729
  /*#__PURE__*/ record(String, Unknown));
88
730
  /**
89
- * Typed object representing structured-concurrency abort control flow.
731
+ * Runtime Type for structured-concurrency abort control flow.
90
732
  *
91
- * AbortError is thrown to stop Task execution when a Run observes an abort
92
- * request. AbortableFiber catches AbortError and returns it as a {@link Result}
93
- * error, so abort can be handled as an ordinary Task outcome.
733
+ * AbortError is thrown to stop {@link Task} execution when a {@link Run} observes
734
+ * an abort request. {@link AbortableFiber} catches AbortError and returns it as
735
+ * a {@link Result} error, so abort can be handled as an ordinary Task outcome.
94
736
  *
95
737
  * The reason explains why the Run was aborted. It can be an explicit abort
96
738
  * reason, {@link runDisposedAbortReason} for normal Run cleanup, or
@@ -124,9 +766,9 @@ export const createAbortError = (reason) => ({
124
766
  /**
125
767
  * Shared abort reason used for ordinary {@link Run} cleanup.
126
768
  *
127
- * Disposal requests abort so child Tasks stop while the Run waits for them to
128
- * settle. This reason distinguishes that cleanup path from explicit abort and
129
- * {@link PanicAbortReason}.
769
+ * Disposal requests abort so child {@link Task}s stop while the Run waits for
770
+ * them to settle. This reason distinguishes that cleanup path from explicit
771
+ * abort and {@link PanicAbortReason}.
130
772
  *
131
773
  * @group Run
132
774
  */
@@ -146,7 +788,7 @@ export const explicitAbortReason = {
146
788
  /**
147
789
  * Shared abort reason for tests that need a non-production abort reason.
148
790
  *
149
- * @group Run
791
+ * @group Testing
150
792
  */
151
793
  export const testAbortReason = {
152
794
  type: "TestAbortReason",
@@ -154,7 +796,7 @@ export const testAbortReason = {
154
796
  /**
155
797
  * Shared {@link AbortError} for tests, created from {@link testAbortReason}.
156
798
  *
157
- * @group Run
799
+ * @group Testing
158
800
  */
159
801
  export const testAbortError = /*#__PURE__*/ createAbortError(testAbortReason);
160
802
  /**
@@ -203,6 +845,28 @@ export const createRunDefaultDeps = () => {
203
845
  /**
204
846
  * Creates a root {@link DisposableRun}.
205
847
  *
848
+ * Use at composition roots such as app, server, worker, or test entry points.
849
+ * The common factory is platform-agnostic; platform adapters can wrap it to add
850
+ * global error handling or shutdown integration.
851
+ *
852
+ * ### Example
853
+ *
854
+ * ```ts
855
+ * import { createRun, ok, type Task } from "@evolu/common";
856
+ *
857
+ * interface ConfigDep {
858
+ * readonly config: { readonly apiUrl: string };
859
+ * }
860
+ *
861
+ * const loadApiUrl: Task<string, never, ConfigDep> = (run) =>
862
+ * ok(run.deps.config.apiUrl);
863
+ *
864
+ * await using run = createRun({
865
+ * config: { apiUrl: "https://api.example.com" },
866
+ * });
867
+ * expectOk(await run(loadApiUrl), "https://api.example.com");
868
+ * ```
869
+ *
206
870
  * @group Run
207
871
  */
208
872
  export const createRun = (deps) => createRunInternal({
@@ -685,10 +1349,11 @@ const mapInput = (input, fn) => Array.isArray(input)
685
1349
  * callbacks. Resolve with `ok(value)` or `err(error)`, or reject with a defect,
686
1350
  * to complete the Task. Settlement is one-shot: the first `resolve` or `reject`
687
1351
  * wins, and later settlement calls are ignored, matching Promise semantics.
688
- * When `reject` wins settlement, the defect panics the Run tree and is observed
689
- * at Fiber boundaries as {@link AbortError} with {@link PanicAbortReason}.
690
- * Rejecting AbortError is reserved for Task abort control flow: direct
691
- * `run(task)` rejects with it, and `run.abortable(task)` returns it as an Err.
1352
+ * When `reject` wins settlement, the defect panics the {@link Run} tree and is
1353
+ * observed at {@link Fiber} boundaries as {@link AbortError} with
1354
+ * {@link PanicAbortReason}. Rejecting AbortError is reserved for Task abort
1355
+ * control flow: direct `run(task)` rejects with it, and `run.abortable(task)`
1356
+ * returns it as an {@link Err}.
692
1357
  *
693
1358
  * This helper is a callback bridge. If `reject` forwards an Error created in a
694
1359
  * separate async chain, V8 cannot reconstruct the caller's zero-cost async
@@ -703,10 +1368,10 @@ const mapInput = (input, fn) => Array.isArray(input)
703
1368
  * Optionally return a synchronous cleanup callback. It runs when the Task Run
704
1369
  * signal aborts, including normal completion and explicit abort. The callback
705
1370
  * must not throw. Cleanup defects panic the root Run; if the callback Task
706
- * already settled, its Fiber keeps the original Result while the root still
707
- * reports the panic. Cleanup must be synchronous; returned promises are not
708
- * awaited. For async cleanup, prefer {@link acquireUseRelease}, `await using`
709
- * with {@link AsyncDisposableStack}, or a Task that owns the resource
1371
+ * already settled, its Fiber keeps the original {@link Result} while the root
1372
+ * still reports the panic. Cleanup must be synchronous; returned promises are
1373
+ * not awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
1374
+ * using` with {@link AsyncDisposableStack}, or a Task that owns the resource
710
1375
  * explicitly.
711
1376
  *
712
1377
  * If setup can throw after acquiring any resource, use a local
@@ -756,7 +1421,16 @@ export const callback = (fn) => (run) => {
756
1421
  /**
757
1422
  * Pauses execution for a specified {@link PositiveDuration}.
758
1423
  *
759
- * Aborting the Task clears the scheduled timeout.
1424
+ * Aborting the {@link Task} clears the scheduled timeout.
1425
+ *
1426
+ * ### Example
1427
+ *
1428
+ * ```ts
1429
+ * import { createRun, sleep } from "@evolu/common";
1430
+ *
1431
+ * await using run = createRun();
1432
+ * expectOk(await run(sleep("1ms")), undefined);
1433
+ * ```
760
1434
  *
761
1435
  * @group Timing
762
1436
  */
@@ -764,19 +1438,6 @@ export const sleep = (duration) => callback(({ run: { deps }, resolve }) => {
764
1438
  const id = deps.time.setTimeout(() => resolve(ok()), duration);
765
1439
  return () => deps.time.clearTimeout(id);
766
1440
  });
767
- /**
768
- * Typed error returned by {@link timeout} when a Task exceeds its duration.
769
- *
770
- * @group Timing
771
- */
772
- export const TimeoutError =
773
- /*#__PURE__*/ typed("TimeoutError");
774
- /**
775
- * The {@link TimeoutError} instance returned by {@link timeout}.
776
- *
777
- * @group Timing
778
- */
779
- export const timeoutError = { type: "TimeoutError" };
780
1441
  /**
781
1442
  * Limits how long a {@link Task} may run.
782
1443
  *
@@ -815,23 +1476,37 @@ export const timeout = (task, duration) => race([
815
1476
  },
816
1477
  ]);
817
1478
  /**
818
- * Retries a Task according to a {@link Schedule}.
1479
+ * Runtime Type for the error returned by {@link timeout} when a {@link Task}
1480
+ * exceeds its duration.
1481
+ *
1482
+ * @group Timing
1483
+ */
1484
+ export const TimeoutError =
1485
+ /*#__PURE__*/ typed("TimeoutError");
1486
+ /**
1487
+ * The {@link TimeoutError} instance returned by {@link timeout}.
1488
+ *
1489
+ * @group Timing
1490
+ */
1491
+ export const timeoutError = { type: "TimeoutError" };
1492
+ /**
1493
+ * Retries a {@link Task} according to a {@link Schedule}.
819
1494
  *
820
1495
  * Use `retry` for failure recovery: it repeats after {@link Err} and wraps the
821
1496
  * final domain error. Use {@link repeat} for success-driven loops: it repeats
822
- * after {@link Ok} and returns the Task's natural Result.
1497
+ * after {@link Ok} and returns the Task's natural {@link Result}.
823
1498
  *
824
1499
  * {@link AbortError} passes through unchanged when returned as a Result error,
825
- * such as from {@link daemon}. Abort from `run(task)` remains Fiber control
826
- * flow. Other errors are domain errors: retrying continues while the schedule
827
- * yields another delay and {@link RetryOptions.shouldRetry} returns `true`. When
828
- * retrying stops, `retry` returns {@link RetryError} with the final domain error
829
- * as `lastError`.
1500
+ * such as from {@link daemon}. Abort from `run(task)` remains {@link Fiber}
1501
+ * control flow. Other errors are domain errors: retrying continues while the
1502
+ * schedule yields another delay and {@link RetryOptions.shouldRetry} returns
1503
+ * `true`. When retrying stops, `retry` returns {@link RetryError} with the final
1504
+ * domain error as `lastError`.
830
1505
  *
831
1506
  * AbortError recognition is structural. Domain errors, especially values parsed
832
1507
  * from untrusted input, must not use the reserved AbortError shape.
833
1508
  *
834
- * ### Example
1509
+ * ### Retrying failures
835
1510
  *
836
1511
  * ```ts
837
1512
  * import {
@@ -842,51 +1517,58 @@ export const timeout = (task, duration) => race([
842
1517
  * type Result,
843
1518
  * type RetryTaskError,
844
1519
  * type Task,
1520
+ * type Typed,
845
1521
  * } from "@evolu/common";
846
1522
  *
847
- * await using run = createRun();
848
- *
849
- * interface FetchDataError {
850
- * readonly type: "FetchDataError";
851
- * }
1523
+ * const fetchData: Task<string, ServiceUnavailableError> = () =>
1524
+ * err({ type: "ServiceUnavailable" });
852
1525
  *
853
- * const fetchData: Task<string, FetchDataError> = () =>
854
- * err({ type: "FetchDataError" });
1526
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
855
1527
  *
856
1528
  * const fetchWithRetry = retry(fetchData, recurs(2));
857
1529
  *
1530
+ * await using run = createRun();
858
1531
  * const result = await run(fetchWithRetry);
859
1532
  * expectTypeOf(result).toEqualTypeOf<
860
- * Result<string, RetryTaskError<FetchDataError>>
1533
+ * Result<string, RetryTaskError<ServiceUnavailableError>>
861
1534
  * >();
862
1535
  * expectErr(result, {
863
1536
  * type: "RetryError",
864
1537
  * attempts: 3,
865
- * lastError: { type: "FetchDataError" },
1538
+ * lastError: { type: "ServiceUnavailable" },
866
1539
  * });
867
1540
  * ```
868
1541
  *
869
- * ### Example
1542
+ * ### Filtering retries
870
1543
  *
871
1544
  * ```ts
872
- * import { createRun, err, recurs, retry, type Task } from "@evolu/common";
1545
+ * import {
1546
+ * createRun,
1547
+ * err,
1548
+ * recurs,
1549
+ * retry,
1550
+ * type Task,
1551
+ * type Typed,
1552
+ * } from "@evolu/common";
873
1553
  *
874
- * interface FetchDataError {
875
- * readonly type: "RecoverableError" | "FatalError";
876
- * }
1554
+ * const fetchData: Task<
1555
+ * string,
1556
+ * TemporaryFailureError | PermanentFailureError
1557
+ * > = () => err({ type: "PermanentFailure" });
1558
+ *
1559
+ * interface TemporaryFailureError extends Typed<"TemporaryFailure"> {}
877
1560
  *
878
- * const fetchData: Task<string, FetchDataError> = () =>
879
- * err({ type: "FatalError" });
1561
+ * interface PermanentFailureError extends Typed<"PermanentFailure"> {}
880
1562
  *
881
1563
  * const fetchWithRetry = retry(fetchData, recurs(5), {
882
- * shouldRetry: (error) => error.type !== "FatalError",
1564
+ * shouldRetry: (error) => error.type !== "PermanentFailure",
883
1565
  * });
884
1566
  *
885
1567
  * await using run = createRun();
886
1568
  * expectErr(await run(fetchWithRetry), {
887
1569
  * type: "RetryError",
888
1570
  * attempts: 1,
889
- * lastError: { type: "FatalError" },
1571
+ * lastError: { type: "PermanentFailure" },
890
1572
  * });
891
1573
  * ```
892
1574
  *
@@ -920,13 +1602,14 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
920
1602
  }
921
1603
  };
922
1604
  /**
923
- * Repeats a Task according to a {@link Schedule}.
1605
+ * Repeats a {@link Task} according to a {@link Schedule}.
924
1606
  *
925
1607
  * Runs the Task once, then repeats while the Task returns {@link Ok}, the
926
1608
  * schedule yields another delay, and {@link RepeatOptions.shouldRepeat} returns
927
- * `true`. When repeating stops, `repeat` returns the last successful Result. If
928
- * the Task returns {@link Err}, including {@link Done} from a {@link NextTask},
929
- * `repeat` returns that error without scheduling another attempt.
1609
+ * `true`. When repeating stops, `repeat` returns the last successful
1610
+ * {@link Result}. If the Task returns {@link Err}, including {@link Done} from a
1611
+ * {@link NextTask}, `repeat` returns that error without scheduling another
1612
+ * attempt.
930
1613
  *
931
1614
  * Use `repeat` for success-driven loops such as polling or consuming a
932
1615
  * NextTask: it repeats after Ok and returns the Task's natural Result. Use
@@ -935,7 +1618,7 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
935
1618
  *
936
1619
  * With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
937
1620
  *
938
- * ### Example
1621
+ * ### Repeating successes
939
1622
  *
940
1623
  * ```ts
941
1624
  * import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
@@ -953,7 +1636,7 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
953
1636
  * expect(attempts).toBe(4);
954
1637
  * ```
955
1638
  *
956
- * ### Example
1639
+ * ### Stopping with Done
957
1640
  *
958
1641
  * ```ts
959
1642
  * import {
@@ -966,8 +1649,6 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
966
1649
  * type NextTask,
967
1650
  * } from "@evolu/common";
968
1651
  *
969
- * await using run = createRun();
970
- *
971
1652
  * interface Item {
972
1653
  * readonly id: string;
973
1654
  * }
@@ -979,6 +1660,7 @@ export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}
979
1660
  * return item ? ok(item) : err(done());
980
1661
  * };
981
1662
  *
1663
+ * await using run = createRun();
982
1664
  * const result = await run(repeat(processQueue, spaced("1ms")));
983
1665
  * expectErr(result, done());
984
1666
  * expect(queue).toEqual([]);
@@ -1006,22 +1688,22 @@ export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } =
1006
1688
  }
1007
1689
  };
1008
1690
  /**
1009
- * Runs Tasks until one returns {@link Ok} or all return {@link Err}.
1691
+ * Runs {@link Task}s until one returns {@link Ok} or all return {@link Err}.
1010
1692
  *
1011
- * Use {@link race} to return the first settled Result instead, whether Ok or
1012
- * {@link Err}.
1693
+ * Use {@link race} to return the first settled {@link Result} instead, whether Ok
1694
+ * or {@link Err}.
1013
1695
  *
1014
- * Returns the first {@link Ok} Result. Losing Tasks are aborted. If no Task
1015
- * returns Ok, returns the last Err by input order, regardless of completion
1016
- * order. Other Err results are discarded; use {@link allSettled} when you need
1017
- * every error.
1696
+ * Returns the first {@link Ok} Result. Queued Tasks are not started, and other
1697
+ * running Tasks are aborted. If no Task returns Ok, returns the last Err by
1698
+ * input order, regardless of completion order. Other Err results are discarded;
1699
+ * use {@link allSettled} when you need every error.
1018
1700
  *
1019
1701
  * Sequential by default; pass a `concurrency` option to run more than one Task
1020
1702
  * at a time.
1021
1703
  *
1022
1704
  * Similar to
1023
1705
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
1024
- * but races Tasks, returns Result values, and aborts losers.
1706
+ * but runs Tasks, returns Result values, and stops after the first Ok.
1025
1707
  *
1026
1708
  * ### Example
1027
1709
  *
@@ -1033,10 +1715,14 @@ export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } =
1033
1715
  * ok,
1034
1716
  * type Result,
1035
1717
  * type Task,
1718
+ * type Typed,
1036
1719
  * } from "@evolu/common";
1037
1720
  *
1038
- * const unavailable: Task<string, "Unavailable"> = () =>
1039
- * err("Unavailable");
1721
+ * const unavailable: Task<string, ServiceUnavailableError> = () =>
1722
+ * err({ type: "ServiceUnavailable" });
1723
+ *
1724
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
1725
+ *
1040
1726
  * let fallbackStarted = false;
1041
1727
  * const fallback: Task<string> = () => {
1042
1728
  * fallbackStarted = true;
@@ -1046,7 +1732,9 @@ export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } =
1046
1732
  * await using run = createRun();
1047
1733
  * const result = await run(any([unavailable, fallback]));
1048
1734
  *
1049
- * expectTypeOf(result).toEqualTypeOf<Result<string, "Unavailable">>();
1735
+ * expectTypeOf(result).toEqualTypeOf<
1736
+ * Result<string, ServiceUnavailableError>
1737
+ * >();
1050
1738
  * expectOk(result, "fallback");
1051
1739
  * expect(fallbackStarted).toBe(true);
1052
1740
  * ```
@@ -1074,7 +1762,7 @@ export const any = (tasks, options) => async (run) => {
1074
1762
  return lastErr;
1075
1763
  };
1076
1764
  /**
1077
- * Runs Tasks until the first Task settles.
1765
+ * Runs {@link Task}s until the first Task settles.
1078
1766
  *
1079
1767
  * Returns the first Task {@link Result} to settle, whether {@link Ok} or
1080
1768
  * {@link Err}.
@@ -1095,7 +1783,7 @@ export const any = (tasks, options) => async (run) => {
1095
1783
  * arrays whose emptiness is only known at runtime, guard with
1096
1784
  * {@link isNonEmptyArray}:
1097
1785
  *
1098
- * ### Example
1786
+ * ### Runtime arrays
1099
1787
  *
1100
1788
  * ```ts
1101
1789
  * import {
@@ -1114,7 +1802,7 @@ export const any = (tasks, options) => async (run) => {
1114
1802
  * }
1115
1803
  * ```
1116
1804
  *
1117
- * ### Example
1805
+ * ### First settlement
1118
1806
  *
1119
1807
  * ```ts
1120
1808
  * import {
@@ -1157,12 +1845,12 @@ export const race = (tasks) => async (run) => {
1157
1845
  return firstResult;
1158
1846
  };
1159
1847
  /**
1160
- * Runs Tasks until `count` Tasks return {@link Ok} or all Tasks settle.
1848
+ * Runs {@link Task}s until `count` Tasks return {@link Ok} or all Tasks settle.
1161
1849
  *
1162
1850
  * Returns {@link Ok} with Ok values in settlement order, not input order.
1163
- * {@link Err} Results are ignored. When `count` Ok values have settled,
1164
- * remaining Tasks are aborted. If fewer than `count` Tasks return Ok, returns
1165
- * the Ok values that did settle.
1851
+ * {@link Err} {@link Result}s are ignored. When `count` Ok values have settled,
1852
+ * queued Tasks are not started and remaining running Tasks are aborted. If
1853
+ * fewer than `count` Tasks return Ok, returns the Ok values that did settle.
1166
1854
  *
1167
1855
  * Sequential by default; pass a `concurrency` option to run more than one Task
1168
1856
  * at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
@@ -1178,17 +1866,23 @@ export const race = (tasks) => async (run) => {
1178
1866
  * ok,
1179
1867
  * sleep,
1180
1868
  * type Task,
1869
+ * type Typed,
1181
1870
  * } from "@evolu/common";
1182
1871
  *
1183
1872
  * let slowCompleted = false;
1184
- * const slow: Task<string, "Failed"> = async (run) => {
1873
+ * const slow: Task<string> = async (run) => {
1185
1874
  * await run.ok(sleep("10ms"));
1186
1875
  * slowCompleted = true;
1187
1876
  * return ok("slow");
1188
1877
  * };
1878
+ * const unavailable: Task<never, ServiceUnavailableError> = () =>
1879
+ * err({ type: "ServiceUnavailable" });
1880
+ *
1881
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
1882
+ *
1189
1883
  * const tasks = [
1190
1884
  * slow,
1191
- * () => err("Failed" as const),
1885
+ * unavailable,
1192
1886
  * () => ok("fast-1"),
1193
1887
  * () => ok("fast-2"),
1194
1888
  * ] as const;
@@ -1213,11 +1907,12 @@ export const firstN = (tasks, count, options) => async (run) => {
1213
1907
  return ok(values);
1214
1908
  };
1215
1909
  /**
1216
- * Runs Tasks until `count` Tasks settle or all Tasks settle.
1910
+ * Runs {@link Task}s until `count` Tasks settle or all Tasks settle.
1217
1911
  *
1218
1912
  * Returns {@link Ok} with Task {@link Result}s in settlement order, not input
1219
- * order. When `count` Results have settled, remaining Tasks are aborted. If
1220
- * fewer than `count` Tasks settle, returns the Results that did settle.
1913
+ * order. When `count` Results have settled, queued Tasks are not started and
1914
+ * remaining running Tasks are aborted. If fewer than `count` Tasks settle,
1915
+ * returns the Results that did settle.
1221
1916
  *
1222
1917
  * Sequential by default; pass a `concurrency` option to run more than one Task
1223
1918
  * at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
@@ -1233,25 +1928,27 @@ export const firstN = (tasks, count, options) => async (run) => {
1233
1928
  * ok,
1234
1929
  * sleep,
1235
1930
  * type Task,
1931
+ * type Typed,
1236
1932
  * } from "@evolu/common";
1237
1933
  *
1238
1934
  * let slowCompleted = false;
1239
- * const slow: Task<string, "Failed"> = async (run) => {
1935
+ * const slow: Task<string> = async (run) => {
1240
1936
  * await run.ok(sleep("10ms"));
1241
1937
  * slowCompleted = true;
1242
1938
  * return ok("slow");
1243
1939
  * };
1244
- * const tasks = [
1245
- * slow,
1246
- * () => err("Failed" as const),
1247
- * () => ok("fast"),
1248
- * ] as const;
1940
+ * const unavailable: Task<never, ServiceUnavailableError> = () =>
1941
+ * err({ type: "ServiceUnavailable" });
1942
+ *
1943
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
1944
+ *
1945
+ * const tasks = [slow, unavailable, () => ok("fast")] as const;
1249
1946
  * await using run = createRun();
1250
1947
  *
1251
1948
  * // Err and Ok both count, and Results use settlement order.
1252
1949
  * const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
1253
1950
  * expectOk(result, [
1254
- * { ok: false, error: "Failed" },
1951
+ * { ok: false, error: { type: "ServiceUnavailable" } },
1255
1952
  * { ok: true, value: "fast" },
1256
1953
  * ]);
1257
1954
  * expect(slowCompleted).toBe(false);
@@ -1269,26 +1966,27 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
1269
1966
  return ok(results);
1270
1967
  };
1271
1968
  /**
1272
- * Runs Tasks under a concurrency limit and calls `onResult` for each Task
1273
- * {@link Result} as it settles.
1969
+ * Runs {@link Task}s under a concurrency limit and calls `onResult` for each
1970
+ * Task {@link Result} as it settles.
1274
1971
  *
1275
- * `onResult` receives the Result and the original input index; call order is
1276
- * settlement order, not input order. Returning `continue` lets queued Tasks
1277
- * start when capacity is available. Returning `stop` prevents queued Tasks from
1278
- * starting and aborts already-running Tasks through structured Run disposal —
1279
- * `each` still waits for them to settle before returning.
1972
+ * `onResult` receives the {@link Result} and the original input index; call
1973
+ * order is settlement order, not input order. Returning `continue` lets queued
1974
+ * Tasks start when capacity is available. Returning `stop` prevents queued
1975
+ * Tasks from starting and aborts already-running Tasks through structured
1976
+ * {@link Run} disposal — `each` still waits for them to settle before
1977
+ * returning.
1280
1978
  *
1281
1979
  * `each` is the scheduling primitive under the collection helpers. Each one is
1282
1980
  * a small `onResult` policy:
1283
1981
  *
1284
- * | Helper | Policy |
1285
- * | --------------------- | ------------------------------------- |
1286
- * | {@link all} | Collect values, stop on the first Err |
1287
- * | {@link allSettled} | Collect every Result, never stop |
1288
- * | {@link any} | Stop on the first Ok |
1289
- * | {@link race} | Stop on the first settled Result |
1290
- * | {@link firstN} | Stop after n Ok values |
1291
- * | {@link firstNSettled} | Stop after n Results |
1982
+ * | Helper | Policy |
1983
+ * | --------------------- | --------------------------------------------- |
1984
+ * | {@link all} | Collect values, stop on the first {@link Err} |
1985
+ * | {@link allSettled} | Collect every Result, never stop |
1986
+ * | {@link any} | Stop on the first {@link Ok} |
1987
+ * | {@link race} | Stop on the first settled Result |
1988
+ * | {@link firstN} | Stop after n Ok values |
1989
+ * | {@link firstNSettled} | Stop after n Results |
1292
1990
  *
1293
1991
  * Use `each` directly to build a collection policy the helpers don't cover. For
1294
1992
  * example, keep the first successful value together with its original input
@@ -1304,6 +2002,7 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
1304
2002
  * ok,
1305
2003
  * sleep,
1306
2004
  * type Task,
2005
+ * type Typed,
1307
2006
  * } from "@evolu/common";
1308
2007
  *
1309
2008
  * let slowCompleted = false;
@@ -1312,11 +2011,12 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
1312
2011
  * slowCompleted = true;
1313
2012
  * return ok("slow");
1314
2013
  * };
1315
- * const tasks = [
1316
- * slow,
1317
- * () => err("Unavailable" as const),
1318
- * () => ok("fast"),
1319
- * ] as const;
2014
+ * const unavailable: Task<never, ServiceUnavailableError> = () =>
2015
+ * err({ type: "ServiceUnavailable" });
2016
+ *
2017
+ * interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
2018
+ *
2019
+ * const tasks = [slow, unavailable, () => ok("fast")] as const;
1320
2020
  * let first: readonly [string, number] | undefined;
1321
2021
  * await using run = createRun();
1322
2022
  * const result = await run(
@@ -1350,7 +2050,7 @@ export const firstNSettled = (tasks, count, options) => async (run) => {
1350
2050
  * building on `each` preserves diagnostics that a hand-rolled scheduling loop
1351
2051
  * typically loses.
1352
2052
  *
1353
- * @group Concurrency
2053
+ * @group Collection
1354
2054
  */
1355
2055
  export const each = (tasks, onResult, options = {}) => async (run) => {
1356
2056
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -1445,9 +2145,9 @@ export const prioritized = (priority, task) => withTaskMeta({ priority })(task);
1445
2145
  * Yields execution to the host scheduler.
1446
2146
  *
1447
2147
  * Uses native `scheduler.yield()` when available, `setImmediate` when
1448
- * available, and `setTimeout` elsewhere. Because this is a Task, `await
1449
- * run(yieldNow)` is an explicit abortable checkpoint and is visible in Run
1450
- * monitoring.
2148
+ * available, and `setTimeout` elsewhere. Because this is a {@link Task}, `await
2149
+ * run(yieldNow)` is an explicit abortable checkpoint and is visible in
2150
+ * {@link Run} monitoring.
1451
2151
  *
1452
2152
  * For example, call it periodically in a long-running synchronous loop to let
1453
2153
  * the host process rendering, input, and other scheduled work.
@@ -1519,12 +2219,13 @@ export const yieldNow = async (run) => {
1519
2219
  * interface ServerDep {
1520
2220
  * readonly port: number;
1521
2221
  * }
2222
+ *
1522
2223
  * interface Server extends AsyncDisposable {}
1523
2224
  *
1524
2225
  * const serverStarted = Promise.withResolvers<void>();
1525
2226
  * let serverStopped = false;
1526
- * const startServer: Task<Server, never, ServerDep> = ({ deps }) => {
1527
- * expect(deps.port).toBe(3000);
2227
+ * const startServer: Task<Server, never, ServerDep> = (run) => {
2228
+ * expect(run.deps.port).toBe(3000);
1528
2229
  * serverStarted.resolve();
1529
2230
  * return ok({
1530
2231
  * [Symbol.asyncDispose]: async () => {
@@ -1588,7 +2289,7 @@ export const waitForAbort = async (run) => {
1588
2289
  * Do not wrap a Task that keeps using a resource the caller may release after
1589
2290
  * this wrapper returns, unless the Task reliably observes abort before using
1590
2291
  * that resource. The daemon Task can continue after the caller stops waiting.
1591
- * Later domain `Err` results from the daemon Task are discarded after the
2292
+ * Later domain {@link Err} results from the daemon Task are discarded after the
1592
2293
  * caller stops waiting. Defects from the daemon Task remain visible to the root
1593
2294
  * Run: if it later throws or rejects, the root Run still panics and reports the
1594
2295
  * defect.
@@ -1598,10 +2299,10 @@ export const waitForAbort = async (run) => {
1598
2299
  * to settle, keeping cleanup and late defects inside the caller's lifetime. A
1599
2300
  * Task that ignores abort can keep them waiting.
1600
2301
  *
1601
- * `run.abortable(task)` returns an owned child Fiber and requests abort through
1602
- * that Fiber; `daemon(task)` starts a daemon child and stops waiting when the
1603
- * current Run aborts. {@link unabortable} masks abort for a Task that must
1604
- * finish once started; daemon lets a Task outlive the caller.
2302
+ * `run.abortable(task)` returns an owned child {@link Fiber} and requests abort
2303
+ * through that Fiber; `daemon(task)` starts a daemon child and stops waiting
2304
+ * when the current Run aborts. {@link unabortable} masks abort for a Task that
2305
+ * must finish once started; daemon lets a Task outlive the caller.
1605
2306
  *
1606
2307
  * Because the Task starts with {@link Run.daemon}, a recorded abort request
1607
2308
  * returns AbortError before the Task starts — including a request masked by
@@ -1648,15 +2349,10 @@ export const waitForAbort = async (run) => {
1648
2349
  * import { createRun, ok, type Result, type Task } from "@evolu/common";
1649
2350
  *
1650
2351
  * type ResultValue = string;
1651
- * interface MyError {
1652
- * readonly type: "MyError";
1653
- * }
1654
- * const createPromiseReturningResult = (): Promise<
1655
- * Result<ResultValue, MyError>
1656
- * > => Promise.resolve(ok("value"));
2352
+ * const createPromiseReturningResult = (): Promise<Result<ResultValue>> =>
2353
+ * Promise.resolve(ok("value"));
1657
2354
  *
1658
- * const task: Task<ResultValue, MyError> = () =>
1659
- * createPromiseReturningResult();
2355
+ * const task: Task<ResultValue> = () => createPromiseReturningResult();
1660
2356
  *
1661
2357
  * await using run = createRun();
1662
2358
  * expectOk(await run(task), "value");
@@ -1669,12 +2365,9 @@ export const waitForAbort = async (run) => {
1669
2365
  * import { ok, type Result, type Task } from "@evolu/common";
1670
2366
  *
1671
2367
  * type ResultValue = string;
1672
- * interface MyError {
1673
- * readonly type: "MyError";
1674
- * }
1675
2368
  * let promiseStarted = false;
1676
2369
  * const createPromiseReturningResult = (): Promise<
1677
- * Result<ResultValue, MyError>
2370
+ * Result<ResultValue>
1678
2371
  * > => {
1679
2372
  * promiseStarted = true;
1680
2373
  * return Promise.resolve(ok("value"));
@@ -1682,10 +2375,10 @@ export const waitForAbort = async (run) => {
1682
2375
  *
1683
2376
  * // Wrong: the Promise starts now, before a Run starts the Task.
1684
2377
  * const promise = createPromiseReturningResult();
1685
- * const task: Task<ResultValue, MyError> = () => promise;
2378
+ * const task: Task<ResultValue> = () => promise;
1686
2379
  *
1687
2380
  * expect(promiseStarted).toBe(true);
1688
- * expectTypeOf(task).toEqualTypeOf<Task<ResultValue, MyError>>();
2381
+ * expectTypeOf(task).toEqualTypeOf<Task<ResultValue>>();
1689
2382
  * ```
1690
2383
  *
1691
2384
  * @group Lifetime
@@ -1723,8 +2416,8 @@ export const daemon = (task) => async (run) => {
1723
2416
  * Abort requests are masked while the Task runs, so `run.signal.aborted`
1724
2417
  * remains false inside the Task. This does not force the Task to start after an
1725
2418
  * abort request has already reached its Run; unabortable means the Task is not
1726
- * interrupted once it has started. Disposing the enclosing Run still waits for
1727
- * the Task to settle.
2419
+ * interrupted once it has started. Disposing the enclosing {@link Run} still
2420
+ * waits for the Task to settle.
1728
2421
  *
1729
2422
  * Apply at most one abort behavior helper to a Task: do not wrap the same Task
1730
2423
  * with both unabortable and restore, or apply either helper more than once.
@@ -1736,10 +2429,10 @@ export const daemon = (task) => async (run) => {
1736
2429
  *
1737
2430
  * const commitStarted = Promise.withResolvers<void>();
1738
2431
  * const finishCommit = Promise.withResolvers<void>();
1739
- * const commit: Task<string> = unabortable(async ({ signal }) => {
2432
+ * const commit: Task<string> = unabortable(async (run) => {
1740
2433
  * commitStarted.resolve();
1741
2434
  * await finishCommit.promise;
1742
- * expect(signal.aborted).toBe(false);
2435
+ * expect(run.signal.aborted).toBe(false);
1743
2436
  * return ok("committed");
1744
2437
  * });
1745
2438
  *
@@ -1758,8 +2451,8 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
1758
2451
  abortBehavior: "unabortable",
1759
2452
  });
1760
2453
  /**
1761
- * Like {@link unabortable}, but provides `restore` for child Tasks that should
1762
- * run with the previous abort mask.
2454
+ * Like {@link unabortable}, but provides `restore` for child {@link Task}s that
2455
+ * should run with the previous abort mask.
1763
2456
  *
1764
2457
  * Use this for acquire/use/release flows where acquire and release must finish
1765
2458
  * once started, while use should remain abortable. Child Tasks inherit the mask
@@ -1787,8 +2480,6 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
1787
2480
  * type Task,
1788
2481
  * } from "@evolu/common";
1789
2482
  *
1790
- * await using run = createRun();
1791
- *
1792
2483
  * interface Resource {
1793
2484
  * readonly id: string;
1794
2485
  * }
@@ -1805,13 +2496,14 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
1805
2496
  * let released = false;
1806
2497
  * const release =
1807
2498
  * (_resource: Resource): Task<void> =>
1808
- * ({ signal }) => {
2499
+ * (run) => {
1809
2500
  * // Release inherits the mask even after abort was requested.
1810
- * expect(signal.aborted).toBe(false);
2501
+ * expect(run.signal.aborted).toBe(false);
1811
2502
  * released = true;
1812
2503
  * return ok();
1813
2504
  * };
1814
2505
  *
2506
+ * await using run = createRun();
1815
2507
  * const fiber = run.abortable(
1816
2508
  * unabortableMask((restore) => async (run) => {
1817
2509
  * // Acquire with abort masked.
@@ -1880,6 +2572,7 @@ export const unabortableMask = (fn) => unabortable((run) => {
1880
2572
  * err,
1881
2573
  * ok,
1882
2574
  * type Task,
2575
+ * type Typed,
1883
2576
  * } from "@evolu/common";
1884
2577
  *
1885
2578
  * interface Connection {
@@ -1890,9 +2583,14 @@ export const unabortableMask = (fn) => unabortable((run) => {
1890
2583
  * const openConnection: Task<Connection> = () =>
1891
2584
  * ok({ user: "Ada", isAvailable: false });
1892
2585
  * const loadUser =
1893
- * (connection: Connection): Task<string, "Unavailable"> =>
2586
+ * (connection: Connection): Task<string, UserUnavailableError> =>
1894
2587
  * () =>
1895
- * connection.isAvailable ? ok(connection.user) : err("Unavailable");
2588
+ * connection.isAvailable
2589
+ * ? ok(connection.user)
2590
+ * : err({ type: "UserUnavailable" });
2591
+ *
2592
+ * interface UserUnavailableError extends Typed<"UserUnavailable"> {}
2593
+ *
1896
2594
  * let connectionClosed = false;
1897
2595
  * const closeConnection =
1898
2596
  * (_connection: Connection): Task<void> =>
@@ -1908,12 +2606,12 @@ export const unabortableMask = (fn) => unabortable((run) => {
1908
2606
  * );
1909
2607
  *
1910
2608
  * await using run = createRun();
1911
- * expectErr(await run(queryUser), "Unavailable");
2609
+ * expectErr(await run(queryUser), { type: "UserUnavailable" });
1912
2610
  * // Release still runs when use returns a domain error.
1913
2611
  * expect(connectionClosed).toBe(true);
1914
2612
  * ```
1915
2613
  *
1916
- * @group Abortability
2614
+ * @group Lifetime
1917
2615
  */
1918
2616
  export const acquireUseRelease = (acquire, use, release) => unabortableMask((restore) => async (run) => {
1919
2617
  const resourceResult = await run(acquire);
@@ -1930,6 +2628,48 @@ export const acquireUseRelease = (acquire, use, release) => unabortableMask((res
1930
2628
  /**
1931
2629
  * Creates a {@link Deferred}.
1932
2630
  *
2631
+ * ### Example
2632
+ *
2633
+ * ```ts
2634
+ * import {
2635
+ * createDeferred,
2636
+ * createRun,
2637
+ * ok,
2638
+ * type Result,
2639
+ * } from "@evolu/common";
2640
+ *
2641
+ * await using run = createRun();
2642
+ * const deferred = createDeferred<string>();
2643
+ *
2644
+ * const fiber = run(deferred.task);
2645
+ * expect(deferred.resolve(ok("ready"))).toBe(true);
2646
+ *
2647
+ * const result = await fiber;
2648
+ * expectTypeOf(result).toEqualTypeOf<Result<string>>();
2649
+ * expectOk(result, "ready");
2650
+ *
2651
+ * // A Deferred is one-shot: later resolutions are ignored, and future
2652
+ * // waiters receive the original Result.
2653
+ * expect(deferred.resolve(ok("late"))).toBe(false);
2654
+ * expectOk(await run(deferred.task), "ready");
2655
+ * ```
2656
+ *
2657
+ * ### Aborting a waiter
2658
+ *
2659
+ * ```ts
2660
+ * import { AbortError, createDeferred, createRun } from "@evolu/common";
2661
+ *
2662
+ * await using run = createRun();
2663
+ * const deferred = createDeferred<string>();
2664
+ *
2665
+ * const fiber = run.abortable(deferred.task);
2666
+ * fiber.abort({ type: "NoLongerNeeded" });
2667
+ *
2668
+ * const result = await fiber;
2669
+ * assert(!result.ok);
2670
+ * expect(AbortError.is(result.error)).toBe(true);
2671
+ * ```
2672
+ *
1933
2673
  * @group Concurrency primitives
1934
2674
  */
1935
2675
  export const createDeferred = () => {
@@ -1960,6 +2700,34 @@ export const createDeferred = () => {
1960
2700
  /**
1961
2701
  * Creates a {@link Gate}.
1962
2702
  *
2703
+ * ### Example
2704
+ *
2705
+ * ```ts
2706
+ * import { createGate, createRun, ok, type Task } from "@evolu/common";
2707
+ *
2708
+ * await using run = createRun();
2709
+ * const networkGate = createGate();
2710
+ *
2711
+ * const uploadedItems: Array<string> = [];
2712
+ * const syncOnce =
2713
+ * (item: string): Task<void> =>
2714
+ * async (run) => {
2715
+ * await run.ok(networkGate.wait);
2716
+ * uploadedItems.push(item);
2717
+ * return ok();
2718
+ * };
2719
+ *
2720
+ * const first = run(syncOnce("first"));
2721
+ * const second = run(syncOnce("second"));
2722
+ * expect(uploadedItems).toEqual([]);
2723
+ *
2724
+ * networkGate.open();
2725
+ * expectOk(await first, undefined);
2726
+ * expectOk(await second, undefined);
2727
+ * expect(uploadedItems).toEqual(["first", "second"]);
2728
+ * expect(networkGate.isOpen()).toBe(true);
2729
+ * ```
2730
+ *
1963
2731
  * @group Concurrency primitives
1964
2732
  */
1965
2733
  export const createGate = ({ isOpen = false, } = {}) => {
@@ -1997,6 +2765,45 @@ export const createGate = ({ isOpen = false, } = {}) => {
1997
2765
  /**
1998
2766
  * Creates a {@link Semaphore}.
1999
2767
  *
2768
+ * ### Example
2769
+ *
2770
+ * ```ts
2771
+ * import {
2772
+ * createRun,
2773
+ * createSemaphore,
2774
+ * getOk,
2775
+ * ok,
2776
+ * sleep,
2777
+ * type Task,
2778
+ * } from "@evolu/common";
2779
+ *
2780
+ * await using run = createRun();
2781
+ *
2782
+ * const semaphore = createSemaphore(2);
2783
+ * let activeSaves = 0;
2784
+ * let maxActiveSaves = 0;
2785
+ *
2786
+ * const saveUser =
2787
+ * (id: string): Task<string> =>
2788
+ * async (run) => {
2789
+ * activeSaves += 1;
2790
+ * maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
2791
+ * await run.ok(sleep("10ms"));
2792
+ * activeSaves -= 1;
2793
+ * return ok(`saved:${id}`);
2794
+ * };
2795
+ *
2796
+ * const results = await Promise.all([
2797
+ * run(semaphore.withPermit(saveUser("1"))),
2798
+ * run(semaphore.withPermit(saveUser("2"))),
2799
+ * run(semaphore.withPermit(saveUser("3"))),
2800
+ * ]);
2801
+ *
2802
+ * const savedUsers = results.map(getOk);
2803
+ * expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
2804
+ * expect(maxActiveSaves).toBe(2);
2805
+ * ```
2806
+ *
2000
2807
  * @group Concurrency primitives
2001
2808
  */
2002
2809
  export const createSemaphore = (initialPermits, { policy = "fifo", } = {}) => {
@@ -2112,6 +2919,37 @@ export const createSemaphore = (initialPermits, { policy = "fifo", } = {}) => {
2112
2919
  /**
2113
2920
  * Creates a {@link Mutex}.
2114
2921
  *
2922
+ * ### Example
2923
+ *
2924
+ * ```ts
2925
+ * import {
2926
+ * createMutex,
2927
+ * createRun,
2928
+ * ok,
2929
+ * sleep,
2930
+ * type Task,
2931
+ * } from "@evolu/common";
2932
+ *
2933
+ * const mutex = createMutex();
2934
+ * let balance = 0;
2935
+ * const deposit = (amount: number): Task<void> =>
2936
+ * mutex.withLock(async (run) => {
2937
+ * const currentBalance = balance;
2938
+ * await run.ok(sleep("1ms"));
2939
+ * balance = currentBalance + amount;
2940
+ * return ok();
2941
+ * });
2942
+ *
2943
+ * await using run = createRun();
2944
+ * const [first, second] = await Promise.all([
2945
+ * run(deposit(2)),
2946
+ * run(deposit(3)),
2947
+ * ]);
2948
+ * expectOk(first, undefined);
2949
+ * expectOk(second, undefined);
2950
+ * expect(balance).toBe(5);
2951
+ * ```
2952
+ *
2115
2953
  * @group Concurrency primitives
2116
2954
  */
2117
2955
  export const createMutex = () => {
@@ -2163,6 +3001,19 @@ export function createMutexByKey({ lookup = identity, } = {}) {
2163
3001
  /**
2164
3002
  * Creates a {@link MutexRef}.
2165
3003
  *
3004
+ * ### Example
3005
+ *
3006
+ * ```ts
3007
+ * import { createMutexRef, createRun, ok } from "@evolu/common";
3008
+ *
3009
+ * const counter = createMutexRef(0);
3010
+ * const increment = counter.updateAndGet((value) => () => ok(value + 1));
3011
+ *
3012
+ * await using run = createRun();
3013
+ * expectOk(await run(increment), 1);
3014
+ * expectOk(await run(counter.get), 1);
3015
+ * ```
3016
+ *
2166
3017
  * @group Concurrency primitives
2167
3018
  */
2168
3019
  export const createMutexRef = (initialValue) => {
@@ -2209,7 +3060,7 @@ export const createMutexRef = (initialValue) => {
2209
3060
  snapshot: mutex.snapshot,
2210
3061
  };
2211
3062
  };
2212
- // TODO: Add Run observability after Task migration.
3063
+ // TODO: Expand Run observability.
2213
3064
  // - Structured logging with levels, inherited log annotations, JSON output,
2214
3065
  // filtering, and pluggable log sinks.
2215
3066
  // - Tracing spans with names, timing, parent-child relationships, attributes,