@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/Object.ts CHANGED
@@ -22,15 +22,20 @@ export type ReadonlyRecord<K extends keyof any, V> = Readonly<Record<K, V>>;
22
22
  * Object.prototype` so it also works for plain objects coming from another
23
23
  * JavaScript realm.
24
24
  *
25
+ * TODO: deprecated Use `Object.is` from Type. Define a dedicated Type when
26
+ * another object domain is required.
27
+ *
25
28
  * ### Example
26
29
  *
27
30
  * ```ts
28
- * isPlainObject({}); // true
29
- * isPlainObject(Object.create(null)); // true
30
- * isPlainObject(new Date()); // false
31
- * isPlainObject(new (class Example {})()); // false
32
- * isPlainObject([]); // false
33
- * isPlainObject(null); // false
31
+ * import { isPlainObject } from "@evolu/common";
32
+ *
33
+ * expect(isPlainObject({})).toBe(true);
34
+ * expect(isPlainObject(Object.create(null))).toBe(true);
35
+ * expect(isPlainObject(new Date())).toBe(false);
36
+ * expect(isPlainObject(new (class Example {})())).toBe(false);
37
+ * expect(isPlainObject([])).toBe(false);
38
+ * expect(isPlainObject(null)).toBe(false);
34
39
  * ```
35
40
  */
36
41
  export const isPlainObject = (
@@ -50,9 +55,11 @@ export const isPlainObject = (
50
55
  * ### Example
51
56
  *
52
57
  * ```ts
53
- * isFunction(() => {}); // true
54
- * isFunction(function () {}); // true
55
- * isFunction({}); // false
58
+ * import { isFunction } from "@evolu/common";
59
+ *
60
+ * expect(isFunction(() => {})).toBe(true);
61
+ * expect(isFunction(function () {})).toBe(true);
62
+ * expect(isFunction({})).toBe(false);
56
63
  * ```
57
64
  */
58
65
  export const isFunction = (value: unknown): value is globalThis.Function =>
@@ -64,9 +71,11 @@ export const isFunction = (value: unknown): value is globalThis.Function =>
64
71
  * ### Example
65
72
  *
66
73
  * ```ts
67
- * isIterable([1, 2, 3]); // true
68
- * isIterable("abc"); // true
69
- * isIterable({}); // false
74
+ * import { isIterable } from "@evolu/common";
75
+ *
76
+ * expect(isIterable([1, 2, 3])).toBe(true);
77
+ * expect(isIterable("abc")).toBe(true);
78
+ * expect(isIterable({})).toBe(false);
70
79
  * ```
71
80
  */
72
81
  export const isIterable = (value: unknown): value is Iterable<unknown> =>
@@ -79,17 +88,26 @@ export const isIterable = (value: unknown): value is Iterable<unknown> =>
79
88
  * ### Example
80
89
  *
81
90
  * ```ts
82
- * type UserId = string & { readonly __brand: "UserId" };
83
- * const users = createRecord<UserId, string>();
84
- * const entries = objectToEntries(users); // [UserId, string][]
91
+ * import {
92
+ * objectToEntries,
93
+ * type Brand,
94
+ * type ReadonlyRecord,
95
+ * } from "@evolu/common";
96
+ *
97
+ * type UserId = string & Brand<"UserId">;
98
+ *
99
+ * const userId = "u1" as UserId;
100
+ * const users: ReadonlyRecord<UserId, string> = { [userId]: "Alice" };
101
+ * const entries = objectToEntries(users);
102
+ *
103
+ * expectTypeOf(entries).toEqualTypeOf<ReadonlyArray<[UserId, string]>>();
104
+ * expect(entries).toEqual([[userId, "Alice"]]);
85
105
  * ```
86
106
  */
87
107
  export const objectToEntries = <T extends Record<string, any>>(
88
108
  record: T,
89
109
  ): ReadonlyArray<[StringKeyOf<T>, T[StringKeyOf<T>]]> =>
90
- Object.entries(record) as Array<
91
- [StringKeyOf<T>, T[StringKeyOf<T>]]
92
- >;
110
+ Object.entries(record) as Array<[StringKeyOf<T>, T[StringKeyOf<T>]]>;
93
111
 
94
112
  // A helper type to remove symbol keys (e.g for branded objects).
95
113
  type StringKeyOf<T> = Extract<keyof T, string>;
@@ -103,11 +121,20 @@ type StringKeyOf<T> = Extract<keyof T, string>;
103
121
  * ### Example
104
122
  *
105
123
  * ```ts
