@evolu/common 7.4.0 → 8.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
package/src/local-first/Db.ts
CHANGED
|
@@ -1,483 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic Evolu DbWorker.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import {
|
|
8
|
+
appendToArray,
|
|
2
9
|
firstInArray,
|
|
3
|
-
|
|
4
|
-
NonEmptyReadonlyArray,
|
|
10
|
+
type NonEmptyArray,
|
|
11
|
+
type NonEmptyReadonlyArray,
|
|
5
12
|
} from "../Array.js";
|
|
6
|
-
import { assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
7
|
-
import {
|
|
8
|
-
import { ConsoleConfig, ConsoleDep } from "../Console.js";
|
|
13
|
+
import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
14
|
+
import type { ConsoleLevel } from "../Console.js";
|
|
9
15
|
import {
|
|
10
|
-
createSymmetricCrypto,
|
|
11
16
|
EncryptionKey,
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
type DecryptWithXChaCha20Poly1305Error,
|
|
18
|
+
type RandomBytesDep,
|
|
14
19
|
} from "../Crypto.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { ok, Result } from "../Result.js";
|
|
20
|
+
import { exhaustiveCheck, lazyFalse, lazyVoid } from "../Function.js";
|
|
21
|
+
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
22
|
+
import { ok, type Result } from "../Result.js";
|
|
23
|
+
import type {
|
|
24
|
+
CreateSqliteDriverDep,
|
|
25
|
+
SqliteDep,
|
|
26
|
+
SqliteRow,
|
|
27
|
+
SqliteSchema,
|
|
28
|
+
} from "../Sqlite.js";
|
|
18
29
|
import {
|
|
30
|
+
booleanToSqliteBoolean,
|
|
19
31
|
createSqlite,
|
|
20
|
-
CreateSqliteDriverDep,
|
|
21
32
|
sql,
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
SqliteBoolean,
|
|
34
|
+
sqliteBooleanToBoolean,
|
|
35
|
+
sqliteQueryStringToSqliteQuery,
|
|
36
|
+
SqliteValue,
|
|
24
37
|
} from "../Sqlite.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
38
|
+
import { callback, type LeaderLockDep, type Task } from "../Task.js";
|
|
39
|
+
import { Millis, millisToDateIso, type TimeDep } from "../Time.js";
|
|
40
|
+
import type { Name } from "../Type.js";
|
|
28
41
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
Id,
|
|
43
|
+
IdBytes,
|
|
44
|
+
idBytesToId,
|
|
45
|
+
idToIdBytes,
|
|
46
|
+
onePositiveInt,
|
|
47
|
+
} from "../Type.js";
|
|
48
|
+
import type { ExtractType } from "../Types.js";
|
|
49
|
+
import type {
|
|
50
|
+
NativeMessagePort,
|
|
31
51
|
Worker,
|
|
52
|
+
WorkerDeps,
|
|
53
|
+
WorkerSelf,
|
|
32
54
|
} from "../Worker.js";
|
|
55
|
+
import type { OwnerId, OwnerIdBytes } from "./Owner.js";
|
|
56
|
+
import { ownerIdBytesToOwnerId, ownerIdToOwnerIdBytes } from "./Owner.js";
|
|
33
57
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} from "./
|
|
45
|
-
import {
|
|
58
|
+
applyProtocolMessageAsClient,
|
|
59
|
+
createProtocolMessageForSync,
|
|
60
|
+
decryptAndDecodeDbChange,
|
|
61
|
+
encodeAndEncryptDbChange,
|
|
62
|
+
protocolVersion,
|
|
63
|
+
SubscriptionFlags,
|
|
64
|
+
type ProtocolInvalidDataError,
|
|
65
|
+
type ProtocolMessage,
|
|
66
|
+
type ProtocolTimestampMismatchError,
|
|
67
|
+
} from "./Protocol.js";
|
|
68
|
+
import type { Query, RowsByQueryMap } from "./Query.js";
|
|
69
|
+
import type { MutationChange, SqliteSchemaDep } from "./Schema.js";
|
|
46
70
|
import {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Query,
|
|
51
|
-
QueryPatches,
|
|
52
|
-
} from "./Query.js";
|
|
53
|
-
import {
|
|
54
|
-
DbSchema,
|
|
55
|
-
ensureDbSchema,
|
|
56
|
-
getDbSchema,
|
|
57
|
-
MutationChange,
|
|
71
|
+
ensureSqliteSchema,
|
|
72
|
+
getEvoluSqliteSchema,
|
|
73
|
+
systemColumns,
|
|
58
74
|
} from "./Schema.js";
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
applyLocalOnlyChange,
|
|
62
|
-
Clock,
|
|
63
|
-
createClock,
|
|
64
|
-
createSync,
|
|
65
|
-
SyncDep,
|
|
66
|
-
SyncOwner,
|
|
67
|
-
tryApplyQuarantinedMessages,
|
|
68
|
-
} from "./Sync.js";
|
|
75
|
+
import type { DbWorkerInput, DbWorkerOutput } from "./Shared.js";
|
|
69
76
|
import {
|
|
77
|
+
createBaseSqliteStorage,
|
|
78
|
+
createBaseSqliteStorageTables,
|
|
79
|
+
DbChange,
|
|
80
|
+
getOwnerUsage,
|
|
81
|
+
getTimestampInsertStrategy,
|
|
82
|
+
updateOwnerUsage,
|
|
83
|
+
type BaseSqliteStorage,
|
|
84
|
+
type BaseSqliteStorageDep,
|
|
85
|
+
type CrdtMessage,
|
|
86
|
+
type Storage,
|
|
87
|
+
} from "./Storage.js";
|
|
88
|
+
import type {
|
|
70
89
|
Timestamp,
|
|
90
|
+
TimestampCounterOverflowError,
|
|
91
|
+
TimestampDriftError,
|
|
92
|
+
TimestampTimeOutOfRangeError,
|
|
93
|
+
} from "./Timestamp.js";
|
|
94
|
+
import {
|
|
95
|
+
createInitialTimestamp,
|
|
96
|
+
defaultTimestampMaxDrift,
|
|
97
|
+
receiveTimestamp,
|
|
98
|
+
sendTimestamp,
|
|
71
99
|
TimestampBytes,
|
|
72
100
|
timestampBytesToTimestamp,
|
|
73
|
-
TimestampConfig,
|
|
74
|
-
TimestampError,
|
|
75
101
|
timestampToTimestampBytes,
|
|
102
|
+
type TimestampConfigDep,
|
|
76
103
|
} from "./Timestamp.js";
|
|
77
104
|
|
|
78
|
-
export
|
|
79
|
-
/**
|
|
80
|
-
* The name of the Evolu instance. Evolu is multitenant - it can run multiple
|
|
81
|
-
* instances concurrently. Each instance must have a unique name.
|
|
82
|
-
*
|
|
83
|
-
* The instance name is used as the SQLite database filename for persistent
|
|
84
|
-
* storage, ensuring that database files are separated and invisible to each
|
|
85
|
-
* other.
|
|
86
|
-
*
|
|
87
|
-
* The default value is: `Evolu`.
|
|
88
|
-
*
|
|
89
|
-
* ### Example
|
|
90
|
-
*
|
|
91
|
-
* ```ts
|
|
92
|
-
* // name: SimpleName.orThrow("MyApp")
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
readonly name: SimpleName;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Transport configuration for data sync and backup. Supports single transport
|
|
99
|
-
* or multiple transports simultaneously for redundancy.
|
|
100
|
-
*
|
|
101
|
-
* **Redundancy:** The ideal setup uses at least two completely independent
|
|
102
|
-
* relays - for example, a home relay and a geographically separate relay.
|
|
103
|
-
* Data is sent to both relays simultaneously, providing true redundancy
|
|
104
|
-
* similar to using two independent clouds. This eliminates vendor lock-in and
|
|
105
|
-
* ensures your app continues working regardless of circumstances - whether
|
|
106
|
-
* home relay hardware is stolen or a remote relay provider shuts down.
|
|
107
|
-
*
|
|
108
|
-
* Currently supports:
|
|
109
|
-
*
|
|
110
|
-
* - WebSocket: Real-time bidirectional communication with relay servers
|
|
111
|
-
*
|
|
112
|
-
* Empty transports create local-only instances. Transports can be dynamically
|
|
113
|
-
* added and removed for any owner (including {@link AppOwner}) via
|
|
114
|
-
* {@link Evolu#useOwner}.
|
|
115
|
-
*
|
|
116
|
-
* Use {@link createOwnerWebSocketTransport} to create WebSocket transport
|
|
117
|
-
* configurations with proper URL formatting and {@link OwnerId} inclusion. The
|
|
118
|
-
* {@link OwnerId} in the URL enables relay authentication, allowing relay
|
|
119
|
-
* servers to control access (e.g., for paid tiers or private instances).
|
|
120
|
-
*
|
|
121
|
-
* The default value is:
|
|
122
|
-
*
|
|
123
|
-
* `{ type: "WebSocket", url: "wss://free.evoluhq.com" }`.
|
|
124
|
-
*
|
|
125
|
-
* ### Example
|
|
126
|
-
*
|
|
127
|
-
* ```ts
|
|
128
|
-
* // Single WebSocket relay
|
|
129
|
-
* transports: [{ type: "WebSocket", url: "wss://relay1.example.com" }];
|
|
130
|
-
*
|
|
131
|
-
* // Multiple WebSocket relays for redundancy
|
|
132
|
-
* transports: [
|
|
133
|
-
* { type: "WebSocket", url: "wss://relay1.example.com" },
|
|
134
|
-
* { type: "WebSocket", url: "wss://relay2.example.com" },
|
|
135
|
-
* { type: "WebSocket", url: "wss://relay3.example.com" },
|
|
136
|
-
* ];
|
|
137
|
-
*
|
|
138
|
-
* // Local-only instance (no sync) - useful for device settings or when relay
|
|
139
|
-
* // URL will be provided later (e.g., after authentication), allowing users
|
|
140
|
-
* // to work offline before the app connects
|
|
141
|
-
* transports: [];
|
|
142
|
-
*
|
|
143
|
-
* // Using createOwnerWebSocketTransport helper for relay authentication
|
|
144
|
-
* transports: [
|
|
145
|
-
* createOwnerWebSocketTransport({
|
|
146
|
-
* url: "ws://localhost:4000",
|
|
147
|
-
* ownerId,
|
|
148
|
-
* }),
|
|
149
|
-
* ];
|
|
150
|
-
* ```
|
|
151
|
-
*/
|
|
152
|
-
readonly transports: ReadonlyArray<OwnerTransport>;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* External AppOwner to use when creating Evolu instance. Use this when you
|
|
156
|
-
* want to manage AppOwner creation and persistence externally (e.g., with
|
|
157
|
-
* your own authentication system). If omitted, Evolu will automatically
|
|
158
|
-
* create and persist an AppOwner locally.
|
|
159
|
-
*
|
|
160
|
-
* For device-specific settings and account management state, we can use a
|
|
161
|
-
* separate local-only Evolu instance via `transports: []`.
|
|
162
|
-
*
|
|
163
|
-
* ### Example
|
|
164
|
-
*
|
|
165
|
-
* ```ts
|
|
166
|
-
* const ConfigId = id("Config");
|
|
167
|
-
* type ConfigId = typeof ConfigId.Type;
|
|
168
|
-
*
|
|
169
|
-
* const DeviceSchema = {
|
|
170
|
-
* config: {
|
|
171
|
-
* id: ConfigId,
|
|
172
|
-
* key: NonEmptyString50,
|
|
173
|
-
* value: NonEmptyString50,
|
|
174
|
-
* },
|
|
175
|
-
* };
|
|
176
|
-
*
|
|
177
|
-
* // Local-only instance for device settings (no sync)
|
|
178
|
-
* const deviceEvolu = createEvolu(evoluReactWebDeps)(DeviceSchema, {
|
|
179
|
-
* name: SimpleName.orThrow("MyApp-Device"),
|
|
180
|
-
* transports: [], // No sync - stays local to device
|
|
181
|
-
* });
|
|
182
|
-
*
|
|
183
|
-
* // Main synced instance for user data
|
|
184
|
-
* const evolu = createEvolu(evoluReactWebDeps)(MainSchema, {
|
|
185
|
-
* name: SimpleName.orThrow("MyApp"),
|
|
186
|
-
* // Default transports for sync
|
|
187
|
-
* });
|
|
188
|
-
* ```
|
|
189
|
-
*/
|
|
190
|
-
readonly externalAppOwner?: AppOwner;
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Use in-memory SQLite database instead of persistent storage. Useful for
|
|
194
|
-
* testing or temporary data that doesn't need persistence.
|
|
195
|
-
*
|
|
196
|
-
* In-memory databases exist only in RAM and are completely destroyed when the
|
|
197
|
-
* process ends, making them forensically safe for sensitive data.
|
|
198
|
-
*
|
|
199
|
-
* The default value is: `false`.
|
|
200
|
-
*/
|
|
201
|
-
readonly inMemory?: boolean;
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Encryption key for the SQLite database.
|
|
205
|
-
*
|
|
206
|
-
* Note: If an unencrypted SQLite database already exists and you provide an
|
|
207
|
-
* encryptionKey, SQLite will throw an error.
|
|
208
|
-
*
|
|
209
|
-
* @experimental
|
|
210
|
-
*/
|
|
211
|
-
readonly encryptionKey?: EncryptionKey;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export const defaultDbConfig: DbConfig = {
|
|
215
|
-
name: SimpleName.orThrow("Evolu"),
|
|
216
|
-
transports: [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
|
|
217
|
-
maxDrift: 5 * 60 * 1000,
|
|
218
|
-
enableLogging: false,
|
|
219
|
-
};
|
|
105
|
+
export type DbWorker = Worker<DbWorkerInit>;
|
|
220
106
|
|
|
221
|
-
export
|
|
107
|
+
export interface DbWorkerInit {
|
|
108
|
+
readonly type: "Init";
|
|
109
|
+
readonly name: Name;
|
|
110
|
+
readonly consoleLevel: ConsoleLevel;
|
|
111
|
+
readonly sqliteSchema: SqliteSchema;
|
|
112
|
+
readonly encryptionKey: EncryptionKey;
|
|
113
|
+
readonly memoryOnly: boolean;
|
|
114
|
+
readonly port: NativeMessagePort<DbWorkerOutput, DbWorkerInput>;
|
|
115
|
+
}
|
|
222
116
|
|
|
223
|
-
export type CreateDbWorker = (
|
|
117
|
+
export type CreateDbWorker = () => DbWorker;
|
|
224
118
|
|
|
225
119
|
export interface CreateDbWorkerDep {
|
|
226
120
|
readonly createDbWorker: CreateDbWorker;
|
|
227
121
|
}
|
|
228
122
|
|
|
229
|
-
export type
|
|
230
|
-
|
|
231
|
-
readonly type: "init";
|
|
232
|
-
readonly config: DbConfig;
|
|
233
|
-
readonly dbSchema: DbSchema;
|
|
234
|
-
}
|
|
235
|
-
| {
|
|
236
|
-
readonly type: "getAppOwner";
|
|
237
|
-
}
|
|
238
|
-
| {
|
|
239
|
-
readonly type: "mutate";
|
|
240
|
-
readonly tabId: Id;
|
|
241
|
-
readonly changes: NonEmptyReadonlyArray<MutationChange>;
|
|
242
|
-
readonly onCompleteIds: ReadonlyArray<CallbackId>;
|
|
243
|
-
readonly subscribedQueries: ReadonlyArray<Query>;
|
|
244
|
-
}
|
|
245
|
-
| {
|
|
246
|
-
readonly type: "query";
|
|
247
|
-
readonly tabId: Id;
|
|
248
|
-
readonly queries: NonEmptyReadonlyArray<Query>;
|
|
249
|
-
}
|
|
250
|
-
| {
|
|
251
|
-
readonly type: "reset";
|
|
252
|
-
readonly onCompleteId: CallbackId;
|
|
253
|
-
readonly reload: boolean;
|
|
254
|
-
readonly restore?: {
|
|
255
|
-
readonly dbSchema: DbSchema;
|
|
256
|
-
readonly mnemonic: Mnemonic;
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
| {
|
|
260
|
-
readonly type: "ensureDbSchema";
|
|
261
|
-
readonly dbSchema: DbSchema;
|
|
262
|
-
}
|
|
263
|
-
| {
|
|
264
|
-
readonly type: "export";
|
|
265
|
-
readonly onCompleteId: CallbackId;
|
|
266
|
-
}
|
|
267
|
-
| {
|
|
268
|
-
readonly type: "useOwner";
|
|
269
|
-
readonly use: boolean;
|
|
270
|
-
readonly owner: SyncOwner;
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
export type DbWorkerOutput =
|
|
274
|
-
| {
|
|
275
|
-
readonly type: "onError";
|
|
276
|
-
readonly error:
|
|
277
|
-
| ProtocolError
|
|
278
|
-
| SqliteError
|
|
279
|
-
| SymmetricCryptoDecryptError
|
|
280
|
-
| TimestampError
|
|
281
|
-
| TransferableError;
|
|
282
|
-
}
|
|
283
|
-
| {
|
|
284
|
-
readonly type: "onGetAppOwner";
|
|
285
|
-
readonly appOwner: AppOwner;
|
|
286
|
-
}
|
|
287
|
-
| {
|
|
288
|
-
readonly type: "onQueryPatches";
|
|
289
|
-
readonly tabId: Id;
|
|
290
|
-
readonly queryPatches: ReadonlyArray<QueryPatches>;
|
|
291
|
-
readonly onCompleteIds: ReadonlyArray<CallbackId>;
|
|
292
|
-
}
|
|
293
|
-
| {
|
|
294
|
-
readonly type: "refreshQueries";
|
|
295
|
-
readonly tabId?: Id;
|
|
296
|
-
}
|
|
297
|
-
| {
|
|
298
|
-
readonly type: "onReset";
|
|
299
|
-
readonly onCompleteId: CallbackId;
|
|
300
|
-
readonly reload: boolean;
|
|
301
|
-
}
|
|
302
|
-
| {
|
|
303
|
-
readonly type: "onExport";
|
|
304
|
-
readonly onCompleteId: CallbackId;
|
|
305
|
-
readonly file: Uint8Array;
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
export type DbWorkerPlatformDeps = ConsoleDep &
|
|
123
|
+
export type DbWorkerDeps = WorkerDeps &
|
|
124
|
+
LeaderLockDep &
|
|
309
125
|
CreateSqliteDriverDep &
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
126
|
+
RandomBytesDep;
|
|
127
|
+
|
|
128
|
+
export const startDbWorker =
|
|
129
|
+
(self: WorkerSelf<DbWorkerInit>): Task<void, never, DbWorkerDeps> =>
|
|
130
|
+
async (_run) => {
|
|
131
|
+
await using stack = new AsyncDisposableStack();
|
|
132
|
+
const run = stack.use(_run.create());
|
|
133
|
+
const { deps } = run;
|
|
134
|
+
|
|
135
|
+
let initialized = false;
|
|
136
|
+
|
|
137
|
+
const initMessage = await run.orThrow(
|
|
138
|
+
callback<DbWorkerInit>(({ ok }) => {
|
|
139
|
+
self.onMessage = (message) => {
|
|
140
|
+
assert(!initialized, "DbWorker must be initialized only once");
|
|
141
|
+
initialized = true;
|
|
142
|
+
ok(message);
|
|
143
|
+
};
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
328
146
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
createInitializedWorkerWithHandlers<
|
|
333
|
-
DbWorkerInput,
|
|
334
|
-
DbWorkerOutput,
|
|
335
|
-
DbWorkerDeps
|
|
336
|
-
>({
|
|
337
|
-
init: async (initMessage, postMessage) => {
|
|
338
|
-
platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
|
|
339
|
-
|
|
340
|
-
const deps = await createDbWorkerDeps(
|
|
341
|
-
platformDeps,
|
|
342
|
-
initMessage,
|
|
343
|
-
postMessage,
|
|
344
|
-
);
|
|
147
|
+
const console = deps.console.child(initMessage.name).child("DbWorker");
|
|
148
|
+
console.setLevel(initMessage.consoleLevel);
|
|
149
|
+
console.info("start DbWorker");
|
|
345
150
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
151
|
+
const port = stack.use(
|
|
152
|
+
deps.createMessagePort<DbWorkerOutput, DbWorkerInput>(initMessage.port),
|
|
153
|
+
);
|
|
350
154
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
155
|
+
stack.defer(
|
|
156
|
+
deps.consoleStoreOutputEntry.subscribe(() => {
|
|
157
|
+
const entry = deps.consoleStoreOutputEntry.get();
|
|
158
|
+
if (entry) port.postMessage({ type: "OnConsoleEntry", entry });
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
355
161
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
initMessage: Extract<DbWorkerInput, { type: "init" }>,
|
|
359
|
-
postMessage: (msg: DbWorkerOutput) => void,
|
|
360
|
-
) => {
|
|
361
|
-
const sqlite = await createSqlite(platformDeps)(initMessage.config.name, {
|
|
362
|
-
memory: initMessage.config.inMemory ?? false,
|
|
363
|
-
encryptionKey: initMessage.config.encryptionKey ?? undefined,
|
|
364
|
-
});
|
|
365
|
-
if (!sqlite.ok) return sqlite;
|
|
162
|
+
stack.use(await run.orThrow(deps.leaderLock.lock(initMessage.name)));
|
|
163
|
+
if (stack.disposed) return ok();
|
|
366
164
|
|
|
367
|
-
|
|
165
|
+
port.postMessage({ type: "LeaderAcquired", name: initMessage.name });
|
|
368
166
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
167
|
+
const sqlite = stack.use(
|
|
168
|
+
await run.orThrow(
|
|
169
|
+
createSqlite(
|
|
170
|
+
initMessage.name,
|
|
171
|
+
initMessage.memoryOnly
|
|
172
|
+
? { mode: "memory" }
|
|
173
|
+
: { mode: "encrypted", encryptionKey: initMessage.encryptionKey },
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
console.debug("SQLite created");
|
|
372
178
|
|
|
373
|
-
const
|
|
179
|
+
const baseSqliteStorage = createBaseSqliteStorage({ sqlite, ...run.deps });
|
|
374
180
|
|
|
375
|
-
|
|
376
|
-
|
|
181
|
+
const dbDeps = {
|
|
182
|
+
...run.deps,
|
|
183
|
+
sqlite,
|
|
184
|
+
sqliteSchema: initMessage.sqliteSchema,
|
|
185
|
+
baseSqliteStorage,
|
|
186
|
+
timestampConfig: { maxDrift: defaultTimestampMaxDrift },
|
|
187
|
+
};
|
|
377
188
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}>(sql`select protocolVersion from evolu_version limit 1;`);
|
|
382
|
-
if (!currentVersion.ok) return currentVersion;
|
|
383
|
-
|
|
384
|
-
const configResult = deps.sqlite.exec<{
|
|
385
|
-
clock: TimestampBytes;
|
|
386
|
-
appOwnerId: OwnerId;
|
|
387
|
-
appOwnerEncryptionKey: OwnerEncryptionKey;
|
|
388
|
-
appOwnerWriteKey: OwnerWriteKey;
|
|
389
|
-
appOwnerMnemonic: Mnemonic | null;
|
|
390
|
-
}>(sql`
|
|
391
|
-
select
|
|
392
|
-
clock,
|
|
393
|
-
appOwnerId,
|
|
394
|
-
appOwnerEncryptionKey,
|
|
395
|
-
appOwnerWriteKey,
|
|
396
|
-
appOwnerMnemonic
|
|
397
|
-
from evolu_config
|
|
398
|
-
limit 1;
|
|
399
|
-
`);
|
|
400
|
-
if (!configResult.ok) return configResult;
|
|
189
|
+
const currentSchema = getEvoluSqliteSchema(dbDeps)();
|
|
190
|
+
const dbIsInitialized = "evolu_version" in currentSchema.tables;
|
|
191
|
+
const clock = createClock(dbDeps)(dbIsInitialized);
|
|
401
192
|
|
|
402
|
-
|
|
403
|
-
|
|
193
|
+
sqlite.transaction(() => {
|
|
194
|
+
if (!dbIsInitialized) initializeDb(dbDeps)(clock.get());
|
|
195
|
+
ensureSqliteSchema(dbDeps)(initMessage.sqliteSchema, currentSchema);
|
|
196
|
+
tryApplyQuarantinedMessages(dbDeps);
|
|
197
|
+
});
|
|
404
198
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
199
|
+
const storage = createClientStorage({ ...dbDeps, clock })({
|
|
200
|
+
onError: (error) => {
|
|
201
|
+
port.postMessage({ type: "OnError", error });
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// TODO: Call on dispose message.
|
|
206
|
+
const _moved = stack.move();
|
|
207
|
+
const runWithStorage = run.addDeps({ storage });
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* SharedWorker repeats sends until it gets a response, so handling here
|
|
211
|
+
* must be idempotent and ignore already processed IDs.
|
|
212
|
+
*
|
|
213
|
+
* TODO: Bound memory growth by evicting old IDs.
|
|
214
|
+
*/
|
|
215
|
+
const processedRequestIds = new Set<Id>();
|
|
216
|
+
|
|
217
|
+
port.onMessage = ({ callbackId, request }) => {
|
|
218
|
+
if (processedRequestIds.has(callbackId)) return;
|
|
219
|
+
processedRequestIds.add(callbackId);
|
|
220
|
+
|
|
221
|
+
const postQueuedResponse = (
|
|
222
|
+
response: ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
|
|
223
|
+
): void => {
|
|
224
|
+
port.postMessage(
|
|
225
|
+
{ type: "OnQueuedResponse", callbackId, response },
|
|
226
|
+
response.type === "ForEvolu" && response.message.type === "Export"
|
|
227
|
+
? [response.message.file.buffer]
|
|
228
|
+
: undefined,
|
|
229
|
+
);
|
|
411
230
|
};
|
|
412
231
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
232
|
+
switch (request.type) {
|
|
233
|
+
case "ForEvolu": {
|
|
234
|
+
switch (request.message.type) {
|
|
235
|
+
case "Mutate": {
|
|
236
|
+
const result = handleMutation({ ...dbDeps, clock })(
|
|
237
|
+
request.message,
|
|
238
|
+
);
|
|
239
|
+
if (!result.ok) {
|
|
240
|
+
port.postMessage({ type: "OnError", error: result.error });
|
|
241
|
+
} else {
|
|
242
|
+
postQueuedResponse({
|
|
243
|
+
type: "ForEvolu",
|
|
244
|
+
evoluPortId: request.evoluPortId,
|
|
245
|
+
message: result.value,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case "Query":
|
|
252
|
+
postQueuedResponse({
|
|
253
|
+
type: "ForEvolu",
|
|
254
|
+
evoluPortId: request.evoluPortId,
|
|
255
|
+
message: {
|
|
256
|
+
type: "Query",
|
|
257
|
+
rowsByQuery: loadQueries(dbDeps)(request.message.queries),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case "Export":
|
|
263
|
+
postQueuedResponse({
|
|
264
|
+
type: "ForEvolu",
|
|
265
|
+
evoluPortId: request.evoluPortId,
|
|
266
|
+
message: {
|
|
267
|
+
type: "Export",
|
|
268
|
+
file: dbDeps.sqlite.export(),
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
default:
|
|
274
|
+
exhaustiveCheck(request.message);
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
418
278
|
|
|
419
|
-
|
|
279
|
+
case "ForSharedWorker": {
|
|
280
|
+
switch (request.message.type) {
|
|
281
|
+
case "CreateSyncMessages": {
|
|
282
|
+
const protocolMessagesByOwnerId = new Map<
|
|
283
|
+
OwnerId,
|
|
284
|
+
ProtocolMessage
|
|
285
|
+
>();
|
|
286
|
+
|
|
287
|
+
for (const owner of request.message.owners) {
|
|
288
|
+
storage.setOwnerState(owner.encryptionKey);
|
|
289
|
+
const protocolMessage = createProtocolMessageForSync({
|
|
290
|
+
storage,
|
|
291
|
+
console,
|
|
292
|
+
})(owner.id, SubscriptionFlags.Subscribe);
|
|
293
|
+
|
|
294
|
+
if (protocolMessage) {
|
|
295
|
+
protocolMessagesByOwnerId.set(owner.id, protocolMessage);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
postQueuedResponse({
|
|
300
|
+
type: "ForSharedWorker",
|
|
301
|
+
message: {
|
|
302
|
+
type: "CreateSyncMessages",
|
|
303
|
+
protocolMessagesByOwnerId,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case "ApplySyncMessage": {
|
|
310
|
+
const { owner, inputMessage } = request.message;
|
|
311
|
+
|
|
312
|
+
runWithStorage<void, never>(async (run) => {
|
|
313
|
+
storage.setOwnerState(owner.encryptionKey);
|
|
314
|
+
|
|
315
|
+
const result = await run(
|
|
316
|
+
applyProtocolMessageAsClient(inputMessage, {
|
|
317
|
+
writeKey: owner.writeKey,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const didWriteMessages = storage.didWriteMessages();
|
|
322
|
+
|
|
323
|
+
postQueuedResponse({
|
|
324
|
+
type: "ForSharedWorker",
|
|
325
|
+
message: {
|
|
326
|
+
type: "ApplySyncMessage",
|
|
327
|
+
ownerId: owner.id,
|
|
328
|
+
didWriteMessages,
|
|
329
|
+
result,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return ok();
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
default:
|
|
339
|
+
exhaustiveCheck(request.message);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
420
343
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
344
|
+
default:
|
|
345
|
+
exhaustiveCheck(request);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
424
348
|
|
|
425
|
-
|
|
426
|
-
const result = ensureDbSchema(deps)(initMessage.dbSchema, dbSchema.value);
|
|
427
|
-
if (!result.ok) return result;
|
|
428
|
-
}
|
|
349
|
+
return ok();
|
|
429
350
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
351
|
+
// TODO: Add parallel stale-leader detection.
|
|
352
|
+
// Heartbeat is emitted by the active DB worker and sent to
|
|
353
|
+
// SharedWorker. SharedWorker tracks last-seen heartbeat per Evolu
|
|
354
|
+
// name and if silent for 10 seconds, it waits for another DB worker
|
|
355
|
+
// to announce itself alive and then routes requests to that worker.
|
|
356
|
+
};
|
|
434
357
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
transports: initMessage.config.transports,
|
|
444
|
-
onError: (error) => {
|
|
445
|
-
postMessage({ type: "onError", error });
|
|
446
|
-
},
|
|
447
|
-
onReceive: () => {
|
|
448
|
-
postMessage({ type: "refreshQueries" });
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
if (!sync.ok) return sync;
|
|
358
|
+
/**
|
|
359
|
+
* Hybrid Logical Clock. Keeps the current timestamp in memory to avoid frequent
|
|
360
|
+
* SQLite reads.
|
|
361
|
+
*/
|
|
362
|
+
interface Clock {
|
|
363
|
+
readonly get: () => Timestamp;
|
|
364
|
+
readonly save: (timestamp: Timestamp) => void;
|
|
365
|
+
}
|
|
452
366
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
367
|
+
interface ClockDep {
|
|
368
|
+
readonly clock: Clock;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const createClock =
|
|
372
|
+
(deps: RandomBytesDep & SqliteDep) =>
|
|
373
|
+
(dbIsInitialized: boolean): Clock => {
|
|
374
|
+
let currentTimestamp: Timestamp;
|
|
375
|
+
|
|
376
|
+
if (dbIsInitialized) {
|
|
377
|
+
const { rows } = deps.sqlite.exec<{ clock: TimestampBytes }>(sql`
|
|
378
|
+
select clock
|
|
379
|
+
from evolu_config
|
|
380
|
+
limit 1;
|
|
381
|
+
`);
|
|
382
|
+
assertNonEmptyReadonlyArray(rows);
|
|
383
|
+
currentTimestamp = timestampBytesToTimestamp(firstInArray(rows).clock);
|
|
384
|
+
} else {
|
|
385
|
+
currentTimestamp = createInitialTimestamp(deps);
|
|
459
386
|
}
|
|
460
387
|
|
|
461
|
-
|
|
388
|
+
return {
|
|
389
|
+
get: () => currentTimestamp,
|
|
462
390
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
};
|
|
391
|
+
save: (timestamp) => {
|
|
392
|
+
currentTimestamp = timestamp;
|
|
393
|
+
|
|
394
|
+
deps.sqlite.exec(sql.prepared`
|
|
395
|
+
update evolu_config
|
|
396
|
+
set "clock" = ${timestampToTimestampBytes(timestamp)};
|
|
397
|
+
`);
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
};
|
|
472
401
|
|
|
473
402
|
const initializeDb =
|
|
474
|
-
(
|
|
475
|
-
(
|
|
476
|
-
initialAppOwner: AppOwner,
|
|
477
|
-
initialClock: Timestamp,
|
|
478
|
-
): Result<void, SqliteError> => {
|
|
403
|
+
({ sqlite }: SqliteDep) =>
|
|
404
|
+
(initialClock: Timestamp): void => {
|
|
479
405
|
for (const query of [
|
|
480
|
-
// Never change structure to ensure all versions can read it.
|
|
481
406
|
sql`
|
|
482
407
|
create table evolu_version (
|
|
483
408
|
"protocolVersion" integer not null
|
|
@@ -492,32 +417,14 @@ const initializeDb =
|
|
|
492
417
|
|
|
493
418
|
sql`
|
|
494
419
|
create table evolu_config (
|
|
495
|
-
"clock" blob not null
|
|
496
|
-
"appOwnerId" text not null,
|
|
497
|
-
"appOwnerEncryptionKey" blob not null,
|
|
498
|
-
"appOwnerWriteKey" blob not null,
|
|
499
|
-
"appOwnerMnemonic" text
|
|
420
|
+
"clock" blob not null
|
|
500
421
|
)
|
|
501
422
|
strict;
|
|
502
423
|
`,
|
|
503
424
|
|
|
504
425
|
sql`
|
|
505
|
-
insert into evolu_config
|
|
506
|
-
|
|
507
|
-
"clock",
|
|
508
|
-
"appOwnerId",
|
|
509
|
-
"appOwnerEncryptionKey",
|
|
510
|
-
"appOwnerWriteKey",
|
|
511
|
-
"appOwnerMnemonic"
|
|
512
|
-
)
|
|
513
|
-
values
|
|
514
|
-
(
|
|
515
|
-
${timestampToTimestampBytes(initialClock)},
|
|
516
|
-
${initialAppOwner.id},
|
|
517
|
-
${initialAppOwner.encryptionKey},
|
|
518
|
-
${initialAppOwner.writeKey},
|
|
519
|
-
${initialAppOwner.mnemonic ?? null}
|
|
520
|
-
);
|
|
426
|
+
insert into evolu_config ("clock")
|
|
427
|
+
values (${timestampToTimestampBytes(initialClock)});
|
|
521
428
|
`,
|
|
522
429
|
|
|
523
430
|
/**
|
|
@@ -556,193 +463,560 @@ const initializeDb =
|
|
|
556
463
|
"timestamp" desc
|
|
557
464
|
);
|
|
558
465
|
`,
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Stores messages with unknown schema in a quarantine table.
|
|
469
|
+
*
|
|
470
|
+
* When a device receives sync messages containing tables or columns that
|
|
471
|
+
* don't exist in its current schema (e.g., from a newer app version),
|
|
472
|
+
* those messages are stored here instead of being discarded. This enables
|
|
473
|
+
* forward compatibility:
|
|
474
|
+
*
|
|
475
|
+
* 1. Unknown data is preserved and can be applied when the app is updated
|
|
476
|
+
* 2. Messages are still propagated to other devices that may understand them
|
|
477
|
+
* 3. Partial messages work - known columns go to app tables, unknown to
|
|
478
|
+
* quarantine
|
|
479
|
+
*
|
|
480
|
+
* The `union all` query in `readDbChange` combines `evolu_history` and
|
|
481
|
+
* this table, ensuring all data (known and unknown) is included when
|
|
482
|
+
* syncing to other devices.
|
|
483
|
+
*/
|
|
484
|
+
sql`
|
|
485
|
+
create table evolu_message_quarantine (
|
|
486
|
+
"ownerId" blob not null,
|
|
487
|
+
"timestamp" blob not null,
|
|
488
|
+
"table" text not null,
|
|
489
|
+
"id" blob not null,
|
|
490
|
+
"column" text not null,
|
|
491
|
+
"value" any,
|
|
492
|
+
primary key ("ownerId", "timestamp", "table", "id", "column")
|
|
493
|
+
)
|
|
494
|
+
strict;
|
|
495
|
+
`,
|
|
559
496
|
]) {
|
|
560
|
-
|
|
561
|
-
if (!result.ok) return result;
|
|
497
|
+
sqlite.exec(query);
|
|
562
498
|
}
|
|
563
499
|
|
|
564
|
-
|
|
565
|
-
|
|
500
|
+
createBaseSqliteStorageTables({ sqlite });
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const tryApplyQuarantinedMessages = (
|
|
504
|
+
deps: SqliteDep & SqliteSchemaDep,
|
|
505
|
+
): void => {
|
|
506
|
+
const rows = deps.sqlite.exec<{
|
|
507
|
+
readonly ownerId: OwnerIdBytes;
|
|
508
|
+
readonly timestamp: TimestampBytes;
|
|
509
|
+
readonly table: string;
|
|
510
|
+
readonly id: IdBytes;
|
|
511
|
+
readonly column: string;
|
|
512
|
+
readonly value: SqliteValue;
|
|
513
|
+
}>(sql`
|
|
514
|
+
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
515
|
+
from evolu_message_quarantine;
|
|
516
|
+
`);
|
|
566
517
|
|
|
567
|
-
|
|
518
|
+
for (const row of rows.rows) {
|
|
519
|
+
if (!validateColumnValue(deps)(row.table, row.column, row.value)) continue;
|
|
520
|
+
|
|
521
|
+
applyColumnChange(deps)(
|
|
522
|
+
row.ownerId,
|
|
523
|
+
ownerIdBytesToOwnerId(row.ownerId),
|
|
524
|
+
row.table,
|
|
525
|
+
row.id,
|
|
526
|
+
idBytesToId(row.id),
|
|
527
|
+
row.column,
|
|
528
|
+
row.value,
|
|
529
|
+
row.timestamp,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
deps.sqlite.exec(sql`
|
|
533
|
+
delete from evolu_message_quarantine
|
|
534
|
+
where
|
|
535
|
+
"ownerId" = ${row.ownerId}
|
|
536
|
+
and "timestamp" = ${row.timestamp}
|
|
537
|
+
and "table" = ${row.table}
|
|
538
|
+
and "id" = ${row.id}
|
|
539
|
+
and "column" = ${row.column};
|
|
540
|
+
`);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const validateColumnValue =
|
|
545
|
+
(deps: SqliteSchemaDep) =>
|
|
546
|
+
(table: string, column: string, _value: SqliteValue): boolean => {
|
|
547
|
+
const schemaColumns = getProperty(deps.sqliteSchema.tables, table);
|
|
548
|
+
return (
|
|
549
|
+
schemaColumns != null &&
|
|
550
|
+
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column))
|
|
551
|
+
);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const systemColumnsWithoutOwnerId = systemColumns.difference(
|
|
555
|
+
new Set(["ownerId"]),
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const applyColumnChange =
|
|
559
|
+
(deps: SqliteDep) =>
|
|
560
|
+
(
|
|
561
|
+
ownerIdBytes: OwnerIdBytes,
|
|
562
|
+
ownerId: OwnerId,
|
|
563
|
+
table: string,
|
|
564
|
+
idBytes: IdBytes,
|
|
565
|
+
id: Id,
|
|
566
|
+
column: string,
|
|
567
|
+
value: SqliteValue,
|
|
568
|
+
timestampBytes: TimestampBytes,
|
|
569
|
+
): void => {
|
|
570
|
+
deps.sqlite.exec(sql.prepared`
|
|
571
|
+
with
|
|
572
|
+
existingTimestamp as (
|
|
573
|
+
select 1
|
|
574
|
+
from evolu_history
|
|
575
|
+
where
|
|
576
|
+
"ownerId" = ${ownerIdBytes}
|
|
577
|
+
and "table" = ${table}
|
|
578
|
+
and "id" = ${idBytes}
|
|
579
|
+
and "column" = ${column}
|
|
580
|
+
and "timestamp" >= ${timestampBytes}
|
|
581
|
+
limit 1
|
|
582
|
+
)
|
|
583
|
+
insert into ${sql.identifier(table)}
|
|
584
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
585
|
+
select ${ownerId}, ${id}, ${value}
|
|
586
|
+
where not exists (select 1 from existingTimestamp)
|
|
587
|
+
on conflict ("ownerId", "id") do update
|
|
588
|
+
set ${sql.identifier(column)} = ${value}
|
|
589
|
+
where not exists (select 1 from existingTimestamp);
|
|
590
|
+
`);
|
|
591
|
+
|
|
592
|
+
deps.sqlite.exec(sql.prepared`
|
|
593
|
+
insert into evolu_history
|
|
594
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
595
|
+
values
|
|
596
|
+
(
|
|
597
|
+
${ownerIdBytes},
|
|
598
|
+
${table},
|
|
599
|
+
${idBytes},
|
|
600
|
+
${column},
|
|
601
|
+
${value},
|
|
602
|
+
${timestampBytes}
|
|
603
|
+
)
|
|
604
|
+
on conflict do nothing;
|
|
605
|
+
`);
|
|
568
606
|
};
|
|
569
607
|
|
|
570
608
|
/**
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* exist in its current schema (e.g., from a newer app version), those messages
|
|
575
|
-
* are stored here instead of being discarded. This enables forward
|
|
576
|
-
* compatibility:
|
|
577
|
-
*
|
|
578
|
-
* 1. Unknown data is preserved and can be applied when the app is updated
|
|
579
|
-
* 2. Messages are still propagated to other devices that may understand them
|
|
580
|
-
* 3. Partial messages work - known columns go to app tables, unknown to quarantine
|
|
581
|
-
*
|
|
582
|
-
* The `union all` query in `readDbChange` combines `evolu_history` and this
|
|
583
|
-
* table, ensuring all data (known and unknown) is included when syncing to
|
|
584
|
-
* other devices.
|
|
609
|
+
* The Db worker needs one object that can both satisfy sync code expecting
|
|
610
|
+
* {@link Storage}, expose {@link BaseSqliteStorage} helpers to the local
|
|
611
|
+
* implementation, and switch owner encryption keys between requests.
|
|
585
612
|
*/
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
create table if not exists evolu_message_quarantine (
|
|
591
|
-
"ownerId" blob not null,
|
|
592
|
-
"timestamp" blob not null,
|
|
593
|
-
"table" text not null,
|
|
594
|
-
"id" blob not null,
|
|
595
|
-
"column" text not null,
|
|
596
|
-
"value" any,
|
|
597
|
-
primary key ("ownerId", "timestamp", "table", "id", "column")
|
|
598
|
-
)
|
|
599
|
-
strict;
|
|
600
|
-
`);
|
|
601
|
-
if (!result.ok) return result;
|
|
602
|
-
return ok();
|
|
603
|
-
};
|
|
613
|
+
interface ClientStorage extends Storage, BaseSqliteStorage {
|
|
614
|
+
readonly setOwnerState: (encryptionKey: EncryptionKey) => void;
|
|
615
|
+
readonly didWriteMessages: () => boolean;
|
|
616
|
+
}
|
|
604
617
|
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
deps
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
618
|
+
const createClientStorage =
|
|
619
|
+
(
|
|
620
|
+
deps: BaseSqliteStorageDep &
|
|
621
|
+
ClockDep &
|
|
622
|
+
SqliteSchemaDep &
|
|
623
|
+
RandomBytesDep &
|
|
624
|
+
SqliteDep &
|
|
625
|
+
TimeDep &
|
|
626
|
+
TimestampConfigDep,
|
|
627
|
+
) =>
|
|
628
|
+
({
|
|
629
|
+
onError,
|
|
630
|
+
}: {
|
|
631
|
+
onError: (
|
|
632
|
+
error:
|
|
633
|
+
| ProtocolInvalidDataError
|
|
634
|
+
| ProtocolTimestampMismatchError
|
|
635
|
+
| DecryptWithXChaCha20Poly1305Error
|
|
636
|
+
| TimestampCounterOverflowError
|
|
637
|
+
| TimestampDriftError
|
|
638
|
+
| TimestampTimeOutOfRangeError,
|
|
639
|
+
) => void;
|
|
640
|
+
}): ClientStorage => {
|
|
641
|
+
let encryptionKey: EncryptionKey | null = null;
|
|
642
|
+
let didWriteMessages = false;
|
|
643
|
+
|
|
644
|
+
const getEncryptionKey = (): EncryptionKey => {
|
|
645
|
+
assert(encryptionKey != null, "ClientStorage encryption key must be set");
|
|
646
|
+
return encryptionKey;
|
|
647
|
+
};
|
|
612
648
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const syncChanges: Array<MutationChange> = [];
|
|
649
|
+
return {
|
|
650
|
+
...deps.baseSqliteStorage,
|
|
616
651
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
652
|
+
// DEV: ClientStorage was designed when Storage and Sync lived in the
|
|
653
|
+
// same file.
|
|
654
|
+
// This is safe because the worker handles one message at a time. We will
|
|
655
|
+
// refactor it later, we will probably have to change Protocol API.
|
|
656
|
+
setOwnerState: (nextEncryptionKey) => {
|
|
657
|
+
encryptionKey = nextEncryptionKey;
|
|
658
|
+
didWriteMessages = false;
|
|
659
|
+
},
|
|
626
660
|
|
|
627
|
-
|
|
628
|
-
const result = deps.sync.applyChanges(syncChanges);
|
|
629
|
-
if (!result.ok) return result;
|
|
630
|
-
}
|
|
661
|
+
didWriteMessages: () => didWriteMessages,
|
|
631
662
|
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
message.subscribedQueries,
|
|
636
|
-
);
|
|
637
|
-
if (!queryPatches.ok) return queryPatches;
|
|
638
|
-
|
|
639
|
-
// Update the tab that performed the mutation.
|
|
640
|
-
deps.postMessage({
|
|
641
|
-
type: "onQueryPatches",
|
|
642
|
-
tabId: message.tabId,
|
|
643
|
-
queryPatches: queryPatches.value,
|
|
644
|
-
onCompleteIds: message.onCompleteIds,
|
|
645
|
-
});
|
|
663
|
+
// Not implemented yet.
|
|
664
|
+
validateWriteKey: lazyFalse,
|
|
665
|
+
setWriteKey: lazyVoid,
|
|
646
666
|
|
|
647
|
-
|
|
648
|
-
|
|
667
|
+
writeMessages: (ownerIdBytes, encryptedMessages) => () => {
|
|
668
|
+
// TODO: Add quota checking for collaborative scenarios.
|
|
669
|
+
// When receiving messages from other owners via relay broadcast,
|
|
670
|
+
// check if this owner is within quota before accepting the data.
|
|
671
|
+
// This prevents an owner from exceeding storage limits when receiving
|
|
672
|
+
// data shared by other collaborators.
|
|
649
673
|
|
|
650
|
-
|
|
651
|
-
|
|
674
|
+
const messages: Array<CrdtMessage> = [];
|
|
675
|
+
const currentEncryptionKey = getEncryptionKey();
|
|
652
676
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
677
|
+
for (const message of encryptedMessages) {
|
|
678
|
+
const change = decryptAndDecodeDbChange(
|
|
679
|
+
message,
|
|
680
|
+
currentEncryptionKey,
|
|
681
|
+
);
|
|
682
|
+
if (!change.ok) {
|
|
683
|
+
onError(change.error);
|
|
684
|
+
return ok();
|
|
685
|
+
}
|
|
686
|
+
messages.push({ timestamp: message.timestamp, change: change.value });
|
|
687
|
+
}
|
|
658
688
|
|
|
659
|
-
|
|
660
|
-
const queryPatches = loadQueries(deps)(message.tabId, message.queries);
|
|
689
|
+
let clockTimestamp = deps.clock.get();
|
|
661
690
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
691
|
+
for (const message of messages) {
|
|
692
|
+
const nextTimestamp = receiveTimestamp(deps)(
|
|
693
|
+
clockTimestamp,
|
|
694
|
+
message.timestamp,
|
|
695
|
+
);
|
|
696
|
+
if (!nextTimestamp.ok) {
|
|
697
|
+
onError(nextTimestamp.error);
|
|
698
|
+
return ok();
|
|
699
|
+
}
|
|
700
|
+
clockTimestamp = nextTimestamp.value;
|
|
701
|
+
}
|
|
666
702
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
703
|
+
assertNonEmptyReadonlyArray(messages);
|
|
704
|
+
|
|
705
|
+
return deps.sqlite.transaction(() => {
|
|
706
|
+
applyMessages(deps)(ownerIdBytesToOwnerId(ownerIdBytes), messages);
|
|
707
|
+
deps.clock.save(clockTimestamp);
|
|
708
|
+
didWriteMessages = true;
|
|
709
|
+
return ok();
|
|
710
|
+
});
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
readDbChange: (ownerId, timestamp) => {
|
|
714
|
+
const result = deps.sqlite.exec<{
|
|
715
|
+
readonly table: string;
|
|
716
|
+
readonly id: IdBytes;
|
|
717
|
+
readonly column: string;
|
|
718
|
+
readonly value: SqliteValue;
|
|
719
|
+
}>(sql`
|
|
720
|
+
select "table", "id", "column", "value"
|
|
721
|
+
from evolu_history
|
|
722
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
723
|
+
union all
|
|
724
|
+
select "table", "id", "column", "value"
|
|
725
|
+
from evolu_message_quarantine
|
|
726
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
689
727
|
`);
|
|
690
|
-
if (!result.ok) return result;
|
|
691
|
-
}
|
|
692
728
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
729
|
+
const { rows } = result;
|
|
730
|
+
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
|
|
731
|
+
const firstRow = firstInArray(rows);
|
|
732
|
+
|
|
733
|
+
const values = createRecord<string, SqliteValue>();
|
|
734
|
+
let isInsert: DbChange["isInsert"] = false;
|
|
735
|
+
let isDelete: DbChange["isDelete"] = null;
|
|
736
|
+
|
|
737
|
+
for (const r of rows) {
|
|
738
|
+
switch (r.column) {
|
|
739
|
+
case "createdAt":
|
|
740
|
+
isInsert = true;
|
|
741
|
+
break;
|
|
742
|
+
case "updatedAt":
|
|
743
|
+
isInsert = false;
|
|
744
|
+
break;
|
|
745
|
+
case "isDeleted":
|
|
746
|
+
if (SqliteBoolean.is(r.value)) {
|
|
747
|
+
isDelete = sqliteBooleanToBoolean(r.value);
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
default:
|
|
751
|
+
values[r.column] = r.value;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const message: CrdtMessage = {
|
|
756
|
+
timestamp: timestampBytesToTimestamp(timestamp),
|
|
757
|
+
change: DbChange.orThrow({
|
|
758
|
+
table: firstRow.table,
|
|
759
|
+
id: idBytesToId(firstRow.id),
|
|
760
|
+
values,
|
|
761
|
+
isInsert,
|
|
762
|
+
isDelete,
|
|
763
|
+
}),
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
return encodeAndEncryptDbChange(deps)(message, getEncryptionKey());
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const handleMutation =
|
|
772
|
+
(
|
|
773
|
+
deps: BaseSqliteStorageDep &
|
|
774
|
+
ClockDep &
|
|
775
|
+
SqliteSchemaDep &
|
|
776
|
+
RandomBytesDep &
|
|
777
|
+
SqliteDep &
|
|
778
|
+
TimeDep &
|
|
779
|
+
TimestampConfigDep,
|
|
780
|
+
) =>
|
|
781
|
+
(
|
|
782
|
+
message: ExtractType<
|
|
783
|
+
ExtractType<DbWorkerInput["request"], "ForEvolu">["message"],
|
|
784
|
+
"Mutate"
|
|
785
|
+
>,
|
|
786
|
+
): Result<
|
|
787
|
+
{
|
|
788
|
+
readonly type: "Mutate";
|
|
789
|
+
readonly messagesByOwnerId: ReadonlyMap<
|
|
790
|
+
OwnerId,
|
|
791
|
+
NonEmptyReadonlyArray<CrdtMessage>
|
|
792
|
+
>;
|
|
793
|
+
readonly rowsByQuery: RowsByQueryMap;
|
|
794
|
+
},
|
|
795
|
+
| TimestampDriftError
|
|
796
|
+
| TimestampCounterOverflowError
|
|
797
|
+
| TimestampTimeOutOfRangeError
|
|
798
|
+
> =>
|
|
799
|
+
deps.sqlite.transaction(() => {
|
|
800
|
+
const messagesByOwnerId = new Map<OwnerId, NonEmptyArray<CrdtMessage>>();
|
|
801
|
+
let clockTimestamp = deps.clock.get();
|
|
802
|
+
let clockChanged = false;
|
|
803
|
+
|
|
804
|
+
for (const change of message.changes) {
|
|
805
|
+
if (change.table.startsWith("_")) {
|
|
806
|
+
applyLocalOnlyChange(deps)(change);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
811
|
+
if (!nextTimestamp.ok) return nextTimestamp;
|
|
812
|
+
|
|
813
|
+
clockTimestamp = nextTimestamp.value;
|
|
814
|
+
clockChanged = true;
|
|
696
815
|
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
|
|
816
|
+
const { ownerId, ...dbChange } = change;
|
|
817
|
+
const message: CrdtMessage = {
|
|
818
|
+
timestamp: clockTimestamp,
|
|
819
|
+
change: dbChange,
|
|
820
|
+
};
|
|
700
821
|
|
|
701
|
-
|
|
822
|
+
const messages = messagesByOwnerId.get(ownerId);
|
|
823
|
+
if (messages) messages.push(message);
|
|
824
|
+
else messagesByOwnerId.set(ownerId, [message]);
|
|
702
825
|
}
|
|
703
826
|
|
|
704
|
-
|
|
827
|
+
for (const [ownerId, messages] of messagesByOwnerId) {
|
|
828
|
+
applyMessages(deps)(ownerId, messages);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (clockChanged) deps.clock.save(clockTimestamp);
|
|
832
|
+
|
|
833
|
+
return ok({
|
|
834
|
+
type: "Mutate",
|
|
835
|
+
messagesByOwnerId,
|
|
836
|
+
rowsByQuery: loadQueries(deps)(message.subscribedQueries),
|
|
837
|
+
});
|
|
705
838
|
});
|
|
706
839
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
840
|
+
const applyLocalOnlyChange =
|
|
841
|
+
(deps: SqliteDep & TimeDep) =>
|
|
842
|
+
(change: MutationChange): void => {
|
|
843
|
+
if (change.isDelete) {
|
|
844
|
+
deps.sqlite.exec(sql`
|
|
845
|
+
delete from ${sql.identifier(change.table)}
|
|
846
|
+
where id = ${change.id};
|
|
847
|
+
`);
|
|
848
|
+
} else {
|
|
849
|
+
const ownerId = change.ownerId;
|
|
850
|
+
const columns = dbChangeToColumns(change, deps.time.now());
|
|
851
|
+
|
|
852
|
+
for (const [column, value] of columns) {
|
|
853
|
+
deps.sqlite.exec(sql.prepared`
|
|
854
|
+
insert into ${sql.identifier(change.table)}
|
|
855
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
856
|
+
values (${ownerId}, ${change.id}, ${value})
|
|
857
|
+
on conflict ("ownerId", "id") do update
|
|
858
|
+
set ${sql.identifier(column)} = ${value};
|
|
859
|
+
`);
|
|
860
|
+
}
|
|
710
861
|
}
|
|
862
|
+
};
|
|
711
863
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
});
|
|
717
|
-
},
|
|
864
|
+
const applyMessages =
|
|
865
|
+
(deps: BaseSqliteStorageDep & ClockDep & SqliteSchemaDep & SqliteDep) =>
|
|
866
|
+
(ownerId: OwnerId, messages: NonEmptyReadonlyArray<CrdtMessage>): void => {
|
|
867
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
718
868
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
869
|
+
const usage = getOwnerUsage(deps)(
|
|
870
|
+
ownerIdBytes,
|
|
871
|
+
timestampToTimestampBytes(firstInArray(messages).timestamp),
|
|
722
872
|
);
|
|
873
|
+
if (!usage.ok) return;
|
|
874
|
+
|
|
875
|
+
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
876
|
+
|
|
877
|
+
for (const { timestamp, change } of messages) {
|
|
878
|
+
const columns = dbChangeToColumns(change, timestamp.millis);
|
|
879
|
+
const idBytes = idToIdBytes(change.id);
|
|
880
|
+
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
881
|
+
|
|
882
|
+
for (const [column, value] of columns) {
|
|
883
|
+
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
884
|
+
applyColumnChange(deps)(
|
|
885
|
+
ownerIdBytes,
|
|
886
|
+
ownerId,
|
|
887
|
+
change.table,
|
|
888
|
+
idBytes,
|
|
889
|
+
change.id,
|
|
890
|
+
column,
|
|
891
|
+
value,
|
|
892
|
+
timestampBytes,
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
deps.sqlite.exec(sql.prepared`
|
|
896
|
+
insert into evolu_message_quarantine
|
|
897
|
+
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
898
|
+
values
|
|
899
|
+
(
|
|
900
|
+
${ownerIdBytes},
|
|
901
|
+
${timestampBytes},
|
|
902
|
+
${change.table},
|
|
903
|
+
${idBytes},
|
|
904
|
+
${column},
|
|
905
|
+
${value}
|
|
906
|
+
)
|
|
907
|
+
on conflict do nothing;
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
723
911
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
912
|
+
let strategy;
|
|
913
|
+
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(
|
|
914
|
+
timestampBytes,
|
|
915
|
+
firstTimestamp,
|
|
916
|
+
lastTimestamp,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
deps.baseSqliteStorage.insertTimestamp(
|
|
920
|
+
ownerIdBytes,
|
|
921
|
+
timestampBytes,
|
|
922
|
+
strategy,
|
|
923
|
+
);
|
|
727
924
|
}
|
|
728
|
-
},
|
|
729
925
|
|
|
730
|
-
|
|
731
|
-
|
|
926
|
+
/**
|
|
927
|
+
* TODO: Implement proper storedBytes tracking for client using received and
|
|
928
|
+
* sent encrypted message sizes.
|
|
929
|
+
*/
|
|
930
|
+
updateOwnerUsage(deps)(
|
|
931
|
+
ownerIdBytes,
|
|
932
|
+
onePositiveInt, // Placeholder until proper tracking implemented
|
|
933
|
+
firstTimestamp,
|
|
934
|
+
lastTimestamp,
|
|
935
|
+
);
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const dbChangeToColumns = (change: DbChange, now: Millis) => {
|
|
939
|
+
let values = objectToEntries(change.values);
|
|
940
|
+
|
|
941
|
+
// SystemColumns are not encoded in change.values.
|
|
942
|
+
values = appendToArray(values, [
|
|
943
|
+
change.isInsert ? "createdAt" : "updatedAt",
|
|
944
|
+
millisToDateIso(now),
|
|
945
|
+
]);
|
|
946
|
+
if (change.isDelete != null) {
|
|
947
|
+
values = appendToArray(values, [
|
|
948
|
+
"isDeleted",
|
|
949
|
+
booleanToSqliteBoolean(change.isDelete),
|
|
950
|
+
]);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return values;
|
|
954
|
+
};
|
|
732
955
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
956
|
+
const loadQueries =
|
|
957
|
+
(deps: SqliteDep) =>
|
|
958
|
+
(queries: Iterable<Query>): Map<Query, ReadonlyArray<SqliteRow>> => {
|
|
959
|
+
const rowsByQuery = new Map<Query, ReadonlyArray<SqliteRow>>();
|
|
960
|
+
|
|
961
|
+
for (const query of queries) {
|
|
962
|
+
const { rows } = deps.sqlite.exec(sqliteQueryStringToSqliteQuery(query));
|
|
963
|
+
rowsByQuery.set(query, rows);
|
|
736
964
|
}
|
|
737
965
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
onCompleteId: message.onCompleteId,
|
|
741
|
-
file: file.value,
|
|
742
|
-
});
|
|
743
|
-
},
|
|
966
|
+
return rowsByQuery;
|
|
967
|
+
};
|
|
744
968
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
969
|
+
// reset: (deps) => (message) => {
|
|
970
|
+
// const result = deps.sqlite.transaction(() => {
|
|
971
|
+
// const sqliteSchema = getSqliteSchema(deps)();
|
|
972
|
+
// if (!sqliteSchema.ok) return sqliteSchema;
|
|
973
|
+
|
|
974
|
+
// for (const tableName in sqliteSchema.value.tables) {
|
|
975
|
+
// /**
|
|
976
|
+
// * The dropped table is completely removed from the database schema and
|
|
977
|
+
// * the disk file. The table can not be recovered. All indices and
|
|
978
|
+
// * triggers associated with the table are also deleted.
|
|
979
|
+
// * https://sqlite.org/lang_droptable.html
|
|
980
|
+
// */
|
|
981
|
+
// const result = deps.sqlite.exec(sql`
|
|
982
|
+
// drop table ${sql.identifier(tableName)};
|
|
983
|
+
// `);
|
|
984
|
+
// if (!result.ok) return result;
|
|
985
|
+
// }
|
|
986
|
+
|
|
987
|
+
// if (message.restore) {
|
|
988
|
+
// const result = ensureSqliteSchema(deps)(message.restore.sqliteSchema);
|
|
989
|
+
// if (!result.ok) return result;
|
|
990
|
+
|
|
991
|
+
// const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
|
|
992
|
+
// const appOwner = createAppOwner(secret);
|
|
993
|
+
// const clock = createClock(deps)();
|
|
994
|
+
|
|
995
|
+
// return initializeDb(deps)(appOwner, clock.get());
|
|
996
|
+
// }
|
|
997
|
+
|
|
998
|
+
// return ok();
|
|
999
|
+
// });
|
|
1000
|
+
|
|
1001
|
+
// if (!result.ok) {
|
|
1002
|
+
// deps.postMessage({ type: "onError", error: result.error });
|
|
1003
|
+
// return;
|
|
1004
|
+
// }
|
|
1005
|
+
|
|
1006
|
+
// deps.postMessage({
|
|
1007
|
+
// type: "onReset",
|
|
1008
|
+
// onCompleteId: message.onCompleteId,
|
|
1009
|
+
// reload: message.reload,
|
|
1010
|
+
// });
|
|
1011
|
+
// },
|
|
1012
|
+
|
|
1013
|
+
// ensureSqliteSchema: (deps) => (message) => {
|
|
1014
|
+
// const result = deps.sqlite.transaction(() =>
|
|
1015
|
+
// ensureSqliteSchema(deps)(message.sqliteSchema),
|
|
1016
|
+
// );
|
|
1017
|
+
|
|
1018
|
+
// if (!result.ok) {
|
|
1019
|
+
// deps.postMessage({ type: "onError", error: result.error });
|
|
1020
|
+
// return;
|
|
1021
|
+
// }
|
|
1022
|
+
// },
|