@evolu/common 6.0.1-preview.25 → 6.0.1-preview.26

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 (66) hide show
  1. package/dist/src/Assert.d.ts +0 -13
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +0 -15
  4. package/dist/src/Cache.d.ts +44 -0
  5. package/dist/src/Cache.d.ts.map +1 -0
  6. package/dist/src/Cache.js +52 -0
  7. package/dist/src/Evolu/Db.d.ts +6 -6
  8. package/dist/src/Evolu/Db.d.ts.map +1 -1
  9. package/dist/src/Evolu/Db.js +4 -0
  10. package/dist/src/Evolu/LocalAuth.d.ts +2 -2
  11. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -1
  12. package/dist/src/Evolu/Owner.d.ts +116 -86
  13. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  14. package/dist/src/Evolu/Owner.js +48 -45
  15. package/dist/src/Evolu/Relay.d.ts +6 -5
  16. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  17. package/dist/src/Evolu/Relay.js +40 -39
  18. package/dist/src/Evolu/Storage.d.ts +6 -11
  19. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  20. package/dist/src/Evolu/Storage.js +30 -9
  21. package/dist/src/Evolu/Sync.d.ts +5 -5
  22. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  23. package/dist/src/Evolu/Sync.js +3 -5
  24. package/dist/src/Evolu/Timestamp.d.ts +24 -0
  25. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  26. package/dist/src/Evolu/Timestamp.js +24 -0
  27. package/dist/src/Identicon.d.ts +35 -0
  28. package/dist/src/Identicon.d.ts.map +1 -0
  29. package/dist/src/Identicon.js +143 -0
  30. package/dist/src/ManyToManyMap.d.ts +0 -3
  31. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  32. package/dist/src/Result.d.ts +13 -6
  33. package/dist/src/Result.d.ts.map +1 -1
  34. package/dist/src/Sqlite.d.ts +36 -1
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +56 -3
  37. package/dist/src/Task.d.ts.map +1 -1
  38. package/dist/src/Task.js +36 -0
  39. package/dist/src/Type.d.ts +5 -8
  40. package/dist/src/Type.d.ts.map +1 -1
  41. package/dist/src/Type.js +10 -10
  42. package/dist/src/Types.d.ts +1 -1
  43. package/dist/src/WebSocket.d.ts.map +1 -1
  44. package/dist/src/WebSocket.js +2 -7
  45. package/dist/src/index.d.ts +2 -0
  46. package/dist/src/index.d.ts.map +1 -1
  47. package/dist/src/index.js +2 -0
  48. package/package.json +1 -1
  49. package/src/Assert.ts +0 -19
  50. package/src/Cache.ts +85 -0
  51. package/src/Evolu/Db.ts +11 -7
  52. package/src/Evolu/LocalAuth.ts +19 -7
  53. package/src/Evolu/Owner.ts +140 -103
  54. package/src/Evolu/Relay.ts +52 -48
  55. package/src/Evolu/Storage.ts +47 -23
  56. package/src/Evolu/Sync.ts +11 -12
  57. package/src/Evolu/Timestamp.ts +24 -0
  58. package/src/Identicon.ts +197 -0
  59. package/src/ManyToManyMap.ts +0 -3
  60. package/src/Result.ts +13 -6
  61. package/src/Sqlite.ts +68 -5
  62. package/src/Task.ts +41 -0
  63. package/src/Type.ts +11 -14
  64. package/src/Types.ts +1 -1
  65. package/src/WebSocket.ts +6 -10
  66. package/src/index.ts +2 -0
@@ -114,9 +114,33 @@ export const maxNodeId = "ffffffffffffffff" as NodeId;
114
114
  /**
115
115
  * Hybrid Logical Clock timestamp.
116
116
  *
117
+ * Timestamps serve as globally unique, causally ordered identifiers for CRDT
118
+ * messages in Evolu's sync protocol.
119
+ *
120
+ * ### References
121
+ *
117
122
  * - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
118
123
  * - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
119
124
  * - https://jaredforsyth.com/posts/hybrid-logical-clocks/
125
+ *
126
+ * ### Privacy Considerations
127
+ *
128
+ * Timestamps are metadata visible to relays and collaborators. While it can be
129
+ * considered a privacy leak, let us explain why it's necessary, and how to
130
+ * avoid it if maximum privacy is required.
131
+ *
132
+ * With real-time communication, participants always see activity (receiving
133
+ * bytes). We cannot trust anyone not to store that information, so explicitly
134
+ * exposing timestamps doesn't add additional risk.
135
+ *
136
+ * If we really want not to leak user activity, we can implement a local write
137
+ * queue:
138
+ *
139
+ * 1. Write changes immediately to a local-only table
140
+ * 2. Periodically/randomly flush messages to sync tables
141
+ * 3. This decouples user activity from sync timing
142
+ *
143
+ * Tradeoff: It breaks real-time collaboration.
120
144
  */