106
- * type UserId = string & { readonly __brand: "UserId" };
124
+ * import {
125
+ * objectFromEntries,
126
+ * type Brand,
127
+ * type ReadonlyRecord,
128
+ * } from "@evolu/common";
129
+ *
130
+ * type UserId = string & Brand<"UserId">;
107
131
  * const entries: ReadonlyArray<[UserId, string]> = [
108
132
  * ["u1" as UserId, "Alice"],
109
133
  * ];
110
- * const users = objectFromEntries(entries); // ReadonlyRecord<UserId, string>
134
+ * const users = objectFromEntries(entries);
135
+ *
136
+ * expectTypeOf(users).toEqualTypeOf<ReadonlyRecord<UserId, string>>();
137
+ * expect(users).toEqual({ u1: "Alice" });
111
138
  * ```
112
139
  */
113
140
  export const objectFromEntries = <K extends string, V>(
@@ -123,11 +150,20 @@ export const objectFromEntries = <K extends string, V>(
123
150
  * ### Example
124
151
  *
125
152
  * ```ts
126
- * objectFrom(["en", "fr", "de"], loadTranslations);
127
- * // { en: Translations, fr: Translations, de: Translations }
153
+ * import { objectFrom, type ReadonlyRecord } from "@evolu/common";
154
+ *
155
+ * const translations = objectFrom(
156
+ * ["en", "fr"] as const,
157
+ * (locale): string => `Hello in ${locale}`,
158
+ * );
128
159
  *
129
- * objectFrom(["trace", "debug", "log"], (level) => createHandler(level));
130
- * // { trace: Handler, debug: Handler, log: Handler }
160
+ * expectTypeOf(translations).toEqualTypeOf<
161
+ * ReadonlyRecord<"en" | "fr", string>
162
+ * >();
163
+ * expect(translations).toEqual({
164
+ * en: "Hello in en",
165
+ * fr: "Hello in fr",
166
+ * });
131
167
  * ```
132
168
  */
133
169
  export const objectFrom = <K extends string, V>(
@@ -168,53 +204,138 @@ export const excludeProp = <T extends object, K extends keyof T>(
168
204
  };
169
205
 
170
206
  /**
171
- * Creates a prototype-less object typed as `Record<K, V>`.
207
+ * Creates a mutable Record.
172
208
  *
173
- * Use this function when you need a plain record without a prototype chain
174
- * (e.g. when keys are controlled by external sources) to avoid prototype
175
- * pollution and accidental collisions with properties like `__proto__`.
209
+ * Use it to build a Record locally through mutation, avoiding repeated object
210
+ * spreads. Keep mutation scoped to the constructing function, and treat the
211
+ * completed Record as immutable after it leaves that scope. When a source is
212
+ * provided, its own enumerable properties are shallow-copied. Inherited and
213
+ * non-enumerable properties are not copied.
176
214
  *
177
- * Example:
215
+ * The Record is created with `Object.create(null)`, so any string can be used
216
+ * as a key while it is being built. Missing keys such as `toString` and
217
+ * `constructor` do not resolve to inherited values, and assigning `__proto__`
218
+ * creates an own data property instead of changing the prototype.
219
+ *
220
+ * The null prototype makes dynamic construction safer, but it does not have to
221
+ * be preserved. Spreading the completed Record into an ordinary object is
222
+ * supported. Use {@link getOwnProp} when a lookup must read only own
223
+ * properties.
224
+ *
225
+ * For immutable empty application data, defaults, or placeholders, use
226
+ * {@link emptyRecord}.
227
+ *
228
+ * ### Example
178
229
  *
179
230
  * ```ts
