@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- 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 +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- 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 +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- 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 +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- 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 +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- 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/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -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/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 +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -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 +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- 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 +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -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 -82
- 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 -97
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Db.ts
CHANGED
|
@@ -1,64 +1,43 @@
|
|
|
1
1
|
import { isNonEmptyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ConsoleDep } from "../Console.js";
|
|
2
|
+
import { CallbackId } from "../CallbackRegistry.js";
|
|
3
|
+
import { ConsoleConfig, ConsoleDep } from "../Console.js";
|
|
5
4
|
import {
|
|
6
|
-
CreateMnemonicDep,
|
|
7
|
-
CreateRandomBytesDep,
|
|
8
5
|
createSymmetricCrypto,
|
|
9
6
|
EncryptionKey,
|
|
7
|
+
RandomBytesDep,
|
|
10
8
|
SymmetricCryptoDecryptError,
|
|
11
|
-
SymmetricCryptoDep,
|
|
12
9
|
} from "../Crypto.js";
|
|
13
|
-
import { eqArrayNumber } from "../Eq.js";
|
|
14
10
|
import { TransferableError } from "../Error.js";
|
|
15
|
-
import { constFalse, exhaustiveCheck } from "../Function.js";
|
|
16
|
-
import { NanoIdLibDep } from "../NanoId.js";
|
|
17
|
-
import { objectToEntries } from "../Object.js";
|
|
18
11
|
import { RandomDep } from "../Random.js";
|
|
19
12
|
import { ok, Result } from "../Result.js";
|
|
20
13
|
import {
|
|
21
14
|
createSqlite,
|
|
22
15
|
CreateSqliteDriverDep,
|
|
23
16
|
explainSqliteQueryPlan,
|
|
24
|
-
SafeSql,
|
|
25
17
|
sql,
|
|
26
18
|
SqliteDep,
|
|
27
19
|
SqliteError,
|
|
28
|
-
SqliteQuery,
|
|
29
|
-
SqliteRow,
|
|
30
|
-
SqliteValue,
|
|
31
20
|
} from "../Sqlite.js";
|
|
32
21
|
import { TimeDep } from "../Time.js";
|
|
33
|
-
import {
|
|
22
|
+
import { Id, Mnemonic, SimpleName } from "../Type.js";
|
|
23
|
+
import { CreateWebSocketDep } from "../WebSocket.js";
|
|
34
24
|
import {
|
|
35
|
-
|
|
25
|
+
createInitializedWorkerWithHandlers,
|
|
26
|
+
MessageHandlers,
|
|
36
27
|
Worker,
|
|
37
|
-
WorkerPostMessageDep,
|
|
38
28
|
} from "../Worker.js";
|
|
39
|
-
import { Config } from "./Config.js";
|
|
40
29
|
import { makePatches, QueryPatches } from "./Diff.js";
|
|
41
|
-
import { AppOwner, createAppOwner, OwnerId, WriteKey } from "./Owner.js";
|
|
42
30
|
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
decryptAndDecodeDbChange,
|
|
54
|
-
encodeAndEncryptDbChange,
|
|
55
|
-
idToBinaryId,
|
|
56
|
-
ownerIdToBinaryOwnerId,
|
|
57
|
-
ProtocolError,
|
|
58
|
-
protocolVersion,
|
|
59
|
-
Storage,
|
|
60
|
-
StorageDep,
|
|
61
|
-
} from "./Protocol.js";
|
|
31
|
+
AppOwner,
|
|
32
|
+
createAppOwner,
|
|
33
|
+
createOwnerSecret,
|
|
34
|
+
mnemonicToOwnerSecret,
|
|
35
|
+
OwnerEncryptionKey,
|
|
36
|
+
OwnerId,
|
|
37
|
+
OwnerWriteKey,
|
|
38
|
+
TransportConfig,
|
|
39
|
+
} from "./Owner.js";
|
|
40
|
+
import { ProtocolError, protocolVersion } from "./Protocol.js";
|
|
62
41
|
import {
|
|
63
42
|
createQueryRowsCache,
|
|
64
43
|
deserializeQuery,
|
|
@@ -67,42 +46,145 @@ import {
|
|
|
67
46
|
QueryRowsCache,
|
|
68
47
|
} from "./Query.js";
|
|
69
48
|
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
DbSchema,
|
|
50
|
+
ensureDbSchema,
|
|
51
|
+
getDbSchema,
|
|
52
|
+
MutationChange,
|
|
53
|
+
} from "./Schema.js";
|
|
54
|
+
import {
|
|
55
|
+
applyLocalOnlyChange,
|
|
56
|
+
Clock,
|
|
57
|
+
createClock,
|
|
58
|
+
createSync,
|
|
59
|
+
SyncDep,
|
|
60
|
+
SyncOwner,
|
|
61
|
+
} from "./Sync.js";
|
|
75
62
|
import {
|
|
76
|
-
binaryTimestampToTimestamp,
|
|
77
|
-
createInitialTimestamp,
|
|
78
|
-
receiveTimestamp,
|
|
79
|
-
sendTimestamp,
|
|
80
63
|
Timestamp,
|
|
81
|
-
|
|
82
|
-
TimestampCounterOverflowError,
|
|
83
|
-
TimestampDriftError,
|
|
64
|
+
TimestampConfig,
|
|
84
65
|
TimestampError,
|
|
85
66
|
TimestampString,
|
|
86
67
|
timestampStringToTimestamp,
|
|
87
|
-
TimestampTimeOutOfRangeError,
|
|
88
|
-
timestampToBinaryTimestamp,
|
|
89
68
|
timestampToTimestampString,
|
|
90
69
|
} from "./Timestamp.js";
|
|
91
70
|
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
71
|
+
export interface DbConfig extends ConsoleConfig, TimestampConfig {
|
|
72
|
+
/**
|
|
73
|
+
* The name of the Evolu instance. Evolu is multitenant - it can run multiple
|
|
74
|
+
* instances concurrently. Each instance must have a unique name.
|
|
75
|
+
*
|
|
76
|
+
* The instance name is used as the SQLite database filename for persistent
|
|
77
|
+
* storage, ensuring that database files are separated and invisible to each
|
|
78
|
+
* other.
|
|
79
|
+
*
|
|
80
|
+
* The default value is: `Evolu`.
|
|
81
|
+
*
|
|
82
|
+
* ### Example
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* // name: SimpleName.orThrow("MyApp")
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
readonly name: SimpleName;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Transport configuration for data sync and backup. Supports single transport
|
|
92
|
+
* or multiple transports simultaneously for redundancy.
|
|
93
|
+
*
|
|
94
|
+
* Currently supports:
|
|
95
|
+
*
|
|
96
|
+
* - WebSocket: Real-time bidirectional communication with relay servers
|
|
97
|
+
*
|
|
98
|
+
* Empty transports create local-only instances. Transports can be dynamically
|
|
99
|
+
* added and removed for any owner (including {@link AppOwner}) via
|
|
100
|
+
* {@link Evolu#useOwner}.
|
|
101
|
+
*
|
|
102
|
+
* The default value is:
|
|
103
|
+
*
|
|
104
|
+
* `{ type: "WebSocket", url: "wss://free.evoluhq.com" }`.
|
|
105
|
+
*
|
|
106
|
+
* ### Example
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* // Single WebSocket relay
|
|
110
|
+
* transports: [{ type: "WebSocket", url: "wss://relay1.example.com" }];
|
|
111
|
+
*
|
|
112
|
+
* // Multiple WebSocket relays for redundancy
|
|
113
|
+
* transports: [
|
|
114
|
+
* { type: "WebSocket", url: "wss://relay1.example.com" },
|
|
115
|
+
* { type: "WebSocket", url: "wss://relay2.example.com" },
|
|
116
|
+
* { type: "WebSocket", url: "wss://relay3.example.com" },
|
|
117
|
+
* ];
|
|
118
|
+
*
|
|
119
|
+
* // Local-only instance (no sync) - useful for device settings
|
|
120
|
+
* transports: [];
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
readonly transports: ReadonlyArray<TransportConfig>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* External AppOwner to use when creating Evolu instance. Use this when you
|
|
127
|
+
* want to manage AppOwner creation and persistence externally (e.g., with
|
|
128
|
+
* your own authentication system). If omitted, Evolu will automatically
|
|
129
|
+
* create and persist an AppOwner locally.
|
|
130
|
+
*
|
|
131
|
+
* For device-specific settings and account management state, we can use a
|
|
132
|
+
* separate local-only Evolu instance via `transports: []`.
|
|
133
|
+
*
|
|
134
|
+
* ### Example
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const ConfigId = id("Config");
|
|
138
|
+
* type ConfigId = typeof ConfigId.Type;
|
|
139
|
+
*
|
|
140
|
+
* const DeviceSchema = {
|
|
141
|
+
* config: {
|
|
142
|
+
* id: ConfigId,
|
|
143
|
+
* key: NonEmptyString50,
|
|
144
|
+
* value: NonEmptyString50,
|
|
145
|
+
* },
|
|
146
|
+
* };
|
|
147
|
+
*
|
|
148
|
+
* // Local-only instance for device settings (no sync)
|
|
149
|
+
* const deviceEvolu = createEvolu(evoluReactWebDeps)(DeviceSchema, {
|
|
150
|
+
* name: SimpleName.orThrow("MyApp-Device"),
|
|
151
|
+
* transports: [], // No sync - stays local to device
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* // Main synced instance for user data
|
|
155
|
+
* const evolu = createEvolu(evoluReactWebDeps)(MainSchema, {
|
|
156
|
+
* name: SimpleName.orThrow("MyApp"),
|
|
157
|
+
* // Default transports for sync
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
readonly externalAppOwner?: AppOwner;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Use in-memory SQLite database instead of persistent storage. Useful for
|
|
165
|
+
* testing or temporary data that doesn't need persistence.
|
|
166
|
+
*
|
|
167
|
+
* In-memory databases exist only in RAM and are completely destroyed when the
|
|
168
|
+
* process ends, making them forensically safe for sensitive data.
|
|
169
|
+
*
|
|
170
|
+
* The default value is: `false`.
|
|
171
|
+
*/
|
|
172
|
+
readonly inMemory?: boolean;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Encryption key for the SQLite database.
|
|
176
|
+
*
|
|
177
|
+
* @experimental
|
|
178
|
+
*/
|
|
179
|
+
readonly encryptionKey?: EncryptionKey;
|
|
180
|
+
}
|
|
100
181
|
|
|
101
|
-
export const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
182
|
+
export const defaultDbConfig: DbConfig = {
|
|
183
|
+
name: SimpleName.orThrow("Evolu"),
|
|
184
|
+
transports: [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
|
|
185
|
+
maxDrift: 5 * 60 * 1000,
|
|
186
|
+
enableLogging: false,
|
|
187
|
+
};
|
|
106
188
|
|
|
107
189
|
export type DbWorker = Worker<DbWorkerInput, DbWorkerOutput>;
|
|
108
190
|
|
|
@@ -115,13 +197,16 @@ export interface CreateDbWorkerDep {
|
|
|
115
197
|
export type DbWorkerInput =
|
|
116
198
|
| {
|
|
117
199
|
readonly type: "init";
|
|
118
|
-
readonly config:
|
|
200
|
+
readonly config: DbConfig;
|
|
119
201
|
readonly dbSchema: DbSchema;
|
|
120
202
|
}
|
|
203
|
+
| {
|
|
204
|
+
readonly type: "getAppOwner";
|
|
205
|
+
}
|
|
121
206
|
| {
|
|
122
207
|
readonly type: "mutate";
|
|
123
208
|
readonly tabId: Id;
|
|
124
|
-
readonly changes: NonEmptyReadonlyArray<
|
|
209
|
+
readonly changes: NonEmptyReadonlyArray<MutationChange>;
|
|
125
210
|
readonly onCompleteIds: ReadonlyArray<CallbackId>;
|
|
126
211
|
readonly subscribedQueries: ReadonlyArray<Query>;
|
|
127
212
|
}
|
|
@@ -146,31 +231,35 @@ export type DbWorkerInput =
|
|
|
146
231
|
| {
|
|
147
232
|
readonly type: "export";
|
|
148
233
|
readonly onCompleteId: CallbackId;
|
|
234
|
+
}
|
|
235
|
+
| {
|
|
236
|
+
readonly type: "useOwner";
|
|
237
|
+
readonly use: boolean;
|
|
238
|
+
readonly owner: SyncOwner;
|
|
149
239
|
};
|
|
150
240
|
|
|
151
241
|
export type DbWorkerOutput =
|
|
152
|
-
| {
|
|
153
|
-
readonly type: "onInit";
|
|
154
|
-
readonly appOwner: AppOwner;
|
|
155
|
-
readonly isFirst: boolean;
|
|
156
|
-
}
|
|
157
242
|
| {
|
|
158
243
|
readonly type: "onError";
|
|
159
244
|
readonly error:
|
|
245
|
+
| ProtocolError
|
|
160
246
|
| SqliteError
|
|
161
|
-
|
|
|
247
|
+
| SymmetricCryptoDecryptError
|
|
162
248
|
| TimestampError
|
|
163
|
-
|
|
|
164
|
-
| SymmetricCryptoDecryptError;
|
|
249
|
+
| TransferableError;
|
|
165
250
|
}
|
|
166
251
|
| {
|
|
167
|
-
readonly type: "
|
|
252
|
+
readonly type: "onGetAppOwner";
|
|
253
|
+
readonly appOwner: AppOwner;
|
|
254
|
+
}
|
|
255
|
+
| {
|
|
256
|
+
readonly type: "onQueryPatches";
|
|
168
257
|
readonly tabId: Id;
|
|
169
|
-
readonly
|
|
258
|
+
readonly queryPatches: ReadonlyArray<QueryPatches>;
|
|
170
259
|
readonly onCompleteIds: ReadonlyArray<CallbackId>;
|
|
171
260
|
}
|
|
172
261
|
| {
|
|
173
|
-
readonly type: "
|
|
262
|
+
readonly type: "refreshQueries";
|
|
174
263
|
readonly tabId?: Id;
|
|
175
264
|
}
|
|
176
265
|
| {
|
|
@@ -184,617 +273,196 @@ export type DbWorkerOutput =
|
|
|
184
273
|
readonly file: Uint8Array;
|
|
185
274
|
};
|
|
186
275
|
|
|
187
|
-
export type DbWorkerPlatformDeps =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
276
|
+
export type DbWorkerPlatformDeps = ConsoleDep &
|
|
277
|
+
CreateSqliteDriverDep &
|
|
278
|
+
CreateWebSocketDep &
|
|
279
|
+
RandomBytesDep &
|
|
191
280
|
RandomDep &
|
|
192
|
-
|
|
193
|
-
CreateMnemonicDep &
|
|
194
|
-
CreateRandomBytesDep;
|
|
281
|
+
TimeDep;
|
|
195
282
|
|
|
196
283
|
type DbWorkerDeps = Omit<
|
|
197
284
|
DbWorkerPlatformDeps,
|
|
198
|
-
keyof CreateSqliteDriverDep | keyof
|
|
285
|
+
keyof CreateSqliteDriverDep | keyof CreateWebSocketDep
|
|
199
286
|
> &
|
|
287
|
+
GetQueryRowsCacheDep &
|
|
288
|
+
PostMessageDep &
|
|
200
289
|
SqliteDep &
|
|
201
290
|
SyncDep &
|
|
202
|
-
|
|
203
|
-
SymmetricCryptoDep &
|
|
204
|
-
PostMessageDep &
|
|
205
|
-
OwnersDep &
|
|
206
|
-
ClockDep &
|
|
207
|
-
GetQueryRowsCacheDep &
|
|
208
|
-
ClientStorageDep;
|
|
209
|
-
|
|
210
|
-
type PostMessageDep = WorkerPostMessageDep<DbWorkerOutput>;
|
|
211
|
-
|
|
212
|
-
interface OwnersDep {
|
|
213
|
-
readonly owners: Owners;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
type Owners = Map<OwnerId, AppOwner>;
|
|
217
|
-
|
|
218
|
-
interface ClockDep {
|
|
219
|
-
readonly clock: Clock;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
interface Clock {
|
|
223
|
-
readonly get: () => Timestamp;
|
|
224
|
-
readonly save: (timestamp: Timestamp) => Result<void, SqliteError>;
|
|
225
|
-
}
|
|
291
|
+
AppOwnerDep;
|
|
226
292
|
|
|
227
293
|
interface GetQueryRowsCacheDep {
|
|
228
294
|
readonly getQueryRowsCache: (tabId: Id) => QueryRowsCache;
|
|
229
295
|
}
|
|
230
296
|
|
|
231
|
-
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
let currentTimestamp = initialTimestamp;
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
get: () => currentTimestamp,
|
|
238
|
-
save: (timestamp) => {
|
|
239
|
-
currentTimestamp = timestamp;
|
|
240
|
-
|
|
241
|
-
const timestampString = timestampToTimestampString(timestamp);
|
|
242
|
-
const saveTimestamp = deps.sqlite.exec(sql.prepared`
|
|
243
|
-
update evolu_config set "clock" = ${timestampString};
|
|
244
|
-
`);
|
|
245
|
-
if (!saveTimestamp.ok) return saveTimestamp;
|
|
297
|
+
export interface PostMessageDep {
|
|
298
|
+
readonly postMessage: (message: DbWorkerOutput) => void;
|
|
299
|
+
}
|
|
246
300
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
301
|
+
export interface AppOwnerDep {
|
|
302
|
+
readonly appOwner: AppOwner;
|
|
303
|
+
}
|
|
251
304
|
|
|
252
305
|
export const createDbWorkerForPlatform = (
|
|
253
306
|
platformDeps: DbWorkerPlatformDeps,
|
|
254
|
-
): DbWorker =>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
307
|
+
): DbWorker =>
|
|
308
|
+
createInitializedWorkerWithHandlers<
|
|
309
|
+
DbWorkerInput,
|
|
310
|
+
DbWorkerOutput,
|
|
311
|
+
DbWorkerDeps
|
|
312
|
+
>({ init: createDbWorkerDeps(platformDeps), handlers });
|
|
313
|
+
|
|
314
|
+
const createDbWorkerDeps =
|
|
315
|
+
(platformDeps: DbWorkerPlatformDeps) =>
|
|
316
|
+
async (
|
|
317
|
+
initMessage: Extract<DbWorkerInput, { type: "init" }>,
|
|
318
|
+
postMessage: (msg: DbWorkerOutput) => void,
|
|
319
|
+
): Promise<DbWorkerDeps | null> => {
|
|
320
|
+
platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
|
|
321
|
+
|
|
322
|
+
const sqliteResult = await createSqlite(platformDeps)(
|
|
323
|
+
initMessage.config.name,
|
|
324
|
+
{
|
|
325
|
+
memory: initMessage.config.inMemory ?? false,
|
|
326
|
+
encryptionKey: initMessage.config.encryptionKey ?? undefined,
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
if (!sqliteResult.ok) {
|
|
330
|
+
postMessage({ type: "onError", error: sqliteResult.error });
|
|
331
|
+
return null;
|
|
261
332
|
}
|
|
262
|
-
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
return createInitializedWorker<DbWorkerInput, DbWorkerOutput, DbWorkerDeps>({
|
|
266
|
-
init: async (initMessage, postMessage, safeHandler) => {
|
|
267
|
-
platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
|
|
268
|
-
|
|
269
|
-
const sqliteResult = await createSqlite(platformDeps)(
|
|
270
|
-
initMessage.config.name,
|
|
271
|
-
{ memory: initMessage.config.inMemory ?? false },
|
|
272
|
-
);
|
|
273
|
-
if (!sqliteResult.ok) {
|
|
274
|
-
postMessage({ type: "onError", error: sqliteResult.error });
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const sqlite = sqliteResult.value;
|
|
279
|
-
const platformDepsWithSqlite = { ...platformDeps, sqlite };
|
|
333
|
+
const sqlite = sqliteResult.value;
|
|
280
334
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
335
|
+
const deps = sqlite.transaction(() => {
|
|
336
|
+
const currentDbSchema = getDbSchema({ sqlite })();
|
|
337
|
+
if (!currentDbSchema.ok) return currentDbSchema;
|
|
284
338
|
|
|
285
|
-
|
|
286
|
-
|
|
339
|
+
let appOwner: AppOwner;
|
|
340
|
+
let clock: Clock;
|
|
287
341
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
342
|
+
const dbIsInitialized = currentDbSchema.value.tables.some(
|
|
343
|
+
(table) => table.name === "evolu_version",
|
|
344
|
+
);
|
|
291
345
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
clock,
|
|
317
|
-
appOwnerId,
|
|
318
|
-
appOwnerEncryptionKey,
|
|
319
|
-
appOwnerWriteKey,
|
|
320
|
-
appOwnerMnemonic
|
|
321
|
-
from evolu_config
|
|
322
|
-
limit 1;
|
|
323
|
-
`);
|
|
324
|
-
if (!configResult.ok) return configResult;
|
|
325
|
-
|
|
326
|
-
const [config] = configResult.value.rows;
|
|
327
|
-
|
|
328
|
-
appOwner = {
|
|
329
|
-
type: "AppOwner",
|
|
330
|
-
id: config.appOwnerId,
|
|
331
|
-
encryptionKey: config.appOwnerEncryptionKey,
|
|
332
|
-
writeKey: config.appOwnerWriteKey,
|
|
333
|
-
mnemonic: config.appOwnerMnemonic,
|
|
334
|
-
};
|
|
335
|
-
clock = createClock(platformDepsWithSqlite)(
|
|
336
|
-
timestampStringToTimestamp(config.clock),
|
|
337
|
-
);
|
|
338
|
-
} else {
|
|
339
|
-
appOwner =
|
|
340
|
-
initMessage.config.initialAppOwner ??
|
|
341
|
-
createAppOwner(platformDeps.createMnemonic());
|
|
342
|
-
clock = createClock(platformDepsWithSqlite)();
|
|
343
|
-
const initializeDbResult = initializeDb(platformDepsWithSqlite)(
|
|
344
|
-
appOwner,
|
|
346
|
+
if (dbIsInitialized) {
|
|
347
|
+
const currentVersion = sqlite.exec<{
|
|
348
|
+
protocolVersion: number;
|
|
349
|
+
}>(sql`select protocolVersion from evolu_version limit 1;`);
|
|
350
|
+
if (!currentVersion.ok) return currentVersion;
|
|
351
|
+
|
|
352
|
+
// TODO: Handle version migrations here if needed
|
|
353
|
+
// const [{ protocolVersion }] = protocolVersionResult.value.rows;
|
|
354
|
+
// if (protocolVersion < currentProtocolVersion) {
|
|
355
|
+
// const migrateResult = migrateDatabase({ sqlite })(
|
|
356
|
+
// protocolVersion,
|
|
357
|
+
// currentProtocolVersion
|
|
358
|
+
// );
|
|
359
|
+
// if (!migrateResult.ok) return migrateResult;
|
|
360
|
+
// }
|
|
361
|
+
|
|
362
|
+
const configResult = sqlite.exec<{
|
|
363
|
+
clock: TimestampString;
|
|
364
|
+
appOwnerId: OwnerId;
|
|
365
|
+
appOwnerEncryptionKey: OwnerEncryptionKey;
|
|
366
|
+
appOwnerWriteKey: OwnerWriteKey;
|
|
367
|
+
appOwnerMnemonic: Mnemonic | null;
|
|
368
|
+
}>(sql`
|
|
369
|
+
select
|
|
345
370
|
clock,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
355
|
-
|
|
356
|
-
const owners: Owners = new Map();
|
|
357
|
-
owners.set(appOwner.id, appOwner);
|
|
358
|
-
|
|
359
|
-
const depsWithoutSyncAndStorage = {
|
|
360
|
-
...platformDeps,
|
|
361
|
-
postMessage,
|
|
362
|
-
sqlite,
|
|
363
|
-
timestampConfig: initMessage.config,
|
|
364
|
-
symmetricCrypto: createSymmetricCrypto(platformDeps),
|
|
365
|
-
getQueryRowsCache,
|
|
366
|
-
clock,
|
|
367
|
-
owners,
|
|
368
|
-
};
|
|
371
|
+
appOwnerId,
|
|
372
|
+
appOwnerEncryptionKey,
|
|
373
|
+
appOwnerWriteKey,
|
|
374
|
+
appOwnerMnemonic
|
|
375
|
+
from evolu_config
|
|
376
|
+
limit 1;
|
|
377
|
+
`);
|
|
378
|
+
if (!configResult.ok) return configResult;
|
|
369
379
|
|
|
370
|
-
const
|
|
371
|
-
onStorageError: (error) => {
|
|
372
|
-
postMessage({ type: "onError", error });
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
if (!storage.ok) return storage;
|
|
380
|
+
const [config] = configResult.value.rows;
|
|
376
381
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
382
|
+
appOwner = {
|
|
383
|
+
type: "AppOwner",
|
|
384
|
+
id: config.appOwnerId,
|
|
385
|
+
encryptionKey: config.appOwnerEncryptionKey,
|
|
386
|
+
writeKey: config.appOwnerWriteKey,
|
|
387
|
+
mnemonic: config.appOwnerMnemonic,
|
|
380
388
|
};
|
|
381
389
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
...initMessage.config,
|
|
390
|
-
onOpen: safeHandler(handleSyncOpen(depsWithoutSync)),
|
|
391
|
-
onMessage: safeHandler(createHandleSyncMessage(depsWithoutSync)),
|
|
392
|
-
});
|
|
390
|
+
clock = createClock({ ...platformDeps, sqlite })(
|
|
391
|
+
timestampStringToTimestamp(config.clock),
|
|
392
|
+
);
|
|
393
|
+
} else {
|
|
394
|
+
appOwner =
|
|
395
|
+
initMessage.config.externalAppOwner ??
|
|
396
|
+
createAppOwner(createOwnerSecret(platformDeps));
|
|
393
397
|
|
|
394
|
-
|
|
395
|
-
});
|
|
398
|
+
clock = createClock({ ...platformDeps, sqlite })();
|
|
396
399
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return null;
|
|
400
|
+
const result = initializeDb({ sqlite })(appOwner, clock.get());
|
|
401
|
+
if (!result.ok) return result;
|
|
400
402
|
}
|
|
401
403
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
case "mutate": {
|
|
408
|
-
const mutate = deps.sqlite.transaction(() => {
|
|
409
|
-
const toSyncChanges = [];
|
|
410
|
-
const localOnlyChanges = [];
|
|
411
|
-
|
|
412
|
-
for (const change of message.changes) {
|
|
413
|
-
// Table name starting with '_' is local-only (not synced).
|
|
414
|
-
if (change.table.startsWith("_")) localOnlyChanges.push(change);
|
|
415
|
-
else toSyncChanges.push(change);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
for (const change of localOnlyChanges) {
|
|
419
|
-
if (
|
|
420
|
-
"isDeleted" in change.values &&
|
|
421
|
-
change.values.isDeleted === 1
|
|
422
|
-
) {
|
|
423
|
-
const result = deps.sqlite.exec(sql`
|
|
424
|
-
delete from ${sql.identifier(change.table)}
|
|
425
|
-
where id = ${change.id};
|
|
426
|
-
`);
|
|
427
|
-
if (!result.ok) return result;
|
|
428
|
-
} else {
|
|
429
|
-
const date = new Date(deps.time.now()).toISOString();
|
|
430
|
-
for (const [column, value] of objectToEntries(change.values)) {
|
|
431
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
432
|
-
insert into ${sql.identifier(change.table)}
|
|
433
|
-
("id", ${sql.identifier(column)}, createdAt, updatedAt)
|
|
434
|
-
values (${change.id}, ${value}, ${date}, ${date})
|
|
435
|
-
on conflict ("id") do update
|
|
436
|
-
set
|
|
437
|
-
${sql.identifier(column)} = ${value},
|
|
438
|
-
updatedAt = ${date};
|
|
439
|
-
`);
|
|
440
|
-
if (!result.ok) return result;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* We don't have to wait for the transaction to end because changes
|
|
447
|
-
* are idempotent, so there is no reason why they should fail. We
|
|
448
|
-
* want to call onChange ASAP.
|
|
449
|
-
*/
|
|
450
|
-
const onChange = () => {
|
|
451
|
-
deps.console.log("[db]", "onChange", {
|
|
452
|
-
subscribedQueries: message.subscribedQueries,
|
|
453
|
-
});
|
|
454
|
-
const patches = loadQueries(deps)(
|
|
455
|
-
message.tabId,
|
|
456
|
-
message.subscribedQueries,
|
|
457
|
-
);
|
|
458
|
-
if (!patches.ok) {
|
|
459
|
-
deps.postMessage({ type: "onError", error: patches.error });
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
// Notify the tab that performed the mutation.
|
|
463
|
-
deps.postMessage({
|
|
464
|
-
type: "onChange",
|
|
465
|
-
tabId: message.tabId,
|
|
466
|
-
patches: patches.value,
|
|
467
|
-
onCompleteIds: message.onCompleteIds,
|
|
468
|
-
});
|
|
469
|
-
// Notify other tabs to refresh their queries.
|
|
470
|
-
deps.postMessage({ type: "onReceive", tabId: message.tabId });
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
if (!isNonEmptyArray(toSyncChanges)) {
|
|
474
|
-
onChange();
|
|
475
|
-
return ok();
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const messages = applyChanges(deps)(toSyncChanges, onChange);
|
|
479
|
-
if (!messages.ok) return messages;
|
|
480
|
-
|
|
481
|
-
// TODO: Use owner from db change or AppOwner
|
|
482
|
-
const owner = Array.from(deps.owners.values())[0];
|
|
483
|
-
|
|
484
|
-
const protocolMessage = createProtocolMessageFromCrdtMessages(deps)(
|
|
485
|
-
owner,
|
|
486
|
-
messages.value,
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
deps.console.log(
|
|
490
|
-
"[db]",
|
|
491
|
-
"send data message",
|
|
492
|
-
messages.value,
|
|
493
|
-
protocolMessage,
|
|
494
|
-
);
|
|
495
|
-
deps.sync.send(protocolMessage);
|
|
496
|
-
|
|
497
|
-
return ok();
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
if (!mutate.ok) {
|
|
501
|
-
deps.postMessage({ type: "onError", error: mutate.error });
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
case "query": {
|
|
509
|
-
const patches = loadQueries(deps)(message.tabId, message.queries);
|
|
510
|
-
|
|
511
|
-
if (!patches.ok) {
|
|
512
|
-
deps.postMessage({ type: "onError", error: patches.error });
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
deps.postMessage({
|
|
517
|
-
type: "onChange",
|
|
518
|
-
tabId: message.tabId,
|
|
519
|
-
patches: patches.value,
|
|
520
|
-
onCompleteIds: [],
|
|
521
|
-
});
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
case "reset": {
|
|
526
|
-
const resetResult = deps.sqlite.transaction(() => {
|
|
527
|
-
const dropAllTablesResult = dropAllTables(deps);
|
|
528
|
-
if (!dropAllTablesResult.ok) return dropAllTablesResult;
|
|
529
|
-
|
|
530
|
-
if (message.restore) {
|
|
531
|
-
const dbSchema = getDbSchema(deps)();
|
|
532
|
-
if (!dbSchema.ok) return dbSchema;
|
|
533
|
-
|
|
534
|
-
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
535
|
-
message.restore.dbSchema,
|
|
536
|
-
dbSchema.value,
|
|
537
|
-
);
|
|
538
|
-
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
539
|
-
|
|
540
|
-
const appOwner = createAppOwner(message.restore.mnemonic);
|
|
541
|
-
const clock = createClock(deps)();
|
|
542
|
-
|
|
543
|
-
const initializeDbResult = initializeDb(deps)(appOwner, clock);
|
|
544
|
-
if (!initializeDbResult.ok) return initializeDbResult;
|
|
545
|
-
}
|
|
546
|
-
return ok();
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
if (!resetResult.ok) {
|
|
550
|
-
deps.postMessage({ type: "onError", error: resetResult.error });
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
deps.postMessage({
|
|
555
|
-
type: "onReset",
|
|
556
|
-
onCompleteId: message.onCompleteId,
|
|
557
|
-
reload: message.reload,
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
break;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
case "ensureDbSchema": {
|
|
564
|
-
const ensureSchema = deps.sqlite.transaction(() => {
|
|
565
|
-
const dbSchema = getDbSchema(deps)();
|
|
566
|
-
if (!dbSchema.ok) return dbSchema;
|
|
567
|
-
|
|
568
|
-
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
569
|
-
message.dbSchema,
|
|
570
|
-
dbSchema.value,
|
|
571
|
-
);
|
|
572
|
-
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
573
|
-
|
|
574
|
-
return ok();
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
if (!ensureSchema.ok) {
|
|
578
|
-
deps.postMessage({ type: "onError", error: ensureSchema.error });
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
404
|
+
const result = ensureDbSchema({ sqlite })(
|
|
405
|
+
initMessage.dbSchema,
|
|
406
|
+
currentDbSchema.value,
|
|
407
|
+
);
|
|
408
|
+
if (!result.ok) return result;
|
|
583
409
|
|
|
584
|
-
|
|
585
|
-
|
|
410
|
+
const sync = createSync({
|
|
411
|
+
...platformDeps,
|
|
412
|
+
clock,
|
|
413
|
+
sqlite,
|
|
414
|
+
symmetricCrypto: createSymmetricCrypto(platformDeps),
|
|
415
|
+
timestampConfig: initMessage.config,
|
|
416
|
+
postMessage,
|
|
417
|
+
})({
|
|
418
|
+
appOwner,
|
|
419
|
+
transports: initMessage.config.transports,
|
|
420
|
+
onError: (error) => {
|
|
421
|
+
postMessage({ type: "onError", error });
|
|
422
|
+
},
|
|
423
|
+
onReceive: () => {
|
|
424
|
+
postMessage({ type: "refreshQueries" });
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
if (!sync.ok) return sync;
|
|
586
428
|
|
|
587
|
-
|
|
588
|
-
deps.postMessage({ type: "onError", error: file.error });
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
429
|
+
sync.value.useOwner(true, appOwner);
|
|
591
430
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
431
|
+
const tabQueryRowsCacheMap = new Map<Id, QueryRowsCache>();
|
|
432
|
+
const getQueryRowsCache = (tabId: Id) => {
|
|
433
|
+
let cache = tabQueryRowsCacheMap.get(tabId);
|
|
434
|
+
if (!cache) {
|
|
435
|
+
cache = createQueryRowsCache();
|
|
436
|
+
tabQueryRowsCacheMap.set(tabId, cache);
|
|
598
437
|
}
|
|
599
|
-
|
|
600
|
-
default:
|
|
601
|
-
exhaustiveCheck(message);
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
});
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Get the current database schema by reading SQLite metadata.
|
|
609
|
-
*
|
|
610
|
-
* TODO: Refactor out Evolu stuff and move it to Sqlite.
|
|
611
|
-
*/
|
|
612
|
-
export const getDbSchema =
|
|
613
|
-
(deps: SqliteDep) =>
|
|
614
|
-
({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
|
|
615
|
-
DbSchema,
|
|
616
|
-
SqliteError
|
|
617
|
-
> => {
|
|
618
|
-
const map = new Map<Base64Url256, Array<Base64Url256>>();
|
|
619
|
-
|
|
620
|
-
const tableAndColumnInfoRows = deps.sqlite.exec(sql`
|
|
621
|
-
select
|
|
622
|
-
sqlite_master.name as tableName,
|
|
623
|
-
table_info.name as columnName
|
|
624
|
-
from
|
|
625
|
-
sqlite_master
|
|
626
|
-
join pragma_table_info(sqlite_master.name) as table_info;
|
|
627
|
-
`);
|
|
628
|
-
|
|
629
|
-
if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
|
|
630
|
-
|
|
631
|
-
tableAndColumnInfoRows.value.rows.forEach((row) => {
|
|
632
|
-
const { tableName, columnName } = row as unknown as {
|
|
633
|
-
tableName: Base64Url256;
|
|
634
|
-
columnName: Base64Url256;
|
|
438
|
+
return cache;
|
|
635
439
|
};
|
|
636
|
-
if (!map.has(tableName)) map.set(tableName, []);
|
|
637
|
-
map.get(tableName)?.push(columnName);
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
|
|
641
|
-
|
|
642
|
-
const indexesRows = deps.sqlite.exec(
|
|
643
|
-
allIndexes
|
|
644
|
-
? sql`
|
|
645
|
-
select name, sql
|
|
646
|
-
from sqlite_master
|
|
647
|
-
where type = 'index' and name not like 'sqlite_%';
|
|
648
|
-
`
|
|
649
|
-
: sql`
|
|
650
|
-
select name, sql
|
|
651
|
-
from sqlite_master
|
|
652
|
-
where
|
|
653
|
-
type = 'index'
|
|
654
|
-
and name not like 'sqlite_%'
|
|
655
|
-
and name not like 'evolu_%';
|
|
656
|
-
`,
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
if (!indexesRows.ok) return indexesRows;
|
|
660
|
-
|
|
661
|
-
const indexes = indexesRows.value.rows.map(
|
|
662
|
-
(row): DbIndex => ({
|
|
663
|
-
name: row.name as string,
|
|
664
|
-
/**
|
|
665
|
-
* SQLite returns "CREATE INDEX" for "create index" for some reason.
|
|
666
|
-
* Other keywords remain unchanged. We have to normalize the casing for
|
|
667
|
-
* {@link indexesAreEqual} manually.
|
|
668
|
-
*/
|
|
669
|
-
sql: (row.sql as string)
|
|
670
|
-
.replace("CREATE INDEX", "create index")
|
|
671
|
-
.replace("CREATE UNIQUE INDEX", "create unique index"),
|
|
672
|
-
}),
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
return ok({ tables, indexes });
|
|
676
|
-
};
|
|
677
440
|
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}>;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// TODO: Move to test helpers.
|
|
690
|
-
export const getDbSnapshot = (deps: SqliteDep): DbSnapshot => {
|
|
691
|
-
const schema = getDbSchema(deps)({ allIndexes: true });
|
|
692
|
-
assert(schema.ok, "bug");
|
|
693
|
-
|
|
694
|
-
const tables = [];
|
|
695
|
-
|
|
696
|
-
for (const table of schema.value.tables) {
|
|
697
|
-
const result = deps.sqlite.exec(sql`
|
|
698
|
-
select * from ${sql.identifier(table.name)};
|
|
699
|
-
`);
|
|
700
|
-
assert(result.ok, "bug");
|
|
441
|
+
const deps: DbWorkerDeps = {
|
|
442
|
+
...platformDeps,
|
|
443
|
+
getQueryRowsCache,
|
|
444
|
+
postMessage,
|
|
445
|
+
sqlite,
|
|
446
|
+
sync: sync.value,
|
|
447
|
+
appOwner,
|
|
448
|
+
};
|
|
701
449
|
|
|
702
|
-
|
|
703
|
-
name: table.name,
|
|
704
|
-
rows: result.value.rows,
|
|
450
|
+
return ok(deps);
|
|
705
451
|
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return { schema: schema.value, tables };
|
|
709
|
-
};
|
|
710
452
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
newSchema: DbSchema,
|
|
715
|
-
currentSchema: DbSchema,
|
|
716
|
-
options?: { ignoreIndexes: boolean },
|
|
717
|
-
): Result<void, SqliteError> => {
|
|
718
|
-
const queries: Array<SqliteQuery> = [];
|
|
719
|
-
|
|
720
|
-
newSchema.tables.forEach((newTable) => {
|
|
721
|
-
const currentTable = currentSchema.tables.find(
|
|
722
|
-
(t) => t.name === newTable.name,
|
|
723
|
-
);
|
|
724
|
-
if (!currentTable) {
|
|
725
|
-
queries.push({
|
|
726
|
-
sql: createAppTable(newTable.name, newTable.columns),
|
|
727
|
-
parameters: [],
|
|
728
|
-
});
|
|
729
|
-
} else {
|
|
730
|
-
newTable.columns
|
|
731
|
-
.filter((newColumn) => !currentTable.columns.includes(newColumn))
|
|
732
|
-
.forEach((newColumn) => {
|
|
733
|
-
queries.push(sql`
|
|
734
|
-
alter table ${sql.identifier(newTable.name)}
|
|
735
|
-
add column ${sql.identifier(newColumn)} blob;
|
|
736
|
-
`);
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
if (options?.ignoreIndexes !== true) {
|
|
742
|
-
// Remove current indexes that are not in the newSchema.
|
|
743
|
-
currentSchema.indexes
|
|
744
|
-
.filter(
|
|
745
|
-
(currentIndex) =>
|
|
746
|
-
!newSchema.indexes.some((newIndex) =>
|
|
747
|
-
indexesAreEqual(newIndex, currentIndex),
|
|
748
|
-
),
|
|
749
|
-
)
|
|
750
|
-
.forEach((index) => {
|
|
751
|
-
queries.push(sql`drop index ${sql.identifier(index.name)};`);
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
// Add new indexes that are not in the currentSchema.
|
|
755
|
-
newSchema.indexes
|
|
756
|
-
.filter(
|
|
757
|
-
(newIndex) =>
|
|
758
|
-
!currentSchema.indexes.some((currentIndex) =>
|
|
759
|
-
indexesAreEqual(newIndex, currentIndex),
|
|
760
|
-
),
|
|
761
|
-
)
|
|
762
|
-
.forEach((newIndex) => {
|
|
763
|
-
queries.push({ sql: `${newIndex.sql};` as SafeSql, parameters: [] });
|
|
764
|
-
});
|
|
453
|
+
if (!deps.ok) {
|
|
454
|
+
postMessage({ type: "onError", error: deps.error });
|
|
455
|
+
return null;
|
|
765
456
|
}
|
|
766
457
|
|
|
767
|
-
|
|
768
|
-
const result = deps.sqlite.exec(query);
|
|
769
|
-
if (!result.ok) return result;
|
|
770
|
-
}
|
|
771
|
-
return ok();
|
|
458
|
+
return deps.value;
|
|
772
459
|
};
|
|
773
460
|
|
|
774
|
-
export const createAppTable = (
|
|
775
|
-
tableName: string,
|
|
776
|
-
columns: ReadonlyArray<string>,
|
|
777
|
-
): SafeSql =>
|
|
778
|
-
`
|
|
779
|
-
create table ${sql.identifier(tableName).sql} (
|
|
780
|
-
"id" text primary key,
|
|
781
|
-
${columns
|
|
782
|
-
// Add default columns.
|
|
783
|
-
.concat(["createdAt", "updatedAt", "isDeleted"])
|
|
784
|
-
.filter((c) => c !== "id")
|
|
785
|
-
// "A column with affinity BLOB does not prefer one storage class over another
|
|
786
|
-
// and no attempt is made to coerce data from one storage class into another."
|
|
787
|
-
// https://www.sqlite.org/datatype3.html
|
|
788
|
-
.map((name) => `${sql.identifier(name).sql} blob`)
|
|
789
|
-
.join(", ")}
|
|
790
|
-
);
|
|
791
|
-
` as SafeSql;
|
|
792
|
-
|
|
793
461
|
const initializeDb =
|
|
794
|
-
(deps: SqliteDep
|
|
462
|
+
(deps: SqliteDep) =>
|
|
795
463
|
(
|
|
796
464
|
initialAppOwner: AppOwner,
|
|
797
|
-
initialClock:
|
|
465
|
+
initialClock: Timestamp,
|
|
798
466
|
): Result<void, SqliteError> => {
|
|
799
467
|
for (const query of [
|
|
800
468
|
// Never change structure to ensure all versions can read it.
|
|
@@ -832,7 +500,7 @@ const initializeDb =
|
|
|
832
500
|
)
|
|
833
501
|
values
|
|
834
502
|
(
|
|
835
|
-
${timestampToTimestampString(initialClock
|
|
503
|
+
${timestampToTimestampString(initialClock)},
|
|
836
504
|
${initialAppOwner.id},
|
|
837
505
|
${initialAppOwner.encryptionKey},
|
|
838
506
|
${initialAppOwner.writeKey},
|
|
@@ -885,140 +553,166 @@ const initializeDb =
|
|
|
885
553
|
return ok();
|
|
886
554
|
};
|
|
887
555
|
|
|
888
|
-
const
|
|
889
|
-
(
|
|
890
|
-
deps
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
OwnersDep &
|
|
896
|
-
ClockDep,
|
|
897
|
-
) =>
|
|
898
|
-
(
|
|
899
|
-
changes: NonEmptyReadonlyArray<DbChange>,
|
|
900
|
-
onChange?: () => void,
|
|
901
|
-
): Result<
|
|
902
|
-
NonEmptyReadonlyArray<CrdtMessage>,
|
|
903
|
-
| TimestampTimeOutOfRangeError
|
|
904
|
-
| TimestampDriftError
|
|
905
|
-
| TimestampCounterOverflowError
|
|
906
|
-
| SqliteError
|
|
907
|
-
> => {
|
|
908
|
-
let clockTimestamp = deps.clock.get();
|
|
909
|
-
|
|
910
|
-
const messages: Array<CrdtMessage> = [];
|
|
911
|
-
|
|
912
|
-
for (const change of changes) {
|
|
913
|
-
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
914
|
-
if (!nextTimestamp.ok) return nextTimestamp;
|
|
915
|
-
clockTimestamp = nextTimestamp.value;
|
|
916
|
-
messages.push({ timestamp: clockTimestamp, change });
|
|
917
|
-
}
|
|
556
|
+
const handlers: Omit<MessageHandlers<DbWorkerInput, DbWorkerDeps>, "init"> = {
|
|
557
|
+
getAppOwner: (deps) => () => {
|
|
558
|
+
deps.postMessage({
|
|
559
|
+
type: "onGetAppOwner",
|
|
560
|
+
appOwner: deps.appOwner,
|
|
561
|
+
});
|
|
562
|
+
},
|
|
918
563
|
|
|
919
|
-
|
|
920
|
-
|
|
564
|
+
mutate: (deps) => (message) => {
|
|
565
|
+
const mutate = deps.sqlite.transaction(() => {
|
|
566
|
+
const syncChanges: Array<MutationChange> = [];
|
|
921
567
|
|
|
922
|
-
|
|
568
|
+
for (const change of message.changes) {
|
|
569
|
+
const isLocalOnlyChange = change.table.startsWith("_");
|
|
570
|
+
if (isLocalOnlyChange) {
|
|
571
|
+
const result = applyLocalOnlyChange(deps)(change);
|
|
572
|
+
if (!result.ok) return result;
|
|
573
|
+
} else {
|
|
574
|
+
syncChanges.push(change);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
923
577
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
578
|
+
if (isNonEmptyArray(syncChanges)) {
|
|
579
|
+
const result = deps.sync.applyChanges(syncChanges);
|
|
580
|
+
if (!result.ok) return result;
|
|
581
|
+
}
|
|
927
582
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
583
|
+
// Read writes before commit to update UI ASAP
|
|
584
|
+
const queryPatches = loadQueries(deps)(
|
|
585
|
+
message.tabId,
|
|
586
|
+
message.subscribedQueries,
|
|
587
|
+
);
|
|
588
|
+
if (!queryPatches.ok) return queryPatches;
|
|
589
|
+
|
|
590
|
+
// Update the tab that performed the mutation.
|
|
591
|
+
deps.postMessage({
|
|
592
|
+
type: "onQueryPatches",
|
|
593
|
+
tabId: message.tabId,
|
|
594
|
+
queryPatches: queryPatches.value,
|
|
595
|
+
onCompleteIds: message.onCompleteIds,
|
|
596
|
+
});
|
|
937
597
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (!result1.ok) return result1;
|
|
598
|
+
// Notify other tabs to refresh their queries.
|
|
599
|
+
deps.postMessage({ type: "refreshQueries", tabId: message.tabId });
|
|
941
600
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
601
|
+
return ok();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (!mutate.ok) {
|
|
605
|
+
deps.postMessage({ type: "onError", error: mutate.error });
|
|
606
|
+
return;
|
|
947
607
|
}
|
|
608
|
+
},
|
|
948
609
|
|
|
949
|
-
|
|
950
|
-
|
|
610
|
+
query: (deps) => (message) => {
|
|
611
|
+
const queryPatches = loadQueries(deps)(message.tabId, message.queries);
|
|
951
612
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const updatedAt = new Date(message.timestamp.millis).toISOString();
|
|
957
|
-
|
|
958
|
-
for (const [column, value] of objectToEntries(message.change.values)) {
|
|
959
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
960
|
-
with
|
|
961
|
-
lastTimestamp as (
|
|
962
|
-
select "timestamp"
|
|
963
|
-
from evolu_history
|
|
964
|
-
where
|
|
965
|
-
"ownerId" = ${ownerId}
|
|
966
|
-
and "table" = ${message.change.table}
|
|
967
|
-
and "id" = ${message.change.id}
|
|
968
|
-
and "column" = ${column}
|
|
969
|
-
order by "timestamp" desc
|
|
970
|
-
limit 1
|
|
971
|
-
)
|
|
972
|
-
insert into ${sql.identifier(message.change.table)}
|
|
973
|
-
("id", ${sql.identifier(column)}, updatedAt)
|
|
974
|
-
select ${message.change.id}, ${value}, ${updatedAt}
|
|
975
|
-
where
|
|
976
|
-
(select "timestamp" from lastTimestamp) is null
|
|
977
|
-
or (select "timestamp" from lastTimestamp) < ${timestamp}
|
|
978
|
-
on conflict ("id") do update
|
|
979
|
-
set
|
|
980
|
-
${sql.identifier(column)} = ${value},
|
|
981
|
-
updatedAt = ${updatedAt}
|
|
982
|
-
where
|
|
983
|
-
(select "timestamp" from lastTimestamp) is null
|
|
984
|
-
or (select "timestamp" from lastTimestamp) < ${timestamp};
|
|
985
|
-
`);
|
|
613
|
+
if (!queryPatches.ok) {
|
|
614
|
+
deps.postMessage({ type: "onError", error: queryPatches.error });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
986
617
|
|
|
987
|
-
|
|
618
|
+
deps.postMessage({
|
|
619
|
+
type: "onQueryPatches",
|
|
620
|
+
tabId: message.tabId,
|
|
621
|
+
queryPatches: queryPatches.value,
|
|
622
|
+
onCompleteIds: [],
|
|
623
|
+
});
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
reset: (deps) => (message) => {
|
|
627
|
+
const resetResult = deps.sqlite.transaction(() => {
|
|
628
|
+
const dbSchema = getDbSchema(deps)();
|
|
629
|
+
if (!dbSchema.ok) return dbSchema;
|
|
630
|
+
|
|
631
|
+
for (const table of dbSchema.value.tables) {
|
|
632
|
+
/**
|
|
633
|
+
* The dropped table is completely removed from the database schema and
|
|
634
|
+
* the disk file. The table can not be recovered. All indices and
|
|
635
|
+
* triggers associated with the table are also deleted.
|
|
636
|
+
* https://sqlite.org/lang_droptable.html
|
|
637
|
+
*/
|
|
638
|
+
const result = deps.sqlite.exec(sql`
|
|
639
|
+
drop table ${sql.identifier(table.name)};
|
|
640
|
+
`);
|
|
641
|
+
if (!result.ok) return result;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (message.restore) {
|
|
645
|
+
const dbSchema = getDbSchema(deps)();
|
|
646
|
+
if (!dbSchema.ok) return dbSchema;
|
|
647
|
+
|
|
648
|
+
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
649
|
+
message.restore.dbSchema,
|
|
650
|
+
dbSchema.value,
|
|
651
|
+
);
|
|
652
|
+
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
653
|
+
|
|
654
|
+
const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
|
|
655
|
+
const appOwner = createAppOwner(secret);
|
|
656
|
+
const clock = createClock(deps)();
|
|
657
|
+
|
|
658
|
+
const initializeDbResult = initializeDb(deps)(appOwner, clock.get());
|
|
659
|
+
if (!initializeDbResult.ok) return initializeDbResult;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return ok();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (!resetResult.ok) {
|
|
666
|
+
deps.postMessage({ type: "onError", error: resetResult.error });
|
|
667
|
+
return;
|
|
988
668
|
}
|
|
989
669
|
|
|
990
|
-
|
|
991
|
-
|
|
670
|
+
deps.postMessage({
|
|
671
|
+
type: "onReset",
|
|
672
|
+
onCompleteId: message.onCompleteId,
|
|
673
|
+
reload: message.reload,
|
|
674
|
+
});
|
|
675
|
+
},
|
|
992
676
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const id = idToBinaryId(message.change.id);
|
|
677
|
+
ensureDbSchema: (deps) => (message) => {
|
|
678
|
+
const ensureSchema = deps.sqlite.transaction(() => {
|
|
679
|
+
const dbSchema = getDbSchema(deps)();
|
|
680
|
+
if (!dbSchema.ok) return dbSchema;
|
|
998
681
|
|
|
999
|
-
|
|
1000
|
-
|
|
682
|
+
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
683
|
+
message.dbSchema,
|
|
684
|
+
dbSchema.value,
|
|
685
|
+
);
|
|
686
|
+
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
1001
687
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
${ownerId},
|
|
1009
|
-
${message.change.table},
|
|
1010
|
-
${id},
|
|
1011
|
-
${column},
|
|
1012
|
-
${value},
|
|
1013
|
-
${timestamp}
|
|
1014
|
-
)
|
|
1015
|
-
on conflict do nothing;
|
|
1016
|
-
`);
|
|
1017
|
-
if (!result.ok) return result;
|
|
688
|
+
return ok();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (!ensureSchema.ok) {
|
|
692
|
+
deps.postMessage({ type: "onError", error: ensureSchema.error });
|
|
693
|
+
return;
|
|
1018
694
|
}
|
|
695
|
+
},
|
|
1019
696
|
|
|
1020
|
-
|
|
1021
|
-
|
|
697
|
+
export: (deps) => (message) => {
|
|
698
|
+
const file = deps.sqlite.export();
|
|
699
|
+
|
|
700
|
+
if (!file.ok) {
|
|
701
|
+
deps.postMessage({ type: "onError", error: file.error });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
deps.postMessage({
|
|
706
|
+
type: "onExport",
|
|
707
|
+
onCompleteId: message.onCompleteId,
|
|
708
|
+
file: file.value,
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
useOwner: (deps) => (message) => {
|
|
713
|
+
deps.sync.useOwner(message.use, message.owner);
|
|
714
|
+
},
|
|
715
|
+
};
|
|
1022
716
|
|
|
1023
717
|
const loadQueries =
|
|
1024
718
|
(deps: GetQueryRowsCacheDep & SqliteDep) =>
|
|
@@ -1057,189 +751,3 @@ const loadQueries =
|
|
|
1057
751
|
);
|
|
1058
752
|
return ok(queryPatchesArray);
|
|
1059
753
|
};
|
|
1060
|
-
|
|
1061
|
-
const dropAllTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
1062
|
-
const schema = getDbSchema(deps)();
|
|
1063
|
-
if (!schema.ok) return schema;
|
|
1064
|
-
for (const table of schema.value.tables) {
|
|
1065
|
-
/**
|
|
1066
|
-
* The dropped table is completely removed from the database schema and the
|
|
1067
|
-
* disk file. The table can not be recovered. All indices and triggers
|
|
1068
|
-
* associated with the table are also deleted.
|
|
1069
|
-
* https://sqlite.org/lang_droptable.html
|
|
1070
|
-
*/
|
|
1071
|
-
const result = deps.sqlite.exec(sql`
|
|
1072
|
-
drop table ${sql.identifier(table.name)};
|
|
1073
|
-
`);
|
|
1074
|
-
if (!result.ok) return result;
|
|
1075
|
-
}
|
|
1076
|
-
return ok();
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
const handleSyncOpen =
|
|
1080
|
-
(deps: StorageDep & ConsoleDep & OwnersDep): SyncConfig["onOpen"] =>
|
|
1081
|
-
(send) => {
|
|
1082
|
-
for (const [id] of deps.owners) {
|
|
1083
|
-
const message = createProtocolMessageForSync(deps)(id);
|
|
1084
|
-
if (!message) return;
|
|
1085
|
-
deps.console.log("[db]", "send initial sync message", message);
|
|
1086
|
-
send(message);
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
const createHandleSyncMessage =
|
|
1091
|
-
(
|
|
1092
|
-
deps: PostMessageDep & StorageDep & SqliteDep & ConsoleDep & OwnersDep,
|
|
1093
|
-
): SyncConfig["onMessage"] =>
|
|
1094
|
-
(input, send) => {
|
|
1095
|
-
deps.console.log("[db]", "receive sync message", input);
|
|
1096
|
-
|
|
1097
|
-
const output = deps.sqlite.transaction(() =>
|
|
1098
|
-
applyProtocolMessageAsClient(deps)(input, {
|
|
1099
|
-
getWriteKey: (ownerId) => deps.owners.get(ownerId)?.writeKey ?? null,
|
|
1100
|
-
}),
|
|
1101
|
-
);
|
|
1102
|
-
if (!output.ok) {
|
|
1103
|
-
deps.postMessage({ type: "onError", error: output.error });
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
if (output.value) {
|
|
1108
|
-
deps.console.log("[db]", "respond sync message", output.value);
|
|
1109
|
-
send(output.value);
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
export interface ClientStorageDep {
|
|
1114
|
-
readonly storage: ClientStorage;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
export interface ClientStorage extends SqliteStorageBase, Storage {}
|
|
1118
|
-
|
|
1119
|
-
const createClientStorage =
|
|
1120
|
-
(
|
|
1121
|
-
deps: SqliteDep &
|
|
1122
|
-
PostMessageDep &
|
|
1123
|
-
SymmetricCryptoDep &
|
|
1124
|
-
RandomDep &
|
|
1125
|
-
TimeDep &
|
|
1126
|
-
OwnersDep &
|
|
1127
|
-
ClockDep &
|
|
1128
|
-
TimestampConfigDep,
|
|
1129
|
-
) =>
|
|
1130
|
-
(
|
|
1131
|
-
options: CreateSqliteStorageBaseOptions,
|
|
1132
|
-
): Result<ClientStorage, SqliteError> => {
|
|
1133
|
-
const sqliteStorageBase = createSqliteStorageBase(deps)(options);
|
|
1134
|
-
if (!sqliteStorageBase.ok) return sqliteStorageBase;
|
|
1135
|
-
|
|
1136
|
-
const storage: ClientStorage = {
|
|
1137
|
-
...sqliteStorageBase.value,
|
|
1138
|
-
|
|
1139
|
-
validateWriteKey: constFalse,
|
|
1140
|
-
setWriteKey: constFalse,
|
|
1141
|
-
|
|
1142
|
-
writeMessages: (ownerId, messages) => {
|
|
1143
|
-
const owner = deps.owners.get(binaryOwnerIdToOwnerId(ownerId));
|
|
1144
|
-
assert(owner, "Missing owner");
|
|
1145
|
-
|
|
1146
|
-
const decodedAndDecryptedMessages: Array<CrdtMessage> = [];
|
|
1147
|
-
|
|
1148
|
-
for (const message of messages) {
|
|
1149
|
-
const dbChange = decryptAndDecodeDbChange(deps)(
|
|
1150
|
-
message,
|
|
1151
|
-
owner.encryptionKey,
|
|
1152
|
-
);
|
|
1153
|
-
|
|
1154
|
-
if (!dbChange.ok) {
|
|
1155
|
-
deps.postMessage({
|
|
1156
|
-
type: "onError",
|
|
1157
|
-
error: dbChange.error,
|
|
1158
|
-
});
|
|
1159
|
-
return false;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
decodedAndDecryptedMessages.push({
|
|
1163
|
-
timestamp: message.timestamp,
|
|
1164
|
-
change: dbChange.value,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
let clockTimestamp = deps.clock.get();
|
|
1169
|
-
|
|
1170
|
-
for (const message of messages) {
|
|
1171
|
-
const receive = receiveTimestamp(deps)(
|
|
1172
|
-
clockTimestamp,
|
|
1173
|
-
message.timestamp,
|
|
1174
|
-
);
|
|
1175
|
-
if (!receive.ok) {
|
|
1176
|
-
deps.postMessage({ type: "onError", error: receive.error });
|
|
1177
|
-
return false;
|
|
1178
|
-
}
|
|
1179
|
-
clockTimestamp = receive.value;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const applyMessagesResult = applyMessages({ ...deps, storage })(
|
|
1183
|
-
decodedAndDecryptedMessages,
|
|
1184
|
-
clockTimestamp,
|
|
1185
|
-
);
|
|
1186
|
-
|
|
1187
|
-
if (!applyMessagesResult.ok) {
|
|
1188
|
-
deps.postMessage({
|
|
1189
|
-
type: "onError",
|
|
1190
|
-
error: applyMessagesResult.error,
|
|
1191
|
-
});
|
|
1192
|
-
return false;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
deps.postMessage({ type: "onReceive" });
|
|
1196
|
-
|
|
1197
|
-
return true;
|
|
1198
|
-
},
|
|
1199
|
-
|
|
1200
|
-
readDbChange: (ownerId, timestamp) => {
|
|
1201
|
-
const owner = deps.owners.get(binaryOwnerIdToOwnerId(ownerId));
|
|
1202
|
-
assert(owner, "Missing owner");
|
|
1203
|
-
|
|
1204
|
-
const result = deps.sqlite.exec<{
|
|
1205
|
-
table: Base64Url256;
|
|
1206
|
-
id: BinaryId;
|
|
1207
|
-
column: Base64Url256;
|
|
1208
|
-
value: SqliteValue;
|
|
1209
|
-
}>(sql`
|
|
1210
|
-
select "table", "id", "column", "value"
|
|
1211
|
-
from evolu_history
|
|
1212
|
-
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
1213
|
-
`);
|
|
1214
|
-
if (!result.ok) {
|
|
1215
|
-
deps.postMessage({ type: "onError", error: result.error });
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
const { rows } = result.value;
|
|
1220
|
-
assert(rows.length > 0, "Rows must not be empty");
|
|
1221
|
-
|
|
1222
|
-
const { table, id } = rows[0];
|
|
1223
|
-
const values: Record<string, SqliteValue> = {};
|
|
1224
|
-
|
|
1225
|
-
for (const r of rows) {
|
|
1226
|
-
assert(r.table === table, "All rows must have the same table");
|
|
1227
|
-
assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
|
|
1228
|
-
values[r.column] = r.value;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
const message: CrdtMessage = {
|
|
1232
|
-
timestamp: binaryTimestampToTimestamp(timestamp),
|
|
1233
|
-
change: {
|
|
1234
|
-
table: rows[0].table,
|
|
1235
|
-
id: binaryIdToId(rows[0].id),
|
|
1236
|
-
values,
|
|
1237
|
-
},
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
|
|
1241
|
-
},
|
|
1242
|
-
};
|
|
1243
|
-
|
|
1244
|
-
return ok(storage);
|
|
1245
|
-
};
|