121
145
  export const Timestamp = object({
122
146
  millis: Millis,
@@ -0,0 +1,197 @@
1
+ import type { Brand } from "./Brand.js";
2
+ import { Id, idToIdBytes } from "./Type.js";
3
+ import { md5 } from "@noble/hashes/legacy.js";
4
+
5
+ /**
6
+ * SVG string representing a visual identicon for an {@link Id}, created with
7
+ * {@link createIdenticon}.
8
+ */
9
+ export type Identicon = string & Brand<"Identicon">;
10
+
11
+ /** {@link Identicon} style. */
12
+ export type IdenticonStyle = "github" | "quadrant" | "gradient" | "sutnar";
13
+
14
+ /**
15
+ * Creates a deterministic identicon SVG from an {@link Id}.
16
+ *
17
+ * Works with any {@link Id} including branded IDs like `OwnerId`, etc.
18
+ *
19
+ * Available styles:
20
+ *
21
+ * - `"github"` (default): 5x5 grid with horizontal mirroring (GitHub-style)
22
+ * - `"quadrant"`: 2x2 grid with direct RGB color mapping from bytes
23
+ * - `"gradient"`: Diagonal stripes with smooth color gradients
24
+ * - `"sutnar"`: Three compositional variants with adaptive colors
25
+ *
26
+ * ### Example
27
+ *
28
+ * ```ts
29
+ * const svg = createIdenticon(id);
30
+ * const quadrantStyle = createIdenticon(id, "quadrant");
31
+ * const gradientStyle = createIdenticon(id, "gradient");
32
+ * const sutnarStyle = createIdenticon(id, "sutnar");
33
+ *
34
+ * // Works with branded IDs
35
+ * const ownerSvg = createIdenticon(ownerId);
36
+ * ```
37
+ */
38
+ export const createIdenticon = (
39
+ id: Id,
40
+ style: IdenticonStyle = "github",
41
+ ): Identicon => {
42
+ const bytes = idToIdBytes(id);
43
+
44
+ switch (style) {
45
+ case "github": {
46
+ // GitHub-style identicon: MD5 hash the bytes first
47
+ const hashedBytes = md5(bytes);
48
+
49
+ // Map function for value ranges
50
+ const map = (
51
+ value: number,
52
+ inMin: number,
53
+ inMax: number,
54
+ outMin: number,
55
+ outMax: number,
56
+ ): number =>
57
+ ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
58
+
59
+ // Extract 12-bit hue from bytes[12] (lower 4 bits) + bytes[13]
60
+ const h = ((hashedBytes[12] & 0x0f) << 8) | hashedBytes[13];
61
+ const hue = map(h, 0, 4095, 0, 360);
62
+ const saturation = 65 - map(hashedBytes[14], 0, 255, 0, 20);
63
+ const lightness = 75 - map(hashedBytes[15], 0, 255, 0, 20);
64
+
65
+ const fgColor = `hsl(${hue},${saturation}%,${lightness}%)`;
66
+ const bgColor = `hsl(${hue},${saturation}%,90%)`;
67
+
68
+ let rects = `<rect width="5" height="5" fill="${bgColor}"/>`;
69
+
70
+ // Extract nibbles and generate pattern
71
+ let nibbleIndex = 0;
72
+ for (let x = 2; x >= 0; x--) {
73
+ for (let y = 0; y < 5; y++) {
74
+ const byte = hashedBytes[Math.floor(nibbleIndex / 2)];
75
+ const nibble = nibbleIndex % 2 === 0 ? byte >> 4 : byte & 0x0f;
76
+ const paint = nibble % 2 === 0;
77
+ nibbleIndex++;
78
+
79
+ if (paint) {
80
+ rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${fgColor}"/>`;
81
+ const mx = 4 - x;
82
+ if (mx !== x) {
83
+ rects += `<rect x="${mx}" y="${y}" width="1" height="1" fill="${fgColor}"/>`;
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 5" shape-rendering="crispEdges">${rects}</svg>` as Identicon;
90
+ }
91
+
92
+ case "quadrant": {
93
+ const toHex = (b: number): string => b.toString(16).padStart(2, "0");
94
+ let rects = "";
95
+ for (let i = 0; i < 4; i++) {
96
+ const x = i % 2;
97
+ const y = Math.floor(i / 2);
98
+ const r = bytes[i * 3];
99
+ const g = bytes[i * 3 + 1];
100
+ const b = bytes[i * 3 + 2];
101
+ const color = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
102
+ rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${color}"/>`;
103
+ }
104
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2">${rects}</svg>` as Identicon;
105
+ }
106
+
107
+ case "gradient": {
108
+ // Smooth color gradients with diagonal stripes.
109
+ const toHex = (b: number): string => b.toString(16).padStart(2, "0");
110
+
111
+ // Generate colors from bytes.
112
+ const color1 = `#${toHex(bytes[0])}${toHex(bytes[1])}${toHex(bytes[2])}`;
113
+ const color2 = `#${toHex(bytes[3])}${toHex(bytes[4])}${toHex(bytes[5])}`;
114
+ const color3 = `#${toHex(bytes[6])}${toHex(bytes[7])}${toHex(bytes[8])}`;
115
+
116
+ let defs = "";
117
+ let shapes = "";
118
+
119
+ // Diagonal stripes with gradient.
120
+ defs += `<linearGradient id="grad1-${id}" x1="0%" y1="0%" x2="0%" y2="100%">`;
121
+ defs += `<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />`;
122
+ defs += `<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />`;
123
+ defs += `</linearGradient>`;
124
+
125
+ defs += `<linearGradient id="grad2-${id}" x1="0%" y1="0%" x2="0%" y2="100%">`;
126
+ defs += `<stop offset="0%" style="stop-color:${color2};stop-opacity:1" />`;
127
+ defs += `<stop offset="100%" style="stop-color:${color3};stop-opacity:1" />`;
128
+ defs += `</linearGradient>`;
129
+
130
+ shapes += `<rect width="100" height="100" fill="url(#grad1-${id})"/>`;
131
+
132
+ const stripeWidth = 15 + (bytes[9] / 255) * 20;
133
+ const angle = 30 + (bytes[10] / 255) * 60;
134
+
135
+ shapes += `<rect x="20" y="-50" width="${stripeWidth}" height="200" fill="url(#grad2-${id})" transform="rotate(${angle} 50 50)" opacity="0.7"/>`;
136
+ shapes += `<rect x="60" y="-50" width="${stripeWidth}" height="200" fill="url(#grad2-${id})" transform="rotate(${angle} 50 50)" opacity="0.5"/>`;
137
+
138
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs>${defs}</defs>${shapes}</svg>` as Identicon;
139
+ }
140
+
141
+ case "sutnar": {
142
+ // Three compositional variants with adaptive colors.
143
+ const hue = (bytes[0] / 255) * 360;
144
+ const saturation = 50 + (bytes[1] / 255) * 30;
145
+ const lightness = 50 + (bytes[2] / 255) * 20;
146
+
147
+ // Generate palette from base hue with variations
148
+ const toHsl = (h: number, s: number, l: number) =>
149
+ `hsl(${h},${s}%,${l}%)`;
150
+
151
+ const color1 = toHsl(hue, saturation, lightness);
152
+ const color2 = toHsl((hue + 120) % 360, saturation, lightness);
153
+ const color3 = toHsl((hue + 240) % 360, saturation, lightness);
154
+ const color4 = toHsl(hue, saturation * 0.3, lightness * 0.5);
155
+ const color5 = toHsl(
156
+ hue,
157
+ saturation * 0.5,
158
+ Math.min(lightness * 1.3, 90),
159
+ );
160
+
161
+ const palette = [color1, color2, color3, color4, color5] as const;
162
+
163
+ // Layout variant based on first byte.
164
+ const variant = bytes[3] % 3;
165
+
166
+ let shapes = "";
167
+
168
+ // Almost white background with subtle tint.
169
+ shapes += `<rect width="100" height="100" fill="${toHsl(hue, 10, 95)}"/>`;
170
+
171
+ if (variant === 0) {
172
+ // Composition A: Circle + horizontal bar.
173
+ const circleColor = palette[bytes[4] % palette.length];
174
+ const barColor = palette[(bytes[4] + 1) % palette.length];
175
+
176
+ shapes += `<circle cx="30" cy="50" r="22" fill="${circleColor}"/>`;
177
+ shapes += `<rect x="60" y="40" width="35" height="20" fill="${barColor}"/>`;
178
+ } else if (variant === 1) {
179
+ // Composition B: Vertical bar + circle.
180
+ const barColor = palette[bytes[5] % palette.length];
181
+ const circleColor = palette[(bytes[5] + 1) % palette.length];
182
+
183
+ shapes += `<rect x="15" y="10" width="18" height="80" fill="${barColor}"/>`;
184
+ shapes += `<circle cx="70" cy="50" r="15" fill="${circleColor}"/>`;
185
+ } else {
186
+ // Composition C: Square + circle.
187
+ const squareColor = palette[bytes[6] % palette.length];
188
+ const circleColor = palette[(bytes[6] + 1) % palette.length];
189
+
190
+ shapes += `<rect x="20" y="20" width="30" height="30" fill="${squareColor}"/>`;
191
+ shapes += `<circle cx="70" cy="70" r="18" fill="${circleColor}"/>`;
192
+ }
193
+
194
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${shapes}</svg>` as Identicon;
195
+ }
196
+ }
197
+ };
@@ -22,9 +22,6 @@ import { assert } from "./Assert.js";
22
22
  * - `deleteKey` and `deleteValue` are O(d) where d is the number of associated
23
23
  * values / keys (the degree). This is optimal because every associated pair
24
24
  * must be touched once.
25
- * - In the Relay use case a socket (value) typically has only dozens of owners
26
- * (degree small), and connection closes (triggering deleteValue) are
27
- * relatively infrequent, so O(d) is acceptable.
28
25
  *
29
26
  * Object identity:
30
27
  *
package/src/Result.ts CHANGED
@@ -87,8 +87,8 @@
87
87
  * ### Naming Convention
88
88
  *
89
89
  * - For values: `const user = getUser()`
90
- * - For void operations: `const result = foo()` (unless it would clash)
91
- * - For clashes, suffix the name: `const saveResult = save()`
90
+ * - For a single void operation: `const result = foo()`
91
+ * - For multiple void operations: use descriptive names for all
92
92
  *
93
93
  * ```ts
94
94
  * const processUser = () => {
@@ -96,13 +96,20 @@
96
96
  * const user = getUser();
97
97
  * if (!user.ok) return user;
98
98
  *
99
- * // void operation
99
+ * // single void operation
100
100
  * const result = saveToDatabase(user.value);
101
101
  * if (!result.ok) return result;
102
102
  *
103
- * // avoiding clash
104
- * const deleteFromCacheResult = deleteFromCache();
105
- * if (!deleteFromCacheResult.ok) return deleteFromCacheResult;
103
+ * return ok();
104
+ * };
105
+ *
106
+ * const setupDatabase = () => {
107
+ * // multiple void operations - use descriptive names
108
+ * const baseTables = createBaseTables();
109
+ * if (!baseTables.ok) return baseTables;
110
+ *
111
+ * const relayTables = createRelayTables();
112
+ * if (!relayTables.ok) return relayTables;
106
113
  *
107
114
  * return ok();
108
115
  * };
package/src/Sqlite.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import { Brand } from "./Brand.js";
2
+ import { createLruCache } from "./Cache.js";
2
3
  import { ConsoleDep } from "./Console.js";
3
4
  import { EncryptionKey } from "./Crypto.js";
4
5
  import { createTransferableError, TransferableError } from "./Error.js";
5
6
  import { err, ok, Result, tryAsync, trySync } from "./Result.js";
6
- import { Null, Number, SimpleName, String, Uint8Array, union } from "./Type.js";
7
+ import {
8
+ Null,
9
+ Number,
10
+ PositiveInt,
11
+ SimpleName,
12
+ String,
13
+ Uint8Array,
14
+ union,
15
+ } from "./Type.js";
7
16
  import { IntentionalNever, Predicate } from "./Types.js";
8
17
 
9
18
  /**
@@ -294,7 +303,38 @@ export interface RawSql {
294
303
 
295
304
  export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
296
305
 
297
- /** TODO: Docs. */
306
+ /**
307
+ * Creates a safe SQL query using a tagged template literal.
308
+ *
309
+ * Parameters are automatically escaped and bound as SQLite values. Use
310
+ * `sql.identifier` for column/table names and `sql.raw` for unescaped SQL.
311
+ *
312
+ * ### Example
313
+ *
314
+ * ```ts
315
+ * const id = 42;
316
+ * const name = "Alice";
317
+ *
318
+ * const result = sqlite.exec(sql`
319
+ * select *
320
+ * from users
321
+ * where id = ${id} and name = ${name};
322
+ * `);
323
+ *
324
+ * // For identifiers
325
+ * const tableName = "users";
326
+ * sqlite.exec(sql`
327
+ * create table ${sql.identifier(tableName)} (
328
+ * "id" text primary key,
329
+ * "name" text not null
330
+ * );
331
+ * `);
332
+ *
333
+ * // For raw SQL (use with caution)
334
+ * const orderBy = "created_at desc";
335
+ * sqlite.exec(sql`select * from users order by ${sql.raw(orderBy)};`);
336
+ * ```
337
+ */
298
338
  export const sql = (
299
339
  strings: TemplateStringsArray,
300
340
  ...parameters: Array<SqlTemplateParam>
@@ -342,6 +382,29 @@ sql.prepared = (
342
382
  return { ...query, options: { prepare: true } };
343
383
  };
344
384
 
385
+ /**
386
+ * Checks if a SQL string contains mutation keywords (insert, update, delete,
387
+ * etc.). Results are cached for performance.
388
+ */
389
+ export const isSqlMutation: Predicate<string> = (sql) => {
390
+ /**
391
+ * Without cache, "insert 1_000_000" Storage test dropped from 57742
392
+ * inserts/sec to 34k. Regex we used was fast, but CodeQL flagged it as a
393
+ * potential ReDoS vulnerability, so manual comment removal was the only
394
+ * option. LRU cache restores performance.
395
+ */
396
+ const cached = isSqlMutationCache.get(sql);
397
+ if (cached !== undefined) return cached;
398
+
399
+ const result = isSqlMutationRegEx.test(removeSqlComments(sql));
400
+ isSqlMutationCache.set(sql, result);
401
+ return result;
402
+ };
403
+
404
+ const isSqlMutationCache = createLruCache<string, boolean>(
405
+ PositiveInt.orThrow(10_000),
406
+ );
407
+
345
408
  const isSqlMutationRegEx = new RegExp(
346
409
  `\\b(${[
347
410
  "alter",
@@ -365,6 +428,9 @@ const isSqlMutationRegEx = new RegExp(
365
428
  * ReDoS vulnerabilities.
366
429
  */
367
430
  const removeSqlComments = (sql: string): string => {
431
+ // Fast path: if there are no comments, return the original string
432
+ if (!sql.includes("--")) return sql;
433
+
368
434
  let result = "";
369
435
  let i = 0;
370
436
 
@@ -390,9 +456,6 @@ const removeSqlComments = (sql: string): string => {
390
456
  return result;
391
457
  };
392
458
 
393
- export const isSqlMutation: Predicate<string> = (sql) =>
394
- isSqlMutationRegEx.test(removeSqlComments(sql));
395
-
396
459
  export interface SqliteQueryPlanRow {
397
460
  id: number;
398
461
  parent: number;
package/src/Task.ts CHANGED
@@ -216,6 +216,32 @@ const isAbortError = (error: unknown): error is AbortError =>
216
216
  error !== null &&
217
217
  (error as { type?: unknown }).type === "AbortError";
218
218
 
219
+ // For React Native
220
+ if (typeof AbortSignal.any !== "function") {
221
+ AbortSignal.any = function (signals: Array<AbortSignal>): AbortSignal {
222
+ const controller = new AbortController();
223
+
224
+ const onAbort = (event: Event) => {
225
+ controller.abort((event.target as AbortSignal).reason);
226
+ cleanup();
227
+ };
228
+
229
+ const cleanup = () => {
230
+ for (const s of signals) s.removeEventListener("abort", onAbort);
231
+ };
232
+
233
+ for (const s of signals) {
234
+ if (s.aborted) {
235
+ controller.abort(s.reason);
236
+ return controller.signal;
237
+ }
238
+ s.addEventListener("abort", onAbort);
239
+ }
240
+
241
+ return controller.signal;
242
+ };
243
+ }
244
+
219
245
  /**
220
246
  * Combines user signal from context with an internal signal.
221
247
  *
@@ -304,6 +330,21 @@ export const toTask = <T, E>(
304
330
  ]);
305
331
  }) as Task<T, E>;
306
332
 
333
+ // For React Native
334
+ if (typeof AbortSignal.timeout !== "function") {
335
+ AbortSignal.timeout = function (ms: number): AbortSignal {
336
+ const controller = new AbortController();
337
+ const id = setTimeout(() => {
338
+ controller.abort();
339
+ }, ms);
340
+ // clear timeout if aborted early
341
+ controller.signal.addEventListener("abort", () => {
342
+ clearTimeout(id);
343
+ });
344
+ return controller.signal;
345
+ };
346
+ }
347
+
307
348
  /**
308
349
  * Creates a {@link Task} that waits for the specified duration.
309
350
  *
package/src/Type.ts CHANGED
@@ -253,7 +253,6 @@ export interface Type<
253
253
  *
254
254
  * - When you need to convert a validation result to a nullable value
255
255
  * - When the error is not important and you just want the value or nothing
256
- * - APIs that expect `T | null`
257
256
  *
258
257
  * ### Example
259
258
  *
@@ -370,8 +369,6 @@ export interface Type<
370
369
  readonly ParentError: ParentError;
371
370
 
372
371
  /**
373
- * Error | ParentError
374
- *
375
372
  * ### Example
376
373
  *
377
374
  * ```ts
@@ -461,7 +458,7 @@ export type InferParentError<A extends AnyType> =
461
458
  : never;
462
459
 
463
460
  /**
464
- * Extracts all error types (Error | ParentError) from a {@link Type}.
461
+ * Extracts all error types from a {@link Type}.
465
462
  *
466
463
  * @category Utilities
467
464
  */
@@ -1150,7 +1147,7 @@ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
1150
1147
  export interface TrimmedError extends TypeError<"Trimmed"> {}
1151
1148
 
1152
1149
  export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
1153
- (error) => `The value ${error.value} is not trimmed.`,
1150
+ (error) => `The value ${error.value} must be trimmed.`,
1154
1151
  );
1155
1152
 
1156
1153
  /**
@@ -1717,7 +1714,7 @@ export const idBytesToId = (idBytes: IdBytes): Id =>
1717
1714
  uint8ArrayToBase64Url(idBytes) as unknown as Id;
1718
1715
 
1719
1716
  /**
1720
- * Positive number.
1717
+ * Positive number (> 0).
1721
1718
  *
1722
1719
  * ### Example
1723
1720
  *
@@ -1740,11 +1737,11 @@ export const positive: BrandFactory<"Positive", number, PositiveError> = (
1740
1737
  export interface PositiveError extends TypeError<"Positive"> {}
1741
1738
 
1742
1739
  export const formatPositiveError = createTypeErrorFormatter<PositiveError>(
1743
- (error) => `The value ${error.value} is not positive.`,
1740
+ (error) => `The value ${error.value} must be positive (> 0).`,
1744
1741
  );
1745
1742
 
1746
1743
  /**
1747
- * Negative number.
1744
+ * Negative number (< 0).
1748
1745
  *
1749
1746
  * ### Example
1750
1747
  *
@@ -1764,11 +1761,11 @@ export const negative: BrandFactory<"Negative", number, NegativeError> = (
1764
1761
  export interface NegativeError extends TypeError<"Negative"> {}
1765
1762
 
1766
1763
  export const formatNegativeError = createTypeErrorFormatter<NegativeError>(
1767
- (error) => `The value ${error.value} is not negative.`,
1764
+ (error) => `The value ${error.value} must be negative (< 0).`,
1768
1765
  );
1769
1766
 
1770
1767
  /**
1771
- * Non-positive number.
1768
+ * Non-positive number (≤ 0).
1772
1769
  *
1773
1770
  * ### Example
1774
1771
  *
@@ -1797,7 +1794,7 @@ export const formatNonPositiveError =
1797
1794
  );
1798
1795
 
1799
1796
  /**
1800
- * Non-negative number.
1797
+ * Non-negative number (≥ 0).
1801
1798
  *
1802
1799
  * ### Example
1803
1800
  *
@@ -1862,7 +1859,7 @@ export const int: BrandFactory<"Int", number, IntError> = (parent) =>
1862
1859
  export interface IntError extends TypeError<"Int"> {}
1863
1860
 
1864
1861
  export const formatIntError = createTypeErrorFormatter<IntError>(
1865
- (error) => `The value ${error.value} is not an integer.`,
1862
+ (error) => `The value ${error.value} must be an integer.`,
1866
1863
  );
1867
1864
 
1868
1865
  /**
@@ -2008,7 +2005,7 @@ export const nonNaN: BrandFactory<"NonNaN", number, NonNaNError> = (parent) =>
2008
2005
  export interface NonNaNError extends TypeError<"NonNaN"> {}
2009
2006
 
2010
2007
  export const formatNonNaNError = createTypeErrorFormatter<NonNaNError>(
2011
- (error) => `The value ${error.value} is NaN (not a number).`,
2008
+ () => `The value must not be NaN.`,
2012
2009
  );
2013
2010
 
2014
2011
  /** @category Number */
@@ -2030,7 +2027,7 @@ export const finite: BrandFactory<"Finite", number, FiniteError> = (parent) =>
2030
2027
  export interface FiniteError extends TypeError<"Finite"> {}
2031
2028
 
2032
2029
  export const formatFiniteError = createTypeErrorFormatter<FiniteError>(
2033
- (error) => `The value ${error.value} is not finite.`,
2030
+ (error) => `The value ${error.value} must be finite.`,
2034
2031
  );
2035
2032
 
2036
2033
  /**
package/src/Types.ts CHANGED
@@ -79,7 +79,7 @@ export type NullablePartial<
79
79
  export type IntentionalNever = never;
80
80
 
81
81
  /**
82
- * String | number | bigint | boolean | undefined | null
82
+ * String, number, bigint, boolean, undefined, null
83
83
  *
84
84
  * https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
85
85
  */
package/src/WebSocket.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import { assertNoErrorInCatch } from "./Assert.js";
8
7
  import { constVoid } from "./Function.js";
9
8
  import { err, ok, Result } from "./Result.js";
10
9
  import { retry, RetryError, RetryOptions } from "./Task.js";
@@ -192,7 +191,7 @@ export const createWebSocket: CreateWebSocket = (
192
191
  * - Is rejected when a connection is closed.
193
192
  * - Is resolved when WebSocket is disposed().
194
193
  */
195
- retry(
194
+ void retry(
196
195
  {
197
196
  ...defaultRetryOptions,
198
197
  ...retryOptions,
@@ -222,6 +221,7 @@ export const createWebSocket: CreateWebSocket = (
222
221
  ? { type: "WebSocketConnectionError", event }
223
222
  : { type: "WebSocketConnectError", event };
224
223
  onError?.(error);
224
+
225
225
  // Trigger reconnect only on WebSocketConnectError.
226
226
  if (error.type === "WebSocketConnectError") {
227
227
  resolve(err(error));
@@ -237,14 +237,10 @@ export const createWebSocket: CreateWebSocket = (
237
237
  onMessage?.(event.data as string | ArrayBuffer | Blob);
238
238
  };
239
239
  }),
240
- )(reconnectController)
241
- .then((result) => {
242
- if (result.ok || result.error.type === "AbortError") return;
243
- onError?.(result.error as WebSocketError);
244
- })
245
- .catch((error: unknown) => {
246
- assertNoErrorInCatch("WebSocket retry", error);
247
- });
240
+ )(reconnectController).then((result) => {
241
+ if (result.ok || result.error.type === "AbortError") return;
242
+ onError?.(result.error as WebSocketError);
243
+ });
248
244
 
249
245
  return {
250
246
  send: (data) => {
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./Assert.js";
3
3
  export * from "./BigInt.js";
4
4
  export * from "./Brand.js";
5
5
  export * from "./Buffer.js";
6
+ export * from "./Cache.js";
6
7
  export * from "./CallbackRegistry.js";
7
8
  export * from "./Console.js";
8
9
  export * from "./Crypto.js";
@@ -10,6 +11,7 @@ export * from "./Eq.js";
10
11
  export * from "./Error.js";
11
12
  export * from "./Evolu/Public.js";
12
13
  export * from "./Function.js";
14
+ export * from "./Identicon.js";
13
15
  export * from "./ManyToManyMap.js";
14
16
  export * from "./Number.js";
15
17
  export * from "./Object.js";