180
- * const values = createRecord<string, SqliteValue>();
181
- * values["__proto__"] = someValue; // safe, no prototype pollution
231
+ * import { createMutableRecord } from "@evolu/common";
232
+ *
233
+ * const createValuesByKey = (
234
+ * entries: ReadonlyArray<readonly [string, number]>,
235
+ * ): Readonly<Record<string, number>> => {
236
+ * const valuesByKey = createMutableRecord<string, number>();
237
+ *
238
+ * for (const [key, value] of entries) {
239
+ * valuesByKey[key] = value;
240
+ * }
241
+ *
242
+ * return valuesByKey;
243
+ * };
244
+ *
245
+ * expect(
246
+ * createValuesByKey([
247
+ * ["a", 1],
248
+ * ["b", 2],
249
+ * ]),
250
+ * ).toEqual({ a: 1, b: 2 });
182
251
  * ```
252
+ *
253
+ * Note that TypeScript does not model an object's runtime prototype. A plain
254
+ * TypeScript Record exposes `Object.prototype` members even when the runtime
255
+ * object has a `null` prototype:
256
+ *
257
+ * ```ts
258
+ * const values = Object.create(null) as Record<string, number>;
259
+ *
260
+ * // TypeScript accepts the call, but `toString` is undefined at runtime.
261
+ * expect(() => values.toString()).toThrow(TypeError);
262
+ * ```
263
+ *
264
+ * `createMutableRecord` uses the same TypeScript Record representation.
265
+ *
266
+ * ```ts
267
+ * import { createMutableRecord } from "@evolu/common";
268
+ *
269
+ * const values = createMutableRecord<string, number>();
270
+ *
271
+ * // TypeScript accepts the call, but `toString` is undefined at runtime.
272
+ * expect(() => values.toString()).toThrow(TypeError);
273
+ * ```
274
+ *
275
+ * In other words, treat the returned object as string-keyed data rather than
276
+ * calling inherited object methods through it.
183
277
  */
184
- export const createRecord = <K extends string = string, V = unknown>(): Record<
185
- K,
186
- V
187
- > => Object.create(null) as Record<K, V>;
278
+ export function createMutableRecord<
279
+ K extends string = string,
280
+ V = unknown,
281
+ >(): Record<K, V>;
282
+ export function createMutableRecord<T extends object>(
283
+ source: T,
284
+ ): { -readonly [K in keyof T]: T[K] };
285
+ export function createMutableRecord(source?: object): Record<string, unknown> {
286
+ const record = Object.create(null) as Record<string, unknown>;
287
+ return source === undefined ? record : Object.assign(record, source);
288
+ }
188
289
 
189
290
  /**
190
- * An empty readonly record.
291
+ * A shared frozen empty readonly Record.
191
292
  *
192
- * Use as a default or initial value to avoid allocating new empty records.
293
+ * Use it as an immutable empty value for defaults and placeholders instead of
294
+ * allocating a new empty object. Because the instance is shared, it is frozen
295
+ * to prevent accidental mutation.
296
+ *
297
+ * Use {@link createMutableRecord} to build a Record locally through mutation
298
+ * instead of repeated object spreads.
193
299
  *
194
300
  * @group Constants
195
301
  */
196
302
  export const emptyRecord: Readonly<Record<string, never>> =
197
- /*#__PURE__*/ createRecord();
303
+ /*#__PURE__*/ Object.freeze(
304
+ /*#__PURE__*/ createMutableRecord<string, never>(),
305
+ );
198
306
 
199
307
  /**
200
- * Safely gets a property from a record, returning `undefined` if the key
201
- * doesn't exist.
308
+ * Gets an own property from a record, returning `undefined` if the key is
309
+ * missing or inherited.
202
310
  *
203
311
  * TypeScript's `Record<K, V>` type assumes all keys exist, but at runtime
204
- * accessing a non-existent key returns `undefined`. This helper provides proper
205
- * typing for that case without needing a type assertion.
312
+ * accessing a missing key returns `undefined`. This helper provides proper
313
+ * typing for that case without treating properties inherited from
314
+ * `Object.prototype` as record data.
206
315
  *
207
316
  * ### Example
208
317
  *
209
318
  * ```ts
