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