@evolu/common 8.3.3 → 8.5.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 (76) hide show
  1. package/dist/src/Array.d.ts +24 -0
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +27 -1
  4. package/dist/src/Assert.d.ts +62 -19
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +64 -17
  7. package/dist/src/LeakDetector.d.ts.map +1 -1
  8. package/dist/src/LeakDetector.js +4 -1
  9. package/dist/src/Lookup.js +1 -0
  10. package/dist/src/Platform.js +1 -0
  11. package/dist/src/Relation.js +3 -3
  12. package/dist/src/Resource.d.ts.map +1 -1
  13. package/dist/src/Resource.js +9 -3
  14. package/dist/src/Result.d.ts.map +1 -1
  15. package/dist/src/Result.js +3 -5
  16. package/dist/src/Sqlite.d.ts.map +1 -1
  17. package/dist/src/Sqlite.js +9 -7
  18. package/dist/src/Store.d.ts.map +1 -1
  19. package/dist/src/Store.js +3 -1
  20. package/dist/src/String.d.ts.map +1 -1
  21. package/dist/src/String.js +1 -1
  22. package/dist/src/Task.d.ts +122 -60
  23. package/dist/src/Task.d.ts.map +1 -1
  24. package/dist/src/Task.js +40 -53
  25. package/dist/src/Test.d.ts.map +1 -1
  26. package/dist/src/Test.js +1 -0
  27. package/dist/src/Time.d.ts.map +1 -1
  28. package/dist/src/Time.js +9 -2
  29. package/dist/src/Type.d.ts +146 -9
  30. package/dist/src/Type.d.ts.map +1 -1
  31. package/dist/src/Type.js +10 -7
  32. package/dist/src/WebSocket.d.ts.map +1 -1
  33. package/dist/src/WebSocket.js +8 -0
  34. package/dist/src/Worker.js +1 -1
  35. package/dist/src/local-first/Db.d.ts.map +1 -1
  36. package/dist/src/local-first/Db.js +7 -6
  37. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  38. package/dist/src/local-first/Evolu.js +1 -0
  39. package/dist/src/local-first/Owner.d.ts +8 -7
  40. package/dist/src/local-first/Owner.d.ts.map +1 -1
  41. package/dist/src/local-first/Owner.js +4 -4
  42. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  43. package/dist/src/local-first/Protocol.js +31 -25
  44. package/dist/src/local-first/Query.d.ts.map +1 -1
  45. package/dist/src/local-first/Query.js +2 -1
  46. package/dist/src/local-first/Relay.js +1 -1
  47. package/dist/src/local-first/Shared.d.ts.map +1 -1
  48. package/dist/src/local-first/Shared.js +4 -2
  49. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  50. package/dist/src/local-first/Timestamp.js +1 -1
  51. package/package.json +1 -1
  52. package/src/Array.ts +28 -1
  53. package/src/Assert.ts +81 -19
  54. package/src/LeakDetector.ts +4 -3
  55. package/src/Lookup.ts +1 -0
  56. package/src/Platform.ts +1 -0
  57. package/src/Relation.ts +3 -3
  58. package/src/Resource.ts +9 -3
  59. package/src/Result.ts +5 -5
  60. package/src/Sqlite.ts +9 -7
  61. package/src/Store.ts +3 -1
  62. package/src/String.ts +1 -1
  63. package/src/Task.ts +133 -82
  64. package/src/Test.ts +1 -0
  65. package/src/Time.ts +9 -2
  66. package/src/Type.ts +173 -29
  67. package/src/WebSocket.ts +8 -0
  68. package/src/Worker.ts +1 -1
  69. package/src/local-first/Db.ts +11 -7
  70. package/src/local-first/Evolu.ts +1 -0
  71. package/src/local-first/Owner.ts +8 -7
  72. package/src/local-first/Protocol.ts +41 -31
  73. package/src/local-first/Query.ts +6 -3
  74. package/src/local-first/Relay.ts +1 -1
  75. package/src/local-first/Shared.ts +4 -2
  76. package/src/local-first/Timestamp.ts +1 -1
package/src/Assert.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * @module
5
5
  */
6
6
 
7
+ import type { NonEmptyArray, NonEmptyReadonlyArray } from "./Array.ts";
7
8
  import type { Type } from "./Type.ts";
8
9
 
