@evolu/common 8.0.0-next.4 → 8.0.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 (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
package/src/Array.ts CHANGED
@@ -20,8 +20,9 @@
20
20
  *
21
21
  * const scores = [3, 1, 2];
22
22
  * const leaderboard = sortScores(scores);
23
- * leaderboard; // [1, 2, 3]
24
- * scores; // [1, 2, 3] — original order lost!
23
+ * expect(leaderboard).toEqual([1, 2, 3]);
24
+ * expect(scores).toEqual([1, 2, 3]);
25
+ * expect(leaderboard).toBe(scores);
25
26
  * ```
26
27
  *
27
28
  * Imagine every method doing that.
@@ -30,71 +31,50 @@
30
31
  * instead:
31
32
  *
32
33
  * ```ts
34
+ * import { sortArray } from "@evolu/common";
35
+ *
33
36
  * const sortScores = (arr: ReadonlyArray<number>) =>
34
37
  * sortArray(arr, (a, b) => a - b);
35
38
  *
36
39
  * const scores: ReadonlyArray<number> = [3, 1, 2];
37
40
  * const leaderboard = sortScores(scores);
38
- * leaderboard; // [1, 2, 3]
39
- * scores; // [3, 1, 2] — safe!
41
+ * expect(leaderboard).toEqual([1, 2, 3]);
42
+ * expect(scores).toEqual([3, 1, 2]);
43
+ * expect(leaderboard).not.toBe(scores);
40
44
  * ```
41
45
  *
42
46
  * Even better, require a {@link NonEmptyReadonlyArray} — there's nothing to sort
43
47
  * if the array is empty anyway:
44
48
  *
45
49
  * ```ts
50
+ * import { sortArray, type NonEmptyReadonlyArray } from "@evolu/common";
51
+ *
46
52
  * const sortScores = (arr: NonEmptyReadonlyArray<number>) =>
47
53
  * sortArray(arr, (a, b) => a - b);
54
+ *
55
+ * const leaderboard = sortScores([3, 1, 2]);
56
+ * expect(leaderboard).toEqual([1, 2, 3]);
57
+ * expectTypeOf(leaderboard).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
48
58
  * ```
49
59
  *
50
60
  * Sorting an empty array isn't expensive, but functions can have side effects
51
61
  * like database queries or network requests. Using non-empty arrays whenever
52
62
  * possible is a good convention.
53
63
  *
54
- * For performance-critical cases where mutation is needed, Evolu provides
55
- * {@link shiftFromArray} and {@link popFromArray} — but only because they improve
56
- * type safety by returning a guaranteed `T` rather than an optional value.
57
- *
58
64
  * ### When to use native methods
59
65
  *
60
66
  * These helpers only exist where they add type-level value. Native methods like
61
67
  * `find`, `some`, `every`, `includes`, `indexOf`, and `findIndex` work well on
62
68
  * readonly arrays without mutation — use them directly.
63
69
  *
64
- * ## Examples
65
- *
66
70
  * ```ts
67
- * // Types - compile-time guarantee of at least one element
68
- * const _valid: NonEmptyReadonlyArray<number> = [1, 2, 3];
69
- * // ts-expect-error - empty array is not a valid NonEmptyReadonlyArray
70
- * const _invalid: NonEmptyReadonlyArray<number> = [];
71
- *
72
- * // Type guards
73
- * const arr: ReadonlyArray<number> = [1, 2, 3];
74
- * if (isNonEmptyArray(arr)) {
75
- * firstInArray(arr);
76
- * }
77
- *
78
- * // Transformations
79
- * const appended = appendToArray([1, 2, 3], 4); // [1, 2, 3, 4]
80
- * const prepended = prependToArray([2, 3], 1); // [1, 2, 3]
81
- * const readonly: ReadonlyArray<number> = [1, 2, 3];
82
- * const mapped = mapArray(readonly, (x) => x * 2); // [2, 4, 6]
83
- * const filtered = filterArray(readonly, (x) => x > 1); // [2, 3]
84
- * const deduped = dedupeArray([1, 2, 1, 3, 2]); // [1, 2, 3]
85
- * const [evens, odds] = partitionArray(
86
- * [1, 2, 3, 4, 5],
87
- * (x) => x % 2 === 0,
88
- * );
71
+ * import { type NonEmptyReadonlyArray } from "@evolu/common";
89
72
  *
90
- * // Accessors
91
- * const first = firstInArray(["a", "b", "c"]); // "a"
92
- * const last = lastInArray(["a", "b", "c"]); // "c"
73
+ * const valid: NonEmptyReadonlyArray<number> = [1, 2, 3];
74
+ * // @ts-expect-error An empty array is not non-empty.
75
+ * const invalid: NonEmptyReadonlyArray<number> = [];
93
76
  *
94
- * // Mutations
95
- * const mutable: NonEmptyArray<number> = [1, 2, 3];
96
- * shiftFromArray(mutable); // 1 (guaranteed to exist)
97
- * mutable; // [2, 3]
77
+ * expect(valid.find((value) => value === 2)).toBe(2);
98
78
  * ```
99
79
  *
100
80
  * ## Composition
@@ -103,16 +83,51 @@
103
83
  * because it's natural for single operations:
104
84
  *
105
85
  * ```ts
86
+ * import { mapArray } from "@evolu/common";
87
+ *
88
+ * interface Message {
89
+ * readonly timestamp: number;
90
+ * }
91
+ *
92
+ * const messages: ReadonlyArray<Message> = [
93
+ * { timestamp: 10 },
94
+ * { timestamp: 20 },
95
+ * ];
106
96
  * const timestamps = mapArray(messages, (m) => m.timestamp);
97
+ * expect(timestamps).toEqual([10, 20]);
107
98
  * ```
108
99
  *
109
100
  * Data-first style also reads well for a few operations, often fitting on a
110
101
  * line:
111
102
  *
112
103
  * ```ts
113
- * const cheapest = firstInArray(sortArray(prices, orderNumber));
104
+ * import {
105
+ * dedupeArray,
106
+ * filterArray,
107
+ * firstInArray,
108
+ * isNonEmptyArray,
109
+ * lastInArray,
110
+ * mapArray,
111
+ * orderNumber,
112
+ * sortArray,
113
+ * } from "@evolu/common";
114
+ *
115
+ * const cheapest = firstInArray(sortArray([30, 10, 20], orderNumber));
116
+ * expect(cheapest).toBe(10);
117
+ *
118
+ * const users = [{ name: "Ada" }, { name: "Linus" }, { name: "Ada" }];
114
119
  * const uniqueNames = dedupeArray(mapArray(users, (u) => u.name));
115
- * const latestDone = lastInArray(filterArray(jobs, isCompletedJob));
120
+ * expect(uniqueNames).toEqual(["Ada", "Linus"]);
121
+ *
122
+ * const jobs = [
123
+ * { id: 1, done: false },
124
+ * { id: 2, done: true },
125
+ * ];
126
+ * const completedJobs = filterArray(jobs, (job) => job.done);
127
+ * if (!isNonEmptyArray(completedJobs)) throw new Error("Expected a job");
128
+ * const latestDone = lastInArray(completedJobs);
129
+ *
130
+ * expect(latestDone).toEqual({ id: 2, done: true });
116
131
  * ```
117
132
  *
118
133
  * For more operations, create a function like `getOldestActiveUser` or a
@@ -128,8 +143,7 @@
128
143
  * @module
129
144
  */
130
145
 
131
- import { identity } from "./Function.js";
132
- import type { PredicateWithIndex, RefinementWithIndex } from "./Types.js";
146
+ import { identity } from "./Function.ts";
133
147
 
134
148
  /**
135
149
  * An array with at least one element.
@@ -145,6 +159,13 @@ export type NonEmptyArray<T> = [T, ...Array<T>];
145
159
  */
146
160
  export type NonEmptyReadonlyArray<T> = readonly [T, ...ReadonlyArray<T>];
147
161
 
162
+ /**
163
+ * A readonly array with at least two elements.
164
+ *
165
+ * @group Types
166
+ */
167
+ export type AtLeastTwoReadonlyArray<T> = readonly [T, T, ...ReadonlyArray<T>];
168
+
148
169
  /**
149
170
  * An empty readonly array.
150
171
  *
@@ -160,14 +181,21 @@ export const emptyArray: ReadonlyArray<never> = [];
160
181
  * - Returns readonly arrays
161
182
  * - Accepts length directly: `arrayFrom(3, fn)` instead of `Array.from({ length:
162
183
  * 3 }, fn)`
163
- * - Skips copying if iterable is already an array (safe because readonly)
184
+ * - Returns existing arrays unchanged
164
185
  *
165
- * ### Example
186
+ * ### Creating from iterables and lengths
166
187
  *
167
188
  * ```ts
168
- * arrayFrom(new Set([1, 2, 3])); // ReadonlyArray<number>
169
- * arrayFrom(3, (i) => i * 10); // [0, 10, 20]
170
- * arrayFrom(iterableMaybeArray); // no unnecessary copy
189
+ * import { arrayFrom } from "@evolu/common";
190
+ *
191
+ * const fromSet = arrayFrom(new Set([1, 2, 3]));
192
+ * expect(fromSet).toEqual([1, 2, 3]);
193
+ * expectTypeOf(fromSet).toEqualTypeOf<ReadonlyArray<number>>();
194
+ *
195
+ * expect(arrayFrom(3, (i) => i * 10)).toEqual([0, 10, 20]);
196
+ *
197
+ * const existing: ReadonlyArray<number> = [1, 2, 3];
198
+ * expect(arrayFrom(existing)).toBe(existing);
171
199
  * ```
172
200
  *
173
201
  * Unlike `Array.from`, there's no map parameter for iterables — use
@@ -203,16 +231,22 @@ export function arrayFrom<T>(
203
231
  * Returns a readonly array and awaits promised items from sync or async
204
232
  * iterables.
205
233
  *
206
- * ### Example
234
+ * ### Awaiting sync and async iterables
207
235
  *
208
236
  * ```ts
209
- * await arrayFromAsync(new Set([1, 2, 3])); // ReadonlyArray<number>
210
- * await arrayFromAsync(
237
+ * import { arrayFromAsync } from "@evolu/common";
238
+ *
239
+ * const fromIterable = await arrayFromAsync(new Set([1, 2, 3]));
240
+ * expect(fromIterable).toEqual([1, 2, 3]);
241
+ *
242
+ * const fromAsyncIterable = await arrayFromAsync(
211
243
  * (async function* () {
212
244
  * yield Promise.resolve(1);
213
245
  * yield Promise.resolve(2);
214
246
  * })(),
215
- * ); // [1, 2]
247
+ * );
248
+ * expect(fromAsyncIterable).toEqual([1, 2]);
249
+ * expectTypeOf(fromAsyncIterable).toEqualTypeOf<ReadonlyArray<number>>();
216
250
  * ```
217
251
  *
218
252
  * Unlike `Array.fromAsync`, there's no map parameter — map the result with
@@ -234,20 +268,25 @@ export const arrayFromAsync = async <T>(
234
268
  * negated guard is better than `.length === 0` for early returns because
235
269
  * TypeScript narrows the type after the check.
236
270
  *
237
- * ### Example
271
+ * ### Narrowing mutable and readonly arrays
238
272
  *
239
273
  * ```ts
240
- * // Mutable array narrows to NonEmptyArray
241
- * const arr: Array<number> = [1, 2, 3];
242
- * if (isNonEmptyArray(arr)) {
243
- * shiftFromArray(arr); // arr is NonEmptyArray<number>
244
- * }
245
- *
246
- * // Readonly array narrows to NonEmptyReadonlyArray
274
+ * import {
275
+ * firstInArray,
276
+ * isNonEmptyArray,
277
+ * type NonEmptyArray,
278
+ * type NonEmptyReadonlyArray,
279
+ * } from "@evolu/common";
280
+ *
281
+ * const mutable: Array<number> = [1, 2, 3];
247
282
  * const readonly: ReadonlyArray<number> = [1, 2, 3];
248
- * if (isNonEmptyArray(readonly)) {
249
- * firstInArray(readonly); // readonly is NonEmptyReadonlyArray<number>
283
+ * if (!isNonEmptyArray(mutable) || !isNonEmptyArray(readonly)) {
284
+ * throw new Error("Expected values");
250
285
  * }
286
+ *
287
+ * expectTypeOf(mutable).toEqualTypeOf<NonEmptyArray<number>>();
288
+ * expectTypeOf(readonly).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
289
+ * expect(firstInArray(readonly)).toBe(1);
251
290
  * ```
252
291
  *
253
292
  * @group Types
@@ -266,10 +305,17 @@ export function isNonEmptyArray<T>(
266
305
  /**
267
306
  * Appends an item to an array, returning a new non-empty readonly array.
268
307
  *
269
- * ### Example
308
+ * ### Appending a value
270
309
  *
271
310
  * ```ts
272
- * appendToArray([1, 2, 3], 4); // [1, 2, 3, 4]
311
+ * import {
312
+ * appendToArray,
313
+ * type NonEmptyReadonlyArray,
314
+ * } from "@evolu/common";
315
+ *
316
+ * const values = appendToArray([1, 2, 3], 4);
317
+ * expect(values).toEqual([1, 2, 3, 4]);
318
+ * expectTypeOf(values).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
273
319
  * ```
274
320
  *
275
321
  * @group Transformations
@@ -278,15 +324,24 @@ export const appendToArray = <T>(
278
324
  array: ReadonlyArray<T>,
279
325
  item: T,
280
326
  ): NonEmptyReadonlyArray<T> =>
327
+ // The appended item guarantees non-emptiness, but TypeScript cannot infer a
328
+ // leading element after spreading a possibly empty array.
281
329
  [...array, item] as ReadonlyArray<T> as NonEmptyReadonlyArray<T>;
282
330
 
283
331
  /**
284
332
  * Prepends an item to an array, returning a new non-empty readonly array.
285
333
  *
286
- * ### Example
334
+ * ### Prepending a value
287
335
  *
288
336
  * ```ts
289
- * prependToArray([2, 3], 1); // [1, 2, 3]
337
+ * import {
338
+ * prependToArray,
339
+ * type NonEmptyReadonlyArray,
340
+ * } from "@evolu/common";
341
+ *
342
+ * const values = prependToArray([2, 3], 1);
343
+ * expect(values).toEqual([1, 2, 3]);
344
+ * expectTypeOf(values).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
290
345
  * ```
291
346
  *
292
347
  * @group Transformations
@@ -294,39 +349,47 @@ export const appendToArray = <T>(
294
349
  export const prependToArray = <T>(
295
350
  array: ReadonlyArray<T>,
296
351
  item: T,
297
- ): NonEmptyReadonlyArray<T> => [item, ...array] as NonEmptyReadonlyArray<T>;
352
+ ): NonEmptyReadonlyArray<T> => [item, ...array];
298
353
 
299
354
  /**
300
355
  * Maps an array using a mapper function, returning a new readonly array.
301
356
  *
302
357
  * Preserves non-empty type.
303
358
  *
304
- * ### Example
359
+ * ### Mapping with indexes while preserving non-emptiness
305
360
  *
306
361
  * ```ts
307
- * mapArray([1, 2, 3], (x) => x * 2); // [2, 4, 6]
362
+ * import { mapArray, type NonEmptyReadonlyArray } from "@evolu/common";
363
+ *
364
+ * const values: ReadonlyArray<number> = [1, 2, 3];
365
+ * const indexed = mapArray(values, (value, index) => value + index);
366
+ * expect(indexed).toEqual([1, 3, 5]);
367
+ * expectTypeOf(indexed).toEqualTypeOf<ReadonlyArray<number>>();
368
+ *
369
+ * const nonEmpty: NonEmptyReadonlyArray<number> = [1, 2, 3];
370
+ * expectTypeOf(mapArray(nonEmpty, (x) => x * 2)).toEqualTypeOf<
371
+ * NonEmptyReadonlyArray<number>
372
+ * >();
308
373
  * ```
309
374
  *
375
+ * The mapper receives `(item, index, array)`, matching native `Array.map`.
376
+ *
310
377
  * @group Transformations
311
378
  */
312
379
  export function mapArray<T, U>(
313
- array: NonEmptyReadonlyArray<T> | NonEmptyArray<T>,
314
- mapper: (item: T, index: number) => U,
380
+ array: NonEmptyReadonlyArray<T>,
381
+ mapper: (item: T, index: number, array: ReadonlyArray<T>) => U,
315
382
  ): NonEmptyReadonlyArray<U>;
316
383
  /** Possibly empty array. */
317
384
  export function mapArray<T, U>(
318
- array: ReadonlyArray<T> | Array<T>,
319
- mapper: (item: T, index: number) => U,
385
+ array: ReadonlyArray<T>,
386
+ mapper: (item: T, index: number, array: ReadonlyArray<T>) => U,
320
387
  ): ReadonlyArray<U>;
321
388
  export function mapArray<T, U>(
322
- array: ReadonlyArray<T> | Array<T>,
323
- mapper: (item: T, index: number) => U,
389
+ array: ReadonlyArray<T>,
390
+ mapper: (item: T, index: number, array: ReadonlyArray<T>) => U,
324
391
  ): ReadonlyArray<U> {
325
- // For loop is faster than array.map.
326
- const length = array.length;
327
- const result = new Array<U>(length);
328
- for (let i = 0; i < length; i++) result[i] = mapper(array[i], i);
329
- return result;
392
+ return array.map(mapper);
330
393
  }
331
394
 
332
395
  /**
@@ -336,15 +399,23 @@ export function mapArray<T, U>(
336
399
  * non-empty arrays. When called without a mapper, flattens nested arrays using
337
400
  * {@link identity}.
338
401
  *
339
- * ### Example
402
+ * ### Flattening and expanding values
340
403
  *
341
404
  * ```ts
342
- * flatMapArray([
405
+ * import { flatMapArray, type NonEmptyReadonlyArray } from "@evolu/common";
406
+ *
407
+ * const flattened = flatMapArray([
343
408
  * [1, 2],
344
409
  * [3, 4],
345
- * ]); // [1, 2, 3, 4]
346
- *
347
- * flatMapArray([1, 2, 3], (x) => [x, x * 10]); // [1, 10, 2, 20, 3, 30]
410
+ * ]);
411
+ * const values: NonEmptyReadonlyArray<number> = [1, 2, 3];
412
+ * const expanded = flatMapArray(
413
+ * values,
414
+ * (value, index): NonEmptyReadonlyArray<number> => [value, index],
415
+ * );
416
+ * expect(flattened).toEqual([1, 2, 3, 4]);
417
+ * expectTypeOf(expanded).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
418
+ * expect(expanded).toEqual([1, 0, 2, 1, 3, 2]);
348
419
  * ```
349
420
  *
350
421
  * ### Filter and map in one pass
@@ -352,47 +423,54 @@ export function mapArray<T, U>(
352
423
  * Return `[]` to filter out, `[value]` to keep:
353
424
  *
354
425
  * ```ts
426
+ * import { err, flatMapArray, ok } from "@evolu/common";
427
+ *
428
+ * const validate = (value: number) =>
429
+ * value > 0 ? ok(value) : err(`${value} is not positive`);
430
+ * const fields = [1, -2, 3, -4];
355
431
  * const errors = flatMapArray(fields, (f) => {
356
432
  * const result = validate(f);
357
433
  * return result.ok ? [] : [result.error];
358
434
  * });
435
+ * expect(errors).toEqual(["-2 is not positive", "-4 is not positive"]);
359
436
  * ```
360
437
  *
438
+ * The mapper receives `(item, index, array)`, matching native `Array.flatMap`.
439
+ *
361
440
  * @group Transformations
362
441
  */
363
442
  export function flatMapArray<T>(
364
- array:
365
- | NonEmptyReadonlyArray<NonEmptyReadonlyArray<T> | NonEmptyArray<T>>
366
- | NonEmptyArray<NonEmptyReadonlyArray<T> | NonEmptyArray<T>>,
443
+ array: NonEmptyReadonlyArray<NonEmptyReadonlyArray<T>>,
367
444
  ): NonEmptyReadonlyArray<T>;
368
445
  /** Possibly empty nested arrays. */
369
446
  export function flatMapArray<T>(
370
- array:
371
- | ReadonlyArray<ReadonlyArray<T> | Array<T>>
372
- | Array<ReadonlyArray<T> | Array<T>>,
447
+ array: ReadonlyArray<ReadonlyArray<T>>,
373
448
  ): ReadonlyArray<T>;
374
449
  /** Non-empty with mapper returning non-empty. */
375
450
  export function flatMapArray<T, U>(
376
- array: NonEmptyReadonlyArray<T> | NonEmptyArray<T>,
451
+ array: NonEmptyReadonlyArray<T>,
377
452
  mapper: (
378
453
  item: T,
379
454
  index: number,
380
- ) => NonEmptyReadonlyArray<U> | NonEmptyArray<U>,
455
+ array: ReadonlyArray<T>,
456
+ ) => NonEmptyReadonlyArray<U>,
381
457
  ): NonEmptyReadonlyArray<U>;
382
458
  /** With mapper function. */
383
459
  export function flatMapArray<T, U>(
384
- array: ReadonlyArray<T> | Array<T>,
385
- mapper: (item: T, index: number) => ReadonlyArray<U> | Array<U>,
460
+ array: ReadonlyArray<T>,
461
+ mapper: (item: T, index: number, array: ReadonlyArray<T>) => ReadonlyArray<U>,
386
462
  ): ReadonlyArray<U>;
387
463
  export function flatMapArray<T, U>(
388
- array: ReadonlyArray<T> | Array<T>,
464
+ array: ReadonlyArray<T>,
389
465
  mapper: (
390
466
  item: T,
391
467
  index: number,
392
- ) => ReadonlyArray<U> | Array<U> = identity as (
468
+ array: ReadonlyArray<T>,
469
+ ) => ReadonlyArray<U> = identity as (
393
470
  item: T,
394
471
  index: number,
395
- ) => ReadonlyArray<U> | Array<U>,
472
+ array: ReadonlyArray<T>,
473
+ ) => ReadonlyArray<U>,
396
474
  ): ReadonlyArray<U> {
397
475
  return array.flatMap(mapper);
398
476
  }
@@ -402,35 +480,42 @@ export function flatMapArray<T, U>(
402
480
  *
403
481
  * Returns a non-empty array when at least one input is non-empty.
404
482
  *
405
- * ### Example
483
+ * ### Concatenating non-empty inputs
406
484
  *
407
485
  * ```ts
408
- * concatArrays([1, 2], [3, 4]); // [1, 2, 3, 4]
409
- * concatArrays([], [1]); // [1] (non-empty)
410
- * concatArrays([1], []); // [1] (non-empty)
486
+ * import { concatArrays, type NonEmptyReadonlyArray } from "@evolu/common";
487
+ *
488
+ * const nonEmpty: NonEmptyReadonlyArray<number> = [1];
489
+ * const joined = concatArrays([1, 2], [3, 4]);
490
+ * const fromLeft = concatArrays(nonEmpty, []);
491
+ * const fromRight = concatArrays([], nonEmpty);
492
+ *
493
+ * expect(joined).toEqual([1, 2, 3, 4]);
494
+ * expectTypeOf(fromLeft).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
495
+ * expectTypeOf(fromRight).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
411
496
  * ```
412
497
  *
413
498
  * @group Transformations
414
499
  */
415
500
  export function concatArrays<T>(
416
- first: NonEmptyReadonlyArray<T> | NonEmptyArray<T>,
417
- second: ReadonlyArray<T> | Array<T>,
501
+ first: NonEmptyReadonlyArray<T>,
502
+ second: ReadonlyArray<T>,
418
503
  ): NonEmptyReadonlyArray<T>;
419
504
  /** Second non-empty. */
420
505
  export function concatArrays<T>(
421
- first: ReadonlyArray<T> | Array<T>,
422
- second: NonEmptyReadonlyArray<T> | NonEmptyArray<T>,
506
+ first: ReadonlyArray<T>,
507
+ second: NonEmptyReadonlyArray<T>,
423
508
  ): NonEmptyReadonlyArray<T>;
424
509
  /** Both possibly empty. */
425
510
  export function concatArrays<T>(
426
- first: ReadonlyArray<T> | Array<T>,
427
- second: ReadonlyArray<T> | Array<T>,
511
+ first: ReadonlyArray<T>,
512
+ second: ReadonlyArray<T>,
428
513
  ): ReadonlyArray<T>;
429
514
  export function concatArrays<T>(
430
- first: ReadonlyArray<T> | Array<T>,
431
- second: ReadonlyArray<T> | Array<T>,
515
+ first: ReadonlyArray<T>,
516
+ second: ReadonlyArray<T>,
432
517
  ): ReadonlyArray<T> {
433
- return [...first, ...second] as ReadonlyArray<T>;
518
+ return [...first, ...second];
434
519
  }
435
520
 
436
521
  /**
@@ -441,77 +526,92 @@ export function concatArrays<T>(
441
526
  * TypeScript will narrow the result type to the narrowed type, making it useful
442
527
  * for filtering with Evolu Types like `PositiveInt.is`.
443
528
  *
444
- * ### Example
445
- *
446
529
  * ### With predicate
447
530
  *
448
531
  * ```ts
449
- * filterArray([1, 2, 3, 4, 5], (x) => x % 2 === 0); // [2, 4]
532
+ * import { filterArray } from "@evolu/common";
533
+ *
534
+ * const evens = filterArray([1, 2, 3, 4, 5], (x) => x % 2 === 0);
535
+ * expect(evens).toEqual([2, 4]);
450
536
  * ```
451
537
  *
452
538
  * ### With refinement
453
539
  *
454
540
  * ```ts
455
- * const mixed: ReadonlyArray<NonEmptyString | PositiveInt> = [
456
- * NonEmptyString.orThrow("hello"),
541
+ * import {
542
+ * filterArray,
543
+ * NonEmptyTrimmedString,
544
+ * PositiveInt,
545
+ * } from "@evolu/common";
546
+ *
547
+ * const mixed: ReadonlyArray<NonEmptyTrimmedString | PositiveInt> = [
548
+ * NonEmptyTrimmedString.orThrow("hello"),
457
549
  * PositiveInt.orThrow(42),
458
550
  * ];
459
551
  * const positiveInts = filterArray(mixed, PositiveInt.is);
460
- * // positiveInts: ReadonlyArray<PositiveInt> (narrowed type)
552
+ * expect(positiveInts).toEqual([42]);
553
+ * expectTypeOf(positiveInts).toEqualTypeOf<ReadonlyArray<PositiveInt>>();
461
554
  * ```
462
555
  *
556
+ * The predicate receives `(item, index, array)`, matching native
557
+ * `Array.filter`.
558
+ *
463
559
  * @group Transformations
464
560
  */
465
561
  export function filterArray<T, S extends T>(
466
562
  array: ReadonlyArray<T>,
467
- refinement: RefinementWithIndex<T, S>,
563
+ refinement: (item: T, index: number, array: ReadonlyArray<T>) => item is S,
468
564
  ): ReadonlyArray<S>;
469
565
  /** With predicate. */
470
566
  export function filterArray<T>(
471
567
  array: ReadonlyArray<T>,
472
- predicate: PredicateWithIndex<T>,
568
+ predicate: (item: T, index: number, array: ReadonlyArray<T>) => boolean,
473
569
  ): ReadonlyArray<T>;
474
570
  export function filterArray<T>(
475
571
  array: ReadonlyArray<T>,
476
- predicate: PredicateWithIndex<T>,
572
+ predicate: (item: T, index: number, array: ReadonlyArray<T>) => boolean,
477
573
  ): ReadonlyArray<T> {
478
574
  return array.filter(predicate);
479
575
  }
480
576
 
481
577
  /**
482
- * Returns a new readonly array with duplicate items removed. If `by` is
483
- * provided, it will be used to derive the key for uniqueness; otherwise values
484
- * are used directly. Dedupes by reference equality of values (or extracted keys
485
- * when `by` is used).
578
+ * Returns a new readonly array with duplicate items removed. Items are compared
579
+ * using `Set` equality (SameValueZero): primitives by value, and objects and
580
+ * arrays by reference. If `by` is provided, it derives the comparison key.
486
581
  *
487
582
  * Preserves non-empty type.
488
583
  *
489
- * ### Example
584
+ * ### Deduplicating by value or key
490
585
  *
491
586
  * ```ts
492
- * // Dedupe primitives by value
493
- * dedupeArray([1, 2, 1, 3, 2]); // [1, 2, 3]
587
+ * import { dedupeArray, type NonEmptyReadonlyArray } from "@evolu/common";
494
588
  *
495
- * // Dedupe objects by property
496
- * dedupeArray(
589
+ * const numbers = dedupeArray([1, 2, 1, 3, 2]);
590
+ * const people = dedupeArray(
497
591
  * [
498
592
  * { id: 1, name: "Alice" },
499
593
  * { id: 2, name: "Bob" },
500
594
  * { id: 1, name: "Alice 2" },
501
595
  * ],
502
596
  * (item) => item.id,
503
- * ); // [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
597
+ * );
598
+ * expectTypeOf(numbers).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
599
+ * expect(numbers).toEqual([1, 2, 3]);
600
+ * expect(people).toEqual([
601
+ * { id: 1, name: "Alice" },
602
+ * { id: 2, name: "Bob" },
603
+ * ]);
504
604
  * ```
505
605
  *
506
606
  * @group Transformations
507
607
  */
508
608
  export function dedupeArray<T>(
509
- array: NonEmptyReadonlyArray<T> | NonEmptyArray<T>,
609
+ array: NonEmptyReadonlyArray<T>,
510
610
  by?: (item: T) => unknown,
511
611
  ): NonEmptyReadonlyArray<T>;
512
612
  /** Possibly empty array. */
513
613
  export function dedupeArray<T>(
514
- array: ReadonlyArray<T> | Array<T>,
614
+ array: ReadonlyArray<T>,
515
615
  by?: (item: T) => unknown,
516
616
  ): ReadonlyArray<T>;
517
617
  export function dedupeArray<T>(
@@ -542,51 +642,62 @@ export function dedupeArray<T>(
542
642
  * TypeScript will narrow the first array to the narrowed type, making it useful
543
643
  * for filtering with Evolu Types like `PositiveInt.is`.
544
644
  *
545
- * ### Example
546
- *
547
645
  * ### With predicate
548
646
  *
549
647
  * ```ts
648
+ * import { partitionArray } from "@evolu/common";
649
+ *
550
650
  * const [evens, odds] = partitionArray(
551
651
  * [1, 2, 3, 4, 5],
552
652
  * (x) => x % 2 === 0,
553
653
  * );
554
- * evens; // [2, 4]
555
- * odds; // [1, 3, 5]
654
+ * expect(evens).toEqual([2, 4]);
655
+ * expect(odds).toEqual([1, 3, 5]);
556
656
  * ```
557
657
  *
558
658
  * ### With refinement
559
659
  *
560
660
  * ```ts
561
- * const mixed: ReadonlyArray<NonEmptyString | PositiveInt> = [
562
- * NonEmptyString.orThrow("hello"),
661
+ * import {
662
+ * NonEmptyTrimmedString,
663
+ * partitionArray,
664
+ * PositiveInt,
665
+ * } from "@evolu/common";
666
+ *
667
+ * const mixed: ReadonlyArray<NonEmptyTrimmedString | PositiveInt> = [
668
+ * NonEmptyTrimmedString.orThrow("hello"),
563
669
  * PositiveInt.orThrow(42),
564
670
  * ];
565
671
  * const [positiveInts, strings] = partitionArray(mixed, PositiveInt.is);
566
- * // positiveInts: ReadonlyArray<PositiveInt> (narrowed type)
567
- * // strings: ReadonlyArray<NonEmptyString> (Exclude<T, PositiveInt>)
672
+ * expect(positiveInts).toEqual([42]);
673
+ * expectTypeOf(positiveInts).toEqualTypeOf<ReadonlyArray<PositiveInt>>();
674
+ * expectTypeOf(strings).toEqualTypeOf<
675
+ * ReadonlyArray<NonEmptyTrimmedString>
676
+ * >();
568
677
  * ```
569
678
  *
679
+ * The predicate receives `(item, index, array)`.
680
+ *
570
681
  * @group Transformations
571
682
  */
572
683
  export function partitionArray<T, S extends T>(
573
684
  array: ReadonlyArray<T>,
574
- refinement: RefinementWithIndex<T, S>,
685
+ refinement: (item: T, index: number, array: ReadonlyArray<T>) => item is S,
575
686
  ): readonly [ReadonlyArray<S>, ReadonlyArray<Exclude<T, S>>];
576
687
  /** With predicate. */
577
688
  export function partitionArray<T>(
578
689
  array: ReadonlyArray<T>,
579
- predicate: PredicateWithIndex<T>,
690
+ predicate: (item: T, index: number, array: ReadonlyArray<T>) => boolean,
580
691
  ): readonly [ReadonlyArray<T>, ReadonlyArray<T>];
581
692
  export function partitionArray<T>(
582
693
  array: ReadonlyArray<T>,
583
- predicate: PredicateWithIndex<T>,
694
+ predicate: (item: T, index: number, array: ReadonlyArray<T>) => boolean,
584
695
  ): readonly [ReadonlyArray<T>, ReadonlyArray<T>] {
585
696
  const trueArray: Array<T> = [];
586
697
  const falseArray: Array<T> = [];
587
698
 
588
699
  for (let i = 0; i < array.length; i++) {
589
- if (predicate(array[i], i)) {
700
+ if (predicate(array[i], i, array)) {
590
701
  trueArray.push(array[i]);
591
702
  } else {
592
703
  falseArray.push(array[i]);
@@ -601,10 +712,15 @@ export function partitionArray<T>(
601
712
  *
602
713
  * Wraps native `toSorted`. Preserves non-empty type.
603
714
  *
604
- * ### Example
715
+ * ### Sorting without mutation
605
716
  *
606
717
  * ```ts
607
- * sortArray([3, 1, 2], (a, b) => a - b); // [1, 2, 3]
718
+ * import { sortArray, type NonEmptyReadonlyArray } from "@evolu/common";
719
+ *
720
+ * const values: NonEmptyReadonlyArray<number> = [3, 1, 2];
721
+ * const sorted = sortArray(values, (a, b) => a - b);
722
+ * expect(sorted).toEqual([1, 2, 3]);
723
+ * expectTypeOf(sorted).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
608
724
  * ```
609
725
  *
610
726
  * @group Transformations
@@ -630,10 +746,15 @@ export function sortArray<T>(
630
746
  *
631
747
  * Wraps native `toReversed`. Preserves non-empty type.
632
748
  *
633
- * ### Example
749
+ * ### Reversing without mutation
634
750
  *
635
751
  * ```ts
636
- * reverseArray([1, 2, 3]); // [3, 2, 1]
752
+ * import { reverseArray, type NonEmptyReadonlyArray } from "@evolu/common";
753
+ *
754
+ * const values: NonEmptyReadonlyArray<number> = [1, 2, 3];
755
+ * const reversed = reverseArray(values);
756
+ * expect(reversed).toEqual([3, 2, 1]);
757
+ * expectTypeOf(reversed).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
637
758
  * ```
638
759
  *
639
760
  * @group Transformations
@@ -652,11 +773,15 @@ export function reverseArray<T>(array: ReadonlyArray<T>): ReadonlyArray<T> {
652
773
  *
653
774
  * Wraps native `toSpliced`.
654
775
  *
655
- * ### Example
776
+ * ### Removing and replacing values
656
777
  *
657
778
  * ```ts
658
- * spliceArray([1, 2, 3, 4], 1, 2); // [1, 4]
659
- * spliceArray([1, 2, 3], 1, 1, 10, 11); // [1, 10, 11, 3]
779
+ * import { spliceArray } from "@evolu/common";
780
+ *
781
+ * const values: ReadonlyArray<number> = [1, 2, 3, 4];
782
+ * expect(spliceArray(values, 1, 2)).toEqual([1, 4]);
783
+ * expect(spliceArray([1, 2, 3], 1, 1, 10, 11)).toEqual([1, 10, 11, 3]);
784
+ * expect(values).toEqual([1, 2, 3, 4]);
660
785
  * ```
661
786
  *
662
787
  * @group Transformations
@@ -685,21 +810,33 @@ export type ZipArrayResult<T extends ReadonlyArray<ReadonlyArray<unknown>>> = {
685
810
  * {@link https://github.com/tc39/proposal-array-zip | TC39 Array.zip proposal}
686
811
  * for the pattern this follows.
687
812
  *
688
- * ### Example
813
+ * ### Zipping to the shortest input
689
814
  *
690
815
  * ```ts
691
- * zipArray([
816
+ * import { zipArray, type NonEmptyReadonlyArray } from "@evolu/common";
817
+ *
818
+ * const pairs = zipArray([
692
819
  * [1, 2, 3],
693
820
  * ["a", "b", "c"],
694
821
  * ]);
695
- * // [[1, "a"], [2, "b"], [3, "c"]]
696
- *
697
- * zipArray([
698
- * [1, 2],
699
- * ["a", "b", "c"],
700
- * [true, false],
822
+ * expect(pairs).toEqual([
823
+ * [1, "a"],
824
+ * [2, "b"],
825
+ * [3, "c"],
701
826
  * ]);
702
- * // [[1, "a", true], [2, "b", false]]
827
+ * expect(
828
+ * zipArray([
829
+ * [1, 2],
830
+ * ["a", "b", "c"],
831
+ * [true, false],
832
+ * ]),
833
+ * ).toEqual([
834
+ * [1, "a", true],
835
+ * [2, "b", false],
836
+ * ]);
837
+ * expectTypeOf(pairs).toEqualTypeOf<
838
+ * NonEmptyReadonlyArray<Readonly<[number, string]>>
839
+ * >();
703
840
  * ```
704
841
  *
705
842
  * @group Transformations
@@ -708,7 +845,7 @@ export function zipArray<
708
845
  T extends NonEmptyReadonlyArray<NonEmptyReadonlyArray<unknown>>,
709
846
  >(arrays: T): NonEmptyReadonlyArray<Readonly<ZipArrayResult<T>>>;
710
847
  /** Possibly empty arrays. */
711
- export function zipArray<T extends ReadonlyArray<ReadonlyArray<unknown>>>(
848
+ export function zipArray<const T extends ReadonlyArray<ReadonlyArray<unknown>>>(
712
849
  arrays: T,
713
850
  ): ReadonlyArray<Readonly<ZipArrayResult<T>>>;
714
851
  export function zipArray<T extends ReadonlyArray<ReadonlyArray<unknown>>>(
@@ -729,10 +866,12 @@ export function zipArray<T extends ReadonlyArray<ReadonlyArray<unknown>>>(
729
866
  /**
730
867
  * Returns the first element of a non-empty array.
731
868
  *
732
- * ### Example
869
+ * ### Reading the first value
733
870
  *
734
871
  * ```ts
735
- * firstInArray(["a", "b", "c"]); // "a"
872
+ * import { firstInArray } from "@evolu/common";
873
+ *
874
+ * expect(firstInArray(["a", "b", "c"])).toBe("a");
736
875
  * ```
737
876
  *
738
877
  * @group Accessors
@@ -742,53 +881,15 @@ export const firstInArray = <T>(array: NonEmptyReadonlyArray<T>): T => array[0];
742
881
  /**
743
882
  * Returns the last element of a non-empty array.
744
883
  *
745
- * ### Example
884
+ * ### Reading the last value
746
885
  *
747
886
  * ```ts
748
- * lastInArray(["a", "b", "c"]); // "c"
887
+ * import { lastInArray } from "@evolu/common";
888
+ *
889
+ * expect(lastInArray(["a", "b", "c"])).toBe("c");
749
890
  * ```
750
891
  *
751
892
  * @group Accessors
752
893
  */
753
894
  export const lastInArray = <T>(array: NonEmptyReadonlyArray<T>): T =>
754
895
  array[array.length - 1];
755
-
756
- /**
757
- * Shifts (removes and returns) the first element from a non-empty mutable
758
- * array.
759
- *
760
- * **Mutates** the original array.
761
- *
762
- * ### Example
763
- *
764
- * ```ts
765
- * // Process a queue of callbacks
766
- * const waitingQueue: Array<() => void> = [callback1, callback2];
767
- * if (isNonEmptyArray(waitingQueue)) {
768
- * shiftFromArray(waitingQueue)(); // Remove and immediately invoke
769
- * }
770
- * ```
771
- *
772
- * @group Mutations
773
- */
774
- export const shiftFromArray = <T>(array: NonEmptyArray<T>): T =>
775
- array.shift() as T;
776
-
777
- /**
778
- * Pops (removes and returns) the last element from a non-empty mutable array.
779
- *
780
- * **Mutates** the original array.
781
- *
782
- * ### Example
783
- *
784
- * ```ts
785
- * // Process a stack of callbacks (LIFO)
786
- * const callbackStack: Array<() => void> = [callback1, callback2];
787
- * if (isNonEmptyArray(callbackStack)) {
788
- * popFromArray(callbackStack)(); // Remove and immediately invoke
789
- * }
790
- * ```
791
- *
792
- * @group Mutations
793
- */
794
- export const popFromArray = <T>(array: NonEmptyArray<T>): T => array.pop() as T;