210
- * const users: Record<string, User> = { alice: { name: "Alice" } };
211
- * const user = getProperty(users, "bob"); // User | undefined
319
+ * import { getOwnProp } from "@evolu/common";
320
+ *
321
+ * interface User {
322
+ * readonly name: string;
323
+ * }
324
+ *
325
+ * const users: Readonly<Record<string, User>> = {
326
+ * alice: { name: "Alice" },
327
+ * };
328
+ * const user = getOwnProp(users, "bob");
329
+ *
330
+ * expectTypeOf(user).toEqualTypeOf<User | undefined>();
331
+ * expect(user).toBeUndefined();
332
+ * expect(getOwnProp(users, "toString")).toBeUndefined();
212
333
  * ```
213
334
  */
214
- export const getProperty = <K extends string, V>(
335
+ export const getOwnProp = <K extends string, V>(
215
336
  record: ReadonlyRecord<K, V>,
216
- key: K,
217
- ): V | undefined => (key in record ? record[key] : undefined);
337
+ key: NoInfer<K>,
338
+ ): V | undefined => (Object.hasOwn(record, key) ? record[key] : undefined);
218
339
 
219
340
  /**
220
341
  * A disposable wrapper around `URL.createObjectURL` that automatically revokes
@@ -224,10 +345,13 @@ export const getProperty = <K extends string, V>(
224
345
  * ### Example
225
346
  *
226
347
  * ```ts
348
+ * import { createObjectURL } from "@evolu/common";
349
+ *
227
350
  * const blob = new Blob(["hello"], { type: "text/plain" });
228
351
  * using objectUrl = createObjectURL(blob);
229
- * console.log(objectUrl.url); // blob:...
230
- * // URL.revokeObjectURL is automatically called when the scope ends
352
+ *
353
+ * expect(objectUrl.url).toMatch(/^blob:/);
354
+ * // URL.revokeObjectURL is automatically called when the scope ends.
231
355
  * ```
232
356
  *
233
357
  * This ensures the URL is always revoked when the scope ends, even if an error
package/src/Option.ts CHANGED
@@ -9,24 +9,27 @@
9
9
  * ### Example
10
10
  *
11
11
  * ```ts
12
- * // A cache that can store any value, including null and undefined
12
+ * import { isNone, isSome, none, type Option, some } from "@evolu/common";
13
+ *
13
14
  * const cache = new Map<string, Option<unknown>>();
14
15
  *
15
16
  * const get = (key: string): Option<unknown> => cache.get(key) ?? none;
16
17
  *
17
- * cache.set("a", some(null)); // Stored null
18
- * cache.set("b", some(undefined)); // Stored undefined
18
+ * cache.set("a", some(null));
19
+ * cache.set("b", some(undefined));
19
20
  *
20
- * isSome(get("a")); // true — value is null
21
- * isSome(get("b")); // true — value is undefined
22
- * isNone(get("c")); // true — key doesn't exist
21
+ * const a = get("a");
22
+ * assert(isSome(a));
23
+ * expect(a.value).toBeNull();
24
+ * expect(isSome(get("b"))).toBe(true);
25
+ * expect(isNone(get("c"))).toBe(true);
23
26
  * ```
24
27
  *
25
28
  * @module
26
29
  */
27
30
 
28
31
  /** Optional value. */
29
- import type { Typed } from "./Type.js";
32
+ import type { Typed } from "./Type.ts";
30
33
 
31
34
  /** Optional value. */
32
35
  export type Option<T> = Some<T> | None;
package/src/Order.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * @module
5
5
  */
6
6
 
7
+ import type { Eq, eqFromOrder } from "./Eq.ts";
8
+
7
9
  /**
8
10
  * Compares two values of type `A` and returns their ordering.
9
11
  *
@@ -12,6 +14,9 @@
12
14
  * - Returns `-1` if `x` is less than `y`.
13
15
  * - Returns `0` if `x` is equal to `y`.
14
16
  * - Returns `1` if `x` is greater than `y`.
17
+ *
18
+ * An `Order` also defines equality. Use {@link eqFromOrder} to derive an
19
+ * {@link Eq}.
15
20
  */
16
21
  export type Order<in A> = (x: A, y: A) => Ordering;
17
22
 
@@ -28,10 +33,12 @@ export type Ordering = -1 | 0 | 1;
28
33
  * ### Example
29
34
  *
30
35
  * ```ts
36
+ * import { createOrder } from "@evolu/common";
37
+ *
31
38
  * const orderNumber = createOrder<number>((x, y) => x < y);
