@evolu/common 7.4.1 → 8.0.0-next.1
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 +162 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +200 -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 +29 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +329 -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 +2274 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1499 -348
- package/dist/src/Test.d.ts +150 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +97 -0
- package/dist/src/Time.d.ts +202 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +199 -88
- 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 +587 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +370 -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 +130 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +274 -113
- 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 +621 -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 +34 -49
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -177
- 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 +54 -58
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +127 -179
- 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 +37 -24
- 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 +429 -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 +520 -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 +3696 -740
- package/src/Test.ts +202 -0
- package/src/Time.ts +357 -147
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +866 -430
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +404 -167
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +867 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -824
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +441 -307
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +119 -273
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +211 -283
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- 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 -526
- package/src/Instances.ts +0 -92
- 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 -949
package/src/local-first/Db.ts
CHANGED
|
@@ -1,483 +1,406 @@
|
|
|
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, assertType } 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
|
+
getTimestampInsertStrategy,
|
|
81
|
+
readOwnerUsageOrDefault,
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
};
|
|
123
|
+
export type DbWorkerDeps = WorkerDeps & LeaderLockDep & CreateSqliteDriverDep;
|
|
124
|
+
|
|
125
|
+
export const startDbWorker =
|
|
126
|
+
(self: WorkerSelf<DbWorkerInit>): Task<void, never, DbWorkerDeps> =>
|
|
127
|
+
async (_run) => {
|
|
128
|
+
await using stack = new AsyncDisposableStack();
|
|
129
|
+
const run = stack.use(_run.create());
|
|
130
|
+
const { deps } = run;
|
|
131
|
+
|
|
132
|
+
let initialized = false;
|
|
133
|
+
|
|
134
|
+
const initMessage = await run.orThrow(
|
|
135
|
+
callback<DbWorkerInit>(({ ok }) => {
|
|
136
|
+
self.onMessage = (message) => {
|
|
137
|
+
assert(!initialized, "DbWorker must be initialized only once");
|
|
138
|
+
initialized = true;
|
|
139
|
+
ok(message);
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
307
143
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
RandomBytesDep &
|
|
312
|
-
RandomDep &
|
|
313
|
-
TimeDep;
|
|
314
|
-
|
|
315
|
-
type DbWorkerDeps = Omit<
|
|
316
|
-
DbWorkerPlatformDeps,
|
|
317
|
-
keyof CreateSqliteDriverDep | keyof CreateWebSocketDep
|
|
318
|
-
> &
|
|
319
|
-
AppOwnerDep &
|
|
320
|
-
GetQueryRowsCacheDep &
|
|
321
|
-
PostMessageDep &
|
|
322
|
-
SqliteDep &
|
|
323
|
-
SyncDep;
|
|
324
|
-
|
|
325
|
-
export interface PostMessageDep {
|
|
326
|
-
readonly postMessage: (message: DbWorkerOutput) => void;
|
|
327
|
-
}
|
|
144
|
+
const console = deps.console.child(initMessage.name).child("DbWorker");
|
|
145
|
+
console.setLevel(initMessage.consoleLevel);
|
|
146
|
+
console.info("start DbWorker");
|
|
328
147
|
|
|
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
|
-
);
|
|
148
|
+
const port = stack.use(
|
|
149
|
+
deps.createMessagePort<DbWorkerOutput, DbWorkerInput>(initMessage.port),
|
|
150
|
+
);
|
|
345
151
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
152
|
+
stack.defer(
|
|
153
|
+
deps.consoleStoreOutputEntry.subscribe(() => {
|
|
154
|
+
const entry = deps.consoleStoreOutputEntry.get();
|
|
155
|
+
if (entry) port.postMessage({ type: "OnConsoleEntry", entry });
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
350
158
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
handlers,
|
|
354
|
-
});
|
|
159
|
+
stack.use(await run.orThrow(deps.leaderLock.lock(initMessage.name)));
|
|
160
|
+
if (stack.disposed) return ok();
|
|
355
161
|
|
|
356
|
-
|
|
357
|
-
platformDeps: DbWorkerPlatformDeps,
|
|
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
|
+
port.postMessage({ type: "LeaderAcquired", name: initMessage.name });
|
|
366
163
|
|
|
367
|
-
|
|
164
|
+
const sqlite = stack.use(
|
|
165
|
+
await run.orThrow(
|
|
166
|
+
createSqlite(
|
|
167
|
+
initMessage.name,
|
|
168
|
+
initMessage.memoryOnly
|
|
169
|
+
? { mode: "memory" }
|
|
170
|
+
: { mode: "encrypted", encryptionKey: initMessage.encryptionKey },
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
console.debug("SQLite created");
|
|
368
175
|
|
|
369
|
-
|
|
370
|
-
const dbSchema = getDbSchema(deps)();
|
|
371
|
-
if (!dbSchema.ok) return dbSchema;
|
|
176
|
+
const baseSqliteStorage = createBaseSqliteStorage({ sqlite, ...run.deps });
|
|
372
177
|
|
|
373
|
-
const
|
|
178
|
+
const dbDeps = {
|
|
179
|
+
...run.deps,
|
|
180
|
+
sqlite,
|
|
181
|
+
sqliteSchema: initMessage.sqliteSchema,
|
|
182
|
+
baseSqliteStorage,
|
|
183
|
+
timestampConfig: { maxDrift: defaultTimestampMaxDrift },
|
|
184
|
+
};
|
|
374
185
|
|
|
375
|
-
|
|
376
|
-
|
|
186
|
+
const currentSchema = getEvoluSqliteSchema(dbDeps)();
|
|
187
|
+
const dbIsInitialized = "evolu_version" in currentSchema.tables;
|
|
188
|
+
const clock = createClock(dbDeps)(dbIsInitialized);
|
|
377
189
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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;
|
|
190
|
+
sqlite.transaction(() => {
|
|
191
|
+
if (!dbIsInitialized) initializeDb(dbDeps)(clock.get());
|
|
192
|
+
ensureSqliteSchema(dbDeps)(initMessage.sqliteSchema, currentSchema);
|
|
193
|
+
tryApplyQuarantinedMessages(dbDeps);
|
|
194
|
+
});
|
|
401
195
|
|
|
402
|
-
|
|
403
|
-
|
|
196
|
+
const storage = createClientStorage({ ...dbDeps, clock })({
|
|
197
|
+
onError: (error) => {
|
|
198
|
+
port.postMessage({ type: "OnError", error });
|
|
199
|
+
},
|
|
200
|
+
});
|
|
404
201
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
202
|
+
// TODO: Call on dispose message.
|
|
203
|
+
const _moved = stack.move();
|
|
204
|
+
const runWithStorage = run.addDeps({ storage });
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* SharedWorker retries until some leader replies, so this worker must
|
|
208
|
+
* ignore callback IDs it already completed.
|
|
209
|
+
*
|
|
210
|
+
* The Set intentionally lives for one leader's lifetime. If that leader tab
|
|
211
|
+
* closes, retries go to a new leader with a fresh Set. Revisit this only if
|
|
212
|
+
* memory pressure shows up; even millions of short callback IDs are
|
|
213
|
+
* acceptable here, and naive eviction could let a still-retried request run
|
|
214
|
+
* again.
|
|
215
|
+
*/
|
|
216
|
+
const processedRequestIds = new Set<Id>();
|
|
217
|
+
|
|
218
|
+
port.onMessage = ({ callbackId, request }) => {
|
|
219
|
+
if (processedRequestIds.has(callbackId)) return;
|
|
220
|
+
processedRequestIds.add(callbackId);
|
|
221
|
+
|
|
222
|
+
const postQueuedResponse = (
|
|
223
|
+
response: ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
|
|
224
|
+
): void => {
|
|
225
|
+
port.postMessage(
|
|
226
|
+
{ type: "OnQueuedResponse", callbackId, response },
|
|
227
|
+
response.type === "ForEvolu" && response.message.type === "Export"
|
|
228
|
+
? [response.message.file.buffer]
|
|
229
|
+
: undefined,
|
|
230
|
+
);
|
|
411
231
|
};
|
|
412
232
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
233
|
+
switch (request.type) {
|
|
234
|
+
case "ForEvolu": {
|
|
235
|
+
switch (request.message.type) {
|
|
236
|
+
case "Mutate": {
|
|
237
|
+
const result = handleMutation({ ...dbDeps, clock })(
|
|
238
|
+
request.message,
|
|
239
|
+
);
|
|
240
|
+
if (!result.ok) {
|
|
241
|
+
port.postMessage({ type: "OnError", error: result.error });
|
|
242
|
+
} else {
|
|
243
|
+
postQueuedResponse({
|
|
244
|
+
type: "ForEvolu",
|
|
245
|
+
evoluPortId: request.evoluPortId,
|
|
246
|
+
message: result.value,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case "Query":
|
|
253
|
+
postQueuedResponse({
|
|
254
|
+
type: "ForEvolu",
|
|
255
|
+
evoluPortId: request.evoluPortId,
|
|
256
|
+
message: {
|
|
257
|
+
type: "Query",
|
|
258
|
+
rowsByQuery: loadQueries(dbDeps)(request.message.queries),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case "Export":
|
|
264
|
+
postQueuedResponse({
|
|
265
|
+
type: "ForEvolu",
|
|
266
|
+
evoluPortId: request.evoluPortId,
|
|
267
|
+
message: {
|
|
268
|
+
type: "Export",
|
|
269
|
+
file: dbDeps.sqlite.export(),
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
default:
|
|
275
|
+
exhaustiveCheck(request.message);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
418
279
|
|
|
419
|
-
|
|
280
|
+
case "ForSharedWorker": {
|
|
281
|
+
switch (request.message.type) {
|
|
282
|
+
case "CreateSyncMessages": {
|
|
283
|
+
const protocolMessagesByOwnerId = new Map<
|
|
284
|
+
OwnerId,
|
|
285
|
+
ProtocolMessage
|
|
286
|
+
>();
|
|
287
|
+
|
|
288
|
+
for (const owner of request.message.owners) {
|
|
289
|
+
storage.setRequestContext(owner.encryptionKey);
|
|
290
|
+
const protocolMessage = createProtocolMessageForSync({
|
|
291
|
+
storage,
|
|
292
|
+
console,
|
|
293
|
+
})(owner.id, SubscriptionFlags.Subscribe);
|
|
294
|
+
protocolMessagesByOwnerId.set(owner.id, protocolMessage);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
postQueuedResponse({
|
|
298
|
+
type: "ForSharedWorker",
|
|
299
|
+
message: {
|
|
300
|
+
type: "CreateSyncMessages",
|
|
301
|
+
protocolMessagesByOwnerId,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case "ApplySyncMessage": {
|
|
308
|
+
const { owner, inputMessage } = request.message;
|
|
309
|
+
|
|
310
|
+
runWithStorage<void, never>(async (run) => {
|
|
311
|
+
storage.setRequestContext(owner.encryptionKey);
|
|
312
|
+
|
|
313
|
+
const result = await run(
|
|
314
|
+
applyProtocolMessageAsClient(inputMessage, {
|
|
315
|
+
writeKey: owner.writeKey,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const didWriteMessages = storage.didWriteMessages();
|
|
320
|
+
|
|
321
|
+
postQueuedResponse({
|
|
322
|
+
type: "ForSharedWorker",
|
|
323
|
+
message: {
|
|
324
|
+
type: "ApplySyncMessage",
|
|
325
|
+
ownerId: owner.id,
|
|
326
|
+
didWriteMessages,
|
|
327
|
+
result,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return ok();
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
default:
|
|
337
|
+
exhaustiveCheck(request.message);
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
420
341
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
342
|
+
default:
|
|
343
|
+
exhaustiveCheck(request);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
424
346
|
|
|
425
|
-
|
|
426
|
-
const result = ensureDbSchema(deps)(initMessage.dbSchema, dbSchema.value);
|
|
427
|
-
if (!result.ok) return result;
|
|
428
|
-
}
|
|
347
|
+
return ok();
|
|
429
348
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
349
|
+
// TODO: Add parallel stale-leader detection.
|
|
350
|
+
// Heartbeat is emitted by the active DB worker and sent to
|
|
351
|
+
// SharedWorker. SharedWorker tracks last-seen heartbeat per Evolu
|
|
352
|
+
// name and if silent for 10 seconds, it waits for another DB worker
|
|
353
|
+
// to announce itself alive and then routes requests to that worker.
|
|
354
|
+
};
|
|
434
355
|
|
|
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;
|
|
356
|
+
/**
|
|
357
|
+
* Hybrid Logical Clock. Keeps the current timestamp in memory to avoid frequent
|
|
358
|
+
* SQLite reads.
|
|
359
|
+
*/
|
|
360
|
+
interface Clock {
|
|
361
|
+
readonly get: () => Timestamp;
|
|
362
|
+
readonly save: (timestamp: Timestamp) => void;
|
|
363
|
+
}
|
|
452
364
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
365
|
+
interface ClockDep {
|
|
366
|
+
readonly clock: Clock;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const createClock =
|
|
370
|
+
(deps: RandomBytesDep & SqliteDep) =>
|
|
371
|
+
(dbIsInitialized: boolean): Clock => {
|
|
372
|
+
let currentTimestamp: Timestamp;
|
|
373
|
+
|
|
374
|
+
if (dbIsInitialized) {
|
|
375
|
+
const { rows } = deps.sqlite.exec<{ clock: TimestampBytes }>(sql`
|
|
376
|
+
select clock
|
|
377
|
+
from evolu_config
|
|
378
|
+
limit 1;
|
|
379
|
+
`);
|
|
380
|
+
assertNonEmptyReadonlyArray(rows);
|
|
381
|
+
currentTimestamp = timestampBytesToTimestamp(firstInArray(rows).clock);
|
|
382
|
+
} else {
|
|
383
|
+
currentTimestamp = createInitialTimestamp(deps);
|
|
459
384
|
}
|
|
460
385
|
|
|
461
|
-
|
|
386
|
+
return {
|
|
387
|
+
get: () => currentTimestamp,
|
|
462
388
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
};
|
|
389
|
+
save: (timestamp) => {
|
|
390
|
+
currentTimestamp = timestamp;
|
|
391
|
+
|
|
392
|
+
deps.sqlite.exec(sql.prepared`
|
|
393
|
+
update evolu_config
|
|
394
|
+
set "clock" = ${timestampToTimestampBytes(timestamp)};
|
|
395
|
+
`);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
};
|
|
472
399
|
|
|
473
400
|
const initializeDb =
|
|
474
|
-
(
|
|
475
|
-
(
|
|
476
|
-
initialAppOwner: AppOwner,
|
|
477
|
-
initialClock: Timestamp,
|
|
478
|
-
): Result<void, SqliteError> => {
|
|
401
|
+
({ sqlite }: SqliteDep) =>
|
|
402
|
+
(initialClock: Timestamp): void => {
|
|
479
403
|
for (const query of [
|
|
480
|
-
// Never change structure to ensure all versions can read it.
|
|
481
404
|
sql`
|
|
482
405
|
create table evolu_version (
|
|
483
406
|
"protocolVersion" integer not null
|
|
@@ -492,32 +415,14 @@ const initializeDb =
|
|
|
492
415
|
|
|
493
416
|
sql`
|
|
494
417
|
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
|
|
418
|
+
"clock" blob not null
|
|
500
419
|
)
|
|
501
420
|
strict;
|
|
502
421
|
`,
|
|
503
422
|
|
|
504
423
|
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
|
-
);
|
|
424
|
+
insert into evolu_config ("clock")
|
|
425
|
+
values (${timestampToTimestampBytes(initialClock)});
|
|
521
426
|
`,
|
|
522
427
|
|
|
523
428
|
/**
|
|
@@ -556,193 +461,558 @@ const initializeDb =
|
|
|
556
461
|
"timestamp" desc
|
|
557
462
|
);
|
|
558
463
|
`,
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Stores messages with unknown schema in a quarantine table.
|
|
467
|
+
*
|
|
468
|
+
* When a device receives sync messages containing tables or columns that
|
|
469
|
+
* don't exist in its current schema (e.g., from a newer app version),
|
|
470
|
+
* those messages are stored here instead of being discarded. This enables
|
|
471
|
+
* forward compatibility:
|
|
472
|
+
*
|
|
473
|
+
* 1. Unknown data is preserved and can be applied when the app is updated
|
|
474
|
+
* 2. Messages are still propagated to other devices that may understand them
|
|
475
|
+
* 3. Partial messages work - known columns go to app tables, unknown to
|
|
476
|
+
* quarantine
|
|
477
|
+
*
|
|
478
|
+
* The `union all` query in `readDbChange` combines `evolu_history` and
|
|
479
|
+
* this table, ensuring all data (known and unknown) is included when
|
|
480
|
+
* syncing to other devices.
|
|
481
|
+
*/
|
|
482
|
+
sql`
|
|
483
|
+
create table evolu_message_quarantine (
|
|
484
|
+
"ownerId" blob not null,
|
|
485
|
+
"timestamp" blob not null,
|
|
486
|
+
"table" text not null,
|
|
487
|
+
"id" blob not null,
|
|
488
|
+
"column" text not null,
|
|
489
|
+
"value" any,
|
|
490
|
+
primary key ("ownerId", "timestamp", "table", "id", "column")
|
|
491
|
+
)
|
|
492
|
+
strict;
|
|
493
|
+
`,
|
|
559
494
|
]) {
|
|
560
|
-
|
|
561
|
-
if (!result.ok) return result;
|
|
495
|
+
sqlite.exec(query);
|
|
562
496
|
}
|
|
563
497
|
|
|
564
|
-
|
|
565
|
-
|
|
498
|
+
createBaseSqliteStorageTables({ sqlite });
|
|
499
|
+
};
|
|
566
500
|
|
|
567
|
-
|
|
501
|
+
const tryApplyQuarantinedMessages = (
|
|
502
|
+
deps: SqliteDep & SqliteSchemaDep,
|
|
503
|
+
): void => {
|
|
504
|
+
const rows = deps.sqlite.exec<{
|
|
505
|
+
readonly ownerId: OwnerIdBytes;
|
|
506
|
+
readonly timestamp: TimestampBytes;
|
|
507
|
+
readonly table: string;
|
|
508
|
+
readonly id: IdBytes;
|
|
509
|
+
readonly column: string;
|
|
510
|
+
readonly value: SqliteValue;
|
|
511
|
+
}>(sql`
|
|
512
|
+
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
513
|
+
from evolu_message_quarantine;
|
|
514
|
+
`);
|
|
515
|
+
|
|
516
|
+
for (const row of rows.rows) {
|
|
517
|
+
if (!validateColumnValue(deps)(row.table, row.column, row.value)) continue;
|
|
518
|
+
|
|
519
|
+
applyColumnChange(deps)(
|
|
520
|
+
row.ownerId,
|
|
521
|
+
ownerIdBytesToOwnerId(row.ownerId),
|
|
522
|
+
row.table,
|
|
523
|
+
row.id,
|
|
524
|
+
idBytesToId(row.id),
|
|
525
|
+
row.column,
|
|
526
|
+
row.value,
|
|
527
|
+
row.timestamp,
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
deps.sqlite.exec(sql`
|
|
531
|
+
delete from evolu_message_quarantine
|
|
532
|
+
where
|
|
533
|
+
"ownerId" = ${row.ownerId}
|
|
534
|
+
and "timestamp" = ${row.timestamp}
|
|
535
|
+
and "table" = ${row.table}
|
|
536
|
+
and "id" = ${row.id}
|
|
537
|
+
and "column" = ${row.column};
|
|
538
|
+
`);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const validateColumnValue =
|
|
543
|
+
(deps: SqliteSchemaDep) =>
|
|
544
|
+
(table: string, column: string, _value: SqliteValue): boolean => {
|
|
545
|
+
const schemaColumns = getProperty(deps.sqliteSchema.tables, table);
|
|
546
|
+
return (
|
|
547
|
+
schemaColumns != null &&
|
|
548
|
+
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column))
|
|
549
|
+
);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const systemColumnsWithoutOwnerId = systemColumns.difference(
|
|
553
|
+
new Set(["ownerId"]),
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
const applyColumnChange =
|
|
557
|
+
(deps: SqliteDep) =>
|
|
558
|
+
(
|
|
559
|
+
ownerIdBytes: OwnerIdBytes,
|
|
560
|
+
ownerId: OwnerId,
|
|
561
|
+
table: string,
|
|
562
|
+
idBytes: IdBytes,
|
|
563
|
+
id: Id,
|
|
564
|
+
column: string,
|
|
565
|
+
value: SqliteValue,
|
|
566
|
+
timestampBytes: TimestampBytes,
|
|
567
|
+
): void => {
|
|
568
|
+
deps.sqlite.exec(sql.prepared`
|
|
569
|
+
with
|
|
570
|
+
existingTimestamp as (
|
|
571
|
+
select 1
|
|
572
|
+
from evolu_history
|
|
573
|
+
where
|
|
574
|
+
"ownerId" = ${ownerIdBytes}
|
|
575
|
+
and "table" = ${table}
|
|
576
|
+
and "id" = ${idBytes}
|
|
577
|
+
and "column" = ${column}
|
|
578
|
+
and "timestamp" >= ${timestampBytes}
|
|
579
|
+
limit 1
|
|
580
|
+
)
|
|
581
|
+
insert into ${sql.identifier(table)}
|
|
582
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
583
|
+
select ${ownerId}, ${id}, ${value}
|
|
584
|
+
where not exists (select 1 from existingTimestamp)
|
|
585
|
+
on conflict ("ownerId", "id") do update
|
|
586
|
+
set ${sql.identifier(column)} = ${value}
|
|
587
|
+
where not exists (select 1 from existingTimestamp);
|
|
588
|
+
`);
|
|
589
|
+
|
|
590
|
+
deps.sqlite.exec(sql.prepared`
|
|
591
|
+
insert into evolu_history
|
|
592
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
593
|
+
values
|
|
594
|
+
(
|
|
595
|
+
${ownerIdBytes},
|
|
596
|
+
${table},
|
|
597
|
+
${idBytes},
|
|
598
|
+
${column},
|
|
599
|
+
${value},
|
|
600
|
+
${timestampBytes}
|
|
601
|
+
)
|
|
602
|
+
on conflict do nothing;
|
|
603
|
+
`);
|
|
568
604
|
};
|
|
569
605
|
|
|
570
606
|
/**
|
|
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.
|
|
607
|
+
* The Db worker needs one object that can both satisfy sync code expecting
|
|
608
|
+
* {@link Storage}, expose {@link BaseSqliteStorage} helpers to the local
|
|
609
|
+
* implementation, and switch owner encryption keys between requests.
|
|
585
610
|
*/
|
|
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
|
-
};
|
|
611
|
+
interface ClientStorage extends Storage, BaseSqliteStorage {
|
|
612
|
+
readonly setRequestContext: (encryptionKey: EncryptionKey) => void;
|
|
613
|
+
readonly didWriteMessages: () => boolean;
|
|
614
|
+
}
|
|
604
615
|
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
deps
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
616
|
+
const createClientStorage =
|
|
617
|
+
(
|
|
618
|
+
deps: BaseSqliteStorageDep &
|
|
619
|
+
ClockDep &
|
|
620
|
+
SqliteSchemaDep &
|
|
621
|
+
RandomBytesDep &
|
|
622
|
+
SqliteDep &
|
|
623
|
+
TimeDep &
|
|
624
|
+
TimestampConfigDep,
|
|
625
|
+
) =>
|
|
626
|
+
({
|
|
627
|
+
onError,
|
|
628
|
+
}: {
|
|
629
|
+
onError: (
|
|
630
|
+
error:
|
|
631
|
+
| ProtocolInvalidDataError
|
|
632
|
+
| ProtocolTimestampMismatchError
|
|
633
|
+
| DecryptWithXChaCha20Poly1305Error
|
|
634
|
+
| TimestampCounterOverflowError
|
|
635
|
+
| TimestampDriftError
|
|
636
|
+
| TimestampTimeOutOfRangeError,
|
|
637
|
+
) => void;
|
|
638
|
+
}): ClientStorage => {
|
|
639
|
+
let encryptionKey: EncryptionKey | null = null;
|
|
640
|
+
let didWriteMessages = false;
|
|
641
|
+
|
|
642
|
+
const getEncryptionKey = (): EncryptionKey => {
|
|
643
|
+
assert(encryptionKey != null, "ClientStorage encryption key must be set");
|
|
644
|
+
return encryptionKey;
|
|
645
|
+
};
|
|
612
646
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const syncChanges: Array<MutationChange> = [];
|
|
647
|
+
return {
|
|
648
|
+
...deps.baseSqliteStorage,
|
|
616
649
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
650
|
+
// DEV: ClientStorage was designed when Storage and Sync lived in the
|
|
651
|
+
// same file.
|
|
652
|
+
// This is safe because the worker handles one message at a time. We will
|
|
653
|
+
// refactor it later, we will probably have to change Protocol API.
|
|
654
|
+
setRequestContext: (nextEncryptionKey) => {
|
|
655
|
+
encryptionKey = nextEncryptionKey;
|
|
656
|
+
didWriteMessages = false;
|
|
657
|
+
},
|
|
626
658
|
|
|
627
|
-
|
|
628
|
-
const result = deps.sync.applyChanges(syncChanges);
|
|
629
|
-
if (!result.ok) return result;
|
|
630
|
-
}
|
|
659
|
+
didWriteMessages: () => didWriteMessages,
|
|
631
660
|
|
|
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
|
-
});
|
|
661
|
+
// Not implemented yet.
|
|
662
|
+
validateWriteKey: lazyFalse,
|
|
663
|
+
setWriteKey: lazyVoid,
|
|
646
664
|
|
|
647
|
-
|
|
648
|
-
|
|
665
|
+
writeMessages: (ownerIdBytes, encryptedMessages) => () => {
|
|
666
|
+
// TODO: Add quota checking for collaborative scenarios.
|
|
667
|
+
// When receiving messages from other owners via relay broadcast,
|
|
668
|
+
// check if this owner is within quota before accepting the data.
|
|
669
|
+
// This prevents an owner from exceeding storage limits when receiving
|
|
670
|
+
// data shared by other collaborators.
|
|
649
671
|
|
|
650
|
-
|
|
651
|
-
|
|
672
|
+
const messages: Array<CrdtMessage> = [];
|
|
673
|
+
const currentEncryptionKey = getEncryptionKey();
|
|
652
674
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
675
|
+
for (const message of encryptedMessages) {
|
|
676
|
+
const change = decryptAndDecodeDbChange(
|
|
677
|
+
message,
|
|
678
|
+
currentEncryptionKey,
|
|
679
|
+
);
|
|
680
|
+
if (!change.ok) {
|
|
681
|
+
onError(change.error);
|
|
682
|
+
return ok();
|
|
683
|
+
}
|
|
684
|
+
messages.push({ timestamp: message.timestamp, change: change.value });
|
|
685
|
+
}
|
|
658
686
|
|
|
659
|
-
|
|
660
|
-
const queryPatches = loadQueries(deps)(message.tabId, message.queries);
|
|
687
|
+
let clockTimestamp = deps.clock.get();
|
|
661
688
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
689
|
+
for (const message of messages) {
|
|
690
|
+
const nextTimestamp = receiveTimestamp(deps)(
|
|
691
|
+
clockTimestamp,
|
|
692
|
+
message.timestamp,
|
|
693
|
+
);
|
|
694
|
+
if (!nextTimestamp.ok) {
|
|
695
|
+
onError(nextTimestamp.error);
|
|
696
|
+
return ok();
|
|
697
|
+
}
|
|
698
|
+
clockTimestamp = nextTimestamp.value;
|
|
699
|
+
}
|
|
666
700
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
701
|
+
assertNonEmptyReadonlyArray(messages);
|
|
702
|
+
|
|
703
|
+
return deps.sqlite.transaction(() => {
|
|
704
|
+
applyMessages(deps)(ownerIdBytesToOwnerId(ownerIdBytes), messages);
|
|
705
|
+
deps.clock.save(clockTimestamp);
|
|
706
|
+
didWriteMessages = true;
|
|
707
|
+
return ok();
|
|
708
|
+
});
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
readDbChange: (ownerId, timestamp) => {
|
|
712
|
+
const result = deps.sqlite.exec<{
|
|
713
|
+
readonly table: string;
|
|
714
|
+
readonly id: IdBytes;
|
|
715
|
+
readonly column: string;
|
|
716
|
+
readonly value: SqliteValue;
|
|
717
|
+
}>(sql`
|
|
718
|
+
select "table", "id", "column", "value"
|
|
719
|
+
from evolu_history
|
|
720
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
721
|
+
union all
|
|
722
|
+
select "table", "id", "column", "value"
|
|
723
|
+
from evolu_message_quarantine
|
|
724
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
689
725
|
`);
|
|
690
|
-
if (!result.ok) return result;
|
|
691
|
-
}
|
|
692
726
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
727
|
+
const { rows } = result;
|
|
728
|
+
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
|
|
729
|
+
const firstRow = firstInArray(rows);
|
|
730
|
+
|
|
731
|
+
const values = createRecord<string, SqliteValue>();
|
|
732
|
+
let isInsert: DbChange["isInsert"] = false;
|
|
733
|
+
let isDelete: DbChange["isDelete"] = null;
|
|
734
|
+
|
|
735
|
+
for (const r of rows) {
|
|
736
|
+
switch (r.column) {
|
|
737
|
+
case "createdAt":
|
|
738
|
+
isInsert = true;
|
|
739
|
+
break;
|
|
740
|
+
case "updatedAt":
|
|
741
|
+
isInsert = false;
|
|
742
|
+
break;
|
|
743
|
+
case "isDeleted":
|
|
744
|
+
assertType(SqliteBoolean, r.value);
|
|
745
|
+
isDelete = sqliteBooleanToBoolean(r.value);
|
|
746
|
+
break;
|
|
747
|
+
default:
|
|
748
|
+
values[r.column] = r.value;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const message: CrdtMessage = {
|
|
753
|
+
timestamp: timestampBytesToTimestamp(timestamp),
|
|
754
|
+
change: DbChange.orThrow({
|
|
755
|
+
table: firstRow.table,
|
|
756
|
+
id: idBytesToId(firstRow.id),
|
|
757
|
+
values,
|
|
758
|
+
isInsert,
|
|
759
|
+
isDelete,
|
|
760
|
+
}),
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
return encodeAndEncryptDbChange(deps)(message, getEncryptionKey());
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const handleMutation =
|
|
769
|
+
(
|
|
770
|
+
deps: BaseSqliteStorageDep &
|
|
771
|
+
ClockDep &
|
|
772
|
+
SqliteSchemaDep &
|
|
773
|
+
RandomBytesDep &
|
|
774
|
+
SqliteDep &
|
|
775
|
+
TimeDep &
|
|
776
|
+
TimestampConfigDep,
|
|
777
|
+
) =>
|
|
778
|
+
(
|
|
779
|
+
message: ExtractType<
|
|
780
|
+
ExtractType<DbWorkerInput["request"], "ForEvolu">["message"],
|
|
781
|
+
"Mutate"
|
|
782
|
+
>,
|
|
783
|
+
): Result<
|
|
784
|
+
{
|
|
785
|
+
readonly type: "Mutate";
|
|
786
|
+
readonly messagesByOwnerId: ReadonlyMap<
|
|
787
|
+
OwnerId,
|
|
788
|
+
NonEmptyReadonlyArray<CrdtMessage>
|
|
789
|
+
>;
|
|
790
|
+
readonly rowsByQuery: RowsByQueryMap;
|
|
791
|
+
},
|
|
792
|
+
| TimestampDriftError
|
|
793
|
+
| TimestampCounterOverflowError
|
|
794
|
+
| TimestampTimeOutOfRangeError
|
|
795
|
+
> =>
|
|
796
|
+
deps.sqlite.transaction(() => {
|
|
797
|
+
const messagesByOwnerId = new Map<OwnerId, NonEmptyArray<CrdtMessage>>();
|
|
798
|
+
let clockTimestamp = deps.clock.get();
|
|
799
|
+
let clockChanged = false;
|
|
800
|
+
|
|
801
|
+
for (const change of message.changes) {
|
|
802
|
+
if (change.table.startsWith("_")) {
|
|
803
|
+
applyLocalOnlyChange(deps)(change);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
808
|
+
if (!nextTimestamp.ok) return nextTimestamp;
|
|
809
|
+
|
|
810
|
+
clockTimestamp = nextTimestamp.value;
|
|
811
|
+
clockChanged = true;
|
|
812
|
+
|
|
813
|
+
const { ownerId, ...dbChange } = change;
|
|
814
|
+
const message: CrdtMessage = {
|
|
815
|
+
timestamp: clockTimestamp,
|
|
816
|
+
change: dbChange,
|
|
817
|
+
};
|
|
696
818
|
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
819
|
+
const messages = messagesByOwnerId.get(ownerId);
|
|
820
|
+
if (messages) messages.push(message);
|
|
821
|
+
else messagesByOwnerId.set(ownerId, [message]);
|
|
822
|
+
}
|
|
700
823
|
|
|
701
|
-
|
|
824
|
+
for (const [ownerId, messages] of messagesByOwnerId) {
|
|
825
|
+
applyMessages(deps)(ownerId, messages);
|
|
702
826
|
}
|
|
703
827
|
|
|
704
|
-
|
|
828
|
+
if (clockChanged) deps.clock.save(clockTimestamp);
|
|
829
|
+
|
|
830
|
+
return ok({
|
|
831
|
+
type: "Mutate",
|
|
832
|
+
messagesByOwnerId,
|
|
833
|
+
rowsByQuery: loadQueries(deps)(message.subscribedQueries),
|
|
834
|
+
});
|
|
705
835
|
});
|
|
706
836
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
837
|
+
const applyLocalOnlyChange =
|
|
838
|
+
(deps: SqliteDep & TimeDep) =>
|
|
839
|
+
(change: MutationChange): void => {
|
|
840
|
+
if (change.isDelete) {
|
|
841
|
+
deps.sqlite.exec(sql`
|
|
842
|
+
delete from ${sql.identifier(change.table)}
|
|
843
|
+
where "ownerId" = ${change.ownerId} and "id" = ${change.id};
|
|
844
|
+
`);
|
|
845
|
+
} else {
|
|
846
|
+
const ownerId = change.ownerId;
|
|
847
|
+
const columns = dbChangeToColumns(change, deps.time.now());
|
|
848
|
+
|
|
849
|
+
for (const [column, value] of columns) {
|
|
850
|
+
deps.sqlite.exec(sql.prepared`
|
|
851
|
+
insert into ${sql.identifier(change.table)}
|
|
852
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
853
|
+
values (${ownerId}, ${change.id}, ${value})
|
|
854
|
+
on conflict ("ownerId", "id") do update
|
|
855
|
+
set ${sql.identifier(column)} = ${value};
|
|
856
|
+
`);
|
|
857
|
+
}
|
|
710
858
|
}
|
|
859
|
+
};
|
|
711
860
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
});
|
|
717
|
-
},
|
|
861
|
+
const applyMessages =
|
|
862
|
+
(deps: BaseSqliteStorageDep & ClockDep & SqliteSchemaDep & SqliteDep) =>
|
|
863
|
+
(ownerId: OwnerId, messages: NonEmptyReadonlyArray<CrdtMessage>): void => {
|
|
864
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
718
865
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
866
|
+
const usage = readOwnerUsageOrDefault(deps)(
|
|
867
|
+
ownerIdBytes,
|
|
868
|
+
timestampToTimestampBytes(firstInArray(messages).timestamp),
|
|
722
869
|
);
|
|
723
870
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
871
|
+
let { firstTimestamp, lastTimestamp } = usage;
|
|
872
|
+
|
|
873
|
+
for (const { timestamp, change } of messages) {
|
|
874
|
+
const columns = dbChangeToColumns(change, timestamp.millis);
|
|
875
|
+
const idBytes = idToIdBytes(change.id);
|
|
876
|
+
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
877
|
+
|
|
878
|
+
for (const [column, value] of columns) {
|
|
879
|
+
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
880
|
+
applyColumnChange(deps)(
|
|
881
|
+
ownerIdBytes,
|
|
882
|
+
ownerId,
|
|
883
|
+
change.table,
|
|
884
|
+
idBytes,
|
|
885
|
+
change.id,
|
|
886
|
+
column,
|
|
887
|
+
value,
|
|
888
|
+
timestampBytes,
|
|
889
|
+
);
|
|
890
|
+
} else {
|
|
891
|
+
deps.sqlite.exec(sql.prepared`
|
|
892
|
+
insert into evolu_message_quarantine
|
|
893
|
+
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
894
|
+
values
|
|
895
|
+
(
|
|
896
|
+
${ownerIdBytes},
|
|
897
|
+
${timestampBytes},
|
|
898
|
+
${change.table},
|
|
899
|
+
${idBytes},
|
|
900
|
+
${column},
|
|
901
|
+
${value}
|
|
902
|
+
)
|
|
903
|
+
on conflict do nothing;
|
|
904
|
+
`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
729
907
|
|
|
730
|
-
|
|
731
|
-
|
|
908
|
+
let strategy;
|
|
909
|
+
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(
|
|
910
|
+
timestampBytes,
|
|
911
|
+
firstTimestamp,
|
|
912
|
+
lastTimestamp,
|
|
913
|
+
);
|
|
732
914
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
915
|
+
deps.baseSqliteStorage.insertTimestamp(
|
|
916
|
+
ownerIdBytes,
|
|
917
|
+
timestampBytes,
|
|
918
|
+
strategy,
|
|
919
|
+
);
|
|
736
920
|
}
|
|
737
921
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
922
|
+
/**
|
|
923
|
+
* TODO: Implement proper storedBytes tracking for client using received and
|
|
924
|
+
* sent encrypted message sizes.
|
|
925
|
+
*/
|
|
926
|
+
updateOwnerUsage(deps)(
|
|
927
|
+
ownerIdBytes,
|
|
928
|
+
onePositiveInt, // Placeholder until proper tracking implemented
|
|
929
|
+
firstTimestamp,
|
|
930
|
+
lastTimestamp,
|
|
931
|
+
);
|
|
932
|
+
};
|
|
744
933
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
934
|
+
const dbChangeToColumns = (change: DbChange, now: Millis) => {
|
|
935
|
+
let values = objectToEntries(change.values);
|
|
936
|
+
|
|
937
|
+
// SystemColumns are not encoded in change.values.
|
|
938
|
+
values = appendToArray(values, [
|
|
939
|
+
change.isInsert ? "createdAt" : "updatedAt",
|
|
940
|
+
millisToDateIso(now),
|
|
941
|
+
]);
|
|
942
|
+
if (change.isDelete != null) {
|
|
943
|
+
values = appendToArray(values, [
|
|
944
|
+
"isDeleted",
|
|
945
|
+
booleanToSqliteBoolean(change.isDelete),
|
|
946
|
+
]);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return values;
|
|
748
950
|
};
|
|
951
|
+
|
|
952
|
+
const loadQueries =
|
|
953
|
+
(deps: SqliteDep) =>
|
|
954
|
+
(queries: Iterable<Query>): Map<Query, ReadonlyArray<SqliteRow>> => {
|
|
955
|
+
const rowsByQuery = new Map<Query, ReadonlyArray<SqliteRow>>();
|
|
956
|
+
|
|
957
|
+
for (const query of queries) {
|
|
958
|
+
const { rows } = deps.sqlite.exec(sqliteQueryStringToSqliteQuery(query));
|
|
959
|
+
rowsByQuery.set(query, rows);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return rowsByQuery;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// reset: (deps) => (message) => {
|
|
966
|
+
// const result = deps.sqlite.transaction(() => {
|
|
967
|
+
// const sqliteSchema = getSqliteSchema(deps)();
|
|
968
|
+
// if (!sqliteSchema.ok) return sqliteSchema;
|
|
969
|
+
|
|
970
|
+
// for (const tableName in sqliteSchema.value.tables) {
|
|
971
|
+
// /**
|
|
972
|
+
// * The dropped table is completely removed from the database schema and
|
|
973
|
+
// * the disk file. The table can not be recovered. All indices and
|
|
974
|
+
// * triggers associated with the table are also deleted.
|
|
975
|
+
// * https://sqlite.org/lang_droptable.html
|
|
976
|
+
// */
|
|
977
|
+
// const result = deps.sqlite.exec(sql`
|
|
978
|
+
// drop table ${sql.identifier(tableName)};
|
|
979
|
+
// `);
|
|
980
|
+
// if (!result.ok) return result;
|
|
981
|
+
// }
|
|
982
|
+
|
|
983
|
+
// if (message.restore) {
|
|
984
|
+
// const result = ensureSqliteSchema(deps)(message.restore.sqliteSchema);
|
|
985
|
+
// if (!result.ok) return result;
|
|
986
|
+
|
|
987
|
+
// const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
|
|
988
|
+
// const appOwner = createAppOwner(secret);
|
|
989
|
+
// const clock = createClock(deps)();
|
|
990
|
+
|
|
991
|
+
// return initializeDb(deps)(appOwner, clock.get());
|
|
992
|
+
// }
|
|
993
|
+
|
|
994
|
+
// return ok();
|
|
995
|
+
// });
|
|
996
|
+
|
|
997
|
+
// if (!result.ok) {
|
|
998
|
+
// deps.postMessage({ type: "onError", error: result.error });
|
|
999
|
+
// return;
|
|
1000
|
+
// }
|
|
1001
|
+
|
|
1002
|
+
// deps.postMessage({
|
|
1003
|
+
// type: "onReset",
|
|
1004
|
+
// onCompleteId: message.onCompleteId,
|
|
1005
|
+
// reload: message.reload,
|
|
1006
|
+
// });
|
|
1007
|
+
// },
|
|
1008
|
+
|
|
1009
|
+
// ensureSqliteSchema: (deps) => (message) => {
|
|
1010
|
+
// const result = deps.sqlite.transaction(() =>
|
|
1011
|
+
// ensureSqliteSchema(deps)(message.sqliteSchema),
|
|
1012
|
+
// );
|
|
1013
|
+
|
|
1014
|
+
// if (!result.ok) {
|
|
1015
|
+
// deps.postMessage({ type: "onError", error: result.error });
|
|
1016
|
+
// return;
|
|
1017
|
+
// }
|
|
1018
|
+
// },
|