@evolu/common 8.15.0 → 8.16.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 (39) hide show
  1. package/dist/src/Console.d.ts +24 -46
  2. package/dist/src/Console.d.ts.map +1 -1
  3. package/dist/src/Console.js +34 -29
  4. package/dist/src/Error.d.ts +10 -6
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +52 -20
  7. package/dist/src/Sqlite.d.ts.map +1 -1
  8. package/dist/src/Sqlite.js +16 -12
  9. package/dist/src/Task.d.ts +3 -1
  10. package/dist/src/Task.d.ts.map +1 -1
  11. package/dist/src/Task.js +15 -13
  12. package/dist/src/Worker.d.ts +3 -0
  13. package/dist/src/Worker.d.ts.map +1 -1
  14. package/dist/src/Worker.js +6 -1
  15. package/dist/src/local-first/Db.d.ts.map +1 -1
  16. package/dist/src/local-first/Db.js +7 -7
  17. package/dist/src/local-first/Evolu.d.ts +4 -0
  18. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  19. package/dist/src/local-first/Schema.d.ts +3 -0
  20. package/dist/src/local-first/Schema.d.ts.map +1 -1
  21. package/dist/src/local-first/Schema.js +9 -5
  22. package/dist/src/local-first/Shared.d.ts +55 -39
  23. package/dist/src/local-first/Shared.d.ts.map +1 -1
  24. package/dist/src/local-first/Shared.js +264 -320
  25. package/package.json +1 -1
  26. package/src/Console.test.ts +75 -247
  27. package/src/Console.ts +68 -110
  28. package/src/Error.test.ts +85 -0
  29. package/src/Error.ts +48 -22
  30. package/src/Sqlite.ts +19 -11
  31. package/src/Task.test.ts +46 -0
  32. package/src/Task.ts +17 -13
  33. package/src/Worker.ts +6 -1
  34. package/src/local-first/Db.ts +12 -8
  35. package/src/local-first/Evolu.ts +4 -0
  36. package/src/local-first/Schema.test.ts +80 -1
  37. package/src/local-first/Schema.ts +15 -5
  38. package/src/local-first/Shared.test.ts +414 -39
  39. package/src/local-first/Shared.ts +386 -440
