@evolu/common 6.0.1-preview.1 → 6.0.1-preview.10

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.
@@ -1,86 +1,27 @@
1
1
  import { pack } from "msgpackr";
2
+ import { assert } from "../Assert.js";
2
3
  import { mapObject, objectToEntries } from "../Object.js";
3
4
  import { err, ok } from "../Result.js";
4
5
  import { SqliteBoolean } from "../Sqlite.js";
5
- import { brand, createTypeErrorFormatter, DateIsoString, EvoluType, nullableToOptional, nullOr, object, omit, optional, record, Unknown, } from "../Type.js";
6
+ import { brand, createTypeErrorFormatter, DateIsoString, nullableToOptional, nullOr, object, omit, optional, } from "../Type.js";
7
+ import { DbSchema } from "./Db.js";
6
8
  import { createIndexes } from "./Kysely.js";
7
- import { Base64Url256, } from "./Protocol.js";
8
- export const DefaultColumns = object({
9
- createdAt: DateIsoString,
10
- updatedAt: DateIsoString,
11
- isDeleted: nullOr(SqliteBoolean),
12
- });
13
- const isDefaultColumnName = (value) => value === "createdAt" || value === "updatedAt" || value === "isDeleted";
14
- /**
15
- * Valid {@link EvoluSchema}.
16
- *
17
- * - Table and column names must be Base64Url strings.
18
- * - Each table must include an `id` column of type {@link Id}.
19
- * - Default column names (`createdAt`, `updatedAt`, `isDeleted`) are not allowed.
20
- */
21
- export const ValidEvoluSchema = brand("ValidEvoluSchema", record(Base64Url256, object({ id: EvoluType }, record(Base64Url256, Unknown))), (value) => {
22
- for (const tableName in value) {
23
- for (const columnName in value[tableName]) {
24
- if (isDefaultColumnName(columnName)) {
25
- return err({
26
- type: "ValidEvoluSchema",
27
- value,
28
- reason: {
29
- kind: "DefaultColumnError",
30
- tableName,
31
- columnName,
32
- },
33
- });
34
- }
35
- }
36
- }
37
- return ok(value);
38
- });
39
- /**
40
- * Asserts that the given value is {@link ValidEvoluSchema}.
41
- *
42
- * Throws an error if the value is not a valid Evolu schema.
43
- */
44
- export const assertValidEvoluSchema = (value) => {
45
- const validEvoluSchema = ValidEvoluSchema.fromUnknown(value);
46
- if (!validEvoluSchema.ok) {
47
- const message = formatValidEvoluSchemaError(validEvoluSchema.error);
48
- throw new Error(`Invalid Evolu schema: ${message}`);
49
- }
50
- return validEvoluSchema.value;
51
- };
52
- const formatValidEvoluSchemaError = (error) => {
53
- if (error.type === "Record") {
54
- if (error.reason.kind === "Key") {
55
- return `The table "${error.reason.key}" has invalid name. A table name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
56
- }
57
- if (error.reason.kind === "Value" &&
58
- error.reason.error.reason.kind === "Props" &&
59
- error.reason.error.reason.errors.id?.type === "EvoluType") {
60
- return `The table "${error.reason.key}" has invalid ID column. Check examples.`;
61
- }
62
- if (error.reason.kind === "Value" &&
63
- error.reason.error.reason.kind === "IndexKey") {
64
- return `The table "${error.reason.key}" has invalid column name "${error.reason.error.reason.key}". A column name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
65
- }
66
- }
67
- if (error.type === "ValidEvoluSchema") {
68
- return `The table "${error.reason.tableName}" uses reserved column name "${error.reason.columnName}". Reserved column names are: createdAt, updatedAt, isDeleted.`;
69
- }
70
- return JSON.stringify(error, null, 2);
71
- };
72
- export const validEvoluSchemaToDbSchema = (validEvoluSchema, indexes) => {
73
- const tables = objectToEntries(validEvoluSchema).map(([tableName, table]) => ({
9
+ export const evoluSchemaToDbSchema = (schema, indexes) => {
10
+ const tables = objectToEntries(schema).map(([tableName, table]) => ({
74
11
  name: tableName,
75
12
  columns: objectToEntries(table)
76
13
  .filter(([k]) => k !== "id")
77
14
  .map(([k]) => k),
78
15
  }));
79
- return {
80
- tables,
81
- indexes: createIndexes(indexes),
82
- };
16
+ const dbSchema = { tables, indexes: createIndexes(indexes) };
17
+ assert(DbSchema.is(dbSchema), "Invalid EvoluSchema: Table and column names must use only characters A-Za-z0-9_- and be at most 256 characters long.");
18
+ return dbSchema;
83
19
  };
20
+ export const DefaultColumns = object({
21
+ createdAt: DateIsoString,
22
+ updatedAt: DateIsoString,
23
+ isDeleted: nullOr(SqliteBoolean),
24
+ });
84
25
  /**
85
26
  * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
86
27
  * the {@link maxProtocolMessageRangesSize}. The max size is 640KB in bytes,
@@ -36,6 +36,7 @@ export interface SqliteStorageBase {
36
36
  readonly fingerprintRanges: Storage["fingerprintRanges"];
37
37
  readonly findLowerBound: Storage["findLowerBound"];
38
38
  readonly iterate: Storage["iterate"];
39
+ readonly deleteOwner: Storage["deleteOwner"];
39
40
  }
40
41
  export interface SqliteStorageBaseDep {
41
42
  readonly storage: SqliteStorageBase;
@@ -1 +1 @@
1
- {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAM,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAO,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAe,cAAc,EAAe,MAAM,YAAY,CAAC;AAGtE,OAAO,EACL,aAAa,EAQb,OAAO,EAER,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAEvE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,eAAe,EAAE,CACxB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,eAAe,KACvB,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAE/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACzD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C;AAED,eAAO,MAAM,uBAAuB,GACjC,MAAM,iBAAiB,MAEtB,SAAS,8BAA8B,KACtC,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAyIvC,CAAC;AA09BJ,eAAO,MAAM,mBAAmB,GAC7B,MAAM,SAAS,MAEd,SAAS,aAAa,EACtB,OAAO,cAAc,KACpB,MAAM,CAAC,eAAe,EAAE,WAAW,CA6ErC,CAAC"}
1
+ {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAM,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAO,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAe,cAAc,EAAe,MAAM,YAAY,CAAC;AAGtE,OAAO,EACL,aAAa,EAQb,OAAO,EAER,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAEvE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,eAAe,EAAE,CACxB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,eAAe,KACvB,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAE/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACzD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C;AAED,eAAO,MAAM,uBAAuB,GACjC,MAAM,iBAAiB,MAEtB,SAAS,8BAA8B,KACtC,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAoJvC,CAAC;AA09BJ,eAAO,MAAM,mBAAmB,GAC7B,MAAM,SAAS,MAEd,SAAS,aAAa,EACtB,OAAO,cAAc,KACpB,MAAM,CAAC,eAAe,EAAE,WAAW,CA6ErC,CAAC"}
@@ -141,6 +141,16 @@ export const createSqliteStorageBase = (deps) => (options) => {
141
141
  return;
142
142
  }
143
143
  },
144
+ deleteOwner: (ownerId) => {
145
+ const result = deps.sqlite.exec(sql `
146
+ delete from evolu_timestamp where ownerId = ${ownerId};
147
+ `);
148
+ if (!result.ok) {
149
+ options.onStorageError(result.error);
150
+ return false;
151
+ }
152
+ return true;
153
+ },
144
154
  });
145
155
  };
146
156
  const assertBeginEnd = (begin, end) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "6.0.1-preview.1",
3
+ "version": "6.0.1-preview.10",
4
4
  "description": "TypeScript library and local-first framework",
5
5
  "keywords": [
6
6
  "evolu",
@@ -54,10 +54,10 @@
54
54
  "@bokuweb/zstd-wasm": "0.0.27",
55
55
  "@types/better-sqlite3": "^7.6.13",
56
56
  "@types/ws": "^8.18.1",
57
- "better-sqlite3": "^11.10.0",
57
+ "better-sqlite3": "^12.1.1",
58
58
  "shx": "^0.3.4",
59
59
  "typescript": "^5.8.3",
60
- "vitest": "^3.2.2",
60
+ "vitest": "^3.2.3",
61
61
  "ws": "^8.18.2",
62
62
  "@evolu/tsconfig": "0.0.2"
63
63
  },
package/src/Assert.ts CHANGED
@@ -32,9 +32,12 @@ import type { Type } from "./Type.js";
32
32
  * assert(true, "true is not true"); // no-op
33
33
  * assert(false, "true is not true"); // throws Error
34
34
  *
35
- * const size = fooSize - buffer.getLength();
36
- * // Ensure required type
37
- * assert(PositiveInt.is(size), "size is not positive int");
35
+ * const length = buffer.getLength();
36
+ * // We know length is logically non-negative, but TypeScript doesn't
37
+ * assert(
38
+ * NonNegativeInt.is(length),
39
+ * "buffer length should be non-negative",
40
+ * );
38
41
  * ```
39
42
  */
40
43
  export const assert: (
package/src/Crypto.ts CHANGED
@@ -189,3 +189,16 @@ export const padmePaddedLength = (length: NonNegativeInt): NonNegativeInt => {
189
189
  export const padmePaddingLength = (length: NonNegativeInt): NonNegativeInt => {
190
190
  return (padmePaddedLength(length) - length) as NonNegativeInt;
191
191
  };
192
+
193
+ /**
194
+ * Performs a timing-safe comparison of two Uint8Arrays. Returns true if they
195
+ * are equal, false otherwise. Takes constant time regardless of where the
196
+ * arrays differ.
197
+ *
198
+ * @see https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b
199
+ */
200
+ export type TimingSafeEqual = (a: Uint8Array, b: Uint8Array) => boolean;
201
+
202
+ export interface TimingSafeEqualDep {
203
+ readonly timingSafeEqual: TimingSafeEqual;
204
+ }
@@ -5,9 +5,12 @@ import type { DbIndexesBuilder } from "./Kysely.js";
5
5
 
6
6
  export interface Config extends ConsoleConfig {
7
7
  /**
8
- * The name of Evolu instances. Evolu is multitenant - it can run more
9
- * instances concurrently. Every Evolu instance has to have its own unique
10
- * name. Database files are separated and invisible to each other.
8
+ * The name of the Evolu instance. Evolu is multitenant - it can run multiple
9
+ * instances concurrently. Each instance must have a unique name.
10
+ *
11
+ * The instance name is used as the SQLite database filename for persistent
12
+ * storage, ensuring that database files are separated and invisible to each
13
+ * other.
11
14
  *
12
15
  * The default value is: `Evolu`.
13
16
  *
@@ -22,7 +25,7 @@ export interface Config extends ConsoleConfig {
22
25
  /**
23
26
  * URL for Evolu sync and backup server.
24
27
  *
25
- * The default value is `https://free.evoluhq.com`.
28
+ * The default value is `wss://free.evoluhq.com`.
26
29
  */
27
30
  readonly syncUrl: string;
28
31
 
@@ -68,6 +71,17 @@ export interface Config extends ConsoleConfig {
68
71
  * until special UX requirements are needed (e.g., multitenancy).
69
72
  */
70
73
  readonly mnemonic?: Mnemonic;
74
+
75
+ /**
76
+ * Use in-memory SQLite database instead of persistent storage. Useful for
77
+ * testing or temporary data that doesn't need persistence.
78
+ *
79
+ * In-memory databases exist only in RAM and are completely destroyed when
80
+ * the process ends, making them forensically safe for sensitive data.
81
+ *
82
+ * The default value is: `false`.
83
+ */
84
+ readonly inMemory?: boolean;
71
85
  }
72
86
 
73
87
  export interface ConfigDep {
@@ -76,7 +90,7 @@ export interface ConfigDep {
76
90
 
77
91
  export const defaultConfig: Config = {
78
92
  name: getOrThrow(SimpleName.fromParent("Evolu")),
79
- syncUrl: "https://free.evoluhq.com",
93
+ syncUrl: "wss://free.evoluhq.com",
80
94
  reloadUrl: "/",
81
95
  maxDrift: 5 * 60 * 1000,
82
96
  enableLogging: false,
package/src/Evolu/Db.ts CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  SqliteValue,
35
35
  } from "../Sqlite.js";
36
36
  import { TimeDep } from "../Time.js";
37
- import { Id, Mnemonic, object, SimpleName, String } from "../Type.js";
37
+ import { array, Id, Mnemonic, object, SimpleName, String } from "../Type.js";
38
38
  import {
39
39
  createInitializedWorker,
40
40
  Worker,
@@ -54,6 +54,7 @@ import {
54
54
  Base64Url256,
55
55
  BinaryId,
56
56
  binaryIdToId,
57
+ BinaryOwnerId,
57
58
  CrdtMessage,
58
59
  createProtocolMessageForSync,
59
60
  createProtocolMessageFromCrdtMessages,
@@ -96,19 +97,21 @@ import {
96
97
  timestampToTimestampString,
97
98
  } from "./Timestamp.js";
98
99
 
99
- export interface DbSchema {
100
- readonly tables: ReadonlyArray<DbTable>;
101
- readonly indexes: ReadonlyArray<DbIndex>;
102
- }
103
-
104
- export interface DbTable {
105
- readonly name: Base64Url256;
106
- readonly columns: ReadonlyArray<Base64Url256>;
107
- }
100
+ export const DbTable = object({
101
+ name: Base64Url256,
102
+ columns: array(Base64Url256),
103
+ });
104
+ export type DbTable = typeof DbTable.Type;
108
105
 
109
106
  export const DbIndex = object({ name: String, sql: String });
110
107
  export type DbIndex = typeof DbIndex.Type;
111
108
 
109
+ export const DbSchema = object({
110
+ tables: array(DbTable),
111
+ indexes: array(DbIndex),
112
+ });
113
+ export type DbSchema = typeof DbSchema.Type;
114
+
112
115
  export type DbWorker = Worker<DbWorkerInput, DbWorkerOutput>;
113
116
 
114
117
  export type CreateDbWorker = (name: SimpleName) => DbWorker;
@@ -241,6 +244,7 @@ export const createDbWorkerForPlatform = (
241
244
 
242
245
  const sqliteResult = await createSqlite(platformDeps)(
243
246
  initMessage.config.name,
247
+ { memory: initMessage.config.inMemory ?? false },
244
248
  );
245
249
 
246
250
  if (!sqliteResult.ok) {
@@ -496,7 +500,6 @@ export const createDbWorkerForPlatform = (
496
500
  onCompleteId: message.onCompleteId,
497
501
  reload: message.reload,
498
502
  });
499
- deps.sqlite[Symbol.dispose]();
500
503
 
501
504
  break;
502
505
  }
@@ -621,34 +624,32 @@ const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
621
624
 
622
625
  export interface DbSnapshot {
623
626
  readonly schema: DbSchema;
624
- readonly rows: Array<{
625
- rows: ReadonlyArray<SqliteRow>;
627
+ readonly tables: Array<{
626
628
  name: string;
629
+ rows: ReadonlyArray<SqliteRow>;
627
630
  }>;
628
631
  }
629
632
 
630
- // TODO: Move it to Sqlite.
631
- export const getDbSnapshot = (
632
- deps: SqliteDep,
633
- ): Result<DbSnapshot, SqliteError> => {
633
+ // TODO: Move to test helpers.
634
+ export const getDbSnapshot = (deps: SqliteDep): DbSnapshot => {
634
635
  const schema = getDbSchema(deps)({ allIndexes: true });
635
- if (!schema.ok) return schema;
636
+ assert(schema.ok, "bug");
636
637
 
637
- const rows = [];
638
+ const tables = [];
638
639
 
639
640
  for (const table of schema.value.tables) {
640
641
  const result = deps.sqlite.exec(sql`
641
642
  select * from ${sql.identifier(table.name)};
642
643
  `);
643
- if (!result.ok) return result;
644
+ assert(result.ok, "bug");
644
645
 
645
- rows.push({
646
- rows: result.value.rows,
646
+ tables.push({
647
647
  name: table.name,
648
+ rows: result.value.rows,
648
649
  });
649
650
  }
650
651
 
651
- return ok({ schema: schema.value, rows });
652
+ return { schema: schema.value, tables };
652
653
  };
653
654
 
654
655
  const ensureDbSchema =
@@ -787,41 +788,39 @@ const initializeDb =
787
788
  `,
788
789
 
789
790
  /**
790
- * The History table stores all values per timestamp, table, row, and
791
- * column. It's required for merging without conflicts. Evolu uses
792
- * last-write-win CRDT. In case of last-write-win value isn't what we
793
- * want, we can use time travel. The current implementation prefers
794
- * performance over storage size. The History table denormalizes Timestamp
795
- * and DbChange to leverage a covering index. Hence, every value change
796
- * has its timestamp, table, row, and column. In the future, we will
797
- * rethink that and store history more efficiently.
798
- *
799
- * There is no need to use `OwnerId` on the client, because timestamps
800
- * (which include a `NodeId`) are globally unique. The relay needs
801
- * `OwnerId` because it hosts multiple apps/owners, but client storage
802
- * represents one DB, and even when it hosts multiple owners, their
803
- * timestamps remain unique.
791
+ * The History table stores all values per ownerId, timestamp, table, id,
792
+ * and column for conflict-free merging using last-write-win CRDT.
793
+ * Denormalizes Timestamp and DbChange for covering index performance.
794
+ * Time travel is available when last-write-win isn't desired. Future
795
+ * optimization will store history more efficiently.
804
796
  */
805
797
  sql`
806
798
  create table evolu_history (
807
- "timestamp" blob not null,
799
+ "ownerId" blob not null,
808
800
  "table" text not null,
809
- "row" blob not null,
801
+ "id" blob not null,
810
802
  "column" text not null,
803
+ "timestamp" blob not null,
811
804
  "value" any
812
805
  )
813
806
  strict;
814
807
  `,
815
808
 
809
+ // Index for reading database changes by owner and timestamp.
810
+ // Timestamp always corresponds to a DbChange.
816
811
  sql`
817
- create index evolu_history_timestamp on evolu_history ("timestamp");
812
+ create index evolu_history_ownerId_timestamp on evolu_history (
813
+ "ownerId",
814
+ "timestamp"
815
+ );
818
816
  `,
819
817
 
820
818
  sql`
821
- create unique index evolu_history_row_column_table_timestampDesc on evolu_history (
822
- "row",
823
- "column",
819
+ create unique index evolu_history_ownerId_table_id_column_timestampDesc on evolu_history (
820
+ "ownerId",
824
821
  "table",
822
+ "id",
823
+ "column",
825
824
  "timestamp" desc
826
825
  );
827
826
  `,
@@ -847,7 +846,14 @@ const initializeDb =
847
846
 
848
847
  const result = deps.sqlite.exec(sql`
849
848
  insert into evolu_owner
850
- (mnemonic, id, encryptionKey, createdAt, writeKey, timestamp)
849
+ (
850
+ "mnemonic",
851
+ "id",
852
+ "encryptionKey",
853
+ "createdAt",
854
+ "writeKey",
855
+ "timestamp"
856
+ )
851
857
  values
852
858
  (
853
859
  ${ownerRow.mnemonic},
@@ -910,16 +916,18 @@ const applyMessages =
910
916
  (
911
917
  messages: ReadonlyArray<CrdtMessage>,
912
918
  lastTimestamp: Timestamp,
913
- options: { isMigrationFromVersion0?: boolean } = {},
914
919
  ): Result<void, SqliteError> => {
920
+ const ownerId = ownerIdToBinaryOwnerId(deps.ownerRowRef.get().id);
921
+
915
922
  for (const message of messages) {
916
- if (!options.isMigrationFromVersion0) {
917
- const apply1 = applyMessageToAppTable(deps)(message);
918
- if (!apply1.ok) return apply1;
919
- }
923
+ const result1 = applyMessageToAppTable(deps)(ownerId, message);
924
+ if (!result1.ok) return result1;
920
925
 
921
- const apply2 = applyMessageToTimestampAndHistoryTables(deps)(message);
922
- if (!apply2.ok) return apply2;
926
+ const result2 = applyMessageToTimestampAndHistoryTables(deps)(
927
+ ownerId,
928
+ message,
929
+ );
930
+ if (!result2.ok) return result2;
923
931
  }
924
932
 
925
933
  const timestamp = timestampToTimestampString(lastTimestamp);
@@ -933,8 +941,8 @@ const applyMessages =
933
941
  };
934
942
 
935
943
  const applyMessageToAppTable =
936
- (deps: SqliteDep) =>
937
- (message: CrdtMessage): Result<void, SqliteError> => {
944
+ (deps: SqliteDep & OwnerRowRefDep) =>
945
+ (ownerId: BinaryOwnerId, message: CrdtMessage): Result<void, SqliteError> => {
938
946
  const date = new Date(message.timestamp.millis).toISOString();
939
947
  const timestamp = timestampToBinaryTimestamp(message.timestamp);
940
948
 
@@ -942,28 +950,29 @@ const applyMessageToAppTable =
942
950
  const result = deps.sqlite.exec(sql.prepared`
943
951
  with
944
952
  lastTimestamp as (
945
- select timestamp
953
+ select "timestamp"
946
954
  from evolu_history
947
955
  where
948
- "row" = ${message.change.id}
949
- and "column" = ${column}
956
+ "ownerId" = ${ownerId}
950
957
  and "table" = ${message.change.table}
951
- order by timestamp desc
958
+ and "id" = ${message.change.id}
959
+ and "column" = ${column}
960
+ order by "timestamp" desc
952
961
  limit 1
953
962
  )
954
963
  insert into ${sql.identifier(message.change.table)}
955
964
  ("id", ${sql.identifier(column)}, createdAt, updatedAt)
956
965
  select ${message.change.id}, ${value}, ${date}, ${date}
957
966
  where
958
- (select timestamp from lastTimestamp) is null
959
- or (select timestamp from lastTimestamp) < ${timestamp}
967
+ (select "timestamp" from lastTimestamp) is null
968
+ or (select "timestamp" from lastTimestamp) < ${timestamp}
960
969
  on conflict ("id") do update
961
970
  set
962
971
  ${sql.identifier(column)} = ${value},
963
972
  updatedAt = ${date}
964
973
  where
965
- (select timestamp from lastTimestamp) is null
966
- or (select timestamp from lastTimestamp) < ${timestamp};
974
+ (select "timestamp" from lastTimestamp) is null
975
+ or (select "timestamp" from lastTimestamp) < ${timestamp};
967
976
  `);
968
977
 
969
978
  if (!result.ok) return result;
@@ -973,10 +982,9 @@ const applyMessageToAppTable =
973
982
  };
974
983
 
975
984
  export const applyMessageToTimestampAndHistoryTables =
976
- (deps: SqliteDep & RandomDep & OwnerRowRefDep & ClientStorageDep) =>
977
- (message: CrdtMessage): Result<void, SqliteError> => {
985
+ (deps: SqliteDep & ClientStorageDep) =>
986
+ (ownerId: BinaryOwnerId, message: CrdtMessage): Result<void, SqliteError> => {
978
987
  const timestamp = timestampToBinaryTimestamp(message.timestamp);
979
- const ownerId = ownerIdToBinaryOwnerId(deps.ownerRowRef.get().id);
980
988
  const id = idToBinaryId(message.change.id);
981
989
 
982
990
  const result = deps.storage.insertTimestamp(ownerId, timestamp);
@@ -985,9 +993,16 @@ export const applyMessageToTimestampAndHistoryTables =
985
993
  for (const [column, value] of Object.entries(message.change.values)) {
986
994
  const result = deps.sqlite.exec(sql.prepared`
987
995
  insert into evolu_history
988
- ("timestamp", "table", "row", "column", "value")
996
+ ("ownerId", "table", "id", "column", "value", "timestamp")
989
997
  values
990
- (${timestamp}, ${message.change.table}, ${id}, ${column}, ${value})
998
+ (
999
+ ${ownerId},
1000
+ ${message.change.table},
1001
+ ${id},
1002
+ ${column},
1003
+ ${value},
1004
+ ${timestamp}
1005
+ )
991
1006
  on conflict do nothing;
992
1007
  `);
993
1008
  if (!result.ok) return result;
@@ -1063,11 +1078,11 @@ export const maybeMigrateToVersion0 =
1063
1078
  const messagesRows = deps.sqlite.exec<{
1064
1079
  timestamp: TimestampString;
1065
1080
  table: Base64Url256;
1066
- row: Id;
1081
+ id: Id;
1067
1082
  column: Base64Url256;
1068
1083
  value: SqliteValue;
1069
1084
  }>(sql`
1070
- select "timestamp", "table", "row", "column", "value" from evolu_message;
1085
+ select "timestamp", "table", "id", "column", "value" from evolu_message;
1071
1086
  `);
1072
1087
 
1073
1088
  if (!messagesRows.ok) return messagesRows;
@@ -1083,7 +1098,7 @@ export const maybeMigrateToVersion0 =
1083
1098
  const messages = messagesRows.value.rows.map((message) => ({
1084
1099
  timestamp: timestampStringToTimestamp(message.timestamp),
1085
1100
  change: {
1086
- id: message.row,
1101
+ id: message.id,
1087
1102
  table: message.table,
1088
1103
  values: { [message.column]: message.value },
1089
1104
  },
@@ -1176,6 +1191,7 @@ const createClientStorage =
1176
1191
  ...sqliteStorageBase.value,
1177
1192
 
1178
1193
  validateWriteKey: constFalse,
1194
+ setWriteKey: constFalse,
1179
1195
 
1180
1196
  writeMessages: (_ownerId, messages) => {
1181
1197
  // TODO: Get owner by _ownerId when we support more.
@@ -1231,16 +1247,16 @@ const createClientStorage =
1231
1247
  return true;
1232
1248
  },
1233
1249
 
1234
- readDbChange: (_ownerId, timestamp) => {
1250
+ readDbChange: (ownerId, timestamp) => {
1235
1251
  const result = deps.sqlite.exec<{
1236
1252
  table: Base64Url256;
1237
- row: BinaryId;
1253
+ id: BinaryId;
1238
1254
  column: Base64Url256;
1239
1255
  value: SqliteValue;
1240
1256
  }>(sql`
1241
- select "table", "row", "column", "value"
1257
+ select "table", "id", "column", "value"
1242
1258
  from evolu_history
1243
- where "timestamp" = ${timestamp};
1259
+ where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
1244
1260
  `);
1245
1261
  if (!result.ok) {
1246
1262
  deps.postMessage({ type: "onError", error: result.error });
@@ -1250,18 +1266,18 @@ const createClientStorage =
1250
1266
  const { rows } = result.value;
1251
1267
  assert(rows.length > 0, "Rows must not be empty");
1252
1268
 
1253
- const { table, row } = rows[0];
1269
+ const { table, id } = rows[0];
1254
1270
  const values: Record<string, SqliteValue> = {};
1255
1271
 
1256
1272
  for (const r of rows) {
1257
1273
  assert(r.table === table, "All rows must have the same table");
1258
- assert(eqArrayNumber(r.row, row), "All rows must have the same Id");
1274
+ assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
1259
1275
  values[r.column] = r.value;
1260
1276
  }
1261
1277
 
1262
1278
  const change: DbChange = {
1263
1279
  table: rows[0].table,
1264
- id: binaryIdToId(rows[0].row),
1280
+ id: binaryIdToId(rows[0].id),
1265
1281
  values,
1266
1282
  };
1267
1283