9
10
  /**
@@ -13,9 +14,9 @@ import type { Type } from "./Type.ts";
13
14
  * Prevents invalid states from propagating through the system by halting
14
15
  * execution when a condition fails, improving reliability and debuggability.
15
16
  *
16
- * Do not use this instead of {@link Type}. Assertions are intended for
17
- * conditions that are logically guaranteed but not statically known by
18
- * TypeScript, or for catching and signaling developer mistakes eagerly.
17
+ * Do not use this instead of {@link Type}. Assertions are intended when a
18
+ * condition is logically guaranteed to be true but TypeScript cannot prove it,
19
+ * or for catching and signaling developer mistakes eagerly.
19
20
  *
20
21
  * ### Example
21
22
  *
@@ -32,6 +33,7 @@ export const assert: (
32
33
  condition: unknown,
33
34
  message: string,
34
35
  ) => asserts condition = (condition, message) => {
36
+ // oxlint-disable-next-line typescript/strict-boolean-expressions -- JavaScript truthiness is the contract of assert.
35
37
  if (!condition) {
36
38
  throw new Error(message);
37
39
  }
@@ -40,8 +42,9 @@ export const assert: (
40
42
  /**
41
43
  * Asserts that a value is non-nullable.
42
44
  *
43
- * Narrows a nullable value to {@link NonNullable} when null or undefined is
44
- * logically impossible but TypeScript cannot prove it.
45
+ * Following TypeScript's {@link NonNullable}, non-nullable here means neither
46
+ * null nor undefined. Use this when a value is logically guaranteed to be
47
+ * non-nullable but TypeScript cannot prove it.
45
48
  */
