@evolu/common 7.4.0 → 8.0.0-next.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 (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -1,580 +1,2420 @@
1
- import { Result } from "./Result.js";
2
- import { Duration } from "./Time.js";
3
- import { PositiveInt } from "./Type.js";
4
1
  /**
5
- * `Task` is a function that creates and returns an optionally cancellable
6
- * Promise using {@link Result}.
2
+ * JavaScript-native structured concurrency.
7
3
  *
8
- * The laziness allows safe composition, e.g. retry logic, because it prevents
9
- * eager execution until the Task is actually invoked.
4
+ * @module
5
+ */
6
+ import { type NonEmptyReadonlyArray } from "./Array.js";
7
+ import { type ConsoleDep } from "./Console.js";
8
+ import type { RandomBytesDep } from "./Crypto.js";
9
+ import { type LookupOption } from "./Lookup.js";
10
+ import type { RandomDep } from "./Random.js";
11
+ import { type Ref } from "./Ref.js";
12
+ import type { Done, Result } from "./Result.js";
13
+ import type { Schedule, ScheduleStep } from "./Schedule.js";
14
+ import type { Duration, TimeDep } from "./Time.js";
15
+ import { Id, type InferType, type Name, NonNegativeInt, PositiveInt, type Typed } from "./Type.js";
16
+ import { type Awaitable, type Callback, type CallbackWithTeardown, type Int1To100, type NewKeys, type Predicate } from "./Types.js";
17
+ /**
18
+ * JavaScript-native structured concurrency.
10
19
  *
11
- * ### Cancellation
20
+ * Structured concurrency is a simple idea: async operations form a tree where
21
+ * no child can outlive its parent — ending a parent aborts its children and
22
+ * waits for them to complete. This eliminates resource leaking and "fire and
23
+ * forget" bugs.
12
24
  *
13
- * Tasks support optional cancellation via signal in {@link TaskContext}. When a
14
- * Task is called without a signal, it cannot be cancelled and {@link AbortError}
15
- * will never be returned. When called with a signal, the Task can be cancelled
16
- * and AbortError is added to the error union with precise type safety.
25
+ * - **Automatic cancellation** abort propagates to all descendants
26
+ * - **Guaranteed cleanup** resources always cleaned up
27
+ * - **Observable state** inspect what’s running and why
17
28
  *
18
- * When composing Tasks, we typically have context and want to abort ASAP by
19
- * passing it through. However, there are valid cases where we don't want to
20
- * abort because we need some atomic unit to complete. For simple scripts and
21
- * tests, omitting context is fine.
29
+ * Evolu implements structured concurrency with these types:
22
30
  *
23
- * ### Task Helpers
31
+ * - **{@link Task}** — a function that takes Run and returns {@link Awaitable}
32
+ * (sync or async) {@link Result}
33
+ * - **{@link Run}** — a callable object that runs Tasks, manages their lifecycle,
34
+ * provides dependencies, and creates Fibers
35
+ * - **{@link Fiber}** — awaitable, abortable/disposable handle to a running Task
24
36
  *
25
- * - {@link toTask} - Convert async function to Task
26
- * - {@link wait} - Delay execution for a specified {@link Duration}
27
- * - {@link timeout} - Add timeout to any Task
28
- * - {@link retry} - Retry failed Tasks with configurable backoff
37
+ * Evolu's structured concurrency core is minimal — one function with
38
+ * closed-over state and a few attached properties and helper methods using
39
+ * native APIs.
29
40
  *
30
41
  * ### Example
31
42
  *
32
43
  * ```ts
33
- * interface FetchError {
34
- * readonly type: "FetchError";
44
+ * // A dependency — wraps native fetch for testability.
45
+ * interface NativeFetchDep {
46
+ * readonly fetch: typeof globalThis.fetch;
47
+ * }
48
+ *
49
+ * interface FetchError extends Typed<"FetchError"> {
35
50
  * readonly error: unknown;
36
51
  * }
37
52
  *
38
- * // Task version of fetch with proper error handling and cancellation support.
39
- * const fetch = (url: string) =>
40
- * toTask((context) =>
53
+ * // A Task wrapping native fetch adds abortability.
54
+ * const fetch =
55
+ * (url: string): Task<Response, FetchError, NativeFetchDep> =>
56
+ * ({ deps, signal }) =>
41
57
  * tryAsync(
42
- * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
43
- * (error): FetchError => ({ type: "FetchError", error }),
44
- * ),
58
+ * () => deps.fetch(url, { signal }),
59
+ * (error): FetchError | AbortError => {
60
+ * if (AbortError.is(error)) return error;
61
+ * return { type: "FetchError", error };
62
+ * },
63
+ * );
64
+ *
65
+ * // In a composition root…
66
+ * const deps: NativeFetchDep = {
67
+ * fetch: globalThis.fetch.bind(globalThis),
68
+ * };
69
+ *
70
+ * // Create a Run with dependencies.
71
+ * await using run = createRun(deps);
72
+ *
73
+ * // Running a Task returns a Fiber that can be awaited.
74
+ * const result = await run(fetch("/users/123"));
75
+ * expectTypeOf(result).toEqualTypeOf<
76
+ * Result<Response, FetchError | AbortError>
77
+ * >();
78
+ *
79
+ * // A Fiber can also be aborted (or disposed with `using`).
80
+ * const fiber = run(fetch("/users/456"));
81
+ * fiber.abort();
82
+ *
83
+ * // When this block ends, `await using` disposes the Run — aborting all Fibers.
84
+ * ```
85
+ *
86
+ * In composition roots, prefer Evolu platform `createRun` adapters when one
87
+ * exists. `@evolu/web`, `@evolu/nodejs`, and `@evolu/react-native` build on the
88
+ * common {@link createRun} and add platform-specific global error handling.
89
+ *
90
+ * ## Composition
91
+ *
92
+ * | Category | Helper | Description |
93
+ * | ---------- | ------------------ | ----------------------------------- |
94
+ * | Collection | {@link all} | fail-fast on first error |
95
+ * | | {@link allSettled} | complete all regardless of failures |
96
+ * | | {@link map} | values to Tasks, fail-fast |
97
+ * | | {@link mapSettled} | values to Tasks, complete all |
98
+ * | Timing | {@link sleep} | pause execution |
99
+ * | | {@link timeout} | time-bounded execution |
100
+ * | | {@link repeat} | repeat with schedule |
101
+ * | | {@link yieldNow} | yield to event loop |
102
+ * | Racing | {@link race} | first to complete wins |
103
+ * | | {@link any} | first success wins |
104
+ * | Resilience | {@link retry} | retry with backoff |
105
+ * | Interop | {@link callback} | wrap callback APIs |
106
+ * | | {@link fetch} | HTTP requests with abort handling |
107
+ *
108
+ * Collection helpers run sequentially by default. Use {@link concurrently} to
109
+ * run Tasks concurrently. Note helpers like {@link race} always run
110
+ * concurrently; sequential execution wouldn't make sense for their semantics.
111
+ *
112
+ * ## Style
113
+ *
114
+ * Imperative code is the preferred way to compose sequential {@link Task}
115
+ * operations inside another Task.
116
+ *
117
+ * ```ts
118
+ * const user = await run(fetchUser(id));
119
+ * if (!user.ok) return user;
120
+ *
121
+ * const profile = await run(fetchProfile(user.value.id));
122
+ * if (!profile.ok) return profile;
123
+ *
124
+ * return ok({ user: user.value, profile: profile.value });
125
+ * ```
126
+ *
127
+ * This is an intentional style choice. Evolu keeps helpers for operations with
128
+ * distinct semantics that plain control flow does not express well, such as
129
+ * concurrency, racing, retries, timeouts, and collection processing. It
130
+ * intentionally does not provide generic chain, flatMap, or pipe-style helpers
131
+ * for ordinary sequential Task composition, because that would duplicate plain
132
+ * control flow and create API ambiguity. While it may seem verbose, it is
133
+ * explicit, transparent, and avoids pipes and nested helpers, which are harder
134
+ * to debug.
135
+ *
136
+ * ### Building a better fetch
137
+ *
138
+ * Use {@link timeout} to prevent hanging:
139
+ *
140
+ * ```ts
141
+ * const fetchWithTimeout = (url: string) => timeout(fetch(url), "30s");
142
+ *
143
+ * expectTypeOf(fetchWithTimeout).toEqualTypeOf<
144
+ * (
145
+ * url: string,
146
+ * ) => Task<Response, FetchError | TimeoutError, NativeFetchDep>
147
+ * >();
148
+ * ```
149
+ *
150
+ * Add {@link retry} for resilience:
151
+ *
152
+ * ```ts
153
+ * const fetchWithRetry = (url: string) =>
154
+ * retry(
155
+ * fetchWithTimeout(url),
156
+ * // A jittered, capped, limited exponential backoff.
157
+ * jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
45
158
  * );
46
159
  *
47
- * // `satisfies` shows the expected type signature.
48
- * fetch satisfies (url: string) => Task<Response, FetchError>;
160
+ * expectTypeOf(fetchWithRetry).toEqualTypeOf<
161
+ * (
162
+ * url: string,
163
+ * ) => Task<
164
+ * Response,
165
+ * RetryError<FetchError | TimeoutError>,
166
+ * NativeFetchDep
167
+ * >
168
+ * >();
169
+ * ```
170
+ *
171
+ * Run composed tasks with {@link concurrently} and {@link map}:
172
+ *
173
+ * ```ts
174
+ * await using run = createRun();
175
+ *
176
+ * const urls = [
177
+ * "https://api.example.com/users",
178
+ * "https://api.example.com/posts",
179
+ * "https://api.example.com/comments",
180
+ * ];
181
+ *
182
+ * // At most 2 concurrent requests
183
+ * const result = await run(concurrently(2, map(urls, fetchWithRetry)));
184
+ *
185
+ * expectTypeOf(result).toEqualTypeOf<
186
+ * Result<
187
+ * readonly Response[],
188
+ * AbortError | RetryError<TimeoutError | FetchError>
189
+ * >
190
+ * >();
191
+ * ```
192
+ *
193
+ * ## Dependency Injection
194
+ *
195
+ * Assumes familiarity with
196
+ * {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}. Task
197
+ * DI is the same but without manually passing deps.
198
+ *
199
+ * Tasks declare dependencies via the `D` type parameter and access them via
200
+ * `run.deps`:
201
+ *
202
+ * ```ts
203
+ * const fetchUser =
204
+ * (id: UserId): Task<User, FetchUserError, FetchDep> =>
205
+ * async (run) => {
206
+ * const { fetch } = run.deps;
207
+ * // ...
208
+ * };
209
+ * ```
210
+ *
211
+ * Provide dependencies when creating a Run:
212
+ *
213
+ * ```ts
214
+ * const deps: FetchDep = {
215
+ * fetch: globalThis.fetch.bind(globalThis),
216
+ * };
217
+ *
218
+ * await using run = createRun(deps);
219
+ * await run(fetchUser(123));
220
+ * ```
221
+ *
222
+ * For runtime-created dependencies, use {@link Run#addDeps}.
223
+ *
224
+ * ### Built-in dependencies
225
+ *
226
+ * {@link createRun} provides default {@link RunDeps} available to all Tasks
227
+ * without declaring `D`:
228
+ *
229
+ * - {@link Console} — logging with hierarchical context via `child()`
230
+ * - {@link Time} — current time
231
+ * - {@link Random} — random number generation
232
+ * - {@link RandomBytes} — cryptographic random bytes
233
+ *
234
+ * For example, using `Console`:
235
+ *
236
+ * ```ts
237
+ * const myTask: Task<void> = async (run) => {
238
+ * const { console } = run.deps;
239
+ * console.log("started");
240
+ * // ...
241
+ * };
242
+ * ```
243
+ *
244
+ * Custom Console with formatted output:
245
+ *
246
+ * ```ts
247
+ * const deps = {
248
+ * console: createConsole({
249
+ * formatter: createConsoleFormatter()({
250
+ * timestampFormat: "absolute",
251
+ * }),
252
+ * }),
253
+ * };
254
+ *
255
+ * await using run = createRun(deps);
256
+ *
257
+ * const console = run.deps.console.child("main");
258
+ *
259
+ * console.log("started");
260
+ * // 21:20:25.588 [main] started
261
+ * ```
262
+ *
263
+ * For testing, use {@link testCreateRun} to get deterministic, controllable
264
+ * implementations of all RunDeps.
265
+ *
266
+ * ## Resource management
267
+ *
268
+ * Evolu uses standard JavaScript
269
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
270
+ *
271
+ * Use the
272
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack | AsyncDisposableStack}
273
+ * for async resource ownership.
274
+ *
275
+ * ### Example
276
+ *
277
+ * ```ts
278
+ * await using stack = new AsyncDisposableStack();
279
+ *
280
+ * const fooResult = await run(createFoo());
281
+ * if (!fooResult.ok) return fooResult;
282
+ * const foo = stack.use(fooResult.value);
283
+ *
284
+ * stack.defer(async () => {
285
+ * await foo.close();
286
+ * });
287
+ * stack.adopt(session, async (session) => {
288
+ * await session.logout();
289
+ * });
290
+ * ```
291
+ *
292
+ * ## Awaitable
293
+ *
294
+ * ```ts
295
+ * type Awaitable<T> = T | PromiseLike<T>;
296
+ * ```
297
+ *
298
+ * Even though {@link Task} returns {@link Awaitable} (allowing sync or async
299
+ * results), the {@link Run} itself is always async. This is a deliberate design
300
+ * choice:
301
+ *
302
+ * - **Sync** → {@link Result}, native `using` / `DisposableStack`
303
+ * - **Async** → {@link Task}, {@link Run}, {@link Fiber}, `await using` /
304
+ * `AsyncDisposableStack`
305
+ *
306
+ * Benefits:
307
+ *
308
+ * - **No API ambiguity** — Task means async, Result means sync
309
+ * - **Zero overhead** — sync code stays with zero overhead
310
+ *
311
+ * While a unified sync/async API is technically possible — with
312
+ * {@link isPromiseLike} detection and two-phase disposal (sync first, async if
313
+ * needed, and a flag for callers) — Evolu prefers plain functions for sync code
314
+ * because most operations involve I/O, which is inherently async, and when we
315
+ * need sync, it's for simplicity (ideally no dependencies) and performance
316
+ * (zero abstraction overhead).
317
+ *
318
+ * Sync functions should be fast, so there's no need to monitor them. They
319
+ * should take values, not dependencies — following the
320
+ * {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
321
+ * pattern where impure code gathers data, pure functions process it, and impure
322
+ * code performs effects with the result. Sync functions taking deps often
323
+ * indicate a design that could be improved — for example, a function taking
324
+ * {@link Random} could instead accept {@link RandomNumber} as a value.
325
+ *
326
+ * Slow sync operations (parsing large JSON, sorting millions of items, complex
327
+ * cryptography) belong in workers. The async boundary to the worker is a
328
+ * {@link Task} with full monitoring — timeout, cancellation, tracing. The sync
329
+ * code inside the worker needs no monitoring; the async call to the worker
330
+ * provides it.
331
+ *
332
+ * ## Glossary
333
+ *
334
+ * - **Cleanup** — generic umbrella term when the exact lifecycle operation is not
335
+ * important.
336
+ * - **Dispose / disposal** — owner-driven cleanup via JavaScript resource
337
+ * management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
338
+ * `AsyncDisposableStack`).
339
+ * - **Create** — construct a new value or a resource.
340
+ * - **Acquire** — obtain a usable resource. Acquisition may create a new
341
+ * resource, borrow one, open one, or take a lease/lock.
342
+ * - **Release** — relinquish a previously acquired resource or lease. Release
343
+ * pairs with acquire and need not mean disposal; examples include unlock,
344
+ * logout, or returning a pooled resource.
345
+ *
346
+ * ## FAQ
347
+ *
348
+ * ### Why can `Task<T, never, D>` still return `AbortError`?
349
+ *
350
+ * The `E` type parameter represents domain errors, not abort control flow.
351
+ *
352
+ * `AbortError` comes from the {@link Run} runtime. A Task can still return it:
353
+ *
354
+ * - Before execution, when the parent or root {@link Run} is already stopped
355
+ * - During execution, when an abortable Task is aborted
356
+ * - At settlement, when abort was requested before the Task result was observed
357
+ *
358
+ * So `Task<T, never, D>` means "no domain errors", not "cannot fail at all".
359
+ *
360
+ * This also applies to {@link unabortable}. `unabortable(task)` only prevents
361
+ * abort from interrupting the Task after it has started running. It cannot
362
+ * force execution to start on a parent or root {@link Run} that is already
363
+ * disposing or settled, so it can still return {@link AbortError} before
364
+ * execution begins. If that abort would indicate a lifecycle bug in your code,
365
+ * use {@link assertNotAborted} to crash immediately instead of threading the
366
+ * impossible case through domain logic.
367
+ *
368
+ * ### How do I type an anonymous Task callback?
369
+ *
370
+ * For one-off inline Tasks, put the type arguments on the {@link Run} call:
371
+ *
372
+ * ```ts
373
+ * run<void, never>(async (run) => {
374
+ * const result = await run(waitForLeader);
375
+ * if (!result.ok) return result;
376
+ *
377
+ * doSomething(result.value);
378
+ * return ok();
379
+ * });
380
+ * ```
381
+ *
382
+ * This constrains the callback to the same shape as `Task<void, never, D>`
383
+ * without extracting a named constant.
384
+ *
385
+ * This pattern is useful for child Tasks started at the call site, especially
386
+ * when success has no meaningful value and domain errors are not expected. In
387
+ * that case, `void, never` documents an abort-only Task whose result can be
388
+ * ignored while the parent {@link Run} still owns its lifecycle.
389
+ *
390
+ * If the Task is reused, exported, or deserves a name, prefer an explicit
391
+ * `const task: Task<T, E, D> = async (run) => { ... }` instead.
392
+ *
393
+ * ### Where is fork and join?
394
+ *
395
+ * For those familiar with other structured concurrency implementations:
396
+ *
397
+ * - **Fork** — `run(task)` creates a {@link Fiber}
398
+ * - **Join** — `await fiber` waits for completion
399
+ *
400
+ * @group Core Types
401
+ */
402
+ export type Task<T, E = never, D = unknown> = (run: Run<D>) => Awaitable<Result<T, E | AbortError>>;
403
+ /**
404
+ * Shorthand for a {@link Task} with `any` type parameters.
405
+ *
406
+ * @group Type utilities
407
+ */
408
+ export type AnyTask = Task<any, any, any>;
409
+ /**
410
+ * Extracts the value type from a {@link Task}.
411
+ *
412
+ * @group Type utilities
413
+ */
414
+ export type InferTaskOk<R extends AnyTask> = R extends Task<infer T, any, any> ? T : never;
415
+ /**
416
+ * Extracts the error type from a {@link Task}.
417
+ *
418
+ * @group Type utilities
419
+ */
420
+ export type InferTaskErr<R extends AnyTask> = R extends Task<any, infer E, any> ? E : never;
421
+ /**
422
+ * Extracts the deps type from a {@link Task}.
423
+ *
424
+ * @group Type utilities
425
+ */
426
+ export type InferTaskDeps<R extends AnyTask> = R extends Task<any, any, infer D> ? D : never;
427
+ /**
428
+ * A {@link Task} that can complete with a value, signal done, or fail.
429
+ *
430
+ * Forms a pair with {@link NextResult}:
431
+ *
432
+ * - `Result<A, E>` → `NextResult<A, E, D>`
433
+ * - `Task<T, E>` → `NextTask<T, E, D>`
434
+ *
435
+ * Use for pull-based protocols like iterators where `Done<D>` signals normal
436
+ * completion rather than an error.
437
+ *
438
+ * @group Core Types
439
+ */
440
+ export type NextTask<T, E = never, D = void> = Task<T, E | Done<D>>;
441
+ /**
442
+ * Extracts the done value type from a {@link NextTask}.
443
+ *
444
+ * @group Type utilities
445
+ */
446
+ export type InferTaskDone<T extends AnyTask> = InferTaskErr<T> extends infer Errors ? Errors extends Done<infer D> ? D : never : never;
447
+ /**
448
+ * Error returned when a {@link Task} is aborted via
449
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | AbortSignal}.
450
+ *
451
+ * This is structured-concurrency control flow, not a domain error. It plays a
452
+ * role similar to an interrupt: in most code, propagate it unchanged or ignore
453
+ * it when cleanup is already handled by the runtime.
454
+ *
455
+ * The `reason` field is `unknown` by design — use typed errors for business
456
+ * logic. If you need to inspect the reason, use type guards like
457
+ * `RaceLostError.is(reason)`.
458
+ *
459
+ * When a piece of logic must continue once started, wrap it with
460
+ * {@link unabortable}. That prevents ordinary mid-flight interruption, but it
461
+ * does not erase {@link AbortError} from the type because the Task can still be
462
+ * rejected before it starts, for example on an already-stopped {@link Run}. In
463
+ * those cases, abort usually indicates a lifecycle bug, so use
464
+ * `assertNotAborted` if you want to fail fast.
465
+ *
466
+ * @group Core Types
467
+ */
468
+ export declare const AbortError: import("./Type.js").TypedType<"AbortError", {
469
+ reason: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
470
+ }>;
471
+ export interface AbortError extends InferType<typeof AbortError> {
472
+ }
473
+ /**
474
+ * Runs a {@link Task} with
475
+ * {@link https://en.wikipedia.org/wiki/Structured_concurrency | structured concurrency}
476
+ * semantics.
477
+ *
478
+ * Each `Run` forms a Task tree: child Tasks are bound to it, abort propagates
479
+ * through that tree, and state is observable via snapshots and events.
480
+ *
481
+ * `Run` is a callable object — callable because it's convenient to run Tasks as
482
+ * `run(task)`, and an object because it holds state.
483
+ *
484
+ * Calling `run(task)` creates a child `Run`, passes it to the Task, and returns
485
+ * a {@link Fiber}. The child is tracked in `getChildren()`/events while running,
486
+ * then disposed and removed when settled.
487
+ *
488
+ * Before Task execution, `run(task)` applies two short-circuit checks:
489
+ *
490
+ * - If this Run is not `Running`, the child is aborted with
491
+ * {@link runStoppedError} and the Task is replaced with `err(AbortError)`.
492
+ * - If this Run's signal is already aborted and the child is abortable
493
+ * (`abortMask === 0`), the child is aborted with the same reason and the Task
494
+ * is replaced with `err(AbortError)`.
495
+ *
496
+ * After execution, the child stores both values: `outcome` (what the Task
497
+ * returned) and `result` (what callers observe). If the child signal is aborted
498
+ * at settlement time, `result` is forced to `err(AbortError)` even when
499
+ * `outcome` is `ok(...)`.
500
+ *
501
+ * That's the whole mechanism: {@link Task} is a function that takes a `Run` and
502
+ * returns an {@link Awaitable} {@link Result}. `run(task)` runs the Task via
503
+ * `Promise.try(task, run)` with aforementioned logic.
504
+ *
505
+ * @group Core Types
506
+ * @see {@link createRun}
507
+ * @see {@link Task}
508
+ */
509
+ export interface Run<D = unknown> extends AsyncDisposable {
510
+ /** Runs a {@link Task} and returns a {@link Fiber} handle. */
511
+ <T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
512
+ /**
513
+ * Runs a {@link Task} and throws if the returned {@link Result} is an error.
514
+ *
515
+ * Use this where failure should crash the current flow instead of being
516
+ * handled locally.
517
+ *
518
+ * This is the async equivalent of {@link getOrThrow}. It runs the Task, awaits
519
+ * its {@link Result}, and returns the value on success.
520
+ *
521
+ * **When to use:**
522
+ *
523
+ * - Application startup or composition-root setup where errors must stop the
524
+ * program immediately. In Evolu apps, errors are handled by
525
+ * platform-specific `createRun` adapters at the app boundary.
526
+ * - Module-level constants
527
+ * - Test setup with values that are expected to be valid
528
+ *
529
+ * Prefer `await run(task)` with an explicit `if (!result.ok)` check in
530
+ * ordinary application logic where the caller can recover, retry, or choose a
531
+ * different flow.
532
+ *
533
+ * Throws: `Error` with the original Task error attached as `cause`.
534
+ */
535
+ readonly orThrow: <T, E>(task: Task<T, E, D>) => Promise<T>;
536
+ /** Unique {@link Id} for this Run. */
537
+ readonly id: Id;
538
+ /** The parent {@link Run}, if this Run was created as a child. */
539
+ readonly parent: Run<D> | null;
540
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal */
541
+ readonly signal: AbortSignal;
542
+ /** The abort mask depth. `0` means abortable, `>= 1` means unabortable. */
543
+ readonly abortMask: AbortMask;
544
+ /**
545
+ * Registers a callback to run when abort is requested.
546
+ *
547
+ * This is a convenience wrapper around subscribing to this Run's abort
548
+ * signal. The callback receives the abort reason extracted from
549
+ * {@link AbortError.reason} rather than the whole {@link AbortError}.
550
+ *
551
+ * If already aborted, the callback is invoked immediately. For
552
+ * {@link unabortable} Tasks, the callback is never invoked because their
553
+ * signal never aborts.
554
+ *
555
+ * Intentionally synchronous and not awaited. The callback runs in the abort
556
+ * request path, which may already be transitioning this Run to `Disposing` or
557
+ * `Settled`, so it is too late to start normal Tasks from there.
558
+ *
559
+ * Use for immediate abort-time reactions such as removing listeners, clearing
560
+ * timers, removing waiters from queues, or resolving pending promises. Do not
561
+ * use it for awaited cleanup or resource ownership. For that, use standard
562
+ * JavaScript resource management with `AsyncDisposableStack`.
563
+ */
564
+ readonly onAbort: (callback: Callback<unknown>) => void;
565
+ /** Returns the current {@link RunState}. */
566
+ readonly getState: () => RunState;
567
+ /** Returns the current child {@link Fiber}s. */
568
+ readonly getChildren: () => ReadonlySet<Fiber<any, any, D>>;
569
+ /**
570
+ * Creates a memoized {@link RunSnapshot} of this Run.
571
+ *
572
+ * Use for monitoring, debugging, or building UI that visualizes Task trees.
573
+ *
574
+ * ### Example
575
+ *
576
+ * ```ts
577
+ * // React integration with useSyncExternalStore
578
+ * const useRunSnapshot = (run: Run) =>
579
+ * useSyncExternalStore(
580
+ * (callback) => {
581
+ * run.onEvent = callback;
582
+ * return () => {
583
+ * run.onEvent = undefined;
584
+ * };
585
+ * },
586
+ * () => run.snapshot(),
587
+ * );
588
+ * ```
589
+ */
590
+ readonly snapshot: () => RunSnapshot;
591
+ /**
592
+ * Callback for monitoring Run events.
593
+ *
594
+ * Called when this Run or any descendant emits a {@link RunEvent}. Events
595
+ * bubble up through parent runs, enabling centralized monitoring. Only
596
+ * emitted when {@link RunConfig.eventsEnabled} is `true`.
597
+ */
598
+ onEvent: ((event: RunEvent) => void) | undefined;
599
+ /**
600
+ * The root {@link Run} of this Task tree.
601
+ *
602
+ * It is called `daemon` because that is how it should be used: for
603
+ * long-running work that must not be disposed when the current Task settles.
604
+ * Normal child Runs are disposed by their parent when they settle. The root
605
+ * Run has no parent, so work started with `run.daemon(task)` is attached to
606
+ * that root Run instead of the current Run and keeps running until the root
607
+ * Run is disposed manually.
608
+ *
609
+ * In application code, that usually means disposing the root Run on process
610
+ * shutdown in Node.js or when another platform-specific lifecycle hook is
611
+ * available. Browsers do not provide a fully reliable app termination hook.
612
+ *
613
+ * ### Example
614
+ *
615
+ * ```ts
616
+ * const myTask: Task<void, never> = async (run) => {
617
+ * // Aborted when myTask ends
618
+ * run(helperTask);
619
+ *
620
+ * // Outlives myTask, aborted when the root Run is disposed
621
+ * const backgroundFiber = run.daemon(backgroundSync);
622
+ *
623
+ * // Can still be aborted manually if needed
624
+ * backgroundFiber.abort();
625
+ *
626
+ * return ok();
627
+ * };
628
+ * ```
629
+ *
630
+ * For a long-lived reusable {@link Run}, use {@link Run.create}.
631
+ */
632
+ readonly daemon: Run<D>;
633
+ /**
634
+ * Creates a {@link Run} from this Run.
635
+ *
636
+ * Like {@link createRun}, the returned Run is daemon: it stays running until
637
+ * disposed. Unlike {@link createRun}, it shares the same Deps as this Run.
638
+ *
639
+ * Use this for long-lived disposable resources that need to own async work.
640
+ * The resource creates one internal Run with `run.create()` and uses that Run
641
+ * for all of its work. Disposing the resource then disposes that internal
642
+ * Run, which aborts in-flight child Tasks, waits for them to settle, and
643
+ * rejects later calls through it.
644
+ *
645
+ * Typical examples are database clients, connection pools, workers, or other
646
+ * reusable resources with async methods and an async dispose operation.
647
+ *
648
+ * To run a single Task as daemon, use {@link Run.daemon}.
649
+ */
650
+ readonly create: () => Run<D>;
651
+ /** Returns the dependencies passed to {@link createRun}. */
652
+ readonly deps: RunDeps & D;
653
+ /**
654
+ * @see {@link Concurrency}
655
+ * @see {@link concurrently}
656
+ */
657
+ readonly concurrency: Concurrency;
658
+ /**
659
+ * Adds additional dependencies to this Run and returns it.
660
+ *
661
+ * Use for runtime-created dependencies — dependencies that cannot be created
662
+ * in the composition root (e.g., app start).
663
+ *
664
+ * ### Example
665
+ *
666
+ * ```ts
667
+ * // One-shot
668
+ * await run.addDeps({ db })(getUser(123));
669
+ *
670
+ * // Multiple deps at once
671
+ * await run.addDeps({ db, cache })(task);
672
+ *
673
+ * // Reusable — config comes from outside (message, file, etc.)
674
+ * type DbWorkerDeps = DbDep; // or DbDep & CacheDep & ...
675
+ *
676
+ * const init =
677
+ * (config: Config): Task<void, InitError, CreateDbDep> =>
678
+ * async (run) => {
679
+ * const { createDb } = run.deps;
680
+ * await using stack = new AsyncDisposableStack();
681
+ *
682
+ * const db = stack.use(await run.orThrow(startApp()));
683
+ * if (!db.ok) return db;
684
+ *
685
+ * const runWithDb = run.addDeps({ db: db.value });
686
+ *
687
+ * await runWithDb(getUser(123));
688
+ * await runWithDb(insertUser(user));
689
+ * return ok();
690
+ * };
691
+ * ```
692
+ *
693
+ * ## FAQ
694
+ *
695
+ * ### How does it work?
696
+ *
697
+ * This is the whole implementation:
698
+ *
699
+ * ```ts
700
+ * run.addDeps = <E extends NewKeys<E, D>>(newDeps: E): Run<D & E> => {
701
+ * depsRef.modify((currentDeps) => {
702
+ * const duplicate = Object.keys(newDeps).find(
703
+ * (k) => k in currentDeps,
704
+ * );
705
+ * assert(!duplicate, `Dependency '${duplicate}' already added.`);
706
+ * return [undefined, { ...currentDeps, ...newDeps }];
707
+ * });
708
+ * return self as unknown as Run<D & E>;
709
+ * };
710
+ * ```
711
+ *
712
+ * Dependencies are stored in a shared {@link Ref}, so `addDeps` propagates to
713
+ * all runs. The runtime assertion ensures dependencies are created once —
714
+ * automatic deduplication would mask poor design (dependencies should have a
715
+ * single, clear point of creation).
716
+ */
717
+ readonly addDeps: <E extends NewKeys<E, D>>(extraDeps: E) => Run<D & E>;
718
+ }
719
+ /**
720
+ * `Fiber` is a handle to a running {@link Task} that can be awaited, aborted, or
721
+ * disposed.
722
+ *
723
+ * ### Example
724
+ *
725
+ * ```ts
726
+ * await using run = createRun();
727
+ *
728
+ * // Await to get Result
729
+ * const result = await run(fetchData);
49
730
  *
50
- * // Add timeout to prevent hanging
51
- * const fetchWithTimeout = (url: string) => timeout("30s", fetch(url));
731
+ * // Abort manually
732
+ * const fiber = run(longRunningTask);
733
+ * fiber.abort();
734
+ * const aborted = await fiber; // Result contains AbortError (unless unabortable)
52
735
  *
53
- * fetchWithTimeout satisfies (
54
- * url: string,
55
- * ) => Task<Response, TimeoutError | FetchError>;
736
+ * // Auto-abort with `using`
737
+ * {
738
+ * using background = run(backgroundSync);
739
+ * await someOtherWork();
740
+ * } // background.abort() called automatically here
741
+ *
742
+ * // Run child tasks in fiber's scope
743
+ * fiber.run(childTask);
744
+ *
745
+ * // Monitor via the Run
746
+ * fiber.run.onEvent = (event) => {
747
+ * // handle event
748
+ * };
749
+ * ```
750
+ *
751
+ * Because `Fiber` is a {@link PromiseLike} object, Fibers can be composed with
752
+ * `Promise.all`, `Promise.race`, etc.
753
+ *
754
+ * Microtask timing: Run wraps the Task's promise with `.then` and `.finally`,
755
+ * which adds microtasks between Task completion and Fiber settlement. Do not
756
+ * write code that relies on a specific number of microtask yields between
757
+ * Tasks. Use explicit synchronization primitives instead.
758
+ *
759
+ * @group Core Types
760
+ */
761
+ export interface Fiber<T = unknown, E = unknown, D = unknown> extends PromiseLike<Result<T, E | AbortError>>, Disposable {
762
+ readonly then: PromiseLike<Result<T, E | AbortError>>["then"];
763
+ /**
764
+ * A {@link Run} of this Fiber.
765
+ *
766
+ * Tasks run via this Run are aborted when the Fiber ends.
767
+ *
768
+ * ### Example
769
+ *
770
+ * ```ts
771
+ * const fiber = run(longRunningTask);
772
+ *
773
+ * // helperTask is aborted when longRunningTask ends
774
+ * fiber.run(helperTask);
775
+ *
776
+ * // Monitor this Fiber's Run
777
+ * fiber.run.onEvent = (event) => {
778
+ * console.log(event);
779
+ * };
780
+ * ```
781
+ */
782
+ readonly run: Run<D>;
783
+ /**
784
+ * Requests abort for this Fiber (and any child it started).
785
+ *
786
+ * ### Example
787
+ *
788
+ * ```ts
789
+ * const fiber = run(fetchData);
790
+ * fiber.abort();
791
+ * const result = await fiber; // err(AbortError)
792
+ * ```
793
+ *
794
+ * When abort is requested, the Fiber's result becomes {@link AbortError} even
795
+ * if the Task completed successfully. This keeps behavior predictable —
796
+ * calling `abort()` always yields `AbortError`.
797
+ *
798
+ * The optional reason is stored in `AbortError.reason`. Since any value can
799
+ * be passed, abort reasons are `unknown` — use typed errors for business
800
+ * logic. To inspect the reason, use type guards like
801
+ * `RaceLostError.is(reason)`.
802
+ *
803
+ * Abort is idempotent — calling multiple times has no additional effect
804
+ * beyond the first call.
805
+ */
806
+ abort(reason?: unknown): void;
807
+ /** Returns the current {@link RunState} of this Fiber's {@link Run}. */
808
+ getState(): RunState<T, E>;
809
+ }
810
+ /**
811
+ * Extracts the value type from a {@link Fiber}.
812
+ *
813
+ * @group Type utilities
814
+ */
815
+ export type InferFiberOk<F extends Fiber<any, any, any>> = F extends Fiber<infer T, any, any> ? T : never;
816
+ /**
817
+ * Extracts the error type from a {@link Fiber}.
818
+ *
819
+ * @group Type utilities
820
+ */
821
+ export type InferFiberErr<F extends Fiber<any, any, any>> = F extends Fiber<any, infer E, any> ? E : never;
822
+ /**
823
+ * Extracts the deps type from a {@link Fiber}.
824
+ *
825
+ * @group Type utilities
826
+ */
827
+ export type InferFiberDeps<F extends Fiber<any, any, any>> = F extends Fiber<any, any, infer D> ? D : never;
828
+ /**
829
+ * Abort mask depth for a {@link Run} or {@link Fiber}.
830
+ *
831
+ * - `0` — abortable (default)
832
+ * - `>= 1` — inside {@link unabortable}, abort requests are ignored
833
+ *
834
+ * The mask tracks nested unabortable regions. When abort is requested, the
835
+ * signal only propagates if `mask === 0`.
836
+ *
837
+ * - {@link unabortable} increments the mask — Task becomes protected
838
+ * - {@link unabortableMask} provides `restore` to restore the previous mask
839
+ * - Tasks inherit their parent's mask by default
840
+ *
841
+ * This enables nested resource lifecycle patterns where each level can have its
842
+ * own abortable section while outer acquisitions remain protected.
843
+ *
844
+ * UI/debugging tools can use this to visually distinguish protected Tasks
845
+ * (e.g., different icon or color) and explain why abort requests are ignored.
846
+ *
847
+ * @group Abort masking
848
+ */
849
+ export declare const AbortMask: import("./Type.js").BrandType<import("./Type.js").BrandType<import("./Type.js").Type<"Brand", number & import("./Brand.js").Brand<"Int">, number, import("./Type.js").IntError, number, import("./Type.js").NumberError>, "NonNegative", import("./Type.js").NonNegativeError, import("./Type.js").NumberError | import("./Type.js").IntError>, "AbortMask", import("./Type.js").BrandWithoutRefineError<"AbortMask", import("./Type.js").NumberError | import("./Type.js").NonNegativeError | import("./Type.js").IntError>, never>;
850
+ export type AbortMask = typeof AbortMask.Type;
851
+ /**
852
+ * Maximum number of concurrent Tasks.
853
+ *
854
+ * Default is 1 (sequential). Use 1-100 as a literal or {@link PositiveInt} for
855
+ * larger values.
856
+ *
857
+ * @group Concurrency primitives
858
+ * @see {@link concurrently}
859
+ * @see {@link createSemaphore}
860
+ */
861
+ export type Concurrency = Int1To100 | PositiveInt;
862
+ /**
863
+ * The lifecycle state of a {@link Run}.
864
+ *
865
+ * - `Running` — Task running, no result yet
866
+ * - `Disposing` — abort requested, waiting for children to settle
867
+ * - `Settled` — settled with result and outcome
868
+ *
869
+ * @group Core Types
870
+ */
871
+ export type RunState<T = unknown, E = unknown> = RunStateRunning | RunStateDisposing | RunStateSettled<T, E>;
872
+ export interface RunStateRunning extends Typed<"Running"> {
873
+ }
874
+ export interface RunStateDisposing extends Typed<"Disposing"> {
875
+ }
876
+ export interface RunStateSettled<T = unknown, E = unknown> extends Typed<"Settled"> {
877
+ /**
878
+ * The Run's completion value.
879
+ *
880
+ * If abort was requested, this is {@link AbortError} even if the Task
881
+ * completed successfully — see `outcome` for what the Task actually
882
+ * returned.
883
+ */
884
+ readonly result: Result<T, E>;
885
+ /**
886
+ * What the Task actually returned.
887
+ *
888
+ * Unlike `result`, not overridden by abort.
889
+ */
890
+ readonly outcome: Result<T, E>;
891
+ }
892
+ /**
893
+ * {@link RunSnapshot} state Type.
894
+ *
895
+ * @group Monitoring
896
+ */
897
+ export declare const RunSnapshotState: import("./Type.js").UnionType<[import("./Type.js").TypedType<"Running", Record<never, never>>, import("./Type.js").TypedType<"Disposing", Record<never, never>>, import("./Type.js").TypedType<"Settled", {
898
+ result: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
899
+ ok: import("./Type.js").LiteralType<true>;
900
+ value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
901
+ }>, import("./Type.js").ObjectType<{
902
+ ok: import("./Type.js").LiteralType<false>;
903
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
904
+ }>]>;
905
+ outcome: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
906
+ ok: import("./Type.js").LiteralType<true>;
907
+ value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
908
+ }>, import("./Type.js").ObjectType<{
909
+ ok: import("./Type.js").LiteralType<false>;
910
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
911
+ }>]>;
912
+ }>]>;
913
+ export type RunSnapshotState = typeof RunSnapshotState.Type;
914
+ /**
915
+ * A recursive snapshot of a {@link Run} tree.
916
+ *
917
+ * Snapshots use structural sharing — unchanged subtrees return the same object
918
+ * reference. This is useful for UI libraries like React that leverage
919
+ * referential transparency to skip re-rendering unchanged parts. Snapshots are
920
+ * computed on demand rather than pushed on every change. Push would require
921
+ * O(depth) new snapshot objects per mutation.
922
+ *
923
+ * @group Core Types
924
+ * @see {@link Run.snapshot}
925
+ */
926
+ export interface RunSnapshot {
927
+ /** The {@link Run.id} this snapshot represents. */
928
+ readonly id: Id;
929
+ /** The current lifecycle state. */
930
+ readonly state: RunSnapshotState;
931
+ /** Child snapshots in run order. */
932
+ readonly children: ReadonlyArray<RunSnapshot>;
933
+ /** The abort mask depth. `0` means abortable, `>= 1` means unabortable. */
934
+ readonly abortMask: AbortMask;
935
+ }
936
+ /**
937
+ * The event-specific payload of a {@link RunEvent}.
938
+ *
939
+ * @group Monitoring
940
+ */
941
+ export declare const RunEventData: import("./Type.js").UnionType<[import("./Type.js").TypedType<"ChildAdded", {
942
+ childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
943
+ }>, import("./Type.js").TypedType<"ChildRemoved", {
944
+ childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
945
+ }>, import("./Type.js").TypedType<"StateChanged", {
946
+ state: import("./Type.js").UnionType<[import("./Type.js").TypedType<"Running", Record<never, never>>, import("./Type.js").TypedType<"Disposing", Record<never, never>>, import("./Type.js").TypedType<"Settled", {
947
+ result: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
948
+ ok: import("./Type.js").LiteralType<true>;
949
+ value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
950
+ }>, import("./Type.js").ObjectType<{
951
+ ok: import("./Type.js").LiteralType<false>;
952
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
953
+ }>]>;
954
+ outcome: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
955
+ ok: import("./Type.js").LiteralType<true>;
956
+ value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
957
+ }>, import("./Type.js").ObjectType<{
958
+ ok: import("./Type.js").LiteralType<false>;
959
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
960
+ }>]>;
961
+ }>]>;
962
+ }>]>;
963
+ export type RunEventData = typeof RunEventData.Type;
964
+ /**
965
+ * Events emitted by a {@link Run} for monitoring and debugging.
966
+ *
967
+ * Events bubble up through parent runs, enabling centralized monitoring at the
968
+ * root. Use with {@link Run.onEvent} to track Run lifecycle.
969
+ *
970
+ * @group Monitoring
971
+ */
972
+ export declare const RunEvent: import("./Type.js").ObjectType<{
973
+ id: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
974
+ timestamp: import("./Type.js").BrandType<import("./Type.js").BrandType<import("./Type.js").Type<"Brand", number & import("./Brand.js").Brand<"Int"> & import("./Brand.js").Brand<"NonNegative">, number, import("./Type.js").NonNegativeError, number & import("./Brand.js").Brand<"Int">, import("./Type.js").NumberError | import("./Type.js").IntError>, "LessThan281474976710655", import("./Type.js").LessThanError<281474976710655>, import("./Type.js").NumberError | import("./Type.js").NonNegativeError | import("./Type.js").IntError>, "Millis", import("./Type.js").BrandWithoutRefineError<"Millis", import("./Type.js").NumberError | import("./Type.js").LessThanError<281474976710655> | import("./Type.js").NonNegativeError | import("./Type.js").IntError>, never>;
975
+ data: import("./Type.js").UnionType<[import("./Type.js").TypedType<"ChildAdded", {
976
+ childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
977
+ }>, import("./Type.js").TypedType<"ChildRemoved", {
978
+ childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
979
+ }>, import("./Type.js").TypedType<"StateChanged", {
980
+ state: import("./Type.js").UnionType<[import("./Type.js").TypedType<"Running", Record<never, never>>, import("./Type.js").TypedType<"Disposing", Record<never, never>>, import("./Type.js").TypedType<"Settled", {
981
+ result: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
982
+ ok: import("./Type.js").LiteralType<true>;
983
+ value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
984
+ }>, import("./Type.js").ObjectType<{
985
+ ok: import("./Type.js").LiteralType<false>;
986
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
987
+ }>]>;
988
+ outcome: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
989
+ ok: import("./Type.js").LiteralType<true>;
990
+ value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
991
+ }>, import("./Type.js").ObjectType<{
992
+ ok: import("./Type.js").LiteralType<false>;
993
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
994
+ }>]>;
995
+ }>]>;
996
+ }>]>;
997
+ }>;
998
+ export interface RunEvent extends InferType<typeof RunEvent> {
999
+ }
1000
+ /**
1001
+ * Configuration for {@link Run} behavior.
1002
+ *
1003
+ * @group Monitoring
1004
+ */
1005
+ export interface RunConfig {
1006
+ /**
1007
+ * Whether to emit {@link RunEvent}s.
1008
+ *
1009
+ * Use a {@link Ref} to enable/disable at runtime without recreating the Run.
1010
+ * Disabled by default for zero overhead in production.
1011
+ */
1012
+ readonly eventsEnabled: Ref<boolean>;
1013
+ }
1014
+ export interface RunConfigDep {
1015
+ readonly runConfig: RunConfig;
1016
+ }
1017
+ /** Default deps provided by {@link createRun}. */
1018
+ export type RunDeps = ConsoleDep & RandomBytesDep & RandomDep & TimeDep & Partial<RunConfigDep>;
1019
+ /**
1020
+ * Factory type for creating root {@link Run} instances.
1021
+ *
1022
+ * @group Creating Run
1023
+ */
1024
+ export interface CreateRun<BaseDeps> {
1025
+ (): Run<BaseDeps>;
1026
+ <D>(deps: D): Run<BaseDeps & D>;
1027
+ }
1028
+ /**
1029
+ * Creates root {@link Run}.
1030
+ *
1031
+ * The root Run is also the daemon Run: it stays running until disposed. Child
1032
+ * Runs created by `run(task)` are disposed by their parent once they settle.
1033
+ *
1034
+ * Call once per entry point (main thread, worker, etc.) and dispose on
1035
+ * shutdown. All Tasks run as descendants of this root Run.
1036
+ *
1037
+ * This common {@link createRun} is platform-agnostic. At application entry
1038
+ * points, prefer the platform adapter when one exists. `@evolu/web` adds
1039
+ * browser `error` and `unhandledrejection` handlers, `@evolu/nodejs` adds
1040
+ * Node.js `uncaughtException`, `unhandledRejection`, and graceful shutdown
1041
+ * handling, and `@evolu/react-native` adds React Native global error handling.
1042
+ *
1043
+ * {@link RunDeps} provides default dependencies:
1044
+ *
1045
+ * - {@link Time}
1046
+ * - {@link Console}
1047
+ * - {@link Random}
1048
+ * - {@link RandomBytes}
1049
+ *
1050
+ * ### Example
1051
+ *
1052
+ * ```ts
1053
+ * // App entry point
1054
+ * await using run = createRun();
1055
+ *
1056
+ * const result = await run(fetchData);
1057
+ * ```
1058
+ *
1059
+ * ### Example with custom dependencies
1060
+ *
1061
+ * ```ts
1062
+ * // Define dependency interfaces
1063
+ * interface Config {
1064
+ * readonly apiUrl: string;
1065
+ * }
1066
+ *
1067
+ * interface ConfigDep {
1068
+ * readonly config: Config;
1069
+ * }
1070
+ *
1071
+ * // Task declares its dependencies via the D type parameter
1072
+ * const fetchUser =
1073
+ * (id: string): Task<User, FetchError, ConfigDep> =>
1074
+ * async (run) => {
1075
+ * const { config } = run.deps;
1076
+ * const response = await fetch(`${config.apiUrl}/users/${id}`);
1077
+ * // ...
1078
+ * };
1079
+ *
1080
+ * // Composition root: create a Run with custom deps
1081
+ * type AppDeps = RunDeps & ConfigDep;
1082
+ *
1083
+ * const appDeps: AppDeps = {
1084
+ * ...testCreateDeps(), // or spread individual deps
1085
+ * config: { apiUrl: "https://api.example.com" },
1086
+ * };
1087
+ *
1088
+ * await using run = createRun(appDeps);
1089
+ *
1090
+ * // Run type is inferred from the deps argument
1091
+ * const result = await run(fetchUser("123"));
1092
+ *
1093
+ * // TypeScript catches missing deps at compile time:
1094
+ * // await using run2 = createRun(); // Run<RunDeps>
1095
+ * // run2(fetchUser("123")); // Error: Property 'config' is missing
1096
+ * ```
1097
+ *
1098
+ * @group Creating Run
1099
+ */
1100
+ export declare const createRun: CreateRun<RunDeps>;
1101
+ /**
1102
+ * Abort reason indicating a {@link Run} can no longer start new Tasks.
1103
+ *
1104
+ * Covers both disposing and settled Runs.
1105
+ *
1106
+ * @group Creating Run
1107
+ */
1108
+ export declare const RunStoppedError: import("./Type.js").TypedType<"RunStoppedError", Record<never, never>>;
1109
+ export interface RunStoppedError extends InferType<typeof RunStoppedError> {
1110
+ }
1111
+ /**
1112
+ * Shared {@link RunStoppedError} instance used as the default
1113
+ * {@link AbortError.reason} when a Task is started on a non-running {@link Run}.
1114
+ *
1115
+ * @group Creating Run
1116
+ */
1117
+ export declare const runStoppedError: RunStoppedError;
1118
+ /**
1119
+ * Makes a {@link Task} unabortable.
1120
+ *
1121
+ * Once started, an unabortable Task always completes — abort requests are
1122
+ * ignored and `signal.aborted` remains `false`.
1123
+ *
1124
+ * If the parent {@link Run} is already disposing or settled, `run(task)`
1125
+ * short-circuits before task execution and returns `err(AbortError)` with
1126
+ * {@link runStoppedError} as reason. So `unabortable` means “do not interrupt
1127
+ * this Task once it has started”, not “remove AbortError from its type”.
1128
+ *
1129
+ * When that pre-start abort would be a programmer error, assert it explicitly
1130
+ * with `assertNotAborted` after awaiting the result.
1131
+ *
1132
+ * ### Example
1133
+ *
1134
+ * ```ts
1135
+ * await using run = createRun();
1136
+ *
1137
+ * const events: Array<string> = [];
1138
+ * const canComplete = Promise.withResolvers<void>();
1139
+ * let signalAbortedInAnalytics = true;
1140
+ *
1141
+ * // Simulate async analytics API (abortable by default)
1142
+ * const sendToAnalytics =
1143
+ * (event: number): Task<void, never> =>
1144
+ * async ({ signal }) => {
1145
+ * await canComplete.promise;
1146
+ * signalAbortedInAnalytics = signal.aborted;
1147
+ * events.push(`sent ${event}`);
1148
+ * return ok();
1149
+ * };
1150
+ *
1151
+ * // Important events must be sent even if the user navigates away
1152
+ * const trackImportantEvent = (event: number) =>
1153
+ * unabortable(sendToAnalytics(event));
1154
+ *
1155
+ * // User clicks, we start tracking (Task runs until first await)
1156
+ * const fiber = run(trackImportantEvent(123));
1157
+ *
1158
+ * // User navigates away (abort requested while task is running)
1159
+ * fiber.abort();
1160
+ * canComplete.resolve();
1161
+ *
1162
+ * const result = await fiber;
1163
+ *
1164
+ * expect(signalAbortedInAnalytics).toBe(false);
1165
+ * // Analytics was sent despite abort
1166
+ * expect(events).toEqual(["sent 123"]);
1167
+ * expect(result).toEqual(ok());
1168
+ * ```
1169
+ *
1170
+ * @group Abort masking
1171
+ */
1172
+ export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
1173
+ /**
1174
+ * Like {@link unabortable}, but provides `restore` to restore abortability for
1175
+ * specific tasks.
1176
+ *
1177
+ * Tasks inherit abort masking from their parent. This means:
1178
+ *
1179
+ * - Tasks run inside `unabortableMask` are unabortable by default
1180
+ * - Tasks wrapped with `restore()` restore the previous abortability
1181
+ *
1182
+ * @group Abort masking
1183
+ */
1184
+ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2>(task: Task<T2, E2, D>) => Task<T2, E2, D>) => Task<T, E, D>) => Task<T, E, D>;
1185
+ /**
1186
+ * Runs tasks concurrently instead of sequentially.
1187
+ *
1188
+ * Sets the {@link Concurrency} level for a {@link Task}, which helpers like
1189
+ * {@link all}, {@link map}, etc. use to control how many tasks run at once.
1190
+ *
1191
+ * By default, tasks run sequentially (one at a time) to encourage thinking
1192
+ * about concurrency explicitly.
1193
+ *
1194
+ * For tuple-based calls like `all([taskA, taskB, taskC])` with a known small
1195
+ * number of tasks, omit the limit (runs unlimited). For arrays of unknown
1196
+ * length, always specify a limit.
1197
+ *
1198
+ * Concurrency is inherited by child tasks and can be overridden at any level.
1199
+ * Composition helpers should respect inherited concurrency — they should not
1200
+ * override it with a fixed number unless semantically required (like
1201
+ * {@link race}). Helpers with a recommended concurrency should export it for use
1202
+ * with `concurrently`.
1203
+ *
1204
+ * ### Example
1205
+ *
1206
+ * ```ts
1207
+ * // Unlimited (omit the limit)
1208
+ * run(concurrently(all([fetchA, fetchB, fetchC])));
1209
+ *
1210
+ * // Limited — at most 5 tasks run at a time
1211
+ * run(concurrently(5, all(tasks)));
1212
+ * run(concurrently(5, map(userIds, fetchUser)));
1213
+ *
1214
+ * // Inherited — inner all() uses parent's limit
1215
+ * const pipeline = concurrently(5, async (run) => {
1216
+ * const users = await run(map(userIds, fetchUser)); // uses 5
1217
+ * if (!users.ok) return users;
1218
+ * return run(map(users.value, enrichUser)); // also uses 5
1219
+ * });
1220
+ * ```
1221
+ *
1222
+ * @group Composition
1223
+ */
1224
+ export declare function concurrently<T, E, D = unknown>(concurrency: Concurrency, task: Task<T, E, D>): Task<T, E, D>;
1225
+ /** Unlimited. */
1226
+ export declare function concurrently<T, E, D = unknown>(task: Task<T, E, D>): Task<T, E, D>;
1227
+ /**
1228
+ * Yields execution to allow other work to proceed.
1229
+ *
1230
+ * Long-running JavaScript blocks the main thread. In browsers, this makes the
1231
+ * UI unresponsive (user interactions, animations). In Node.js, it prevents I/O
1232
+ * callbacks, timers, and other requests from being handled. Inserting yield
1233
+ * points lets the runtime process high-priority work between chunks of code.
1234
+ *
1235
+ * Uses `scheduler.yield()` in browsers for optimal main thread scheduling,
1236
+ * falls back to `setImmediate` in Node.js, or `setTimeout` elsewhere.
1237
+ *
1238
+ * ### Example
1239
+ *
1240
+ * ```ts
1241
+ * const processLargeArray: Task<void, never> = async (run) => {
1242
+ * const { time } = run.deps;
1243
+ * let lastYield = time.now();
1244
+ *
1245
+ * for (const item of largeArray) {
1246
+ * processItem(item);
1247
+ *
1248
+ * // Yield periodically to keep UI responsive
1249
+ * if (time.now() - lastYield > msLongTask) {
1250
+ * const r = await run(yieldNow);
1251
+ * if (!r.ok) return r;
1252
+ * lastYield = time.now();
1253
+ * }
1254
+ * }
1255
+ *
1256
+ * return ok();
1257
+ * };
1258
+ * ```
1259
+ *
1260
+ * Recursive tasks also benefit from periodic yields — without them, deep
1261
+ * recursion overflows the call stack:
1262
+ *
1263
+ * ```ts
1264
+ * const processRecursive =
1265
+ * (count: number, index: number, sum: number): Task<number> =>
1266
+ * async (run) => {
1267
+ * if (index >= count) return ok(sum);
1268
+ *
1269
+ * // Yield periodically to break synchronous call chains.
1270
+ * if (index > 0 && index % 1000 === 0) {
1271
+ * const y = await run(yieldNow);
1272
+ * if (!y.ok) return y;
1273
+ * }
1274
+ *
1275
+ * // Direct tail-call: no fiber overhead, stack-safe thanks to yieldNow.
1276
+ * return await processRecursive(count, index + 1, sum + index)(run);
1277
+ * };
1278
+ * ```
1279
+ *
1280
+ * @group Composition
1281
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield
1282
+ * @see https://web.dev/articles/optimize-long-tasks
1283
+ */
1284
+ export declare const yieldNow: Task<void>;
1285
+ /**
1286
+ * Creates a {@link Task} from a callback-based API.
1287
+ *
1288
+ * Use this to wrap callback-style APIs (event listeners, Node.js callbacks,
1289
+ * etc.) into Tasks with proper abort handling.
1290
+ *
1291
+ * Optionally return a teardown function that runs on abort.
1292
+ *
1293
+ * ### Example
1294
+ *
1295
+ * ```ts
1296
+ * // The sleep helper is implemented using callback:
1297
+ * const sleep = (duration: Duration): Task<void> =>
1298
+ * callback(({ ok, deps: { time } }) => {
1299
+ * const id = time.setTimeout(ok, durationToMillis(duration));
1300
+ * return () => time.clearTimeout(id);
1301
+ * });
1302
+ *
1303
+ * // Wrap an event listener — use signal directly
1304
+ * const waitForClick = (element: HTMLElement): Task<MouseEvent> =>
1305
+ * callback(({ ok, signal }) => {
1306
+ * element.addEventListener("click", ok, { once: true, signal });
1307
+ * });
1308
+ *
1309
+ * // Wrap Node.js callback API
1310
+ * const readFile = (path: string): Task<string, NodeJS.ErrnoException> =>
1311
+ * callback(({ ok, err }) => {
1312
+ * fs.readFile(path, "utf8", (error, data) => {
1313
+ * if (error) err(error);
1314
+ * else ok(data);
1315
+ * });
1316
+ * });
1317
+ * ```
1318
+ *
1319
+ * @group Composition
1320
+ */
1321
+ export declare const callback: <T, E = never>(callback: CallbackWithTeardown<{
1322
+ readonly ok: Callback<T>;
1323
+ readonly err: Callback<E>;
1324
+ readonly signal: AbortSignal;
1325
+ readonly deps: RunDeps;
1326
+ }>) => Task<T, E>;
1327
+ /**
1328
+ * Pauses execution for a specified duration.
1329
+ *
1330
+ * ### Example
1331
+ *
1332
+ * ```ts
1333
+ * const task: Task<void> = async (run) => {
1334
+ * console.log("Starting...");
1335
+ * await run(sleep("1s"));
1336
+ * console.log("Done after 1 second");
1337
+ * return ok();
1338
+ * };
1339
+ * ```
1340
+ *
1341
+ * @group Composition
1342
+ */
1343
+ export declare const sleep: (duration: Duration) => Task<void>;
1344
+ /**
1345
+ * Returns a {@link Task} that completes first.
1346
+ *
1347
+ * Like
1348
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race | Promise.race},
1349
+ * the first Task to complete (whether success or failure) wins. All other Tasks
1350
+ * are aborted. Use {@link any} if you need the first Task to succeed instead.
1351
+ *
1352
+ * Requires a non-empty array — racing zero Tasks has no meaningful result
1353
+ * (there's no "first to complete" without participants). This is enforced at
1354
+ * compile time for non-empty tuple types. For other arrays, guard with
1355
+ * {@link isNonEmptyArray}:
1356
+ *
1357
+ * ```ts
1358
+ * if (isNonEmptyArray(tasks)) {
1359
+ * await run(race(tasks));
1360
+ * }
1361
+ * ```
1362
+ *
1363
+ * ### Example
1364
+ *
1365
+ * ```ts
1366
+ * const fast: Task<string> = () => ok("fast");
1367
+ * const slow: Task<string> = async (run) => {
1368
+ * await run(sleep("10ms"));
1369
+ * return ok("slow");
1370
+ * };
1371
+ *
1372
+ * // First wins, others are aborted.
1373
+ * const result = await run(race([fast, slow])); // ok("fast")
1374
+ * ```
1375
+ *
1376
+ * Always runs with unlimited concurrency — a sequential race makes no sense
1377
+ * since the first Task would always "win".
1378
+ *
1379
+ * @group Composition
1380
+ */
1381
+ export declare const race: <T extends readonly [AnyTask, ...ReadonlyArray<AnyTask>]>(tasks: T, { abortReason, }?: {
1382
+ /** Abort reason for losing tasks. Defaults to {@link raceLostError}. */
1383
+ abortReason?: unknown;
1384
+ }) => Task<InferTaskOk<T[number]>, InferTaskErr<T[number]>, InferTaskDeps<T[number]>>;
1385
+ /**
1386
+ * Abort reason for tasks that lose a {@link race}.
1387
+ *
1388
+ * @group Composition
1389
+ */
1390
+ export declare const RaceLostError: import("./Type.js").TypedType<"RaceLostError", Record<never, never>>;
1391
+ export interface RaceLostError extends InferType<typeof RaceLostError> {
1392
+ }
1393
+ /**
1394
+ * {@link RaceLostError} used as abort reason in {@link race}.
1395
+ *
1396
+ * @group Composition
1397
+ */
1398
+ export declare const raceLostError: RaceLostError;
1399
+ /**
1400
+ * Wraps a {@link Task} with a time limit.
1401
+ *
1402
+ * Returns {@link TimeoutError} if the Task doesn't complete within the specified
1403
+ * duration. The original Task is aborted when the timeout fires.
1404
+ *
1405
+ * ### Example
1406
+ *
1407
+ * ```ts
1408
+ * const fetchWithTimeout = timeout(fetchData, "5s");
1409
+ *
1410
+ * const result = await run(fetchWithTimeout);
1411
+ * if (!result.ok && result.error.type === "TimeoutError") {
1412
+ * console.log("Request timed out");
1413
+ * }
1414
+ * ```
1415
+ *
1416
+ * @group Composition
1417
+ */
1418
+ export declare const timeout: <T, E, D = unknown>(task: Task<T, E, D>, duration: Duration, { abortReason, }?: {
1419
+ /**
1420
+ * Abort reason for the task when timeout fires. Defaults to
1421
+ * {@link timeoutError}.
1422
+ */
1423
+ abortReason?: unknown;
1424
+ }) => Task<T, E | TimeoutError, D>;
1425
+ /**
1426
+ * Typed error returned by {@link timeout} when a task exceeds its time limit.
1427
+ *
1428
+ * @group Composition
1429
+ */
1430
+ export declare const TimeoutError: import("./Type.js").TypedType<"TimeoutError", Record<never, never>>;
1431
+ export interface TimeoutError extends InferType<typeof TimeoutError> {
1432
+ }
1433
+ /**
1434
+ * {@link TimeoutError} used as abort reason in {@link timeout}.
1435
+ *
1436
+ * @group Composition
1437
+ */
1438
+ export declare const timeoutError: TimeoutError;
1439
+ /**
1440
+ * Options for {@link retry}.
1441
+ *
1442
+ * @group Composition
1443
+ */
1444
+ export interface RetryOptions<E, Output> {
1445
+ /** Predicate to determine if error is retryable. Defaults to all errors. */
1446
+ readonly retryable?: Predicate<E>;
1447
+ /**
1448
+ * Callback invoked before each retry attempt with error, retry attempt
1449
+ * number, schedule output, and delay.
1450
+ */
1451
+ readonly onRetry?: (attempt: RetryAttempt<E, Output>) => void;
1452
+ }
1453
+ /**
1454
+ * Info passed to {@link retry} {@link RetryOptions.onRetry} callback.
1455
+ *
1456
+ * @group Composition
1457
+ */
1458
+ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
1459
+ readonly error: E;
1460
+ }
1461
+ /**
1462
+ * Error returned when all retry attempts are exhausted.
1463
+ *
1464
+ * @group Composition
1465
+ */
1466
+ export interface RetryError<E> extends Typed<"RetryError"> {
1467
+ /** The error from the final attempt. */
1468
+ readonly cause: E;
1469
+ /** Total attempts made (initial + retries). */
1470
+ readonly attempts: PositiveInt;
1471
+ }
1472
+ /**
1473
+ * Wraps a {@link Task} with retry logic.
1474
+ *
1475
+ * Retries the Task according to the {@link Schedule}'s rules. Use
1476
+ * {@link RetryOptions.retryable} to filter which errors should trigger retries.
1477
+ *
1478
+ * All non-abort errors are wrapped in {@link RetryError}:
1479
+ *
1480
+ * - Task succeeds → `ok(value)`
1481
+ * - Task returns {@link AbortError} → `err(AbortError)` — passed through, no
1482
+ * retry, no wrapping
1483
+ * - Task returns any other error → retry until schedule exhausted or `retryable`
1484
+ * returns false → `err(RetryError)` with `cause` = the last error
1485
+ *
1486
+ * The `RetryError` is informative: "I tried N times, here's why I finally gave
1487
+ * up" — and `cause` contains the actual underlying error.
1488
+ *
1489
+ * ### Example
1490
+ *
1491
+ * ```ts
1492
+ * import {
1493
+ * exponential,
1494
+ * jitter,
1495
+ * maxDelay,
1496
+ * retry,
1497
+ * take,
1498
+ * } from "@evolu/common";
1499
+ *
1500
+ * const fetchWithRetry = retry(
1501
+ * fetchData,
1502
+ * // A jittered, capped, limited exponential backoff.
1503
+ * jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
1504
+ * );
1505
+ *
1506
+ * const result = await run(fetchWithRetry);
1507
+ * if (!result.ok) {
1508
+ * if (AbortError.is(result.error)) {
1509
+ * // Was aborted externally
1510
+ * } else {
1511
+ * // RetryError — failed after retrying
1512
+ * console.log(`Failed after ${result.error.attempts} attempts`);
1513
+ * console.log(`Last error:`, result.error.cause);
1514
+ * }
1515
+ * }
1516
+ * ```
1517
+ *
1518
+ * The schedule receives the error as input, enabling error-aware strategies
1519
+ * like stopping on fatal errors:
1520
+ *
1521
+ * ```ts
1522
+ * import { whileScheduleInput } from "@evolu/common";
1523
+ *
1524
+ * // Don't retry fatal errors
1525
+ * const smartRetry = retry(
1526
+ * fetchData,
1527
+ * whileScheduleInput((e: FetchError) => e.type !== "FatalError")(
1528
+ * take(5)(spaced("1s")),
1529
+ * ),
1530
+ * );
1531
+ * ```
1532
+ *
1533
+ * @group Composition
1534
+ * @see {@link RetryOptions}
1535
+ */
1536
+ export declare const retry: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, E>, { retryable, onRetry, }?: RetryOptions<E, Output>) => Task<T, RetryError<E>, D>;
1537
+ /**
1538
+ * Options for {@link repeat}.
1539
+ *
1540
+ * @group Composition
1541
+ */
1542
+ export interface RepeatOptions<T, Output> {
1543
+ /** Predicate to determine if value is repeatable. Defaults to all values. */
1544
+ readonly repeatable?: Predicate<T>;
1545
+ /**
1546
+ * Callback invoked before each repeat with value, repeat attempt number,
1547
+ * schedule output, and delay.
1548
+ */
1549
+ readonly onRepeat?: (attempt: RepeatAttempt<T, Output>) => void;
1550
+ }
1551
+ /**
1552
+ * Info passed to {@link repeat} {@link RepeatOptions.onRepeat} callback.
1553
+ *
1554
+ * @group Composition
1555
+ */
1556
+ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
1557
+ readonly value: T;
1558
+ }
1559
+ /**
1560
+ * Repeats a {@link Task} according to a {@link Schedule}.
1561
+ *
1562
+ * Runs the Task, then checks the schedule to determine if it should repeat. The
1563
+ * schedule controls how many repetitions occur and the delay between them.
1564
+ * Continues until the schedule returns `Err(Done<void>)` or the Task fails.
1565
+ *
1566
+ * With `take(n)`, the task runs n+1 times (initial run plus n repetitions).
1567
+ *
1568
+ * Also works with {@link NextTask} — when the Task returns `Err(Done<D>)`,
1569
+ * repeat stops and propagates the done signal.
1570
+ *
1571
+ * ### Example
1572
+ *
1573
+ * ```ts
1574
+ * import { fixed, take } from "@evolu/common/schedule";
1575
+ * import { repeat } from "@evolu/common";
1576
+ *
1577
+ * // Heartbeat every 30 seconds (runs forever until aborted)
1578
+ * const heartbeat = repeat(sendHeartbeat, fixed("30s"));
1579
+ *
1580
+ * // Poll 4 times total (initial + 3 repetitions), 1 second apart
1581
+ * const poll = repeat(checkStatus, take(3)(fixed("1s")));
1582
+ *
1583
+ * // Process queue items until empty (NextTask pattern)
1584
+ * const processQueue: NextTask<Item, ProcessError, void> = async (run) => {
1585
+ * const item = queue.dequeue();
1586
+ * if (!item) return err(done()); // Queue empty, stop
1587
+ * await process(item);
1588
+ * return ok(item);
1589
+ * };
1590
+ *
1591
+ * const result = await run(repeat(processQueue, fixed("100ms")));
1592
+ * if (!result.ok && result.error.type === "Done") {
1593
+ * console.log("Queue exhausted");
1594
+ * }
1595
+ * ```
1596
+ *
1597
+ * @group Composition
1598
+ */
1599
+ export declare const repeat: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, T>, { repeatable, onRepeat, }?: RepeatOptions<T, Output>) => Task<T, E, D>;
1600
+ /**
1601
+ * A value that can be resolved later.
1602
+ *
1603
+ * Similar to
1604
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers | Promise.withResolvers},
1605
+ * but integrated with {@link Task} and {@link Run} for cancellation support.
1606
+ *
1607
+ * Use for bridging callback-based APIs or coordinating between Tasks.
1608
+ *
1609
+ * Disposing aborts all waiting Tasks with an {@link AbortError} with
1610
+ * {@link deferredDisposedError} reason.
1611
+ *
1612
+ * ### Example
1613
+ *
1614
+ * ```ts
1615
+ * const deferred = createDeferred<string, MyError>();
1616
+ *
1617
+ * // Start waiting for the value
1618
+ * const fiber = run(deferred.task);
1619
+ *
1620
+ * // Resolve from elsewhere (callback, another task, etc.)
1621
+ * deferred.resolve(ok("value"));
1622
+ *
1623
+ * const result = await fiber; // ok("value")
1624
+ * ```
1625
+ *
1626
+ * @group Concurrency primitives
1627
+ * @see {@link createDeferred}
1628
+ */
1629
+ export interface Deferred<T, E = never> extends Disposable {
1630
+ /** A {@link Task} that waits until {@link Deferred.resolve} is called. */
1631
+ readonly task: Task<T, E | DeferredDisposedError>;
1632
+ /** Resolves the value. Returns `true` once, then `false`. */
1633
+ readonly resolve: (result: Result<T, E | AbortError | DeferredDisposedError>) => boolean;
1634
+ }
1635
+ /**
1636
+ * Creates a {@link Deferred}.
1637
+ *
1638
+ * @group Concurrency primitives
1639
+ */
1640
+ export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
1641
+ /**
1642
+ * Abort reason used when a {@link Deferred} is disposed.
1643
+ *
1644
+ * @group Concurrency primitives
1645
+ */
1646
+ export declare const DeferredDisposedError: import("./Type.js").TypedType<"DeferredDisposedError", Record<never, never>>;
1647
+ export interface DeferredDisposedError extends InferType<typeof DeferredDisposedError> {
1648
+ }
1649
+ /**
1650
+ * {@link DeferredDisposedError} used as abort reason in {@link createDeferred}.
1651
+ *
1652
+ * @group Concurrency primitives
1653
+ */
1654
+ export declare const deferredDisposedError: DeferredDisposedError;
1655
+ /**
1656
+ * A blocking {@link Task} — like a gate.
56
1657
  *
57
- * // Add retry for resilience
58
- * const fetchWithRetry = (url: string) =>
59
- * retry(
60
- * {
61
- * retries: PositiveInt.orThrow(3),
62
- * initialDelay: "100ms",
63
- * },
64
- * fetchWithTimeout(url),
65
- * );
1658
+ * - **Closed**: Tasks wait.
1659
+ * - **Open**: Tasks proceed.
66
1660
  *
67
- * fetchWithRetry satisfies (
68
- * url: string,
69
- * ) => Task<
70
- * Response,
71
- * TimeoutError | FetchError | RetryError<TimeoutError | FetchError>
72
- * >;
1661
+ * Use it to pause execution based on a condition. Unlike a {@link Deferred}
1662
+ * (which triggers once), a {@link Gate} can be opened and closed repeatedly.
73
1663
  *
74
- * const semaphore = createSemaphore(PositiveInt.orThrow(2));
1664
+ * Disposing aborts all waiting Tasks with {@link deferredDisposedError}.
75
1665
  *
76
- * // Control concurrency with semaphore
77
- * const fetchWithPermit = (url: string) =>
78
- * semaphore.withPermit(fetchWithRetry(url));
79
- *
80
- * fetchWithPermit satisfies (url: string) => Task<
81
- * Response,
82
- * | TimeoutError
83
- * | FetchError
84
- * | AbortError // Semaphore dispose aborts Tasks
85
- * | RetryError<TimeoutError | FetchError>
86
- * >;
87
- *
88
- * // Usage
89
- * const results = await Promise.all(
90
- * [
91
- * "https://api.example.com/users",
92
- * "https://api.example.com/posts",
93
- * "https://api.example.com/comments",
94
- * ]
95
- * .map(fetchWithPermit)
96
- * .map((task) => task()),
97
- * );
1666
+ * @group Concurrency primitives
1667
+ * @see {@link createGate}
1668
+ */
1669
+ export interface Gate<D = unknown> extends Disposable {
1670
+ readonly wait: Task<void, DeferredDisposedError, D>;
1671
+ readonly open: () => void;
1672
+ readonly close: () => void;
1673
+ readonly isOpen: () => boolean;
1674
+ }
1675
+ /**
1676
+ * Creates a {@link Gate} that starts closed.
98
1677
  *
99
- * results satisfies Array<
100
- * Result<
101
- * Response,
102
- * | AbortError
103
- * | TimeoutError
104
- * | FetchError
105
- * | RetryError<TimeoutError | FetchError>
106
- * >
107
- * >;
1678
+ * Useful for "stop/go" logic where multiple Tasks need to wait for a state
1679
+ * change.
108
1680
  *
109
- * // Handle results
110
- * for (const result of results) {
111
- * if (result.ok) {
112
- * // Process successful response
113
- * const response = result.value;
114
- * expect(response).toBeInstanceOf(Response);
115
- * } else {
116
- * // Handle error (TimeoutError, FetchError, RetryError, or AbortError)
117
- * expect(result.error).toBeDefined();
118
- * }
119
- * }
1681
+ * ### Example
120
1682
  *
121
- * // Cancellation support
122
- * const controller = new AbortController();
123
- * const cancelableTask = fetchWithPermit("https://api.example.com/data");
1683
+ * ```ts
1684
+ * const networkGate = createGate();
124
1685
  *
125
- * // Start task
126
- * const promise = cancelableTask(controller);
1686
+ * // Pause processing when offline
1687
+ * const onOffline = () => networkGate.close();
127
1688
  *
128
- * // Cancel after some time
129
- * setTimeout(() => {
130
- * controller.abort("User cancelled");
131
- * }, 1000);
1689
+ * // Resume processing when online
1690
+ * const onOnline = () => networkGate.open();
132
1691
  *
133
- * const _result = await promise;
134
- * // Result will be AbortError if cancelled
1692
+ * const syncLoop = async (run) => {
1693
+ * while (true) {
1694
+ * // Blocks here whenever the gate is closed
1695
+ * await run(networkGate.wait);
1696
+ * await run(uploadNextItem);
1697
+ * }
1698
+ * };
135
1699
  * ```
136
1700
  *
137
- * ### Dependency Injection Integration
1701
+ * @group Concurrency primitives
1702
+ */
1703
+ export declare const createGate: <D = unknown>() => Gate<D>;
1704
+ /**
1705
+ * A semaphore that limits the number of concurrent {@link Task}s.
1706
+ *
1707
+ * For mutual exclusion (limiting to exactly one {@link Task}), use {@link Mutex}
1708
+ * instead.
138
1709
  *
139
- * Tasks integrate naturally with Evolu's DI pattern. Use `deps` for static
140
- * dependencies and `TaskContext` for execution context like cancellation. Usage
141
- * follows the pattern: deps → arguments → execution context.
1710
+ * @group Concurrency primitives
142
1711
  */
