@evolu/common 6.0.1-preview.2 → 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 +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +29 -42
- package/dist/src/Evolu/Db.d.ts +137 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +152 -127
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +88 -107
- 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 +170 -186
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +487 -343
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +4 -3
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +50 -12
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- package/dist/src/Evolu/Sync.d.ts +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 +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +147 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +7 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +462 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +490 -436
- package/dist/src/Types.d.ts +0 -74
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +505 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +221 -235
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +681 -614
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +62 -16
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +152 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +696 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Db.ts
CHANGED
|
@@ -1,72 +1,43 @@
|
|
|
1
|
+
import { isNonEmptyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
+
import { CallbackId } from "../CallbackRegistry.js";
|
|
3
|
+
import { ConsoleConfig, ConsoleDep } from "../Console.js";
|
|
1
4
|
import {
|
|
2
|
-
isNonEmptyArray,
|
|
3
|
-
isNonEmptyReadonlyArray,
|
|
4
|
-
NonEmptyReadonlyArray,
|
|
5
|
-
} from "../Array.js";
|
|
6
|
-
import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
7
|
-
import { CallbackId } from "../Callbacks.js";
|
|
8
|
-
import { ConsoleDep } from "../Console.js";
|
|
9
|
-
import {
|
|
10
|
-
CreateMnemonicDep,
|
|
11
|
-
CreateRandomBytesDep,
|
|
12
5
|
createSymmetricCrypto,
|
|
6
|
+
EncryptionKey,
|
|
7
|
+
RandomBytesDep,
|
|
13
8
|
SymmetricCryptoDecryptError,
|
|
14
|
-
SymmetricCryptoDep,
|
|
15
9
|
} from "../Crypto.js";
|
|
16
|
-
import { eqArrayNumber } from "../Eq.js";
|
|
17
10
|
import { TransferableError } from "../Error.js";
|
|
18
|
-
import { constFalse, exhaustiveCheck } from "../Function.js";
|
|
19
|
-
import { NanoIdLibDep } from "../NanoId.js";
|
|
20
|
-
import { objectToEntries } from "../Object.js";
|
|
21
11
|
import { RandomDep } from "../Random.js";
|
|
22
|
-
import {
|
|
23
|
-
import { err, ok, Result } from "../Result.js";
|
|
12
|
+
import { ok, Result } from "../Result.js";
|
|
24
13
|
import {
|
|
25
14
|
createSqlite,
|
|
26
15
|
CreateSqliteDriverDep,
|
|
27
16
|
explainSqliteQueryPlan,
|
|
28
|
-
SafeSql,
|
|
29
17
|
sql,
|
|
30
18
|
SqliteDep,
|
|
31
19
|
SqliteError,
|
|
32
|
-
SqliteQuery,
|
|
33
|
-
SqliteRow,
|
|
34
|
-
SqliteValue,
|
|
35
20
|
} from "../Sqlite.js";
|
|
36
21
|
import { TimeDep } from "../Time.js";
|
|
37
|
-
import { Id, Mnemonic,
|
|
22
|
+
import { Id, Mnemonic, SimpleName } from "../Type.js";
|
|
23
|
+
import { CreateWebSocketDep } from "../WebSocket.js";
|
|
38
24
|
import {
|
|
39
|
-
|
|
25
|
+
createInitializedWorkerWithHandlers,
|
|
26
|
+
MessageHandlers,
|
|
40
27
|
Worker,
|
|
41
|
-
WorkerPostMessageDep,
|
|
42
28
|
} from "../Worker.js";
|
|
43
|
-
import { Config } from "./Config.js";
|
|
44
29
|
import { makePatches, QueryPatches } from "./Diff.js";
|
|
45
30
|
import {
|
|
46
31
|
AppOwner,
|
|
47
32
|
createAppOwner,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
33
|
+
createOwnerSecret,
|
|
34
|
+
mnemonicToOwnerSecret,
|
|
35
|
+
OwnerEncryptionKey,
|
|
36
|
+
OwnerId,
|
|
37
|
+
OwnerWriteKey,
|
|
38
|
+
TransportConfig,
|
|
51
39
|
} from "./Owner.js";
|
|
52
|
-
import {
|
|
53
|
-
applyProtocolMessageAsClient,
|
|
54
|
-
Base64Url256,
|
|
55
|
-
BinaryId,
|
|
56
|
-
binaryIdToId,
|
|
57
|
-
CrdtMessage,
|
|
58
|
-
createProtocolMessageForSync,
|
|
59
|
-
createProtocolMessageFromCrdtMessages,
|
|
60
|
-
DbChange,
|
|
61
|
-
decryptAndDecodeDbChange,
|
|
62
|
-
encodeAndEncryptDbChange,
|
|
63
|
-
idToBinaryId,
|
|
64
|
-
ownerIdToBinaryOwnerId,
|
|
65
|
-
ProtocolError,
|
|
66
|
-
protocolVersion,
|
|
67
|
-
Storage,
|
|
68
|
-
StorageDep,
|
|
69
|
-
} from "./Protocol.js";
|
|
40
|
+
import { ProtocolError, protocolVersion } from "./Protocol.js";
|
|
70
41
|
import {
|
|
71
42
|
createQueryRowsCache,
|
|
72
43
|
deserializeQuery,
|
|
@@ -75,39 +46,145 @@ import {
|
|
|
75
46
|
QueryRowsCache,
|
|
76
47
|
} from "./Query.js";
|
|
77
48
|
import {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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";
|
|
83
62
|
import {
|
|
84
|
-
Millis,
|
|
85
|
-
receiveTimestamp,
|
|
86
|
-
sendTimestamp,
|
|
87
63
|
Timestamp,
|
|
88
|
-
|
|
89
|
-
TimestampCounterOverflowError,
|
|
90
|
-
TimestampDriftError,
|
|
64
|
+
TimestampConfig,
|
|
91
65
|
TimestampError,
|
|
92
66
|
TimestampString,
|
|
93
67
|
timestampStringToTimestamp,
|
|
94
|
-
TimestampTimeOutOfRangeError,
|
|
95
|
-
timestampToBinaryTimestamp,
|
|
96
68
|
timestampToTimestampString,
|
|
97
69
|
} from "./Timestamp.js";
|
|
98
70
|
|
|
99
|
-
export interface
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
180
|
}
|
|
108
181
|
|
|
109
|
-
export const
|
|
110
|
-
|
|
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
|
+
};
|
|
111
188
|
|
|
112
189
|
export type DbWorker = Worker<DbWorkerInput, DbWorkerOutput>;
|
|
113
190
|
|
|
@@ -120,14 +197,16 @@ export interface CreateDbWorkerDep {
|
|
|
120
197
|
export type DbWorkerInput =
|
|
121
198
|
| {
|
|
122
199
|
readonly type: "init";
|
|
123
|
-
readonly config:
|
|
200
|
+
readonly config: DbConfig;
|
|
124
201
|
readonly dbSchema: DbSchema;
|
|
125
|
-
|
|
202
|
+
}
|
|
203
|
+
| {
|
|
204
|
+
readonly type: "getAppOwner";
|
|
126
205
|
}
|
|
127
206
|
| {
|
|
128
207
|
readonly type: "mutate";
|
|
129
208
|
readonly tabId: Id;
|
|
130
|
-
readonly changes: NonEmptyReadonlyArray<
|
|
209
|
+
readonly changes: NonEmptyReadonlyArray<MutationChange>;
|
|
131
210
|
readonly onCompleteIds: ReadonlyArray<CallbackId>;
|
|
132
211
|
readonly subscribedQueries: ReadonlyArray<Query>;
|
|
133
212
|
}
|
|
@@ -152,30 +231,35 @@ export type DbWorkerInput =
|
|
|
152
231
|
| {
|
|
153
232
|
readonly type: "export";
|
|
154
233
|
readonly onCompleteId: CallbackId;
|
|
234
|
+
}
|
|
235
|
+
| {
|
|
236
|
+
readonly type: "useOwner";
|
|
237
|
+
readonly use: boolean;
|
|
238
|
+
readonly owner: SyncOwner;
|
|
155
239
|
};
|
|
156
240
|
|
|
157
241
|
export type DbWorkerOutput =
|
|
158
|
-
| {
|
|
159
|
-
readonly type: "onInit";
|
|
160
|
-
readonly owner: AppOwner;
|
|
161
|
-
}
|
|
162
242
|
| {
|
|
163
243
|
readonly type: "onError";
|
|
164
244
|
readonly error:
|
|
245
|
+
| ProtocolError
|
|
165
246
|
| SqliteError
|
|
166
|
-
|
|
|
247
|
+
| SymmetricCryptoDecryptError
|
|
167
248
|
| TimestampError
|
|
168
|
-
|
|
|
169
|
-
|
|
249
|
+
| TransferableError;
|
|
250
|
+
}
|
|
251
|
+
| {
|
|
252
|
+
readonly type: "onGetAppOwner";
|
|
253
|
+
readonly appOwner: AppOwner;
|
|
170
254
|
}
|
|
171
255
|
| {
|
|
172
|
-
readonly type: "
|
|
256
|
+
readonly type: "onQueryPatches";
|
|
173
257
|
readonly tabId: Id;
|
|
174
|
-
readonly
|
|
258
|
+
readonly queryPatches: ReadonlyArray<QueryPatches>;
|
|
175
259
|
readonly onCompleteIds: ReadonlyArray<CallbackId>;
|
|
176
260
|
}
|
|
177
261
|
| {
|
|
178
|
-
readonly type: "
|
|
262
|
+
readonly type: "refreshQueries";
|
|
179
263
|
readonly tabId?: Id;
|
|
180
264
|
}
|
|
181
265
|
| {
|
|
@@ -189,812 +273,446 @@ export type DbWorkerOutput =
|
|
|
189
273
|
readonly file: Uint8Array;
|
|
190
274
|
};
|
|
191
275
|
|
|
192
|
-
export type DbWorkerPlatformDeps =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
276
|
+
export type DbWorkerPlatformDeps = ConsoleDep &
|
|
277
|
+
CreateSqliteDriverDep &
|
|
278
|
+
CreateWebSocketDep &
|
|
279
|
+
RandomBytesDep &
|
|
196
280
|
RandomDep &
|
|
197
|
-
|
|
198
|
-
CreateMnemonicDep &
|
|
199
|
-
CreateRandomBytesDep;
|
|
281
|
+
TimeDep;
|
|
200
282
|
|
|
201
283
|
type DbWorkerDeps = Omit<
|
|
202
284
|
DbWorkerPlatformDeps,
|
|
203
|
-
keyof CreateSqliteDriverDep | keyof
|
|
285
|
+
keyof CreateSqliteDriverDep | keyof CreateWebSocketDep
|
|
204
286
|
> &
|
|
287
|
+
GetQueryRowsCacheDep &
|
|
288
|
+
PostMessageDep &
|
|
205
289
|
SqliteDep &
|
|
206
290
|
SyncDep &
|
|
207
|
-
|
|
208
|
-
SymmetricCryptoDep &
|
|
209
|
-
PostMessageDep &
|
|
210
|
-
OwnerRowRefDep &
|
|
211
|
-
GetQueryRowsCacheDep &
|
|
212
|
-
ClientStorageDep;
|
|
291
|
+
AppOwnerDep;
|
|
213
292
|
|
|
214
|
-
|
|
293
|
+
interface GetQueryRowsCacheDep {
|
|
294
|
+
readonly getQueryRowsCache: (tabId: Id) => QueryRowsCache;
|
|
295
|
+
}
|
|
215
296
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
readonly ownerRowRef: Ref<OwnerRow>;
|
|
297
|
+
export interface PostMessageDep {
|
|
298
|
+
readonly postMessage: (message: DbWorkerOutput) => void;
|
|
219
299
|
}
|
|
220
300
|
|
|
221
|
-
interface
|
|
222
|
-
readonly
|
|
301
|
+
export interface AppOwnerDep {
|
|
302
|
+
readonly appOwner: AppOwner;
|
|
223
303
|
}
|
|
224
304
|
|
|
225
305
|
export const createDbWorkerForPlatform = (
|
|
226
306
|
platformDeps: DbWorkerPlatformDeps,
|
|
227
|
-
): DbWorker =>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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;
|
|
234
332
|
}
|
|
235
|
-
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return createInitializedWorker<DbWorkerInput, DbWorkerOutput, DbWorkerDeps>({
|
|
239
|
-
init: async (initMessage, postMessage, safeHandler) => {
|
|
240
|
-
platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
|
|
333
|
+
const sqlite = sqliteResult.value;
|
|
241
334
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
if (!sqliteResult.ok) {
|
|
247
|
-
postMessage({ type: "onError", error: sqliteResult.error });
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
const sqlite = sqliteResult.value;
|
|
335
|
+
const deps = sqlite.transaction(() => {
|
|
336
|
+
const currentDbSchema = getDbSchema({ sqlite })();
|
|
337
|
+
if (!currentDbSchema.ok) return currentDbSchema;
|
|
251
338
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!currentDbSchema.ok) return currentDbSchema;
|
|
339
|
+
let appOwner: AppOwner;
|
|
340
|
+
let clock: Clock;
|
|
255
341
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
260
|
-
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
261
|
-
|
|
262
|
-
const maybeMigrateToVersion0Result = maybeMigrateToVersion0({ sqlite })(
|
|
263
|
-
currentDbSchema.value,
|
|
264
|
-
);
|
|
265
|
-
if (!maybeMigrateToVersion0Result.ok)
|
|
266
|
-
return maybeMigrateToVersion0Result;
|
|
267
|
-
|
|
268
|
-
const ownerExists = currentDbSchema.value.tables.some(
|
|
269
|
-
(table) => table.name === "evolu_owner",
|
|
270
|
-
);
|
|
342
|
+
const dbIsInitialized = currentDbSchema.value.tables.some(
|
|
343
|
+
(table) => table.name === "evolu_version",
|
|
344
|
+
);
|
|
271
345
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
370
|
+
clock,
|
|
371
|
+
appOwnerId,
|
|
372
|
+
appOwnerEncryptionKey,
|
|
373
|
+
appOwnerWriteKey,
|
|
374
|
+
appOwnerMnemonic
|
|
375
|
+
from evolu_config
|
|
376
|
+
limit 1;
|
|
377
|
+
`);
|
|
378
|
+
if (!configResult.ok) return configResult;
|
|
291
379
|
|
|
292
|
-
const
|
|
293
|
-
onStorageError: (error) => {
|
|
294
|
-
postMessage({ type: "onError", error });
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
if (!storage.ok) return storage;
|
|
380
|
+
const [config] = configResult.value.rows;
|
|
298
381
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
382
|
+
appOwner = {
|
|
383
|
+
type: "AppOwner",
|
|
384
|
+
id: config.appOwnerId,
|
|
385
|
+
encryptionKey: config.appOwnerEncryptionKey,
|
|
386
|
+
writeKey: config.appOwnerWriteKey,
|
|
387
|
+
mnemonic: config.appOwnerMnemonic,
|
|
302
388
|
};
|
|
303
389
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (!ownerExists && isNonEmptyReadonlyArray(initMessage.initialData)) {
|
|
313
|
-
const result = applyChanges(depsWithoutSync)(initMessage.initialData);
|
|
314
|
-
if (!result.ok) return result;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
postMessage({ type: "onInit", owner: appOwner });
|
|
318
|
-
|
|
319
|
-
const sync = platformDeps.createSync(platformDeps)({
|
|
320
|
-
...initMessage.config,
|
|
321
|
-
onOpen: safeHandler(handleSyncOpen(depsWithoutSync)),
|
|
322
|
-
onMessage: safeHandler(createHandleSyncMessage(depsWithoutSync)),
|
|
323
|
-
});
|
|
390
|
+
clock = createClock({ ...platformDeps, sqlite })(
|
|
391
|
+
timestampStringToTimestamp(config.clock),
|
|
392
|
+
);
|
|
393
|
+
} else {
|
|
394
|
+
appOwner =
|
|
395
|
+
initMessage.config.externalAppOwner ??
|
|
396
|
+
createAppOwner(createOwnerSecret(platformDeps));
|
|
324
397
|
|
|
325
|
-
|
|
326
|
-
});
|
|
398
|
+
clock = createClock({ ...platformDeps, sqlite })();
|
|
327
399
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return null;
|
|
400
|
+
const result = initializeDb({ sqlite })(appOwner, clock.get());
|
|
401
|
+
if (!result.ok) return result;
|
|
331
402
|
}
|
|
332
403
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
case "mutate": {
|
|
339
|
-
const mutate = deps.sqlite.transaction(() => {
|
|
340
|
-
const toSyncChanges = [];
|
|
341
|
-
const localOnlyChanges = [];
|
|
342
|
-
|
|
343
|
-
for (const change of message.changes) {
|
|
344
|
-
// Table name starting with '_' is local-only (not synced).
|
|
345
|
-
if (change.table.startsWith("_")) localOnlyChanges.push(change);
|
|
346
|
-
else toSyncChanges.push(change);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
for (const change of localOnlyChanges) {
|
|
350
|
-
if (
|
|
351
|
-
"isDeleted" in change.values &&
|
|
352
|
-
change.values.isDeleted === 1
|
|
353
|
-
) {
|
|
354
|
-
const result = deps.sqlite.exec(sql`
|
|
355
|
-
delete from ${sql.identifier(change.table)}
|
|
356
|
-
where id = ${change.id};
|
|
357
|
-
`);
|
|
358
|
-
if (!result.ok) return result;
|
|
359
|
-
} else {
|
|
360
|
-
const millis = Millis.from(deps.time.now());
|
|
361
|
-
if (!millis.ok) {
|
|
362
|
-
return err<TimestampTimeOutOfRangeError>({
|
|
363
|
-
type: "TimestampTimeOutOfRangeError",
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
const date = new Date(millis.value).toISOString();
|
|
367
|
-
for (const [column, value] of objectToEntries(change.values)) {
|
|
368
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
369
|
-
insert into ${sql.identifier(change.table)}
|
|
370
|
-
("id", ${sql.identifier(column)}, createdAt, updatedAt)
|
|
371
|
-
values (${change.id}, ${value}, ${date}, ${date})
|
|
372
|
-
on conflict ("id") do update
|
|
373
|
-
set
|
|
374
|
-
${sql.identifier(column)} = ${value},
|
|
375
|
-
updatedAt = ${date};
|
|
376
|
-
`);
|
|
377
|
-
if (!result.ok) return result;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* We don't have to wait for the transaction to end because changes
|
|
384
|
-
* are idempotent, so there is no reason why they should fail. We
|
|
385
|
-
* want to call onChange ASAP.
|
|
386
|
-
*/
|
|
387
|
-
const onChange = () => {
|
|
388
|
-
deps.console.log("[db]", "onChange", {
|
|
389
|
-
subscribedQueries: message.subscribedQueries,
|
|
390
|
-
});
|
|
391
|
-
const patches = loadQueries(deps)(
|
|
392
|
-
message.tabId,
|
|
393
|
-
message.subscribedQueries,
|
|
394
|
-
);
|
|
395
|
-
if (!patches.ok) {
|
|
396
|
-
deps.postMessage({ type: "onError", error: patches.error });
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
// Notify the tab that performed the mutation.
|
|
400
|
-
deps.postMessage({
|
|
401
|
-
type: "onChange",
|
|
402
|
-
tabId: message.tabId,
|
|
403
|
-
patches: patches.value,
|
|
404
|
-
onCompleteIds: message.onCompleteIds,
|
|
405
|
-
});
|
|
406
|
-
// Notify other tabs to refresh their queries.
|
|
407
|
-
deps.postMessage({ type: "onReceive", tabId: message.tabId });
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
if (!isNonEmptyArray(toSyncChanges)) {
|
|
411
|
-
onChange();
|
|
412
|
-
return ok();
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const messages = applyChanges(deps)(toSyncChanges, onChange);
|
|
416
|
-
if (!messages.ok) return messages;
|
|
417
|
-
|
|
418
|
-
const owner = deps.ownerRowRef.get();
|
|
419
|
-
// TODO: Check owner whether it's allowed to write, return an
|
|
420
|
-
// error if not.
|
|
421
|
-
if (owner.writeKey == null) {
|
|
422
|
-
return ok();
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const protocolMessage = createProtocolMessageFromCrdtMessages(deps)(
|
|
426
|
-
owner as OwnerWithWriteAccess,
|
|
427
|
-
messages.value,
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
deps.console.log(
|
|
431
|
-
"[db]",
|
|
432
|
-
"send data message",
|
|
433
|
-
messages.value,
|
|
434
|
-
protocolMessage,
|
|
435
|
-
);
|
|
436
|
-
deps.sync.send(protocolMessage);
|
|
437
|
-
|
|
438
|
-
return ok();
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
if (!mutate.ok) {
|
|
442
|
-
deps.postMessage({ type: "onError", error: mutate.error });
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
break;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
case "query": {
|
|
450
|
-
const patches = loadQueries(deps)(message.tabId, message.queries);
|
|
451
|
-
|
|
452
|
-
if (!patches.ok) {
|
|
453
|
-
deps.postMessage({ type: "onError", error: patches.error });
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
deps.postMessage({
|
|
458
|
-
type: "onChange",
|
|
459
|
-
tabId: message.tabId,
|
|
460
|
-
patches: patches.value,
|
|
461
|
-
onCompleteIds: [],
|
|
462
|
-
});
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
case "reset": {
|
|
467
|
-
const resetResult = deps.sqlite.transaction(() => {
|
|
468
|
-
const dropAllTablesResult = dropAllTables(deps);
|
|
469
|
-
if (!dropAllTablesResult.ok) return dropAllTablesResult;
|
|
470
|
-
|
|
471
|
-
if (message.restore) {
|
|
472
|
-
const dbSchema = getDbSchema(deps)();
|
|
473
|
-
if (!dbSchema.ok) return dbSchema;
|
|
474
|
-
|
|
475
|
-
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
476
|
-
message.restore.dbSchema,
|
|
477
|
-
dbSchema.value,
|
|
478
|
-
);
|
|
479
|
-
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
480
|
-
|
|
481
|
-
const initializeDbResult = initializeDb(deps)(
|
|
482
|
-
message.restore.mnemonic,
|
|
483
|
-
);
|
|
484
|
-
if (!initializeDbResult.ok) return initializeDbResult;
|
|
485
|
-
}
|
|
486
|
-
return ok();
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
if (!resetResult.ok) {
|
|
490
|
-
deps.postMessage({ type: "onError", error: resetResult.error });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
deps.postMessage({
|
|
495
|
-
type: "onReset",
|
|
496
|
-
onCompleteId: message.onCompleteId,
|
|
497
|
-
reload: message.reload,
|
|
498
|
-
});
|
|
499
|
-
deps.sqlite[Symbol.dispose]();
|
|
500
|
-
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
case "ensureDbSchema": {
|
|
505
|
-
const ensureSchema = deps.sqlite.transaction(() => {
|
|
506
|
-
const dbSchema = getDbSchema(deps)();
|
|
507
|
-
if (!dbSchema.ok) return dbSchema;
|
|
508
|
-
|
|
509
|
-
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
510
|
-
message.dbSchema,
|
|
511
|
-
dbSchema.value,
|
|
512
|
-
);
|
|
513
|
-
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
514
|
-
|
|
515
|
-
return ok();
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
if (!ensureSchema.ok) {
|
|
519
|
-
deps.postMessage({ type: "onError", error: ensureSchema.error });
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
404
|
+
const result = ensureDbSchema({ sqlite })(
|
|
405
|
+
initMessage.dbSchema,
|
|
406
|
+
currentDbSchema.value,
|
|
407
|
+
);
|
|
408
|
+
if (!result.ok) return result;
|
|
524
409
|
|
|
525
|
-
|
|
526
|
-
|
|
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;
|
|
527
428
|
|
|
528
|
-
|
|
529
|
-
deps.postMessage({ type: "onError", error: file.error });
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
429
|
+
sync.value.useOwner(true, appOwner);
|
|
532
430
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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);
|
|
539
437
|
}
|
|
540
|
-
|
|
541
|
-
default:
|
|
542
|
-
exhaustiveCheck(message);
|
|
543
|
-
}
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Get the current database schema by reading SQLite metadata.
|
|
550
|
-
*
|
|
551
|
-
* TODO: Refactor out Evolu stuff and move it to Sqlite.
|
|
552
|
-
*/
|
|
553
|
-
export const getDbSchema =
|
|
554
|
-
(deps: SqliteDep) =>
|
|
555
|
-
({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
|
|
556
|
-
DbSchema,
|
|
557
|
-
SqliteError
|
|
558
|
-
> => {
|
|
559
|
-
const map = new Map<Base64Url256, Array<Base64Url256>>();
|
|
560
|
-
|
|
561
|
-
const tableAndColumnInfoRows = deps.sqlite.exec(sql`
|
|
562
|
-
select
|
|
563
|
-
sqlite_master.name as tableName,
|
|
564
|
-
table_info.name as columnName
|
|
565
|
-
from
|
|
566
|
-
sqlite_master
|
|
567
|
-
join pragma_table_info(sqlite_master.name) as table_info;
|
|
568
|
-
`);
|
|
569
|
-
|
|
570
|
-
if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
|
|
571
|
-
|
|
572
|
-
tableAndColumnInfoRows.value.rows.forEach((row) => {
|
|
573
|
-
const { tableName, columnName } = row as unknown as {
|
|
574
|
-
tableName: Base64Url256;
|
|
575
|
-
columnName: Base64Url256;
|
|
438
|
+
return cache;
|
|
576
439
|
};
|
|
577
|
-
if (!map.has(tableName)) map.set(tableName, []);
|
|
578
|
-
map.get(tableName)?.push(columnName);
|
|
579
|
-
});
|
|
580
440
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
`
|
|
590
|
-
: sql`
|
|
591
|
-
select name, sql
|
|
592
|
-
from sqlite_master
|
|
593
|
-
where
|
|
594
|
-
type = 'index'
|
|
595
|
-
and name not like 'sqlite_%'
|
|
596
|
-
and name not like 'evolu_%';
|
|
597
|
-
`,
|
|
598
|
-
);
|
|
441
|
+
const deps: DbWorkerDeps = {
|
|
442
|
+
...platformDeps,
|
|
443
|
+
getQueryRowsCache,
|
|
444
|
+
postMessage,
|
|
445
|
+
sqlite,
|
|
446
|
+
sync: sync.value,
|
|
447
|
+
appOwner,
|
|
448
|
+
};
|
|
599
449
|
|
|
600
|
-
|
|
450
|
+
return ok(deps);
|
|
451
|
+
});
|
|
601
452
|
|
|
602
|
-
|
|
603
|
-
(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
* SQLite returns "CREATE INDEX" for "create index" for some reason.
|
|
607
|
-
* Other keywords remain unchanged. We have to normalize the casing for
|
|
608
|
-
* {@link indexesAreEqual} manually.
|
|
609
|
-
*/
|
|
610
|
-
sql: (row.sql as string)
|
|
611
|
-
.replace("CREATE INDEX", "create index")
|
|
612
|
-
.replace("CREATE UNIQUE INDEX", "create unique index"),
|
|
613
|
-
}),
|
|
614
|
-
);
|
|
453
|
+
if (!deps.ok) {
|
|
454
|
+
postMessage({ type: "onError", error: deps.error });
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
615
457
|
|
|
616
|
-
return
|
|
458
|
+
return deps.value;
|
|
617
459
|
};
|
|
618
460
|
|
|
619
|
-
const
|
|
620
|
-
self.name === that.name && self.sql === that.sql;
|
|
621
|
-
|
|
622
|
-
export interface DbSnapshot {
|
|
623
|
-
readonly schema: DbSchema;
|
|
624
|
-
readonly rows: Array<{
|
|
625
|
-
rows: ReadonlyArray<SqliteRow>;
|
|
626
|
-
name: string;
|
|
627
|
-
}>;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// TODO: Move it to Sqlite.
|
|
631
|
-
export const getDbSnapshot = (
|
|
632
|
-
deps: SqliteDep,
|
|
633
|
-
): Result<DbSnapshot, SqliteError> => {
|
|
634
|
-
const schema = getDbSchema(deps)({ allIndexes: true });
|
|
635
|
-
if (!schema.ok) return schema;
|
|
636
|
-
|
|
637
|
-
const rows = [];
|
|
638
|
-
|
|
639
|
-
for (const table of schema.value.tables) {
|
|
640
|
-
const result = deps.sqlite.exec(sql`
|
|
641
|
-
select * from ${sql.identifier(table.name)};
|
|
642
|
-
`);
|
|
643
|
-
if (!result.ok) return result;
|
|
644
|
-
|
|
645
|
-
rows.push({
|
|
646
|
-
rows: result.value.rows,
|
|
647
|
-
name: table.name,
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return ok({ schema: schema.value, rows });
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
const ensureDbSchema =
|
|
461
|
+
const initializeDb =
|
|
655
462
|
(deps: SqliteDep) =>
|
|
656
463
|
(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
options?: { ignoreIndexes: boolean },
|
|
464
|
+
initialAppOwner: AppOwner,
|
|
465
|
+
initialClock: Timestamp,
|
|
660
466
|
): Result<void, SqliteError> => {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
);
|
|
667
|
-
if (!currentTable) {
|
|
668
|
-
queries.push({
|
|
669
|
-
sql: createAppTable(newTable.name, newTable.columns),
|
|
670
|
-
parameters: [],
|
|
671
|
-
});
|
|
672
|
-
} else {
|
|
673
|
-
newTable.columns
|
|
674
|
-
.filter((newColumn) => !currentTable.columns.includes(newColumn))
|
|
675
|
-
.forEach((newColumn) => {
|
|
676
|
-
queries.push(sql`
|
|
677
|
-
alter table ${sql.identifier(newTable.name)}
|
|
678
|
-
add column ${sql.identifier(newColumn)} blob;
|
|
679
|
-
`);
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
if (options?.ignoreIndexes !== true) {
|
|
685
|
-
// Remove current indexes that are not in the newSchema.
|
|
686
|
-
currentSchema.indexes
|
|
687
|
-
.filter(
|
|
688
|
-
(currentIndex) =>
|
|
689
|
-
!newSchema.indexes.some((newIndex) =>
|
|
690
|
-
indexesAreEqual(newIndex, currentIndex),
|
|
691
|
-
),
|
|
692
|
-
)
|
|
693
|
-
.forEach((index) => {
|
|
694
|
-
queries.push(sql`drop index ${sql.identifier(index.name)};`);
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
// Add new indexes that are not in the currentSchema.
|
|
698
|
-
newSchema.indexes
|
|
699
|
-
.filter(
|
|
700
|
-
(newIndex) =>
|
|
701
|
-
!currentSchema.indexes.some((currentIndex) =>
|
|
702
|
-
indexesAreEqual(newIndex, currentIndex),
|
|
703
|
-
),
|
|
467
|
+
for (const query of [
|
|
468
|
+
// Never change structure to ensure all versions can read it.
|
|
469
|
+
sql`
|
|
470
|
+
create table evolu_version (
|
|
471
|
+
"protocolVersion" integer not null
|
|
704
472
|
)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
for (const query of queries) {
|
|
711
|
-
const result = deps.sqlite.exec(query);
|
|
712
|
-
if (!result.ok) return result;
|
|
713
|
-
}
|
|
714
|
-
return ok();
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
export const createAppTable = (
|
|
718
|
-
tableName: string,
|
|
719
|
-
columns: ReadonlyArray<string>,
|
|
720
|
-
): SafeSql =>
|
|
721
|
-
`
|
|
722
|
-
create table ${sql.identifier(tableName).sql} (
|
|
723
|
-
"id" text primary key,
|
|
724
|
-
${columns
|
|
725
|
-
// Add default columns.
|
|
726
|
-
.concat(["createdAt", "updatedAt", "isDeleted"])
|
|
727
|
-
.filter((c) => c !== "id")
|
|
728
|
-
// "A column with affinity BLOB does not prefer one storage class over another
|
|
729
|
-
// and no attempt is made to coerce data from one storage class into another."
|
|
730
|
-
// https://www.sqlite.org/datatype3.html
|
|
731
|
-
.map((name) => `${sql.identifier(name).sql} blob`)
|
|
732
|
-
.join(", ")}
|
|
733
|
-
);
|
|
734
|
-
` as SafeSql;
|
|
735
|
-
|
|
736
|
-
const selectAppOwner = (
|
|
737
|
-
deps: SqliteDep,
|
|
738
|
-
): Result<[AppOwner, OwnerRow], SqliteError> => {
|
|
739
|
-
const result = deps.sqlite.exec<OwnerRow>(sql`
|
|
740
|
-
select mnemonic, id, createdAt, encryptionKey, writeKey, timestamp
|
|
741
|
-
from evolu_owner
|
|
742
|
-
order by createdAt asc
|
|
743
|
-
limit 1;
|
|
744
|
-
`);
|
|
745
|
-
|
|
746
|
-
if (!result.ok) return result;
|
|
747
|
-
|
|
748
|
-
const {
|
|
749
|
-
rows: [ownerRow],
|
|
750
|
-
} = result.value;
|
|
751
|
-
|
|
752
|
-
assert(ownerRow.writeKey != null, "The writeKey is null");
|
|
753
|
-
|
|
754
|
-
const appOwner: AppOwner = {
|
|
755
|
-
type: "AppOwner",
|
|
756
|
-
mnemonic: ownerRow.mnemonic,
|
|
757
|
-
createdAt: ownerRow.createdAt,
|
|
758
|
-
id: ownerRow.id,
|
|
759
|
-
encryptionKey: ownerRow.encryptionKey,
|
|
760
|
-
writeKey: ownerRow.writeKey,
|
|
761
|
-
};
|
|
473
|
+
strict;
|
|
474
|
+
`,
|
|
762
475
|
|
|
763
|
-
|
|
764
|
-
|
|
476
|
+
sql`
|
|
477
|
+
insert into evolu_version ("protocolVersion")
|
|
478
|
+
values (${protocolVersion});
|
|
479
|
+
`,
|
|
765
480
|
|
|
766
|
-
const initializeDb =
|
|
767
|
-
(
|
|
768
|
-
deps: SqliteDep &
|
|
769
|
-
NanoIdLibDep &
|
|
770
|
-
CreateMnemonicDep &
|
|
771
|
-
TimeDep &
|
|
772
|
-
CreateRandomBytesDep,
|
|
773
|
-
) =>
|
|
774
|
-
(mnemonic?: Mnemonic): Result<[AppOwner, OwnerRow], SqliteError> => {
|
|
775
|
-
for (const query of [
|
|
776
481
|
sql`
|
|
777
482
|
create table evolu_config (
|
|
778
|
-
"
|
|
779
|
-
"
|
|
483
|
+
"clock" text not null,
|
|
484
|
+
"appOwnerId" text not null,
|
|
485
|
+
"appOwnerEncryptionKey" blob not null,
|
|
486
|
+
"appOwnerWriteKey" blob not null,
|
|
487
|
+
"appOwnerMnemonic" text
|
|
780
488
|
)
|
|
781
489
|
strict;
|
|
782
490
|
`,
|
|
783
491
|
|
|
784
492
|
sql`
|
|
785
|
-
insert into evolu_config
|
|
786
|
-
|
|
493
|
+
insert into evolu_config
|
|
494
|
+
(
|
|
495
|
+
"clock",
|
|
496
|
+
"appOwnerId",
|
|
497
|
+
"appOwnerEncryptionKey",
|
|
498
|
+
"appOwnerWriteKey",
|
|
499
|
+
"appOwnerMnemonic"
|
|
500
|
+
)
|
|
501
|
+
values
|
|
502
|
+
(
|
|
503
|
+
${timestampToTimestampString(initialClock)},
|
|
504
|
+
${initialAppOwner.id},
|
|
505
|
+
${initialAppOwner.encryptionKey},
|
|
506
|
+
${initialAppOwner.writeKey},
|
|
507
|
+
${initialAppOwner.mnemonic ?? null}
|
|
508
|
+
);
|
|
787
509
|
`,
|
|
788
510
|
|
|
789
511
|
/**
|
|
790
|
-
* The History table stores all values per timestamp, table,
|
|
791
|
-
* column
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
* and DbChange to leverage a covering index. Hence, every value change
|
|
796
|
-
* has its timestamp, table, row, and column. In the future, we will
|
|
797
|
-
* rethink that and store history more efficiently.
|
|
798
|
-
*
|
|
799
|
-
* There is no need to use `OwnerId` on the client, because timestamps
|
|
800
|
-
* (which include a `NodeId`) are globally unique. The relay needs
|
|
801
|
-
* `OwnerId` because it hosts multiple apps/owners, but client storage
|
|
802
|
-
* represents one DB, and even when it hosts multiple owners, their
|
|
803
|
-
* timestamps remain unique.
|
|
512
|
+
* The History table stores all values per ownerId, timestamp, table, id,
|
|
513
|
+
* and column for conflict-free merging using last-write-win CRDT.
|
|
514
|
+
* Denormalizes Timestamp and DbChange for covering index performance.
|
|
515
|
+
* Time travel is available when last-write-win isn't desired. Future
|
|
516
|
+
* optimization will store history more efficiently.
|
|
804
517
|
*/
|
|
805
518
|
sql`
|
|
806
519
|
create table evolu_history (
|
|
807
|
-
"
|
|
520
|
+
"ownerId" blob not null,
|
|
808
521
|
"table" text not null,
|
|
809
|
-
"
|
|
522
|
+
"id" blob not null,
|
|
810
523
|
"column" text not null,
|
|
524
|
+
"timestamp" blob not null,
|
|
811
525
|
"value" any
|
|
812
526
|
)
|
|
813
527
|
strict;
|
|
814
528
|
`,
|
|
815
529
|
|
|
530
|
+
// Index for reading database changes by owner and timestamp.
|
|
531
|
+
// Timestamp always corresponds to a DbChange.
|
|
816
532
|
sql`
|
|
817
|
-
create index
|
|
533
|
+
create index evolu_history_ownerId_timestamp on evolu_history (
|
|
534
|
+
"ownerId",
|
|
535
|
+
"timestamp"
|
|
536
|
+
);
|
|
818
537
|
`,
|
|
819
538
|
|
|
820
539
|
sql`
|
|
821
|
-
create unique index
|
|
822
|
-
"
|
|
823
|
-
"column",
|
|
540
|
+
create unique index evolu_history_ownerId_table_id_column_timestampDesc on evolu_history (
|
|
541
|
+
"ownerId",
|
|
824
542
|
"table",
|
|
543
|
+
"id",
|
|
544
|
+
"column",
|
|
825
545
|
"timestamp" desc
|
|
826
546
|
);
|
|
827
547
|
`,
|
|
828
|
-
|
|
829
|
-
sql`
|
|
830
|
-
create table evolu_owner (
|
|
831
|
-
"mnemonic" text not null primary key,
|
|
832
|
-
"id" text not null,
|
|
833
|
-
"encryptionKey" blob not null,
|
|
834
|
-
"createdAt" text not null,
|
|
835
|
-
"writeKey" blob,
|
|
836
|
-
"timestamp" text not null
|
|
837
|
-
)
|
|
838
|
-
strict;
|
|
839
|
-
`,
|
|
840
548
|
]) {
|
|
841
549
|
const result = deps.sqlite.exec(query);
|
|
842
550
|
if (!result.ok) return result;
|
|
843
551
|
}
|
|
844
552
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const result = deps.sqlite.exec(sql`
|
|
849
|
-
insert into evolu_owner
|
|
850
|
-
(mnemonic, id, encryptionKey, createdAt, writeKey, timestamp)
|
|
851
|
-
values
|
|
852
|
-
(
|
|
853
|
-
${ownerRow.mnemonic},
|
|
854
|
-
${ownerRow.id},
|
|
855
|
-
${ownerRow.encryptionKey},
|
|
856
|
-
${ownerRow.createdAt},
|
|
857
|
-
${ownerRow.writeKey},
|
|
858
|
-
${ownerRow.timestamp}
|
|
859
|
-
);
|
|
860
|
-
`);
|
|
553
|
+
return ok();
|
|
554
|
+
};
|
|
861
555
|
|
|
862
|
-
|
|
556
|
+
const handlers: Omit<MessageHandlers<DbWorkerInput, DbWorkerDeps>, "init"> = {
|
|
557
|
+
getAppOwner: (deps) => () => {
|
|
558
|
+
deps.postMessage({
|
|
559
|
+
type: "onGetAppOwner",
|
|
560
|
+
appOwner: deps.appOwner,
|
|
561
|
+
});
|
|
562
|
+
},
|
|
863
563
|
|
|
864
|
-
|
|
865
|
-
|
|
564
|
+
mutate: (deps) => (message) => {
|
|
565
|
+
const mutate = deps.sqlite.transaction(() => {
|
|
566
|
+
const syncChanges: Array<MutationChange> = [];
|
|
866
567
|
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
+
}
|
|
577
|
+
|
|
578
|
+
if (isNonEmptyArray(syncChanges)) {
|
|
579
|
+
const result = deps.sync.applyChanges(syncChanges);
|
|
580
|
+
if (!result.ok) return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
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
|
+
});
|
|
889
597
|
|
|
890
|
-
|
|
598
|
+
// Notify other tabs to refresh their queries.
|
|
599
|
+
deps.postMessage({ type: "refreshQueries", tabId: message.tabId });
|
|
600
|
+
|
|
601
|
+
return ok();
|
|
602
|
+
});
|
|
891
603
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
lastTimestamp = nextTimestamp.value;
|
|
896
|
-
messages.push({ timestamp: lastTimestamp, change });
|
|
604
|
+
if (!mutate.ok) {
|
|
605
|
+
deps.postMessage({ type: "onError", error: mutate.error });
|
|
606
|
+
return;
|
|
897
607
|
}
|
|
608
|
+
},
|
|
898
609
|
|
|
899
|
-
|
|
900
|
-
|
|
610
|
+
query: (deps) => (message) => {
|
|
611
|
+
const queryPatches = loadQueries(deps)(message.tabId, message.queries);
|
|
901
612
|
|
|
902
|
-
if (
|
|
613
|
+
if (!queryPatches.ok) {
|
|
614
|
+
deps.postMessage({ type: "onError", error: queryPatches.error });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
903
617
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
618
|
+
deps.postMessage({
|
|
619
|
+
type: "onQueryPatches",
|
|
620
|
+
tabId: message.tabId,
|
|
621
|
+
queryPatches: queryPatches.value,
|
|
622
|
+
onCompleteIds: [],
|
|
623
|
+
});
|
|
624
|
+
},
|
|
907
625
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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;
|
|
919
642
|
}
|
|
920
643
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
644
|
+
if (message.restore) {
|
|
645
|
+
const dbSchema = getDbSchema(deps)();
|
|
646
|
+
if (!dbSchema.ok) return dbSchema;
|
|
924
647
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (!saveTimestamp.ok) return saveTimestamp;
|
|
648
|
+
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
649
|
+
message.restore.dbSchema,
|
|
650
|
+
dbSchema.value,
|
|
651
|
+
);
|
|
652
|
+
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
931
653
|
|
|
932
|
-
|
|
933
|
-
|
|
654
|
+
const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
|
|
655
|
+
const appOwner = createAppOwner(secret);
|
|
656
|
+
const clock = createClock(deps)();
|
|
934
657
|
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const date = new Date(message.timestamp.millis).toISOString();
|
|
939
|
-
const timestamp = timestampToBinaryTimestamp(message.timestamp);
|
|
940
|
-
|
|
941
|
-
for (const [column, value] of objectToEntries(message.change.values)) {
|
|
942
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
943
|
-
with
|
|
944
|
-
lastTimestamp as (
|
|
945
|
-
select timestamp
|
|
946
|
-
from evolu_history
|
|
947
|
-
where
|
|
948
|
-
"row" = ${message.change.id}
|
|
949
|
-
and "column" = ${column}
|
|
950
|
-
and "table" = ${message.change.table}
|
|
951
|
-
order by timestamp desc
|
|
952
|
-
limit 1
|
|
953
|
-
)
|
|
954
|
-
insert into ${sql.identifier(message.change.table)}
|
|
955
|
-
("id", ${sql.identifier(column)}, createdAt, updatedAt)
|
|
956
|
-
select ${message.change.id}, ${value}, ${date}, ${date}
|
|
957
|
-
where
|
|
958
|
-
(select timestamp from lastTimestamp) is null
|
|
959
|
-
or (select timestamp from lastTimestamp) < ${timestamp}
|
|
960
|
-
on conflict ("id") do update
|
|
961
|
-
set
|
|
962
|
-
${sql.identifier(column)} = ${value},
|
|
963
|
-
updatedAt = ${date}
|
|
964
|
-
where
|
|
965
|
-
(select timestamp from lastTimestamp) is null
|
|
966
|
-
or (select timestamp from lastTimestamp) < ${timestamp};
|
|
967
|
-
`);
|
|
658
|
+
const initializeDbResult = initializeDb(deps)(appOwner, clock.get());
|
|
659
|
+
if (!initializeDbResult.ok) return initializeDbResult;
|
|
660
|
+
}
|
|
968
661
|
|
|
969
|
-
|
|
662
|
+
return ok();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (!resetResult.ok) {
|
|
666
|
+
deps.postMessage({ type: "onError", error: resetResult.error });
|
|
667
|
+
return;
|
|
970
668
|
}
|
|
971
669
|
|
|
972
|
-
|
|
973
|
-
|
|
670
|
+
deps.postMessage({
|
|
671
|
+
type: "onReset",
|
|
672
|
+
onCompleteId: message.onCompleteId,
|
|
673
|
+
reload: message.reload,
|
|
674
|
+
});
|
|
675
|
+
},
|
|
974
676
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const ownerId = ownerIdToBinaryOwnerId(deps.ownerRowRef.get().id);
|
|
980
|
-
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;
|
|
981
681
|
|
|
982
|
-
|
|
983
|
-
|
|
682
|
+
const ensureDbSchemaResult = ensureDbSchema(deps)(
|
|
683
|
+
message.dbSchema,
|
|
684
|
+
dbSchema.value,
|
|
685
|
+
);
|
|
686
|
+
if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
|
|
984
687
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
on conflict do nothing;
|
|
992
|
-
`);
|
|
993
|
-
if (!result.ok) return result;
|
|
688
|
+
return ok();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (!ensureSchema.ok) {
|
|
692
|
+
deps.postMessage({ type: "onError", error: ensureSchema.error });
|
|
693
|
+
return;
|
|
994
694
|
}
|
|
695
|
+
},
|
|
995
696
|
|
|
996
|
-
|
|
997
|
-
|
|
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
|
+
};
|
|
998
716
|
|
|
999
717
|
const loadQueries =
|
|
1000
718
|
(deps: GetQueryRowsCacheDep & SqliteDep) =>
|
|
@@ -1033,243 +751,3 @@ const loadQueries =
|
|
|
1033
751
|
);
|
|
1034
752
|
return ok(queryPatchesArray);
|
|
1035
753
|
};
|
|
1036
|
-
|
|
1037
|
-
export const maybeMigrateToVersion0 =
|
|
1038
|
-
(deps: SqliteDep) =>
|
|
1039
|
-
(
|
|
1040
|
-
schema: DbSchema,
|
|
1041
|
-
): Result<
|
|
1042
|
-
{
|
|
1043
|
-
readonly messages: ReadonlyArray<CrdtMessage>;
|
|
1044
|
-
readonly mnemonic: Mnemonic;
|
|
1045
|
-
readonly lastTimestamp: Timestamp;
|
|
1046
|
-
} | null,
|
|
1047
|
-
SqliteError
|
|
1048
|
-
> => {
|
|
1049
|
-
// evolu_history is a new table
|
|
1050
|
-
const hasOwnerButNoHistory =
|
|
1051
|
-
schema.tables.some((t) => t.name === "evolu_owner") &&
|
|
1052
|
-
!schema.tables.some((t) => t.name === "evolu_history");
|
|
1053
|
-
if (!hasOwnerButNoHistory) {
|
|
1054
|
-
return ok(null);
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
const mnemonicAndLastTimestamp = deps.sqlite.exec<{
|
|
1058
|
-
mnemonic: Mnemonic;
|
|
1059
|
-
timestamp: TimestampString;
|
|
1060
|
-
}>(sql` select mnemonic, timestamp from evolu_owner limit 1; `);
|
|
1061
|
-
if (!mnemonicAndLastTimestamp.ok) return mnemonicAndLastTimestamp;
|
|
1062
|
-
|
|
1063
|
-
const messagesRows = deps.sqlite.exec<{
|
|
1064
|
-
timestamp: TimestampString;
|
|
1065
|
-
table: Base64Url256;
|
|
1066
|
-
row: Id;
|
|
1067
|
-
column: Base64Url256;
|
|
1068
|
-
value: SqliteValue;
|
|
1069
|
-
}>(sql`
|
|
1070
|
-
select "timestamp", "table", "row", "column", "value" from evolu_message;
|
|
1071
|
-
`);
|
|
1072
|
-
|
|
1073
|
-
if (!messagesRows.ok) return messagesRows;
|
|
1074
|
-
|
|
1075
|
-
for (const query of [
|
|
1076
|
-
sql`drop table evolu_owner;`,
|
|
1077
|
-
sql`drop table evolu_message;`,
|
|
1078
|
-
]) {
|
|
1079
|
-
const result = deps.sqlite.exec(query);
|
|
1080
|
-
if (!result.ok) return result;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const messages = messagesRows.value.rows.map((message) => ({
|
|
1084
|
-
timestamp: timestampStringToTimestamp(message.timestamp),
|
|
1085
|
-
change: {
|
|
1086
|
-
id: message.row,
|
|
1087
|
-
table: message.table,
|
|
1088
|
-
values: { [message.column]: message.value },
|
|
1089
|
-
},
|
|
1090
|
-
}));
|
|
1091
|
-
|
|
1092
|
-
const {
|
|
1093
|
-
rows: [{ mnemonic, timestamp }],
|
|
1094
|
-
} = mnemonicAndLastTimestamp.value;
|
|
1095
|
-
const lastTimestamp = timestampStringToTimestamp(timestamp);
|
|
1096
|
-
|
|
1097
|
-
return ok({ messages, mnemonic, lastTimestamp });
|
|
1098
|
-
};
|
|
1099
|
-
|
|
1100
|
-
const dropAllTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
1101
|
-
const schema = getDbSchema(deps)();
|
|
1102
|
-
if (!schema.ok) return schema;
|
|
1103
|
-
for (const table of schema.value.tables) {
|
|
1104
|
-
/**
|
|
1105
|
-
* The dropped table is completely removed from the database schema and the
|
|
1106
|
-
* disk file. The table can not be recovered. All indices and triggers
|
|
1107
|
-
* associated with the table are also deleted.
|
|
1108
|
-
* https://sqlite.org/lang_droptable.html
|
|
1109
|
-
*/
|
|
1110
|
-
const result = deps.sqlite.exec(sql`
|
|
1111
|
-
drop table ${sql.identifier(table.name)};
|
|
1112
|
-
`);
|
|
1113
|
-
if (!result.ok) return result;
|
|
1114
|
-
}
|
|
1115
|
-
return ok();
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
|
-
const handleSyncOpen =
|
|
1119
|
-
(deps: OwnerRowRefDep & StorageDep & ConsoleDep): SyncConfig["onOpen"] =>
|
|
1120
|
-
(send) => {
|
|
1121
|
-
const ownerId = deps.ownerRowRef.get().id;
|
|
1122
|
-
const message = createProtocolMessageForSync(deps)(ownerId);
|
|
1123
|
-
if (message) {
|
|
1124
|
-
deps.console.log("[db]", "send initial sync message", message);
|
|
1125
|
-
send(message);
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
const createHandleSyncMessage =
|
|
1130
|
-
(
|
|
1131
|
-
deps: PostMessageDep & StorageDep & SqliteDep & ConsoleDep & OwnerRowRefDep,
|
|
1132
|
-
): SyncConfig["onMessage"] =>
|
|
1133
|
-
(input, send) => {
|
|
1134
|
-
deps.console.log("[db]", "receive sync message", input);
|
|
1135
|
-
const { writeKey } = deps.ownerRowRef.get();
|
|
1136
|
-
|
|
1137
|
-
const output = deps.sqlite.transaction(() =>
|
|
1138
|
-
applyProtocolMessageAsClient(deps)(input, {
|
|
1139
|
-
getWriteKey: (_ownerId) => writeKey,
|
|
1140
|
-
}),
|
|
1141
|
-
);
|
|
1142
|
-
if (!output.ok) {
|
|
1143
|
-
deps.postMessage({ type: "onError", error: output.error });
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
if (output.value) {
|
|
1148
|
-
deps.console.log("[db]", "respond sync message", output.value);
|
|
1149
|
-
send(output.value);
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
export interface ClientStorage extends SqliteStorageBase, Storage {}
|
|
1154
|
-
|
|
1155
|
-
export interface ClientStorageDep {
|
|
1156
|
-
readonly storage: ClientStorage;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const createClientStorage =
|
|
1160
|
-
(
|
|
1161
|
-
deps: SqliteDep &
|
|
1162
|
-
PostMessageDep &
|
|
1163
|
-
SymmetricCryptoDep &
|
|
1164
|
-
OwnerRowRefDep &
|
|
1165
|
-
RandomDep &
|
|
1166
|
-
TimeDep &
|
|
1167
|
-
TimestampConfigDep,
|
|
1168
|
-
) =>
|
|
1169
|
-
(
|
|
1170
|
-
options: CreateSqliteStorageBaseOptions,
|
|
1171
|
-
): Result<ClientStorage, SqliteError> => {
|
|
1172
|
-
const sqliteStorageBase = createSqliteStorageBase(deps)(options);
|
|
1173
|
-
if (!sqliteStorageBase.ok) return sqliteStorageBase;
|
|
1174
|
-
|
|
1175
|
-
const storage: ClientStorage = {
|
|
1176
|
-
...sqliteStorageBase.value,
|
|
1177
|
-
|
|
1178
|
-
validateWriteKey: constFalse,
|
|
1179
|
-
|
|
1180
|
-
writeMessages: (_ownerId, messages) => {
|
|
1181
|
-
// TODO: Get owner by _ownerId when we support more.
|
|
1182
|
-
const owner = deps.ownerRowRef.get();
|
|
1183
|
-
const decodedAndDecryptedMessages: Array<CrdtMessage> = [];
|
|
1184
|
-
|
|
1185
|
-
for (const message of messages) {
|
|
1186
|
-
const dbChange = decryptAndDecodeDbChange(deps)(
|
|
1187
|
-
message.change,
|
|
1188
|
-
owner.encryptionKey,
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
if (!dbChange.ok) {
|
|
1192
|
-
deps.postMessage({
|
|
1193
|
-
type: "onError",
|
|
1194
|
-
error: dbChange.error,
|
|
1195
|
-
});
|
|
1196
|
-
return false;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
decodedAndDecryptedMessages.push({
|
|
1200
|
-
timestamp: message.timestamp,
|
|
1201
|
-
change: dbChange.value,
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
let timestamp = timestampStringToTimestamp(owner.timestamp);
|
|
1206
|
-
|
|
1207
|
-
for (const message of messages) {
|
|
1208
|
-
const receive = receiveTimestamp(deps)(timestamp, message.timestamp);
|
|
1209
|
-
if (!receive.ok) {
|
|
1210
|
-
deps.postMessage({ type: "onError", error: receive.error });
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
timestamp = receive.value;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const applyMessagesResult = applyMessages({ ...deps, storage })(
|
|
1217
|
-
decodedAndDecryptedMessages,
|
|
1218
|
-
timestamp,
|
|
1219
|
-
);
|
|
1220
|
-
|
|
1221
|
-
if (!applyMessagesResult.ok) {
|
|
1222
|
-
deps.postMessage({
|
|
1223
|
-
type: "onError",
|
|
1224
|
-
error: applyMessagesResult.error,
|
|
1225
|
-
});
|
|
1226
|
-
return false;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
deps.postMessage({ type: "onReceive" });
|
|
1230
|
-
|
|
1231
|
-
return true;
|
|
1232
|
-
},
|
|
1233
|
-
|
|
1234
|
-
readDbChange: (_ownerId, timestamp) => {
|
|
1235
|
-
const result = deps.sqlite.exec<{
|
|
1236
|
-
table: Base64Url256;
|
|
1237
|
-
row: BinaryId;
|
|
1238
|
-
column: Base64Url256;
|
|
1239
|
-
value: SqliteValue;
|
|
1240
|
-
}>(sql`
|
|
1241
|
-
select "table", "row", "column", "value"
|
|
1242
|
-
from evolu_history
|
|
1243
|
-
where "timestamp" = ${timestamp};
|
|
1244
|
-
`);
|
|
1245
|
-
if (!result.ok) {
|
|
1246
|
-
deps.postMessage({ type: "onError", error: result.error });
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const { rows } = result.value;
|
|
1251
|
-
assert(rows.length > 0, "Rows must not be empty");
|
|
1252
|
-
|
|
1253
|
-
const { table, row } = rows[0];
|
|
1254
|
-
const values: Record<string, SqliteValue> = {};
|
|
1255
|
-
|
|
1256
|
-
for (const r of rows) {
|
|
1257
|
-
assert(r.table === table, "All rows must have the same table");
|
|
1258
|
-
assert(eqArrayNumber(r.row, row), "All rows must have the same Id");
|
|
1259
|
-
values[r.column] = r.value;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
const change: DbChange = {
|
|
1263
|
-
table: rows[0].table,
|
|
1264
|
-
id: binaryIdToId(rows[0].row),
|
|
1265
|
-
values,
|
|
1266
|
-
};
|
|
1267
|
-
|
|
1268
|
-
const { encryptionKey } = deps.ownerRowRef.get();
|
|
1269
|
-
|
|
1270
|
-
return encodeAndEncryptDbChange(deps)(change, encryptionKey);
|
|
1271
|
-
},
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
|
-
return ok(storage);
|
|
1275
|
-
};
|