32
- * expect(orderNumber(1, 2)).toEqual(-1);
33
- * expect(orderNumber(2, 1)).toEqual(1);
34
- * expect(orderNumber(1, 1)).toEqual(0);
39
+ * expect(orderNumber(1, 2)).toBe(-1);
40
+ * expect(orderNumber(2, 1)).toBe(1);
41
+ * expect(orderNumber(1, 1)).toBe(0);
35
42
  * ```
36
43
  */
37
44
  export const createOrder =
@@ -45,9 +52,10 @@ export const createOrder =
45
52
  * ### Example
46
53
  *
47
54
  * ```ts
48
- * reverseOrder(orderNumber)(1, 2); // 1
49
- * reverseOrder(orderNumber)(2, 1); // -1
50
- * reverseOrder(orderNumber)(1, 1); // 0
55
+ * import { orderNumber, reverseOrder } from "@evolu/common";
56
+ *
57
+ * const descendingOrderNumber = reverseOrder(orderNumber);
58
+ * expect([3, 1, 2].toSorted(descendingOrderNumber)).toEqual([3, 2, 1]);
51
59
  * ```
52
60
  */
53
61
  export const reverseOrder =
@@ -61,10 +69,9 @@ export const reverseOrder =
61
69
  * ### Example
62
70
  *
63
71
  * ```ts
64
- * orderString("a", "b"); // -1
65
- * orderString("b", "a"); // 1
66
- * orderString("a", "a"); // 0
67
- * ["c", "b", "a"].toSorted(orderString); // ["a", "b", "c"]
72
+ * import { orderString } from "@evolu/common";
73
+ *
74
+ * expect(["c", "b", "a"].toSorted(orderString)).toEqual(["a", "b", "c"]);
68
75
  * ```
69
76
  */
70
77
  export const orderString: Order<string> = /*#__PURE__*/ createOrder(
@@ -77,13 +84,9 @@ export const orderString: Order<string> = /*#__PURE__*/ createOrder(
77
84
  * ### Example
78
85
  *
79
86
  * ```ts
80
- * orderNumber(1, 2); // -1
81
- * orderNumber(2, 1); // 1
82
- * orderNumber(1, 1); // 0
83
- * [2, 1, 3].toSorted(orderNumber); // [1, 2, 3]
84
- * reverseOrder(orderNumber)(1, 2); // 1
85
- * reverseOrder(orderNumber)(2, 1); // -1
86
- * reverseOrder(orderNumber)(1, 1); // 0
87
+ * import { orderNumber } from "@evolu/common";
88
+ *
89
+ * expect([2, 1, 3].toSorted(orderNumber)).toEqual([1, 2, 3]);
87
90
  * ```
88
91
  */
89
92
  export const orderNumber = /*#__PURE__*/ createOrder<number>((a, b) => a < b);
@@ -94,10 +97,9 @@ export const orderNumber = /*#__PURE__*/ createOrder<number>((a, b) => a < b);
94
97
  * ### Example
95
98
  *
96
99
  * ```ts
