@evolu/common 7.2.3 → 7.3.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 (68) hide show
  1. package/dist/src/Function.d.ts +61 -0
  2. package/dist/src/Function.d.ts.map +1 -1
  3. package/dist/src/Function.js +19 -0
  4. package/dist/src/Object.d.ts +16 -0
  5. package/dist/src/Object.d.ts.map +1 -1
  6. package/dist/src/Object.js +16 -0
  7. package/dist/src/Result.d.ts +36 -29
  8. package/dist/src/Result.d.ts.map +1 -1
  9. package/dist/src/Sqlite.d.ts +9 -7
  10. package/dist/src/Sqlite.d.ts.map +1 -1
  11. package/dist/src/Sqlite.js +8 -0
  12. package/dist/src/Task.d.ts.map +1 -1
  13. package/dist/src/Task.js +6 -4
  14. package/dist/src/Type.d.ts +30 -2
  15. package/dist/src/Type.d.ts.map +1 -1
  16. package/dist/src/Type.js +82 -0
  17. package/dist/src/local-first/Db.d.ts +1 -5
  18. package/dist/src/local-first/Db.d.ts.map +1 -1
  19. package/dist/src/local-first/Db.js +109 -108
  20. package/dist/src/local-first/Evolu.d.ts +3 -0
  21. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  22. package/dist/src/local-first/Evolu.js +6 -7
  23. package/dist/src/local-first/LocalAuth.d.ts +2 -2
  24. package/dist/src/local-first/Owner.d.ts +3 -0
  25. package/dist/src/local-first/Owner.d.ts.map +1 -1
  26. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  27. package/dist/src/local-first/Protocol.js +10 -10
  28. package/dist/src/local-first/PublicKysely.js +1 -1
  29. package/dist/src/local-first/Query.d.ts +54 -6
  30. package/dist/src/local-first/Query.d.ts.map +1 -1
  31. package/dist/src/local-first/Query.js +130 -11
  32. package/dist/src/local-first/Relay.d.ts.map +1 -1
  33. package/dist/src/local-first/Relay.js +17 -18
  34. package/dist/src/local-first/Schema.d.ts +15 -14
  35. package/dist/src/local-first/Schema.d.ts.map +1 -1
  36. package/dist/src/local-first/Schema.js +43 -51
  37. package/dist/src/local-first/Storage.d.ts +2 -2
  38. package/dist/src/local-first/Storage.d.ts.map +1 -1
  39. package/dist/src/local-first/Storage.js +2 -6
  40. package/dist/src/local-first/Sync.d.ts +9 -4
  41. package/dist/src/local-first/Sync.d.ts.map +1 -1
  42. package/dist/src/local-first/Sync.js +132 -59
  43. package/dist/src/local-first/index.d.ts +0 -1
  44. package/dist/src/local-first/index.d.ts.map +1 -1
  45. package/dist/src/local-first/index.js +0 -1
  46. package/package.json +1 -1
  47. package/src/Function.ts +74 -0
  48. package/src/Object.ts +20 -0
  49. package/src/Result.ts +38 -29
  50. package/src/Sqlite.ts +18 -7
  51. package/src/Task.ts +6 -4
  52. package/src/Type.ts +134 -1
  53. package/src/local-first/Db.ts +181 -209
  54. package/src/local-first/Evolu.ts +9 -6
  55. package/src/local-first/LocalAuth.ts +2 -2
  56. package/src/local-first/Owner.ts +4 -0
  57. package/src/local-first/Protocol.ts +10 -16
  58. package/src/local-first/PublicKysely.ts +1 -1
  59. package/src/local-first/Query.ts +216 -21
  60. package/src/local-first/Relay.ts +20 -24
  61. package/src/local-first/Schema.ts +83 -70
  62. package/src/local-first/Storage.ts +3 -8
  63. package/src/local-first/Sync.ts +190 -66
  64. package/src/local-first/index.ts +0 -1
  65. package/dist/src/local-first/Diff.d.ts +0 -43
  66. package/dist/src/local-first/Diff.d.ts.map +0 -1
  67. package/dist/src/local-first/Diff.js +0 -97
  68. package/src/local-first/Diff.ts +0 -144
