@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20

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 (140) hide show
  1. package/dist/src/Assert.d.ts.map +1 -1
  2. package/dist/src/Assert.js +1 -1
  3. package/dist/src/Buffer.d.ts +1 -1
  4. package/dist/src/Buffer.d.ts.map +1 -1
  5. package/dist/src/Buffer.js +1 -1
  6. package/dist/src/CallbackRegistry.d.ts +53 -0
  7. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  8. package/dist/src/CallbackRegistry.js +25 -0
  9. package/dist/src/Console.d.ts +31 -6
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +72 -9
  12. package/dist/src/Crypto.d.ts +48 -37
  13. package/dist/src/Crypto.d.ts.map +1 -1
  14. package/dist/src/Crypto.js +27 -50
  15. package/dist/src/Evolu/Db.d.ts +138 -66
  16. package/dist/src/Evolu/Db.d.ts.map +1 -1
  17. package/dist/src/Evolu/Db.js +248 -645
  18. package/dist/src/Evolu/Diff.d.ts +3 -3
  19. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  20. package/dist/src/Evolu/Diff.js +7 -5
  21. package/dist/src/Evolu/Evolu.d.ts +79 -116
  22. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  23. package/dist/src/Evolu/Evolu.js +275 -132
  24. package/dist/src/Evolu/Internal.d.ts +0 -2
  25. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  26. package/dist/src/Evolu/Internal.js +0 -2
  27. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  28. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  29. package/dist/src/Evolu/LocalAuth.js +171 -0
  30. package/dist/src/Evolu/Owner.d.ts +129 -83
  31. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  32. package/dist/src/Evolu/Owner.js +80 -89
  33. package/dist/src/Evolu/Platform.d.ts +9 -7
  34. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  35. package/dist/src/Evolu/Protocol.d.ts +114 -191
  36. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  37. package/dist/src/Evolu/Protocol.js +409 -416
  38. package/dist/src/Evolu/Public.d.ts +6 -8
  39. package/dist/src/Evolu/Public.d.ts.map +1 -1
  40. package/dist/src/Evolu/Public.js +2 -3
  41. package/dist/src/Evolu/PublicKysely.js +3 -3
  42. package/dist/src/Evolu/Relay.d.ts +1 -2
  43. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  44. package/dist/src/Evolu/Relay.js +11 -9
  45. package/dist/src/Evolu/Schema.d.ts +88 -27
  46. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  47. package/dist/src/Evolu/Schema.js +141 -24
  48. package/dist/src/Evolu/Storage.d.ts +158 -14
  49. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  50. package/dist/src/Evolu/Storage.js +32 -32
  51. package/dist/src/Evolu/Sync.d.ts +77 -13
  52. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  53. package/dist/src/Evolu/Sync.js +453 -20
  54. package/dist/src/Evolu/Timestamp.d.ts +29 -27
  55. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  56. package/dist/src/Evolu/Timestamp.js +20 -18
  57. package/dist/src/ManyToManyMap.d.ts +74 -10
  58. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  59. package/dist/src/ManyToManyMap.js +41 -6
  60. package/dist/src/Random.d.ts +3 -2
  61. package/dist/src/Random.d.ts.map +1 -1
  62. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  63. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  64. package/dist/src/RefCountedResourceManager.js +197 -0
  65. package/dist/src/Result.d.ts +144 -22
  66. package/dist/src/Result.d.ts.map +1 -1
  67. package/dist/src/Result.js +5 -2
  68. package/dist/src/Sqlite.d.ts +20 -4
  69. package/dist/src/Sqlite.d.ts.map +1 -1
  70. package/dist/src/Sqlite.js +50 -8
  71. package/dist/src/Task.d.ts +511 -0
  72. package/dist/src/Task.d.ts.map +1 -0
  73. package/dist/src/Task.js +410 -0
  74. package/dist/src/Time.d.ts +59 -0
  75. package/dist/src/Time.d.ts.map +1 -1
  76. package/dist/src/Time.js +87 -4
  77. package/dist/src/Type.d.ts +431 -341
  78. package/dist/src/Type.d.ts.map +1 -1
  79. package/dist/src/Type.js +458 -466
  80. package/dist/src/WebSocket.d.ts +5 -2
  81. package/dist/src/WebSocket.d.ts.map +1 -1
  82. package/dist/src/WebSocket.js +12 -13
  83. package/dist/src/Worker.d.ts +39 -11
  84. package/dist/src/Worker.d.ts.map +1 -1
  85. package/dist/src/Worker.js +22 -4
  86. package/dist/src/index.d.ts +2 -3
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +2 -3
  89. package/package.json +7 -7
  90. package/src/Assert.ts +2 -4
  91. package/src/Buffer.ts +1 -1
  92. package/src/CallbackRegistry.ts +84 -0
  93. package/src/Console.ts +91 -11
  94. package/src/Crypto.ts +78 -91
  95. package/src/Evolu/Db.ts +455 -947
  96. package/src/Evolu/Diff.ts +7 -5
  97. package/src/Evolu/Evolu.ts +545 -307
  98. package/src/Evolu/Internal.ts +0 -2
  99. package/src/Evolu/LocalAuth.ts +422 -0
  100. package/src/Evolu/Owner.ts +191 -131
  101. package/src/Evolu/Platform.ts +9 -9
  102. package/src/Evolu/Protocol.ts +536 -653
  103. package/src/Evolu/Public.ts +7 -9
  104. package/src/Evolu/PublicKysely.ts +3 -3
  105. package/src/Evolu/Relay.ts +17 -12
  106. package/src/Evolu/Schema.ts +271 -66
  107. package/src/Evolu/Storage.ts +263 -55
  108. package/src/Evolu/Sync.ts +758 -37
  109. package/src/Evolu/Timestamp.ts +30 -35
  110. package/src/ManyToManyMap.ts +127 -24
  111. package/src/Random.ts +3 -2
  112. package/src/RefCountedResourceManager.ts +368 -0
  113. package/src/Result.ts +149 -23
  114. package/src/Sqlite.ts +59 -24
  115. package/src/Task.ts +779 -0
  116. package/src/Time.ts +168 -4
  117. package/src/Type.ts +657 -695
  118. package/src/WebSocket.ts +23 -17
  119. package/src/Worker.ts +72 -23
  120. package/src/index.ts +2 -3
  121. package/dist/src/Callbacks.d.ts +0 -20
  122. package/dist/src/Callbacks.d.ts.map +0 -1
  123. package/dist/src/Callbacks.js +0 -18
  124. package/dist/src/Evolu/Config.d.ts +0 -82
  125. package/dist/src/Evolu/Config.d.ts.map +0 -1
  126. package/dist/src/Evolu/Config.js +0 -9
  127. package/dist/src/Evolu/Kysely.d.ts +0 -6
  128. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  129. package/dist/src/Evolu/Kysely.js +0 -21
  130. package/dist/src/NanoId.d.ts +0 -27
  131. package/dist/src/NanoId.d.ts.map +0 -1
  132. package/dist/src/NanoId.js +0 -6
  133. package/dist/src/Promise.d.ts +0 -180
  134. package/dist/src/Promise.d.ts.map +0 -1
  135. package/dist/src/Promise.js +0 -176
  136. package/src/Callbacks.ts +0 -43
  137. package/src/Evolu/Config.ts +0 -97
  138. package/src/Evolu/Kysely.ts +0 -38
  139. package/src/NanoId.ts +0 -39
  140. package/src/Promise.ts +0 -295
