@evolu/common 8.4.0 → 8.6.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 (353) hide show
  1. package/dist/src/Array.d.ts +191 -94
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +91 -40
  4. package/dist/src/Assert.d.ts +233 -30
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +151 -30
  7. package/dist/src/BigInt.d.ts +3 -3
  8. package/dist/src/BigInt.js +3 -3
  9. package/dist/src/Brand.d.ts +7 -7
  10. package/dist/src/Buffer.d.ts +12 -4
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Cache.d.ts +8 -3
  13. package/dist/src/Cache.d.ts.map +1 -1
  14. package/dist/src/Cache.js +8 -3
  15. package/dist/src/Callbacks.d.ts +8 -4
  16. package/dist/src/Callbacks.d.ts.map +1 -1
  17. package/dist/src/Console.d.ts +45 -30
  18. package/dist/src/Console.d.ts.map +1 -1
  19. package/dist/src/Console.js +32 -21
  20. package/dist/src/Crypto.d.ts +13 -8
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +6 -3
  23. package/dist/src/Eq.d.ts +107 -58
  24. package/dist/src/Eq.d.ts.map +1 -1
  25. package/dist/src/Eq.js +308 -112
  26. package/dist/src/Function.d.ts +54 -24
  27. package/dist/src/Function.d.ts.map +1 -1
  28. package/dist/src/Function.js +34 -17
  29. package/dist/src/Http.d.ts +52 -19
  30. package/dist/src/Http.d.ts.map +1 -1
  31. package/dist/src/Identicon.d.ts +9 -4
  32. package/dist/src/Identicon.d.ts.map +1 -1
  33. package/dist/src/Identicon.js +9 -4
  34. package/dist/src/LeakDetector.d.ts.map +1 -1
  35. package/dist/src/LeakDetector.js +4 -1
  36. package/dist/src/LockManager.d.ts +12 -10
  37. package/dist/src/LockManager.d.ts.map +1 -1
  38. package/dist/src/Lookup.d.ts +4 -2
  39. package/dist/src/Lookup.d.ts.map +1 -1
  40. package/dist/src/Lookup.js +5 -2
  41. package/dist/src/Number.d.ts +28 -24
  42. package/dist/src/Number.d.ts.map +1 -1
  43. package/dist/src/Number.js +16 -17
  44. package/dist/src/Object.d.ts +89 -47
  45. package/dist/src/Object.d.ts.map +1 -1
  46. package/dist/src/Object.js +88 -41
  47. package/dist/src/Option.d.ts +14 -5
  48. package/dist/src/Option.d.ts.map +1 -1
  49. package/dist/src/Option.js +14 -5
  50. package/dist/src/Order.d.ts +12 -12
  51. package/dist/src/Order.js +12 -12
  52. package/dist/src/Platform.d.ts +2 -2
  53. package/dist/src/Platform.js +1 -0
  54. package/dist/src/Random.d.ts +7 -4
  55. package/dist/src/Random.d.ts.map +1 -1
  56. package/dist/src/Redacted.d.ts +13 -6
  57. package/dist/src/Redacted.d.ts.map +1 -1
  58. package/dist/src/Redacted.js +4 -2
  59. package/dist/src/Ref.d.ts +4 -4
  60. package/dist/src/Relation.d.ts +5 -7
  61. package/dist/src/Relation.d.ts.map +1 -1
  62. package/dist/src/Relation.js +3 -3
  63. package/dist/src/Resource.d.ts +27 -12
  64. package/dist/src/Resource.d.ts.map +1 -1
  65. package/dist/src/Resource.js +9 -3
  66. package/dist/src/Result.d.ts +258 -114
  67. package/dist/src/Result.d.ts.map +1 -1
  68. package/dist/src/Result.js +74 -39
  69. package/dist/src/Schedule.d.ts +233 -116
  70. package/dist/src/Schedule.d.ts.map +1 -1
  71. package/dist/src/Schedule.js +204 -110
  72. package/dist/src/Set.d.ts +43 -24
  73. package/dist/src/Set.d.ts.map +1 -1
  74. package/dist/src/Set.js +25 -13
  75. package/dist/src/Sqlite.d.ts +4 -4
  76. package/dist/src/Sqlite.d.ts.map +1 -1
  77. package/dist/src/Sqlite.js +18 -15
  78. package/dist/src/Store.d.ts.map +1 -1
  79. package/dist/src/Store.js +3 -1
  80. package/dist/src/String.d.ts.map +1 -1
  81. package/dist/src/String.js +1 -1
  82. package/dist/src/Task.d.ts +555 -264
  83. package/dist/src/Task.d.ts.map +1 -1
  84. package/dist/src/Task.js +278 -168
  85. package/dist/src/Test.d.ts +10 -4
  86. package/dist/src/Test.d.ts.map +1 -1
  87. package/dist/src/Test.js +11 -4
  88. package/dist/src/Time.d.ts +27 -17
  89. package/dist/src/Time.d.ts.map +1 -1
  90. package/dist/src/Time.js +25 -8
  91. package/dist/src/Type.d.ts +1218 -372
  92. package/dist/src/Type.d.ts.map +1 -1
  93. package/dist/src/Type.js +1056 -379
  94. package/dist/src/Types.d.ts +95 -40
  95. package/dist/src/Types.d.ts.map +1 -1
  96. package/dist/src/Types.js +13 -4
  97. package/dist/src/WebSocket.d.ts +14 -6
  98. package/dist/src/WebSocket.d.ts.map +1 -1
  99. package/dist/src/WebSocket.js +8 -0
  100. package/dist/src/Worker.d.ts +10 -6
  101. package/dist/src/Worker.d.ts.map +1 -1
  102. package/dist/src/Worker.js +1 -1
  103. package/dist/src/intl/_en.d.ts +3 -1
  104. package/dist/src/intl/_en.d.ts.map +1 -1
  105. package/dist/src/intl/_en.js +17 -3
  106. package/dist/src/intl/ar.d.ts +3 -1
  107. package/dist/src/intl/ar.d.ts.map +1 -1
  108. package/dist/src/intl/ar.js +15 -2
  109. package/dist/src/intl/bn.d.ts +3 -1
  110. package/dist/src/intl/bn.d.ts.map +1 -1
  111. package/dist/src/intl/bn.js +16 -3
  112. package/dist/src/intl/ca.d.ts +3 -1
  113. package/dist/src/intl/ca.d.ts.map +1 -1
  114. package/dist/src/intl/ca.js +16 -3
  115. package/dist/src/intl/cs.d.ts +3 -1
  116. package/dist/src/intl/cs.d.ts.map +1 -1
  117. package/dist/src/intl/cs.js +16 -3
  118. package/dist/src/intl/da.d.ts +3 -1
  119. package/dist/src/intl/da.d.ts.map +1 -1
  120. package/dist/src/intl/da.js +16 -3
  121. package/dist/src/intl/de.d.ts +3 -1
  122. package/dist/src/intl/de.d.ts.map +1 -1
  123. package/dist/src/intl/de.js +16 -3
  124. package/dist/src/intl/el.d.ts +3 -1
  125. package/dist/src/intl/el.d.ts.map +1 -1
  126. package/dist/src/intl/el.js +16 -3
  127. package/dist/src/intl/es.d.ts +3 -1
  128. package/dist/src/intl/es.d.ts.map +1 -1
  129. package/dist/src/intl/es.js +16 -3
  130. package/dist/src/intl/fa.d.ts +3 -1
  131. package/dist/src/intl/fa.d.ts.map +1 -1
  132. package/dist/src/intl/fa.js +16 -3
  133. package/dist/src/intl/fi.d.ts +3 -1
  134. package/dist/src/intl/fi.d.ts.map +1 -1
  135. package/dist/src/intl/fi.js +16 -3
  136. package/dist/src/intl/fil.d.ts +3 -1
  137. package/dist/src/intl/fil.d.ts.map +1 -1
  138. package/dist/src/intl/fil.js +16 -3
  139. package/dist/src/intl/fr.d.ts +3 -1
  140. package/dist/src/intl/fr.d.ts.map +1 -1
  141. package/dist/src/intl/fr.js +16 -3
  142. package/dist/src/intl/he.d.ts +3 -1
  143. package/dist/src/intl/he.d.ts.map +1 -1
  144. package/dist/src/intl/he.js +16 -3
  145. package/dist/src/intl/hi.d.ts +3 -1
  146. package/dist/src/intl/hi.d.ts.map +1 -1
  147. package/dist/src/intl/hi.js +16 -3
  148. package/dist/src/intl/hr.d.ts +3 -1
  149. package/dist/src/intl/hr.d.ts.map +1 -1
  150. package/dist/src/intl/hr.js +16 -3
  151. package/dist/src/intl/hu.d.ts +2 -1
  152. package/dist/src/intl/hu.d.ts.map +1 -1
  153. package/dist/src/intl/hu.js +15 -3
  154. package/dist/src/intl/id.d.ts +3 -1
  155. package/dist/src/intl/id.d.ts.map +1 -1
  156. package/dist/src/intl/id.js +16 -3
  157. package/dist/src/intl/it.d.ts +3 -1
  158. package/dist/src/intl/it.d.ts.map +1 -1
  159. package/dist/src/intl/it.js +16 -3
  160. package/dist/src/intl/ja.d.ts +3 -1
  161. package/dist/src/intl/ja.d.ts.map +1 -1
  162. package/dist/src/intl/ja.js +16 -3
  163. package/dist/src/intl/ko.d.ts +3 -1
  164. package/dist/src/intl/ko.d.ts.map +1 -1
  165. package/dist/src/intl/ko.js +16 -3
  166. package/dist/src/intl/ml.d.ts +3 -1
  167. package/dist/src/intl/ml.d.ts.map +1 -1
  168. package/dist/src/intl/ml.js +16 -3
  169. package/dist/src/intl/mr.d.ts +3 -1
  170. package/dist/src/intl/mr.d.ts.map +1 -1
  171. package/dist/src/intl/mr.js +16 -3
  172. package/dist/src/intl/ms.d.ts +3 -1
  173. package/dist/src/intl/ms.d.ts.map +1 -1
  174. package/dist/src/intl/ms.js +15 -2
  175. package/dist/src/intl/nb.d.ts +2 -1
  176. package/dist/src/intl/nb.d.ts.map +1 -1
  177. package/dist/src/intl/nb.js +14 -2
  178. package/dist/src/intl/nl.d.ts +3 -1
  179. package/dist/src/intl/nl.d.ts.map +1 -1
  180. package/dist/src/intl/nl.js +16 -3
  181. package/dist/src/intl/pa.d.ts +3 -1
  182. package/dist/src/intl/pa.d.ts.map +1 -1
  183. package/dist/src/intl/pa.js +16 -3
  184. package/dist/src/intl/pl.d.ts +2 -0
  185. package/dist/src/intl/pl.d.ts.map +1 -1
  186. package/dist/src/intl/pl.js +15 -2
  187. package/dist/src/intl/pt-BR.d.ts +3 -1
  188. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  189. package/dist/src/intl/pt-BR.js +16 -3
  190. package/dist/src/intl/pt.d.ts +3 -1
  191. package/dist/src/intl/pt.d.ts.map +1 -1
  192. package/dist/src/intl/pt.js +15 -2
  193. package/dist/src/intl/ro.d.ts +3 -1
  194. package/dist/src/intl/ro.d.ts.map +1 -1
  195. package/dist/src/intl/ro.js +16 -3
  196. package/dist/src/intl/sk.d.ts +3 -1
  197. package/dist/src/intl/sk.d.ts.map +1 -1
  198. package/dist/src/intl/sk.js +15 -2
  199. package/dist/src/intl/sl.d.ts +3 -1
  200. package/dist/src/intl/sl.d.ts.map +1 -1
  201. package/dist/src/intl/sl.js +16 -3
  202. package/dist/src/intl/sv.d.ts +3 -1
  203. package/dist/src/intl/sv.d.ts.map +1 -1
  204. package/dist/src/intl/sv.js +16 -3
  205. package/dist/src/intl/sw.d.ts +1 -0
  206. package/dist/src/intl/sw.d.ts.map +1 -1
  207. package/dist/src/intl/sw.js +14 -2
  208. package/dist/src/intl/ta.d.ts +3 -1
  209. package/dist/src/intl/ta.d.ts.map +1 -1
  210. package/dist/src/intl/ta.js +16 -3
  211. package/dist/src/intl/te.d.ts +3 -1
  212. package/dist/src/intl/te.d.ts.map +1 -1
  213. package/dist/src/intl/te.js +16 -3
  214. package/dist/src/intl/th.d.ts +3 -1
  215. package/dist/src/intl/th.d.ts.map +1 -1
  216. package/dist/src/intl/th.js +16 -3
  217. package/dist/src/intl/tr.d.ts +3 -1
  218. package/dist/src/intl/tr.d.ts.map +1 -1
  219. package/dist/src/intl/tr.js +16 -3
  220. package/dist/src/intl/uk.d.ts +3 -1
  221. package/dist/src/intl/uk.d.ts.map +1 -1
  222. package/dist/src/intl/uk.js +16 -3
  223. package/dist/src/intl/ur.d.ts +3 -1
  224. package/dist/src/intl/ur.d.ts.map +1 -1
  225. package/dist/src/intl/ur.js +15 -2
  226. package/dist/src/intl/vi.d.ts +3 -1
  227. package/dist/src/intl/vi.d.ts.map +1 -1
  228. package/dist/src/intl/vi.js +16 -3
  229. package/dist/src/intl/zh-CN.d.ts +3 -1
  230. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  231. package/dist/src/intl/zh-CN.js +16 -3
  232. package/dist/src/intl/zh-TW.d.ts +3 -1
  233. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  234. package/dist/src/intl/zh-TW.js +16 -3
  235. package/dist/src/local-first/Db.js +3 -2
  236. package/dist/src/local-first/Evolu.d.ts +47 -24
  237. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  238. package/dist/src/local-first/Evolu.js +1 -0
  239. package/dist/src/local-first/Owner.d.ts +18 -14
  240. package/dist/src/local-first/Owner.d.ts.map +1 -1
  241. package/dist/src/local-first/Owner.js +14 -11
  242. package/dist/src/local-first/Protocol.d.ts +5 -5
  243. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  244. package/dist/src/local-first/Protocol.js +35 -29
  245. package/dist/src/local-first/Query.d.ts +34 -21
  246. package/dist/src/local-first/Query.d.ts.map +1 -1
  247. package/dist/src/local-first/Query.js +27 -14
  248. package/dist/src/local-first/Relay.d.ts +12 -5
  249. package/dist/src/local-first/Relay.d.ts.map +1 -1
  250. package/dist/src/local-first/Relay.js +1 -1
  251. package/dist/src/local-first/Schema.d.ts +19 -11
  252. package/dist/src/local-first/Schema.d.ts.map +1 -1
  253. package/dist/src/local-first/Schema.js +11 -6
  254. package/dist/src/local-first/Shared.d.ts.map +1 -1
  255. package/dist/src/local-first/Shared.js +4 -2
  256. package/dist/src/local-first/Storage.d.ts +4 -2
  257. package/dist/src/local-first/Storage.d.ts.map +1 -1
  258. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  259. package/dist/src/local-first/Timestamp.js +1 -1
  260. package/package.json +3 -4
  261. package/src/Array.ts +195 -95
  262. package/src/Assert.ts +337 -31
  263. package/src/BigInt.ts +3 -3
  264. package/src/Brand.ts +7 -7
  265. package/src/Buffer.ts +12 -4
  266. package/src/Cache.ts +8 -3
  267. package/src/Callbacks.ts +8 -4
  268. package/src/Console.ts +45 -30
  269. package/src/Crypto.ts +13 -8
  270. package/src/Eq.ts +450 -122
  271. package/src/Function.ts +54 -24
  272. package/src/Http.ts +52 -19
  273. package/src/Identicon.ts +9 -4
  274. package/src/LeakDetector.ts +4 -3
  275. package/src/LockManager.ts +12 -10
  276. package/src/Lookup.ts +5 -2
  277. package/src/Number.ts +28 -24
  278. package/src/Object.ts +115 -49
  279. package/src/Option.ts +14 -5
  280. package/src/Order.ts +12 -12
  281. package/src/Platform.ts +3 -2
  282. package/src/Random.ts +7 -4
  283. package/src/Redacted.ts +13 -6
  284. package/src/Ref.ts +4 -4
  285. package/src/Relation.ts +8 -10
  286. package/src/Resource.ts +36 -15
  287. package/src/Result.ts +263 -119
  288. package/src/Schedule.ts +233 -116
  289. package/src/Set.ts +43 -24
  290. package/src/Sqlite.ts +18 -15
  291. package/src/Store.ts +3 -1
  292. package/src/String.ts +1 -1
  293. package/src/Task.ts +566 -286
  294. package/src/Test.ts +11 -4
  295. package/src/Time.ts +36 -19
  296. package/src/Type.ts +2191 -590
  297. package/src/Types.ts +108 -40
  298. package/src/WebSocket.ts +22 -6
  299. package/src/Worker.ts +11 -7
  300. package/src/intl/_en.ts +20 -4
  301. package/src/intl/ar.ts +17 -2
  302. package/src/intl/bn.ts +19 -4
  303. package/src/intl/ca.ts +19 -4
  304. package/src/intl/cs.ts +19 -4
  305. package/src/intl/da.ts +19 -4
  306. package/src/intl/de.ts +19 -4
  307. package/src/intl/el.ts +19 -4
  308. package/src/intl/es.ts +19 -4
  309. package/src/intl/fa.ts +19 -4
  310. package/src/intl/fi.ts +19 -4
  311. package/src/intl/fil.ts +19 -4
  312. package/src/intl/fr.ts +19 -4
  313. package/src/intl/he.ts +19 -4
  314. package/src/intl/hi.ts +19 -4
  315. package/src/intl/hr.ts +19 -4
  316. package/src/intl/hu.ts +18 -4
  317. package/src/intl/id.ts +19 -4
  318. package/src/intl/it.ts +19 -4
  319. package/src/intl/ja.ts +19 -4
  320. package/src/intl/ko.ts +19 -4
  321. package/src/intl/ml.ts +19 -4
  322. package/src/intl/mr.ts +19 -4
  323. package/src/intl/ms.ts +17 -2
  324. package/src/intl/nb.ts +16 -2
  325. package/src/intl/nl.ts +19 -4
  326. package/src/intl/pa.ts +19 -4
  327. package/src/intl/pl.ts +18 -2
  328. package/src/intl/pt-BR.ts +19 -4
  329. package/src/intl/pt.ts +17 -2
  330. package/src/intl/ro.ts +19 -4
  331. package/src/intl/sk.ts +17 -2
  332. package/src/intl/sl.ts +19 -4
  333. package/src/intl/sv.ts +19 -4
  334. package/src/intl/sw.ts +15 -2
  335. package/src/intl/ta.ts +19 -4
  336. package/src/intl/te.ts +19 -4
  337. package/src/intl/th.ts +19 -4
  338. package/src/intl/tr.ts +19 -4
  339. package/src/intl/uk.ts +19 -4
  340. package/src/intl/ur.ts +17 -2
  341. package/src/intl/vi.ts +19 -3
  342. package/src/intl/zh-CN.ts +19 -4
  343. package/src/intl/zh-TW.ts +19 -4
  344. package/src/local-first/Db.ts +2 -1
  345. package/src/local-first/Evolu.ts +48 -24
  346. package/src/local-first/Owner.ts +18 -14
  347. package/src/local-first/Protocol.ts +45 -35
  348. package/src/local-first/Query.ts +40 -24
  349. package/src/local-first/Relay.ts +13 -6
  350. package/src/local-first/Schema.ts +19 -11
  351. package/src/local-first/Shared.ts +4 -2
  352. package/src/local-first/Storage.ts +4 -2
  353. package/src/local-first/Timestamp.ts +1 -1
