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