@@ -1,450 +1,133 @@
1
1
  import { isNonEmptyArray } from "../Array.js";
2
- import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
3
2
  import { createSymmetricCrypto, } from "../Crypto.js";
4
- import { eqArrayNumber } from "../Eq.js";
5
- import { constFalse, exhaustiveCheck } from "../Function.js";
6
- import { objectToEntries } from "../Object.js";
7
3
  import { ok } from "../Result.js";
8
4
  import { createSqlite, explainSqliteQueryPlan, sql, } from "../Sqlite.js";
9
- import { array, object, String } from "../Type.js";
10
- import { createInitializedWorker, } from "../Worker.js";
5
+ import { SimpleName } from "../Type.js";
6
+ import { createInitializedWorkerWithHandlers, } from "../Worker.js";
11
7
  import { makePatches } from "./Diff.js";
12
- import { createAppOwner } from "./Owner.js";
13
- import { applyProtocolMessageAsClient, Base64Url256, binaryIdToId, binaryOwnerIdToOwnerId, createProtocolMessageForSync, createProtocolMessageFromCrdtMessages, decryptAndDecodeDbChange, encodeAndEncryptDbChange, idToBinaryId, ownerIdToBinaryOwnerId, protocolVersion, } from "./Protocol.js";
8
+ import { createAppOwner, createOwnerSecret, mnemonicToOwnerSecret, } from "./Owner.js";
9
+ import { protocolVersion } from "./Protocol.js";
14
10
  import { createQueryRowsCache, deserializeQuery, emptyRows, } from "./Query.js";