package/src/Function.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { NonEmptyArray, NonEmptyReadonlyArray } from "./Array.js";
2
+ import { ReadonlyRecord } from "./Object.js";
3
+
1
4
  /**
2
5
  * Helper function to ensure exhaustive matching in a switch statement. Throws
3
6
  * an error if an unhandled case is encountered.
@@ -31,8 +34,79 @@ export const exhaustiveCheck = (value: never): never => {
31
34
  throw new Error(`exhaustiveCheck unhandled case: ${JSON.stringify(value)}`);
32
35
  };
33
36
 
37
+ /**
38
+ * Returns the input value unchanged.
39
+ *
40
+ * Useful as a default transformation, placeholder callback, or when a function
41
+ * is required but no transformation is needed.
42
+ *
43
+ * ### Example
44
+ *
45
+ * ```ts
46
+ * const values = [1, 2, 3];
47
+ * const same = values.map(identity); // [1, 2, 3]
48
+ *
49
+ * const getTransform = (shouldDouble: boolean) =>
50
+ * shouldDouble ? (x: number) => x * 2 : identity;
51
+ * ```
52
+ */
34
53
  export const identity = <A>(a: A): A => a;
35
54
 
55
+ /**
56
+ * Casts an array, set, record, or map to its readonly counterpart.
57
+ *
58
+ * Zero runtime cost — returns the same value with a readonly type. Use this to
59
+ * enforce immutability at the type level. Preserves {@link NonEmptyArray} as
60
+ * {@link NonEmptyReadonlyArray}.
61
+ *
62
+ * ### Example
63
+ *
64
+ * ```ts
65
+ * // Array literals become NonEmptyReadonlyArray
66
+ * const items = readonly([1, 2, 3]);
67
+ * // Type: NonEmptyReadonlyArray<number>
68
+ *
69
+ * // NonEmptyArray is preserved as NonEmptyReadonlyArray
70
+ * const nonEmpty: NonEmptyArray<number> = [1, 2, 3];
71
+ * const readonlyNonEmpty = readonly(nonEmpty);
72
+ * // Type: NonEmptyReadonlyArray<number>
73
+ *
74
+ * // Regular arrays become ReadonlyArray
75
+ * const arr: Array<number> = getNumbers();
76
+ * const readonlyArr = readonly(arr);
77
+ * // Type: ReadonlyArray<number>
78
+ *
79
+ * // Sets, Records, and Maps
80
+ * const ids = readonly(new Set(["a", "b"]));
81
+ * // Type: ReadonlySet<string>
82
+ *
83
+ * const users: Record<UserId, string> = { ... };
84
+ * const readonlyUsers = readonly(users);
85
+ * // Type: ReadonlyRecord<UserId, string>
86
+ *
87
+ * const lookup = readonly(new Map([["key", "value"]]));
88
+ * // Type: ReadonlyMap<string, string>
89
+ * ```
90
+ *
91
+ * @experimental
92
+ */
93
+ export function readonly<T>(array: NonEmptyArray<T>): NonEmptyReadonlyArray<T>;
94
+ export function readonly<T>(array: Array<T>): ReadonlyArray<T>;
95
+ export function readonly<T>(set: Set<T>): ReadonlySet<T>;
96
+ export function readonly<K, V>(map: Map<K, V>): ReadonlyMap<K, V>;
97
+ export function readonly<K extends keyof any, V>(
98
+ record: Record<K, V>,
99
+ ): ReadonlyRecord<K, V>;
100
+ export function readonly<T, K extends keyof any, V>(
101
+ value: Array<T> | Set<T> | Map<K, V> | Record<K, V>,
102
+ ):
103
+ | ReadonlyArray<T>
104
+ | ReadonlySet<T>
105
+ | ReadonlyMap<K, V>
106
+ | ReadonlyRecord<K, V> {
107
+ return value;
108
+ }
109
+
36
110
  /**
37
111
  * A function that delays computation and returns a value of type T.
38
112
  *
package/src/Object.ts CHANGED
@@ -89,3 +89,23 @@ export const createRecord = <K extends string = string, V = unknown>(): Record<
89
89
  K,
90
90
  V
91
91
  > => Object.create(null) as Record<K, V>;
92
+
93
+ /**
94
+ * Safely gets a property from a record, returning `undefined` if the key
95
+ * doesn't exist.
96
+ *
97
+ * TypeScript's `Record<K, V>` type assumes all keys exist, but at runtime
98
+ * accessing a non-existent key returns `undefined`. This helper provides proper
99
+ * typing for that case without needing a type assertion.
100
+ *
101
+ * ### Example
102
+ *
103
+ * ```ts
104
+ * const users: Record<string, User> = { alice: { name: "Alice" } };
105
+ * const user = getProperty(users, "bob"); // User | undefined
106
+ * ```
107
+ */
108
+ export const getProperty = <K extends string, V>(
109
+ record: ReadonlyRecord<K, V>,
110
+ key: string,
111
+ ): V | undefined => (key in record ? record[key as K] : undefined);
package/src/Result.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { Task } from "./Task.js";
2
+
1
3
  /**
2
4
  * The problem with throwing an exception in JavaScript is that the caught error
3
5
  * is always of an unknown type. The unknown type is a problem because we can't
@@ -82,19 +84,22 @@
82
84
  * };
83
85
  * ```
84
86
  *
87
+ * For lazy, cancellable async operations, see {@link Task}.
88
+ *
85
89
  * ### Naming convention
86
90
  *
87
- * - For values: `const user = getUser()`
88
- * - For a single void operation: `const result = foo()`
89
- * - For multiple void operations: use descriptive names for all
91
+ * - **For values you need:** use a name without Result suffix (`user`, `config`)
92
+ * - **For void operations:** use `result` (no value to name)
93
+ *
94
+ * For multiple void operations, use block scopes to avoid potentially long
95
+ * names like `createBaseTablesResult`, `createRelayTablesResult`, or counters
96
+ * like `result1`, `result2`:
90
97
  *
91
98
  * ```ts
92
99
  * const processUser = () => {
93
- * // we have a value
94
100
  * const user = getUser();
95
101
  * if (!user.ok) return user;
96
102
  *
97
- * // single void operation
98
103
  * const result = saveToDatabase(user.value);
99
104
  * if (!result.ok) return result;
100
105
  *
@@ -102,12 +107,15 @@
102
107
  * };
103
108
  *
104
109
  * const setupDatabase = () => {
105
- * // multiple void operations - use descriptive names
106
- * const baseTables = createBaseTables();
107
- * if (!baseTables.ok) return baseTables;
108
- *
109
- * const relayTables = createRelayTables();
110
- * if (!relayTables.ok) return relayTables;
110
+ * // Multiple void operations - use block scopes to avoid name clash
111
+ * {
112
+ * const result = createBaseTables();
113
+ * if (!result.ok) return result;
114
+ * }
115
+ * {
116
+ * const result = createRelayTables();
117
+ * if (!result.ok) return result;
118
+ * }
111
119
  *
112
120
  * return ok();
113
121
  * };
@@ -124,30 +132,31 @@
124
132
  * schema, and initializes the database, stopping on the first error:
125
133
  *
126
134
  * ```ts
127
- * const resetResult = deps.sqlite.transaction(() => {
128
- * const dropAllTablesResult = dropAllTables(deps);
129
- * if (!dropAllTablesResult.ok) return dropAllTablesResult;
135
+ * const result = deps.sqlite.transaction(() => {
136
+ * const result = dropAllTables(deps);
137
+ * if (!result.ok) return result;
130
138
  *
131
139
  * if (message.restore) {
132
140
  * const dbSchema = getDbSchema(deps)();
133
141
  * if (!dbSchema.ok) return dbSchema;
134
142
  *
135
- * const ensureDbSchemaResult = ensureDbSchema(deps)(
136
- * message.restore.dbSchema,
137
- * dbSchema.value,
138
- * );
139
- * if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
140
- *
141
- * const initializeDbResult = initializeDb(deps)(
142
- * message.restore.mnemonic,
143
- * );
144
- * if (!initializeDbResult.ok) return initializeDbResult;
143
+ * {
144
+ * const result = ensureDbSchema(deps)(
145
+ * message.restore.dbSchema,
146
+ * dbSchema.value,
147
+ * );
148
+ * if (!result.ok) return result;
149
+ * }
150
+ * {
151
+ * const result = initializeDb(deps)(message.restore.mnemonic);
152
+ * if (!result.ok) return result;
153
+ * }
145
154
  * }
146
155
  * return ok();
147
156
  * });
148
157
  *
149
- * if (!resetResult.ok) {
150
- * deps.postMessage({ type: "onError", error: resetResult.error });
158
+ * if (!result.ok) {
159
+ * deps.postMessage({ type: "onError", error: result.error });
151
160
  * return;
152
161
  * }
153
162
  * ```
@@ -254,13 +263,13 @@
254
263
  * ```ts
255
264
  * // ✅ Safe to return void - unsafe code is wrapped and error is handled
256
265
  * const processData = (data: string): void => {
257
- * const parseResult = trySync(
266
+ * const result = trySync(
258
267
  * () => JSON.parse(data),
259
268
  * (error) => ({ type: "ParseError", message: String(error) }),
260
269
  * );
261
270
  *
262
- * if (!parseResult.ok) {
263
- * logError(parseResult.error);
271
+ * if (!result.ok) {
272
+ * logError(result.error);
264
273
  * return;
265
274
  * }
266
275
  *
package/src/Sqlite.ts CHANGED
@@ -2,6 +2,7 @@ import { Brand } from "./Brand.js";
2
2
  import { createLruCache } from "./Cache.js";
3
3
  import { ConsoleDep } from "./Console.js";
4
4
  import { EncryptionKey } from "./Crypto.js";
5
+ import { Eq, eqArrayNumber } from "./Eq.js";
5
6
  import { createTransferableError, TransferableError } from "./Error.js";
6
7
  import { err, ok, Result, tryAsync, trySync } from "./Result.js";
7
8
  import {
@@ -34,8 +35,8 @@ export interface CreateSqliteDriverDep {
34
35
  }
35
36
 
36
37
  export interface SqliteDriverOptions {
37
- memory?: boolean;
38
- encryptionKey?: EncryptionKey | undefined;
38
+ readonly memory?: boolean;
39
+ readonly encryptionKey?: EncryptionKey | undefined;
39
40
  }
40
41
 
41
42
  /**
@@ -75,7 +76,7 @@ export interface SqliteQuery {
75
76
  }
76
77
 
77
78
  /** A type representing a sanitized SQL string. */