46
49
  export const assertNonNullable: <T>(
47
50
  value: T,
@@ -53,21 +56,80 @@ export const assertNonNullable: <T>(
53
56
  assert(value != null, message);
54
57
  };
55
58
 
59
+ /**
60
+ * Asserts that a value is not null while preserving undefined.
61
+ *
62
+ * Use this when a value is logically guaranteed not to be null but TypeScript
63
+ * cannot prove it.
64
+ *
65
+ * ### Example
66
+ *
67
+ * ```ts
68
+ * import { assertNotNull } from "@evolu/common";
69
+ *
70
+ * const value = undefined as string | null | undefined;
71
+ * assertNotNull(value);
72
+ * expectTypeOf(value).toEqualTypeOf<string | undefined>();
73
+ * expect(value).toBeUndefined();
74
+ * expect(() => assertNotNull(null)).toThrow(
75
+ * "Expected value not to be null.",
76
+ * );
77
+ * ```
78
+ */
79
+ export const assertNotNull: <T>(
80
+ value: T,
81
+ message?: string,
82
+ ) => asserts value is T & ({} | undefined) = (
83
+ value,
84
+ message = "Expected value not to be null.",
85
+ ) => {
86
+ assert(value !== null, message);
87
+ };
88
+
89
+ /**
90
+ * Asserts that a value is not undefined while preserving null.
91
+ *
92
+ * Use this when a value is logically guaranteed not to be undefined but
93
+ * TypeScript cannot prove it.
94
+ *
95
+ * ### Example
96
+ *
97
+ * ```ts
98
+ * import { assertNotUndefined } from "@evolu/common";
99
+ *
100
+ * const value = null as string | null | undefined;
101
+ * assertNotUndefined(value);
102
+ * expectTypeOf(value).toEqualTypeOf<string | null>();
103
+ * expect(value).toBeNull();
104
+ * expect(() => assertNotUndefined(undefined)).toThrow(
105
+ * "Expected value not to be undefined.",
106
+ * );
107
+ * ```
108
+ */
109
+ export const assertNotUndefined: <T>(
110
+ value: T,
111
+ message?: string,
112
+ ) => asserts value is T & ({} | null) = (
113
+ value,
114
+ message = "Expected value not to be undefined.",
115
+ ) => {
116
+ assert(value !== undefined, message);
117
+ };
118
+
56
119
  /**
57
120
  * Asserts that an array is non-empty.
58
121
  *
59
- * Ensures the provided array has at least one element, helping TypeScript infer
60
- * the array as non-empty when this is logically guaranteed but not statically
61
- * known.
122
+ * Use this when an array is logically guaranteed to be non-empty but TypeScript
123
+ * cannot prove it.
62
124
  *
63
125
  * ### Example
64
126
  *
65
127
  * ```ts
66
- * import { assertNonEmptyArray } from "@evolu/common";
128
+ * import { assertNonEmptyArray, type NonEmptyArray } from "@evolu/common";
67
129
  *
68
130
  * const values = [1, 2, 3];
69
131
  * assertNonEmptyArray(values);
70
- * expectTypeOf(values).toEqualTypeOf<[number, ...Array<number>]>();
132
+ * expectTypeOf(values).toEqualTypeOf<NonEmptyArray<number>>();
71
133
  * expect(values[0]).toBe(1);
72
134
  * expect(() => assertNonEmptyArray([])).toThrow();
73
135
  * ```
@@ -75,7 +137,7 @@ export const assertNonNullable: <T>(
75
137
  export const assertNonEmptyArray: <T>(
76
138
  arr: Array<T>,
77
139
  message?: string,
78
- ) => asserts arr is [T, ...Array<T>] = (
140
+ ) => asserts arr is NonEmptyArray<T> = (
79
141
  arr,
80
142
  message = "Expected a non-empty array.",
81
143
  ) => {
@@ -85,20 +147,20 @@ export const assertNonEmptyArray: <T>(
85
147
  /**
86
148
  * Asserts that a readonly array is non-empty.
87
149
  *
88
- * Ensures the provided readonly array has at least one element, helping
89
- * TypeScript infer non-emptiness when this is logically guaranteed but not
90
- * statically known.
150
+ * Use this when a readonly array is logically guaranteed to be non-empty but
151
+ * TypeScript cannot prove it.
91
152
  *
92
153
  * ### Example
93
154
  *
94
155
  * ```ts
95
- * import { assertNonEmptyReadonlyArray } from "@evolu/common";
156
+ * import {
157
+ * assertNonEmptyReadonlyArray,
158
+ * type NonEmptyReadonlyArray,
159
+ * } from "@evolu/common";
96
160
  *
97
161
  * const values: ReadonlyArray<number> = [1, 2, 3];
98
162
  * assertNonEmptyReadonlyArray(values);
99
- * expectTypeOf(values).toEqualTypeOf<
100
- * readonly [number, ...Array<number>]
101
- * >();
163
+ * expectTypeOf(values).toEqualTypeOf<NonEmptyReadonlyArray<number>>();
102
164
  * expect(values[0]).toBe(1);
103
165
  * expect(() => assertNonEmptyReadonlyArray([])).toThrow();
104
166
  * ```
@@ -106,7 +168,7 @@ export const assertNonEmptyArray: <T>(
106
168
  export const assertNonEmptyReadonlyArray: <T>(
107
169
  arr: ReadonlyArray<T>,
108
170
  message?: string,
109
- ) => asserts arr is readonly [T, ...Array<T>] = (
171
+ ) => asserts arr is NonEmptyReadonlyArray<T> = (
110
172
  arr,
111
173
  message = "Expected a non-empty readonly array.",
112
174
  ) => {
@@ -66,9 +66,10 @@ export const createLeakDetector = (deps: ConsoleDep): LeakDetector => {
66
66
  if (typeof globalThis.FinalizationRegistry !== "function")
67
67
  return noopLeakDetector;
68
68
 
69
- const registry = new globalThis.FinalizationRegistry<TrackedLeak>(
70
- reportLeak(deps),
71
- );
69
+ const report = reportLeak(deps);
70
+ const registry = new globalThis.FinalizationRegistry<TrackedLeak>((leak) => {
71
+ report(leak);
72
+ });
72
73
 
73
74
  return {
74
75
  track: (target, leak, unregisterToken) => {
package/src/Lookup.ts CHANGED
@@ -384,6 +384,7 @@ const structuralLookupInternal = (
384
384
  value: unknown,
385
385
  path: Set<object> = new Set(),
386
386
  ): string => {
387
+ // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- The default rejects every unsupported unknown runtime type.
387
388
  switch (typeof value) {
388
389
  case "string":
389
390
  return `s:${JSON.stringify(value)}`;
package/src/Platform.ts CHANGED
@@ -176,6 +176,7 @@ const createTestGlobalErrors = (
176
176
  globalThis.removeEventListener(webEvent, listener);
177
177
  });
178
178
  } else {
179
+ // oxlint-disable-next-line unicorn/prefer-type-error -- An unsupported runtime environment is not an invalid argument type.
179
180
  throw new Error(`Unsupported platform global ${errorType} reporting.`);
180
181
  }
181
182
 
package/src/Relation.ts CHANGED
@@ -250,14 +250,14 @@ export function createRelation<A, B, LA = A, LB = B>({
250
250
  removeByA: (a) => {
251
251
  const relatedB = bByA.get(a);
252
252
  if (!relatedB) return false;
253
- for (const b of [...relatedB.keys()]) removePair(a, b);
253
+ for (const b of relatedB.keys()) removePair(a, b);
254
254
  return true;
255
255
  },
256
256
 
257
257
  removeByB: (b) => {
258
258
  const relatedA = aByB.get(b);
259
259
  if (!relatedA) return false;
260
- for (const a of [...relatedA.keys()]) removePair(a, b);
260
+ for (const a of relatedA.keys()) removePair(a, b);
261
261
  return true;
262
262
  },
263
263
 
@@ -306,7 +306,7 @@ export function createRelation<A, B, LA = A, LB = B>({
306
306
  const assertRelationMappingConsistency: (
307
307
  condition: unknown,
308
308
  ) => asserts condition = (condition) => {
309
- assert(condition, "Relation mapping inconsistency");
309
+ assert(Boolean(condition), "Relation mapping inconsistency");
310
310
  };
311
311
 
312
312
  /**
package/src/Resource.ts CHANGED
@@ -392,7 +392,7 @@ export const createSharedResource =
392
392
  // only.
393
393
  if (sharedResourceRun.getState().type !== "Running") return true;
394
394
 
395
- if (idleDisposeAfter) {
395
+ if (idleDisposeAfter !== undefined) {
396
396
  idleDisposeFiber = sharedResourceRun.abortable<void, never>(
397
397
  async (run) => {
398
398
  await run.ok(sleep(idleDisposeAfter));
@@ -414,7 +414,9 @@ export const createSharedResource =
414
414
  resource: current as unknown as BorrowedResource<T>,
415
415
  created,
416
416
  release,
417
- [Symbol.dispose]: release,
417
+ [Symbol.dispose]: () => {
418
+ release();
419
+ },
418
420
  };
419
421
 
420
422
  leakDetector.track(
@@ -832,6 +834,7 @@ export function createSharedResourceByKey<
832
834
  using leaseDisposer = new DisposableStack();
833
835
  const leasesByKey: Array<readonly [Lease<T>, K]> = [];
834
836
 
837
+ // oxlint-disable-next-line unicorn/no-useless-spread -- Snapshot keys because each acquisition awaits and can mutate the registry.
835
838
  for (const [key] of [...sharedResourcesByKey]) {
836
839
  const lease = await run.ok(sharedResourceByKey.acquireCurrent(key));
837
840
  if (lease) leasesByKey.push([leaseDisposer.use(lease), key]);
@@ -1346,7 +1349,9 @@ export const createSharedResourceByKeyWithClaims =
1346
1349
  const release = (): boolean => releaseClaimLease(heldClaimLease);
1347
1350
  const claimLease: ClaimLease = {
1348
1351
  release,
1349
- [Symbol.dispose]: release,
1352
+ [Symbol.dispose]: () => {
1353
+ release();
1354
+ },
1350
1355
  };
1351
1356
 
1352
1357
  leakDetector.track(
@@ -1367,6 +1372,7 @@ export const createSharedResourceByKeyWithClaims =
1367
1372
  resourceKey,
1368
1373
  ] of firstClaimTransitions) {
1369
1374
  onFirstClaimAdded?.(claim, resource, resourceKey);
1375
+ // oxlint-disable-next-line eslint/no-loop-func -- Captures this iteration's block-scoped transition values for rollback.
1370
1376
  compensations.defer(() => {
1371
1377
  if (!succeeded) {
1372
1378
  notifyLastClaimRemoved(claim, resource, resourceKey);
package/src/Result.ts CHANGED
@@ -189,6 +189,7 @@
189
189
  * @module
190
190
  */
191
191
  import {
192
+ createMutableArray,
192
193
  emptyArray,
193
194
  isNonEmptyArray,
194
195
  type NonEmptyReadonlyArray,
@@ -442,9 +443,8 @@ export const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
442
443
  export const getOrThrow = <T, E>(result: Result<T, E>): T => {
443
444
  if (result.ok) {
444
445
  return result.value;
445
- } else {
446
- throw new Error("getOrThrow", { cause: result.error });
447
446
  }
447
+ throw new Error("getOrThrow", { cause: result.error });
448
448
  };
449
449
 
450
450
  /**
@@ -572,7 +572,7 @@ export function trySync<T, E>(
572
572
  export function trySync<T, E>(
573
573
  fn: () => T,
574
574
  mapError?: (error: unknown) => E,
575
- ): Result<T, E | unknown> {
575
+ ): Result<T, unknown> {
576
576
  try {
577
577
  return ok(fn());
578
578
  } catch (error) {
@@ -644,7 +644,7 @@ export function tryAsync<T, E>(
644
644
  export async function tryAsync<T, E>(
645
645
  promiseThunk: Thunk<Awaitable<T>>,
646
646
  mapError?: (error: unknown) => E,
647
- ): Promise<Result<T, E | unknown>> {
647
+ ): Promise<Result<T, unknown>> {
648
648
  try {
649
649
  return ok(await promiseThunk());
650
650
  } catch (error) {
@@ -1181,7 +1181,7 @@ export function allResult(
1181
1181
  const length = input.length;
1182
1182
  if (length === 0) return ok(emptyArray);
1183
1183
 
1184
- const values = new Array<unknown>(length);
1184
+ const values = createMutableArray<unknown>(length);
1185
1185
  for (let i = 0; i < length; i++) {
1186
1186
  const result = fn ? fn(input[i]) : (input[i] as AnyResult);
1187
1187
  if (!result.ok) return result;
package/src/Sqlite.ts CHANGED
@@ -256,12 +256,13 @@ export const createSqlite =
256
256
  console.debug({ query });
257
257
 
258
258
  const label =
259
- query.options?.logQueryExecutionTime &&
260
- `SqliteQueryExecutionTime ${query.sql}`;
259
+ query.options?.logQueryExecutionTime === true
260
+ ? `SqliteQueryExecutionTime ${query.sql}`
261
+ : null;
261
262
 
262
- if (label) console.time(label);
263
+ if (label !== null) console.time(label);
263
264
  const result = driver.exec(query);
264
- if (label) console.timeEnd(label);
265
+ if (label !== null) console.timeEnd(label);
265
266
 
266
267
  if (query.options?.logExplainQueryPlan) {
267
268
  const result = driver.exec({
@@ -343,11 +344,12 @@ const drawSqliteQueryPlan = (rows: Array<SqliteQueryPlanRow>): string =>
343
344
  let indent = 0;
344
345
 
345
346
  do {
347
+ // oxlint-disable-next-line eslint/no-loop-func -- find invokes the callback synchronously before parentId changes.
346
348
  const parent = rows.find((r) => r.id === parentId);
347
349
  if (!parent) break;
348
350
  parentId = parent.parent;
349
351
  indent++;
350
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
352
+ // oxlint-disable-next-line eslint/no-constant-condition -- A missing parent terminates the traversal above.
351
353
  } while (true);
352
354
 
353
355
  return `${" ".repeat(indent)}${row.detail}`;
@@ -395,7 +397,7 @@ export const createPreparedStatementsCache = <P>(
395
397
  if (alwaysPrepare !== true && !query.options?.prepare)
396
398
  return null as never;
397
399
  let statement = statementsBySql.get(query.sql);
398
- if (!statement) {
400
+ if (statement === undefined) {
399
401
  statement = factory(query.sql);
400
402
  statementsBySql.set(query.sql, statement);
401
403
  }
@@ -492,7 +494,7 @@ export const sql = (
492
494
  sql.identifier = (identifier: string): SqlIdentifier => ({
493
495
  type: "SqlIdentifier",
494
496
  // From Kysely
495
- sql: `"${identifier.replace(/"/g, '""')}"` as SafeSql,
497
+ sql: `"${identifier.replaceAll('"', '""')}"` as SafeSql,
496
498
  });
497
499
 
498
500
  /**
package/src/Store.ts CHANGED
@@ -70,7 +70,9 @@ export const createStore = <T>(initialState: T, eq?: Eq<T>): Store<T> => {
70
70
  get: ref.get,
71
71
  subscribe: (listener) => {
72
72
  listeners.add(listener);
73
- return () => listeners.delete(listener);
73
+ return () => {
74
+ listeners.delete(listener);
75
+ };
74
76
  },
75
77
 
76
78
  set: (state) => {
package/src/String.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * pattern.
10
10
  */
11
11
  export const escapeRegExp = (value: string): string =>
12
- value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12
+ value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
13
13
 
14
14
  /** Converts an unknown value to a compact, human-readable diagnostic string. */
15
15
  export const safelyStringifyUnknownValue = (value: unknown): string => {