15
- import { createSqliteStorageBase, } from "./Storage.js";
16
- import { binaryTimestampToTimestamp, createInitialTimestamp, receiveTimestamp, sendTimestamp, timestampStringToTimestamp, timestampToBinaryTimestamp, timestampToTimestampString, } from "./Timestamp.js";
17
- export const DbTable = object({
18
- name: Base64Url256,
19
- columns: array(Base64Url256),
20
- });
21
- export const DbIndex = object({ name: String, sql: String });
22
- export const DbSchema = object({
23
- tables: array(DbTable),
24
- indexes: array(DbIndex),
25
- });
26
- const createClock = (deps) => (initialTimestamp = createInitialTimestamp(deps)) => {
27
- let currentTimestamp = initialTimestamp;
28
- return {
29
- get: () => currentTimestamp,
30
- save: (timestamp) => {
31
- currentTimestamp = timestamp;
32
- const timestampString = timestampToTimestampString(timestamp);
33
- const saveTimestamp = deps.sqlite.exec(sql.prepared `
34
- update evolu_config set "clock" = ${timestampString};
35
- `);
36
- if (!saveTimestamp.ok)
37
- return saveTimestamp;
38
- return ok();
39
- },
40
- };
41
- };
42
- export const createDbWorkerForPlatform = (platformDeps) => {
43
- const tabQueryRowsCacheMap = new Map();
44
- const getQueryRowsCache = (tabId) => {
45
- let cache = tabQueryRowsCacheMap.get(tabId);
46
- if (!cache) {
47
- cache = createQueryRowsCache();
48
- tabQueryRowsCacheMap.set(tabId, cache);
49
- }
50
- return cache;
51
- };
52
- return createInitializedWorker({
53
- init: async (initMessage, postMessage, safeHandler) => {
54
- platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
55
- const sqliteResult = await createSqlite(platformDeps)(initMessage.config.name, { memory: initMessage.config.inMemory ?? false });
56
- if (!sqliteResult.ok) {
57
- postMessage({ type: "onError", error: sqliteResult.error });
58
- return null;
59
- }
60
- const sqlite = sqliteResult.value;
61
- const platformDepsWithSqlite = { ...platformDeps, sqlite };
62
- const deps = sqlite.transaction(() => {
63
- const currentDbSchema = getDbSchema({ sqlite })();
64
- if (!currentDbSchema.ok)
65
- return currentDbSchema;
66
- let appOwner;
67
- let clock;
68
- const dbIsInitialized = currentDbSchema.value.tables.some((table) => table.name === "evolu_version");
69
- if (dbIsInitialized) {
70
- const versionResult = sqlite.exec(sql `select protocolVersion from evolu_version limit 1;`);
71
- if (!versionResult.ok)
72
- return versionResult;
73
- // TODO: Handle version migrations here if needed
74
- // const [{ protocolVersion }] = protocolVersionResult.value.rows;
75
- // if (protocolVersion < currentProtocolVersion) {
76
- // const migrateResult = migrateDatabase({ sqlite })(
77
- // protocolVersion,
78
- // currentProtocolVersion
79
- // );
80
- // if (!migrateResult.ok) return migrateResult;
81
- // }
82
- const configResult = sqlite.exec(sql `
83
- select
84
- clock,
85
- appOwnerId,
86
- appOwnerEncryptionKey,
87
- appOwnerWriteKey,
88
- appOwnerMnemonic
89
- from evolu_config
90
- limit 1;
91
- `);
92
- if (!configResult.ok)
93
- return configResult;
94
- const [config] = configResult.value.rows;
95
- appOwner = {
96
- type: "AppOwner",
97
- id: config.appOwnerId,
98
- encryptionKey: config.appOwnerEncryptionKey,
99
- writeKey: config.appOwnerWriteKey,
100
- mnemonic: config.appOwnerMnemonic,
101
- };
102
- clock = createClock(platformDepsWithSqlite)(timestampStringToTimestamp(config.clock));
103
- }
104
- else {
105
- appOwner =
106
- initMessage.config.initialAppOwner ??
107
- createAppOwner(platformDeps.createMnemonic());
108
- clock = createClock(platformDepsWithSqlite)();
109
- const initializeDbResult = initializeDb(platformDepsWithSqlite)(appOwner, clock);
110
- if (!initializeDbResult.ok)
111
- return initializeDbResult;
112
- }
113
- const ensureDbSchemaResult = ensureDbSchema({ sqlite })(initMessage.dbSchema, currentDbSchema.value);
114
- if (!ensureDbSchemaResult.ok)
115
- return ensureDbSchemaResult;
116
- const owners = new Map();
117
- owners.set(appOwner.id, appOwner);
118
- const depsWithoutSyncAndStorage = {
119
- ...platformDeps,
120
- postMessage,
121
- sqlite,
122
- timestampConfig: initMessage.config,
123
- symmetricCrypto: createSymmetricCrypto(platformDeps),
124
- getQueryRowsCache,
125
- clock,
126
- owners,
127
- };
128
- const storage = createClientStorage(depsWithoutSyncAndStorage)({
129
- onStorageError: (error) => {
130
- postMessage({ type: "onError", error });
131
- },
132
- });
133
- if (!storage.ok)
134
- return storage;
135
- const depsWithoutSync = {
136
- ...depsWithoutSyncAndStorage,
137
- storage: storage.value,
138
- };
139
- postMessage({
140
- type: "onInit",
141
- appOwner,
142
- isFirst: !dbIsInitialized,
143
- });
144
- const sync = platformDeps.createSync(platformDeps)({
145
- ...initMessage.config,
146
- onOpen: safeHandler(handleSyncOpen(depsWithoutSync)),
147
- onMessage: safeHandler(createHandleSyncMessage(depsWithoutSync)),
148
- });
149
- return ok({ ...depsWithoutSync, sync });
150
- });
151
- if (!deps.ok) {
152
- postMessage({ type: "onError", error: deps.error });
153
- return null;
154
- }
155
- return deps.value;
156
- },
157
- onMessage: (deps) => (message) => {
158
- switch (message.type) {
159
- case "mutate": {
160
- const mutate = deps.sqlite.transaction(() => {
161
- const toSyncChanges = [];
162
- const localOnlyChanges = [];
163
- for (const change of message.changes) {
164
- // Table name starting with '_' is local-only (not synced).
165
- if (change.table.startsWith("_"))
166
- localOnlyChanges.push(change);
167
- else
168
- toSyncChanges.push(change);
169
- }
170
- for (const change of localOnlyChanges) {
171
- if ("isDeleted" in change.values &&
172
- change.values.isDeleted === 1) {
173
- const result = deps.sqlite.exec(sql `
174
- delete from ${sql.identifier(change.table)}
175
- where id = ${change.id};
176
- `);
177
- if (!result.ok)
178
- return result;
179
- }
180
- else {
181
- const date = new Date(deps.time.now()).toISOString();
182
- for (const [column, value] of objectToEntries(change.values)) {
183
- const result = deps.sqlite.exec(sql.prepared `
184
- insert into ${sql.identifier(change.table)}
185
- ("id", ${sql.identifier(column)}, createdAt, updatedAt)
186
- values (${change.id}, ${value}, ${date}, ${date})
187
- on conflict ("id") do update
188
- set
189
- ${sql.identifier(column)} = ${value},
190
- updatedAt = ${date};
191
- `);
192
- if (!result.ok)
193
- return result;
194
- }
195
- }
196
- }
197
- /**
198
- * We don't have to wait for the transaction to end because changes
199
- * are idempotent, so there is no reason why they should fail. We
200
- * want to call onChange ASAP.
201
- */
202
- const onChange = () => {
203
- deps.console.log("[db]", "onChange", {
204
- subscribedQueries: message.subscribedQueries,
205
- });
206
- const patches = loadQueries(deps)(message.tabId, message.subscribedQueries);
207
- if (!patches.ok) {
208
- deps.postMessage({ type: "onError", error: patches.error });
209
- return;
210
- }
211
- // Notify the tab that performed the mutation.
212
- deps.postMessage({
213
- type: "onChange",
214
- tabId: message.tabId,
215
- patches: patches.value,
216
- onCompleteIds: message.onCompleteIds,
217
- });
218
- // Notify other tabs to refresh their queries.
219
- deps.postMessage({ type: "onReceive", tabId: message.tabId });
220
- };
221
- if (!isNonEmptyArray(toSyncChanges)) {
222
- onChange();
223
- return ok();
224
- }
225
- const messages = applyChanges(deps)(toSyncChanges, onChange);
226
- if (!messages.ok)
227
- return messages;
228
- // TODO: Use owner from db change or AppOwner
229
- const owner = Array.from(deps.owners.values())[0];
230
- const protocolMessage = createProtocolMessageFromCrdtMessages(deps)(owner, messages.value);
231
- deps.console.log("[db]", "send data message", messages.value, protocolMessage);
232
- deps.sync.send(protocolMessage);
233
- return ok();
234
- });
235
- if (!mutate.ok) {
236
- deps.postMessage({ type: "onError", error: mutate.error });
237
- return;
238
- }
239
- break;
240
- }
241
- case "query": {
242
- const patches = loadQueries(deps)(message.tabId, message.queries);
243
- if (!patches.ok) {
244
- deps.postMessage({ type: "onError", error: patches.error });
245
- return;
246
- }
247
- deps.postMessage({
248
- type: "onChange",
249
- tabId: message.tabId,
250
- patches: patches.value,
251
- onCompleteIds: [],
252
- });
253
- break;
254
- }
255
- case "reset": {
256
- const resetResult = deps.sqlite.transaction(() => {
257
- const dropAllTablesResult = dropAllTables(deps);
258
- if (!dropAllTablesResult.ok)
259
- return dropAllTablesResult;
260
- if (message.restore) {
261
- const dbSchema = getDbSchema(deps)();
262
- if (!dbSchema.ok)
263
- return dbSchema;
264
- const ensureDbSchemaResult = ensureDbSchema(deps)(message.restore.dbSchema, dbSchema.value);
265
- if (!ensureDbSchemaResult.ok)
266
- return ensureDbSchemaResult;
267
- const appOwner = createAppOwner(message.restore.mnemonic);
268
- const clock = createClock(deps)();
269
- const initializeDbResult = initializeDb(deps)(appOwner, clock);
270
- if (!initializeDbResult.ok)
271
- return initializeDbResult;
272
- }
273
- return ok();
274
- });
275
- if (!resetResult.ok) {
276
- deps.postMessage({ type: "onError", error: resetResult.error });
277
- return;
278
- }
279
- deps.postMessage({
280
- type: "onReset",
281
- onCompleteId: message.onCompleteId,
282
- reload: message.reload,
283
- });
284
- break;
285
- }
286
- case "ensureDbSchema": {
287
- const ensureSchema = deps.sqlite.transaction(() => {
288
- const dbSchema = getDbSchema(deps)();
289
- if (!dbSchema.ok)
290
- return dbSchema;
291
- const ensureDbSchemaResult = ensureDbSchema(deps)(message.dbSchema, dbSchema.value);
292
- if (!ensureDbSchemaResult.ok)
293
- return ensureDbSchemaResult;
294
- return ok();
295
- });
296
- if (!ensureSchema.ok) {
297
- deps.postMessage({ type: "onError", error: ensureSchema.error });
298
- return;
299
- }
300
- break;
301
- }
302
- case "export": {
303
- const file = deps.sqlite.export();
304
- if (!file.ok) {
305
- deps.postMessage({ type: "onError", error: file.error });
306
- return;
307
- }
308
- deps.postMessage({
309
- type: "onExport",
310
- onCompleteId: message.onCompleteId,
311
- file: file.value,
312
- });
313
- break;
314
- }
315
- default:
316
- exhaustiveCheck(message);
317
- }
318
- },
319
- });
11
+ import { ensureDbSchema, getDbSchema, } from "./Schema.js";
12
+ import { applyLocalOnlyChange, createClock, createSync, } from "./Sync.js";
13
+ import { timestampStringToTimestamp, timestampToTimestampString, } from "./Timestamp.js";
14
+ export const defaultDbConfig = {
15
+ name: SimpleName.orThrow("Evolu"),
16
+ transports: [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
17
+ maxDrift: 5 * 60 * 1000,
18
+ enableLogging: false,
320
19
  };
321
- /**
322
- * Get the current database schema by reading SQLite metadata.
323
- *
324
- * TODO: Refactor out Evolu stuff and move it to Sqlite.
325
- */
326
- export const getDbSchema = (deps) => ({ allIndexes = false } = {}) => {
327
- const map = new Map();
328
- const tableAndColumnInfoRows = deps.sqlite.exec(sql `
329
- select
330
- sqlite_master.name as tableName,
331
- table_info.name as columnName
332
- from
333
- sqlite_master
334
- join pragma_table_info(sqlite_master.name) as table_info;
335
- `);
336
- if (!tableAndColumnInfoRows.ok)
337
- return tableAndColumnInfoRows;
338
- tableAndColumnInfoRows.value.rows.forEach((row) => {
339
- const { tableName, columnName } = row;
340
- if (!map.has(tableName))
341
- map.set(tableName, []);
342
- map.get(tableName)?.push(columnName);
20
+ export const createDbWorkerForPlatform = (platformDeps) => createInitializedWorkerWithHandlers({ init: createDbWorkerDeps(platformDeps), handlers });
21
+ const createDbWorkerDeps = (platformDeps) => async (initMessage, postMessage) => {
22
+ platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
23
+ const sqliteResult = await createSqlite(platformDeps)(initMessage.config.name, {
24
+ memory: initMessage.config.inMemory ?? false,
25
+ encryptionKey: initMessage.config.encryptionKey ?? undefined,
343
26
  });
344
- const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
345
- const indexesRows = deps.sqlite.exec(allIndexes
346
- ? sql `
347
- select name, sql
348
- from sqlite_master
349
- where type = 'index' and name not like 'sqlite_%';
350
- `
351
- : sql `
352
- select name, sql
353
- from sqlite_master
354
- where
355
- type = 'index'
356
- and name not like 'sqlite_%'
357
- and name not like 'evolu_%';
358
- `);
359
- if (!indexesRows.ok)
360
- return indexesRows;
361
- const indexes = indexesRows.value.rows.map((row) => ({
362
- name: row.name,
363
- /**
364
- * SQLite returns "CREATE INDEX" for "create index" for some reason.
365
- * Other keywords remain unchanged. We have to normalize the casing for
366
- * {@link indexesAreEqual} manually.
367
- */
368
- sql: row.sql
369
- .replace("CREATE INDEX", "create index")
370
- .replace("CREATE UNIQUE INDEX", "create unique index"),
371
- }));
372
- return ok({ tables, indexes });
373
- };
374
- const indexesAreEqual = (self, that) => self.name === that.name && self.sql === that.sql;
375
- // TODO: Move to test helpers.
376
- export const getDbSnapshot = (deps) => {
377
- const schema = getDbSchema(deps)({ allIndexes: true });
378
- assert(schema.ok, "bug");
379
- const tables = [];
380
- for (const table of schema.value.tables) {
381
- const result = deps.sqlite.exec(sql `
382
- select * from ${sql.identifier(table.name)};
383
- `);
384
- assert(result.ok, "bug");
385
- tables.push({
386
- name: table.name,
387
- rows: result.value.rows,
388
- });
27
+ if (!sqliteResult.ok) {
28
+ postMessage({ type: "onError", error: sqliteResult.error });
29
+ return null;
389
30
  }
390
- return { schema: schema.value, tables };
391
- };
392
- const ensureDbSchema = (deps) => (newSchema, currentSchema, options) => {
393
- const queries = [];
394
- newSchema.tables.forEach((newTable) => {
395
- const currentTable = currentSchema.tables.find((t) => t.name === newTable.name);
396
- if (!currentTable) {
397
- queries.push({
398
- sql: createAppTable(newTable.name, newTable.columns),
399
- parameters: [],
400
- });
31
+ const sqlite = sqliteResult.value;
32
+ const deps = sqlite.transaction(() => {
33
+ const currentDbSchema = getDbSchema({ sqlite })();
34
+ if (!currentDbSchema.ok)
35
+ return currentDbSchema;
36
+ let appOwner;
37
+ let clock;
38
+ const dbIsInitialized = currentDbSchema.value.tables.some((table) => table.name === "evolu_version");
39
+ if (dbIsInitialized) {
40
+ const currentVersion = sqlite.exec(sql `select protocolVersion from evolu_version limit 1;`);
41
+ if (!currentVersion.ok)
42
+ return currentVersion;
43
+ // TODO: Handle version migrations here if needed
44
+ // const [{ protocolVersion }] = protocolVersionResult.value.rows;
45
+ // if (protocolVersion < currentProtocolVersion) {
46
+ // const migrateResult = migrateDatabase({ sqlite })(
47
+ // protocolVersion,
48
+ // currentProtocolVersion
49
+ // );
50
+ // if (!migrateResult.ok) return migrateResult;
51
+ // }
52
+ const configResult = sqlite.exec(sql `
53
+ select
54
+ clock,
55
+ appOwnerId,
56
+ appOwnerEncryptionKey,
57
+ appOwnerWriteKey,
58
+ appOwnerMnemonic
59
+ from evolu_config
60
+ limit 1;
61
+ `);
62
+ if (!configResult.ok)
63
+ return configResult;
64
+ const [config] = configResult.value.rows;
65
+ appOwner = {
66
+ type: "AppOwner",
67
+ id: config.appOwnerId,
68
+ encryptionKey: config.appOwnerEncryptionKey,
69
+ writeKey: config.appOwnerWriteKey,
70
+ mnemonic: config.appOwnerMnemonic,
71
+ };
72
+ clock = createClock({ ...platformDeps, sqlite })(timestampStringToTimestamp(config.clock));
401
73
  }
402
74
  else {
403
- newTable.columns
404
- .filter((newColumn) => !currentTable.columns.includes(newColumn))
405
- .forEach((newColumn) => {
406
- queries.push(sql `
407
- alter table ${sql.identifier(newTable.name)}
408
- add column ${sql.identifier(newColumn)} blob;
409
- `);
410
- });
75
+ appOwner =
76
+ initMessage.config.externalAppOwner ??
77
+ createAppOwner(createOwnerSecret(platformDeps));
78
+ clock = createClock({ ...platformDeps, sqlite })();
79
+ const result = initializeDb({ sqlite })(appOwner, clock.get());
80
+ if (!result.ok)
81
+ return result;
411
82
  }
412
- });
413
- if (options?.ignoreIndexes !== true) {
414
- // Remove current indexes that are not in the newSchema.
415
- currentSchema.indexes
416
- .filter((currentIndex) => !newSchema.indexes.some((newIndex) => indexesAreEqual(newIndex, currentIndex)))
417
- .forEach((index) => {
418
- queries.push(sql `drop index ${sql.identifier(index.name)};`);
419
- });
420
- // Add new indexes that are not in the currentSchema.
421
- newSchema.indexes
422
- .filter((newIndex) => !currentSchema.indexes.some((currentIndex) => indexesAreEqual(newIndex, currentIndex)))
423
- .forEach((newIndex) => {
424
- queries.push({ sql: `${newIndex.sql};`, parameters: [] });
425
- });
426
- }
427
- for (const query of queries) {
428
- const result = deps.sqlite.exec(query);
83
+ const result = ensureDbSchema({ sqlite })(initMessage.dbSchema, currentDbSchema.value);
429
84
  if (!result.ok)
430
85
  return result;
86
+ const sync = createSync({
87
+ ...platformDeps,
88
+ clock,
89
+ sqlite,
90
+ symmetricCrypto: createSymmetricCrypto(platformDeps),
91
+ timestampConfig: initMessage.config,
92
+ postMessage,
93
+ })({
94
+ appOwner,
95
+ transports: initMessage.config.transports,
96
+ onError: (error) => {
97
+ postMessage({ type: "onError", error });
98
+ },
99
+ onReceive: () => {
100
+ postMessage({ type: "refreshQueries" });
101
+ },
102
+ });
103
+ if (!sync.ok)
104
+ return sync;
105
+ sync.value.useOwner(true, appOwner);
106
+ const tabQueryRowsCacheMap = new Map();
107
+ const getQueryRowsCache = (tabId) => {
108
+ let cache = tabQueryRowsCacheMap.get(tabId);
109
+ if (!cache) {
110
+ cache = createQueryRowsCache();
111
+ tabQueryRowsCacheMap.set(tabId, cache);
112
+ }
113
+ return cache;
114
+ };
115
+ const deps = {
116
+ ...platformDeps,
117
+ getQueryRowsCache,
118
+ postMessage,
119
+ sqlite,
120
+ sync: sync.value,
121
+ appOwner,
122
+ };
123
+ return ok(deps);
124
+ });
125
+ if (!deps.ok) {
126
+ postMessage({ type: "onError", error: deps.error });
127
+ return null;
431
128
  }
432
- return ok();
129
+ return deps.value;
433
130
  };
434
- export const createAppTable = (tableName, columns) => `
435
- create table ${sql.identifier(tableName).sql} (
436
- "id" text primary key,
437
- ${columns
438
- // Add default columns.
439
- .concat(["createdAt", "updatedAt", "isDeleted"])
440
- .filter((c) => c !== "id")
441
- // "A column with affinity BLOB does not prefer one storage class over another
442
- // and no attempt is made to coerce data from one storage class into another."
443
- // https://www.sqlite.org/datatype3.html
444
- .map((name) => `${sql.identifier(name).sql} blob`)
445
- .join(", ")}
446
- );
447
- `;
448
131
  const initializeDb = (deps) => (initialAppOwner, initialClock) => {
449
132
  for (const query of [
450
133
  // Never change structure to ensure all versions can read it.
@@ -479,7 +162,7 @@ const initializeDb = (deps) => (initialAppOwner, initialClock) => {
479
162
  )
480
163
  values
481
164
  (
482
- ${timestampToTimestampString(initialClock.get())},
165
+ ${timestampToTimestampString(initialClock)},
483
166
  ${initialAppOwner.id},
484
167
  ${initialAppOwner.encryptionKey},
485
168
  ${initialAppOwner.writeKey},
@@ -528,97 +211,139 @@ const initializeDb = (deps) => (initialAppOwner, initialClock) => {
528
211
  }
529
212
  return ok();
530
213
  };
531
- const applyChanges = (deps) => (changes, onChange) => {
532
- let clockTimestamp = deps.clock.get();
533
- const messages = [];
534
- for (const change of changes) {
535
- const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
536
- if (!nextTimestamp.ok)
537
- return nextTimestamp;
538
- clockTimestamp = nextTimestamp.value;
539
- messages.push({ timestamp: clockTimestamp, change });
540
- }
541
- const apply = applyMessages(deps)(messages, clockTimestamp);
542
- if (!apply.ok)
543
- return apply;
544
- if (onChange)
545
- onChange();
546
- assertNonEmptyReadonlyArray(messages);
547
- return ok(messages);
548
- };
549
- const applyMessages = (deps) => (messages, clockTimestamp) => {
550
- const ownerId = ownerIdToBinaryOwnerId(Array.from(deps.owners.values())[0].id);
551
- for (const message of messages) {
552
- const result1 = applyMessageToAppTable(deps)(ownerId, message);
553
- if (!result1.ok)
554
- return result1;
555
- const result2 = applyMessageToTimestampAndHistoryTables(deps)(ownerId, message);
556
- if (!result2.ok)
557
- return result2;
558
- }
559
- return deps.clock.save(clockTimestamp);
560
- };
561
- const applyMessageToAppTable = (deps) => (ownerId, message) => {
562
- const timestamp = timestampToBinaryTimestamp(message.timestamp);
563
- const updatedAt = new Date(message.timestamp.millis).toISOString();
564
- for (const [column, value] of objectToEntries(message.change.values)) {
565
- const result = deps.sqlite.exec(sql.prepared `
566
- with
567
- lastTimestamp as (
568
- select "timestamp"
569
- from evolu_history
570
- where
571
- "ownerId" = ${ownerId}
572
- and "table" = ${message.change.table}
573
- and "id" = ${message.change.id}
574
- and "column" = ${column}
575
- order by "timestamp" desc
576
- limit 1
577
- )
578
- insert into ${sql.identifier(message.change.table)}
579
- ("id", ${sql.identifier(column)}, updatedAt)
580
- select ${message.change.id}, ${value}, ${updatedAt}
581
- where
582
- (select "timestamp" from lastTimestamp) is null
583
- or (select "timestamp" from lastTimestamp) < ${timestamp}
584
- on conflict ("id") do update
585
- set
586
- ${sql.identifier(column)} = ${value},
587
- updatedAt = ${updatedAt}
588
- where
589
- (select "timestamp" from lastTimestamp) is null
590
- or (select "timestamp" from lastTimestamp) < ${timestamp};
591
- `);
592
- if (!result.ok)
593
- return result;
594
- }
595
- return ok();
596
- };
597
- export const applyMessageToTimestampAndHistoryTables = (deps) => (ownerId, message) => {
598
- const timestamp = timestampToBinaryTimestamp(message.timestamp);
599
- const id = idToBinaryId(message.change.id);
600
- const result = deps.storage.insertTimestamp(ownerId, timestamp);
601
- if (!result.ok)
602
- return result;
603
- for (const [column, value] of Object.entries(message.change.values)) {
604
- const result = deps.sqlite.exec(sql.prepared `
605
- insert into evolu_history
606
- ("ownerId", "table", "id", "column", "value", "timestamp")
607
- values
608
- (
609
- ${ownerId},
610
- ${message.change.table},
611
- ${id},
612
- ${column},
613
- ${value},
614
- ${timestamp}
615
- )
616
- on conflict do nothing;
617
- `);
618
- if (!result.ok)
619
- return result;
620
- }
621
- return ok();
214
+ const handlers = {
215
+ getAppOwner: (deps) => () => {
216
+ deps.postMessage({
217
+ type: "onGetAppOwner",
218
+ appOwner: deps.appOwner,
219
+ });
220
+ },
221
+ mutate: (deps) => (message) => {
222
+ const mutate = deps.sqlite.transaction(() => {
223
+ const syncChanges = [];
224
+ for (const change of message.changes) {
225
+ const isLocalOnlyChange = change.table.startsWith("_");
226
+ if (isLocalOnlyChange) {
227
+ const result = applyLocalOnlyChange(deps)(change);
228
+ if (!result.ok)
229
+ return result;
230
+ }
231
+ else {
232
+ syncChanges.push(change);
233
+ }
234
+ }
235
+ if (isNonEmptyArray(syncChanges)) {
236
+ const result = deps.sync.applyChanges(syncChanges);
237
+ if (!result.ok)
238
+ return result;
239
+ }
240
+ // Read writes before commit to update UI ASAP
241
+ const queryPatches = loadQueries(deps)(message.tabId, message.subscribedQueries);
242
+ if (!queryPatches.ok)
243
+ return queryPatches;
244
+ // Update the tab that performed the mutation.
245
+ deps.postMessage({
246
+ type: "onQueryPatches",
247
+ tabId: message.tabId,
248
+ queryPatches: queryPatches.value,
249
+ onCompleteIds: message.onCompleteIds,
250
+ });
251
+ // Notify other tabs to refresh their queries.
252
+ deps.postMessage({ type: "refreshQueries", tabId: message.tabId });
253
+ return ok();
254
+ });
255
+ if (!mutate.ok) {
256
+ deps.postMessage({ type: "onError", error: mutate.error });
257
+ return;
258
+ }
259
+ },
260
+ query: (deps) => (message) => {
261
+ const queryPatches = loadQueries(deps)(message.tabId, message.queries);
262
+ if (!queryPatches.ok) {
263
+ deps.postMessage({ type: "onError", error: queryPatches.error });
264
+ return;
265
+ }
266
+ deps.postMessage({
267
+ type: "onQueryPatches",
268
+ tabId: message.tabId,
269
+ queryPatches: queryPatches.value,
270
+ onCompleteIds: [],
271
+ });
272
+ },
273
+ reset: (deps) => (message) => {
274
+ const resetResult = deps.sqlite.transaction(() => {
275
+ const dbSchema = getDbSchema(deps)();
276
+ if (!dbSchema.ok)
277
+ return dbSchema;
278
+ for (const table of dbSchema.value.tables) {
279
+ /**
280
+ * The dropped table is completely removed from the database schema and
281
+ * the disk file. The table can not be recovered. All indices and
282
+ * triggers associated with the table are also deleted.
283
+ * https://sqlite.org/lang_droptable.html
284
+ */
285
+ const result = deps.sqlite.exec(sql `
286
+ drop table ${sql.identifier(table.name)};
287
+ `);
288
+ if (!result.ok)
289
+ return result;
290
+ }
291
+ if (message.restore) {
292
+ const dbSchema = getDbSchema(deps)();
293
+ if (!dbSchema.ok)
294
+ return dbSchema;
295
+ const ensureDbSchemaResult = ensureDbSchema(deps)(message.restore.dbSchema, dbSchema.value);
296
+ if (!ensureDbSchemaResult.ok)
297
+ return ensureDbSchemaResult;
298
+ const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
299
+ const appOwner = createAppOwner(secret);
300
+ const clock = createClock(deps)();
301
+ const initializeDbResult = initializeDb(deps)(appOwner, clock.get());
302
+ if (!initializeDbResult.ok)
303
+ return initializeDbResult;
304
+ }
305
+ return ok();
306
+ });
307
+ if (!resetResult.ok) {
308
+ deps.postMessage({ type: "onError", error: resetResult.error });
309
+ return;
310
+ }
311
+ deps.postMessage({
312
+ type: "onReset",
313
+ onCompleteId: message.onCompleteId,
314
+ reload: message.reload,
315
+ });
316
+ },
317
+ ensureDbSchema: (deps) => (message) => {
318
+ const ensureSchema = deps.sqlite.transaction(() => {
319
+ const dbSchema = getDbSchema(deps)();
320
+ if (!dbSchema.ok)
321
+ return dbSchema;
322
+ const ensureDbSchemaResult = ensureDbSchema(deps)(message.dbSchema, dbSchema.value);
323
+ if (!ensureDbSchemaResult.ok)
324
+ return ensureDbSchemaResult;
325
+ return ok();
326
+ });
327
+ if (!ensureSchema.ok) {
328
+ deps.postMessage({ type: "onError", error: ensureSchema.error });
329
+ return;
330
+ }
331
+ },
332
+ export: (deps) => (message) => {
333
+ const file = deps.sqlite.export();
334
+ if (!file.ok) {
335
+ deps.postMessage({ type: "onError", error: file.error });
336
+ return;
337
+ }
338
+ deps.postMessage({
339
+ type: "onExport",
340
+ onCompleteId: message.onCompleteId,
341
+ file: file.value,
342
+ });
343
+ },
344
+ useOwner: (deps) => (message) => {
345
+ deps.sync.useOwner(message.use, message.owner);
346
+ },
622
347
  };
623
348
  const loadQueries = (deps) => (tabId, queries) => {
624
349
  const queriesRows = [];
@@ -642,125 +367,3 @@ const loadQueries = (deps) => (tabId, queries) => {
642
367
  }));
643
368
  return ok(queryPatchesArray);
644
369
  };
645
- const dropAllTables = (deps) => {
646
- const schema = getDbSchema(deps)();
647
- if (!schema.ok)
648
- return schema;
649
- for (const table of schema.value.tables) {
650
- /**
651
- * The dropped table is completely removed from the database schema and the
652
- * disk file. The table can not be recovered. All indices and triggers
653
- * associated with the table are also deleted.
654
- * https://sqlite.org/lang_droptable.html
655
- */
656
- const result = deps.sqlite.exec(sql `
657
- drop table ${sql.identifier(table.name)};
658
- `);
659
- if (!result.ok)
660
- return result;
661
- }
662
- return ok();
663
- };
664
- const handleSyncOpen = (deps) => (send) => {
665
- for (const [id] of deps.owners) {
666
- const message = createProtocolMessageForSync(deps)(id);
667
- if (!message)
668
- return;
669
- deps.console.log("[db]", "send initial sync message", message);
670
- send(message);
671
- }
672
- };
673
- const createHandleSyncMessage = (deps) => (input, send) => {
674
- deps.console.log("[db]", "receive sync message", input);
675
- const output = deps.sqlite.transaction(() => applyProtocolMessageAsClient(deps)(input, {
676
- getWriteKey: (ownerId) => deps.owners.get(ownerId)?.writeKey ?? null,
677
- }));
678
- if (!output.ok) {
679
- deps.postMessage({ type: "onError", error: output.error });
680
- return;
681
- }
682
- if (output.value) {
683
- deps.console.log("[db]", "respond sync message", output.value);
684
- send(output.value);
685
- }
686
- };
687
- const createClientStorage = (deps) => (options) => {
688
- const sqliteStorageBase = createSqliteStorageBase(deps)(options);
689
- if (!sqliteStorageBase.ok)
690
- return sqliteStorageBase;
691
- const storage = {
692
- ...sqliteStorageBase.value,
693
- validateWriteKey: constFalse,
694
- setWriteKey: constFalse,
695
- writeMessages: (ownerId, messages) => {
696
- const owner = deps.owners.get(binaryOwnerIdToOwnerId(ownerId));
697
- assert(owner, "Missing owner");
698
- const decodedAndDecryptedMessages = [];
699
- for (const message of messages) {
700
- const dbChange = decryptAndDecodeDbChange(deps)(message, owner.encryptionKey);
701
- if (!dbChange.ok) {
702
- deps.postMessage({
703
- type: "onError",
704
- error: dbChange.error,
705
- });
706
- return false;
707
- }
708
- decodedAndDecryptedMessages.push({
709
- timestamp: message.timestamp,
710
- change: dbChange.value,
711
- });
712
- }
713
- let clockTimestamp = deps.clock.get();
714
- for (const message of messages) {
715
- const receive = receiveTimestamp(deps)(clockTimestamp, message.timestamp);
716
- if (!receive.ok) {
717
- deps.postMessage({ type: "onError", error: receive.error });
718
- return false;
719
- }
720
- clockTimestamp = receive.value;
721
- }
722
- const applyMessagesResult = applyMessages({ ...deps, storage })(decodedAndDecryptedMessages, clockTimestamp);
723
- if (!applyMessagesResult.ok) {
724
- deps.postMessage({
725
- type: "onError",
726
- error: applyMessagesResult.error,
727
- });
728
- return false;
729
- }
730
- deps.postMessage({ type: "onReceive" });
731
- return true;
732
- },
733
- readDbChange: (ownerId, timestamp) => {
734
- const owner = deps.owners.get(binaryOwnerIdToOwnerId(ownerId));
735
- assert(owner, "Missing owner");
736
- const result = deps.sqlite.exec(sql `
737
- select "table", "id", "column", "value"
738
- from evolu_history
739
- where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
740
- `);
741
- if (!result.ok) {
742
- deps.postMessage({ type: "onError", error: result.error });
743
- return null;
744
- }
745
- const { rows } = result.value;
746
- assert(rows.length > 0, "Rows must not be empty");
747
- const { table, id } = rows[0];
748
- const values = {};
749
- for (const r of rows) {
750
- assert(r.table === table, "All rows must have the same table");
751
- assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
752
- values[r.column] = r.value;
753
- }
754
- const message = {
755
- timestamp: binaryTimestampToTimestamp(timestamp),
756
- change: {
757
- table: rows[0].table,
758
- id: binaryIdToId(rows[0].id),
759
- values,
760
- },
761
- };
762
- return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
763
- },
764
- };
765
- return ok(storage);
766
- };