@evolu/common 8.7.0 → 8.8.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 (300) hide show
  1. package/dist/src/Array.d.ts +21 -21
  2. package/dist/src/Array.js +5 -5
  3. package/dist/src/Assert.d.ts +478 -141
  4. package/dist/src/Assert.d.ts.map +1 -1
  5. package/dist/src/Assert.js +644 -99
  6. package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
  7. package/dist/src/Binary.d.ts.map +1 -0
  8. package/dist/src/{Buffer.js → Binary.js} +206 -24
  9. package/dist/src/Console.d.ts +2 -10
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +6 -12
  12. package/dist/src/Crypto.d.ts +5 -5
  13. package/dist/src/Crypto.js +1 -1
  14. package/dist/src/Eq.d.ts +35 -40
  15. package/dist/src/Eq.d.ts.map +1 -1
  16. package/dist/src/Eq.js +51 -87
  17. package/dist/src/Function.d.ts +9 -14
  18. package/dist/src/Function.d.ts.map +1 -1
  19. package/dist/src/Function.js +7 -9
  20. package/dist/src/Http.d.ts +6 -7
  21. package/dist/src/Http.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +1 -0
  24. package/dist/src/LockManager.d.ts +1 -1
  25. package/dist/src/LockManager.js +1 -1
  26. package/dist/src/Lookup.d.ts +3 -2
  27. package/dist/src/Lookup.d.ts.map +1 -1
  28. package/dist/src/Lookup.js +4 -3
  29. package/dist/src/Number.d.ts +1 -1
  30. package/dist/src/Number.d.ts.map +1 -1
  31. package/dist/src/Number.js +1 -1
  32. package/dist/src/Object.d.ts +4 -4
  33. package/dist/src/Object.d.ts.map +1 -1
  34. package/dist/src/Object.js +5 -4
  35. package/dist/src/Option.d.ts +1 -2
  36. package/dist/src/Option.d.ts.map +1 -1
  37. package/dist/src/Option.js +1 -2
  38. package/dist/src/Platform.js +2 -0
  39. package/dist/src/Polyfills.js +4 -1
  40. package/dist/src/Random.d.ts +1 -1
  41. package/dist/src/Result.d.ts +49 -54
  42. package/dist/src/Result.d.ts.map +1 -1
  43. package/dist/src/Result.js +17 -22
  44. package/dist/src/Set.d.ts +6 -6
  45. package/dist/src/Set.js +2 -2
  46. package/dist/src/Sqlite.js +1 -1
  47. package/dist/src/String.js +3 -2
  48. package/dist/src/Task.d.ts +32 -32
  49. package/dist/src/Task.d.ts.map +1 -1
  50. package/dist/src/Task.js +20 -22
  51. package/dist/src/Test.d.ts +31 -3
  52. package/dist/src/Test.d.ts.map +1 -1
  53. package/dist/src/Test.js +46 -3
  54. package/dist/src/Time.d.ts +3 -11
  55. package/dist/src/Time.d.ts.map +1 -1
  56. package/dist/src/Time.js +10 -18
  57. package/dist/src/Type.d.ts +87 -85
  58. package/dist/src/Type.d.ts.map +1 -1
  59. package/dist/src/Type.js +70 -63
  60. package/dist/src/Types.d.ts +12 -12
  61. package/dist/src/Types.js +1 -1
  62. package/dist/src/WebSocket.d.ts +4 -7
  63. package/dist/src/WebSocket.d.ts.map +1 -1
  64. package/dist/src/Worker.d.ts +1 -1
  65. package/dist/src/Worker.js +7 -7
  66. package/dist/src/index.d.ts +1 -1
  67. package/dist/src/index.js +1 -1
  68. package/dist/src/intl/_en.d.ts.map +1 -1
  69. package/dist/src/intl/_en.js +1 -1
  70. package/dist/src/intl/ar.d.ts.map +1 -1
  71. package/dist/src/intl/ar.js +1 -1
  72. package/dist/src/intl/bn.d.ts.map +1 -1
  73. package/dist/src/intl/bn.js +1 -1
  74. package/dist/src/intl/ca.d.ts.map +1 -1
  75. package/dist/src/intl/ca.js +1 -1
  76. package/dist/src/intl/cs.d.ts.map +1 -1
  77. package/dist/src/intl/cs.js +1 -1
  78. package/dist/src/intl/da.d.ts.map +1 -1
  79. package/dist/src/intl/da.js +1 -1
  80. package/dist/src/intl/de.d.ts.map +1 -1
  81. package/dist/src/intl/de.js +1 -1
  82. package/dist/src/intl/el.d.ts.map +1 -1
  83. package/dist/src/intl/el.js +1 -1
  84. package/dist/src/intl/es.d.ts.map +1 -1
  85. package/dist/src/intl/es.js +1 -1
  86. package/dist/src/intl/fa.d.ts.map +1 -1
  87. package/dist/src/intl/fa.js +1 -1
  88. package/dist/src/intl/fi.d.ts.map +1 -1
  89. package/dist/src/intl/fi.js +1 -1
  90. package/dist/src/intl/fil.d.ts.map +1 -1
  91. package/dist/src/intl/fil.js +1 -1
  92. package/dist/src/intl/fr.d.ts.map +1 -1
  93. package/dist/src/intl/fr.js +1 -1
  94. package/dist/src/intl/he.d.ts.map +1 -1
  95. package/dist/src/intl/he.js +1 -1
  96. package/dist/src/intl/hi.d.ts.map +1 -1
  97. package/dist/src/intl/hi.js +1 -1
  98. package/dist/src/intl/hr.d.ts.map +1 -1
  99. package/dist/src/intl/hr.js +1 -1
  100. package/dist/src/intl/hu.d.ts.map +1 -1
  101. package/dist/src/intl/hu.js +1 -1
  102. package/dist/src/intl/id.d.ts.map +1 -1
  103. package/dist/src/intl/id.js +1 -1
  104. package/dist/src/intl/it.d.ts.map +1 -1
  105. package/dist/src/intl/it.js +1 -1
  106. package/dist/src/intl/ja.d.ts.map +1 -1
  107. package/dist/src/intl/ja.js +1 -1
  108. package/dist/src/intl/ko.d.ts.map +1 -1
  109. package/dist/src/intl/ko.js +1 -1
  110. package/dist/src/intl/ml.d.ts.map +1 -1
  111. package/dist/src/intl/ml.js +1 -1
  112. package/dist/src/intl/mr.d.ts.map +1 -1
  113. package/dist/src/intl/mr.js +1 -1
  114. package/dist/src/intl/ms.d.ts.map +1 -1
  115. package/dist/src/intl/ms.js +1 -1
  116. package/dist/src/intl/nb.d.ts.map +1 -1
  117. package/dist/src/intl/nb.js +1 -1
  118. package/dist/src/intl/nl.d.ts.map +1 -1
  119. package/dist/src/intl/nl.js +1 -1
  120. package/dist/src/intl/pa.d.ts.map +1 -1
  121. package/dist/src/intl/pa.js +1 -1
  122. package/dist/src/intl/pl.d.ts.map +1 -1
  123. package/dist/src/intl/pl.js +1 -1
  124. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  125. package/dist/src/intl/pt-BR.js +1 -1
  126. package/dist/src/intl/pt.d.ts.map +1 -1
  127. package/dist/src/intl/pt.js +1 -1
  128. package/dist/src/intl/ro.d.ts.map +1 -1
  129. package/dist/src/intl/ro.js +1 -1
  130. package/dist/src/intl/sk.d.ts.map +1 -1
  131. package/dist/src/intl/sk.js +1 -1
  132. package/dist/src/intl/sl.d.ts.map +1 -1
  133. package/dist/src/intl/sl.js +1 -1
  134. package/dist/src/intl/sv.d.ts.map +1 -1
  135. package/dist/src/intl/sv.js +1 -1
  136. package/dist/src/intl/sw.d.ts.map +1 -1
  137. package/dist/src/intl/sw.js +1 -1
  138. package/dist/src/intl/ta.d.ts.map +1 -1
  139. package/dist/src/intl/ta.js +1 -1
  140. package/dist/src/intl/te.d.ts.map +1 -1
  141. package/dist/src/intl/te.js +1 -1
  142. package/dist/src/intl/th.d.ts.map +1 -1
  143. package/dist/src/intl/th.js +1 -1
  144. package/dist/src/intl/tr.d.ts.map +1 -1
  145. package/dist/src/intl/tr.js +1 -1
  146. package/dist/src/intl/uk.d.ts.map +1 -1
  147. package/dist/src/intl/uk.js +1 -1
  148. package/dist/src/intl/ur.d.ts.map +1 -1
  149. package/dist/src/intl/ur.js +1 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +1 -1
  152. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  153. package/dist/src/intl/zh-CN.js +1 -1
  154. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  155. package/dist/src/intl/zh-TW.js +1 -1
  156. package/dist/src/local-first/Evolu.d.ts +12 -12
  157. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  158. package/dist/src/local-first/Evolu.js +2 -2
  159. package/dist/src/local-first/Protocol.d.ts +14 -76
  160. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  161. package/dist/src/local-first/Protocol.js +29 -206
  162. package/dist/src/local-first/Query.d.ts +11 -11
  163. package/dist/src/local-first/Query.js +6 -6
  164. package/dist/src/local-first/Relay.d.ts +2 -2
  165. package/dist/src/local-first/Schema.d.ts +3 -5
  166. package/dist/src/local-first/Schema.d.ts.map +1 -1
  167. package/dist/src/local-first/Schema.js +2 -4
  168. package/dist/src/local-first/Shared.d.ts.map +1 -1
  169. package/dist/src/local-first/Shared.js +5 -5
  170. package/dist/src/local-first/Storage.js +4 -4
  171. package/dist/src/local-first/Timestamp.d.ts +9 -0
  172. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  173. package/dist/src/local-first/Timestamp.js +13 -10
  174. package/package.json +7 -4
  175. package/src/Array.test.ts +809 -0
  176. package/src/Array.ts +21 -21
  177. package/src/Assert.test.ts +1181 -0
  178. package/src/Assert.ts +971 -227
  179. package/src/BigInt.test.ts +28 -0
  180. package/src/Binary.test.ts +1128 -0
  181. package/src/{Buffer.ts → Binary.ts} +273 -46
  182. package/src/Brand.test.ts +90 -0
  183. package/src/Cache.test.ts +183 -0
  184. package/src/Callbacks.test.ts +66 -0
  185. package/src/Console.test.ts +809 -0
  186. package/src/Console.ts +9 -17
  187. package/src/Crypto.test.ts +138 -0
  188. package/src/Crypto.ts +5 -5
  189. package/src/Eq.test.ts +333 -0
  190. package/src/Eq.ts +110 -176
  191. package/src/Error.test.ts +118 -0
  192. package/src/Function.test.ts +204 -0
  193. package/src/Function.ts +10 -15
  194. package/src/Http.test.ts +816 -0
  195. package/src/Http.ts +6 -7
  196. package/src/LeakDetector.test.ts +210 -0
  197. package/src/LeakDetector.ts +1 -0
  198. package/src/LockManager.ts +2 -2
  199. package/src/Lookup.test.ts +418 -0
  200. package/src/Lookup.ts +4 -3
  201. package/src/Microtask.test.ts +140 -0
  202. package/src/Number.test.ts +218 -0
  203. package/src/Number.ts +2 -2
  204. package/src/Object.test.ts +293 -0
  205. package/src/Object.ts +5 -4
  206. package/src/Option.test.ts +104 -0
  207. package/src/Option.ts +1 -2
  208. package/src/Order.test.ts +56 -0
  209. package/src/Platform.ts +2 -0
  210. package/src/Polyfills.ts +3 -4
  211. package/src/Random.test.ts +89 -0
  212. package/src/Random.ts +1 -1
  213. package/src/Ref.test.ts +139 -0
  214. package/src/RefCount.test.ts +184 -0
  215. package/src/Relation.test.ts +505 -0
  216. package/src/Resource.test.ts +4623 -0
  217. package/src/Result.test.ts +1829 -0
  218. package/src/Result.ts +49 -54
  219. package/src/Schedule.test.ts +1554 -0
  220. package/src/Set.test.ts +279 -0
  221. package/src/Set.ts +6 -6
  222. package/src/Sqlite.ts +1 -1
  223. package/src/Store.test.ts +288 -0
  224. package/src/String.test.ts +54 -0
  225. package/src/String.ts +3 -3
  226. package/src/Task.test.ts +10820 -0
  227. package/src/Task.ts +38 -43
  228. package/src/Test.test.ts +88 -0
  229. package/src/Test.ts +49 -3
  230. package/src/Time.test.ts +926 -0
  231. package/src/Time.ts +13 -21
  232. package/src/Type.base64.test.ts +88 -0
  233. package/src/Type.test.ts +20465 -0
  234. package/src/Type.ts +114 -111
  235. package/src/Types.test.ts +324 -0
  236. package/src/Types.ts +13 -13
  237. package/src/WebSocket.ts +4 -7
  238. package/src/Worker.test.ts +755 -0
  239. package/src/Worker.ts +13 -18
  240. package/src/index.ts +1 -1
  241. package/src/intl/_en.ts +1 -1
  242. package/src/intl/ar.ts +1 -1
  243. package/src/intl/bn.ts +1 -1
  244. package/src/intl/ca.ts +1 -1
  245. package/src/intl/cs.ts +1 -1
  246. package/src/intl/da.ts +1 -1
  247. package/src/intl/de.ts +1 -1
  248. package/src/intl/el.ts +1 -1
  249. package/src/intl/es.ts +1 -1
  250. package/src/intl/fa.ts +1 -1
  251. package/src/intl/fi.ts +1 -1
  252. package/src/intl/fil.ts +1 -1
  253. package/src/intl/fr.ts +1 -1
  254. package/src/intl/he.ts +1 -1
  255. package/src/intl/hi.ts +1 -1
  256. package/src/intl/hr.ts +1 -1
  257. package/src/intl/hu.ts +1 -1
  258. package/src/intl/id.ts +1 -1
  259. package/src/intl/intl.test.ts +3106 -0
  260. package/src/intl/it.ts +1 -1
  261. package/src/intl/ja.ts +1 -1
  262. package/src/intl/ko.ts +1 -1
  263. package/src/intl/ml.ts +1 -1
  264. package/src/intl/mr.ts +1 -1
  265. package/src/intl/ms.ts +1 -1
  266. package/src/intl/nb.ts +1 -1
  267. package/src/intl/nl.ts +1 -1
  268. package/src/intl/pa.ts +1 -1
  269. package/src/intl/pl.ts +1 -1
  270. package/src/intl/pt-BR.ts +1 -1
  271. package/src/intl/pt.ts +1 -1
  272. package/src/intl/ro.ts +1 -1
  273. package/src/intl/sk.ts +1 -1
  274. package/src/intl/sl.ts +1 -1
  275. package/src/intl/sv.ts +1 -1
  276. package/src/intl/sw.ts +1 -1
  277. package/src/intl/ta.ts +1 -1
  278. package/src/intl/te.ts +1 -1
  279. package/src/intl/th.ts +1 -1
  280. package/src/intl/tr.ts +1 -1
  281. package/src/intl/uk.ts +1 -1
  282. package/src/intl/ur.ts +1 -1
  283. package/src/intl/vi.ts +1 -1
  284. package/src/intl/zh-CN.ts +1 -1
  285. package/src/intl/zh-TW.ts +1 -1
  286. package/src/local-first/Evolu.test.ts +1921 -0
  287. package/src/local-first/Evolu.ts +18 -14
  288. package/src/local-first/Owner.test.ts +83 -0
  289. package/src/local-first/Protocol.test.ts +1037 -0
  290. package/src/local-first/Protocol.ts +58 -261
  291. package/src/local-first/Query.test.ts +349 -0
  292. package/src/local-first/Query.ts +11 -11
  293. package/src/local-first/Relay.ts +2 -2
  294. package/src/local-first/Schema.ts +3 -5
  295. package/src/local-first/Shared.test.ts +1828 -0
  296. package/src/local-first/Shared.ts +9 -8
  297. package/src/local-first/Storage.ts +4 -4
  298. package/src/local-first/Timestamp.test.ts +374 -0
  299. package/src/local-first/Timestamp.ts +22 -10
  300. package/dist/src/Buffer.d.ts.map +0 -1