package/src/Task.ts CHANGED
@@ -1,41 +1,39 @@
1
1
  /**
2
- * ## Intro
3
- *
4
2
  * JavaScript-native structured concurrency.
5
3
  *
6
- * Structured concurrency makes ownership of asynchronous work explicit.
7
- * Operations form a tree where every child belongs to a parent. A parent waits
8
- * for its children before it completes, and abort follows the tree: aborting a
9
- * parent requests abort of all its descendants. Races and fail-fast operations
10
- * also abort their remaining sibling branches.
4
+ * Structured concurrency organizes running tasks into a tree. Every child
5
+ * belongs to a parent, a parent waits for its children before it completes, and
6
+ * abort propagates from parents to descendants. Races and fail-fast control
7
+ * flow abort siblings that are no longer needed.
11
8
  *
12
9
  * With plain {@link AbortController} code, these guarantees depend on call-site
13
- * discipline: someone must remember the `finally` that aborts started work and
14
- * the await that waits for cleanup. {@link Run} makes both structural:
15
- * `run(task)` registers every child before it starts, and the parent settles
10
+ * discipline: someone must remember the `finally` that aborts started tasks and
11
+ * the await that waits for cleanup. Evolu makes both structural: `run(task)`
12
+ * registers every child before it starts, and the parent {@link Run} settles
16
13
  * only after child cleanup finishes.
17
14
  *
18
- * Evolu models structured concurrency with ordinary JavaScript:
15
+ * Evolu implements structured concurrency with:
19
16
  *
20
- * - A {@link Task} describes an asynchronous operation and its dependencies.
17
+ * - A {@link Task} is a function passed to Run that returns an {@link Awaitable}
18
+ * {@link Result} and declares its dependencies.
21
19
  * - A {@link Run} starts Tasks and owns their lifetimes.
22
20
  * - A {@link Fiber} is the Promise-backed handle returned when a Run starts a
23
21
  * Task.
24
22
  * - An {@link AbortableFiber} adds explicit abort and async disposal.
25
23
  *
26
- * The runtime core is deliberately small: ordinary functions, a callable Run
27
- * with closed-over state, Promise-backed Fibers, {@link AbortSignal}
28
- * propagation, and JavaScript resource management. Together, these primitives
29
- * provide abort, cleanup, defect handling, dependency injection, monitoring,
30
- * concurrency, and resource bracketing.
24
+ * Together, these APIs provide abort, cleanup, defect handling, dependency
25
+ * injection, monitoring, and resource management.
31
26
  *
32
- * Tasks return domain success or failure as {@link Result}. Abort is control
33
- * flow represented by {@link AbortError}. If a Task throws or rejects with
34
- * anything else, that is a defect: the root Run reports it and shuts down its
35
- * tree so code does not continue in a potentially invalid state.
27
+ * Tasks return a {@link Result} containing either success or a domain error.
28
+ * Abort is control flow represented by {@link AbortError}. If a Task throws or
29
+ * rejects with anything else, that is a defect: the root Run reports it and
30
+ * shuts down its tree so code does not continue in a potentially invalid
31
+ * state.
36
32
  *
37
33
  * ```ts
38
34
  * import {
35
+ * assertOk,
36
+ * assertType,
39
37
  * createRun,
40
38
  * err,
41
39
  * ok,
@@ -78,8 +76,8 @@
78
76
  * });
79
77
  *
80
78
  * const result = await run(getUser(user.id));
81
- * expectTypeOf(result).toEqualTypeOf<Result<User, UserNotFoundError>>();
82
- * expectOk(result, user);
79
+ * assertType<Result<User, UserNotFoundError>, typeof result>();
80
+ * assertOk(result, user);
83
81
  * ```
84
82
  *
85
83
  * In composition roots, prefer the lifecycle API from the matching Evolu
@@ -122,6 +120,8 @@
122
120
  *
123
121
  * ```ts
124
122
  * import {
123
+ * assertOk,
124
+ * assertType,
125
125
  * createRun,
126
126
  * err,
127
127
  * ok,
@@ -180,13 +180,14 @@
180
180
  *
181
181
  * await using run = createRun();
182
182
  * const result = await run(getUserWithProfile("user-1"));
183
- * expectTypeOf(result).toEqualTypeOf<
183
+ * assertType<
184
184
  * Result<
185
185
  * { readonly user: User; readonly profile: Profile },
186
186
  * UserNotFoundError | ProfileNotFoundError
187
- * >
187
+ * >,
188
+ * typeof result
188
189
  * >();
189
- * expectOk(result, {
190
+ * assertOk(result, {
190
191
  * user: { id: "user-1", profileId: "profile-1" },
191
192
  * profile: { id: "profile-1" },
192
193
  * });
@@ -201,10 +202,11 @@
201
202
  *
202
203
  * {@link fetch} with a body mode already returns a plain value, so resilience is
203
204
  * ordinary Task composition. Combine {@link timeout} and {@link retry} to bound
204
- * each attempt and retry recoverable domain failures:
205
+ * each attempt and retry recoverable domain errors:
205
206
  *
206
207
  * ```ts
207
208
  * import {
209
+ * assertType,
208
210
  * exponential,
209
211
  * fetch,
210
212
  * jitter,
@@ -225,8 +227,9 @@
225
227
  * jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
226
228
  * );
227
229
  *
228
- * expectTypeOf(fetchWithRetry).returns.toEqualTypeOf<
229
- * Task<string, RetryTaskError<FetchError | TimeoutError>>
230
+ * assertType<
231
+ * Task<string, RetryTaskError<FetchError | TimeoutError>>,
232
+ * ReturnType<typeof fetchWithRetry>
230
233
  * >();
231
234
  * ```
232
235
  *
@@ -235,7 +238,15 @@
235
238
  * Run composed Tasks with a `concurrency` option and {@link all}:
236
239
  *
237
240
  * ```ts
238
- * import { all, createRun, ok, sleep, type Task } from "@evolu/common";
241
+ * import {
242
+ * assertEqual,
243
+ * assertOk,
244
+ * all,
245
+ * createRun,
246
+ * ok,
247
+ * sleep,
248
+ * type Task,
249
+ * } from "@evolu/common";
239
250
  *
240
251
  * await using run = createRun();
241
252
  *
@@ -258,8 +269,8 @@
258
269
  *
259
270
  * // At most 2 concurrent requests.
260
271
  * const result = await run(all(urls, fetchUrl, { concurrency: 2 }));
261
- * expectOk(result, urls);
262
- * expect(maxActiveRequests).toBe(2);
272
+ * assertOk(result, urls);
273
+ * assertEqual(maxActiveRequests, 2);
263
274
  * ```
264
275
  *
265
276
  * Task helpers compose Tasks; concurrency primitives are stateful objects that
@@ -291,7 +302,7 @@
291
302
  * resources, or services shared by all code running inside a Run.
292
303
  *
293
304
  * ```ts
294
- * import { createRun, ok, type Task } from "@evolu/common";
305
+ * import { assertOk, createRun, ok, type Task } from "@evolu/common";
295
306
  *
296
307
  * interface GreetingFormatter {
297
308
  * readonly format: (name: string) => string;
@@ -316,10 +327,10 @@
316
327
  * await using run = createRun({ greetingFormatter: formal });
317
328
  *
318
329
  * // Root dependencies are inherited.
319
- * expectOk(await run(greet("Ada")), "Hello, Ada");
330
+ * assertOk(await run(greet("Ada")), "Hello, Ada");
320
331
  *
321
332
  * // Child-specific dependencies replace the root's custom dependencies.
322
- * expectOk(
333
+ * assertOk(
323
334
  * await run(greet("Ada"), { greetingFormatter: casual }),
324
335
  * "Hi, Ada",
325
336
  * );
@@ -370,7 +381,15 @@
370
381
  * `undefined` should represent valid absence, not failure.
371
382
  *
372
383
  * ```ts
373
- * import { createRun, ok, type Task, type Typed } from "@evolu/common";
384
+ * import {
385
+ * assertEqual,
386
+ * assertFalse,
387
+ * assertTrue,
388
+ * createRun,
389
+ * ok,
390
+ * type Task,
391
+ * type Typed,
392
+ * } from "@evolu/common";
374
393
  *
375
394
  * interface Socket extends AsyncDisposable {
376
395
  * readonly send: (message: string) => string;
@@ -384,8 +403,9 @@
384
403
  * const openSocket: Task<Socket, ConnectionFailedError> = () =>
385
404
  * ok({
386
405
  * send: (message) => message,
387
- * [Symbol.asyncDispose]: async () => {
406
+ * [Symbol.asyncDispose]: () => {
388
407
  * socketDisposed = true;
408
+ * return Promise.resolve();
389
409
  * },
390
410
  * });
391
411
  *
@@ -417,16 +437,16 @@
417
437
  *
418
438
  * await using run = createRun();
419
439
  * const result = await run(createConnection);
420
- * assert(result.ok);
421
- * expect(socketDisposed).toBe(false);
422
- * expect(result.value.send("hello")).toBe("hello");
440
+ * assertTrue(result.ok);
441
+ * assertFalse(socketDisposed);
442
+ * assertEqual(result.value.send("hello"), "hello");
423
443
  * await result.value[Symbol.asyncDispose]();
424
- * expect(socketDisposed).toBe(true);
444
+ * assertTrue(socketDisposed);
425
445
  * ```
426
446
  *
427
- * Use {@link Run.ok} with `await using` when an infallible Task returns a
428
- * disposable value. Use {@link acquireUseRelease} when acquisition and release
429
- * are separate operations rather than a disposable value.
447
+ * Use {@link Run.ok} with `await using` when a Task whose error type is `never`
448
+ * returns a disposable value. Use {@link acquireUseRelease} when acquisition and
449
+ * release are separate steps rather than a disposable value.
430
450
  *
431
451
  * ## Awaitable
432
452
  *
@@ -439,7 +459,7 @@
439
459
  *
440
460
  * A Task is an async ownership boundary, not a general unit of program
441
461
  * decomposition. Calling `run(task)` always creates a child Run by design. Use
442
- * ordinary promises when an async operation does not need its own Run.
462
+ * a plain async function when it does not need its own Run.
443
463
  *
444
464
  * A unified sync/async effect API is technically possible. It can detect
445
465
  * Promise-like values with {@link isPromiseLike}, dispose synchronous resources
@@ -457,7 +477,7 @@
457
477
  * code performs effects with the result. For example, a pure function can
458
478
  * accept a {@link RandomNumber} value instead of depending on {@link Random}.
459
479
  *
460
- * Large CPU-bound operations, such as parsing large JSON, sorting millions of
480
+ * Large CPU-bound computations, such as parsing large JSON, sorting millions of
461
481
  * items, or complex cryptography, belong in a worker. Model the asynchronous
462
482
  * call to that worker as a Task so Run can provide timeout, abort, cleanup, and
463
483
  * monitoring.
@@ -517,10 +537,10 @@
517
537
  * ### What should Task code do with defects?
518
538
  *
519
539
  * Nothing. Once a defect reaches the {@link Run}, it is too late: the root Run
520
- * panics, running Tasks are aborted, and the Run tree shuts down. If an
521
- * operation can throw or reject for a recoverable reason, wrap that operation
522
- * with {@link trySync} or {@link tryAsync} so the failure becomes a typed
523
- * {@link Result} error. Let unrecoverable failures propagate as defects.
540
+ * panics, running Tasks are aborted, and the Run tree shuts down. Use
541
+ * {@link trySync} or {@link tryAsync} to turn recoverable exceptions and Promise
542
+ * rejections into typed {@link Result} errors. Let unrecoverable failures
543
+ * propagate as defects.
524
544
  *
525
545
  * ### Why does a defect panic the whole Run tree?
526
546
  *
@@ -578,7 +598,7 @@
578
598
  * each iteration reuses the same stack frame:
579
599
  *
580
600
  * ```ts
581
- * import { createRun, ok, type Task } from "@evolu/common";
601
+ * import { assertOk, createRun, ok, type Task } from "@evolu/common";
582
602
  *
583
603
  * interface TreeNode {
584
604
  * readonly value: string;
@@ -602,7 +622,7 @@
602
622
  * };
603
623
  *
604
624
  * await using run = createRun();
605
- * expectOk(
625
+ * assertOk(
606
626
  * await run(
607
627
  * visitTree({
608
628
  * value: "root",
@@ -619,13 +639,6 @@
619
639
  * periodically await {@link yieldNow} for cooperative scheduling, and move
620
640
  * CPU-bound work to a worker.
621
641
  *
622
- * ### Should a Task be called directly?
623
- *
624
- * Only inside Task internals that explicitly require same-Run execution. A
625
- * direct call, `task(run)`, uses the current Run instead of creating a child
626
- * Run, so it bypasses child lifetime tracking, scheduling metadata, and child
627
- * disposal boundaries. Application code should use `run(task)`.
628
- *
629
642
  * ### Where are fork and join?
630
643
  *
631
644
  * Calling `run(task)` is fork: it starts a child Task and returns a
@@ -756,16 +769,90 @@ import type {
756
769
  Predicate,
757
770
  } from "./Types.ts";
758
771
 
759
- // Core
760
-
761
772
  /**
762
- * An operation run by {@link Run} that returns a {@link Result} synchronously or
763
- * asynchronously and declares its dependencies through `D`.
764
- *
765
- * Its return type is {@link Awaitable}.
773
+ * A function passed to {@link Run} that returns an {@link Awaitable}
774
+ * {@link Result} and declares its dependencies through `D`.
766
775
  *
767
776
  * See the {@link @evolu/common!Task | Task overview}.
768
777
  *
778
+ * ### Example
779
+ *
780
+ * A Task that can't fail with a domain error:
781
+ *
782
+ * ```ts
783
+ * import {
784
+ * assertEqual,
785
+ * assertOk,
786
+ * createRun,
787
+ * ok,
788
+ * type Task,
789
+ * } from "@evolu/common";
790
+ *
791
+ * const greet: Task<string> = () => ok("Hello!");
792
+ * const main: Task<string> = (run) => run(greet);
793
+ *
794
+ * await using run = createRun();
795
+ * assertOk(await run(main), "Hello!");
796
+ *
797
+ * // Without domain errors, `run.ok` returns the Ok value.
798
+ * assertEqual(await run.ok(main), "Hello!");
799
+ * ```
800
+ *
801
+ * A Task that can fail with a domain error:
802
+ *
803
+ * ```ts
804
+ * import {
805
+ * assertErr,
806
+ * createRun,
807
+ * err,
808
+ * type Task,
809
+ * type Typed,
810
+ * } from "@evolu/common";
811
+ *
812
+ * const findUser =
813
+ * (id: string): Task<string, UserNotFoundError> =>
814
+ * () =>
815
+ * err({ type: "UserNotFound", id });
816
+ *
817
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {
818
+ * readonly id: string;
819
+ * }
820
+ *
821
+ * await using run = createRun();
822
+ * assertErr(await run(findUser("user-1")), {
823
+ * type: "UserNotFound",
824
+ * id: "user-1",
825
+ * });
826
+ * ```
827
+ *
828
+ * A Task with dependencies:
829
+ *
830
+ * ```ts
831
+ * import { assertOk, createRun, ok, type Task } from "@evolu/common";
832
+ *
833
+ * interface Config {
834
+ * readonly greeting: string;
835
+ * }
836
+ *
837
+ * interface ConfigDep {
838
+ * readonly config: Config;
839
+ * }
840
+ *
841
+ * const greet: Task<string, never, ConfigDep> = (run) =>
842
+ * ok(`${run.deps.config.greeting}!`);
843
+ *
844
+ * const config: Config = { greeting: "Hello" };
845
+ * await using run = createRun({ config });
846
+ * assertOk(await run(greet), "Hello!");
847
+ * ```
848
+ *
849
+ * Start Tasks with `run(task)`, as shown above.
850
+ *
851
+ * A Task can also be called directly as `task(run)`, but this is rarely needed.
852
+ * The call executes the Task inline in the current Run, as if its body were
853
+ * part of the parent Task, so it does not create a child Run. This is mainly
854
+ * useful for Task composition helpers.
855
+ *
769
856
  * @group Core
770
857
  */
