@evolu/common 7.4.1 → 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 +851 -430
- 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 -824
- 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 +206 -277
- 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 -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/Sync.ts
DELETED
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendToArray,
|
|
3
|
-
firstInArray,
|
|
4
|
-
isNonEmptyReadonlyArray,
|
|
5
|
-
NonEmptyArray,
|
|
6
|
-
NonEmptyReadonlyArray,
|
|
7
|
-
} from "../Array.js";
|
|
8
|
-
import { assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
9
|
-
import { Brand } from "../Brand.js";
|
|
10
|
-
import { ConsoleDep } from "../Console.js";
|
|
11
|
-
import {
|
|
12
|
-
RandomBytesDep,
|
|
13
|
-
SymmetricCryptoDecryptError,
|
|
14
|
-
SymmetricCryptoDep,
|
|
15
|
-
} from "../Crypto.js";
|
|
16
|
-
import { createTransferableError, TransferableError } from "../Error.js";
|
|
17
|
-
import { constFalse, constTrue } from "../Function.js";
|
|
18
|
-
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
19
|
-
import { RandomDep } from "../Random.js";
|
|
20
|
-
import { createResources } from "../Resources.js";
|
|
21
|
-
import { err, ok, Result } from "../Result.js";
|
|
22
|
-
import {
|
|
23
|
-
booleanToSqliteBoolean,
|
|
24
|
-
sql,
|
|
25
|
-
SqliteBoolean,
|
|
26
|
-
sqliteBooleanToBoolean,
|
|
27
|
-
SqliteDep,
|
|
28
|
-
SqliteError,
|
|
29
|
-
SqliteValue,
|
|
30
|
-
} from "../Sqlite.js";
|
|
31
|
-
import { AbortError, createMutex } from "../Task.js";
|
|
32
|
-
import { TimeDep } from "../Time.js";
|
|
33
|
-
import {
|
|
34
|
-
DateIso,
|
|
35
|
-
Id,
|
|
36
|
-
IdBytes,
|
|
37
|
-
idBytesToId,
|
|
38
|
-
idToIdBytes,
|
|
39
|
-
PositiveInt,
|
|
40
|
-
} from "../Type.js";
|
|
41
|
-
import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
|
|
42
|
-
import {
|
|
43
|
-
AppOwner,
|
|
44
|
-
AppOwnerDep,
|
|
45
|
-
Owner,
|
|
46
|
-
OwnerId,
|
|
47
|
-
OwnerIdBytes,
|
|
48
|
-
ownerIdBytesToOwnerId,
|
|
49
|
-
ownerIdToOwnerIdBytes,
|
|
50
|
-
OwnerTransport,
|
|
51
|
-
ReadonlyOwner,
|
|
52
|
-
} from "./Owner.js";
|
|
53
|
-
import {
|
|
54
|
-
applyProtocolMessageAsClient,
|
|
55
|
-
createProtocolMessageForSync,
|
|
56
|
-
createProtocolMessageForUnsubscribe,
|
|
57
|
-
createProtocolMessageFromCrdtMessages,
|
|
58
|
-
decryptAndDecodeDbChange,
|
|
59
|
-
encodeAndEncryptDbChange,
|
|
60
|
-
ProtocolError,
|
|
61
|
-
ProtocolInvalidDataError,
|
|
62
|
-
ProtocolTimestampMismatchError,
|
|
63
|
-
SubscriptionFlags,
|
|
64
|
-
} from "./Protocol.js";
|
|
65
|
-
import { DbSchemaDep, MutationChange, systemColumns } from "./Schema.js";
|
|
66
|
-
import {
|
|
67
|
-
BaseSqliteStorage,
|
|
68
|
-
CrdtMessage,
|
|
69
|
-
createBaseSqliteStorage,
|
|
70
|
-
DbChange,
|
|
71
|
-
getOwnerUsage,
|
|
72
|
-
getTimestampInsertStrategy,
|
|
73
|
-
Storage,
|
|
74
|
-
StorageWriteError,
|
|
75
|
-
updateOwnerUsage,
|
|
76
|
-
} from "./Storage.js";
|
|
77
|
-
import {
|
|
78
|
-
createInitialTimestamp,
|
|
79
|
-
Millis,
|
|
80
|
-
receiveTimestamp,
|
|
81
|
-
sendTimestamp,
|
|
82
|
-
Timestamp,
|
|
83
|
-
TimestampBytes,
|
|
84
|
-
timestampBytesToTimestamp,
|
|
85
|
-
TimestampConfigDep,
|
|
86
|
-
TimestampCounterOverflowError,
|
|
87
|
-
TimestampDriftError,
|
|
88
|
-
TimestampTimeOutOfRangeError,
|
|
89
|
-
timestampToDateIso,
|
|
90
|
-
timestampToTimestampBytes,
|
|
91
|
-
} from "./Timestamp.js";
|
|
92
|
-
|
|
93
|
-
export interface Sync extends Disposable {
|
|
94
|
-
/**
|
|
95
|
-
* Assigns or removes an owner to/from transports with reference counting.
|
|
96
|
-
*
|
|
97
|
-
* Owners are only synced if assigned to at least one transport. Uses
|
|
98
|
-
* `owner.transports` or falls back to {@link SyncConfig} transports. Multiple
|
|
99
|
-
* calls increment/decrement reference counts (useful for React Hooks).
|
|
100
|
-
*/
|
|
101
|
-
readonly useOwner: (use: boolean, owner: SyncOwner) => void;
|
|
102
|
-
|
|
103
|
-
readonly applyChanges: (
|
|
104
|
-
changes: NonEmptyReadonlyArray<MutationChange>,
|
|
105
|
-
) => Result<
|
|
106
|
-
void,
|
|
107
|
-
| SqliteError
|
|
108
|
-
| TimestampCounterOverflowError
|
|
109
|
-
| TimestampDriftError
|
|
110
|
-
| TimestampTimeOutOfRangeError
|
|
111
|
-
>;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface SyncDep {
|
|
115
|
-
readonly sync: Sync;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Represents an owner for sync operations.
|
|
120
|
-
*
|
|
121
|
-
* Includes readonly owner fields plus optional write key (for clients that
|
|
122
|
-
* write) and optional transports to override SyncConfig transports per owner.
|
|
123
|
-
*/
|
|
124
|
-
export interface SyncOwner extends ReadonlyOwner {
|
|
125
|
-
readonly writeKey?: Owner["writeKey"];
|
|
126
|
-
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface SyncConfig {
|
|
130
|
-
readonly appOwner: AppOwner;
|
|
131
|
-
|
|
132
|
-
readonly transports: ReadonlyArray<OwnerTransport>;
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Delay in milliseconds before disposing unused WebSocket connections.
|
|
136
|
-
* Defaults to 100ms.
|
|
137
|
-
*/
|
|
138
|
-
readonly disposalDelayMs?: number;
|
|
139
|
-
|
|
140
|
-
readonly onError: (
|
|
141
|
-
error:
|
|
142
|
-
| ProtocolError
|
|
143
|
-
| ProtocolInvalidDataError
|
|
144
|
-
| ProtocolTimestampMismatchError
|
|
145
|
-
| SqliteError
|
|
146
|
-
| SymmetricCryptoDecryptError
|
|
147
|
-
| TimestampCounterOverflowError
|
|
148
|
-
| TimestampDriftError
|
|
149
|
-
| TimestampTimeOutOfRangeError
|
|
150
|
-
| TransferableError,
|
|
151
|
-
) => void;
|
|
152
|
-
|
|
153
|
-
readonly onReceive: () => void;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export const createSync =
|
|
157
|
-
(
|
|
158
|
-
deps: ClockDep &
|
|
159
|
-
ConsoleDep &
|
|
160
|
-
CreateWebSocketDep &
|
|
161
|
-
DbSchemaDep &
|
|
162
|
-
// PostMessageDep &
|
|
163
|
-
RandomBytesDep &
|
|
164
|
-
RandomDep &
|
|
165
|
-
SqliteDep &
|
|
166
|
-
SymmetricCryptoDep &
|
|
167
|
-
TimeDep &
|
|
168
|
-
TimestampConfigDep,
|
|
169
|
-
) =>
|
|
170
|
-
(config: SyncConfig): Result<Sync, SqliteError> => {
|
|
171
|
-
let isDisposed = false;
|
|
172
|
-
|
|
173
|
-
/** Returns owner data only if actively assigned to at least one transport. */
|
|
174
|
-
const getSyncOwner = (ownerId: OwnerId): SyncOwner | null => {
|
|
175
|
-
if (isDisposed) return null;
|
|
176
|
-
return resources.getConsumer(ownerId);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const storageResult = createClientStorage({
|
|
180
|
-
...deps,
|
|
181
|
-
getSyncOwner,
|
|
182
|
-
})(config);
|
|
183
|
-
|
|
184
|
-
if (!storageResult.ok) return storageResult;
|
|
185
|
-
const storage = storageResult.value;
|
|
186
|
-
|
|
187
|
-
const createResource = (transport: OwnerTransport): WebSocket => {
|
|
188
|
-
const transportKey = createTransportKey(transport);
|
|
189
|
-
|
|
190
|
-
deps.console.log("[sync]", "createWebSocket", {
|
|
191
|
-
transportKey,
|
|
192
|
-
url: transport.url,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
return deps.createWebSocket(transport.url, {
|
|
196
|
-
binaryType: "arraybuffer",
|
|
197
|
-
|
|
198
|
-
onOpen: () => {
|
|
199
|
-
if (isDisposed) return;
|
|
200
|
-
|
|
201
|
-
const webSocket = resources.getResource(transportKey);
|
|
202
|
-
if (!webSocket) return;
|
|
203
|
-
|
|
204
|
-
const ownerIds = resources.getConsumersForResource(transportKey);
|
|
205
|
-
deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
|
|
206
|
-
|
|
207
|
-
for (const ownerId of ownerIds) {
|
|
208
|
-
const message = createProtocolMessageForSync({ storage })(
|
|
209
|
-
ownerId,
|
|
210
|
-
SubscriptionFlags.Subscribe,
|
|
211
|
-
);
|
|
212
|
-
if (!message) continue;
|
|
213
|
-
deps.console.log("[sync]", "send", { message });
|
|
214
|
-
webSocket.send(message);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
|
|
218
|
-
onClose: (event) => {
|
|
219
|
-
deps.console.log("[sync]", "onClose", {
|
|
220
|
-
transportKey,
|
|
221
|
-
code: event.code,
|
|
222
|
-
reason: event.reason,
|
|
223
|
-
wasClean: event.wasClean,
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
onError: (error) => {
|
|
228
|
-
deps.console.warn("[sync]", "onError", { transportKey, error });
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
onMessage: (data: string | ArrayBuffer | Blob) => {
|
|
232
|
-
// Only handle ArrayBuffer data for sync messages
|
|
233
|
-
if (isDisposed || !(data instanceof ArrayBuffer)) return;
|
|
234
|
-
|
|
235
|
-
const webSocket = resources.getResource(transportKey);
|
|
236
|
-
if (!webSocket) return;
|
|
237
|
-
|
|
238
|
-
const input = new Uint8Array(data);
|
|
239
|
-
deps.console.log("[sync]", "onMessage", {
|
|
240
|
-
transportKey,
|
|
241
|
-
message: input,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
applyProtocolMessageAsClient({ storage })(input, {
|
|
245
|
-
// No write key, no sync (for a case when an owner was unused).
|
|
246
|
-
getWriteKey: (ownerId) => getSyncOwner(ownerId)?.writeKey ?? null,
|
|
247
|
-
})
|
|
248
|
-
.then((message) => {
|
|
249
|
-
if (!message.ok) {
|
|
250
|
-
config.onError(message.error);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
switch (message.value.type) {
|
|
255
|
-
case "response":
|
|
256
|
-
webSocket.send(message.value.message);
|
|
257
|
-
break;
|
|
258
|
-
case "no-response":
|
|
259
|
-
// Sync complete, no response needed
|
|
260
|
-
break;
|
|
261
|
-
case "broadcast":
|
|
262
|
-
// This was a broadcast message, don't affect sync counter
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
.catch((error: unknown) => {
|
|
267
|
-
config.onError(createTransferableError(error));
|
|
268
|
-
});
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const resources = createResources<
|
|
274
|
-
WebSocket,
|
|
275
|
-
TransportKey,
|
|
276
|
-
OwnerTransport,
|
|
277
|
-
SyncOwner,
|
|
278
|
-
OwnerId
|
|
279
|
-
>({
|
|
280
|
-
createResource,
|
|
281
|
-
getResourceKey: createTransportKey,
|
|
282
|
-
getConsumerId: (owner) => owner.id,
|
|
283
|
-
disposalDelay: config.disposalDelayMs ?? 100,
|
|
284
|
-
|
|
285
|
-
onConsumerAdded: (owner, webSocket) => {
|
|
286
|
-
deps.console.log("[sync]", "onConsumerAdded", {
|
|
287
|
-
ownerId: owner.id,
|
|
288
|
-
isOpen: webSocket.isOpen(),
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// The onOpen handler will sync it.
|
|
292
|
-
if (!webSocket.isOpen()) return;
|
|
293
|
-
const message = createProtocolMessageForSync({ storage })(
|
|
294
|
-
owner.id,
|
|
295
|
-
SubscriptionFlags.Subscribe,
|
|
296
|
-
);
|
|
297
|
-
if (message) webSocket.send(message);
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
onConsumerRemoved: (owner, webSocket) => {
|
|
301
|
-
deps.console.log("[sync]", "onConsumerRemoved", {
|
|
302
|
-
ownerId: owner.id,
|
|
303
|
-
isOpen: webSocket.isOpen(),
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
const message = createProtocolMessageForUnsubscribe(owner.id);
|
|
307
|
-
webSocket.send(message);
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const sync: Sync = {
|
|
312
|
-
useOwner: (use, owner) => {
|
|
313
|
-
if (isDisposed) {
|
|
314
|
-
deps.console.warn(
|
|
315
|
-
"[sync]",
|
|
316
|
-
"useOwner called on disposed Sync instance",
|
|
317
|
-
{ owner },
|
|
318
|
-
);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
deps.console.log("[sync]", "useOwner", { use, owner });
|
|
323
|
-
const transports = owner.transports ?? config.transports;
|
|
324
|
-
|
|
325
|
-
if (use) {
|
|
326
|
-
resources.addConsumer(owner, transports);
|
|
327
|
-
} else {
|
|
328
|
-
const result = resources.removeConsumer(owner, transports);
|
|
329
|
-
if (!result.ok) {
|
|
330
|
-
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
331
|
-
transports,
|
|
332
|
-
ownerId: owner.id,
|
|
333
|
-
error: result.error,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
applyChanges: (changes) => {
|
|
340
|
-
deps.console.log("[sync]", "applyChanges", { changes });
|
|
341
|
-
|
|
342
|
-
let clockTimestamp = deps.clock.get();
|
|
343
|
-
const ownerMessages = new Map<OwnerId, NonEmptyArray<CrdtMessage>>();
|
|
344
|
-
|
|
345
|
-
for (const change of changes) {
|
|
346
|
-
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
347
|
-
if (!nextTimestamp.ok) return nextTimestamp;
|
|
348
|
-
clockTimestamp = nextTimestamp.value;
|
|
349
|
-
|
|
350
|
-
const { ownerId = config.appOwner.id, ...dbChange } = change;
|
|
351
|
-
const message: CrdtMessage = {
|
|
352
|
-
timestamp: clockTimestamp,
|
|
353
|
-
change: dbChange,
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
const messages = ownerMessages.get(ownerId);
|
|
357
|
-
if (messages) messages.push(message);
|
|
358
|
-
else ownerMessages.set(ownerId, [message]);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
for (const [ownerId, messages] of ownerMessages) {
|
|
362
|
-
const result = applyMessages({ ...deps, storage })(ownerId, messages);
|
|
363
|
-
if (!result.ok) return result;
|
|
364
|
-
|
|
365
|
-
const owner = getSyncOwner(ownerId);
|
|
366
|
-
if (!owner?.writeKey) continue;
|
|
367
|
-
|
|
368
|
-
const message = createProtocolMessageFromCrdtMessages(deps)(
|
|
369
|
-
{
|
|
370
|
-
id: owner.id,
|
|
371
|
-
encryptionKey: owner.encryptionKey,
|
|
372
|
-
writeKey: owner.writeKey,
|
|
373
|
-
},
|
|
374
|
-
messages,
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const transports = owner.transports ?? config.transports;
|
|
378
|
-
|
|
379
|
-
// Send message to all transports for this owner
|
|
380
|
-
for (const transport of transports) {
|
|
381
|
-
const transportKey = createTransportKey(transport);
|
|
382
|
-
|
|
383
|
-
const webSocket = resources.getResource(transportKey);
|
|
384
|
-
if (!webSocket) continue;
|
|
385
|
-
|
|
386
|
-
if (webSocket.isOpen()) {
|
|
387
|
-
deps.console.log("[sync]", "send", { transportKey, message });
|
|
388
|
-
webSocket.send(message);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return deps.clock.save(clockTimestamp);
|
|
394
|
-
},
|
|
395
|
-
|
|
396
|
-
[Symbol.dispose]: () => {
|
|
397
|
-
if (isDisposed) return;
|
|
398
|
-
isDisposed = true;
|
|
399
|
-
resources[Symbol.dispose]();
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
return ok(sync);
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
export interface ClockDep {
|
|
407
|
-
readonly clock: Clock;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export interface Clock {
|
|
411
|
-
readonly get: () => Timestamp;
|
|
412
|
-
readonly save: (timestamp: Timestamp) => Result<void, SqliteError>;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export const createClock =
|
|
416
|
-
(deps: RandomBytesDep & SqliteDep) =>
|
|
417
|
-
(initialTimestamp = createInitialTimestamp(deps)): Clock => {
|
|
418
|
-
let currentTimestamp = initialTimestamp;
|
|
419
|
-
|
|
420
|
-
return {
|
|
421
|
-
get: () => currentTimestamp,
|
|
422
|
-
save: (timestamp) => {
|
|
423
|
-
currentTimestamp = timestamp;
|
|
424
|
-
|
|
425
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
426
|
-
update evolu_config
|
|
427
|
-
set "clock" = ${timestampToTimestampBytes(timestamp)};
|
|
428
|
-
`);
|
|
429
|
-
if (!result.ok) return result;
|
|
430
|
-
|
|
431
|
-
return ok();
|
|
432
|
-
},
|
|
433
|
-
};
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
interface GetSyncOwnerDep {
|
|
437
|
-
readonly getSyncOwner: (ownerId: OwnerId) => SyncOwner | null;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export interface ClientStorage extends Storage, BaseSqliteStorage {}
|
|
441
|
-
|
|
442
|
-
export interface ClientStorageDep {
|
|
443
|
-
readonly storage: ClientStorage;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const createClientStorage =
|
|
447
|
-
(
|
|
448
|
-
deps: ClockDep &
|
|
449
|
-
DbSchemaDep &
|
|
450
|
-
GetSyncOwnerDep &
|
|
451
|
-
RandomDep &
|
|
452
|
-
SqliteDep &
|
|
453
|
-
SymmetricCryptoDep &
|
|
454
|
-
TimeDep &
|
|
455
|
-
TimestampConfigDep,
|
|
456
|
-
) =>
|
|
457
|
-
(config: {
|
|
458
|
-
onError: (
|
|
459
|
-
error:
|
|
460
|
-
| ProtocolInvalidDataError
|
|
461
|
-
| ProtocolTimestampMismatchError
|
|
462
|
-
| SqliteError
|
|
463
|
-
| SymmetricCryptoDecryptError
|
|
464
|
-
| TimestampCounterOverflowError
|
|
465
|
-
| TimestampDriftError
|
|
466
|
-
| TimestampTimeOutOfRangeError,
|
|
467
|
-
) => void;
|
|
468
|
-
onReceive: () => void;
|
|
469
|
-
}): Result<ClientStorage, SqliteError> => {
|
|
470
|
-
const sqliteStorageBase = createBaseSqliteStorage(deps)({
|
|
471
|
-
onStorageError: config.onError,
|
|
472
|
-
isOwnerWithinQuota: constTrue, // Clients don't have quota limits
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
// TODO: Mutex per OwnerId
|
|
476
|
-
const mutex = createMutex();
|
|
477
|
-
|
|
478
|
-
const storage: ClientStorage = {
|
|
479
|
-
...sqliteStorageBase,
|
|
480
|
-
|
|
481
|
-
// Not implemented yet.
|
|
482
|
-
validateWriteKey: constFalse,
|
|
483
|
-
setWriteKey: constFalse,
|
|
484
|
-
|
|
485
|
-
writeMessages: async (ownerIdBytes, encryptedMessages) => {
|
|
486
|
-
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
487
|
-
|
|
488
|
-
// Everything is sync now, but we will need async crypto in the future.
|
|
489
|
-
const result = await mutex.withLock<
|
|
490
|
-
boolean,
|
|
491
|
-
| AbortError
|
|
492
|
-
| ProtocolInvalidDataError
|
|
493
|
-
| ProtocolTimestampMismatchError
|
|
494
|
-
| SqliteError
|
|
495
|
-
| SymmetricCryptoDecryptError
|
|
496
|
-
| TimestampCounterOverflowError
|
|
497
|
-
| TimestampDriftError
|
|
498
|
-
| TimestampTimeOutOfRangeError
|
|
499
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
500
|
-
>(async () => {
|
|
501
|
-
const owner = deps.getSyncOwner(ownerId);
|
|
502
|
-
// Owner can be removed during syncing.
|
|
503
|
-
// `ok(true)` means success, we just skipped the write.
|
|
504
|
-
if (!owner) return ok(true);
|
|
505
|
-
|
|
506
|
-
// TODO: Add quota checking for collaborative scenarios.
|
|
507
|
-
// When receiving messages from other owners via relay broadcast,
|
|
508
|
-
// check if this owner is within quota before accepting the data.
|
|
509
|
-
// This prevents an owner from exceeding storage limits when receiving
|
|
510
|
-
// data shared by other collaborators.
|
|
511
|
-
|
|
512
|
-
const messages: Array<CrdtMessage> = [];
|
|
513
|
-
|
|
514
|
-
for (const message of encryptedMessages) {
|
|
515
|
-
const change = decryptAndDecodeDbChange(deps)(
|
|
516
|
-
message,
|
|
517
|
-
owner.encryptionKey,
|
|
518
|
-
);
|
|
519
|
-
if (!change.ok) return change;
|
|
520
|
-
|
|
521
|
-
messages.push({
|
|
522
|
-
timestamp: message.timestamp,
|
|
523
|
-
change: change.value,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const transaction = deps.sqlite.transaction(() => {
|
|
528
|
-
let clockTimestamp = deps.clock.get();
|
|
529
|
-
|
|
530
|
-
for (const message of messages) {
|
|
531
|
-
const nextTimestamp = receiveTimestamp(deps)(
|
|
532
|
-
clockTimestamp,
|
|
533
|
-
message.timestamp,
|
|
534
|
-
);
|
|
535
|
-
if (!nextTimestamp.ok) return nextTimestamp;
|
|
536
|
-
|
|
537
|
-
clockTimestamp = nextTimestamp.value;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (isNonEmptyReadonlyArray(messages)) {
|
|
541
|
-
const result = applyMessages({ ...deps, storage })(
|
|
542
|
-
owner.id,
|
|
543
|
-
messages,
|
|
544
|
-
);
|
|
545
|
-
if (!result.ok) return result;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return deps.clock.save(clockTimestamp);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
if (!transaction.ok) return transaction;
|
|
552
|
-
|
|
553
|
-
return ok(true);
|
|
554
|
-
})();
|
|
555
|
-
|
|
556
|
-
if (!result.ok) {
|
|
557
|
-
if (result.error.type !== "AbortError") {
|
|
558
|
-
config.onError(result.error);
|
|
559
|
-
}
|
|
560
|
-
return err<StorageWriteError>({ type: "StorageWriteError", ownerId });
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
config.onReceive();
|
|
564
|
-
|
|
565
|
-
return ok();
|
|
566
|
-
},
|
|
567
|
-
|
|
568
|
-
readDbChange: (ownerId, timestamp) => {
|
|
569
|
-
const owner = deps.getSyncOwner(ownerIdBytesToOwnerId(ownerId));
|
|
570
|
-
// Owner can be removed to stop syncing.
|
|
571
|
-
if (!owner) return null;
|
|
572
|
-
|
|
573
|
-
const result = deps.sqlite.exec<{
|
|
574
|
-
readonly table: string;
|
|
575
|
-
readonly id: IdBytes;
|
|
576
|
-
readonly column: string;
|
|
577
|
-
readonly value: SqliteValue;
|
|
578
|
-
}>(sql`
|
|
579
|
-
select "table", "id", "column", "value"
|
|
580
|
-
from evolu_history
|
|
581
|
-
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
582
|
-
union all
|
|
583
|
-
select "table", "id", "column", "value"
|
|
584
|
-
from evolu_message_quarantine
|
|
585
|
-
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
586
|
-
`);
|
|
587
|
-
|
|
588
|
-
if (!result.ok) {
|
|
589
|
-
config.onError(result.error);
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const { rows } = result.value;
|
|
594
|
-
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
|
|
595
|
-
const firstRow = firstInArray(rows);
|
|
596
|
-
|
|
597
|
-
const values = createRecord<string, SqliteValue>();
|
|
598
|
-
let isInsert: DbChange["isInsert"] = false;
|
|
599
|
-
let isDelete: DbChange["isDelete"] = null;
|
|
600
|
-
|
|
601
|
-
for (const r of rows) {
|
|
602
|
-
switch (r.column) {
|
|
603
|
-
case "createdAt":
|
|
604
|
-
isInsert = true;
|
|
605
|
-
break;
|
|
606
|
-
case "updatedAt":
|
|
607
|
-
isInsert = false;
|
|
608
|
-
break;
|
|
609
|
-
case "isDeleted":
|
|
610
|
-
if (SqliteBoolean.is(r.value)) {
|
|
611
|
-
isDelete = sqliteBooleanToBoolean(r.value);
|
|
612
|
-
}
|
|
613
|
-
break;
|
|
614
|
-
default:
|
|
615
|
-
values[r.column] = r.value;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const message: CrdtMessage = {
|
|
620
|
-
timestamp: timestampBytesToTimestamp(timestamp),
|
|
621
|
-
change: DbChange.orThrow({
|
|
622
|
-
table: firstRow.table,
|
|
623
|
-
id: idBytesToId(firstRow.id),
|
|
624
|
-
values,
|
|
625
|
-
isInsert,
|
|
626
|
-
isDelete,
|
|
627
|
-
}),
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
|
|
631
|
-
},
|
|
632
|
-
};
|
|
633
|
-
|
|
634
|
-
return ok(storage);
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
type TransportKey = string & Brand<"TransportKey">;
|
|
638
|
-
|
|
639
|
-
/** Creates a unique identifier for a {@link OwnerTransport}. */
|
|
640
|
-
const createTransportKey = (transport: OwnerTransport): TransportKey => {
|
|
641
|
-
return `${transport.type}:${transport.url}` as TransportKey;
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
const dbChangeToColumns = (change: DbChange, now: DateIso) => {
|
|
645
|
-
let values = objectToEntries(change.values);
|
|
646
|
-
|
|
647
|
-
// SystemColumns are not encoded in change.values.
|
|
648
|
-
values = appendToArray(values, [
|
|
649
|
-
change.isInsert ? "createdAt" : "updatedAt",
|
|
650
|
-
now,
|
|
651
|
-
]);
|
|
652
|
-
if (change.isDelete != null) {
|
|
653
|
-
values = appendToArray(values, [
|
|
654
|
-
"isDeleted",
|
|
655
|
-
booleanToSqliteBoolean(change.isDelete),
|
|
656
|
-
]);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return values;
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
export const applyLocalOnlyChange =
|
|
663
|
-
(deps: SqliteDep & TimeDep & AppOwnerDep) =>
|
|
664
|
-
(change: MutationChange): Result<void, SqliteError> => {
|
|
665
|
-
if (change.isDelete) {
|
|
666
|
-
const result = deps.sqlite.exec(sql`
|
|
667
|
-
delete from ${sql.identifier(change.table)}
|
|
668
|
-
where id = ${change.id};
|
|
669
|
-
`);
|
|
670
|
-
if (!result.ok) return result;
|
|
671
|
-
} else {
|
|
672
|
-
const ownerId = deps.appOwner.id;
|
|
673
|
-
const columns = dbChangeToColumns(change, deps.time.nowIso());
|
|
674
|
-
|
|
675
|
-
for (const [column, value] of columns) {
|
|
676
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
677
|
-
insert into ${sql.identifier(change.table)}
|
|
678
|
-
("ownerId", "id", ${sql.identifier(column)})
|
|
679
|
-
values (${ownerId}, ${change.id}, ${value})
|
|
680
|
-
on conflict ("ownerId", "id") do update
|
|
681
|
-
set ${sql.identifier(column)} = ${value};
|
|
682
|
-
`);
|
|
683
|
-
if (!result.ok) return result;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return ok();
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
const applyMessages =
|
|
691
|
-
(deps: ClientStorageDep & ClockDep & DbSchemaDep & RandomDep & SqliteDep) =>
|
|
692
|
-
(
|
|
693
|
-
ownerId: OwnerId,
|
|
694
|
-
messages: NonEmptyReadonlyArray<CrdtMessage>,
|
|
695
|
-
): Result<void, SqliteError> => {
|
|
696
|
-
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
697
|
-
|
|
698
|
-
const usage = getOwnerUsage(deps)(
|
|
699
|
-
ownerIdBytes,
|
|
700
|
-
timestampToTimestampBytes(firstInArray(messages).timestamp),
|
|
701
|
-
);
|
|
702
|
-
if (!usage.ok) return usage;
|
|
703
|
-
|
|
704
|
-
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
705
|
-
|
|
706
|
-
for (const { timestamp, change } of messages) {
|
|
707
|
-
const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
|
|
708
|
-
const idBytes = idToIdBytes(change.id);
|
|
709
|
-
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
710
|
-
|
|
711
|
-
for (const [column, value] of columns) {
|
|
712
|
-
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
713
|
-
const result = applyColumnChange(deps)(
|
|
714
|
-
ownerIdBytes,
|
|
715
|
-
ownerId,
|
|
716
|
-
change.table,
|
|
717
|
-
idBytes,
|
|
718
|
-
change.id,
|
|
719
|
-
column,
|
|
720
|
-
value,
|
|
721
|
-
timestampBytes,
|
|
722
|
-
);
|
|
723
|
-
if (!result.ok) return result;
|
|
724
|
-
} else {
|
|
725
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
726
|
-
insert into evolu_message_quarantine
|
|
727
|
-
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
728
|
-
values
|
|
729
|
-
(
|
|
730
|
-
${ownerIdBytes},
|
|
731
|
-
${timestampBytes},
|
|
732
|
-
${change.table},
|
|
733
|
-
${idBytes},
|
|
734
|
-
${column},
|
|
735
|
-
${value}
|
|
736
|
-
)
|
|
737
|
-
on conflict do nothing;
|
|
738
|
-
`);
|
|
739
|
-
if (!result.ok) return result;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
let strategy;
|
|
744
|
-
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(
|
|
745
|
-
timestampBytes,
|
|
746
|
-
firstTimestamp,
|
|
747
|
-
lastTimestamp,
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
const result = deps.storage.insertTimestamp(
|
|
751
|
-
ownerIdBytes,
|
|
752
|
-
timestampBytes,
|
|
753
|
-
strategy,
|
|
754
|
-
);
|
|
755
|
-
if (!result.ok) return result;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* TODO: Implement proper storedBytes tracking for client using received and
|
|
760
|
-
* sent encrypted message sizes.
|
|
761
|
-
*/
|
|
762
|
-
return updateOwnerUsage(deps)(
|
|
763
|
-
ownerIdBytes,
|
|
764
|
-
1 as PositiveInt, // Placeholder until proper tracking implemented
|
|
765
|
-
firstTimestamp,
|
|
766
|
-
lastTimestamp,
|
|
767
|
-
);
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* System columns that can appear in sync messages. Excludes `ownerId` because
|
|
772
|
-
* it's handled separately (stored per-row, not per-column in messages).
|
|
773
|
-
*/
|
|
774
|
-
const systemColumnsWithoutOwnerId = systemColumns.difference(
|
|
775
|
-
new Set(["ownerId"]),
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
const validateColumnValue =
|
|
779
|
-
(deps: DbSchemaDep) =>
|
|
780
|
-
(table: string, column: string, _value: SqliteValue): boolean => {
|
|
781
|
-
const schemaColumns = getProperty(deps.dbSchema.tables, table);
|
|
782
|
-
return (
|
|
783
|
-
schemaColumns != null &&
|
|
784
|
-
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column))
|
|
785
|
-
);
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
const applyColumnChange =
|
|
789
|
-
(deps: SqliteDep) =>
|
|
790
|
-
(
|
|
791
|
-
ownerIdBytes: OwnerIdBytes,
|
|
792
|
-
ownerId: OwnerId,
|
|
793
|
-
table: string,
|
|
794
|
-
idBytes: IdBytes,
|
|
795
|
-
id: Id,
|
|
796
|
-
column: string,
|
|
797
|
-
value: SqliteValue,
|
|
798
|
-
timestampBytes: TimestampBytes,
|
|
799
|
-
): Result<void, SqliteError> => {
|
|
800
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
801
|
-
with
|
|
802
|
-
existingTimestamp as (
|
|
803
|
-
select 1
|
|
804
|
-
from evolu_history
|
|
805
|
-
where
|
|
806
|
-
"ownerId" = ${ownerIdBytes}
|
|
807
|
-
and "table" = ${table}
|
|
808
|
-
and "id" = ${idBytes}
|
|
809
|
-
and "column" = ${column}
|
|
810
|
-
and "timestamp" >= ${timestampBytes}
|
|
811
|
-
limit 1
|
|
812
|
-
)
|
|
813
|
-
insert into ${sql.identifier(table)}
|
|
814
|
-
("ownerId", "id", ${sql.identifier(column)})
|
|
815
|
-
select ${ownerId}, ${id}, ${value}
|
|
816
|
-
where not exists (select 1 from existingTimestamp)
|
|
817
|
-
on conflict ("ownerId", "id") do update
|
|
818
|
-
set ${sql.identifier(column)} = ${value}
|
|
819
|
-
where not exists (select 1 from existingTimestamp);
|
|
820
|
-
`);
|
|
821
|
-
if (!result.ok) return result;
|
|
822
|
-
|
|
823
|
-
{
|
|
824
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
825
|
-
insert into evolu_history
|
|
826
|
-
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
827
|
-
values
|
|
828
|
-
(
|
|
829
|
-
${ownerIdBytes},
|
|
830
|
-
${table},
|
|
831
|
-
${idBytes},
|
|
832
|
-
${column},
|
|
833
|
-
${value},
|
|
834
|
-
${timestampBytes}
|
|
835
|
-
)
|
|
836
|
-
on conflict do nothing;
|
|
837
|
-
`);
|
|
838
|
-
if (!result.ok) return result;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return ok();
|
|
842
|
-
};
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* Attempts to apply quarantined messages that may now be valid after a schema
|
|
846
|
-
* update. Messages are quarantined when they reference tables or columns that
|
|
847
|
-
* don't exist in the current schema (e.g., from a newer app version).
|
|
848
|
-
*/
|
|
849
|
-
export const tryApplyQuarantinedMessages =
|
|
850
|
-
(deps: DbSchemaDep & SqliteDep) => (): Result<void, SqliteError> => {
|
|
851
|
-
const rows = deps.sqlite.exec<{
|
|
852
|
-
readonly ownerId: OwnerIdBytes;
|
|
853
|
-
readonly timestamp: TimestampBytes;
|
|
854
|
-
readonly table: string;
|
|
855
|
-
readonly id: IdBytes;
|
|
856
|
-
readonly column: string;
|
|
857
|
-
readonly value: SqliteValue;
|
|
858
|
-
}>(sql`
|
|
859
|
-
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
860
|
-
from evolu_message_quarantine;
|
|
861
|
-
`);
|
|
862
|
-
if (!rows.ok) return rows;
|
|
863
|
-
|
|
864
|
-
for (const row of rows.value.rows) {
|
|
865
|
-
if (!validateColumnValue(deps)(row.table, row.column, row.value))
|
|
866
|
-
continue;
|
|
867
|
-
|
|
868
|
-
const result = applyColumnChange(deps)(
|
|
869
|
-
row.ownerId,
|
|
870
|
-
ownerIdBytesToOwnerId(row.ownerId),
|
|
871
|
-
row.table,
|
|
872
|
-
row.id,
|
|
873
|
-
idBytesToId(row.id),
|
|
874
|
-
row.column,
|
|
875
|
-
row.value,
|
|
876
|
-
row.timestamp,
|
|
877
|
-
);
|
|
878
|
-
if (!result.ok) return result;
|
|
879
|
-
|
|
880
|
-
{
|
|
881
|
-
const result = deps.sqlite.exec(sql`
|
|
882
|
-
delete from evolu_message_quarantine
|
|
883
|
-
where
|
|
884
|
-
"ownerId" = ${row.ownerId}
|
|
885
|
-
and "timestamp" = ${row.timestamp}
|
|
886
|
-
and "table" = ${row.table}
|
|
887
|
-
and "id" = ${row.id}
|
|
888
|
-
and "column" = ${row.column};
|
|
889
|
-
`);
|
|
890
|
-
if (!result.ok) return result;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
return ok();
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* TODO: Rework for the new owners API.
|
|
899
|
-
*
|
|
900
|
-
* The possible states of a synchronization process. The `SyncState` can be one
|
|
901
|
-
* of the following:
|
|
902
|
-
*
|
|
903
|
-
* - {@link SyncStateInitial}
|
|
904
|
-
* - {@link SyncStateIsSyncing}
|
|
905
|
-
* - {@link SyncStateIsSynced}
|
|
906
|
-
* - {@link SyncStateIsNotSynced}
|
|
907
|
-
*/
|
|
908
|
-
export type SyncState =
|
|
909
|
-
| SyncStateInitial
|
|
910
|
-
| SyncStateIsSyncing
|
|
911
|
-
| SyncStateIsSynced
|
|
912
|
-
| SyncStateIsNotSynced;
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* The initial synchronization state when the app starts. In this state, the app
|
|
916
|
-
* needs to determine whether the data is synced.
|
|
917
|
-
*/
|
|
918
|
-
export interface SyncStateInitial {
|
|
919
|
-
readonly type: "SyncStateInitial";
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
export interface SyncStateIsSyncing {
|
|
923
|
-
readonly type: "SyncStateIsSyncing";
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
export interface SyncStateIsSynced {
|
|
927
|
-
readonly type: "SyncStateIsSynced";
|
|
928
|
-
readonly time: Millis;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
export interface SyncStateIsNotSynced {
|
|
932
|
-
readonly type: "SyncStateIsNotSynced";
|
|
933
|
-
readonly error: NetworkError | ServerError | PaymentRequiredError;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
export interface NetworkError {
|
|
937
|
-
readonly type: "NetworkError";
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
export interface ServerError {
|
|
941
|
-
readonly type: "ServerError";
|
|
942
|
-
readonly status: number;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
export interface PaymentRequiredError {
|
|
946
|
-
readonly type: "PaymentRequiredError";
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
export const initialSyncState: SyncStateInitial = { type: "SyncStateInitial" };
|