package/src/Assert.ts CHANGED
@@ -1,11 +1,49 @@
1
1
  /**
2
- * Runtime assertions for invariants and value checks.
2
+ * Platform-agnostic assertions for invariants, examples, and tests.
3
+ *
4
+ * An assertion documents a condition required for the program to be correct.
5
+ * Ideally, the type system should enforce it so incorrect code cannot compile,
6
+ * but that is not always possible (not even in Rust). A runtime assertion
7
+ * failure therefore indicates a bug that must be fixed. Throwing at the point
8
+ * of violation prevents invalid state from propagating and makes the defect
9
+ * easier to diagnose.
10
+ *
11
+ * Evolu provides its own assertions so the same concise API works for
12
+ * production invariants, executable examples, and tests on every platform. The
13
+ * general {@link assert} requires a message explaining the invariant, and
14
+ * specialized assertions provide focused diagnostics and narrowing. The API
15
+ * deliberately favors strict and predictable contracts: one {@link assertEqual}
16
+ * covers primitives and structural Data comparisons, {@link assertEqualBytes}
17
+ * compares bytes across array representations, and {@link assertSame} is
18
+ * reserved for SameValue or reference identity. Generic partial structural
19
+ * assertions such as `assertMatches` are omitted so unmentioned state cannot
20
+ * hide regressions. If a complete expected value is too large to keep inline,
21
+ * use a fixture or a focused domain helper that keeps the contract explicit.
22
+ * {@link assertOk} and {@link assertErr} provide convenient Result narrowing,
23
+ * while {@link assertType} provides runtime Type assertions and compile-time
24
+ * type equality similar to Vitest's `expectTypeOf` without requiring a test
25
+ * runner. {@link assertThrows} and {@link assertRejects} verify thrown and
26
+ * rejected values without matcher semantics. Together, these assertions keep
27
+ * documentation examples concise and directly copyable.
28
+ *
29
+ * In Node.js, failures use the native `AssertionError` for structured
30
+ * diagnostics and diffs. Other platforms use a compatible fallback. You should
31
+ * not need to import Node.js assertions unless Evolu does not provide an
32
+ * equivalent or a test requires exact Node.js semantics.
33
+ *
34
+ * Do not use assertions to validate external input. Use a {@link Type}
35
+ * declaration's `fromUnknown` so invalid input is represented by a typed
36
+ * {@link Result}.
37
+ *
38
+ * TODO(next major): Add a separate production build that replaces full
39
+ * assertion messages with numeric error codes and a decoder, following React's
40
+ * approach, to reduce the core bundle size since Evolu has many assertions.
3
41
  *
4
42
  * @module
5
43
  */
