@evolu/common 7.4.0 → 8.0.0-next.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.
- package/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
|
@@ -1,135 +1,281 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { applyLocalOnlyChange, createClock, createSync, tryApplyQuarantinedMessages, } 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,
|
|
20
|
-
};
|
|
21
|
-
export const createDbWorkerForPlatform = (platformDeps) => createInitializedWorkerWithHandlers({
|
|
22
|
-
init: async (initMessage, postMessage) => {
|
|
23
|
-
platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
|
|
24
|
-
const deps = await createDbWorkerDeps(platformDeps, initMessage, postMessage);
|
|
25
|
-
if (!deps.ok) {
|
|
26
|
-
postMessage({ type: "onError", error: deps.error });
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
return deps.value;
|
|
30
|
-
},
|
|
31
|
-
handlers,
|
|
32
|
-
});
|
|
33
|
-
const createDbWorkerDeps = async (platformDeps, initMessage, postMessage) => {
|
|
34
|
-
const sqlite = await createSqlite(platformDeps)(initMessage.config.name, {
|
|
35
|
-
memory: initMessage.config.inMemory ?? false,
|
|
36
|
-
encryptionKey: initMessage.config.encryptionKey ?? undefined,
|
|
37
|
-
});
|
|
38
|
-
if (!sqlite.ok)
|
|
39
|
-
return sqlite;
|
|
40
|
-
const deps = { ...platformDeps, sqlite: sqlite.value };
|
|
41
|
-
return deps.sqlite.transaction(() => {
|
|
42
|
-
const dbSchema = getDbSchema(deps)();
|
|
43
|
-
if (!dbSchema.ok)
|
|
44
|
-
return dbSchema;
|
|
45
|
-
const dbIsInitialized = "evolu_version" in dbSchema.value.tables;
|
|
46
|
-
let appOwner;
|
|
47
|
-
let clock;
|
|
48
|
-
if (dbIsInitialized) {
|
|
49
|
-
const currentVersion = deps.sqlite.exec(sql `select protocolVersion from evolu_version limit 1;`);
|
|
50
|
-
if (!currentVersion.ok)
|
|
51
|
-
return currentVersion;
|
|
52
|
-
const configResult = deps.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
|
-
assertNonEmptyReadonlyArray(configResult.value.rows);
|
|
65
|
-
const config = firstInArray(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(deps)(timestampBytesToTimestamp(config.clock));
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic Evolu DbWorker.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
7
|
+
if (value !== null && value !== void 0) {
|
|
8
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
9
|
+
var dispose, inner;
|
|
10
|
+
if (async) {
|
|
11
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
12
|
+
dispose = value[Symbol.asyncDispose];
|
|
74
13
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
clock = createClock(deps)();
|
|
80
|
-
const result = initializeDb(deps)(appOwner, clock.get());
|
|
81
|
-
if (!result.ok)
|
|
82
|
-
return result;
|
|
14
|
+
if (dispose === void 0) {
|
|
15
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
16
|
+
dispose = value[Symbol.dispose];
|
|
17
|
+
if (async) inner = dispose;
|
|
83
18
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
19
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
20
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
21
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
22
|
+
}
|
|
23
|
+
else if (async) {
|
|
24
|
+
env.stack.push({ async: true });
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
29
|
+
return function (env) {
|
|
30
|
+
function fail(e) {
|
|
31
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
32
|
+
env.hasError = true;
|
|
88
33
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
34
|
+
var r, s = 0;
|
|
35
|
+
function next() {
|
|
36
|
+
while (r = env.stack.pop()) {
|
|
37
|
+
try {
|
|
38
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
39
|
+
if (r.dispose) {
|
|
40
|
+
var result = r.dispose.call(r.value);
|
|
41
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
42
|
+
}
|
|
43
|
+
else s |= 1;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
fail(e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
50
|
+
if (env.hasError) throw env.error;
|
|
93
51
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
52
|
+
return next();
|
|
53
|
+
};
|
|
54
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
55
|
+
var e = new Error(message);
|
|
56
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
57
|
+
});
|
|
58
|
+
import { appendToArray, firstInArray, } from "../Array.js";
|
|
59
|
+
import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
60
|
+
import { EncryptionKey, } from "../Crypto.js";
|
|
61
|
+
import { exhaustiveCheck, lazyFalse, lazyVoid } from "../Function.js";
|
|
62
|
+
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
63
|
+
import { ok } from "../Result.js";
|
|
64
|
+
import { booleanToSqliteBoolean, createSqlite, sql, SqliteBoolean, sqliteBooleanToBoolean, sqliteQueryStringToSqliteQuery, SqliteValue, } from "../Sqlite.js";
|
|
65
|
+
import { callback } from "../Task.js";
|
|
66
|
+
import { Millis, millisToDateIso } from "../Time.js";
|
|
67
|
+
import { Id, IdBytes, idBytesToId, idToIdBytes, onePositiveInt, } from "../Type.js";
|
|
68
|
+
import { ownerIdBytesToOwnerId, ownerIdToOwnerIdBytes } from "./Owner.js";
|
|
69
|
+
import { applyProtocolMessageAsClient, createProtocolMessageForSync, decryptAndDecodeDbChange, encodeAndEncryptDbChange, protocolVersion, SubscriptionFlags, } from "./Protocol.js";
|
|
70
|
+
import { ensureSqliteSchema, getEvoluSqliteSchema, systemColumns, } from "./Schema.js";
|
|
71
|
+
import { createBaseSqliteStorage, createBaseSqliteStorageTables, DbChange, getOwnerUsage, getTimestampInsertStrategy, updateOwnerUsage, } from "./Storage.js";
|
|
72
|
+
import { createInitialTimestamp, defaultTimestampMaxDrift, receiveTimestamp, sendTimestamp, TimestampBytes, timestampBytesToTimestamp, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
73
|
+
export const startDbWorker = (self) => async (_run) => {
|
|
74
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
75
|
+
try {
|
|
76
|
+
const stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
|
|
77
|
+
const run = stack.use(_run.create());
|
|
78
|
+
const { deps } = run;
|
|
79
|
+
let initialized = false;
|
|
80
|
+
const initMessage = await run.orThrow(callback(({ ok }) => {
|
|
81
|
+
self.onMessage = (message) => {
|
|
82
|
+
assert(!initialized, "DbWorker must be initialized only once");
|
|
83
|
+
initialized = true;
|
|
84
|
+
ok(message);
|
|
85
|
+
};
|
|
86
|
+
}));
|
|
87
|
+
const console = deps.console.child(initMessage.name).child("DbWorker");
|
|
88
|
+
console.setLevel(initMessage.consoleLevel);
|
|
89
|
+
console.info("start DbWorker");
|
|
90
|
+
const port = stack.use(deps.createMessagePort(initMessage.port));
|
|
91
|
+
stack.defer(deps.consoleStoreOutputEntry.subscribe(() => {
|
|
92
|
+
const entry = deps.consoleStoreOutputEntry.get();
|
|
93
|
+
if (entry)
|
|
94
|
+
port.postMessage({ type: "OnConsoleEntry", entry });
|
|
95
|
+
}));
|
|
96
|
+
stack.use(await run.orThrow(deps.leaderLock.lock(initMessage.name)));
|
|
97
|
+
if (stack.disposed)
|
|
98
|
+
return ok();
|
|
99
|
+
port.postMessage({ type: "LeaderAcquired", name: initMessage.name });
|
|
100
|
+
const sqlite = stack.use(await run.orThrow(createSqlite(initMessage.name, initMessage.memoryOnly
|
|
101
|
+
? { mode: "memory" }
|
|
102
|
+
: { mode: "encrypted", encryptionKey: initMessage.encryptionKey })));
|
|
103
|
+
console.debug("SQLite created");
|
|
104
|
+
const baseSqliteStorage = createBaseSqliteStorage({ sqlite, ...run.deps });
|
|
105
|
+
const dbDeps = {
|
|
106
|
+
...run.deps,
|
|
107
|
+
sqlite,
|
|
108
|
+
sqliteSchema: initMessage.sqliteSchema,
|
|
109
|
+
baseSqliteStorage,
|
|
110
|
+
timestampConfig: { maxDrift: defaultTimestampMaxDrift },
|
|
111
|
+
};
|
|
112
|
+
const currentSchema = getEvoluSqliteSchema(dbDeps)();
|
|
113
|
+
const dbIsInitialized = "evolu_version" in currentSchema.tables;
|
|
114
|
+
const clock = createClock(dbDeps)(dbIsInitialized);
|
|
115
|
+
sqlite.transaction(() => {
|
|
116
|
+
if (!dbIsInitialized)
|
|
117
|
+
initializeDb(dbDeps)(clock.get());
|
|
118
|
+
ensureSqliteSchema(dbDeps)(initMessage.sqliteSchema, currentSchema);
|
|
119
|
+
tryApplyQuarantinedMessages(dbDeps);
|
|
120
|
+
});
|
|
121
|
+
const storage = createClientStorage({ ...dbDeps, clock })({
|
|
103
122
|
onError: (error) => {
|
|
104
|
-
postMessage({ type: "
|
|
105
|
-
},
|
|
106
|
-
onReceive: () => {
|
|
107
|
-
postMessage({ type: "refreshQueries" });
|
|
123
|
+
port.postMessage({ type: "OnError", error });
|
|
108
124
|
},
|
|
109
125
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
// TODO: Call on dispose message.
|
|
127
|
+
const _moved = stack.move();
|
|
128
|
+
const runWithStorage = run.addDeps({ storage });
|
|
129
|
+
/**
|
|
130
|
+
* SharedWorker repeats sends until it gets a response, so handling here
|
|
131
|
+
* must be idempotent and ignore already processed IDs.
|
|
132
|
+
*
|
|
133
|
+
* TODO: Bound memory growth by evicting old IDs.
|
|
134
|
+
*/
|
|
135
|
+
const processedRequestIds = new Set();
|
|
136
|
+
port.onMessage = ({ callbackId, request }) => {
|
|
137
|
+
if (processedRequestIds.has(callbackId))
|
|
138
|
+
return;
|
|
139
|
+
processedRequestIds.add(callbackId);
|
|
140
|
+
const postQueuedResponse = (response) => {
|
|
141
|
+
port.postMessage({ type: "OnQueuedResponse", callbackId, response }, response.type === "ForEvolu" && response.message.type === "Export"
|
|
142
|
+
? [response.message.file.buffer]
|
|
143
|
+
: undefined);
|
|
144
|
+
};
|
|
145
|
+
switch (request.type) {
|
|
146
|
+
case "ForEvolu": {
|
|
147
|
+
switch (request.message.type) {
|
|
148
|
+
case "Mutate": {
|
|
149
|
+
const result = handleMutation({ ...dbDeps, clock })(request.message);
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
port.postMessage({ type: "OnError", error: result.error });
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
postQueuedResponse({
|
|
155
|
+
type: "ForEvolu",
|
|
156
|
+
evoluPortId: request.evoluPortId,
|
|
157
|
+
message: result.value,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "Query":
|
|
163
|
+
postQueuedResponse({
|
|
164
|
+
type: "ForEvolu",
|
|
165
|
+
evoluPortId: request.evoluPortId,
|
|
166
|
+
message: {
|
|
167
|
+
type: "Query",
|
|
168
|
+
rowsByQuery: loadQueries(dbDeps)(request.message.queries),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
case "Export":
|
|
173
|
+
postQueuedResponse({
|
|
174
|
+
type: "ForEvolu",
|
|
175
|
+
evoluPortId: request.evoluPortId,
|
|
176
|
+
message: {
|
|
177
|
+
type: "Export",
|
|
178
|
+
file: dbDeps.sqlite.export(),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
exhaustiveCheck(request.message);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "ForSharedWorker": {
|
|
188
|
+
switch (request.message.type) {
|
|
189
|
+
case "CreateSyncMessages": {
|
|
190
|
+
const protocolMessagesByOwnerId = new Map();
|
|
191
|
+
for (const owner of request.message.owners) {
|
|
192
|
+
storage.setOwnerState(owner.encryptionKey);
|
|
193
|
+
const protocolMessage = createProtocolMessageForSync({
|
|
194
|
+
storage,
|
|
195
|
+
console,
|
|
196
|
+
})(owner.id, SubscriptionFlags.Subscribe);
|
|
197
|
+
if (protocolMessage) {
|
|
198
|
+
protocolMessagesByOwnerId.set(owner.id, protocolMessage);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
postQueuedResponse({
|
|
202
|
+
type: "ForSharedWorker",
|
|
203
|
+
message: {
|
|
204
|
+
type: "CreateSyncMessages",
|
|
205
|
+
protocolMessagesByOwnerId,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "ApplySyncMessage": {
|
|
211
|
+
const { owner, inputMessage } = request.message;
|
|
212
|
+
runWithStorage(async (run) => {
|
|
213
|
+
storage.setOwnerState(owner.encryptionKey);
|
|
214
|
+
const result = await run(applyProtocolMessageAsClient(inputMessage, {
|
|
215
|
+
writeKey: owner.writeKey,
|
|
216
|
+
}));
|
|
217
|
+
const didWriteMessages = storage.didWriteMessages();
|
|
218
|
+
postQueuedResponse({
|
|
219
|
+
type: "ForSharedWorker",
|
|
220
|
+
message: {
|
|
221
|
+
type: "ApplySyncMessage",
|
|
222
|
+
ownerId: owner.id,
|
|
223
|
+
didWriteMessages,
|
|
224
|
+
result,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
return ok();
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
exhaustiveCheck(request.message);
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
exhaustiveCheck(request);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
return ok();
|
|
241
|
+
}
|
|
242
|
+
catch (e_1) {
|
|
243
|
+
env_1.error = e_1;
|
|
244
|
+
env_1.hasError = true;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
const result_1 = __disposeResources(env_1);
|
|
248
|
+
if (result_1)
|
|
249
|
+
await result_1;
|
|
250
|
+
}
|
|
129
251
|
};
|
|
130
|
-
const
|
|
252
|
+
const createClock = (deps) => (dbIsInitialized) => {
|
|
253
|
+
let currentTimestamp;
|
|
254
|
+
if (dbIsInitialized) {
|
|
255
|
+
const { rows } = deps.sqlite.exec(sql `
|
|
256
|
+
select clock
|
|
257
|
+
from evolu_config
|
|
258
|
+
limit 1;
|
|
259
|
+
`);
|
|
260
|
+
assertNonEmptyReadonlyArray(rows);
|
|
261
|
+
currentTimestamp = timestampBytesToTimestamp(firstInArray(rows).clock);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
currentTimestamp = createInitialTimestamp(deps);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
get: () => currentTimestamp,
|
|
268
|
+
save: (timestamp) => {
|
|
269
|
+
currentTimestamp = timestamp;
|
|
270
|
+
deps.sqlite.exec(sql.prepared `
|
|
271
|
+
update evolu_config
|
|
272
|
+
set "clock" = ${timestampToTimestampBytes(timestamp)};
|
|
273
|
+
`);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
const initializeDb = ({ sqlite }) => (initialClock) => {
|
|
131
278
|
for (const query of [
|
|
132
|
-
// Never change structure to ensure all versions can read it.
|
|
133
279
|
sql `
|
|
134
280
|
create table evolu_version (
|
|
135
281
|
"protocolVersion" integer not null
|
|
@@ -142,31 +288,13 @@ const initializeDb = (deps) => (initialAppOwner, initialClock) => {
|
|
|
142
288
|
`,
|
|
143
289
|
sql `
|
|
144
290
|
create table evolu_config (
|
|
145
|
-
"clock" blob not null
|
|
146
|
-
"appOwnerId" text not null,
|
|
147
|
-
"appOwnerEncryptionKey" blob not null,
|
|
148
|
-
"appOwnerWriteKey" blob not null,
|
|
149
|
-
"appOwnerMnemonic" text
|
|
291
|
+
"clock" blob not null
|
|
150
292
|
)
|
|
151
293
|
strict;
|
|
152
294
|
`,
|
|
153
295
|
sql `
|
|
154
|
-
insert into evolu_config
|
|
155
|
-
|
|
156
|
-
"clock",
|
|
157
|
-
"appOwnerId",
|
|
158
|
-
"appOwnerEncryptionKey",
|
|
159
|
-
"appOwnerWriteKey",
|
|
160
|
-
"appOwnerMnemonic"
|
|
161
|
-
)
|
|
162
|
-
values
|
|
163
|
-
(
|
|
164
|
-
${timestampToTimestampBytes(initialClock)},
|
|
165
|
-
${initialAppOwner.id},
|
|
166
|
-
${initialAppOwner.encryptionKey},
|
|
167
|
-
${initialAppOwner.writeKey},
|
|
168
|
-
${initialAppOwner.mnemonic ?? null}
|
|
169
|
-
);
|
|
296
|
+
insert into evolu_config ("clock")
|
|
297
|
+
values (${timestampToTimestampBytes(initialClock)});
|
|
170
298
|
`,
|
|
171
299
|
/**
|
|
172
300
|
* The History table stores all values per ownerId, timestamp, table, id,
|
|
@@ -202,167 +330,368 @@ const initializeDb = (deps) => (initialAppOwner, initialClock) => {
|
|
|
202
330
|
"timestamp" desc
|
|
203
331
|
);
|
|
204
332
|
`,
|
|
333
|
+
/**
|
|
334
|
+
* Stores messages with unknown schema in a quarantine table.
|
|
335
|
+
*
|
|
336
|
+
* When a device receives sync messages containing tables or columns that
|
|
337
|
+
* don't exist in its current schema (e.g., from a newer app version),
|
|
338
|
+
* those messages are stored here instead of being discarded. This enables
|
|
339
|
+
* forward compatibility:
|
|
340
|
+
*
|
|
341
|
+
* 1. Unknown data is preserved and can be applied when the app is updated
|
|
342
|
+
* 2. Messages are still propagated to other devices that may understand them
|
|
343
|
+
* 3. Partial messages work - known columns go to app tables, unknown to
|
|
344
|
+
* quarantine
|
|
345
|
+
*
|
|
346
|
+
* The `union all` query in `readDbChange` combines `evolu_history` and
|
|
347
|
+
* this table, ensuring all data (known and unknown) is included when
|
|
348
|
+
* syncing to other devices.
|
|
349
|
+
*/
|
|
350
|
+
sql `
|
|
351
|
+
create table evolu_message_quarantine (
|
|
352
|
+
"ownerId" blob not null,
|
|
353
|
+
"timestamp" blob not null,
|
|
354
|
+
"table" text not null,
|
|
355
|
+
"id" blob not null,
|
|
356
|
+
"column" text not null,
|
|
357
|
+
"value" any,
|
|
358
|
+
primary key ("ownerId", "timestamp", "table", "id", "column")
|
|
359
|
+
)
|
|
360
|
+
strict;
|
|
361
|
+
`,
|
|
205
362
|
]) {
|
|
206
|
-
|
|
207
|
-
if (!result.ok)
|
|
208
|
-
return result;
|
|
363
|
+
sqlite.exec(query);
|
|
209
364
|
}
|
|
210
|
-
|
|
211
|
-
if (!result.ok)
|
|
212
|
-
return result;
|
|
213
|
-
return ok();
|
|
365
|
+
createBaseSqliteStorageTables({ sqlite });
|
|
214
366
|
};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* exist in its current schema (e.g., from a newer app version), those messages
|
|
220
|
-
* are stored here instead of being discarded. This enables forward
|
|
221
|
-
* compatibility:
|
|
222
|
-
*
|
|
223
|
-
* 1. Unknown data is preserved and can be applied when the app is updated
|
|
224
|
-
* 2. Messages are still propagated to other devices that may understand them
|
|
225
|
-
* 3. Partial messages work - known columns go to app tables, unknown to quarantine
|
|
226
|
-
*
|
|
227
|
-
* The `union all` query in `readDbChange` combines `evolu_history` and this
|
|
228
|
-
* table, ensuring all data (known and unknown) is included when syncing to
|
|
229
|
-
* other devices.
|
|
230
|
-
*/
|
|
231
|
-
const ensureMessageQuarantineTable = (deps) => {
|
|
232
|
-
const result = deps.sqlite.exec(sql `
|
|
233
|
-
create table if not exists evolu_message_quarantine (
|
|
234
|
-
"ownerId" blob not null,
|
|
235
|
-
"timestamp" blob not null,
|
|
236
|
-
"table" text not null,
|
|
237
|
-
"id" blob not null,
|
|
238
|
-
"column" text not null,
|
|
239
|
-
"value" any,
|
|
240
|
-
primary key ("ownerId", "timestamp", "table", "id", "column")
|
|
241
|
-
)
|
|
242
|
-
strict;
|
|
367
|
+
const tryApplyQuarantinedMessages = (deps) => {
|
|
368
|
+
const rows = deps.sqlite.exec(sql `
|
|
369
|
+
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
370
|
+
from evolu_message_quarantine;
|
|
243
371
|
`);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
372
|
+
for (const row of rows.rows) {
|
|
373
|
+
if (!validateColumnValue(deps)(row.table, row.column, row.value))
|
|
374
|
+
continue;
|
|
375
|
+
applyColumnChange(deps)(row.ownerId, ownerIdBytesToOwnerId(row.ownerId), row.table, row.id, idBytesToId(row.id), row.column, row.value, row.timestamp);
|
|
376
|
+
deps.sqlite.exec(sql `
|
|
377
|
+
delete from evolu_message_quarantine
|
|
378
|
+
where
|
|
379
|
+
"ownerId" = ${row.ownerId}
|
|
380
|
+
and "timestamp" = ${row.timestamp}
|
|
381
|
+
and "table" = ${row.table}
|
|
382
|
+
and "id" = ${row.id}
|
|
383
|
+
and "column" = ${row.column};
|
|
384
|
+
`);
|
|
385
|
+
}
|
|
247
386
|
};
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
387
|
+
const validateColumnValue = (deps) => (table, column, _value) => {
|
|
388
|
+
const schemaColumns = getProperty(deps.sqliteSchema.tables, table);
|
|
389
|
+
return (schemaColumns != null &&
|
|
390
|
+
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column)));
|
|
391
|
+
};
|
|
392
|
+
const systemColumnsWithoutOwnerId = systemColumns.difference(new Set(["ownerId"]));
|
|
393
|
+
const applyColumnChange = (deps) => (ownerIdBytes, ownerId, table, idBytes, id, column, value, timestampBytes) => {
|
|
394
|
+
deps.sqlite.exec(sql.prepared `
|
|
395
|
+
with
|
|
396
|
+
existingTimestamp as (
|
|
397
|
+
select 1
|
|
398
|
+
from evolu_history
|
|
399
|
+
where
|
|
400
|
+
"ownerId" = ${ownerIdBytes}
|
|
401
|
+
and "table" = ${table}
|
|
402
|
+
and "id" = ${idBytes}
|
|
403
|
+
and "column" = ${column}
|
|
404
|
+
and "timestamp" >= ${timestampBytes}
|
|
405
|
+
limit 1
|
|
406
|
+
)
|
|
407
|
+
insert into ${sql.identifier(table)}
|
|
408
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
409
|
+
select ${ownerId}, ${id}, ${value}
|
|
410
|
+
where not exists (select 1 from existingTimestamp)
|
|
411
|
+
on conflict ("ownerId", "id") do update
|
|
412
|
+
set ${sql.identifier(column)} = ${value}
|
|
413
|
+
where not exists (select 1 from existingTimestamp);
|
|
414
|
+
`);
|
|
415
|
+
deps.sqlite.exec(sql.prepared `
|
|
416
|
+
insert into evolu_history
|
|
417
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
418
|
+
values
|
|
419
|
+
(
|
|
420
|
+
${ownerIdBytes},
|
|
421
|
+
${table},
|
|
422
|
+
${idBytes},
|
|
423
|
+
${column},
|
|
424
|
+
${value},
|
|
425
|
+
${timestampBytes}
|
|
426
|
+
)
|
|
427
|
+
on conflict do nothing;
|
|
428
|
+
`);
|
|
429
|
+
};
|
|
430
|
+
const createClientStorage = (deps) => ({ onError, }) => {
|
|
431
|
+
let encryptionKey = null;
|
|
432
|
+
let didWriteMessages = false;
|
|
433
|
+
const getEncryptionKey = () => {
|
|
434
|
+
assert(encryptionKey != null, "ClientStorage encryption key must be set");
|
|
435
|
+
return encryptionKey;
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
...deps.baseSqliteStorage,
|
|
439
|
+
// DEV: ClientStorage was designed when Storage and Sync lived in the
|
|
440
|
+
// same file.
|
|
441
|
+
// This is safe because the worker handles one message at a time. We will
|
|
442
|
+
// refactor it later, we will probably have to change Protocol API.
|
|
443
|
+
setOwnerState: (nextEncryptionKey) => {
|
|
444
|
+
encryptionKey = nextEncryptionKey;
|
|
445
|
+
didWriteMessages = false;
|
|
446
|
+
},
|
|
447
|
+
didWriteMessages: () => didWriteMessages,
|
|
448
|
+
// Not implemented yet.
|
|
449
|
+
validateWriteKey: lazyFalse,
|
|
450
|
+
setWriteKey: lazyVoid,
|
|
451
|
+
writeMessages: (ownerIdBytes, encryptedMessages) => () => {
|
|
452
|
+
// TODO: Add quota checking for collaborative scenarios.
|
|
453
|
+
// When receiving messages from other owners via relay broadcast,
|
|
454
|
+
// check if this owner is within quota before accepting the data.
|
|
455
|
+
// This prevents an owner from exceeding storage limits when receiving
|
|
456
|
+
// data shared by other collaborators.
|
|
457
|
+
const messages = [];
|
|
458
|
+
const currentEncryptionKey = getEncryptionKey();
|
|
459
|
+
for (const message of encryptedMessages) {
|
|
460
|
+
const change = decryptAndDecodeDbChange(message, currentEncryptionKey);
|
|
461
|
+
if (!change.ok) {
|
|
462
|
+
onError(change.error);
|
|
463
|
+
return ok();
|
|
267
464
|
}
|
|
465
|
+
messages.push({ timestamp: message.timestamp, change: change.value });
|
|
268
466
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
467
|
+
let clockTimestamp = deps.clock.get();
|
|
468
|
+
for (const message of messages) {
|
|
469
|
+
const nextTimestamp = receiveTimestamp(deps)(clockTimestamp, message.timestamp);
|
|
470
|
+
if (!nextTimestamp.ok) {
|
|
471
|
+
onError(nextTimestamp.error);
|
|
472
|
+
return ok();
|
|
473
|
+
}
|
|
474
|
+
clockTimestamp = nextTimestamp.value;
|
|
273
475
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
type: "onQueryPatches",
|
|
281
|
-
tabId: message.tabId,
|
|
282
|
-
queryPatches: queryPatches.value,
|
|
283
|
-
onCompleteIds: message.onCompleteIds,
|
|
476
|
+
assertNonEmptyReadonlyArray(messages);
|
|
477
|
+
return deps.sqlite.transaction(() => {
|
|
478
|
+
applyMessages(deps)(ownerIdBytesToOwnerId(ownerIdBytes), messages);
|
|
479
|
+
deps.clock.save(clockTimestamp);
|
|
480
|
+
didWriteMessages = true;
|
|
481
|
+
return ok();
|
|
284
482
|
});
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const queryPatches = loadQueries(deps)(message.tabId, message.queries);
|
|
296
|
-
if (!queryPatches.ok) {
|
|
297
|
-
deps.postMessage({ type: "onError", error: queryPatches.error });
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
deps.postMessage({
|
|
301
|
-
type: "onQueryPatches",
|
|
302
|
-
tabId: message.tabId,
|
|
303
|
-
queryPatches: queryPatches.value,
|
|
304
|
-
onCompleteIds: [],
|
|
305
|
-
});
|
|
306
|
-
},
|
|
307
|
-
reset: (deps) => (message) => {
|
|
308
|
-
const result = deps.sqlite.transaction(() => {
|
|
309
|
-
const dbSchema = getDbSchema(deps)();
|
|
310
|
-
if (!dbSchema.ok)
|
|
311
|
-
return dbSchema;
|
|
312
|
-
for (const tableName in dbSchema.value.tables) {
|
|
313
|
-
/**
|
|
314
|
-
* The dropped table is completely removed from the database schema and
|
|
315
|
-
* the disk file. The table can not be recovered. All indices and
|
|
316
|
-
* triggers associated with the table are also deleted.
|
|
317
|
-
* https://sqlite.org/lang_droptable.html
|
|
318
|
-
*/
|
|
319
|
-
const result = deps.sqlite.exec(sql `
|
|
320
|
-
drop table ${sql.identifier(tableName)};
|
|
483
|
+
},
|
|
484
|
+
readDbChange: (ownerId, timestamp) => {
|
|
485
|
+
const result = deps.sqlite.exec(sql `
|
|
486
|
+
select "table", "id", "column", "value"
|
|
487
|
+
from evolu_history
|
|
488
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
489
|
+
union all
|
|
490
|
+
select "table", "id", "column", "value"
|
|
491
|
+
from evolu_message_quarantine
|
|
492
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
321
493
|
`);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
494
|
+
const { rows } = result;
|
|
495
|
+
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
|
|
496
|
+
const firstRow = firstInArray(rows);
|
|
497
|
+
const values = createRecord();
|
|
498
|
+
let isInsert = false;
|
|
499
|
+
let isDelete = null;
|
|
500
|
+
for (const r of rows) {
|
|
501
|
+
switch (r.column) {
|
|
502
|
+
case "createdAt":
|
|
503
|
+
isInsert = true;
|
|
504
|
+
break;
|
|
505
|
+
case "updatedAt":
|
|
506
|
+
isInsert = false;
|
|
507
|
+
break;
|
|
508
|
+
case "isDeleted":
|
|
509
|
+
if (SqliteBoolean.is(r.value)) {
|
|
510
|
+
isDelete = sqliteBooleanToBoolean(r.value);
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
default:
|
|
514
|
+
values[r.column] = r.value;
|
|
515
|
+
}
|
|
333
516
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
517
|
+
const message = {
|
|
518
|
+
timestamp: timestampBytesToTimestamp(timestamp),
|
|
519
|
+
change: DbChange.orThrow({
|
|
520
|
+
table: firstRow.table,
|
|
521
|
+
id: idBytesToId(firstRow.id),
|
|
522
|
+
values,
|
|
523
|
+
isInsert,
|
|
524
|
+
isDelete,
|
|
525
|
+
}),
|
|
526
|
+
};
|
|
527
|
+
return encodeAndEncryptDbChange(deps)(message, getEncryptionKey());
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
const handleMutation = (deps) => (message) => deps.sqlite.transaction(() => {
|
|
532
|
+
const messagesByOwnerId = new Map();
|
|
533
|
+
let clockTimestamp = deps.clock.get();
|
|
534
|
+
let clockChanged = false;
|
|
535
|
+
for (const change of message.changes) {
|
|
536
|
+
if (change.table.startsWith("_")) {
|
|
537
|
+
applyLocalOnlyChange(deps)(change);
|
|
538
|
+
continue;
|
|
339
539
|
}
|
|
340
|
-
deps
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
540
|
+
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
541
|
+
if (!nextTimestamp.ok)
|
|
542
|
+
return nextTimestamp;
|
|
543
|
+
clockTimestamp = nextTimestamp.value;
|
|
544
|
+
clockChanged = true;
|
|
545
|
+
const { ownerId, ...dbChange } = change;
|
|
546
|
+
const message = {
|
|
547
|
+
timestamp: clockTimestamp,
|
|
548
|
+
change: dbChange,
|
|
549
|
+
};
|
|
550
|
+
const messages = messagesByOwnerId.get(ownerId);
|
|
551
|
+
if (messages)
|
|
552
|
+
messages.push(message);
|
|
553
|
+
else
|
|
554
|
+
messagesByOwnerId.set(ownerId, [message]);
|
|
555
|
+
}
|
|
556
|
+
for (const [ownerId, messages] of messagesByOwnerId) {
|
|
557
|
+
applyMessages(deps)(ownerId, messages);
|
|
558
|
+
}
|
|
559
|
+
if (clockChanged)
|
|
560
|
+
deps.clock.save(clockTimestamp);
|
|
561
|
+
return ok({
|
|
562
|
+
type: "Mutate",
|
|
563
|
+
messagesByOwnerId,
|
|
564
|
+
rowsByQuery: loadQueries(deps)(message.subscribedQueries),
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
const applyLocalOnlyChange = (deps) => (change) => {
|
|
568
|
+
if (change.isDelete) {
|
|
569
|
+
deps.sqlite.exec(sql `
|
|
570
|
+
delete from ${sql.identifier(change.table)}
|
|
571
|
+
where id = ${change.id};
|
|
572
|
+
`);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const ownerId = change.ownerId;
|
|
576
|
+
const columns = dbChangeToColumns(change, deps.time.now());
|
|
577
|
+
for (const [column, value] of columns) {
|
|
578
|
+
deps.sqlite.exec(sql.prepared `
|
|
579
|
+
insert into ${sql.identifier(change.table)}
|
|
580
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
581
|
+
values (${ownerId}, ${change.id}, ${value})
|
|
582
|
+
on conflict ("ownerId", "id") do update
|
|
583
|
+
set ${sql.identifier(column)} = ${value};
|
|
584
|
+
`);
|
|
351
585
|
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const applyMessages = (deps) => (ownerId, messages) => {
|
|
589
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
590
|
+
const usage = getOwnerUsage(deps)(ownerIdBytes, timestampToTimestampBytes(firstInArray(messages).timestamp));
|
|
591
|
+
if (!usage.ok)
|
|
592
|
+
return;
|
|
593
|
+
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
594
|
+
for (const { timestamp, change } of messages) {
|
|
595
|
+
const columns = dbChangeToColumns(change, timestamp.millis);
|
|
596
|
+
const idBytes = idToIdBytes(change.id);
|
|
597
|
+
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
598
|
+
for (const [column, value] of columns) {
|
|
599
|
+
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
600
|
+
applyColumnChange(deps)(ownerIdBytes, ownerId, change.table, idBytes, change.id, column, value, timestampBytes);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
deps.sqlite.exec(sql.prepared `
|
|
604
|
+
insert into evolu_message_quarantine
|
|
605
|
+
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
606
|
+
values
|
|
607
|
+
(
|
|
608
|
+
${ownerIdBytes},
|
|
609
|
+
${timestampBytes},
|
|
610
|
+
${change.table},
|
|
611
|
+
${idBytes},
|
|
612
|
+
${column},
|
|
613
|
+
${value}
|
|
614
|
+
)
|
|
615
|
+
on conflict do nothing;
|
|
616
|
+
`);
|
|
617
|
+
}
|
|
358
618
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
619
|
+
let strategy;
|
|
620
|
+
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(timestampBytes, firstTimestamp, lastTimestamp);
|
|
621
|
+
deps.baseSqliteStorage.insertTimestamp(ownerIdBytes, timestampBytes, strategy);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* TODO: Implement proper storedBytes tracking for client using received and
|
|
625
|
+
* sent encrypted message sizes.
|
|
626
|
+
*/
|
|
627
|
+
updateOwnerUsage(deps)(ownerIdBytes, onePositiveInt, // Placeholder until proper tracking implemented
|
|
628
|
+
firstTimestamp, lastTimestamp);
|
|
629
|
+
};
|
|
630
|
+
const dbChangeToColumns = (change, now) => {
|
|
631
|
+
let values = objectToEntries(change.values);
|
|
632
|
+
// SystemColumns are not encoded in change.values.
|
|
633
|
+
values = appendToArray(values, [
|
|
634
|
+
change.isInsert ? "createdAt" : "updatedAt",
|
|
635
|
+
millisToDateIso(now),
|
|
636
|
+
]);
|
|
637
|
+
if (change.isDelete != null) {
|
|
638
|
+
values = appendToArray(values, [
|
|
639
|
+
"isDeleted",
|
|
640
|
+
booleanToSqliteBoolean(change.isDelete),
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
return values;
|
|
644
|
+
};
|
|
645
|
+
const loadQueries = (deps) => (queries) => {
|
|
646
|
+
const rowsByQuery = new Map();
|
|
647
|
+
for (const query of queries) {
|
|
648
|
+
const { rows } = deps.sqlite.exec(sqliteQueryStringToSqliteQuery(query));
|
|
649
|
+
rowsByQuery.set(query, rows);
|
|
650
|
+
}
|
|
651
|
+
return rowsByQuery;
|
|
368
652
|
};
|
|
653
|
+
// reset: (deps) => (message) => {
|
|
654
|
+
// const result = deps.sqlite.transaction(() => {
|
|
655
|
+
// const sqliteSchema = getSqliteSchema(deps)();
|
|
656
|
+
// if (!sqliteSchema.ok) return sqliteSchema;
|
|
657
|
+
// for (const tableName in sqliteSchema.value.tables) {
|
|
658
|
+
// /**
|
|
659
|
+
// * The dropped table is completely removed from the database schema and
|
|
660
|
+
// * the disk file. The table can not be recovered. All indices and
|
|
661
|
+
// * triggers associated with the table are also deleted.
|
|
662
|
+
// * https://sqlite.org/lang_droptable.html
|
|
663
|
+
// */
|
|
664
|
+
// const result = deps.sqlite.exec(sql`
|
|
665
|
+
// drop table ${sql.identifier(tableName)};
|
|
666
|
+
// `);
|
|
667
|
+
// if (!result.ok) return result;
|
|
668
|
+
// }
|
|
669
|
+
// if (message.restore) {
|
|
670
|
+
// const result = ensureSqliteSchema(deps)(message.restore.sqliteSchema);
|
|
671
|
+
// if (!result.ok) return result;
|
|
672
|
+
// const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
|
|
673
|
+
// const appOwner = createAppOwner(secret);
|
|
674
|
+
// const clock = createClock(deps)();
|
|
675
|
+
// return initializeDb(deps)(appOwner, clock.get());
|
|
676
|
+
// }
|
|
677
|
+
// return ok();
|
|
678
|
+
// });
|
|
679
|
+
// if (!result.ok) {
|
|
680
|
+
// deps.postMessage({ type: "onError", error: result.error });
|
|
681
|
+
// return;
|
|
682
|
+
// }
|
|
683
|
+
// deps.postMessage({
|
|
684
|
+
// type: "onReset",
|
|
685
|
+
// onCompleteId: message.onCompleteId,
|
|
686
|
+
// reload: message.reload,
|
|
687
|
+
// });
|
|
688
|
+
// },
|
|
689
|
+
// ensureSqliteSchema: (deps) => (message) => {
|
|
690
|
+
// const result = deps.sqlite.transaction(() =>
|
|
691
|
+
// ensureSqliteSchema(deps)(message.sqliteSchema),
|
|
692
|
+
// );
|
|
693
|
+
// if (!result.ok) {
|
|
694
|
+
// deps.postMessage({ type: "onError", error: result.error });
|
|
695
|
+
// return;
|
|
696
|
+
// }
|
|
697
|
+
// },
|