97
- * orderBigInt(1n, 2n); // -1
98
- * orderBigInt(2n, 1n); // 1
99
- * orderBigInt(1n, 1n); // 0
100
- * [2n, 1n, 3n].toSorted(orderBigInt); // [1n, 2n, 3n]
100
+ * import { orderBigInt } from "@evolu/common";
101
+ *
102
+ * expect([2n, 1n, 3n].toSorted(orderBigInt)).toEqual([1n, 2n, 3n]);
101
103
  * ```
102
104
  */
103
105
  export const orderBigInt = /*#__PURE__*/ createOrder<bigint>((a, b) => a < b);
package/src/Platform.ts CHANGED
@@ -10,6 +10,22 @@ export const isHermes = "HermesInternal" in globalThis;
10
10
  /** Returns true if running in a server environment (no DOM). */
11
11
  export const isServer = typeof document === "undefined";
12
12
 
13
+ /**
14
+ * True outside production builds.
15
+ *
16
+ * Bundlers (webpack, Vite, esbuild, Metro) replace the text
17
+ * `process.env.NODE_ENV` statically at build time, so in production bundles
18
+ * this expression folds to `false` and code behind it is dead-code-eliminated —
19
+ * the same mechanism React uses. The `typeof` guard makes the runtime read safe
20
+ * where no bundler ran, such as un-bundled browser ESM, where it fails closed
21
+ * to production behavior. Node.js reads it natively; React Native polyfills it.
22
+ * A missing `NODE_ENV` counts as development, matching React semantics.
23
+ */
24
+ export const isDev =
25
+ typeof process === "undefined"
26
+ ? false
27
+ : process.env.NODE_ENV !== "production";
28
+
13
29
  /**
14
30
  * Detects if Node.js Buffer is available and should be used.
15
31
  *
@@ -54,3 +70,123 @@ export type ReloadApp = (url?: string) => void;
54
70
  export interface ReloadAppDep {
55
71
  readonly reloadApp: ReloadApp;
56
72
  }
73
+
74
+ export interface TestGlobalErrors extends Disposable {
75
+ readonly errors: ReadonlyArray<unknown>;
76
+ readonly next: () => Promise<unknown>;
77
+
78
+ /**
79
+ * Waits until every error already reported to the platform has been delivered
80
+ * to this recorder, then returns {@link TestGlobalErrors.errors}.
81
+ *
82
+ * Platforms deliver global errors asynchronously and provide no hook for
83
+ * "nothing was reported", so settle emits a sentinel of the same kind and
84
+ * resolves when it arrives. Platform delivery is ordered, so all earlier
85
+ * errors are recorded by then. Use it to assert absence: `expect(await
86
+ * unhandledRejections.settle()).toEqual([])`.
87
+ */
88
+ readonly settle: () => Promise<ReadonlyArray<unknown>>;
89
+ }
90
+
91
+ /** Records platform global uncaught-error reporting until disposed. */
92
+ export const testGlobalUncaughtErrors = (): TestGlobalErrors =>
93
+ createTestGlobalErrors("uncaughtErrors");
94
+
95
+ /** Records platform global unhandled-rejection reporting until disposed. */
96
+ export const testGlobalUnhandledRejections = (): TestGlobalErrors =>
97
+ createTestGlobalErrors("unhandledRejection");
98
+
99
+ const settleSentinel = new Error("TestGlobalErrors.settle sentinel");
100
+
101
+ const createTestGlobalErrors = (
102
+ kind: "uncaughtErrors" | "unhandledRejection",
103
+ ): TestGlobalErrors => {
104
+ const nodeEvent =
105
+ kind === "uncaughtErrors" ? "uncaughtException" : "unhandledRejection";
106
+ const webEvent = kind === "uncaughtErrors" ? "error" : "unhandledrejection";
107
+ const webErrorKey = kind === "uncaughtErrors" ? "error" : "reason";
108
+ const errorType =
109
+ kind === "uncaughtErrors" ? "uncaught-error" : "unhandled-rejection";
110
+ const disposableStack = new DisposableStack();
111
+ const errors: Array<unknown> = [];
112
+ const nextWaiters: Array<(error: unknown) => void> = [];
113
+ const settleWaiters: Array<() => void> = [];
114
+ let nextIndex = 0;
115
+
116
+ const next = (): Promise<unknown> => {
117
+ if (nextIndex < errors.length) {
118
+ const error = errors[nextIndex];
119
+ nextIndex += 1;
120
+ return Promise.resolve(error);
121
+ }
122
+
123
+ const next = Promise.withResolvers<unknown>();
124
+ nextWaiters.push(next.resolve);
125
+ return next.promise;
126
+ };
127
+
128
+ const record = (error: unknown): void => {
129
+ if (error === settleSentinel) {
130
+ settleWaiters.shift()?.();
131
+ return;
132
+ }
133
+
134
+ errors.push(error);
135
+ const resolveNext = nextWaiters.shift();
136
+ if (!resolveNext) return;
137
+
138
+ nextIndex += 1;
139
+ resolveNext(error);
140
+ };
141
+
142
+ const settle = (): Promise<ReadonlyArray<unknown>> => {
143
+ const settled = Promise.withResolvers<ReadonlyArray<unknown>>();
144
+ settleWaiters.push(() => {
145
+ settled.resolve(errors);
146
+ });
147
+
148
+ if (kind === "unhandledRejection") {
149
+ void Promise.reject(settleSentinel);
150
+ } else {
151
+ queueMicrotask(() => {
152
+ throw settleSentinel;
153
+ });
154
+ }
155
+
156
+ return settled.promise;
157
+ };
158
+
159
+ if (
160
+ typeof process === "object" &&
161
+ typeof process.on === "function" &&
162
+ typeof process.off === "function"
163
+ ) {
164
+ process.on(nodeEvent, record);
165
+ disposableStack.defer(() => {
166
+ process.off(nodeEvent, record);
167
+ });
168
+ } else if (typeof globalThis.addEventListener === "function") {
169
+ const listener = (event: unknown): void => {
170
+ (event as Event).preventDefault();
171
+ record((event as Record<typeof webErrorKey, unknown>)[webErrorKey]);
172
+ };
173
+
174
+ globalThis.addEventListener(webEvent, listener);
175
+ disposableStack.defer(() => {
176
+ globalThis.removeEventListener(webEvent, listener);
177
+ });
178
+ } else {
179
+ throw new Error(`Unsupported platform global ${errorType} reporting.`);
180
+ }
181
+
182
+ return {
183
+ errors,
184
+ next,
185
+ settle,
186
+ [Symbol.dispose]: () => {
187
+ nextWaiters.length = 0;
188
+ settleWaiters.length = 0;
189
+ disposableStack.dispose();
190
+ },
191
+ };
192
+ };
package/src/Polyfills.ts CHANGED
@@ -199,13 +199,10 @@ const installSuppressedError = (): void => {
199
199
  const createDisposableStackPolyfill = (
200
200
  symbolDispose: symbol,
201
201
  ): new () => DisposableStack => {
202
- class DisposableStackPolyfill implements DisposableStack {
202
+ class DisposableStackPolyfill {
203
203
  #disposed = false;
204
204
  #resources: Array<DisposableResource> = [];
205
205
 
206
- declare readonly [Symbol.toStringTag]: "DisposableStack";
207
- declare [Symbol.dispose]: () => void;
208
-
209
206
  get disposed(): boolean {
210
207
  return this.#disposed;
211
208
  }
@@ -278,7 +275,7 @@ const createDisposableStackPolyfill = (
278
275
  this.#resources = [];
279
276
  this.#disposed = true;
280
277
 
281
- return moved;
278
+ return moved as unknown as DisposableStack;
282
279
  }
283
280
 
284
281
  dispose(): void {
@@ -321,20 +318,17 @@ const createDisposableStackPolyfill = (
321
318
  value: "DisposableStack",
322
319
  });
323
320
 
324
- return DisposableStackPolyfill;
321
+ return DisposableStackPolyfill as unknown as new () => DisposableStack;
325
322
  };
326
323
 
327
324
  const createAsyncDisposableStackPolyfill = (
328
325
  symbolDispose: symbol,
329
326
  symbolAsyncDispose: symbol,
330
327
  ): new () => AsyncDisposableStack => {
331
- class AsyncDisposableStackPolyfill implements AsyncDisposableStack {
328
+ class AsyncDisposableStackPolyfill {
332
329
  #disposed = false;
333
330
  #resources: Array<AsyncDisposableResource> = [];
334
331
 
335
- declare readonly [Symbol.toStringTag]: "AsyncDisposableStack";
336
- declare [Symbol.asyncDispose]: () => Promise<void>;
337
-
338
332
  get disposed(): boolean {
339
333
  return this.#disposed;
340
334
  }
@@ -417,7 +411,7 @@ const createAsyncDisposableStackPolyfill = (
417
411
  this.#resources = [];
418
412
  this.#disposed = true;
419
413
 
420
- return moved;
414
+ return moved as unknown as AsyncDisposableStack;
421
415
  }
422
416
 
423
417
  async disposeAsync(): Promise<void> {
@@ -464,7 +458,7 @@ const createAsyncDisposableStackPolyfill = (
464
458
  },
465
459
  );
466
460
 
467
- return AsyncDisposableStackPolyfill;
461
+ return AsyncDisposableStackPolyfill as unknown as new () => AsyncDisposableStack;
468
462
  };
469
463
 
470
464
  /** Installs `Map` and `WeakMap` upsert polyfills missing from the runtime. */