6
44
 
7
45
  import type { NonEmptyArray, NonEmptyReadonlyArray } from "./Array.ts";
8
- import { eqData, eqStrict, type Eq } from "./Eq.ts";
46
+ import { eqArrayNumber, eqData, eqSameValue, type Eq } from "./Eq.ts";
9
47
  import type {
10
48
  AnyResult,
11
49
  Err,
@@ -14,306 +52,616 @@ import type {
14
52
  Ok,
15
53
  Result,
16
54
  } from "./Result.ts";
17
- import type { Data, IsData, Type } from "./Type.ts";
18
- import type { CompileTimeError } from "./Types.ts";
55
+ import type { assertType, Type } from "./Type.ts";
56
+ import type { ValueWithLength } from "./Types.ts";
19
57
 
20
58
  /**
21
- * Ensures a condition is true, throwing an error with the provided message if
22
- * not.
59
+ * Asserts that a condition is truthy.
60
+ *
61
+ * Throws an `AssertionError` with the provided message if the condition is
62
+ * falsy, preventing invalid state from propagating and making the failure
63
+ * easier to diagnose.
23
64
  *
24
- * Prevents invalid states from propagating through the system by halting
25
- * execution when a condition fails, improving reliability and debuggability.
65
+ * Use `assert` only when a more specific assertion, such as {@link assertTrue}
66
+ * or {@link assertEqual}, does not apply. For that reason, `assert` requires a
67
+ * custom message explaining the expected condition.
26
68
  *
27
- * Do not use this instead of {@link Type}. Assertions are intended when a
28
- * condition is logically guaranteed to be true but TypeScript cannot prove it,
29
- * or for catching and signaling developer mistakes eagerly.
69
+ * Options provide structured diagnostics for custom assertions. Pass an
70
+ * underlying failure as `cause` when it explains why the asserted condition
71
+ * failed, and pass the custom assertion as `stackStartFn` to omit its
72
+ * implementation from the stack trace.
30
73
  *
31
74
  * ### Example
32
75
  *
33
76
  * ```ts
34
- * import { assert, assertEqual, assertErr, trySync } from "@evolu/common";
77
+ * import { assert, assertEqual, assertType } from "@evolu/common";
35
78
  *
36
- * assert(true, "Expected true.");
37
- * const result = trySync(() => assert(false, "Expected true."));
38
- * assertErr(result);
39
- * assert(result.error instanceof Error, "Expected an Error.");
40
- * assertEqual(result.error.message, "Expected true.");
79
+ * const value: unknown = "Evolu";
80
+ * assert(typeof value === "string", "Expected a string.");
81
+ *
82
+ * assertType<typeof value, string>();
83
+ * assertEqual(value, "Evolu");
41
84
  * ```
85
+ *
86
+ * @group Assertions
42
87
  */
43
88
  export const assert: (
44
89
  condition: unknown,
45
90
  message: string,
46
- ) => asserts condition = (condition, message) => {
91
+ options?: {
92
+ readonly actual?: unknown;
93
+ readonly expected?: unknown;
94
+ readonly operator?: string;
95
+ readonly diff?: "full" | undefined;
96
+ readonly cause?: unknown;
97
+ readonly stackStartFn?: (...args: Array<never>) => unknown;
98
+ },
99
+ ) => asserts condition = (condition, message, options = {}) => {
47
100
  // oxlint-disable-next-line typescript/strict-boolean-expressions -- JavaScript truthiness is the contract of assert.
48
- if (!condition) {
49
- throw new Error(message);
101
+ if (condition) return;
102
+
103
+ const actual = Object.hasOwn(options, "actual") ? options.actual : condition;
104
+ const expected = Object.hasOwn(options, "expected") ? options.expected : true;
105
+ const operator = options.operator ?? "==";
106
+ const stackStartFn = options.stackStartFn ?? assert;
107
+ if (NodeAssert !== undefined) {
108
+ const error = new NodeAssert.AssertionError({
109
+ message,
110
+ actual,
111
+ expected,
112
+ operator: options.diff === "full" ? "deepStrictEqual" : operator,
113
+ diff: options.diff,
114
+ stackStartFn,
115
+ });
116
+
117
+ if (options.diff === "full") {
118
+ error.operator = operator;
119
+ }
120
+
121
+ if (options.cause !== undefined) {
122
+ Object.defineProperty(error, "cause", {
123
+ configurable: true,
124
+ value: options.cause,
125
+ writable: true,
126
+ });
127
+ }
128
+
129
+ throw error;
50
130
  }
131
+
132
+ const error = Object.assign(
133
+ new Error(
134
+ message,
135
+ options.cause === undefined ? undefined : { cause: options.cause },
136
+ ),
137
+ {
138
+ name: "AssertionError",
139
+ code: "ERR_ASSERTION" as const,
140
+ actual,
141
+ expected,
142
+ generatedMessage: false,
143
+ operator,
144
+ },
145
+ );
146
+
147
+ Error.captureStackTrace?.(error, stackStartFn);
148
+
149
+ throw error;
51
150
  };
52
151
 
152
+ // Node.js generates assertion diffs inside its AssertionError constructor; the
153
+ // test runner does not derive them from actual and expected metadata alone. Use
154
+ // the native constructor when available and the portable fallback elsewhere.
155
+ const NodeAssert = globalThis.process?.getBuiltinModule?.("node:assert/strict");
156
+
53
157
  /**
54
- * Asserts that two values are the same according to {@link eqStrict}.
158
+ * Asserts that a value is exactly `true`.
55
159
  *
56
- * `eqStrict` uses SameValue equality: it considers `NaN` the same as itself,
57
- * distinguishes `0` from `-0`, and compares objects by reference identity.
160
+ * Boolean conditions preserve their control-flow narrowing, including named
161
+ * type guards. Unknown values narrow to the literal `true`. Unlike
162
+ * {@link assert}, this checks an exact boolean value instead of truthiness and
163
+ * does not require a custom message.
58
164
  *
59
165
  * ### Example
60
166
  *
61
167
  * ```ts
62
- * import { assertSame, assertType } from "@evolu/common";
168
+ * import { assertTrue, assertType } from "@evolu/common";
63
169
  *
64
170
  * interface User {
65
171
  * readonly name: string;
66
172
  * }
67
173
  *
68
- * const user: User = { name: "Ada" };
69
- * const value: unknown = user;
174
+ * const isUser = (value: unknown): value is User =>
175
+ * typeof value === "object" &&
176
+ * value !== null &&
177
+ * "name" in value &&
178
+ * typeof value.name === "string";
70
179
  *
71
- * assertSame(value, user);
72
- * assertType<User, typeof value>();
180
+ * const value: unknown = { name: "Ada" };
181
+ * assertTrue(isUser(value));
182
+ * assertType<typeof value, User>();
183
+ *
184
+ * const condition: unknown = true;
185
+ * assertTrue(condition);
186
+ * assertType<typeof condition, true>();
73
187
  * ```
188
+ *
189
+ * @group Assertions
74
190
  */
75
- export const assertSame: <Expected>(
76
- actual: unknown,
77
- expected: Expected,
78
- ) => asserts actual is Expected = (actual, expected) => {
79
- assert(eqStrict(actual, expected), "Expected values to be the same.");
80
- };
191
+ export function assertTrue(condition: boolean): asserts condition;
192
+ export function assertTrue(value: unknown): asserts value is true;
193
+ export function assertTrue(value: unknown): void {
194
+ assert(value === true, "Expected true.", {
195
+ actual: value,
196
+ expected: true,
197
+ operator: "strictEqual",
198
+ stackStartFn: assertTrue,
199
+ });
200
+ }
81
201
 
82
202
  /**
83
- * Asserts that a value is exactly `true` and narrows it to `true`.
203
+ * Asserts that a value is exactly `false` and narrows it to `false`.
84
204
  *
85
- * Unlike {@link assert}, this checks an exact boolean value instead of
86
- * truthiness and does not require a custom message.
205
+ * Unlike {@link assert}, this checks an exact boolean value instead of falsiness
206
+ * and does not require a custom message.
87
207
  *
88
208
  * ### Example
89
209
  *
90
210
  * ```ts
91
- * import { assertTrue, assertType } from "@evolu/common";
211
+ * import { assertFalse, assertType } from "@evolu/common";
92
212
  *
93
- * const value: unknown = true;
94
- * assertTrue(value);
95
- * assertType<true, typeof value>();
213
+ * const value: unknown = false;
214
+ * assertFalse(value);
215
+ * assertType<typeof value, false>();
96
216
  * ```
217
+ *
218
+ * @group Assertions
97
219
  */
98
- export const assertTrue: (value: unknown) => asserts value is true = (
220
+ export const assertFalse: (value: unknown) => asserts value is false = (
99
221
  value,
100
222
  ) => {
101
- assert(value === true, "Expected true.");
223
+ assert(value === false, "Expected false.", {
224
+ actual: value,
225
+ expected: false,
226
+ operator: "strictEqual",
227
+ stackStartFn: assertFalse,
228
+ });
102
229
  };
103
230
 
104
231
  /**
105
- * Asserts that a value is exactly `false` and narrows it to `false`.
232
+ * Asserts that a condition becomes true after exactly the specified number of
233
+ * microtasks.
106
234
  *
107
- * Unlike {@link assert}, this checks an exact boolean value instead of falsiness
108
- * and does not require a custom message.
235
+ * Use this in tests that intentionally specify async scheduling behavior.
236
+ * Application code must not depend on exact microtask counts. Maintainers
237
+ * should review count changes because they indicate that an async pipeline
238
+ * changed.
109
239
  *
110
240
  * ### Example
111
241
  *
112
242
  * ```ts
113
- * import { assertFalse, assertType } from "@evolu/common";
243
+ * import { assertConditionAfterMicrotasks } from "@evolu/common";
114
244
  *
115
- * const value: unknown = false;
116
- * assertFalse(value);
117
- * assertType<false, typeof value>();
245
+ * let ready = false;
246
+ * queueMicrotask(() => {
247
+ * ready = true;
248
+ * });
249
+ *
250
+ * await assertConditionAfterMicrotasks(() => ready, 1);
118
251
  * ```
252
+ *
253
+ * @group Assertions
119
254
  */
120
- export const assertFalse: (value: unknown) => asserts value is false = (
121
- value,
122
- ) => {
123
- assert(value === false, "Expected false.");
255
+ export const assertConditionAfterMicrotasks = async (
256
+ condition: () => boolean,
257
+ expectedMicrotaskCount: number,
258
+ ): Promise<void> => {
259
+ for (
260
+ let microtaskCount = 0;
261
+ microtaskCount < expectedMicrotaskCount;
262
+ microtaskCount++
263
+ ) {
264
+ const actual = condition();
265
+ assert(
266
+ !actual,
267
+ `Expected condition to be false after ${microtaskCount} microtasks.`,
268
+ {
269
+ actual,
270
+ expected: false,
271
+ operator: "strictEqual",
272
+ stackStartFn: assertConditionAfterMicrotasks,
273
+ },
274
+ );
275
+ await Promise.resolve();
276
+ }
277
+
278
+ const actual = condition();
279
+ assert(
280
+ actual,
281
+ `Expected condition to be true after exactly ${expectedMicrotaskCount} microtasks.`,
282
+ {
283
+ actual,
284
+ expected: true,
285
+ operator: "strictEqual",
286
+ stackStartFn: assertConditionAfterMicrotasks,
287
+ },
288
+ );
124
289
  };
125
290
 
126
291
  /**
127
- * Asserts that two {@link Data} values are equal according to {@link eqData}.
292
+ * Asserts that a function throws the expected value.
128
293
  *
129
- * Use this for concise value checks in platform-independent examples. Use
130
- * {@link assert} with a descriptive message for application invariants.
294
+ * Expected values use {@link assertEqual} semantics. To perform several or
295
+ * specialized assertions, pass an assertion function that receives the thrown
296
+ * value and returns nothing. Returning a predicate result fails. Use
297
+ * {@link assertThrowsSame} for SameValue or reference identity and
298
+ * {@link assertThrowsInstanceOf} for a runtime type.
299
+ *
300
+ * Because JavaScript permits throwing functions, a function second argument is
301
+ * always treated as an assertion function. Use `assertThrowsSame` to assert
302
+ * that a particular function was thrown.
131
303
  *
132
304
  * ### Example
133
305
  *
134
306
  * ```ts
135
- * import { assertEqual } from "@evolu/common";
307
+ * import { assertThrows } from "@evolu/common";
136
308
  *
137
- * const actual = new Map([["roles", new Set(["admin", "author"])]]);
138
- * const expected = new Map([["roles", new Set(["author", "admin"])]]);
139
- *
140
- * assertEqual(actual, expected);
309
+ * assertThrows(
310
+ * () => {
311
+ * // oxlint-disable-next-line eslint/no-throw-literal, typescript/only-throw-error -- Exercise arbitrary thrown values.
312
+ * throw { type: "ExpectedFailure", value: 42 };
313
+ * },
314
+ * { type: "ExpectedFailure", value: 42 },
315
+ * );
141
316
  * ```
317
+ *
318
+ * @group Assertions
142
319
  */
143
- export function assertEqual<Actual, Expected>(
144
- actual: Actual,
145
- expected: Expected,
146
- ...dataError: AssertEqualError<Actual | Expected>
320
+ export function assertThrows(
321
+ run: () => unknown,
322
+ assertThrown: (thrown: unknown) => void,
147
323
  ): void;
148
- export function assertEqual(actual: Data, expected: Data): void {
149
- assert(eqData(actual, expected), "Expected values to be equal.");
324
+ export function assertThrows(run: () => unknown, expected: unknown): void;
325
+ export function assertThrows(
326
+ run: () => unknown,
327
+ expectedOrAssert: unknown,
328
+ ): void {
329
+ const thrown = captureThrown(run, assertThrows);
330
+
331
+ if (typeof expectedOrAssert === "function") {
332
+ const result = (expectedOrAssert as (thrown: unknown) => unknown)(thrown);
333
+ assert(
334
+ result === undefined,
335
+ "Expected the thrown value assertion to return undefined.",
336
+ {
337
+ actual: result,
338
+ expected: undefined,
339
+ operator: "strictEqual",
340
+ stackStartFn: assertThrows,
341
+ },
342
+ );
343
+ return;
344
+ }
345
+
346
+ assert(
347
+ eqUnknown(thrown, expectedOrAssert),
348
+ "Expected the thrown value to equal the expected value.",
349
+ {
350
+ actual: thrown,
351
+ expected: expectedOrAssert,
352
+ operator: "eqData",
353
+ diff: "full",
354
+ stackStartFn: assertThrows,
355
+ },
356
+ );
150
357
  }
151
358
 
152
- type AssertEqualError<Value> =
153
- IsData<Value> extends true
154
- ? []
155
- : [
156
- error: CompileTimeError<
157
- "assertEqual",
158
- "Actual and expected values must consist only of Data."
159
- >,
160
- ];
359
+ /**
360
+ * Asserts that a function throws the same value using `Object.is`.
361
+ *
362
+ * Use this to verify that a thrown value was propagated unchanged. For value
363
+ * comparisons, use {@link assertThrows}.
364
+ *
365
+ * ### Example
366
+ *
367
+ * ```ts
368
+ * import { assertThrowsSame } from "@evolu/common";
369
+ *
370
+ * const expected = new Error("Unavailable.");
371
+ * assertThrowsSame(() => {
372
+ * throw expected;
373
+ * }, expected);
374
+ * ```
375
+ *
376
+ * @group Assertions
377
+ */
378
+ export const assertThrowsSame = (
379
+ run: () => unknown,
380
+ expected: unknown,
381
+ ): void => {
382
+ const thrown = captureThrown(run, assertThrowsSame);
383
+ assert(
384
+ eqSameValue(thrown, expected),
385
+ "Expected the thrown value to be the same as the expected value.",
386
+ {
387
+ actual: thrown,
388
+ expected,
389
+ operator: "strictEqual",
390
+ stackStartFn: assertThrowsSame,
391
+ },
392
+ );
393
+ };
161
394
 
162
395
  /**
163
- * Asserts that a {@link Result} is an {@link Ok}, optionally compares its value,
164
- * and narrows the Result.
396
+ * Asserts that a function throws an instance of a constructor.
165
397
  *
166
- * With only a Result, this checks the variant without inspecting the value.
167
- * When an expected value is provided, it uses {@link eqData}; its Data type is
168
- * inferred independently, so an unbranded literal can compare a branded
169
- * primitive or collection. Pass a custom {@link Eq} when either value is outside
170
- * {@link Data} or needs domain-specific equality.
398
+ * Returns the narrowed instance so additional properties can be asserted
399
+ * without running the function again.
171
400
  *
172
401
  * ### Example
173
402
  *
174
403
  * ```ts
175
- * import {
176
- * assertEqual,
177
- * assertOk,
178
- * assertType,
179
- * ok,
180
- * type Ok,
181
- * type Result,
182
- * type Typed,
183
- * } from "@evolu/common";
404
+ * import { assertEqual, assertThrowsInstanceOf } from "@evolu/common";
184
405
  *
185
- * interface User {
186
- * readonly id: string;
187
- * }
406
+ * const error = assertThrowsInstanceOf(() => {
407
+ * throw new TypeError("Expected a string.");
408
+ * }, TypeError);
409
+ * assertEqual(error.message, "Expected a string.");
410
+ * ```
188
411
  *
189
- * interface UserNotFoundError extends Typed<"UserNotFound"> {}
412
+ * @group Assertions
413
+ */
414
+ export const assertThrowsInstanceOf = <
415
+ T extends {
416
+ readonly name: string;
417
+ } & (abstract new (...args: Array<never>) => unknown),
418
+ >(
419
+ run: () => unknown,
420
+ constructor: T,
421
+ ): InstanceType<T> => {
422
+ const thrown = captureThrown(run, assertThrowsInstanceOf);
423
+ assert(
424
+ thrown instanceof constructor,
425
+ constructor.name === ""
426
+ ? "Expected the thrown value to be an instance of the provided constructor."
427
+ : `Expected the thrown value to be an instance of ${constructor.name}.`,
428
+ {
429
+ actual: thrown,
430
+ expected: constructor,
431
+ operator: "instanceof",
432
+ stackStartFn: assertThrowsInstanceOf,
433
+ },
434
+ );
435
+ return thrown as InstanceType<T>;
436
+ };
437
+
438
+ const captureThrown = (
439
+ run: () => unknown,
440
+ stackStartFn: (...args: Array<never>) => unknown,
441
+ ): unknown => {
442
+ let value: unknown;
443
+
444
+ try {
445
+ value = run();
446
+ } catch (error) {
447
+ return error;
448
+ }
449
+
450
+ assert(false, "Expected function to throw.", {
451
+ actual: value,
452
+ expected: "throw",
453
+ operator: "throws",
454
+ stackStartFn,
455
+ });
456
+ };
457
+
458
+ /**
459
+ * Asserts that a promise rejects with the expected value.
190
460
  *
191
- * const result: Result<User, UserNotFoundError> = ok({ id: "user-1" });
461
+ * Expected values use {@link assertEqual} semantics. To perform several or
462
+ * specialized assertions, pass an assertion function that receives the
463
+ * rejection reason and returns nothing. Returning a predicate result fails. Use
464
+ * {@link assertRejectsSame} for SameValue or reference identity and
465
+ * {@link assertRejectsInstanceOf} for a runtime type.
466
+ *
467
+ * Because JavaScript permits rejecting with functions, a function second
468
+ * argument is always treated as an assertion function. Use `assertRejectsSame`
469
+ * to assert that a particular function was rejected.
470
+ *
471
+ * ### Example
192
472
  *
193
- * assertOk(result);
194
- * assertType<Ok<User>, typeof result>();
195
- * assertEqual(result.value.id, "user-1");
473
+ * ```ts
474
+ * import { assertRejects } from "@evolu/common";
475
+ *
476
+ * const expected = new Error("Unavailable.");
477
+ * await assertRejects(Promise.reject(expected), expected);
196
478
  * ```
479
+ *
480
+ * @group Assertions
197
481
  */
198
- export function assertOk<R extends Result<unknown, unknown>>(
199
- result: R,
200
- ): asserts result is Extract<R, Ok<unknown>>;
201
- export function assertOk<R extends Result<unknown, unknown>>(
202
- result: R,
203
- expectedValue: InferOk<R>,
204
- eq: Eq<InferOk<R>>,
205
- ): asserts result is Extract<R, Ok<unknown>>;
206
- // Constrain the argument tuple so any cannot absorb the CompileTimeError.
207
- export function assertOk<R extends Result<unknown, unknown>, ExpectedValue>(
208
- result: R,
209
- ...comparison: [expectedValue: ExpectedValue] &
210
- (IsData<InferOk<R> | ExpectedValue> extends true
211
- ? unknown
212
- : {
213
- readonly [
214
- Error in CompileTimeError<
215
- "assertOk",
216
- "Result value and expected value must consist only of Data when no custom Eq is provided."
217
- >
218
- ]: never;
219
- })
220
- ): asserts result is Extract<R, Ok<unknown>>;
221
- export function assertOk(
222
- result: AnyResult,
223
- ...comparison: [] | [expectedValue: unknown, eq?: Eq<any>]
224
- ): asserts result is Ok<unknown> {
225
- assert(result.ok, "Expected an Ok result.");
226
- if (comparison.length === 0) return;
482
+ export function assertRejects(
483
+ promise: PromiseLike<unknown>,
484
+ assertRejected: (reason: unknown) => void,
485
+ ): Promise<void>;
486
+ export function assertRejects(
487
+ promise: PromiseLike<unknown>,
488
+ expected: unknown,
489
+ ): Promise<void>;
490
+ export async function assertRejects(
491
+ promise: PromiseLike<unknown>,
492
+ expectedOrAssert: unknown,
493
+ ): Promise<void> {
494
+ const rejected = await captureRejected(promise, assertRejects);
495
+
496
+ if (typeof expectedOrAssert === "function") {
497
+ const result = (expectedOrAssert as (reason: unknown) => unknown)(rejected);
498
+ assert(
499
+ result === undefined,
500
+ "Expected the rejection assertion to return undefined.",
501
+ {
502
+ actual: result,
503
+ expected: undefined,
504
+ operator: "strictEqual",
505
+ stackStartFn: assertRejects,
506
+ },
507
+ );
508
+ return;
509
+ }
227
510
 
228
- const expectedValue = comparison[0];
229
- const eq: Eq<any> = comparison[1] ?? eqData;
230
511
  assert(
231
- eq(result.value, expectedValue),
232
- "Expected the value to equal the expected value.",
512
+ eqUnknown(rejected, expectedOrAssert),
513
+ "Expected the rejection reason to equal the expected value.",
514
+ {
515
+ actual: rejected,
516
+ expected: expectedOrAssert,
517
+ operator: "eqData",
518
+ diff: "full",
519
+ stackStartFn: assertRejects,
520
+ },
233
521
  );
234
522
  }
235
523
 
236
524
  /**
237
- * Asserts that a {@link Result} is an {@link Err}, optionally compares its error,
238
- * and narrows the Result.
525
+ * Asserts that a promise rejects with the same value using `Object.is`.
239
526
  *
240
- * With only a Result, this checks the variant without inspecting the error.
241
- * When an expected error is provided, it uses {@link eqData}; its Data type is
242
- * inferred independently, so an unbranded literal can compare a branded
243
- * primitive or collection. Pass a custom {@link Eq} when either value is outside
244
- * {@link Data} or needs domain-specific equality.
527
+ * Use this to verify that a rejection reason was propagated unchanged. For
528
+ * value comparisons, use {@link assertRejects}.
245
529
  *
246
530
  * ### Example
247
531
  *
248
532
  * ```ts
249
- * import {
250
- * assertEqual,
251
- * assertErr,
252
- * assertType,
253
- * err,
254
- * type Err,
255
- * type Result,
256
- * type Typed,
257
- * } from "@evolu/common";
533
+ * import { assertRejectsSame } from "@evolu/common";
258
534
  *
259
- * interface UserNotFoundError extends Typed<"UserNotFound"> {
260
- * readonly id: string;
261
- * }
535
+ * const expected = new Error("Unavailable.");
536
+ * await assertRejectsSame(Promise.reject(expected), expected);
537
+ * ```
262
538
  *
263
- * const result: Result<string, UserNotFoundError> = err({
264
- * type: "UserNotFound",
265
- * id: "user-1",
266
- * });
539
+ * @group Assertions
540
+ */
541
+ export const assertRejectsSame = async (
542
+ promise: PromiseLike<unknown>,
543
+ expected: unknown,
544
+ ): Promise<void> => {
545
+ const rejected = await captureRejected(promise, assertRejectsSame);
546
+ assert(
547
+ eqSameValue(rejected, expected),
548
+ "Expected the rejection reason to be the same as the expected value.",
549
+ {
550
+ actual: rejected,
551
+ expected,
552
+ operator: "strictEqual",
553
+ stackStartFn: assertRejectsSame,
554
+ },
555
+ );
556
+ };
557
+
558
+ /**
559
+ * Asserts that a promise rejects with an instance of a constructor.
267
560
  *
268
- * assertErr(result);
269
- * assertType<Err<UserNotFoundError>, typeof result>();
270
- * assertEqual(result.error.id, "user-1");
561
+ * Returns the narrowed instance so additional properties can be asserted.
562
+ *
563
+ * ### Example
564
+ *
565
+ * ```ts
566
+ * import { assertEqual, assertRejectsInstanceOf } from "@evolu/common";
567
+ *
568
+ * const error = await assertRejectsInstanceOf(
569
+ * Promise.reject(new TypeError("Unavailable.")),
570
+ * TypeError,
571
+ * );
572
+ * assertEqual(error.message, "Unavailable.");
271
573
  * ```
574
+ *
575
+ * @group Assertions
272
576
  */
273
- export function assertErr<R extends Result<unknown, unknown>>(
274
- result: R,
275
- ): asserts result is Extract<R, Err<unknown>>;
276
- export function assertErr<R extends Result<unknown, unknown>>(
277
- result: R,
278
- expectedError: InferErr<R>,
279
- eq: Eq<InferErr<R>>,
280
- ): asserts result is Extract<R, Err<unknown>>;
281
- // Constrain the argument tuple so any cannot absorb the CompileTimeError.
282
- export function assertErr<R extends Result<unknown, unknown>, ExpectedError>(
283
- result: R,
284
- ...comparison: [expectedError: ExpectedError] &
285
- (IsData<InferErr<R> | ExpectedError> extends true
286
- ? unknown
287
- : {
288
- readonly [
289
- Error in CompileTimeError<
290
- "assertErr",
291
- "Result error and expected error must consist only of Data when no custom Eq is provided."
292
- >
293
- ]: never;
294
- })
295
- ): asserts result is Extract<R, Err<unknown>>;
296
- export function assertErr(
297
- result: AnyResult,
298
- ...comparison: [] | [expectedError: unknown, eq?: Eq<any>]
299
- ): asserts result is Err<unknown> {
300
- assert(!result.ok, "Expected an Err result.");
301
- if (comparison.length === 0) return;
577
+ export const assertRejectsInstanceOf = async <
578
+ T extends {
579
+ readonly name: string;
580
+ } & (abstract new (...args: Array<never>) => unknown),
581
+ >(
582
+ promise: PromiseLike<unknown>,
583
+ constructor: T,
584
+ ): Promise<InstanceType<T>> => {
585
+ const rejected = await captureRejected(promise, assertRejectsInstanceOf);
586
+ assert(
587
+ rejected instanceof constructor,
588
+ constructor.name === ""
589
+ ? "Expected the rejection reason to be an instance of the provided constructor."
590
+ : `Expected the rejection reason to be an instance of ${constructor.name}.`,
591
+ {
592
+ actual: rejected,
593
+ expected: constructor,
594
+ operator: "instanceof",
595
+ stackStartFn: assertRejectsInstanceOf,
596
+ },
597
+ );
598
+ return rejected as InstanceType<T>;
599
+ };
302
600
 
303
- const expectedError = comparison[0];
304
- const eq: Eq<any> = comparison[1] ?? eqData;
601
+ const captureRejected = async (
602
+ promise: PromiseLike<unknown>,
603
+ stackStartFn: (...args: Array<never>) => unknown,
604
+ ): Promise<unknown> => {
605
+ let value: unknown;
606
+
607
+ try {
608
+ value = await promise;
609
+ } catch (error) {
610
+ return error;
611
+ }
612
+
613
+ assert(false, "Expected promise to reject.", {
614
+ actual: value,
615
+ expected: "rejection",
616
+ operator: "rejects",
617
+ stackStartFn,
618
+ });
619
+ };
620
+
621
+ /**
622
+ * Asserts that a value is an instance of a constructor and narrows it.
623
+ *
624
+ * ### Example
625
+ *
626
+ * ```ts
627
+ * import { assertInstanceOf, assertType } from "@evolu/common";
628
+ *
629
+ * const value: unknown = new TypeError("Invalid value.");
630
+ * assertInstanceOf(value, TypeError);
631
+ * assertType<typeof value, TypeError>();
632
+ * ```
633
+ *
634
+ * @group Assertions
635
+ */
636
+ export const assertInstanceOf: <
637
+ Constructor extends {
638
+ readonly name: string;
639
+ } & (abstract new (...args: Array<never>) => unknown),
640
+ >(
641
+ value: unknown,
642
+ constructor: Constructor,
643
+ ) => asserts value is InstanceType<Constructor> = (value, constructor) => {
305
644
  assert(
306
- eq(result.error, expectedError),
307
- "Expected the error to equal the expected error.",
645
+ value instanceof constructor,
646
+ constructor.name === ""
647
+ ? "Expected an instance of the provided constructor."
648
+ : `Expected an instance of ${constructor.name}.`,
649
+ {
650
+ actual: value,
651
+ expected: constructor,
652
+ operator: "instanceof",
653
+ stackStartFn: assertInstanceOf,
654
+ },
308
655
  );
309
- }
656
+ };
310
657
 
311
658
  /**
312
659
  * Asserts that a value is non-nullable.
313
660
  *
314
661
  * Following TypeScript's {@link NonNullable}, non-nullable here means neither
315
- * null nor undefined. Use this when a value is logically guaranteed to be
316
- * non-nullable but TypeScript cannot prove it.
662
+ * null nor undefined.
663
+ *
664
+ * @group Assertions
317
665
  */
318
666
  export const assertNonNullable: <T>(
319
667
  value: T,
@@ -322,22 +670,24 @@ export const assertNonNullable: <T>(
322
670
  value,
323
671
  message = "Expected value to be non-nullable.",
324
672
  ) => {
325
- assert(value != null, message);
673
+ assert(value != null, message, {
674
+ actual: value,
675
+ expected: null,
676
+ operator: "!=",
677
+ stackStartFn: assertNonNullable,
678
+ });
326
679
  };
327
680
 
328
681
  /**
329
682
  * Asserts that a value is not null while preserving undefined.
330
683
  *
331
- * Use this when a value is logically guaranteed not to be null but TypeScript
332
- * cannot prove it.
333
- *
334
684
  * ### Example
335
685
  *
336
686
  * ```ts
337
687
  * import {
338
- * assert,
339
688
  * assertEqual,
340
689
  * assertErr,
690
+ * assertInstanceOf,
341
691
  * assertNotNull,
342
692
  * assertType,
343
693
  * trySync,
@@ -345,13 +695,17 @@ export const assertNonNullable: <T>(
345
695
  *
346
696
  * const value = undefined as string | null | undefined;
347
697
  * assertNotNull(value);
348
- * assertType<string | undefined, typeof value>();
698
+ *
699
+ * assertType<typeof value, string | undefined>();
349
700
  * assertEqual(value, undefined);
701
+ *
350
702
  * const result = trySync(() => assertNotNull(null));
351
703
  * assertErr(result);
352
- * assert(result.error instanceof Error, "Expected an Error.");
704
+ * assertInstanceOf(result.error, Error);
353
705
  * assertEqual(result.error.message, "Expected value not to be null.");
354
706
  * ```
707
+ *
708
+ * @group Assertions
355
709
  */
356
710
  export const assertNotNull: <T>(
357
711
  value: T,
@@ -360,22 +714,24 @@ export const assertNotNull: <T>(
360
714
  value,
361
715
  message = "Expected value not to be null.",
362
716
  ) => {
363
- assert(value !== null, message);
717
+ assert(value !== null, message, {
718
+ actual: value,
719
+ expected: null,
720
+ operator: "notStrictEqual",
721
+ stackStartFn: assertNotNull,
722
+ });
364
723
  };
365
724
 
366
725
  /**
367
726
  * Asserts that a value is not undefined while preserving null.
368
727
  *
369
- * Use this when a value is logically guaranteed not to be undefined but
370
- * TypeScript cannot prove it.
371
- *
372
728
  * ### Example
373
729
  *
374
730
  * ```ts
375
731
  * import {
376
- * assert,
377
732
  * assertEqual,
378
733
  * assertErr,
734
+ * assertInstanceOf,
379
735
  * assertNotUndefined,
380
736
  * assertType,
381
737
  * trySync,
@@ -383,13 +739,15 @@ export const assertNotNull: <T>(
383
739
  *
384
740
  * const value = null as string | null | undefined;
385
741
  * assertNotUndefined(value);
386
- * assertType<string | null, typeof value>();
742
+ * assertType<typeof value, string | null>();
387
743
  * assertEqual(value, null);
388
744
  * const result = trySync(() => assertNotUndefined(undefined));
389
745
  * assertErr(result);
390
- * assert(result.error instanceof Error, "Expected an Error.");
746
+ * assertInstanceOf(result.error, Error);
391
747
  * assertEqual(result.error.message, "Expected value not to be undefined.");
392
748
  * ```
749
+ *
750
+ * @group Assertions
393
751
  */
394
752
  export const assertNotUndefined: <T>(
395
753
  value: T,
@@ -398,14 +756,53 @@ export const assertNotUndefined: <T>(
398
756
  value,
399
757
  message = "Expected value not to be undefined.",
400
758
  ) => {
401
- assert(value !== undefined, message);
759
+ assert(value !== undefined, message, {
760
+ actual: value,
761
+ expected: undefined,
762
+ operator: "notStrictEqual",
763
+ stackStartFn: assertNotUndefined,
764
+ });
402
765
  };
403
766
 
404
767
  /**
405
- * Asserts that an array is non-empty.
768
+ * Asserts that a value has the expected length and narrows its length.
406
769
  *
407
- * Use this when an array is logically guaranteed to be non-empty but TypeScript
408
- * cannot prove it.
770
+ * ### Example
771
+ *
772
+ * ```ts
773
+ * import { assertLength, assertType } from "@evolu/common";
774
+ *
775
+ * const values: ReadonlyArray<string> = ["Ada", "Grace"];
776
+ * assertLength(values, 2);
777
+ * assertType<typeof values.length, 2>();
778
+ * ```
779
+ *
780
+ * @group Assertions
781
+ */
782
+ export const assertLength: <
783
+ Value extends ValueWithLength,
784
+ const Length extends number,
785
+ >(
786
+ value: Value,
787
+ expectedLength: Length,
788
+ ) => asserts value is Value & { readonly length: Length } = (
789
+ value,
790
+ expectedLength,
791
+ ) => {
792
+ assert(
793
+ eqSameValue(value.length, expectedLength),
794
+ `Expected value to have length ${expectedLength}.`,
795
+ {
796
+ actual: value.length,
797
+ expected: expectedLength,
798
+ operator: "strictEqual",
799
+ stackStartFn: assertLength,
800
+ },
801
+ );
802
+ };
803
+
804
+ /**
805
+ * Asserts that an array is non-empty.
409
806
  *
410
807
  * ### Example
411
808
  *
@@ -421,11 +818,13 @@ export const assertNotUndefined: <T>(
421
818
  *
422
819
  * const values = [1, 2, 3];
423
820
  * assertNonEmptyArray(values);
424
- * assertType<NonEmptyArray<number>, typeof values>();
821
+ * assertType<typeof values, NonEmptyArray<number>>();
425
822
  * assertEqual(values[0], 1);
426
823
  * const result = trySync(() => assertNonEmptyArray([]));
427
824
  * assertErr(result);
428
825
  * ```
826
+ *
827
+ * @group Assertions
429
828
  */
430
829
  export const assertNonEmptyArray: <T>(
431
830
  arr: Array<T>,
@@ -434,15 +833,17 @@ export const assertNonEmptyArray: <T>(
434
833
  arr,
435
834
  message = "Expected a non-empty array.",
436
835
  ) => {
437
- assert(arr.length > 0, message);
836
+ assert(arr.length > 0, message, {
837
+ actual: arr.length,
838
+ expected: 0,
839
+ operator: ">",
840
+ stackStartFn: assertNonEmptyArray,
841
+ });
438
842
  };
439
843
 
440
844
  /**
441
845
  * Asserts that a readonly array is non-empty.
442
846
  *
443
- * Use this when a readonly array is logically guaranteed to be non-empty but
444
- * TypeScript cannot prove it.
445
- *
446
847
  * ### Example
447
848
  *
448
849
  * ```ts
@@ -457,11 +858,13 @@ export const assertNonEmptyArray: <T>(
457
858
  *
458
859
  * const values: ReadonlyArray<number> = [1, 2, 3];
459
860
  * assertNonEmptyReadonlyArray(values);
460
- * assertType<NonEmptyReadonlyArray<number>, typeof values>();
861
+ * assertType<typeof values, NonEmptyReadonlyArray<number>>();
461
862
  * assertEqual(values[0], 1);
462
863
  * const result = trySync(() => assertNonEmptyReadonlyArray([]));
463
864
  * assertErr(result);
464
865
  * ```
866
+ *
867
+ * @group Assertions
465
868
  */
466
869
  export const assertNonEmptyReadonlyArray: <T>(
467
870
  arr: ReadonlyArray<T>,
@@ -470,7 +873,12 @@ export const assertNonEmptyReadonlyArray: <T>(
470
873
  arr,
471
874
  message = "Expected a non-empty readonly array.",
472
875
  ) => {
473
- assert(arr.length > 0, message);
876
+ assert(arr.length > 0, message, {
877
+ actual: arr.length,
878
+ expected: 0,
879
+ operator: ">",
880
+ stackStartFn: assertNonEmptyReadonlyArray,
881
+ });
474
882
  };
475
883
 
476
884
  /**
@@ -482,30 +890,366 @@ export const assertNonEmptyReadonlyArray: <T>(
482
890
  *
483
891
  * ```ts
484
892
  * import {
485
- * assert,
486
893
  * assertEqual,
487
894
  * assertErr,
895
+ * assertInstanceOf,
488
896
  * assertNotDisposed,
489
897
  * trySync,
490
898
  * } from "@evolu/common";
491
899
  *
492
- * await using disposer = new globalThis.AsyncDisposableStack();
900
+ * await using disposer = new AsyncDisposableStack();
493
901
  * assertNotDisposed(disposer);
494
902
  * await disposer.disposeAsync();
495
903
  * const result = trySync(() => assertNotDisposed(disposer));
496
904
  * assertErr(result);
497
- * assert(result.error instanceof Error, "Expected an Error.");
905
+ * assertInstanceOf(result.error, Error);
498
906
  * assertEqual(result.error.message, "Cannot use a disposed object.");
499
907
  * ```
500
908
  *
501
909
  * This is the JavaScript equivalent of the .NET `ObjectDisposedException`
502
910
  * pattern: once a helper has been disposed, calling its synchronous methods is
503
- * a programmer error and should throw immediately instead of continuing with
504
- * invalid state.
911
+ * a bug and should throw immediately instead of continuing with invalid state.
912
+ *
913
+ * @group Assertions
505
914
  */
506
915
  export const assertNotDisposed = (
507
916
  value:
508
917
  DisposableStack | AsyncDisposableStack | { readonly disposed: boolean },
509
918
  ): void => {
510
- assert(!value.disposed, "Cannot use a disposed object.");
919
+ assert(!value.disposed, "Cannot use a disposed object.", {
920
+ actual: value.disposed,
921
+ expected: false,
922
+ operator: "==",
923
+ stackStartFn: assertNotDisposed,
924
+ });
925
+ };
926
+
927
+ /**
928
+ * Asserts that two values are the same using `Object.is`.
929
+ *
930
+ * Use this when exact sameness is the contract, such as asserting reference
931
+ * identity or narrowing the actual value to the expected value's type, as shown
932
+ * below. For value comparisons, use {@link assertEqual}.
933
+ *
934
+ * Uses the same equality semantics as `assert.strictEqual` from
935
+ * `node:assert/strict`, but is platform-agnostic. See {@link eqSameValue} for
936
+ * the equality semantics.
937
+ *
938
+ * ### Example
939
+ *
940
+ * ```ts
941
+ * import { assertSame, assertType } from "@evolu/common";
942
+ *
943
+ * interface User {
944
+ * readonly name: string;
945
+ * }
946
+ *
947
+ * const user: User = { name: "Ada" };
948
+ * const value: unknown = user;
949
+ *
950
+ * assertSame(value, user);
951
+ * // `assertSame` narrows `value` from `unknown` to `User`.
952
+ * assertType<typeof value, User>();
953
+ * ```
954
+ *
955
+ * @group Assertions
956
+ */
957
+ export const assertSame: <Expected>(
958
+ actual: unknown,
959
+ expected: Expected,
960
+ ) => asserts actual is Expected = (actual, expected) => {
961
+ // `Object.is` implements ECMAScript SameValue, whereas strict equality means
962
+ // `===`. Keep the platform-agnostic "same" wording instead of reproducing
963
+ // Node.js's value-dependent messages, which sometimes say "strictly equal".
964
+ assert(eqSameValue(actual, expected), "Expected values to be the same.", {
965
+ actual,
966
+ expected,
967
+ operator: "strictEqual",
968
+ stackStartFn: assertSame,
969
+ });
511
970
  };
971
+
972
+ /**
973
+ * Asserts that two values are not the same using `Object.is`.
974
+ *
975
+ * The opposite of {@link assertSame}.
976
+ *
977
+ * ### Example
978
+ *
979
+ * ```ts
980
+ * import { assertNotSame } from "@evolu/common";
981
+ *
982
+ * const first = { name: "Ada" };
983
+ * const second = { name: "Ada" };
984
+ *
985
+ * assertNotSame(first, second);
986
+ * assertNotSame(0, -0);
987
+ * ```
988
+ *
989
+ * @group Assertions
990
+ */
991
+ export const assertNotSame = (actual: unknown, expected: unknown): void => {
992
+ // `Object.is` implements ECMAScript SameValue, whereas strict equality means
993
+ // `===`. Keep the platform-agnostic "same" wording instead of reproducing
994
+ // Node.js's value-dependent messages, which sometimes say "strictly equal".
995
+ assert(
996
+ !eqSameValue(actual, expected),
997
+ "Expected values not to be the same.",
998
+ {
999
+ actual,
1000
+ expected,
1001
+ operator: "notStrictEqual",
1002
+ stackStartFn: assertNotSame,
1003
+ },
1004
+ );
1005
+ };
1006
+
1007
+ // oxlint-disable eslint/func-style -- Function declarations make TypeDoc categorize these APIs as functions.
1008
+
1009
+ /**
1010
+ * Asserts that two values are equal.
1011
+ *
1012
+ * Use this for value comparisons of both primitive and deeply structured
1013
+ * values. Use {@link assertSame} when exact sameness or narrowing is required.
1014
+ *
1015
+ * Like `assert.deepEqual` from `node:assert/strict`, it performs deep
1016
+ * structural comparison, but uses deliberately smaller, platform-agnostic
1017
+ * equality logic based on {@link eqData}. More complex comparison logic is
1018
+ * difficult to reason about and rarely needed. Other values are opaque and
1019
+ * compare equal only by identity. If broader comparison semantics are needed in
1020
+ * Node.js, use the native assertion instead.
1021
+ *
1022
+ * ### Example
1023
+ *
1024
+ * ```ts
1025
+ * import { assertEqual } from "@evolu/common";
1026
+ *
1027
+ * assertEqual(21 * 2, 42);
1028
+ *
1029
+ * const actual: unknown = new Map([
1030
+ * ["roles", new Set(["admin", "author"])],
1031
+ * ]);
1032
+ * const expected = new Map([["roles", new Set(["author", "admin"])]]);
1033
+ *
1034
+ * assertEqual(actual, expected);
1035
+ * ```
1036
+ *
1037
+ * @group Assertions
1038
+ */
1039
+ export function assertEqual(actual: unknown, expected: unknown): void {
1040
+ assert(eqUnknown(actual, expected), "Expected values to be equal.", {
1041
+ actual,
1042
+ expected,
1043
+ operator: "eqData",
1044
+ diff: "full",
1045
+ stackStartFn: assertEqual,
1046
+ });
1047
+ }
1048
+
1049
+ /**
1050
+ * Asserts that a `Uint8Array` contains the expected bytes.
1051
+ *
1052
+ * Uses {@link eqArrayNumber}, so the runtime representations may differ. For
1053
+ * example, a `Uint8Array` can be compared directly with a regular array.
1054
+ *
1055
+ * ### Example
1056
+ *
1057
+ * ```ts
1058
+ * import { assertEqualBytes } from "@evolu/common";
1059
+ *
1060
+ * const bytes = new Uint8Array([1, 5, 39, 254]);
1061
+ *
1062
+ * assertEqualBytes(bytes, [1, 5, 39, 254]);
1063
+ * ```
1064
+ *
1065
+ * @group Assertions
1066
+ */
1067
+ export const assertEqualBytes = (
1068
+ actual: Uint8Array,
1069
+ expected: ArrayLike<number>,
1070
+ ): void => {
1071
+ assert(eqArrayNumber(actual, expected), "Expected bytes to be equal.", {
1072
+ actual,
1073
+ expected,
1074
+ operator: "eqArrayNumber",
1075
+ diff: "full",
1076
+ stackStartFn: assertEqualBytes,
1077
+ });
1078
+ };
1079
+
1080
+ /**
1081
+ * Asserts that two values are not equal.
1082
+ *
1083
+ * The opposite of {@link assertEqual}.
1084
+ *
1085
+ * ### Example
1086
+ *
1087
+ * ```ts
1088
+ * import { assertNotEqual } from "@evolu/common";
1089
+ *
1090
+ * assertNotEqual({ name: "Ada" }, { name: "Grace" });
1091
+ * assertNotEqual([1, 2], [2, 1]);
1092
+ * ```
1093
+ *
1094
+ * @group Assertions
1095
+ */
1096
+ export function assertNotEqual(actual: unknown, expected: unknown): void {
1097
+ assert(!eqUnknown(actual, expected), "Expected values not to be equal.", {
1098
+ actual,
1099
+ expected,
1100
+ operator: "notEqData",
1101
+ stackStartFn: assertNotEqual,
1102
+ });
1103
+ }
1104
+
1105
+ // oxlint-enable eslint/func-style
1106
+
1107
+ // Assertion equality extends eqData to unknown values. The comparator's
1108
+ // SameValue fast path treats identical opaque values as equal, while its
1109
+ // unsupported branches make distinct opaque values unequal.
1110
+ const eqUnknown = eqData as Eq<unknown>;
1111
+
1112
+ /**
1113
+ * Asserts that a {@link Result} is an {@link Ok} and narrows it, optionally
1114
+ * comparing its value.
1115
+ *
1116
+ * When an expected value is provided, it is compared using {@link assertEqual}
1117
+ * semantics by default. Pass a custom {@link Eq} for different equality
1118
+ * semantics.
1119
+ *
1120
+ * ### Example
1121
+ *
1122
+ * ```ts
1123
+ * import {
1124
+ * assertOk,
1125
+ * assertType,
1126
+ * ok,
1127
+ * type Ok,
1128
+ * type Result,
1129
+ * type Typed,
1130
+ * } from "@evolu/common";
1131
+ *
1132
+ * interface User {
1133
+ * readonly id: string;
1134
+ * }
1135
+ *
1136
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {}
1137
+ *
1138
+ * const result: Result<User, UserNotFoundError> = ok({ id: "user-1" });
1139
+ *
1140
+ * assertOk(result, { id: "user-1" });
1141
+ * assertType<typeof result, Ok<User>>();
1142
+ * ```
1143
+ *
1144
+ * @group Assertions
1145
+ */
1146
+ export function assertOk<R extends Result<unknown, unknown>>(
1147
+ result: R,
1148
+ ): asserts result is Extract<R, Ok<unknown>>;
1149
+ export function assertOk<R extends Result<unknown, unknown>>(
1150
+ result: R,
1151
+ expectedValue: InferOk<R>,
1152
+ eq: Eq<InferOk<R>>,
1153
+ ): asserts result is Extract<R, Ok<unknown>>;
1154
+ export function assertOk<R extends Result<unknown, unknown>>(
1155
+ result: R,
1156
+ expectedValue: unknown,
1157
+ ): asserts result is Extract<R, Ok<unknown>>;
1158
+ export function assertOk(
1159
+ result: AnyResult,
1160
+ ...comparison: [] | [expectedValue: unknown, eq?: Eq<any>]
1161
+ ): asserts result is Ok<unknown> {
1162
+ assert(result.ok, "Expected an Ok result.", {
1163
+ actual: result.ok,
1164
+ expected: true,
1165
+ operator: "strictEqual",
1166
+ stackStartFn: assertOk,
1167
+ });
1168
+ if (comparison.length === 0) return;
1169
+
1170
+ const expectedValue = comparison[0];
1171
+ assert(
1172
+ (comparison[1] ?? eqUnknown)(result.value, expectedValue),
1173
+ "Expected the value to equal the expected value.",
1174
+ {
1175
+ actual: result.value,
1176
+ expected: expectedValue,
1177
+ operator: comparison[1] === undefined ? "eqData" : "Eq",
1178
+ diff: comparison[1] === undefined ? "full" : undefined,
1179
+ stackStartFn: assertOk,
1180
+ },
1181
+ );
1182
+ }
1183
+
1184
+ /**
1185
+ * Asserts that a {@link Result} is an {@link Err} and narrows it, optionally
1186
+ * comparing its error.
1187
+ *
1188
+ * When an expected error is provided, it is compared using {@link assertEqual}
1189
+ * semantics by default. Pass a custom {@link Eq} for different equality
1190
+ * semantics.
1191
+ *
1192
+ * ### Example
1193
+ *
1194
+ * ```ts
1195
+ * import {
1196
+ * assertErr,
1197
+ * assertType,
1198
+ * err,
1199
+ * type Err,
1200
+ * type Result,
1201
+ * type Typed,
1202
+ * } from "@evolu/common";
1203
+ *
1204
+ * interface UserNotFoundError extends Typed<"UserNotFound"> {
1205
+ * readonly id: string;
1206
+ * }
1207
+ *
1208
+ * const result: Result<string, UserNotFoundError> = err({
1209
+ * type: "UserNotFound",
1210
+ * id: "user-1",
1211
+ * });
1212
+ *
1213
+ * assertErr(result, { type: "UserNotFound", id: "user-1" });
1214
+ * assertType<typeof result, Err<UserNotFoundError>>();
1215
+ * ```
1216
+ *
1217
+ * @group Assertions
1218
+ */
1219
+ export function assertErr<R extends Result<unknown, unknown>>(
1220
+ result: R,
1221
+ ): asserts result is Extract<R, Err<unknown>>;
1222
+ export function assertErr<R extends Result<unknown, unknown>>(
1223
+ result: R,
1224
+ expectedError: InferErr<R>,
1225
+ eq: Eq<InferErr<R>>,
1226
+ ): asserts result is Extract<R, Err<unknown>>;
1227
+ export function assertErr<R extends Result<unknown, unknown>>(
1228
+ result: R,
1229
+ expectedError: unknown,
1230
+ ): asserts result is Extract<R, Err<unknown>>;
1231
+ export function assertErr(
1232
+ result: AnyResult,
1233
+ ...comparison: [] | [expectedError: unknown, eq?: Eq<any>]
1234
+ ): asserts result is Err<unknown> {
1235
+ assert(!result.ok, "Expected an Err result.", {
1236
+ actual: result.ok,
1237
+ expected: false,
1238
+ operator: "strictEqual",
1239
+ stackStartFn: assertErr,
1240
+ });
1241
+ if (comparison.length === 0) return;
1242
+
1243
+ const expectedError = comparison[0];
1244
+ assert(
1245
+ (comparison[1] ?? eqUnknown)(result.error, expectedError),
1246
+ "Expected the error to equal the expected error.",
1247
+ {
1248
+ actual: result.error,
1249
+ expected: expectedError,
1250
+ operator: comparison[1] === undefined ? "eqData" : "Eq",
1251
+ diff: comparison[1] === undefined ? "full" : undefined,
1252
+ stackStartFn: assertErr,
1253
+ },
1254
+ );
1255
+ }