package/src/Worker.ts CHANGED
@@ -656,6 +656,9 @@ export const testCreateMessagePort: CreateMessagePort = <Input, Output = never>(
656
656
  /**
657
657
  * Creates an in-memory {@link BroadcastChannel} for testing.
658
658
  *
659
+ * Like a native channel, it structured-clones each message, so posting a value
660
+ * that cannot be cloned throws.
661
+ *
659
662
  * @group Testing
660
663
  */
661
664
  export const testCreateBroadcastChannel = <Input, Output = Input>(
@@ -667,7 +670,9 @@ export const testCreateBroadcastChannel = <Input, Output = Input>(
667
670
  const disposables = disposer.move();
668
671
 
669
672
  return {
670
- postMessage: channel.postMessage,
673
+ postMessage: (message) => {
674
+ channel.postMessage(structuredClone(message));
675
+ },
671
676
  get onMessage() {
672
677
  return channel.onMessage;
673
678
  },
@@ -137,7 +137,10 @@ import type {
137
137
  EvoluInput,
138
138
  SharedWorkerId,
139
139
  } from "./Shared.ts";
140
- import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.ts";
140
+ import {
141
+ consoleEntryOrErrorBroadcastChannelName,
142
+ postConsoleEntry,
143
+ } from "./Shared.ts";
141
144
  import {
142
145
  createBaseSqliteStorage,
143
146
  createBaseSqliteStorageTables,
@@ -229,6 +232,13 @@ export const startDbWorker =
229
232
  self.onMessage = (message) => resolve(ok(message));
230
233
  }),
231
234
  );
235
+ // Tabs report the errors this worker logs as an UnknownError, so it
236
+ // forwards them even for a silent app, whose tabs do not print them.
237
+ deps.console.setLevel(
238
+ initMessage.consoleLevel === "silent"
239
+ ? "error"
240
+ : initMessage.consoleLevel,
241
+ );
232
242
 
233
243
  const port = disposer.use(
234
244
  deps.createMessagePort<DbWorkerOutput, DbWorkerInput>(initMessage.port),
@@ -242,11 +252,7 @@ export const startDbWorker =
242
252
  disposer.defer(
243
253
  deps.consoleStoreOutputEntry.subscribe(() => {
244
254
  const entry = deps.consoleStoreOutputEntry.get();
245
- if (entry)
246
- consoleEntryOrErrorBroadcastChannel.postMessage({
247
- type: "ConsoleEntry",
248
- entry,
249
- });
255
+ if (entry) postConsoleEntry(consoleEntryOrErrorBroadcastChannel, entry);
250
256
  }),
251
257
  );
252
258
 
@@ -310,7 +316,6 @@ export const startDbWorker =
310
316
  // waits on. The tab leader lock is unaffected.
311
317
  port.postMessage({
312
318
  type: "LeaderRefused",
313
- name: initMessage.name,
314
319
  error: startup.error,
315
320
  });
316
321
  return ok();
@@ -322,7 +327,6 @@ export const startDbWorker =
322
327
 
323
328
  port.postMessage({
324
329
  type: "LeaderAcquired",
325
- name: initMessage.name,
326
330
  clock: initialClock,
327
331
  });
328
332
 
@@ -195,6 +195,10 @@ export interface EvoluConfig {
195
195
  /**
196
196
  * Use the `indexes` option to define SQLite indexes.
197
197
  *
198
+ * Unique indexes are rejected with an assertion because concurrent offline
199
+ * writes can violate uniqueness during synchronization. See
200
+ * [Uniqueness](https://www.evolu.dev/docs/schema#uniqueness).
201
+ *
198
202
  * Table and column names are not typed because Kysely doesn't support it.
199
203
  *
200
204
  * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
@@ -4,6 +4,7 @@ import {
4
4
  assertErr,
5
5
  assertOk,
6
6
  assertSame,
7
+ assertThrowsInstanceOf,
7
8
  assertType,
8
9
  createEvolu,
9
10
  createQueryBuilder,
@@ -27,7 +28,7 @@ import {
27
28
  type TimestampBytes,
28
29
  } from "../index.ts";
29
30
  import type { EvoluPlatformDeps } from "./Evolu.ts";
30
- import type { ValidateSchema } from "./Schema.ts";
31
+ import { evoluSchemaToSqliteSchema, type ValidateSchema } from "./Schema.ts";
31
32
 
32
33
  test("history queries select and filter by ownerId bytes", () => {
33
34
  const createQuery = createQueryBuilder(testEvoluSchema);
@@ -44,6 +45,84 @@ test("history queries select and filter by ownerId bytes", () => {
44
45
  >();
45
46
  });
46
47
 
48
+ test("evoluSchemaToSqliteSchema compiles ordinary indexes", () => {
49
+ const sqliteSchema = evoluSchemaToSqliteSchema(testEvoluSchema, (create) => [
50
+ create("todoTitle").on("todo").column("title"),
51
+ create("todoProjectIdTitle").on("todo").columns(["projectId", "title"]),
52
+ create("todoTitleNotNull")
53
+ .on("todo")
54
+ .column("title")
55
+ .where("title", "is not", null),
56
+ ]);
57
+
58
+ assertEqual(sqliteSchema.indexes, [
59
+ {
60
+ name: "todoTitle",
61
+ sql: 'create index "todoTitle" on "todo" ("title")',
62
+ },
63
+ {
64
+ name: "todoProjectIdTitle",
65
+ sql: 'create index "todoProjectIdTitle" on "todo" ("projectId", "title")',
66
+ },
67
+ {
68
+ name: "todoTitleNotNull",
69
+ sql: 'create index "todoTitleNotNull" on "todo" ("title") where "title" is not null',
70
+ },
71
+ ]);
72
+ });
73
+
74
+ test("evoluSchemaToSqliteSchema rejects unique indexes", () => {
75
+ const error = assertThrowsInstanceOf(
76
+ () =>
77
+ evoluSchemaToSqliteSchema(testEvoluSchema, (create) => [
78
+ create("todoUniqueTitle").on("todo").column("title").unique(),
79
+ ]),
80
+ Error,
81
+ );
82
+
83
+ assertEqual(
84
+ error.message,
85
+ "Unique indexes are not supported because they can prevent synchronization.",
86
+ );
87
+ });
88
+
89
+ test("evoluSchemaToSqliteSchema rejects composite unique indexes", () => {
90
+ const error = assertThrowsInstanceOf(
91
+ () =>
92
+ evoluSchemaToSqliteSchema(testEvoluSchema, (create) => [
93
+ create("todoUniqueProjectIdTitle")
94
+ .on("todo")
95
+ .columns(["projectId", "title"])
96
+ .unique(),
97
+ ]),
98
+ Error,
99
+ );
100
+
101
+ assertEqual(
102
+ error.message,
103
+ "Unique indexes are not supported because they can prevent synchronization.",
104
+ );
105
+ });
106
+
107
+ test("evoluSchemaToSqliteSchema rejects partial unique indexes", () => {
108
+ const error = assertThrowsInstanceOf(
109
+ () =>
110
+ evoluSchemaToSqliteSchema(testEvoluSchema, (create) => [
111
+ create("todoUniqueTitleNotNull")
112
+ .on("todo")
113
+ .column("title")
114
+ .where("title", "is not", null)
115
+ .unique(),
116
+ ]),
117
+ Error,
118
+ );
119
+
120
+ assertEqual(
121
+ error.message,
122
+ "Unique indexes are not supported because they can prevent synchronization.",
123
+ );
124
+ });
125
+
47
126
  test("testEvoluSchema supports project-linked and independent todos", () => {
48
127
  assertType<TestEvoluSchema, typeof testEvoluSchema>();
49
128
  assertType<ValidateSchema<TestEvoluSchema>, TestEvoluSchema>();
@@ -6,7 +6,7 @@
6
6
 
7
7
  import * as Kysely from "kysely";
8
8
  import type { StandardSchemaV1 } from "@standard-schema/spec";
9
- import { assertNonNullable } from "../Assert.ts";
9
+ import { assert, assertNonNullable } from "../Assert.ts";
10
10
  import { getOwnProp, mapObject, type ReadonlyRecord } from "../Object.ts";
11
11
  import {
12
12
  eqSqliteIndex,
@@ -299,6 +299,9 @@ export type ValidateSchema<S extends EvoluSchema> =
299
299
  /**
300
300
  * Defines SQLite indexes with Kysely's index builder.
301
301
  *
302
+ * Unique indexes are not supported because concurrent offline writes can
303
+ * violate uniqueness during synchronization.
304
+ *
302
305
  * @group SQLite
303
306
  */
304
307
  export type IndexesConfig = (
@@ -800,10 +803,17 @@ export const evoluSchemaToSqliteSchema = <S extends EvoluSchema>(
800
803
  );
801
804
 
802
805
  const indexes = indexesConfig
803
- ? indexesConfig(createIndex).map((index): SqliteIndex => ({
804
- name: index.toOperationNode().name.name,
805
- sql: index.compile().sql,
806
- }))
806
+ ? indexesConfig(createIndex).map((index): SqliteIndex => {
807
+ const node = index.toOperationNode();
808
+ assert(
809
+ !node.unique,
810
+ "Unique indexes are not supported because they can prevent synchronization.",
811
+ );
812
+ return {
813
+ name: node.name.name,
814
+ sql: index.compile().sql,
815
+ };
816
+ })
807
817
  : [];
808
818
 
809
819
  return { tables, indexes };