143
- export interface Task<T, E> {
1712
+ export interface Semaphore extends Disposable {
144
1713
  /**
145
- * Invoke the Task.
146
- *
147
- * Provide a context with an AbortSignal to enable cancellation. When called
148
- * without a signal, {@link AbortError} cannot occur and the error type narrows
149
- * accordingly.
1714
+ * Executes a {@link Task} while holding a semaphore permit.
150
1715
  *
151
- * ### Example
1716
+ * The Task waits until a permit is available. If the semaphore is disposed
1717
+ * while waiting or running, the Task is aborted with an {@link AbortError}
1718
+ * whose reason is {@link semaphoreDisposedError}.
1719
+ */
1720
+ readonly withPermit: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
1721
+ /**
1722
+ * Executes a {@link Task} while holding a specified number of permits.
152
1723
  *
153
- * ```ts
154
- * interface FetchError {
155
- * readonly type: "FetchError";
156
- * readonly error: unknown;
157
- * }
158
- *
159
- * // Task version of fetch with proper error handling and cancellation support.
160
- * const fetch = (url: string) =>
161
- * toTask((context) =>
162
- * tryAsync(
163
- * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
164
- * (error): FetchError => ({ type: "FetchError", error }),
165
- * ),
166
- * );
1724
+ * If insufficient permits are available, waits in FIFO order until permits
1725
+ * become available. If disposed while waiting or running, the Task is aborted
1726
+ * with {@link semaphoreDisposedError}.
167
1727
  *
168
- * // `satisfies` shows the expected type signature.
169
- * fetch satisfies (url: string) => Task<Response, FetchError>;
1728
+ * Use this for weighted concurrency where a Task represents a resource
1729
+ * demand, not just "one more Task". One permit is one resource unit.
170
1730
  *
171
- * const result1 = await fetch("https://api.example.com/data")();
172
- * expectTypeOf(result1).toEqualTypeOf<Result<Response, FetchError>>();
1731
+ * Example: with capacity `10`, a lightweight operation can reserve `1` permit
1732
+ * while a heavy operation reserves `4` permits. This models shared budgets
1733
+ * such as DB connections, API credits, memory/CPU buckets, or batch
1734
+ * processing slots.
173
1735
  *
174
- * // With AbortController
175
- * const controller = new AbortController();
176
- * const result2 = await fetch("https://api.example.com/data")(
177
- * controller,
178
- * );
179
- * expectTypeOf(result2).toEqualTypeOf<
180
- * Result<Response, FetchError | AbortError>
181
- * >();
182
- * ```
1736
+ * {@link Semaphore.withPermit} is equivalent to `withPermits(1)`.
183
1737
  */
184
- <TContext extends TaskContext | undefined = undefined>(context?: TContext): Promise<Result<T, TContext extends {
185
- signal: AbortSignal;
186
- } ? E | AbortError : E>>;
1738
+ readonly withPermits: <T, E, D>(permits: Concurrency) => (task: Task<T, E, D>) => Task<T, E, D>;
1739
+ /** Returns the current semaphore state for monitoring/debugging. */
1740
+ readonly snapshot: () => SemaphoreSnapshot;
187
1741
  }
188
- /** Context passed to {@link Task}s for cancellation. */
189
- export interface TaskContext {
190
- /** Signal for cancellation */
191
- readonly signal?: AbortSignal;
192
- }
193
- /** Error returned when a {@link Task} is cancelled via AbortSignal. */
194
- export interface AbortError {
195
- readonly type: "AbortError";
196
- readonly reason?: unknown;
1742
+ /** Snapshot returned by {@link Semaphore.snapshot}. */
1743
+ export interface SemaphoreSnapshot {
1744
+ /** Total permits configured at creation. */
1745
+ readonly permits: Concurrency;
1746
+ /** Currently held permits. */
1747
+ readonly taken: NonNegativeInt;
1748
+ /** Number of currently waiting Tasks. */
1749
+ readonly waiting: NonNegativeInt;
1750
+ /** Currently available permits. */
1751
+ readonly available: NonNegativeInt;
1752
+ /** Whether no permits are taken and no Tasks are waiting. */
1753
+ readonly isIdle: boolean;
1754
+ /** Whether the semaphore has been disposed. */
1755
+ readonly disposed: boolean;
197
1756
  }
198
1757
  /**
199
- * Converts async function returning {@link Result} to a {@link Task}.
1758
+ * Creates a {@link Semaphore} that limits concurrent {@link Task}s.
200
1759
  *
201
1760
  * ### Example
202
1761
  *
203
1762
  * ```ts
204
- * interface FetchError {
205
- * readonly type: "FetchError";
206
- * readonly error: unknown;
207
- * }
208
- *
209
- * // Task version of fetch with proper error handling and cancellation support.
210
- * const fetch = (url: string) =>
211
- * toTask((context) =>
212
- * tryAsync(
213
- * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
214
- * (error): FetchError => ({ type: "FetchError", error }),
215
- * ),
216
- * );
1763
+ * await using run = createRun();
217
1764
  *
218
- * // `satisfies` shows the expected type signature.
219
- * fetch satisfies (url: string) => Task<Response, FetchError>;
1765
+ * const semaphore = createSemaphore(PositiveInt.orThrow(2));
220
1766
  *
221
- * const result1 = await fetch("https://api.example.com/data")();
222
- * result1 satisfies Result<Response, FetchError>;
1767
+ * const fetchUser =
1768
+ * (id: string): Task<string> =>
1769
+ * async (run) => {
1770
+ * const { console } = run.deps;
1771
+ * console.log("[demo]", "start", id);
1772
+ * const slept = await run(sleep("10ms"));
1773
+ * if (!slept.ok) return slept;
1774
+ * console.log("[demo]", "end", id);
1775
+ * return ok(`user:${id}`);
1776
+ * };
1777
+ *
1778
+ * const fetchWithPermit = (id: string) =>
1779
+ * semaphore.withPermit(fetchUser(id));
1780
+ *
1781
+ * await Promise.all([
1782
+ * run(fetchWithPermit("1")),
1783
+ * run(fetchWithPermit("2")),
1784
+ * run(fetchWithPermit("3")),
1785
+ * ]);
223
1786
  *
224
- * // With AbortController
225
- * const controller = new AbortController();
226
- * const result2 = await fetch("https://api.example.com/data")(controller);
227
- * result2 satisfies Result<Response, FetchError | AbortError>;
1787
+ * // [demo] start 1
1788
+ * // [demo] start 2
1789
+ * // [demo] end 1
1790
+ * // [demo] start 3
1791
+ * // [demo] end 2
1792
+ * // [demo] end 3
228
1793
  * ```
1794
+ *
1795
+ * @group Concurrency primitives
1796
+ */
1797
+ export declare const createSemaphore: (permits: Concurrency) => Semaphore;
1798
+ /**
1799
+ * Abort reason used when a {@link Semaphore} is disposed.
1800
+ *
1801
+ * @group Concurrency primitives
1802
+ */
1803
+ export declare const SemaphoreDisposedError: import("./Type.js").TypedType<"SemaphoreDisposedError", Record<never, never>>;
1804
+ export interface SemaphoreDisposedError extends InferType<typeof SemaphoreDisposedError> {
1805
+ }
1806
+ /**
1807
+ * {@link SemaphoreDisposedError} used as abort reason in {@link createSemaphore}.
1808
+ *
1809
+ * @group Concurrency primitives
1810
+ */
1811
+ export declare const semaphoreDisposedError: SemaphoreDisposedError;
1812
+ /**
1813
+ * A keyed {@link Semaphore} registry.
1814
+ *
1815
+ * Provides semaphore operations per key while preserving the same API shape as
1816
+ * {@link Semaphore}.
1817
+ *
1818
+ * By default, {@link createSemaphoreByKey} uses reference identity for keys,
1819
+ * matching native `Map`. Callers may instead provide a {@link Lookup lookup} so
1820
+ * logical equality is based on a derived stable key.
1821
+ *
1822
+ * @group Concurrency primitives
1823
+ */
1824
+ export interface SemaphoreByKey<K = unknown> extends Disposable {
1825
+ /**
1826
+ * Executes a {@link Task} while holding one permit for a specific key.
1827
+ *
1828
+ * Behaves like {@link Semaphore.withPermit}, scoped to `key`.
1829
+ */
1830
+ readonly withPermit: <T, E, D>(key: K, task: Task<T, E, D>) => Task<T, E, D>;
1831
+ /**
1832
+ * Executes a {@link Task} while holding permits for a specific key.
1833
+ *
1834
+ * Behaves like {@link Semaphore.withPermits}, scoped to `key`.
1835
+ */
1836
+ readonly withPermits: <T, E, D>(key: K, permits: Concurrency) => (task: Task<T, E, D>) => Task<T, E, D>;
1837
+ /** Returns current semaphore state for a key, or `null` if absent. */
1838
+ readonly snapshot: (key: K) => SemaphoreSnapshot | null;
1839
+ }
1840
+ /** Options for {@link createSemaphoreByKey}. */
1841
+ export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<K, L> {
1842
+ }
1843
+ /**
1844
+ * Creates a {@link SemaphoreByKey}.
1845
+ *
1846
+ * Each key gets its own semaphore with the same permit capacity.
1847
+ *
1848
+ * @group Concurrency primitives
229
1849
  */
230
- export declare const toTask: <T, E>(fn: (context?: TaskContext) => Promise<Result<T, E>>) => Task<T, E>;
1850
+ export declare function createSemaphoreByKey<K = unknown>(permits: Concurrency): SemaphoreByKey<K>;
1851
+ export declare function createSemaphoreByKey<K, L>(permits: Concurrency, options: CreateSemaphoreByKeyOptions<K, L>): SemaphoreByKey<K>;
231
1852
  /**
232
- * Creates a {@link Task} that waits for the specified duration.
1853
+ * A mutex (mutual exclusion) that ensures only one {@link Task} runs at a time.
1854
+ *
1855
+ * This is a specialized version of a {@link Semaphore} with a permit count of 1.
233
1856
  *
234
1857
  * ### Example
235
1858
  *
236
1859
  * ```ts
237
- * const result1 = await wait("10ms")();
238
- * result1 satisfies Result<void, never>;
1860
+ * await using run = createRun();
1861
+ *
1862
+ * const mutex = createMutex();
1863
+ *
1864
+ * const task =
1865
+ * (id: string): Task<void> =>
1866
+ * async (run) => {
1867
+ * const { console } = run.deps;
1868
+ * console.log("start", id);
1869
+ * await run(sleep("10ms"));
1870
+ * console.log("end", id);
1871
+ * return ok();
1872
+ * };
1873
+ *
1874
+ * await Promise.all([
1875
+ * run(mutex.withLock(task("1"))),
1876
+ * run(mutex.withLock(task("2"))),
1877
+ * ]);
239
1878
  *
240
- * // With AbortController
241
- * const controller = new AbortController();
242
- * const result2 = await wait("10ms")(controller);
243
- * result2 satisfies Result<void, AbortError>;
1879
+ * // start 1
1880
+ * // end 1
1881
+ * // start 2
1882
+ * // end 2
244
1883
  * ```
1884
+ *
1885
+ * @group Concurrency primitives
245
1886
  */
246
- export declare const wait: (duration: Duration) => Task<void, never>;
247
- /** Error returned when {@link timeout} exceeds the specified duration. */
248
- export interface TimeoutError {
249
- readonly type: "TimeoutError";
250
- readonly timeoutMs: number;
1887
+ export interface Mutex extends Disposable {
1888
+ /**
1889
+ * Executes a {@link Task} while holding the mutex lock.
1890
+ *
1891
+ * Only one Task can hold the lock at a time. Other Tasks wait until the lock
1892
+ * is released.
1893
+ */
1894
+ readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
1895
+ /** Returns the current mutex state for monitoring/debugging. */
1896
+ readonly snapshot: () => SemaphoreSnapshot;
251
1897
  }
252
1898
  /**
253
- * Adds timeout behavior to a {@link Task}.
1899
+ * Creates a {@link Mutex}.
254
1900
  *
255
- * ### Example
1901
+ * @group Concurrency primitives
1902
+ */
1903
+ export declare const createMutex: () => Mutex;
1904
+ /**
1905
+ * A keyed {@link Mutex} registry.
256
1906
  *
257
- * ```ts
258
- * interface FetchError {
259
- * readonly type: "FetchError";
260
- * readonly error: unknown;
261
- * }
1907
+ * Provides mutex operations per key.
262
1908
  *
263
- * // Task version of fetch with proper error handling and cancellation support.
264
- * const fetch = (url: string) =>
265
- * toTask((context) =>
266
- * tryAsync(
267
- * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
268
- * (error): FetchError => ({ type: "FetchError", error }),
269
- * ),
270
- * );
1909
+ * By default, {@link createMutexByKey} uses reference identity for keys,
1910
+ * matching native `Map`. Callers may instead provide a {@link Lookup lookup} so
1911
+ * logical equality is based on a derived stable key.
271
1912
  *
272
- * // `satisfies` shows the expected type signature.
273
- * fetch satisfies (url: string) => Task<Response, FetchError>;
1913
+ * @group Concurrency primitives
1914
+ */
1915
+ export interface MutexByKey<K = unknown> extends Disposable {
1916
+ /**
1917
+ * Executes a {@link Task} while holding the mutex lock for a specific key.
1918
+ *
1919
+ * Behaves like {@link Mutex.withLock}, scoped to `key`.
1920
+ */
1921
+ readonly withLock: <T, E, D>(key: K, task: Task<T, E, D>) => Task<T, E, D>;
1922
+ /** Returns the current mutex state for `key`, or `null` if absent. */
1923
+ readonly snapshot: (key: K) => SemaphoreSnapshot | null;
1924
+ }
1925
+ /** Options for {@link createMutexByKey}. */
1926
+ export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {
1927
+ }
1928
+ /**
1929
+ * Creates a {@link MutexByKey}.
274
1930
  *
275
- * const fetchWithTimeout = (url: string) => timeout("2m", fetch(url));
1931
+ * @group Concurrency primitives
1932
+ */
1933
+ export declare function createMutexByKey<K = unknown>(): MutexByKey<K>;
1934
+ export declare function createMutexByKey<K, L>(options: CreateMutexByKeyOptions<K, L>): MutexByKey<K>;
1935
+ /**
1936
+ * {@link Ref} protected by a {@link Mutex}.
276
1937
  *
277
- * const result1 = await fetchWithTimeout("https://api.example.com/data")();
278
- * result1 satisfies Result<Response, FetchError | TimeoutError>;
1938
+ * `MutexRef` serializes all operations through an internal {@link Mutex}. Reads,
1939
+ * writes, and updates observe one consistent value transition at a time. If the
1940
+ * update fails or is aborted, the previous value is preserved.
279
1941
  *
280
- * // With AbortController
281
- * const controller = new AbortController();
282
- * const result2 = await fetchWithTimeout("https://api.example.com/data")(
283
- * controller,
284
- * );
285
- * result2 satisfies Result<
286
- * Response,
287
- * FetchError | TimeoutError | AbortError
288
- * >;
289
- * ```
1942
+ * Typical use cases are small coordinators such as caches, session values,
1943
+ * in-memory registries, and counters whose transitions need to run {@link Task}s
1944
+ * atomically.
1945
+ *
1946
+ * @group Concurrency primitives
1947
+ */
1948
+ export interface MutexRef<T> extends Disposable {
1949
+ /** Returns the current value. */
1950
+ readonly get: Task<T>;
1951
+ /** Sets the current value. */
1952
+ readonly set: (value: T) => Task<void>;
1953
+ /** Sets the current value and returns the previous value. */
1954
+ readonly getAndSet: (value: T) => Task<T>;
1955
+ /** Sets the current value and returns it. */
1956
+ readonly setAndGet: (value: T) => Task<T>;
1957
+ /** Updates the current value. */
1958
+ readonly update: <E = never, D = unknown>(updater: (current: T) => Task<T, E, D>) => Task<void, E, D>;
1959
+ /** Updates the current value and returns the previous value. */
1960
+ readonly getAndUpdate: <E = never, D = unknown>(updater: (current: T) => Task<T, E, D>) => Task<T, E, D>;
1961
+ /** Updates the current value and returns it. */
1962
+ readonly updateAndGet: <E = never, D = unknown>(updater: (current: T) => Task<T, E, D>) => Task<T, E, D>;
1963
+ /** Modifies the current value and returns a computed result. */
1964
+ readonly modify: <R, E = never, D = unknown>(modifier: (current: T) => Task<readonly [result: R, nextValue: T], E, D>) => Task<R, E, D>;
1965
+ }
1966
+ /**
1967
+ * Creates a {@link MutexRef} with the given initial immutable value.
1968
+ *
1969
+ * @group Concurrency primitives
1970
+ */
1971
+ export declare const createMutexRef: <T>(initialValue: T) => MutexRef<T>;
1972
+ /**
1973
+ * Cross-platform leader lock abstraction.
1974
+ *
1975
+ * `lock` returns a {@link Task} that waits until leadership is acquired and
1976
+ * yields a lease.
1977
+ *
1978
+ * Returns {@link AsyncDisposable} lease. Dispose it to release leadership.
1979
+ *
1980
+ * @group Concurrency primitives
1981
+ */
1982
+ export interface LeaderLock {
1983
+ readonly lock: (name: Name) => Task<AsyncDisposable>;
1984
+ }
1985
+ /** @group Concurrency primitives */
1986
+ export interface LeaderLockDep {
1987
+ readonly leaderLock: LeaderLock;
1988
+ }
1989
+ /**
1990
+ * Creates an in-process {@link LeaderLock}.
1991
+ *
1992
+ * Uses one {@link Mutex} per {@link Name}. Suitable for runtimes without a
1993
+ * cross-process lock manager (for example in-memory worker tests or React
1994
+ * Native).
1995
+ *
1996
+ * @group Concurrency primitives
290
1997
  */
291
- export declare const timeout: <T, E>(duration: Duration, task: Task<T, E>) => Task<T, E | TimeoutError>;
292
- /** Options for configuring {@link retry} behavior. */
293
- export interface RetryOptions<E> {
294
- /** Number of retry attempts after the initial failure. */
295
- readonly retries: PositiveInt;
1998
+ export declare const createInMemoryLeaderLock: () => LeaderLock;
1999
+ /**
2000
+ * Options for {@link all}, {@link allSettled}, {@link map}, and {@link mapSettled}.
2001
+ *
2002
+ * @group Composition
2003
+ */
2004
+ export interface CollectOptions<Collect extends boolean = true> {
296
2005
  /**
297
- * Initial delay for exponential backoff (1st retry uses this, 2nd uses
298
- * this×factor, 3rd uses this×factor², etc.). Actual delays are randomized by
299
- * {@link RetryOptions.jitter}.
2006
+ * Whether to collect results. When `false`, returns `Task<void, E, D>`.
2007
+ *
2008
+ * @default true
300
2009
  */
301
- readonly initialDelay?: Duration;
302
- /** Maximum delay between retries. */
303
- readonly maxDelay?: Duration;
304
- /** Exponential backoff multiplier. */
305
- readonly factor?: number;
306
- /** Random jitter factor (0-1) to prevent thundering herd. */
307
- readonly jitter?: number;
2010
+ readonly collect?: Collect;
308
2011
  /**
309
- * Predicate to determine if error should trigger retry. Receives AbortError
310
- * too.
2012
+ * Custom reason for aborting remaining tasks on failure.
2013
+ *
2014
+ * By default, uses the helper's default abort error.
311
2015
  */
312
- readonly retryable?: (error: E | AbortError) => boolean;
313
- /** Callback invoked before each retry attempt. */
314
- readonly onRetry?: (error: E, attempt: number, delay: number) => void;
315
- }
316
- /** Error returned when {@link retry} exhausts all retry attempts. */
317
- export interface RetryError<E> {
318
- readonly type: "RetryError";
319
- readonly cause: E;
320
- readonly attempts: number;
2016
+ readonly abortReason?: unknown;
321
2017
  }
322
2018
  /**
323
- * Adds retry logic with exponential backoff and jitter to a {@link Task}.
2019
+ * Fails fast on first error across multiple {@link Task}s.
2020
+ *
2021
+ * Sequential by default — use {@link concurrently} to run concurrently.
324
2022
  *
325
2023
  * ### Example
326
2024
  *
327
2025
  * ```ts
328
- * interface FetchError {
329
- * readonly type: "FetchError";
330
- * readonly error: unknown;
331
- * }
2026
+ * const result = await run(all([fetchUser, fetchPosts, fetchComments]));
2027
+ * if (!result.ok) return result;
2028
+ * const [user, posts, comments] = result.value;
2029
+ * ```
332
2030
  *
333
- * // Task version of fetch with proper error handling and cancellation support.
334
- * const fetch = (url: string) =>
335
- * toTask((context) =>
336
- * tryAsync(
337
- * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
338
- * (error): FetchError => ({ type: "FetchError", error }),
339
- * ),
340
- * );
2031
+ * @group Composition
2032
+ * @see {@link CollectOptions}
2033
+ */
2034
+ export declare function all<const T extends readonly [AnyTask, ...ReadonlyArray<AnyTask>]>(tasks: T, options?: CollectOptions): Task<{
2035
+ [K in keyof T]: InferTaskOk<T[K]>;
2036
+ }, InferTaskErr<T[number]>, InferTaskDeps<T[number]>>;
2037
+ /**
2038
+ * Returns object with same keys.
341
2039
  *
342
- * // `satisfies` shows the expected type signature.
343
- * fetch satisfies (url: string) => Task<Response, FetchError>;
2040
+ * ```ts
2041
+ * const result = await run(all({ user: fetchUser, posts: fetchPosts }));
2042
+ * if (!result.ok) return result;
2043
+ * const { user, posts } = result.value;
2044
+ * ```
2045
+ */
2046
+ export declare function all<T extends Readonly<Record<string, AnyTask>>>(tasks: T, options?: CollectOptions): Task<{
2047
+ [P in keyof T]: InferTaskOk<T[P]>;
2048
+ }, [
2049
+ keyof T
2050
+ ] extends [never] ? never : InferTaskErr<T[keyof T]>, [
2051
+ keyof T
2052
+ ] extends [never] ? unknown : InferTaskDeps<T[keyof T]>>;
2053
+ /**
2054
+ * For dynamic or generated task lists.
344
2055
  *
345
- * const fetchWithRetry = (url: string) =>
346
- * retry({ retries: PositiveInt.orThrow(3) }, fetch(url));
2056
+ * ```ts
2057
+ * const urls: ReadonlyArray<string> = getUrls();
2058
+ * const result = await run(map(urls, fetchUrl));
2059
+ * if (!result.ok) return result;
2060
+ * // result.value: ReadonlyArray<Response>
2061
+ * ```
2062
+ */
2063
+ export declare function all<T, E, D>(tasks: Iterable<Task<T, E, D>>, options?: CollectOptions): Task<ReadonlyArray<T>, E, D>;
2064
+ /**
2065
+ * Guarantees non-empty result.
347
2066
  *
348
- * const result1 = await fetchWithRetry("https://api.example.com/data")();
349
- * result1 satisfies Result<Response, FetchError | RetryError<FetchError>>;
2067
+ * ```ts
2068
+ * const tasks: NonEmptyReadonlyArray<Task<Response, FetchError>> = [
2069
+ * fetchUrl("/a"),
2070
+ * fetchUrl("/b"),
2071
+ * ];
2072
+ * const result = await run(all(tasks));
2073
+ * if (!result.ok) return result;
2074
+ * // result.value: NonEmptyReadonlyArray<Response>
2075
+ * ```
2076
+ */
2077
+ export declare function all<T, E, D>(tasks: NonEmptyReadonlyArray<Task<T, E, D>>, options?: CollectOptions): Task<NonEmptyReadonlyArray<T>, E, D>;
2078
+ /**
2079
+ * Run for side effects only.
350
2080
  *
351
- * // With AbortController
352
- * const controller = new AbortController();
353
- * const result2 = await fetchWithRetry("https://api.example.com/data")(
354
- * controller,
355
- * );
356
- * result2 satisfies Result<
357
- * Response,
358
- * FetchError | RetryError<FetchError> | AbortError
359
- * >;
2081
+ * ```ts
2082
+ * const result = await run(all(tasks, { collect: false }));
2083
+ * // result.value: void
360
2084
  * ```
361
2085
  */
362
- export declare const retry: <T, E>({ retries, initialDelay, maxDelay, factor, jitter, retryable, onRetry, }: RetryOptions<E>, task: Task<T, E>) => Task<T, E | RetryError<E>>;
2086
+ export declare function all<T, E, D>(tasks: Iterable<Task<T, E, D>> | Readonly<Record<string, Task<T, E, D>>>, options: CollectOptions<false>): Task<void, E, D>;
363
2087
  /**
364
- * A semaphore that limits the number of concurrent async Tasks.
2088
+ * Abort reason used by {@link all} when aborting remaining tasks.
365
2089
  *
366
- * For mutual exclusion (limiting to exactly one Task), consider using
367
- * {@link Mutex} instead.
2090
+ * Used when a Task fails and other Tasks need to be aborted.
368
2091
  *
369
- * @see {@link createSemaphore} to create a semaphore instance.
2092
+ * @group Composition
370
2093
  */
371
- export interface Semaphore extends Disposable {
372
- /**
373
- * Executes a Task while holding a semaphore permit.
374
- *
375
- * The Task will wait until a permit is available before executing. Supports
376
- * cancellation via AbortSignal - if the signal is aborted while waiting for a
377
- * permit or during execution, the Task is cancelled and permits are properly
378
- * released.
379
- */
380
- readonly withPermit: <T, E>(task: Task<T, E>) => Task<T, E | AbortError>;
2094
+ export declare const AllAbortError: import("./Type.js").TypedType<"AllAbortError", Record<never, never>>;
2095
+ export interface AllAbortError extends InferType<typeof AllAbortError> {
381
2096
  }
382
2097
  /**
383
- * Creates a semaphore that limits concurrent async Tasks to the specified
384
- * count.
2098
+ * {@link AllAbortError} used as abort reason in {@link all}.
2099
+ *
2100
+ * @group Composition
2101
+ */
2102
+ export declare const allAbortError: AllAbortError;
2103
+ /**
2104
+ * Completes all {@link Task}s regardless of individual failures.
385
2105
  *
386
- * A semaphore controls access to a resource by maintaining a count of available
387
- * permits. Tasks acquire a permit before executing and release it when
388
- * complete.
2106
+ * Like
2107
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled | Promise.allSettled},
2108
+ * all Tasks run to completion regardless of individual failures. Returns an
2109
+ * array of {@link Result}s preserving the original order.
389
2110
  *
390
- * For mutual exclusion (exactly one Task at a time), consider using
391
- * {@link createMutex} instead.
2111
+ * Sequential by default. Use {@link concurrently} for concurrent execution.
392
2112
  *
393
2113
  * ### Example
394
2114
  *
395
2115
  * ```ts
396
- * // Allow maximum 3 concurrent Tasks
397
- * const semaphore = createSemaphore(PositiveInt.orThrow(3));
398
- *
399
- * let currentConcurrent = 0;
400
- * const events: Array<string> = [];
2116
+ * const results = await run(
2117
+ * allSettled([fetchUser, fetchPosts, fetchComments]),
2118
+ * );
2119
+ * if (!results.ok) return results; // Only AbortError
401
2120
  *
402
- * const fetchData = (id: number) =>
403
- * toTask<number, never>(async (context) => {
404
- * currentConcurrent++;
405
- * events.push(`start ${id} (concurrent: ${currentConcurrent})`);
2121
+ * for (const result of results.value) {
2122
+ * if (result.ok) {
2123
+ * console.log("Success:", result.value);
2124
+ * } else {
2125
+ * console.log("Failed:", result.error);
2126
+ * }
2127
+ * }
2128
+ * ```
406
2129
  *
407
- * await wait("10ms")(context);
2130
+ * @group Composition
2131
+ * @see {@link CollectOptions}
2132
+ */
2133
+ export declare function allSettled<const T extends readonly [AnyTask, ...ReadonlyArray<AnyTask>]>(tasks: T, options?: CollectOptions): Task<{
2134
+ [K in keyof T]: Result<InferTaskOk<T[K]>, InferTaskErr<T[K]> | AbortError>;
2135
+ }, never, InferTaskDeps<T[number]>>;
2136
+ /**
2137
+ * Returns object with same keys.
408
2138
  *
409
- * currentConcurrent--;
410
- * events.push(`end ${id} (concurrent: ${currentConcurrent})`);
411
- * return ok(id * 10);
412
- * });
2139
+ * ```ts
2140
+ * const results = await run(
2141
+ * allSettled({ user: fetchUser, posts: fetchPosts }),
2142
+ * );
2143
+ * if (!results.ok) return results;
2144
+ * const { user, posts } = results.value; // Each is Result<T, E>
2145
+ * ```
2146
+ */
2147
+ export declare function allSettled<T extends Readonly<Record<string, AnyTask>>>(tasks: T, options?: CollectOptions): Task<{
2148
+ [P in keyof T]: Result<InferTaskOk<T[P]>, InferTaskErr<T[P]> | AbortError>;
2149
+ }, never, [
2150
+ keyof T
2151
+ ] extends [never] ? unknown : InferTaskDeps<T[keyof T]>>;
2152
+ /**
2153
+ * For dynamic or generated task lists.
413
2154
  *
414
- * // These will execute with at most 3 running concurrently
415
- * const results = await Promise.all([
416
- * semaphore.withPermit(fetchData(1))(),
417
- * semaphore.withPermit(fetchData(2))(),
418
- * semaphore.withPermit(fetchData(3))(),
419
- * semaphore.withPermit(fetchData(4))(), // waits for one above to complete
420
- * semaphore.withPermit(fetchData(5))(), // waits for permit
421
- * ]);
2155
+ * ```ts
2156
+ * const urls: ReadonlyArray<string> = getUrls();
2157
+ * const results = await run(allSettled(tasks));
2158
+ * if (!results.ok) return results;
2159
+ * // results.value: ReadonlyArray<Result<Response, FetchError | AbortError>>
2160
+ * ```
2161
+ */
2162
+ export declare function allSettled<T, E, D>(tasks: Iterable<Task<T, E, D>>, options?: CollectOptions): Task<ReadonlyArray<Result<T, E | AbortError>>, never, D>;
2163
+ /**
2164
+ * Guarantees non-empty result.
422
2165
  *
423
- * expect(results.map(getOrThrow)).toEqual([10, 20, 30, 40, 50]);
424
- * expect(events).toMatchInlineSnapshot(`
425
- * [
426
- * "start 1 (concurrent: 1)",
427
- * "start 2 (concurrent: 2)",
428
- * "start 3 (concurrent: 3)",
429
- * "end 1 (concurrent: 2)",
430
- * "start 4 (concurrent: 3)",
431
- * "end 2 (concurrent: 2)",
432
- * "start 5 (concurrent: 3)",
433
- * "end 3 (concurrent: 2)",
434
- * "end 4 (concurrent: 1)",
435
- * "end 5 (concurrent: 0)",
436
- * ]
437
- * `);
2166
+ * ```ts
2167
+ * const tasks: NonEmptyReadonlyArray<Task<Response, FetchError>> = [
2168
+ * fetchUrl("/a"),
2169
+ * fetchUrl("/b"),
2170
+ * ];
2171
+ * const results = await run(allSettled(tasks));
2172
+ * if (!results.ok) return results;
2173
+ * // results.value: NonEmptyReadonlyArray<Result<Response, FetchError | AbortError>>
438
2174
  * ```
439
2175
  */
440
- export declare const createSemaphore: (maxConcurrent: PositiveInt) => Semaphore;
2176
+ export declare function allSettled<T, E, D>(tasks: NonEmptyReadonlyArray<Task<T, E, D>>, options?: CollectOptions): Task<NonEmptyReadonlyArray<Result<T, E | AbortError>>, never, D>;
441
2177
  /**
442
- * A mutex (mutual exclusion) that ensures only one Task runs at a time.
2178
+ * Run for side effects only.
443
2179
  *
444
- * This is a specialized version of a {@link Semaphore} with a permit count of 1.
2180
+ * ```ts
2181
+ * const result = await run(allSettled(tasks, { collect: false }));
2182
+ * // result.value: void
2183
+ * ```
2184
+ */
2185
+ export declare function allSettled<T, E, D>(tasks: Iterable<Task<T, E, D>> | Readonly<Record<string, Task<T, E, D>>>, options: CollectOptions<false>): Task<void, never, D>;
2186
+ /**
2187
+ * Abort reason used by {@link allSettled} when aborted externally.
445
2188
  *
446
- * @see {@link createMutex} to create a mutex instance.
2189
+ * @group Composition
447
2190
  */
448
- export interface Mutex extends Disposable {
449
- /**
450
- * Executes a Task while holding the mutex lock.
451
- *
452
- * Only one Task can hold the lock at a time. Other Tasks will wait until the
453
- * lock is released. Supports cancellation via AbortSignal.
454
- */
455
- readonly withLock: <T, E>(task: Task<T, E>) => Task<T, E | AbortError>;
2191
+ export declare const AllSettledAbortError: import("./Type.js").TypedType<"AllSettledAbortError", Record<never, never>>;
2192
+ export interface AllSettledAbortError extends InferType<typeof AllSettledAbortError> {
456
2193
  }
457
2194
  /**
458
- * Creates a new mutex for ensuring mutual exclusion.
2195
+ * {@link AllSettledAbortError} used as abort reason in {@link allSettled}.
459
2196
  *
460
- * A mutex is a {@link createSemaphore} with exactly one permit, ensuring that
461
- * only one Task can execute at a time.
2197
+ * @group Composition
2198
+ */
2199
+ export declare const allSettledAbortError: AllSettledAbortError;
2200
+ /**
2201
+ * Maps values to {@link Task}s, failing fast on first error.
2202
+ *
2203
+ * Sequential by default — use {@link concurrently} for concurrent execution.
462
2204
  *
463
2205
  * ### Example
464
2206
  *
465
2207
  * ```ts
466
- * const mutex = createMutex();
2208
+ * const result = await run(map(userIds, fetchUser));
2209
+ * if (!result.ok) return result;
2210
+ * // result.value: ReadonlyArray<User>
2211
+ * ```
467
2212
  *
468
- * const updateTask = (id: number) =>
469
- * toTask((context) =>
470
- * tryAsync(
471
- * () => updateSharedResource(id, context),
472
- * (error): UpdateError => ({ type: "UpdateError", error }),
473
- * ),
474
- * );
2213
+ * @group Composition
2214
+ * @see {@link CollectOptions}
2215
+ */
2216
+ export declare function map<A, T, E, D>(items: Iterable<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<ReadonlyArray<T>, E, D>;
2217
+ /**
2218
+ * Returns object with same keys.
475
2219
  *
476
- * // These Tasks will execute one at a time
477
- * const results = await Promise.all([
478
- * mutex.withLock(updateTask(1))(),
479
- * mutex.withLock(updateTask(2))(),
480
- * mutex.withLock(updateTask(3))(),
481
- * ]);
2220
+ * ```ts
2221
+ * const result = await run(map({ a: 1, b: 2 }, (n) => double(n)));
2222
+ * if (!result.ok) return result;
2223
+ * // result.value: { a: number, b: number }
482
2224
  * ```
483
2225
  */
484
- export declare const createMutex: () => Mutex;
2226
+ export declare function map<A, T, E, D, K extends string>(items: Readonly<Record<K, A>>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<Readonly<Record<K, T>>, E, D>;
485
2227
  /**
486
- * Schedule a task to run after all interactions (animations, gestures,
487
- * navigation) have completed.
2228
+ * Guarantees non-empty result.
488
2229
  *
489
- * This uses `requestIdleCallback` when available, otherwise falls back to
490
- * `setTimeout(0)` for cross-platform compatibility.
2230
+ * ```ts
2231
+ * const ids: NonEmptyReadonlyArray<UserId> = [id1, id2];
2232
+ * const result = await run(map(ids, fetchUser));
2233
+ * if (!result.ok) return result;
2234
+ * // result.value: NonEmptyReadonlyArray<User>
2235
+ * ```
2236
+ */
2237
+ export declare function map<A, T, E, D>(items: NonEmptyReadonlyArray<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<NonEmptyReadonlyArray<T>, E, D>;
2238
+ /**
2239
+ * Run for side effects only.
2240
+ *
2241
+ * ```ts
2242
+ * const result = await run(map(userIds, sendEmail, { collect: false }));
2243
+ * // result.value: void
2244
+ * ```
2245
+ */
2246
+ export declare function map<A, T, E, D>(items: Iterable<A> | Readonly<Record<string, A>>, task: (a: A) => Task<T, E, D>, options: CollectOptions<false>): Task<void, E, D>;
2247
+ /**
2248
+ * Abort reason used by {@link map} when aborting remaining tasks.
2249
+ *
2250
+ * @group Composition
2251
+ */
2252
+ export declare const MapAbortError: import("./Type.js").TypedType<"MapAbortError", Record<never, never>>;
2253
+ export interface MapAbortError extends InferType<typeof MapAbortError> {
2254
+ }
2255
+ /**
2256
+ * {@link MapAbortError} used as abort reason in {@link map}.
2257
+ *
2258
+ * @group Composition
2259
+ */
2260
+ export declare const mapAbortError: MapAbortError;
2261
+ /**
2262
+ * Maps values to {@link Task}s, completing all regardless of failures.
2263
+ *
2264
+ * Returns an array of {@link Result}s preserving the original order. Sequential
2265
+ * by default — use {@link concurrently} for concurrent execution.
491
2266
  *
492
2267
  * ### Example
493
2268
  *
494
2269
  * ```ts
495
- * const processDataTask: Task<void, ProcessError> = toTask(async () => {
496
- * // Heavy processing work
497
- * return ok();
498
- * });
2270
+ * const results = await run(mapSettled(userIds, fetchUser));
2271
+ * if (!results.ok) return results; // Only AbortError
499
2272
  *
500
- * // Schedule the task to run when idle
501
- * void requestIdleTask(processDataTask)();
2273
+ * for (const result of results.value) {
2274
+ * if (result.ok) {
2275
+ * console.log("Success:", result.value);
2276
+ * } else {
2277
+ * console.log("Failed:", result.error);
2278
+ * }
2279
+ * }
502
2280
  * ```
2281
+ *
2282
+ * @group Composition
2283
+ * @see {@link CollectOptions}
503
2284
  */
504
- export declare const requestIdleTask: <T, E>(task: Task<T, E>) => Task<T, E>;
2285
+ export declare function mapSettled<A, T, E, D>(items: Iterable<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<ReadonlyArray<Result<T, E | AbortError>>, never, D>;
505
2286
  /**
506
- * Represents a value that can be either synchronous or asynchronous.
2287
+ * Returns object with same keys.
507
2288
  *
508
- * This type is useful for functions that may complete synchronously or
509
- * asynchronously depending on runtime conditions (e.g., cache hit vs network
510
- * fetch).
2289
+ * ```ts
2290
+ * const results = await run(mapSettled({ a: 1, b: 2 }, (n) => double(n)));
2291
+ * if (!results.ok) return results;
2292
+ * // results.value: { a: Result<number, E>, b: Result<number, E> }
2293
+ * ```
2294
+ */
2295
+ export declare function mapSettled<A, T, E, D, K extends string>(items: Readonly<Record<K, A>>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<Readonly<Record<K, Result<T, E | AbortError>>>, never, D>;
2296
+ /**
2297
+ * Guarantees non-empty result.
511
2298
  *
512
- * ### Why MaybeAsync?
2299
+ * ```ts
2300
+ * const ids: NonEmptyReadonlyArray<UserId> = [id1, id2];
2301
+ * const results = await run(mapSettled(ids, fetchUser));
2302
+ * if (!results.ok) return results;
2303
+ * // results.value: NonEmptyReadonlyArray<Result<User, FetchError | AbortError>>
2304
+ * ```
2305
+ */
2306
+ export declare function mapSettled<A, T, E, D>(items: NonEmptyReadonlyArray<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<NonEmptyReadonlyArray<Result<T, E | AbortError>>, never, D>;
2307
+ /**
2308
+ * Run for side effects only.
513
2309
  *
514
- * When a function can be sync or async, the typical approaches are:
2310
+ * ```ts
2311
+ * const result = await run(
2312
+ * mapSettled(userIds, sendEmail, { collect: false }),
2313
+ * );
2314
+ * // result.value: void
2315
+ * ```
2316
+ */
2317
+ export declare function mapSettled<A, T, E, D>(items: Iterable<A> | Readonly<Record<string, A>>, task: (a: A) => Task<T, E, D>, options: CollectOptions<false>): Task<void, never, D>;
2318
+ /**
2319
+ * Returns the first {@link Task} that succeeds.
515
2320
  *
516
- * 1. **Always return Promise** - Simple but forces microtask overhead even for
517
- * sync values (see "await always adds microtask" test in Task.test.ts)
518
- * 2. **Use callbacks** - Can avoid microtask, but calling code must still `await`
519
- * for sane composition, which adds microtask anyway
520
- * 3. **Return `T | PromiseLike<T>`** - Calling code can check the value and only
521
- * `await` when needed, avoiding microtask overhead for sync cases
2321
+ * Like
2322
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
2323
+ * the first Task to succeed wins. All other Tasks are aborted. If all Tasks
2324
+ * fail, returns the last error (by input order).
522
2325
  *
523
- * The third approach (MaybeAsync) provides:
2326
+ * Sequential by default. Use {@link concurrently} for concurrent execution.
524
2327
  *
525
- * - **Performance**: No microtask overhead for synchronous operations
526
- * - **Reliability**: No interleaving via microtask queue when operations are
527
- * _synchronous_, reducing need for mutexes to protect shared state
2328
+ * Think of it like `Array.prototype.some()` it stops on the first success.
2329
+ * This is in contrast to {@link race}, which returns the first task to complete
2330
+ * (whether success or failure).
528
2331
  *
529
2332
  * ### Example
530
2333
  *
531
2334
  * ```ts
532
- * // Function that may be sync or async
533
- * const getData = (id: string): MaybeAsync<Data> => {
534
- * const cached = cache.get(id);
535
- * if (cached) return cached; // Sync path
536
- * return fetchData(id); // Async path
537
- * };
538
- *
539
- * // Caller can optimize based on actual behavior
540
- * const result = getData(id);
541
- * const data = isAsync(result) ? await result : result;
2335
+ * // Try multiple endpoints concurrently, first success wins
2336
+ * const result = await run(
2337
+ * concurrently(
2338
+ * any([fetchFromPrimary, fetchFromSecondary, fetchFromTertiary]),
2339
+ * ),
2340
+ * );
542
2341
  * ```
543
2342
  *
544
- * ### Alternative Approaches
2343
+ * @group Composition
2344
+ */
2345
+ export declare function any<T, E, D>(tasks: NonEmptyReadonlyArray<Task<T, E, D>>, options?: {
2346
+ /** How to choose an error if all tasks fail. */
2347
+ allFailed?: AnyAllFailed;
2348
+ }): Task<T, E, D>;
2349
+ /**
2350
+ * Tie-breaker for {@link any} when all tasks fail.
2351
+ *
2352
+ * Used only when no task succeeds.
2353
+ *
2354
+ * - `"input"` returns the error from the last task in the input array. This is
2355
+ * stable under concurrency and generally produces deterministic tests.
2356
+ * - `"completion"` returns the error from the Task that finished last. This
2357
+ * reflects timing but can vary across runs when task timing varies.
2358
+ *
2359
+ * ### Example
2360
+ *
2361
+ * ```ts
2362
+ * await using run = createRun();
2363
+ * const result = await run(
2364
+ * concurrently(any([a, b, c], { allFailed: "completion" })),
2365
+ * );
2366
+ * ```
2367
+ */
2368
+ export type AnyAllFailed = "input" | "completion";
2369
+ /**
2370
+ * Abort reason used by {@link any} when aborting remaining tasks.
545
2371
  *
546
- * It's possible to eliminate the sync/async distinction using complex
547
- * frameworks with custom schedulers. However, such frameworks require depending
548
- * on other people's code that controls how your code executes, resulting in
549
- * more complex stack traces and debugging experiences. With MaybeAsync, we
550
- * don't need that machinery - it works directly with JavaScript's native
551
- * primitives and TypeScript's type system.
2372
+ * @group Composition
2373
+ */
2374
+ export declare const AnyAbortError: import("./Type.js").TypedType<"AnyAbortError", Record<never, never>>;
2375
+ export interface AnyAbortError extends InferType<typeof AnyAbortError> {
2376
+ }
2377
+ /**
2378
+ * {@link AnyAbortError} used as abort reason in {@link any}.
552
2379
  *
553
- * ### TODO: Consider
2380
+ * @group Composition
2381
+ */
2382
+ export declare const anyAbortError: AnyAbortError;
2383
+ /**
2384
+ * Error returned when a {@link fetch} {@link Task} fails.
554
2385
  *
555
- * Use MaybeAsync in Task and Task helpers to preserve synchronous execution
556
- * when possible (e.g., mutex with available permit, retry on first success).
2386
+ * @group Composition
557
2387
  */
558
- export type MaybeAsync<T> = T | PromiseLike<T>;
2388
+ export declare const FetchError: import("./Type.js").TypedType<"FetchError", {
2389
+ error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
2390
+ }>;
2391
+ export interface FetchError extends InferType<typeof FetchError> {
2392
+ }
559
2393
  /**
560
- * Type guard to check if a {@link MaybeAsync} value is async (a promise).
2394
+ * Creates a {@link Task} that wraps the native
2395
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}.
561
2396
  *
562
- * This function narrows the type of a {@link MaybeAsync} value, allowing you to
563
- * conditionally `await` only when necessary.
2397
+ * Handles cross-browser abort behavior WebKit throws a `DOMException` with
2398
+ * message "Fetch is aborted" instead of propagating `signal.reason`. This
2399
+ * helper normalizes the behavior to always return {@link AbortError}.
564
2400
  *
565
2401
  * ### Example
566
2402
  *
567
2403
  * ```ts
568
- * const getData = (id: string): MaybeAsync<Data> => {
569
- * const cached = cache.get(id);
570
- * if (cached) return cached; // Sync path
571
- * return fetchData(id); // Async path
572
- * };
2404
+ * await using run = createRun();
2405
+ *
2406
+ * const result = await run(fetch("https://api.example.com/users"));
573
2407
  *
574
- * const result = getData(id);
575
- * const data = isAsync(result) ? await result : result;
576
- * // No microtask overhead when cached!
2408
+ * if (!result.ok) {
2409
+ * // Handle FetchError or AbortError
2410
+ * }
2411
+ *
2412
+ * // Compose with timeout and retry
2413
+ * const fetchWithRetry = (url: string) =>
2414
+ * retry(timeout(fetch(url), "10s"), retryStrategyAws);
577
2415
  * ```
2416
+ *
2417
+ * @group Composition
578
2418
  */
579
- export declare const isAsync: <T>(value: MaybeAsync<T>) => value is T extends PromiseLike<unknown> ? never : PromiseLike<T>;
2419
+ export declare const fetch: (input: RequestInfo | URL, init?: RequestInit) => Task<Response, FetchError>;
580
2420
  //# sourceMappingURL=Task.d.ts.map