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