771
858
  export type Task<T, E = never, D = unknown> = (
@@ -914,7 +1001,13 @@ export interface Run<D = unknown> {
914
1001
  * ### Example
915
1002
  *
916
1003
  * ```ts
917
- * import { createRun, ok, type Task } from "@evolu/common";
1004
+ * import {
1005
+ * assertSame,
1006
+ * assertOk,
1007
+ * createRun,
1008
+ * ok,
1009
+ * type Task,
1010
+ * } from "@evolu/common";
918
1011
  *
919
1012
  * interface Db {
920
1013
  * readonly name: string;
@@ -927,16 +1020,23 @@ export interface Run<D = unknown> {
927
1020
  * const db: Db = { name: "main" };
928
1021
  * const loadUser: Task<string> = () => ok("Ada");
929
1022
  * const saveUser: Task<void, never, DbDep> = (run) => {
930
- * expect(run.deps.db).toBe(db);
1023
+ * assertSame(run.deps.db, db);
931
1024
  * return ok();
932
1025
  * };
933
1026
  *
934
1027
  * await using run = createRun({ db });
935
1028
  * const userResult = await run(loadUser);
936
1029
  * const savedResult = await run(saveUser);
937
- * expectOk(userResult, "Ada");
938
- * expectOk(savedResult, undefined);
1030
+ * assertOk(userResult, "Ada");
1031
+ * assertOk(savedResult, undefined);
939
1032
  * ```
1033
+ *
1034
+ * Start Tasks with `run(task)`, as shown above.
1035
+ *
1036
+ * A Task can also be called directly as `task(run)`, but this is rarely
1037
+ * needed. The call executes the Task inline in the current Run, as if its
1038
+ * body were part of the parent Task, so it does not create a child Run. This
1039
+ * is mainly useful for Task composition helpers.
940
1040
  */
941
1041
  <T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
942
1042
 
@@ -962,7 +1062,13 @@ export interface Run<D = unknown> {
962
1062
  * ### Example
963
1063
  *
964
1064
  * ```ts
965
- * import { createRun, ok, type Task, type Typed } from "@evolu/common";
1065
+ * import {
1066
+ * assertEqual,
1067
+ * createRun,
1068
+ * ok,
1069
+ * type Task,
1070
+ * type Typed,
1071
+ * } from "@evolu/common";
966
1072
  *
967
1073
  * const loadConfig: Task<string, ConfigInvalidError> = () =>
968
1074
  * ok("config");
@@ -970,7 +1076,7 @@ export interface Run<D = unknown> {
970
1076
  * interface ConfigInvalidError extends Typed<"ConfigInvalid"> {}
971
1077
  *
972
1078
  * await using run = createRun();
973
- * expect(await run.orThrow(loadConfig)).toBe("config");
1079
+ * assertEqual(await run.orThrow(loadConfig), "config");
974
1080
  * ```
975
1081
  */
976
1082
  readonly orThrow: {
@@ -992,7 +1098,13 @@ export interface Run<D = unknown> {
992
1098
  * ### Example
993
1099
  *
994
1100
  * ```ts
995
- * import { createRun, ok, type Task } from "@evolu/common";
1101
+ * import {
1102
+ * assertEqual,
1103
+ * assertTrue,
1104
+ * createRun,
1105
+ * ok,
1106
+ * type Task,
1107
+ * } from "@evolu/common";
996
1108
  *
997
1109
  * interface Resource extends AsyncDisposable {
998
1110
  * readonly value: string;
@@ -1002,17 +1114,18 @@ export interface Run<D = unknown> {
1002
1114
  * const openResource: Task<Resource> = () =>
1003
1115
  * ok({
1004
1116
  * value: "resource",
1005
- * [Symbol.asyncDispose]: async () => {
1117
+ * [Symbol.asyncDispose]: () => {
1006
1118
  * disposed = true;
1119
+ * return Promise.resolve();
1007
1120
  * },
1008
1121
  * });
1009
1122
  *
1010
1123
  * await using run = createRun();
1011
1124
  * {
1012
1125
  * await using resource = await run.ok(openResource);
1013
- * expect(resource.value).toBe("resource");
1126
+ * assertEqual(resource.value, "resource");
1014
1127
  * }
1015
- * expect(disposed).toBe(true);
1128
+ * assertTrue(disposed);
1016
1129
  * ```
1017
1130
  */
1018
1131
  readonly ok: {
@@ -1041,6 +1154,9 @@ export interface Run<D = unknown> {
1041
1154
  *
1042
1155
  * ```ts
1043
1156
  * import {
1157
+ * assertFalse,
1158
+ * assertType,
1159
+ * assertTrue,
1044
1160
  * AbortError,
1045
1161
  * createRun,
1046
1162
  * ok,
@@ -1061,13 +1177,11 @@ export interface Run<D = unknown> {
1061
1177
  *
1062
1178
  * await using run = createRun();
1063
1179
  * const fiber = run.abortable(loadUser, { db });
1064
- * expectTypeOf(fiber).toEqualTypeOf<
1065
- * AbortableFiber<string, never, DbDep>
1066
- * >();
1180
+ * assertType<AbortableFiber<string, never, DbDep>, typeof fiber>();
1067
1181
  * fiber.abort();
1068
1182
  * const userResult = await fiber;
1069
- * assert(!userResult.ok);
1070
- * expect(AbortError.is(userResult.error)).toBe(true);
1183
+ * assertFalse(userResult.ok);
1184
+ * assertTrue(AbortError.is(userResult.error));
1071
1185
  * ```
1072
1186
  */
1073
1187
  readonly abortable: {
@@ -1107,35 +1221,45 @@ export interface Run<D = unknown> {
1107
1221
  * ### Abort masks
1108
1222
  *
1109
1223
  * ```ts
1110
- * import { createRun, ok, unabortable, type Task } from "@evolu/common";
1224
+ * import {
1225
+ * assertEqual,
1226
+ * assertOk,
1227
+ * assertTrue,
1228
+ * createRun,
1229
+ * ok,
1230
+ * unabortable,
1231
+ * type Task,
1232
+ * } from "@evolu/common";
1111
1233
  *
1112
1234
  * const syncUsers: Task<string> = () => ok("synced");
1113
1235
  * const syncParent = unabortable(async (run) => {
1114
- * expect(run.snapshot().abortMask).toBe(1);
1236
+ * assertEqual(run.snapshot().abortMask, 1);
1115
1237
  *
1116
1238
  * // Plain daemon — the caller's mask does not follow it, so abort
1117
1239
  * // requests are observed.
1118
1240
  * const fiber = run.daemon(syncUsers);
1119
- * expect(fiber.run.snapshot().abortMask).toBe(0);
1241
+ * assertEqual(fiber.run.snapshot().abortMask, 0);
1120
1242
  *
1121
1243
  * // Explicitly masked daemon — finishes once started.
1122
1244
  * const maskedFiber = run.daemon(unabortable(syncUsers));
1123
- * expect(maskedFiber.run.snapshot().abortMask).toBe(1);
1245
+ * assertEqual(maskedFiber.run.snapshot().abortMask, 1);
1124
1246
  * const firstResult = await fiber;
1125
1247
  * const secondResult = await maskedFiber;
1126
- * assert(firstResult.ok);
1127
- * assert(secondResult.ok);
1248
+ * assertTrue(firstResult.ok);
1249
+ * assertTrue(secondResult.ok);
1128
1250
  * return ok([firstResult.value, secondResult.value] as const);
1129
1251
  * });
1130
1252
  *
1131
1253
  * await using run = createRun();
1132
- * expectOk(await run(syncParent), ["synced", "synced"]);
1254
+ * assertOk(await run(syncParent), ["synced", "synced"]);
1133
1255
  * ```
1134
1256
  *
1135
1257
  * ### Aborting a daemon
1136
1258
  *
1137
1259
  * ```ts
1138
1260
  * import {
1261
+ * assertFalse,
1262
+ * assertTrue,
1139
1263
  * AbortError,
1140
1264
  * createRun,
1141
1265
  * ok,
@@ -1157,14 +1281,21 @@ export interface Run<D = unknown> {
1157
1281
  * const fiber = run.daemon(syncUsers, { db });
1158
1282
  * fiber.abort();
1159
1283
  * const syncResult = await fiber;
1160
- * assert(!syncResult.ok);
1161
- * expect(AbortError.is(syncResult.error)).toBe(true);
1284
+ * assertFalse(syncResult.ok);
1285
+ * assertTrue(AbortError.is(syncResult.error));
1162
1286
  * ```
1163
1287
  *
1164
1288
  * ### Disposing a daemon
1165
1289
  *
1166
1290
  * ```ts
1167
- * import { createRun, ok, waitForAbort, type Task } from "@evolu/common";
1291
+ * import {
1292
+ * assertOk,
1293
+ * assertTrue,
1294
+ * createRun,
1295
+ * ok,
1296
+ * waitForAbort,
1297
+ * type Task,
1298
+ * } from "@evolu/common";
1168
1299
  *
1169
1300
  * let syncStopped = false;
1170
1301
  * const syncUsers: Task<never> = async (run) => {
@@ -1179,9 +1310,9 @@ export interface Run<D = unknown> {
1179
1310
  * {
1180
1311
  * // Async disposal requests abort and waits for the daemon to stop.
1181
1312
  * await using _syncFiber = run.daemon(syncUsers);
1182
- * expectOk(await run(loadUser), "Ada");
1313
+ * assertOk(await run(loadUser), "Ada");
1183
1314
  * }
1184
- * expect(syncStopped).toBe(true);
1315
+ * assertTrue(syncStopped);
1185
1316
  * ```
1186
1317
  */
1187
1318
  readonly daemon: {
@@ -1196,8 +1327,8 @@ export interface Run<D = unknown> {
1196
1327
  * Creates a {@link DisposableRun} attached to the root {@link Run} with this
1197
1328
  * Run's deps.
1198
1329
  *
1199
- * Use it when you need a Run that can be reused across multiple operations.
1200
- * For a single long-lived {@link Task}, use {@link Run.daemon}.
1330
+ * Use it to give multiple related Tasks a shared lifetime. For a single
1331
+ * long-lived {@link Task}, use {@link Run.daemon}.
1201
1332
  *
1202
1333
  * Use deps to replace the created Run's custom deps. Default deps
1203
1334
  * ({@link RunDefaultDeps}) are inherited unless explicitly replaced with
@@ -1211,7 +1342,13 @@ export interface Run<D = unknown> {
1211
1342
  * ### Example
1212
1343
  *
1213
1344
  * ```ts
1214
- * import { createRun, ok, type Task } from "@evolu/common";
1345
+ * import {
1346
+ * assertEqual,
1347
+ * assertOk,
1348
+ * createRun,
1349
+ * ok,
1350
+ * type Task,
1351
+ * } from "@evolu/common";
1215
1352
  *
1216
1353
  * interface DbDep {
1217
1354
  * readonly db: { readonly users: Array<string> };
@@ -1229,9 +1366,9 @@ export interface Run<D = unknown> {
1229
1366
  * await using createdRun = run.create({ db });
1230
1367
  * const userResult = await createdRun(loadUser);
1231
1368
  * const savedResult = await createdRun(saveUser);
1232
- * expectOk(userResult, "Ada");
1233
- * expectOk(savedResult, undefined);
1234
- * expect(db.users).toEqual(["Ada", "Grace"]);
1369
+ * assertOk(userResult, "Ada");
1370
+ * assertOk(savedResult, undefined);
1371
+ * assertEqual(db.users, ["Ada", "Grace"]);
1235
1372
  * ```
1236
1373
  */
1237
1374
  readonly create: {
@@ -1287,20 +1424,27 @@ export interface Run<D = unknown> {
1287
1424
  * ### Example
1288
1425
  *
1289
1426
  * ```ts
1290
- * import { AbortError, createRun, ok, sleep } from "@evolu/common";
1427
+ * import {
1428
+ * assertFalse,
1429
+ * assertTrue,
1430
+ * AbortError,
1431
+ * createRun,
1432
+ * ok,
1433
+ * sleep,
1434
+ * } from "@evolu/common";
1291
1435
  *
1292
1436
  * let socketClosed = false;
1293
1437
  * const openSocket = () => ({
1294
1438
  * close: () => {
1295
1439
  * socketClosed = true;
1296
1440
  * },
1297
- * read: async () => "message",
1441
+ * read: () => Promise.resolve("message"),
1298
1442
  * });
1299
1443
  *
1300
1444
  * await using run = createRun();
1301
1445
  * const fiber = run.abortable(async (run) => {
1302
1446
  * const socket = openSocket();
1303
- * using closeOnAbort = run.onAbort(() => {
1447
+ * using _closeOnAbort = run.onAbort(() => {
1304
1448
  * socket.close();
1305
1449
  * });
1306
1450
  *
@@ -1310,9 +1454,9 @@ export interface Run<D = unknown> {
1310
1454
  * });
1311
1455
  * fiber.abort();
1312
1456
  * const result = await fiber;
1313
- * assert(!result.ok);
1314
- * expect(AbortError.is(result.error)).toBe(true);
1315
- * expect(socketClosed).toBe(true);
1457
+ * assertFalse(result.ok);
1458
+ * assertTrue(AbortError.is(result.error));
1459
+ * assertTrue(socketClosed);
1316
1460
  * ```
1317
1461
  */
1318
1462
  readonly onAbort: (
@@ -1389,7 +1533,7 @@ export interface DisposableRun<D = unknown>
1389
1533
  * ### Example
1390
1534
  *
1391
1535
  * ```ts
1392
- * import { createRun } from "@evolu/common";
1536
+ * import { assertFalse, assertTrue, createRun } from "@evolu/common";
1393
1537
  *
1394
1538
  * let connectionClosed = false;
1395
1539
  * {
@@ -1398,9 +1542,9 @@ export interface DisposableRun<D = unknown>
1398
1542
  * connectionClosed = true;
1399
1543
  * });
1400
1544
  *
1401
- * expect(connectionClosed).toBe(false);
1545
+ * assertFalse(connectionClosed);
1402
1546
  * }
1403
- * expect(connectionClosed).toBe(true);
1547
+ * assertTrue(connectionClosed);
1404
1548
  * ```
1405
1549
  */
1406
1550
  readonly defer: (finalizer: () => Awaitable<void>) => void;
@@ -1467,7 +1611,15 @@ export interface DisposableRun<D = unknown>
1467
1611
  * ### Example
1468
1612
  *
1469
1613
  * ```ts
1470
- * import { createRun, ok, type Fiber, type Task } from "@evolu/common";
1614
+ * import {
1615
+ * assertEqual,
1616
+ * assertOk,
1617
+ * assertType,
1618
+ * createRun,
1619
+ * ok,
1620
+ * type Fiber,
1621
+ * type Task,
1622
+ * } from "@evolu/common";
1471
1623
  *
1472
1624
  * await using run = createRun();
1473
1625
  *
@@ -1477,9 +1629,9 @@ export interface DisposableRun<D = unknown>
1477
1629
  * const userResult = await fiber;
1478
1630
  * const snapshot = fiber.run.snapshot();
1479
1631
  *
1480
- * expectTypeOf(fiber).toEqualTypeOf<Fiber<string, never>>();
1481
- * expectOk(userResult, "Ada");
1482
- * expect(snapshot.id).toBe(fiber.run.id);
1632
+ * assertType<Fiber<string, never>, typeof fiber>();
1633
+ * assertOk(userResult, "Ada");
1634
+ * assertEqual(snapshot.id, fiber.run.id);
1483
1635
  * ```
1484
1636
  *
1485
1637
  * @group Core
@@ -1539,6 +1691,9 @@ export type InferFiberDeps<TFiber extends AnyFiber> =
1539
1691
  *
1540
1692
  * ```ts
1541
1693
  * import {
1694
+ * assertFalse,
1695
+ * assertType,
1696
+ * assertTrue,
1542
1697
  * AbortError,
1543
1698
  * createRun,
1544
1699
  * ok,
@@ -1555,11 +1710,11 @@ export type InferFiberDeps<TFiber extends AnyFiber> =
1555
1710
  * };
1556
1711
  *
1557
1712
  * const fiber = run.abortable<string, never>(fetchData);
1558
- * expectTypeOf(fiber).toEqualTypeOf<AbortableFiber<string, never>>();
1713
+ * assertType<AbortableFiber<string, never>, typeof fiber>();
1559
1714
  * fiber.abort();
1560
1715
  * const result = await fiber;
1561
- * assert(!result.ok);
1562
- * expect(AbortError.is(result.error)).toBe(true);
1716
+ * assertFalse(result.ok);
1717
+ * assertTrue(AbortError.is(result.error));
1563
1718
  * ```
1564
1719
  *
1565
1720
  * @group Core
@@ -2007,7 +2162,7 @@ export interface CreateRun {
2007
2162
  * ### Example
2008
2163
  *
2009
2164
  * ```ts
2010
- * import { createRun, ok, type Task } from "@evolu/common";
2165
+ * import { assertOk, createRun, ok, type Task } from "@evolu/common";
2011
2166
  *
2012
2167
  * interface ConfigDep {
2013
2168
  * readonly config: { readonly apiUrl: string };
@@ -2019,7 +2174,7 @@ export interface CreateRun {
2019
2174
  * await using run = createRun({
2020
2175
  * config: { apiUrl: "https://api.example.com" },
2021
2176
  * });
2022
- * expectOk(await run(loadApiUrl), "https://api.example.com");
2177
+ * assertOk(await run(loadApiUrl), "https://api.example.com");
2023
2178
  * ```
2024
2179
  *
2025
2180
  * @group Run
@@ -2196,12 +2351,12 @@ export const testCreateDeps = (options?: {
2196
2351
  * ### Example
2197
2352
  *
2198
2353
  * ```ts
2199
- * import { ok, testCreateRun, type Task } from "@evolu/common";
2354
+ * import { assertOk, ok, testCreateRun, type Task } from "@evolu/common";
2200
2355
  *
2201
2356
  * const readTime: Task<number> = (run) => ok(run.deps.time.now());
2202
2357
  *
2203
2358
  * await using run = testCreateRun();
2204
- * expectOk(await run(readTime), 0);
2359
+ * assertOk(await run(readTime), 0);
2205
2360
  * ```
2206
2361
  *
2207
2362
  * @group Testing
@@ -2216,7 +2371,7 @@ export function testCreateRun(
2216
2371
  * ### Example
2217
2372
  *
2218
2373
  * ```ts
2219
- * import { ok, testCreateRun, type Task } from "@evolu/common";
2374
+ * import { assertOk, ok, testCreateRun, type Task } from "@evolu/common";
2220
2375
  *
2221
2376
  * interface FeatureDep {
2222
2377
  * readonly feature: { readonly enabled: boolean };
@@ -2226,7 +2381,7 @@ export function testCreateRun(
2226
2381
  * ok(run.deps.feature.enabled);
2227
2382
  *
2228
2383
  * await using run = testCreateRun({ feature: { enabled: true } });
2229
- * expectOk(await run(isFeatureEnabled), true);
2384
+ * assertOk(await run(isFeatureEnabled), true);
2230
2385
  * ```
2231
2386
  */
2232
2387
  export function testCreateRun<D extends object>(
@@ -2520,7 +2675,6 @@ const createRunInternal = <D extends object>(
2520
2675
  result = await scheduler.postTask(
2521
2676
  () => {
2522
2677
  startSignal.throwIfAborted();
2523
- // eslint-disable-next-line evolu/no-direct-task-call -- The executor invokes the Task with its child Run.
2524
2678
  return task(taskRun);
2525
2679
  },
2526
2680
  {
@@ -2530,7 +2684,6 @@ const createRunInternal = <D extends object>(
2530
2684
  );
2531
2685
  } else {
2532
2686
  startSignal.throwIfAborted();
2533
- // eslint-disable-next-line evolu/no-direct-task-call -- The executor invokes the Task with its child Run.
2534
2687
  result = await task(taskRun);
2535
2688
  }
2536
2689
 
@@ -2595,7 +2748,7 @@ const createRunInternal = <D extends object>(
2595
2748
  getOrThrow(await run(task, taskDeps))) as Run<D>["orThrow"];
2596
2749
 
2597
2750
  run.ok = (async (task: TaskInternal, taskDeps?: object) =>
2598
- getOk((await run(task, taskDeps)) as Result<any, never>)) as Run<D>["ok"];
2751
+ getOk((await run(task, taskDeps)) as Result<any>)) as Run<D>["ok"];
2599
2752
  /* eslint-enable @typescript-eslint/no-unsafe-return */
2600
2753
 
2601
2754
  run.abortable = ((task: TaskInternal, deps?: object) =>
@@ -2724,15 +2877,12 @@ const withTaskMeta =
2724
2877
  taskInternal[taskMetaSymbol]?.abortBehavior === undefined,
2725
2878
  "abort behavior helpers cannot wrap the same Task",
2726
2879
  );
2727
- // eslint-disable-next-line evolu/no-direct-task-call -- Preserve the wrapped Task's child Run.
2728
2880
  const wrapped: TaskInternal<T, E, D> = (run) => task(run);
2729
2881
  const taskMeta = taskInternal[taskMetaSymbol];
2730
2882
  wrapped[taskMetaSymbol] = taskMeta ? { ...taskMeta, ...meta } : meta;
2731
2883
  return wrapped;
2732
2884
  };
2733
2885
 
2734
- // Task helpers
2735
-
2736
2886
  /**
2737
2887
  * A readonly record whose values are {@link Task}s.
2738
2888
  *
@@ -2768,7 +2918,7 @@ export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<
2768
2918
  * Options shared by {@link Task} collection helpers.
2769
2919
  *
2770
2920
  * `concurrency` controls how many Tasks run at once. It defaults to `1`. For
2771
- * CPU-bound Tasks backed by workers or parallel native operations, a platform
2921
+ * CPU-bound Tasks backed by workers or native parallelism, a platform
2772
2922
  * `availableParallelism()` result is often a good limit. For network or
2773
2923
  * database Tasks, choose a limit based on the transport, server, connection
2774
2924
  * pool, and rate limits.
@@ -2834,6 +2984,9 @@ export type InferTasksOk<TTasks> = {
2834
2984
  *
2835
2985
  * ```ts
2836
2986
  * import {
2987
+ * assertErr,
2988
+ * assertEqual,
2989
+ * assertType,
2837
2990
  * all,
2838
2991
  * createRun,
2839
2992
  * err,
@@ -2864,14 +3017,12 @@ export type InferTasksOk<TTasks> = {
2864
3017
  * collect: false,
2865
3018
  * }),
2866
3019
  * );
2867
- * expectTypeOf(saveResult).toEqualTypeOf<
2868
- * Result<void, SaveUserFailedError>
2869
- * >();
2870
- * expectErr(saveResult, {
3020
+ * assertType<Result<void, SaveUserFailedError>, typeof saveResult>();
3021
+ * assertErr(saveResult, {
2871
3022
  * type: "SaveUserFailed",
2872
3023
  * userId: "missing",
2873
3024
  * });
2874
- * expect(savedUserIds).toEqual(["user-1"]);
3025
+ * assertEqual(savedUserIds, ["user-1"]);
2875
3026
  * ```
2876
3027
  *
2877
3028
  * @group Collection
@@ -2894,6 +3045,8 @@ export function all<const TTasks extends TaskRecord>(
2894
3045
  *
2895
3046
  * ```ts
2896
3047
  * import {
3048
+ * assertOk,
3049
+ * assertType,
2897
3050
  * all,
2898
3051
  * createRun,
2899
3052
  * ok,
@@ -2915,10 +3068,11 @@ export function all<const TTasks extends TaskRecord>(
2915
3068
  *
2916
3069
  * await using run = createRun();
2917
3070
  * const dashboard = await run(all([fetchUser, fetchPosts]));
2918
- * expectTypeOf(dashboard).toEqualTypeOf<
2919
- * Result<readonly [User, ReadonlyArray<Post>]>
3071
+ * assertType<
3072
+ * Result<readonly [User, ReadonlyArray<Post>]>,
3073
+ * typeof dashboard
2920
3074
  * >();
2921
- * expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
3075
+ * assertOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
2922
3076
  * ```
2923
3077
  */
2924
3078
  export function all<const TTasks extends ReadonlyArray<AnyTask>>(
@@ -2937,6 +3091,8 @@ export function all<const TTasks extends ReadonlyArray<AnyTask>>(
2937
3091
  *
2938
3092
  * ```ts
2939
3093
  * import {
3094
+ * assertOk,
3095
+ * assertType,
2940
3096
  * all,
2941
3097
  * createRun,
2942
3098
  * ok,
@@ -2959,10 +3115,11 @@ export function all<const TTasks extends ReadonlyArray<AnyTask>>(
2959
3115
  * await using run = createRun();
2960
3116
  * const result = await run(all({ user: fetchUser, posts: fetchPosts }));
2961
3117
  *
2962
- * expectTypeOf(result).toEqualTypeOf<
2963
- * Result<{ readonly user: User; readonly posts: ReadonlyArray<Post> }>
3118
+ * assertType<
3119
+ * Result<{ readonly user: User; readonly posts: ReadonlyArray<Post> }>,
3120
+ * typeof result
2964
3121
  * >();
2965
- * expectOk(result, {
3122
+ * assertOk(result, {
2966
3123
  * user: { id: "user-1" },
2967
3124
  * posts: [{ id: "post-1" }],
2968
3125
  * });
@@ -2993,7 +3150,7 @@ export function all<
2993
3150
  TTask extends AnyTask,
2994
3151
  >(
2995
3152
  values: TValues,
2996
- // eslint-disable-next-line @typescript-eslint/unified-signatures -- Separate array and record overloads keep callback parameter inference precise.
3153
+ // Separate array and record overloads keep callback parameter inference precise.
2997
3154
  fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
2998
3155
  options: AllOptions,
2999
3156
  ): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
@@ -3006,6 +3163,9 @@ export function all<
3006
3163
  *
3007
3164
  * ```ts
3008
3165
  * import {
3166
+ * assertEqual,
3167
+ * assertOk,
3168
+ * assertType,
3009
3169
  * all,
3010
3170
  * createRun,
3011
3171
  * ok,
@@ -3030,12 +3190,12 @@ export function all<
3030
3190
  * });
3031
3191
  *
3032
3192
  * // Mapping is eager: it happens before the returned Task starts.
3033
- * expect(indexes).toEqual([0, 1]);
3193
+ * assertEqual(indexes, [0, 1]);
3034
3194
  *
3035
3195
  * await using run = createRun();
3036
3196
  * const result = await run(loadUsers);
3037
- * expectTypeOf(result).toEqualTypeOf<Result<readonly [User, User]>>();
3038
- * expectOk(result, [{ id: "user-1" }, { id: "user-2" }]);
3197
+ * assertType<Result<readonly [User, User]>, typeof result>();
3198
+ * assertOk(result, [{ id: "user-1" }, { id: "user-2" }]);
3039
3199
  * ```
3040
3200
  */
3041
3201
  export function all<
@@ -3058,6 +3218,9 @@ export function all<
3058
3218
  *
3059
3219
  * ```ts
3060
3220
  * import {
3221
+ * assertEqual,
3222
+ * assertOk,
3223
+ * assertType,
3061
3224
  * all,
3062
3225
  * createRun,
3063
3226
  * ok,
@@ -3085,14 +3248,15 @@ export function all<
3085
3248
  * });
3086
3249
  *
3087
3250
  * // Mapping is eager: it happens before the returned Task starts.
3088
- * expect(roles).toEqual(["admin", "reviewer"]);
3251
+ * assertEqual(roles, ["admin", "reviewer"]);
3089
3252
  *
3090
3253
  * await using run = createRun();
3091
3254
  * const result = await run(loadUsersByRole);
3092
- * expectTypeOf(result).toEqualTypeOf<
3093
- * Result<{ readonly admin: User; readonly reviewer: User }>
3255
+ * assertType<
3256
+ * Result<{ readonly admin: User; readonly reviewer: User }>,
3257
+ * typeof result
3094
3258
  * >();
3095
- * expectOk(result, {
3259
+ * assertOk(result, {
3096
3260
  * admin: { id: "user-1" },
3097
3261
  * reviewer: { id: "user-2" },
3098
3262
  * });
@@ -3103,7 +3267,7 @@ export function all<
3103
3267
  TTask extends AnyTask,
3104
3268
  >(
3105
3269
  values: TValues,
3106
- // eslint-disable-next-line @typescript-eslint/unified-signatures -- Separate array and record overloads keep callback parameter inference precise.
3270
+ // Separate array and record overloads keep callback parameter inference precise.
3107
3271
  fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
3108
3272
  options?: TaskCollectionOptions,
3109
3273
  ): Task<
@@ -3172,6 +3336,9 @@ export type InferTasksSettled<TTasks> = {
3172
3336
  *
3173
3337
  * ```ts
3174
3338
  * import {
3339
+ * assertOk,
3340
+ * assertType,
3341
+ * assertTrue,
3175
3342
  * allSettled,
3176
3343
  * createRun,
3177
3344
  * err,
@@ -3194,20 +3361,21 @@ export type InferTasksSettled<TTasks> = {
3194
3361
  *
3195
3362
  * await using run = createRun();
3196
3363
  * const results = await run(allSettled([loadProfile, loadActivity]));
3197
- * expectTypeOf(results).toEqualTypeOf<
3364
+ * assertType<
3198
3365
  * Result<
3199
3366
  * readonly [
3200
3367
  * Result<string, ProfileNotFoundError>,
3201
3368
  * Result<ReadonlyArray<string>>,
3202
3369
  * ]
3203
- * >
3370
+ * >,
3371
+ * typeof results
3204
3372
  * >();
3205
- * expectOk(results, [
3373
+ * assertOk(results, [
3206
3374
  * { ok: false, error: { type: "ProfileNotFound" } },
3207
3375
  * { ok: true, value: ["signed-in"] },
3208
3376
  * ]);
3209
3377
  * // Unlike all, a later Task still runs after an Err.
3210
- * expect(activityLoaded).toBe(true);
3378
+ * assertTrue(activityLoaded);
3211
3379
  * ```
3212
3380
  *
3213
3381
  * @group Collection
@@ -3224,6 +3392,8 @@ export function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
3224
3392
  *
3225
3393
  * ```ts
3226
3394
  * import {
3395
+ * assertOk,
3396
+ * assertType,
3227
3397
  * allSettled,
3228
3398
  * createRun,
3229
3399
  * err,
@@ -3248,13 +3418,14 @@ export function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
3248
3418
  * allSettled({ user: fetchUser, profile: fetchProfile }),
3249
3419
  * );
3250
3420
  *
3251
- * expectTypeOf(results).toEqualTypeOf<
3421
+ * assertType<
3252
3422
  * Result<{
3253
3423
  * readonly user: Result<User>;
3254
3424
  * readonly profile: Result<string, ProfileNotFoundError>;
3255
- * }>
3425
+ * }>,
3426
+ * typeof results
3256
3427
  * >();
3257
- * expectOk(results, {
3428
+ * assertOk(results, {
3258
3429
  * user: { ok: true, value: { id: "user-1" } },
3259
3430
  * profile: { ok: false, error: { type: "ProfileNotFound" } },
3260
3431
  * });
@@ -3272,6 +3443,9 @@ export function allSettled<const TTasks extends TaskRecord>(
3272
3443
  *
3273
3444
  * ```ts
3274
3445
  * import {
3446
+ * assertEqual,
3447
+ * assertOk,
3448
+ * assertType,
3275
3449
  * allSettled,
3276
3450
  * createRun,
3277
3451
  * err,
@@ -3300,19 +3474,20 @@ export function allSettled<const TTasks extends TaskRecord>(
3300
3474
  * });
3301
3475
  *
3302
3476
  * // Mapping is eager: it happens before the returned Task starts.
3303
- * expect(indexes).toEqual([0, 1]);
3477
+ * assertEqual(indexes, [0, 1]);
3304
3478
  *
3305
3479
  * await using run = createRun();
3306
3480
  * const results = await run(loadUsers);
3307
- * expectTypeOf(results).toEqualTypeOf<
3481
+ * assertType<
3308
3482
  * Result<
3309
3483
  * readonly [
3310
3484
  * Result<User, UserNotFoundError>,
3311
3485
  * Result<User, UserNotFoundError>,
3312
3486
  * ]
3313
- * >
3487
+ * >,
3488
+ * typeof results
3314
3489
  * >();
3315
- * expectOk(results, [
3490
+ * assertOk(results, [
3316
3491
  * { ok: true, value: { id: "user-1" } },
3317
3492
  * { ok: false, error: { type: "UserNotFound" } },
3318
3493
  * ]);
@@ -3343,6 +3518,9 @@ export function allSettled<
3343
3518
  *
3344
3519
  * ```ts
3345
3520
  * import {
3521
+ * assertEqual,
3522
+ * assertOk,
3523
+ * assertType,
3346
3524
  * allSettled,
3347
3525
  * createRun,
3348
3526
  * err,
@@ -3371,17 +3549,18 @@ export function allSettled<
3371
3549
  * });
3372
3550
  *
3373
3551
  * // Mapping is eager: it happens before the returned Task starts.
3374
- * expect(roles).toEqual(["admin", "reviewer"]);
3552
+ * assertEqual(roles, ["admin", "reviewer"]);
3375
3553
  *
3376
3554
  * await using run = createRun();
3377
3555
  * const results = await run(loadUsersByRole);
3378
- * expectTypeOf(results).toEqualTypeOf<
3556
+ * assertType<
3379
3557
  * Result<{
3380
3558
  * readonly admin: Result<User, UserNotFoundError>;
3381
3559
  * readonly reviewer: Result<User, UserNotFoundError>;
3382
- * }>
3560
+ * }>,
3561
+ * typeof results
3383
3562
  * >();
3384
- * expectOk(results, {
3563
+ * assertOk(results, {
3385
3564
  * admin: { ok: true, value: { id: "user-1" } },
3386
3565
  * reviewer: { ok: false, error: { type: "UserNotFound" } },
3387
3566
  * });
@@ -3392,7 +3571,7 @@ export function allSettled<
3392
3571
  TTask extends AnyTask,
3393
3572
  >(
3394
3573
  values: TValues,
3395
- // eslint-disable-next-line @typescript-eslint/unified-signatures -- Separate array and record overloads keep callback parameter inference precise.
3574
+ // Separate array and record overloads keep callback parameter inference precise.
3396
3575
  fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask,
3397
3576
  options?: TaskCollectionOptions,
3398
3577
  ): Task<
@@ -3499,7 +3678,7 @@ const mapInput = (
3499
3678
  * This helper is a callback bridge. If `reject` forwards an Error created in a
3500
3679
  * separate async chain, V8 cannot reconstruct the caller's zero-cost async
3501
3680
  * stack through this bridge. Prefer native promise APIs and `await` when the
3502
- * wrapped operation already has a promise-shaped API.
3681
+ * wrapped API already returns a Promise.
3503
3682
  *
3504
3683
  * One-shot settlement applies only to `resolve` and `reject`. A synchronous
3505
3684
  * throw from the setup function is a defect that panics the Run tree even after
@@ -3524,14 +3703,23 @@ const mapInput = (
3524
3703
  * ### Example
3525
3704
  *
3526
3705
  * ```ts
3527
- * import { callback, createRun, ok, type Task } from "@evolu/common";
3706
+ * import {
3707
+ * assertEqual,
3708
+ * assertOk,
3709
+ * callback,
3710
+ * createRun,
3711
+ * ok,
3712
+ * type Task,
3713
+ * } from "@evolu/common";
3528
3714
  *
3529
3715
  * const listeners = new Set<(message: string) => void>();
3530
3716
  * const subscribe = (
3531
3717
  * listener: (message: string) => void,
3532
3718
  * ): (() => void) => {
3533
3719
  * listeners.add(listener);
3534
- * return () => listeners.delete(listener);
3720
+ * return () => {
3721
+ * listeners.delete(listener);
3722
+ * };
3535
3723
  * };
3536
3724
  * const nextMessage: Task<string> = callback(({ resolve }) =>
3537
3725
  * subscribe((message) => resolve(ok(message))),
@@ -3539,11 +3727,11 @@ const mapInput = (
3539
3727
  *
3540
3728
  * await using run = createRun();
3541
3729
  * const fiber = run(nextMessage);
3542
- * expect(listeners.size).toBe(1);
3730
+ * assertEqual(listeners.size, 1);
3543
3731
  * for (const listener of listeners) listener("ready");
3544
- * expectOk(await fiber, "ready");
3732
+ * assertOk(await fiber, "ready");
3545
3733
  * // The callback cleanup unsubscribes after settlement.
3546
- * expect(listeners.size).toBe(0);
3734
+ * assertEqual(listeners.size, 0);
3547
3735
  * ```
3548
3736
  *
3549
3737
  * @group Interop
@@ -3578,10 +3766,10 @@ export const callback =
3578
3766
  * ### Example
3579
3767
  *
3580
3768
  * ```ts
3581
- * import { createRun, sleep } from "@evolu/common";
3769
+ * import { assertOk, createRun, sleep } from "@evolu/common";
3582
3770
  *
3583
3771
  * await using run = createRun();
3584
- * expectOk(await run(sleep("1ms")), undefined);
3772
+ * assertOk(await run(sleep("1ms")), undefined);
3585
3773
  * ```
3586
3774
  *
3587
3775
  * @group Timing
@@ -3605,6 +3793,8 @@ export const sleep = (duration: PositiveDuration): Task<void> =>
3605
3793
  *
3606
3794
  * ```ts
3607
3795
  * import {
3796
+ * assertErr,
3797
+ * assertType,
3608
3798
  * createRun,
3609
3799
  * timeout,
3610
3800
  * timeoutError,
@@ -3616,8 +3806,8 @@ export const sleep = (duration: PositiveDuration): Task<void> =>
3616
3806
  * await using run = createRun();
3617
3807
  *
3618
3808
  * const result = await run(timeout(waitForAbort, "1ms"));
3619
- * expectTypeOf(result).toEqualTypeOf<Result<never, TimeoutError>>();
3620
- * expectErr(result, timeoutError);
3809
+ * assertType<Result<never, TimeoutError>, typeof result>();
3810
+ * assertErr(result, timeoutError);
3621
3811
  * ```
3622
3812
  *
3623
3813
  * @group Timing
@@ -3716,6 +3906,8 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
3716
3906
  *
3717
3907
  * ```ts
3718
3908
  * import {
3909
+ * assertErr,
3910
+ * assertType,
3719
3911
  * createRun,
3720
3912
  * err,
3721
3913
  * recurs,
@@ -3735,10 +3927,11 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
3735
3927
  *
3736
3928
  * await using run = createRun();
3737
3929
  * const result = await run(fetchWithRetry);
3738
- * expectTypeOf(result).toEqualTypeOf<
3739
- * Result<string, RetryTaskError<ServiceUnavailableError>>
3930
+ * assertType<
3931
+ * Result<string, RetryTaskError<ServiceUnavailableError>>,
3932
+ * typeof result
3740
3933
  * >();
3741
- * expectErr(result, {
3934
+ * assertErr(result, {
3742
3935
  * type: "RetryError",
3743
3936
  * attempts: 3,
3744
3937
  * lastError: { type: "ServiceUnavailable" },
@@ -3749,6 +3942,7 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
3749
3942
  *
3750
3943
  * ```ts
3751
3944
  * import {
3945
+ * assertErr,
3752
3946
  * createRun,
3753
3947
  * err,
3754
3948
  * recurs,
@@ -3771,7 +3965,8 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
3771
3965
  * });
3772
3966
  *
3773
3967
  * await using run = createRun();
3774
- * expectErr(await run(fetchWithRetry), {
3968
+ * const result = await run(fetchWithRetry);
3969
+ * assertErr(result, {
3775
3970
  * type: "RetryError",
3776
3971
  * attempts: 1,
3777
3972
  * lastError: { type: "PermanentFailure" },
@@ -3903,7 +4098,15 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
3903
4098
  * ### Repeating successes
3904
4099
  *
3905
4100
  * ```ts
3906
- * import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
4101
+ * import {
4102
+ * assertEqual,
4103
+ * assertOk,
4104
+ * createRun,
4105
+ * ok,
4106
+ * recurs,
4107
+ * repeat,
4108
+ * type Task,
4109
+ * } from "@evolu/common";
3907
4110
  *
3908
4111
  * let attempts = 0;
3909
4112
  * const checkStatus: Task<string> = () => {
@@ -3914,14 +4117,16 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
3914
4117
  * const poll = repeat(checkStatus, recurs(3));
3915
4118
  *
3916
4119
  * await using run = createRun();
3917
- * expectOk(await run(poll), "pending");
3918
- * expect(attempts).toBe(4);
4120
+ * assertOk(await run(poll), "pending");
4121
+ * assertEqual(attempts, 4);
3919
4122
  * ```
3920
4123
  *
3921
4124
  * ### Stopping with Done
3922
4125
  *
3923
4126
  * ```ts
3924
4127
  * import {
4128
+ * assertErr,
4129
+ * assertEqual,
3925
4130
  * createRun,
3926
4131
  * done,
3927
4132
  * err,
@@ -3944,8 +4149,8 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
3944
4149
  *
3945
4150
  * await using run = createRun();
3946
4151
  * const result = await run(repeat(processQueue, spaced("1ms")));
3947
- * expectErr(result, done());
3948
- * expect(queue).toEqual([]);
4152
+ * assertErr(result, done());
4153
+ * assertEqual(queue, []);
3949
4154
  * ```
3950
4155
  *
3951
4156
  * @group Repetition
@@ -4007,6 +4212,9 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> =
4007
4212
  *
4008
4213
  * ```ts
4009
4214
  * import {
4215
+ * assertOk,
4216
+ * assertType,
4217
+ * assertTrue,
4010
4218
  * any,
4011
4219
  * createRun,
4012
4220
  * err,
@@ -4030,11 +4238,9 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> =
4030
4238
  * await using run = createRun();
4031
4239
  * const result = await run(any([unavailable, fallback]));
4032
4240
  *
4033
- * expectTypeOf(result).toEqualTypeOf<
4034
- * Result<string, ServiceUnavailableError>
4035
- * >();
4036
- * expectOk(result, "fallback");
4037
- * expect(fallbackStarted).toBe(true);
4241
+ * assertType<Result<string, ServiceUnavailableError>, typeof result>();
4242
+ * assertOk(result, "fallback");
4243
+ * assertTrue(fallbackStarted);
4038
4244
  * ```
4039
4245
  *
4040
4246
  * @group Racing
@@ -4105,6 +4311,7 @@ export const any =
4105
4311
  *
4106
4312
  * ```ts
4107
4313
  * import {
4314
+ * assertOk,
4108
4315
  * createRun,
4109
4316
  * isNonEmptyArray,
4110
4317
  * ok,
@@ -4116,7 +4323,7 @@ export const any =
4116
4323
  * await using run = createRun();
4117
4324
  * if (isNonEmptyArray(tasks)) {
4118
4325
  * const result = await run(race(tasks));
4119
- * expectOk(result, "first");
4326
+ * assertOk(result, "first");
4120
4327
  * }
4121
4328
  * ```
4122
4329
  *
@@ -4124,6 +4331,9 @@ export const any =
4124
4331
  *
4125
4332
  * ```ts
4126
4333
  * import {
4334
+ * assertFalse,
4335
+ * assertOk,
4336
+ * assertType,
4127
4337
  * createRun,
4128
4338
  * ok,
4129
4339
  * race,
@@ -4145,9 +4355,9 @@ export const any =
4145
4355
  * // Input order does not matter: the first settled Result wins, and the
4146
4356
  * // still-running loser is aborted.
4147
4357
  * const result = await run(race([slow, fast]));
4148
- * expectTypeOf(result).toEqualTypeOf<Result<string>>();
4149
- * expectOk(result, "fast");
4150
- * expect(slowCompleted).toBe(false);
4358
+ * assertType<Result<string>, typeof result>();
4359
+ * assertOk(result, "fast");
4360
+ * assertFalse(slowCompleted);
4151
4361
  * ```
4152
4362
  *
4153
4363
  * @group Racing
@@ -4196,6 +4406,8 @@ export const race =
4196
4406
  *
4197
4407
  * ```ts
4198
4408
  * import {
4409
+ * assertFalse,
4410
+ * assertOk,
4199
4411
  * createRun,
4200
4412
  * err,
4201
4413
  * firstN,
@@ -4226,8 +4438,8 @@ export const race =
4226
4438
  *
4227
4439
  * // Errs do not count. After two Ok values settle, the slow Task is aborted.
4228
4440
  * const result = await run(firstN(tasks, 2, { concurrency: 4 }));
4229
- * expectOk(result, ["fast-1", "fast-2"]);
4230
- * expect(slowCompleted).toBe(false);
4441
+ * assertOk(result, ["fast-1", "fast-2"]);
4442
+ * assertFalse(slowCompleted);
4231
4443
  * ```
4232
4444
  *
4233
4445
  * @group Racing
@@ -4275,6 +4487,8 @@ export const firstN =
4275
4487
  *
4276
4488
  * ```ts
4277
4489
  * import {
4490
+ * assertFalse,
4491
+ * assertOk,
4278
4492
  * createRun,
4279
4493
  * err,
4280
4494
  * firstNSettled,
@@ -4300,11 +4514,11 @@ export const firstN =
4300
4514
  *
4301
4515
  * // Err and Ok both count, and Results use settlement order.
4302
4516
  * const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
4303
- * expectOk(result, [
4517
+ * assertOk(result, [
4304
4518
  * { ok: false, error: { type: "ServiceUnavailable" } },
4305
4519
  * { ok: true, value: "fast" },
4306
4520
  * ]);
4307
- * expect(slowCompleted).toBe(false);
4521
+ * assertFalse(slowCompleted);
4308
4522
  * ```
4309
4523
  *
4310
4524
  * @group Racing
@@ -4391,6 +4605,9 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
4391
4605
  *
4392
4606
  * ```ts
4393
4607
  * import {
4608
+ * assertEqual,
4609
+ * assertFalse,
4610
+ * assertOk,
4394
4611
  * createRun,
4395
4612
  * each,
4396
4613
  * err,
@@ -4426,9 +4643,9 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (
4426
4643
  * ),
4427
4644
  * );
4428
4645
  *
4429
- * expectOk(result, undefined);
4430
- * expect(first).toEqual(["fast", 2]);
4431
- * expect(slowCompleted).toBe(false);
4646
+ * assertOk(result, undefined);
4647
+ * assertEqual(first, ["fast", 2]);
4648
+ * assertFalse(slowCompleted);
4432
4649
  * ```
4433
4650
  *
4434
4651
  * `onResult` is a synchronous scheduling decision, not a place to do work. It
@@ -4484,7 +4701,7 @@ export const each =
4484
4701
  nextIndex += 1;
4485
4702
 
4486
4703
  const result = await run(tasks[index]);
4487
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- stopped can flip across the await via sibling workers
4704
+ // stopped can flip across the await via sibling workers.
4488
4705
  if (stopped) break;
4489
4706
  run.signal.throwIfAborted();
4490
4707
  const decision = onResult(result, index);
@@ -4502,6 +4719,7 @@ export const each =
4502
4719
 
4503
4720
  active -= 1;
4504
4721
  if (active === 0) wake.resolve();
4722
+ // oxlint-disable-next-line typescript/return-await -- StackTrace.test.ts measures this direct await edge in the worker topology.
4505
4723
  return await parked;
4506
4724
  };
4507
4725
 
@@ -4537,13 +4755,19 @@ export type TaskPriority = "user-blocking" | "user-visible" | "background";
4537
4755
  * ### Example
4538
4756
  *
4539
4757
  * ```ts
4540
- * import { createRun, ok, prioritized, type Task } from "@evolu/common";
4758
+ * import {
4759
+ * assertOk,
4760
+ * createRun,
4761
+ * ok,
4762
+ * prioritized,
4763
+ * type Task,
4764
+ * } from "@evolu/common";
4541
4765
  *
4542
4766
  * const rebuildSearchIndex: Task<string> = () => ok("indexed");
4543
4767
  * const backgroundIndexing = prioritized("background", rebuildSearchIndex);
4544
4768
  *
4545
4769
  * await using run = createRun();
4546
- * expectOk(await run(backgroundIndexing), "indexed");
4770
+ * assertOk(await run(backgroundIndexing), "indexed");
4547
4771
  * ```
4548
4772
  *
4549
4773
  * @group Scheduling
@@ -4567,7 +4791,13 @@ export const prioritized = <T, E, D = unknown>(
4567
4791
  * ### Example
4568
4792
  *
4569
4793
  * ```ts
4570
- * import { createRun, ok, yieldNow, type Task } from "@evolu/common";
4794
+ * import {
4795
+ * assertOk,
4796
+ * createRun,
4797
+ * ok,
4798
+ * yieldNow,
4799
+ * type Task,
4800
+ * } from "@evolu/common";
4571
4801
  *
4572
4802
  * const sumTo =
4573
4803
  * (count: number): Task<number> =>
@@ -4583,7 +4813,7 @@ export const prioritized = <T, E, D = unknown>(
4583
4813
  * };
4584
4814
  *
4585
4815
  * await using run = createRun();
4586
- * expectOk(await run(sumTo(1001)), 500500);
4816
+ * assertOk(await run(sumTo(1001)), 500500);
4587
4817
  * ```
4588
4818
  *
4589
4819
  * @group Scheduling
@@ -4609,8 +4839,6 @@ export const yieldNow: Task<void> = async (run) => {
4609
4839
  return ok();
4610
4840
  };
4611
4841
 
4612
- // Abortability
4613
-
4614
4842
  /**
4615
4843
  * Waits until the current {@link Run} aborts, then rejects with its
4616
4844
  * {@link AbortError}.
@@ -4622,6 +4850,10 @@ export const yieldNow: Task<void> = async (run) => {
4622
4850
  *
4623
4851
  * ```ts
4624
4852
  * import {
4853
+ * assertEqual,
4854
+ * assertFalse,
4855
+ * assertType,
4856
+ * assertTrue,
4625
4857
  * AbortError,
4626
4858
  * createRun,
4627
4859
  * ok,
@@ -4638,23 +4870,22 @@ export const yieldNow: Task<void> = async (run) => {
4638
4870
  * const serverStarted = Promise.withResolvers<void>();
4639
4871
  * let serverStopped = false;
4640
4872
  * const startServer: Task<Server, never, ServerDep> = (run) => {
4641
- * expect(run.deps.port).toBe(3000);
4873
+ * assertEqual(run.deps.port, 3000);
4642
4874
  * serverStarted.resolve();
4643
4875
  * return ok({
4644
- * [Symbol.asyncDispose]: async () => {
4876
+ * [Symbol.asyncDispose]: () => {
4645
4877
  * serverStopped = true;
4878
+ * return Promise.resolve();
4646
4879
  * },
4647
4880
  * });
4648
4881
  * };
4649
4882
  *
4650
4883
  * const serve = (): Task<never, never, ServerDep> => async (run) => {
4651
- * await using server = await run.ok(startServer);
4884
+ * await using _ = await run.ok(startServer);
4652
4885
  * return await run(waitForAbort);
4653
4886
  * };
4654
4887
  *
4655
- * expectTypeOf(serve).returns.toEqualTypeOf<
4656
- * Task<never, never, ServerDep>
4657
- * >();
4888
+ * assertType<Task<never, never, ServerDep>, ReturnType<typeof serve>>();
4658
4889
  *
4659
4890
  * await using run = createRun();
4660
4891
  * const fiber = run.abortable(serve(), { port: 3000 });
@@ -4662,9 +4893,9 @@ export const yieldNow: Task<void> = async (run) => {
4662
4893
  * fiber.abort();
4663
4894
  *
4664
4895
  * const result = await fiber;
4665
- * assert(!result.ok);
4666
- * expect(AbortError.is(result.error)).toBe(true);
4667
- * expect(serverStopped).toBe(true);
4896
+ * assertFalse(result.ok);
4897
+ * assertTrue(AbortError.is(result.error));
4898
+ * assertTrue(serverStopped);
4668
4899
  * ```
4669
4900
  *
4670
4901
  * @group Abortability
@@ -4685,8 +4916,8 @@ export const waitForAbort: Task<never> = async (run) => {
4685
4916
  * execution. The daemon Task continues under root Run ownership until it
4686
4917
  * settles, observes abort, or the root Run is disposed.
4687
4918
  *
4688
- * This is not a replacement for direct {@link AbortSignal} support in operations
4689
- * that can observe abort, such as {@link fetch}, timers that accept a signal, or
4919
+ * This is not a replacement for direct {@link AbortSignal} support in APIs that
4920
+ * can observe abort, such as {@link fetch}, timers that accept a signal, or
4690
4921
  * callback APIs that accept a signal. Use it as an escape hatch for Tasks that
4691
4922
  * ignore abort when an abort request must stop waiting immediately.
4692
4923
  *
@@ -4719,6 +4950,9 @@ export const waitForAbort: Task<never> = async (run) => {
4719
4950
  *
4720
4951
  * ```ts
4721
4952
  * import {
4953
+ * assertEqual,
4954
+ * assertFalse,
4955
+ * assertTrue,
4722
4956
  * createRun,
4723
4957
  * daemon,
4724
4958
  * ok,
@@ -4739,18 +4973,24 @@ export const waitForAbort: Task<never> = async (run) => {
4739
4973
  * {
4740
4974
  * await using run = createRun();
4741
4975
  * const result = await run(timeout(daemon(taskNotUsingAbort), "1ms"));
4742
- * assert(!result.ok);
4743
- * expect(result.error.type).toBe("TimeoutError");
4744
- * expect(finished).toBe(false);
4976
+ * assertFalse(result.ok);
4977
+ * assertEqual(result.error.type, "TimeoutError");
4978
+ * assertFalse(finished);
4745
4979
  * finishTask();
4746
4980
  * }
4747
- * expect(finished).toBe(true);
4981
+ * assertTrue(finished);
4748
4982
  * ```
4749
4983
  *
4750
- * Promise-producing operations should start inside the Task, not before it.
4984
+ * Promise-returning functions should be called inside the Task, not before it.
4751
4985
  *
4752
4986
  * ```ts
4753
- * import { createRun, ok, type Result, type Task } from "@evolu/common";
4987
+ * import {
4988
+ * assertOk,
4989
+ * createRun,
4990
+ * ok,
4991
+ * type Result,
4992
+ * type Task,
4993
+ * } from "@evolu/common";
4754
4994
  *
4755
4995
  * type ResultValue = string;
4756
4996
  * const createPromiseReturningResult = (): Promise<Result<ResultValue>> =>
@@ -4759,14 +4999,20 @@ export const waitForAbort: Task<never> = async (run) => {
4759
4999
  * const task: Task<ResultValue> = () => createPromiseReturningResult();
4760
5000
  *
4761
5001
  * await using run = createRun();
4762
- * expectOk(await run(task), "value");
5002
+ * assertOk(await run(task), "value");
4763
5003
  * ```
4764
5004
  *
4765
5005
  * Do not reuse an already-running Promise. It started outside the Task, so the
4766
5006
  * Run cannot own its lifetime or request abort before it begins.
4767
5007
  *
4768
5008
  * ```ts
4769
- * import { ok, type Result, type Task } from "@evolu/common";
5009
+ * import {
5010
+ * assertType,
5011
+ * assertTrue,
5012
+ * ok,
5013
+ * type Result,
5014
+ * type Task,
5015
+ * } from "@evolu/common";
4770
5016
  *
4771
5017
  * type ResultValue = string;
4772
5018
  * let promiseStarted = false;
@@ -4781,8 +5027,8 @@ export const waitForAbort: Task<never> = async (run) => {
4781
5027
  * const promise = createPromiseReturningResult();
4782
5028
  * const task: Task<ResultValue> = () => promise;
4783
5029
  *
4784
- * expect(promiseStarted).toBe(true);
4785
- * expectTypeOf(task).toEqualTypeOf<Task<ResultValue>>();
5030
+ * assertTrue(promiseStarted);
5031
+ * assertType<Task<ResultValue>, typeof task>();
4786
5032
  * ```
4787
5033
  *
4788
5034
  * @group Lifetime
@@ -4820,14 +5066,21 @@ export const daemon =
4820
5066
  * ### Example
4821
5067
  *
4822
5068
  * ```ts
4823
- * import { createRun, ok, unabortable, type Task } from "@evolu/common";
5069
+ * import {
5070
+ * assertFalse,
5071
+ * assertOk,
5072
+ * createRun,
5073
+ * ok,
5074
+ * unabortable,
5075
+ * type Task,
5076
+ * } from "@evolu/common";
4824
5077
  *
4825
5078
  * const commitStarted = Promise.withResolvers<void>();
4826
5079
  * const finishCommit = Promise.withResolvers<void>();
4827
5080
  * const commit: Task<string> = unabortable(async (run) => {
4828
5081
  * commitStarted.resolve();
4829
5082
  * await finishCommit.promise;
4830
- * expect(run.signal.aborted).toBe(false);
5083
+ * assertFalse(run.signal.aborted);
4831
5084
  * return ok("committed");
4832
5085
  * });
4833
5086
  *
@@ -4837,7 +5090,7 @@ export const daemon =
4837
5090
  * fiber.abort();
4838
5091
  * finishCommit.resolve();
4839
5092
  *
4840
- * expectOk(await fiber, "committed");
5093
+ * assertOk(await fiber, "committed");
4841
5094
  * ```
4842
5095
  *
4843
5096
  * @group Abortability
@@ -4856,7 +5109,7 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
4856
5109
  *
4857
5110
  * An abort request before the mask Task starts prevents entering the mask. Once
4858
5111
  * the body starts, plain child Tasks inherit the mask, so acquire and release
4859
- * can run after abort. Put release operations directly in the original mask's
5112
+ * can run after abort. Start release Tasks directly in the original mask's
4860
5113
  * `finally`; do not wrap release in a nested `unabortableMask`, which is a new
4861
5114
  * critical-section entry and may not start after abort.
4862
5115
  *
@@ -4868,6 +5121,9 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
4868
5121
  *
4869
5122
  * ```ts
4870
5123
  * import {
5124
+ * assertEqual,
5125
+ * assertFalse,
5126
+ * assertTrue,
4871
5127
  * AbortError,
4872
5128
  * createRun,
4873
5129
  * ok,
@@ -4884,17 +5140,17 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
4884
5140
  * const operationStarted = Promise.withResolvers<void>();
4885
5141
  * const operate =
4886
5142
  * (resource: Resource): Task<never> =>
4887
- * async (run) => {
4888
- * expect(resource.id).toBe("resource-1");
5143
+ * (run) => {
5144
+ * assertEqual(resource.id, "resource-1");
4889
5145
  * operationStarted.resolve();
4890
- * return await run(waitForAbort);
5146
+ * return run(waitForAbort);
4891
5147
  * };
4892
5148
  * let released = false;
4893
5149
  * const release =
4894
5150
  * (_resource: Resource): Task<void> =>
4895
5151
  * (run) => {
4896
5152
  * // Release inherits the mask even after abort was requested.
4897
- * expect(run.signal.aborted).toBe(false);
5153
+ * assertFalse(run.signal.aborted);
4898
5154
  * released = true;
4899
5155
  * return ok();
4900
5156
  * };
@@ -4918,9 +5174,9 @@ export const unabortable = /*#__PURE__*/ withTaskMeta({
4918
5174
  * await operationStarted.promise;
4919
5175
  * fiber.abort();
4920
5176
  * const result = await fiber;
4921
- * assert(!result.ok);
4922
- * expect(AbortError.is(result.error)).toBe(true);
4923
- * expect(released).toBe(true);
5177
+ * assertFalse(result.ok);
5178
+ * assertTrue(AbortError.is(result.error));
5179
+ * assertTrue(released);
4924
5180
  * ```
4925
5181
  *
4926
5182
  * @group Abortability
@@ -4938,7 +5194,7 @@ export const unabortableMask = <T, E, D = unknown>(
4938
5194
  runInternal.abortMask > abortableMask,
4939
5195
  "unabortableMask requires a masked Run; use run(task), not a direct call",
4940
5196
  );
4941
- const restoreToken = Symbol() as RestoreToken;
5197
+ const restoreToken = Symbol("restore") as RestoreToken;
4942
5198
 
4943
5199
  // The token is local to this Task Run; descendant Runs inherit the token
4944
5200
  // set so helpers can receive restore while the mask Task is alive.
@@ -4973,13 +5229,15 @@ export const unabortableMask = <T, E, D = unknown>(
4973
5229
  * Prefer native `using`, `await using`, or {@link AsyncDisposableStack} for
4974
5230
  * owned values that implement {@link Disposable} or {@link AsyncDisposable}. Use
4975
5231
  * `acquireUseRelease` when acquisition must be balanced with a separate release
4976
- * operation, such as unlocking, returning a pooled value, releasing a lease, or
5232
+ * step, such as unlocking, returning a pooled value, releasing a lease, or
4977
5233
  * logging out of a session.
4978
5234
  *
4979
5235
  * ### Example
4980
5236
  *
4981
5237
  * ```ts
4982
5238
  * import {
5239
+ * assertErr,
5240
+ * assertTrue,
4983
5241
  * acquireUseRelease,
4984
5242
  * createRun,
4985
5243
  * err,
@@ -5019,9 +5277,9 @@ export const unabortableMask = <T, E, D = unknown>(
5019
5277
  * );
5020
5278
  *
5021
5279
  * await using run = createRun();
5022
- * expectErr(await run(queryUser), { type: "UserUnavailable" });
5280
+ * assertErr(await run(queryUser), { type: "UserUnavailable" });
5023
5281
  * // Release still runs when use returns a domain error.
5024
- * expect(connectionClosed).toBe(true);
5282
+ * assertTrue(connectionClosed);
5025
5283
  * ```
5026
5284
  *
5027
5285
  * @group Lifetime
@@ -5045,7 +5303,7 @@ export const acquireUseRelease = <
5045
5303
  if (!resourceResult.ok) return resourceResult;
5046
5304
 
5047
5305
  try {
5048
- // eslint-disable-next-line react-hooks/rules-of-hooks -- `use` is an acquireUseRelease callback, not a React Hook.
5306
+ // oxlint-disable-next-line react/rules-of-hooks -- `use` is an acquireUseRelease callback, not a React Hook.
5049
5307
  return await run(restore(use(resourceResult.value)));
5050
5308
  } finally {
5051
5309
  await run.ok(release(resourceResult.value));
@@ -5053,8 +5311,6 @@ export const acquireUseRelease = <
5053
5311
  },
5054
5312
  );
5055
5313
 
5056
- // Concurrency primitives
5057
-
5058
5314
  /**
5059
5315
  * A one-shot value resolved from outside the waiting {@link Task}.
5060
5316
  *
@@ -5085,6 +5341,10 @@ export interface Deferred<T, E = never> {
5085
5341
  *
5086
5342
  * ```ts
5087
5343
  * import {
5344
+ * assertFalse,
5345
+ * assertOk,
5346
+ * assertType,
5347
+ * assertTrue,
5088
5348
  * createDeferred,
5089
5349
  * createRun,
5090
5350
  * ok,
@@ -5095,22 +5355,28 @@ export interface Deferred<T, E = never> {
5095
5355
  * const deferred = createDeferred<string>();
5096
5356
  *
5097
5357
  * const fiber = run(deferred.task);
5098
- * expect(deferred.resolve(ok("ready"))).toBe(true);
5358
+ * assertTrue(deferred.resolve(ok("ready")));
5099
5359
  *
5100
5360
  * const result = await fiber;
5101
- * expectTypeOf(result).toEqualTypeOf<Result<string>>();
5102
- * expectOk(result, "ready");
5361
+ * assertType<Result<string>, typeof result>();
5362
+ * assertOk(result, "ready");
5103
5363
  *
5104
5364
  * // A Deferred is one-shot: later resolutions are ignored, and future
5105
5365
  * // waiters receive the original Result.
5106
- * expect(deferred.resolve(ok("late"))).toBe(false);
5107
- * expectOk(await run(deferred.task), "ready");
5366
+ * assertFalse(deferred.resolve(ok("late")));
5367
+ * assertOk(await run(deferred.task), "ready");
5108
5368
  * ```
5109
5369
  *
5110
5370
  * ### Aborting a waiter
5111
5371
  *
5112
5372
  * ```ts
5113
- * import { AbortError, createDeferred, createRun } from "@evolu/common";
5373
+ * import {
5374
+ * assertFalse,
5375
+ * assertTrue,
5376
+ * AbortError,
5377
+ * createDeferred,
5378
+ * createRun,
5379
+ * } from "@evolu/common";
5114
5380
  *
5115
5381
  * await using run = createRun();
5116
5382
  * const deferred = createDeferred<string>();
@@ -5119,8 +5385,8 @@ export interface Deferred<T, E = never> {
5119
5385
  * fiber.abort({ type: "NoLongerNeeded" });
5120
5386
  *
5121
5387
  * const result = await fiber;
5122
- * assert(!result.ok);
5123
- * expect(AbortError.is(result.error)).toBe(true);
5388
+ * assertFalse(result.ok);
5389
+ * assertTrue(AbortError.is(result.error));
5124
5390
  * ```
5125
5391
  *
5126
5392
  * @group Concurrency primitives
@@ -5193,7 +5459,15 @@ export interface Gate {
5193
5459
  * ### Example
5194
5460
  *
5195
5461
  * ```ts
5196
- * import { createGate, createRun, ok, type Task } from "@evolu/common";
5462
+ * import {
5463
+ * assertEqual,
5464
+ * assertOk,
5465
+ * assertTrue,
5466
+ * createGate,
5467
+ * createRun,
5468
+ * ok,
5469
+ * type Task,
5470
+ * } from "@evolu/common";
5197
5471
  *
5198
5472
  * await using run = createRun();
5199
5473
  * const networkGate = createGate();
@@ -5209,13 +5483,13 @@ export interface Gate {
5209
5483
  *
5210
5484
  * const first = run(syncOnce("first"));
5211
5485
  * const second = run(syncOnce("second"));
5212
- * expect(uploadedItems).toEqual([]);
5486
+ * assertEqual(uploadedItems, []);
5213
5487
  *
5214
5488
  * networkGate.open();
5215
- * expectOk(await first, undefined);
5216
- * expectOk(await second, undefined);
5217
- * expect(uploadedItems).toEqual(["first", "second"]);
5218
- * expect(networkGate.isOpen()).toBe(true);
5489
+ * assertOk(await first, undefined);
5490
+ * assertOk(await second, undefined);
5491
+ * assertEqual(uploadedItems, ["first", "second"]);
5492
+ * assertTrue(networkGate.isOpen());
5219
5493
  * ```
5220
5494
  *
5221
5495
  * @group Concurrency primitives
@@ -5230,7 +5504,6 @@ export const createGate = ({
5230
5504
 
5231
5505
  return {
5232
5506
  // Direct same-Run delegation is intentional so wait observes the current deferred.
5233
- // eslint-disable-next-line evolu/no-direct-task-call
5234
5507
  wait: (run) => deferred.task(run),
5235
5508
  open: () => {
5236
5509
  if (isOpen) return false;
@@ -5259,8 +5532,8 @@ export const createGate = ({
5259
5532
  *
5260
5533
  * Use {@link Semaphore.withPermit} or {@link Semaphore.withPermits} to acquire
5261
5534
  * permits for one Task and release them when it settles. Use
5262
- * {@link Semaphore.take} when permits must be held across multiple operations;
5263
- * the returned {@link SemaphorePermit} owns release and is disposable.
5535
+ * {@link Semaphore.take} when one permit must cover several child Tasks; the
5536
+ * returned {@link SemaphorePermit} owns release and is disposable.
5264
5537
  *
5265
5538
  * Requests are not capped by the current permit count because
5266
5539
  * {@link Semaphore.resize} can increase it later.
@@ -5362,8 +5635,8 @@ export interface SemaphorePermit extends Disposable {
5362
5635
  * semaphore does not reorder requests to maximize utilization.
5363
5636
  *
5364
5637
  * Use `"fifo"` when fairness and predictable progress matter, such as tenant
5365
- * sync, API quota, or database operations where large requests must not be
5366
- * starved by a stream of smaller requests.
5638
+ * sync, API quota, or database pools where large requests must not be starved
5639
+ * by a stream of smaller requests.
5367
5640
  *
5368
5641
  * Use `"greedy"` when permits represent a shared budget and smaller or
5369
5642
  * latency-sensitive requests should proceed around larger queued requests. For
@@ -5418,6 +5691,7 @@ export interface SemaphoreSnapshot {
5418
5691
  *
5419
5692
  * ```ts
5420
5693
  * import {
5694
+ * assertEqual,
5421
5695
  * createRun,
5422
5696
  * createSemaphore,
5423
5697
  * getOk,
@@ -5449,8 +5723,8 @@ export interface SemaphoreSnapshot {
5449
5723
  * ]);
5450
5724
  *
5451
5725
  * const savedUsers = results.map(getOk);
5452
- * expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
5453
- * expect(maxActiveSaves).toBe(2);
5726
+ * assertEqual(savedUsers, ["saved:1", "saved:2", "saved:3"]);
5727
+ * assertEqual(maxActiveSaves, 2);
5454
5728
  * ```
5455
5729
  *
5456
5730
  * @group Concurrency primitives
@@ -5619,6 +5893,8 @@ export interface Mutex {
5619
5893
  *
5620
5894
  * ```ts
5621
5895
  * import {
5896
+ * assertEqual,
5897
+ * assertOk,
5622
5898
  * createMutex,
5623
5899
  * createRun,
5624
5900
  * ok,
@@ -5641,9 +5917,9 @@ export interface Mutex {
5641
5917
  * run(deposit(2)),
5642
5918
  * run(deposit(3)),
5643
5919
  * ]);
5644
- * expectOk(first, undefined);
5645
- * expectOk(second, undefined);
5646
- * expect(balance).toBe(5);
5920
+ * assertOk(first, undefined);
5921
+ * assertOk(second, undefined);
5922
+ * assertEqual(balance, 5);
5647
5923
  * ```
5648
5924
  *
5649
5925
  * @group Concurrency primitives
@@ -5666,8 +5942,8 @@ export const createMutex = (): Mutex => {
5666
5942
  * resources, making idle-key cleanup less predictable and making accidental key
5667
5943
  * retention easier.
5668
5944
  *
5669
- * Use Semaphore directly when callers need to hold permits across multiple
5670
- * operations or resize a permit pool. Use `SemaphoreByKey` when permit
5945
+ * Use Semaphore directly when callers need to hold permits while starting
5946
+ * several child Tasks or resize a permit pool. Use `SemaphoreByKey` when permit
5671
5947
  * ownership should be tied to one Task lifetime and idle keys can be forgotten
5672
5948
  * automatically.
5673
5949
  *
@@ -5707,6 +5983,8 @@ export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<
5707
5983
  *
5708
5984
  * ```ts
5709
5985
  * import {
5986
+ * assertOk,
5987
+ * assertTrue,
5710
5988
  * createRun,
5711
5989
  * createSemaphoreByKey,
5712
5990
  * ok,
@@ -5719,11 +5997,11 @@ export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<
5719
5997
  * downloadsByHost.withPermit(host, () => ok(`${host}/${file}`));
5720
5998
  *
5721
5999
  * await using run = createRun();
5722
- * expectOk(
6000
+ * assertOk(
5723
6001
  * await run(download("a.example", "index.json")),
5724
6002
  * "a.example/index.json",
5725
6003
  * );
5726
- * expect(downloadsByHost.isIdle("a.example")).toBe(true);
6004
+ * assertTrue(downloadsByHost.isIdle("a.example"));
5727
6005
  * ```
5728
6006
  *
5729
6007
  * @group Concurrency primitives
@@ -5801,6 +6079,8 @@ export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {}
5801
6079
  *
5802
6080
  * ```ts
5803
6081
  * import {
6082
+ * assertOk,
6083
+ * assertTrue,
5804
6084
  * createMutexByKey,
5805
6085
  * createRun,
5806
6086
  * ok,
@@ -5817,9 +6097,9 @@ export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {}
5817
6097
  * });
5818
6098
  *
5819
6099
  * await using run = createRun();
5820
- * expectOk(await run(deposit("checking", 2)), 2);
5821
- * expectOk(await run(deposit("checking", 3)), 5);
5822
- * expect(accountLocks.isIdle("checking")).toBe(true);
6100
+ * assertOk(await run(deposit("checking", 2)), 2);
6101
+ * assertOk(await run(deposit("checking", 3)), 5);
6102
+ * assertTrue(accountLocks.isIdle("checking"));
5823
6103
  * ```
5824
6104
  *
5825
6105
  * @group Concurrency primitives
@@ -5847,9 +6127,9 @@ export function createMutexByKey<K, L = K>({
5847
6127
  /**
5848
6128
  * {@link Ref} protected by a {@link Mutex}.
5849
6129
  *
5850
- * `MutexRef` serializes reads, writes, and updates through an internal Mutex,
5851
- * so every operation observes one consistent value transition at a time. When
5852
- * an update fails or is aborted, the previous value is preserved.
6130
+ * `MutexRef` serializes reads, writes, and updates through an internal Mutex.
6131
+ * Reads see a stable value, while writes and updates commit one transition at a
6132
+ * time. When an update fails or is aborted, the previous value is preserved.
5853
6133
  *
5854
6134
  * `MutexRef` is non-reentrant. Updaters and modifiers run while holding the
5855
6135
  * internal Mutex, so calling another method on the same MutexRef from inside
@@ -5859,8 +6139,8 @@ export function createMutexByKey<K, L = K>({
5859
6139
  * read-modify-write. Plain Ref cannot express that — between a sync read and a
5860
6140
  * later write, a concurrent transition can interleave and get lost.
5861
6141
  *
5862
- * `MutexRef` operations are Tasks and incur normal {@link Run} lifecycle
5863
- * overhead. Use {@link Ref} instead for synchronous state transitions,
6142
+ * `MutexRef` reads, writes, and updates are Tasks and incur normal {@link Run}
6143
+ * lifecycle overhead. Use {@link Ref} instead for synchronous state transitions,
5864
6144
  * especially on allocation-sensitive hot paths.
5865
6145
  *
5866
6146
  * @group Concurrency primitives
@@ -5909,14 +6189,14 @@ export interface MutexRef<T> {
5909
6189
  * ### Example
5910
6190
  *
5911
6191
  * ```ts
5912
- * import { createMutexRef, createRun, ok } from "@evolu/common";
6192
+ * import { assertOk, createMutexRef, createRun, ok } from "@evolu/common";
5913
6193
  *
5914
6194
  * const counter = createMutexRef(0);
5915
6195
  * const increment = counter.updateAndGet((value) => () => ok(value + 1));
5916
6196
  *
5917
6197
  * await using run = createRun();
5918
- * expectOk(await run(increment), 1);
5919
- * expectOk(await run(counter.get), 1);
6198
+ * assertOk(await run(increment), 1);
6199
+ * assertOk(await run(counter.get), 1);
5920
6200
  * ```
5921
6201
  *
5922
6202
  * @group Concurrency primitives
@@ -5981,7 +6261,7 @@ export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
5981
6261
  // filtering, and pluggable log sinks.
5982
6262
  // - Tracing spans with names, timing, parent-child relationships, attributes,
5983
6263
  // error status, and helpers for annotating the current or child spans.
5984
- // - Metrics for counters, gauges, histograms, and operation durations.
6264
+ // - Metrics for counters, gauges, histograms, and Task execution durations.
5985
6265
  // - Resource metadata for service name, service version, deployment
5986
6266
  // environment, and user-provided attributes.
5987
6267
  // - Exporters for production telemetry backends, including OTLP-compatible
@@ -5994,5 +6274,5 @@ export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
5994
6274
  // - Run labels and structured annotations for rendering useful snapshot trees
5995
6275
  // instead of anonymous ids.
5996
6276
  // - Snapshot and trace views should preserve ownership boundaries, so reusable
5997
- // resources and long-lived operations appear as labeled subtrees instead of
5998
- // unrelated child operations.
6277
+ // resources and long-lived Runs appear as labeled subtrees instead of
6278
+ // unrelated child Runs.