78
- export type SafeSql = string & Brand<"TimestampString">;
79
+ export type SafeSql = string & Brand<"SafeSql">;
79
80
 
80
81
  /**
81
82
  * A value that can be stored in Sqlite.
@@ -86,6 +87,16 @@ export type SafeSql = string & Brand<"TimestampString">;
86
87
  export const SqliteValue = union(Null, String, Number, Uint8Array);
87
88
  export type SqliteValue = typeof SqliteValue.Type;
88
89
 
90
+ export const eqSqliteValue: Eq<SqliteValue> = (x, y) => {
91
+ if (
92
+ x instanceof globalThis.Uint8Array &&
93
+ y instanceof globalThis.Uint8Array
94
+ ) {
95
+ return eqArrayNumber(x, y);
96
+ }
97
+ return x === y;
98
+ };
99
+
89
100
  export interface SqliteQueryOptions {
90
101
  /**
91
102
  * If set to `true`, logs the time taken to execute the SQL query. Useful for
@@ -292,13 +303,13 @@ export const createPreparedStatementsCache = <P>(
292
303
  };
293
304
 
294
305
  export interface SqlIdentifier {
295
- type: "SqlIdentifier";
296
- sql: SafeSql;
306
+ readonly type: "SqlIdentifier";
307
+ readonly sql: SafeSql;
297
308
  }
298
309
 
299
310
  export interface RawSql {
300
- type: "RawSql";
301
- sql: string;
311
+ readonly type: "RawSql";
312
+ readonly sql: string;
302
313
  }
303
314
 
304
315
  export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
package/src/Task.ts CHANGED
@@ -570,10 +570,12 @@ export const retry = <T, E>(
570
570
  }
571
571
 
572
572
  // Wait before retry
573
- const delayResult = await wait(NonNegativeInt.orThrow(delay))(context);
574
- if (!delayResult.ok) {
575
- // If delay was aborted, return AbortError (will be handled by toTask)
576
- return delayResult;
573
+ {
574
+ const result = await wait(NonNegativeInt.orThrow(delay))(context);
575
+ if (!result.ok) {
576
+ // If delay was aborted, return AbortError (will be handled by toTask)
577
+ return result;
578
+ }
577
579
  }
578
580
  }
579
581
  });
package/src/Type.ts CHANGED
@@ -207,7 +207,7 @@ import * as bip39 from "@scure/bip39";
207
207
  import { wordlist } from "@scure/bip39/wordlists/english.js";
208
208
  import { pack } from "msgpackr";
209
209
  import type { Brand } from "./Brand.js";
210
- import { type RandomBytesDep } from "./Crypto.js";
210
+ import type { RandomBytesDep } from "./Crypto.js";
211
211
  import { isPlainObject } from "./Object.js";
212
212
  import { hasNodeBuffer } from "./Platform.js";
213
213
  import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
@@ -2391,6 +2391,124 @@ export const formatArrayError = <Error extends TypeError>(
2391
2391
  }
2392
2392
  });
2393
2393
 
2394
+ /**
2395
+ * Set of a specific {@link Type}.
2396
+ *
2397
+ * ### Example
2398
+ *
2399
+ * ```ts
2400
+ * const NumberSet = set(Number);
2401
+ *
2402
+ * const result1 = NumberSet.from(new Set([1, 2, 3])); // ok(Set { 1, 2, 3 })
2403
+ * const result2 = NumberSet.from(new Set(["a", "b"])); // err(...)
2404
+ * ```
2405
+ *
2406
+ * @category Base Factories
2407
+ */
2408
+ export const set = <ElementType extends AnyType>(
2409
+ element: ElementType,
2410
+ ): SetType<ElementType> => {
2411
+ const fromUnknown = (
2412
+ value: unknown,
2413
+ ): Result<
2414
+ ReadonlySet<InferType<ElementType>>,
2415
+ SetError<InferErrors<ElementType>>
2416
+ > => {
2417
+ if (!(value instanceof globalThis.Set)) {
2418
+ return err<SetError<InferErrors<ElementType>>>({
2419
+ type: "Set",
2420
+ value,
2421
+ reason: { kind: "NotSet" },
2422
+ });
2423
+ }
2424
+
2425
+ let index = 0;
2426
+ for (const item of value) {
2427
+ const elementResult = element.fromUnknown(item);
2428
+ if (!elementResult.ok) {
2429
+ return err<SetError<InferErrors<ElementType>>>({
2430
+ type: "Set",
2431
+ value,
2432
+ reason: {
2433
+ kind: "Element",
2434
+ index,
2435
+ error: elementResult.error as InferErrors<ElementType>,
2436
+ },
2437
+ });
2438
+ }
2439
+ index++;
2440
+ }
2441
+
2442
+ return ok(value as ReadonlySet<InferType<ElementType>>);
2443
+ };
2444
+
2445
+ const fromParent = (
2446
+ value: ReadonlySet<InferParent<ElementType>>,
2447
+ ): Result<
2448
+ ReadonlySet<InferType<ElementType>>,
2449
+ SetError<InferError<ElementType>>
2450
+ > => {
2451
+ let index = 0;
2452
+ for (const item of value) {
2453
+ const elementResult = element.fromParent(item);
2454
+ if (!elementResult.ok) {
2455
+ return err({
2456
+ type: "Set",
2457
+ value,
2458
+ reason: {
2459
+ kind: "Element",
2460
+ index,
2461
+ error: elementResult.error as InferError<ElementType>,
2462
+ },
2463
+ });
2464
+ }
2465
+ index++;
2466
+ }
2467
+ return ok(value as ReadonlySet<InferType<ElementType>>);
2468
+ };
2469
+
2470
+ return {
2471
+ ...createType("Set", { fromUnknown, fromParent }),
2472
+ element,
2473
+ };
2474
+ };
2475
+
2476
+ /** SetType extends Type with an additional `element` property for reflection. */
2477
+ export interface SetType<ElementType extends AnyType>
2478
+ extends Type<
2479
+ "Set",
2480
+ ReadonlySet<InferType<ElementType>>,
2481
+ ReadonlySet<InferInput<ElementType>>,
2482
+ SetError<InferError<ElementType>>,
2483
+ ReadonlySet<InferParent<ElementType>>,
2484
+ SetError<InferParentError<ElementType>>
2485
+ > {
2486
+ readonly element: ElementType;
2487
+ }
2488
+
2489
+ export interface SetError<Error extends TypeError = TypeError>
2490
+ extends TypeErrorWithReason<
2491
+ "Set",
2492
+ | { readonly kind: "NotSet" }
2493
+ | {
2494
+ readonly kind: "Element";
2495
+ readonly index: number;
2496
+ readonly error: Error;
2497
+ }
2498
+ > {}
2499
+
2500
+ export const formatSetError = <Error extends TypeError>(
2501
+ formatTypeError: TypeErrorFormatter<Error>,
2502
+ ): TypeErrorFormatter<SetError<Error>> =>
2503
+ createTypeErrorFormatter((error) => {
2504
+ switch (error.reason.kind) {
2505
+ case "NotSet":
2506
+ return `Expected a Set but received ${error.value}.`;
2507
+ case "Element":
2508
+ return `Invalid element at index ${error.reason.index}: ${formatTypeError(error.reason.error)}`;
2509
+ }
2510
+ });
2511
+
2394
2512
  /**
2395
2513
  * Record of a key {@link Type} and value {@link Type}.
2396
2514
  *
@@ -3970,6 +4088,7 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3970
4088
  | ExtraErrors
3971
4089
  // Composite errors
3972
4090
  | ArrayError<TypeErrors<ExtraErrors>>
4091
+ | SetError<TypeErrors<ExtraErrors>>
3973
4092
  | RecordError<TypeErrors<ExtraErrors>, TypeErrors<ExtraErrors>>
3974
4093
  | ObjectError<Record<string, TypeErrors<ExtraErrors>>>
3975
4094
  | ObjectWithRecordError<
@@ -4154,6 +4273,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
4154
4273
  return formatSimplePasswordError(formatTypeError)(error);
4155
4274
  case "Array":
4156
4275
  return formatArrayError(formatTypeError)(error);
4276
+ case "Set":
4277
+ return formatSetError(formatTypeError)(error);
4157
4278
  case "Record":
4158
4279
  return formatRecordError(formatTypeError)(error);
4159
4280
  case "Object":
@@ -4202,6 +4323,18 @@ export const typeErrorToStandardSchemaIssues = <
4202
4323
  );
4203
4324
  }
4204
4325
 
4326
+ if (error.type === "Set") {
4327
+ const setError = error as SetError;
4328
+ if (setError.reason.kind === "NotSet") {
4329
+ return [{ message: formatTypeError(error), path }];
4330
+ }
4331
+ return typeErrorToStandardSchemaIssues(
4332
+ setError.reason.error as TypeErrors<ExtraErrors>,
4333
+ formatTypeError,
4334
+ [...path, setError.reason.index],
4335
+ );
4336
+ }
4337
+
4205
4338
  if (error.type === "Object") {
4206
4339
  const objectError = error as ObjectError;
4207
4340
  if (