@evolu/common 6.0.1-preview.8 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/Array.d.ts +256 -12
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +215 -9
- package/dist/src/Assert.d.ts +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +56 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +40 -53
- package/dist/src/Evolu/Db.d.ts +162 -74
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +284 -702
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +214 -134
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +189 -180
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +268 -240
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +600 -454
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
- package/dist/src/Evolu/PublicKysely.js +3 -4
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Relay.d.ts +91 -8
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +214 -88
- package/dist/src/Evolu/Schema.d.ts +125 -47
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +175 -31
- package/dist/src/Evolu/Storage.d.ts +249 -27
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +198 -92
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +469 -20
- package/dist/src/Evolu/Timestamp.d.ts +83 -30
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +79 -34
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +79 -0
- package/dist/src/Relation.d.ts.map +1 -0
- package/dist/src/Relation.js +127 -0
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +89 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +136 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +676 -343
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +718 -467
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/package.json +15 -14
- package/src/Array.ts +301 -19
- package/src/Assert.ts +0 -21
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -90
- package/src/Evolu/Db.ts +519 -1026
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +477 -361
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +842 -742
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +4 -5
- package/src/Evolu/Query.ts +3 -2
- package/src/Evolu/Relay.ts +406 -103
- package/src/Evolu/Schema.ts +323 -91
- package/src/Evolu/Storage.ts +559 -137
- package/src/Evolu/Sync.ts +819 -36
- package/src/Evolu/Timestamp.ts +90 -58
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/Number.ts +6 -10
- package/src/Object.ts +13 -5
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Relation.ts +234 -0
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +152 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1135 -730
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +8 -3
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/ManyToManyMap.d.ts +0 -26
- package/dist/src/ManyToManyMap.d.ts.map +0 -1
- package/dist/src/ManyToManyMap.js +0 -92
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/ManyToManyMap.ts +0 -140
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Evolu/Sync.js
CHANGED
|
@@ -1,29 +1,478 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { firstInArray, isNonEmptyReadonlyArray, } from "../Array.js";
|
|
2
|
+
import { assert } from "../Assert.js";
|
|
3
|
+
import { eqArrayNumber } from "../Eq.js";
|
|
4
|
+
import { createTransferableError } from "../Error.js";
|
|
5
|
+
import { constFalse, constTrue } from "../Function.js";
|
|
6
|
+
import { objectToEntries } from "../Object.js";
|
|
7
|
+
import { createResources } from "../Resources.js";
|
|
8
|
+
import { err, ok } from "../Result.js";
|
|
9
|
+
import { booleanToSqliteBoolean, sql, SqliteBoolean, sqliteBooleanToBoolean, sqliteFalse, } from "../Sqlite.js";
|
|
10
|
+
import { createMutex } from "../Task.js";
|
|
11
|
+
import { idBytesToId, idToIdBytes, } from "../Type.js";
|
|
12
|
+
import { ownerIdBytesToOwnerId, ownerIdToOwnerIdBytes, } from "./Owner.js";
|
|
13
|
+
import { applyProtocolMessageAsClient, createProtocolMessageForSync, createProtocolMessageForUnsubscribe, createProtocolMessageFromCrdtMessages, decryptAndDecodeDbChange, encodeAndEncryptDbChange, SubscriptionFlags, } from "./Protocol.js";
|
|
14
|
+
import { createBaseSqliteStorage, DbChange, getOwnerUsage, getTimestampInsertStrategy, updateOwnerUsage, } from "./Storage.js";
|
|
15
|
+
import { createInitialTimestamp, receiveTimestamp, sendTimestamp, timestampBytesToTimestamp, timestampToDateIso, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
16
|
+
export const createSync = (deps) => (config) => {
|
|
17
|
+
let isDisposed = false;
|
|
18
|
+
/** Returns owner data only if actively assigned to at least one transport. */
|
|
19
|
+
const getSyncOwner = (ownerId) => {
|
|
20
|
+
if (isDisposed)
|
|
21
|
+
return null;
|
|
22
|
+
return transports.getConsumer(ownerId);
|
|
23
|
+
};
|
|
24
|
+
const storageResult = createClientStorage({
|
|
25
|
+
...deps,
|
|
26
|
+
getSyncOwner,
|
|
27
|
+
})(config);
|
|
28
|
+
if (!storageResult.ok)
|
|
29
|
+
return storageResult;
|
|
30
|
+
const storage = storageResult.value;
|
|
31
|
+
const createResource = (transportConfig) => {
|
|
32
|
+
const transportKey = createTransportKey(transportConfig);
|
|
33
|
+
deps.console.log("[sync]", "createWebSocket", {
|
|
34
|
+
transportKey,
|
|
35
|
+
url: transportConfig.url,
|
|
36
|
+
});
|
|
37
|
+
return deps.createWebSocket(transportConfig.url, {
|
|
38
|
+
binaryType: "arraybuffer",
|
|
39
|
+
onOpen: () => {
|
|
40
|
+
if (isDisposed)
|
|
41
|
+
return;
|
|
42
|
+
const webSocket = transports.getResource(transportKey);
|
|
43
|
+
if (!webSocket)
|
|
44
|
+
return;
|
|
45
|
+
const ownerIds = transports.getConsumersForResource(transportKey);
|
|
46
|
+
deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
|
|
47
|
+
for (const ownerId of ownerIds) {
|
|
48
|
+
const message = createProtocolMessageForSync({ storage })(ownerId, SubscriptionFlags.Subscribe);
|
|
49
|
+
if (!message)
|
|
50
|
+
continue;
|
|
51
|
+
deps.console.log("[sync]", "send", { message });
|
|
52
|
+
webSocket.send(message);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
onClose: (event) => {
|
|
56
|
+
deps.console.log("[sync]", "onClose", {
|
|
57
|
+
transportKey,
|
|
58
|
+
code: event.code,
|
|
59
|
+
reason: event.reason,
|
|
60
|
+
wasClean: event.wasClean,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
onError: (error) => {
|
|
64
|
+
deps.console.warn("[sync]", "onError", { transportKey, error });
|
|
65
|
+
},
|
|
66
|
+
onMessage: (data) => {
|
|
67
|
+
// Only handle ArrayBuffer data for sync messages
|
|
68
|
+
if (isDisposed || !(data instanceof ArrayBuffer))
|
|
69
|
+
return;
|
|
70
|
+
const webSocket = transports.getResource(transportKey);
|
|
71
|
+
if (!webSocket)
|
|
72
|
+
return;
|
|
73
|
+
const input = new Uint8Array(data);
|
|
74
|
+
deps.console.log("[sync]", "onMessage", {
|
|
75
|
+
transportKey,
|
|
76
|
+
message: input,
|
|
77
|
+
});
|
|
78
|
+
applyProtocolMessageAsClient({ storage })(input, {
|
|
79
|
+
// No write key, no sync (for a case when an owner was unused).
|
|
80
|
+
getWriteKey: (ownerId) => getSyncOwner(ownerId)?.writeKey ?? null,
|
|
81
|
+
})
|
|
82
|
+
.then((message) => {
|
|
83
|
+
if (!message.ok) {
|
|
84
|
+
config.onError(message.error);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
switch (message.value.type) {
|
|
88
|
+
case "response":
|
|
89
|
+
webSocket.send(message.value.message);
|
|
90
|
+
break;
|
|
91
|
+
case "no-response":
|
|
92
|
+
// Sync complete, no response needed
|
|
93
|
+
break;
|
|
94
|
+
case "broadcast":
|
|
95
|
+
// This was a broadcast message, don't affect sync counter
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
config.onError(createTransferableError(error));
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const transports = createResources({
|
|
106
|
+
createResource,
|
|
107
|
+
getResourceKey: createTransportKey,
|
|
108
|
+
getConsumerId: (owner) => owner.id,
|
|
109
|
+
disposalDelay: config.disposalDelayMs ?? 100,
|
|
110
|
+
onConsumerAdded: (owner, webSocket) => {
|
|
111
|
+
deps.console.log("[sync]", "onConsumerAdded", {
|
|
112
|
+
ownerId: owner.id,
|
|
113
|
+
isOpen: webSocket.isOpen(),
|
|
114
|
+
});
|
|
115
|
+
// The onOpen handler will sync it.
|
|
116
|
+
if (!webSocket.isOpen())
|
|
117
|
+
return;
|
|
118
|
+
const message = createProtocolMessageForSync({ storage })(owner.id, SubscriptionFlags.Subscribe);
|
|
119
|
+
if (message)
|
|
120
|
+
webSocket.send(message);
|
|
121
|
+
},
|
|
122
|
+
onConsumerRemoved: (owner, webSocket) => {
|
|
123
|
+
deps.console.log("[sync]", "onConsumerRemoved", {
|
|
124
|
+
ownerId: owner.id,
|
|
125
|
+
isOpen: webSocket.isOpen(),
|
|
126
|
+
});
|
|
127
|
+
const message = createProtocolMessageForUnsubscribe(owner.id);
|
|
128
|
+
webSocket.send(message);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
3
131
|
const sync = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* and mutations are stored in SQLite. Dropped CRDT messages are synced
|
|
8
|
-
* when the web socket connection is open.
|
|
9
|
-
*/
|
|
10
|
-
if (socket.getReadyState() !== "open")
|
|
132
|
+
useOwner: (use, owner) => {
|
|
133
|
+
if (isDisposed) {
|
|
134
|
+
deps.console.warn("[sync]", "useOwner called on disposed Sync instance", { owner });
|
|
11
135
|
return;
|
|
12
|
-
|
|
136
|
+
}
|
|
137
|
+
deps.console.log("[sync]", "useOwner", { use, owner });
|
|
138
|
+
const transportsToUse = owner.transports ?? config.transports;
|
|
139
|
+
if (use) {
|
|
140
|
+
transports.addConsumer(owner, transportsToUse);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const result = transports.removeConsumer(owner, transportsToUse);
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
146
|
+
transportsToUse,
|
|
147
|
+
ownerId: owner.id,
|
|
148
|
+
error: result.error,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
applyChanges: (changes) => {
|
|
154
|
+
deps.console.log("[sync]", "applyChanges", { changes });
|
|
155
|
+
let clockTimestamp = deps.clock.get();
|
|
156
|
+
const ownerMessages = new Map();
|
|
157
|
+
for (const change of changes) {
|
|
158
|
+
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
159
|
+
if (!nextTimestamp.ok)
|
|
160
|
+
return nextTimestamp;
|
|
161
|
+
clockTimestamp = nextTimestamp.value;
|
|
162
|
+
const { ownerId = config.appOwner.id, ...dbChange } = change;
|
|
163
|
+
const message = {
|
|
164
|
+
timestamp: clockTimestamp,
|
|
165
|
+
change: dbChange,
|
|
166
|
+
};
|
|
167
|
+
const messages = ownerMessages.get(ownerId);
|
|
168
|
+
if (messages)
|
|
169
|
+
messages.push(message);
|
|
170
|
+
else
|
|
171
|
+
ownerMessages.set(ownerId, [message]);
|
|
172
|
+
}
|
|
173
|
+
for (const [ownerId, messages] of ownerMessages) {
|
|
174
|
+
const result = applyMessages({ ...deps, storage })(ownerId, messages);
|
|
175
|
+
if (!result.ok)
|
|
176
|
+
return result;
|
|
177
|
+
const owner = getSyncOwner(ownerId);
|
|
178
|
+
if (!owner?.writeKey)
|
|
179
|
+
continue;
|
|
180
|
+
const message = createProtocolMessageFromCrdtMessages(deps)({
|
|
181
|
+
id: owner.id,
|
|
182
|
+
encryptionKey: owner.encryptionKey,
|
|
183
|
+
writeKey: owner.writeKey,
|
|
184
|
+
}, messages);
|
|
185
|
+
const transportsToUse = owner.transports ?? config.transports;
|
|
186
|
+
// Send message to all transports for this owner
|
|
187
|
+
for (const transportConfig of transportsToUse) {
|
|
188
|
+
const transportKey = createTransportKey(transportConfig);
|
|
189
|
+
const webSocket = transports.getResource(transportKey);
|
|
190
|
+
if (!webSocket)
|
|
191
|
+
continue;
|
|
192
|
+
if (webSocket.isOpen()) {
|
|
193
|
+
deps.console.log("[sync]", "send", { transportKey, message });
|
|
194
|
+
webSocket.send(message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return deps.clock.save(clockTimestamp);
|
|
199
|
+
},
|
|
200
|
+
[Symbol.dispose]: () => {
|
|
201
|
+
if (isDisposed)
|
|
202
|
+
return;
|
|
203
|
+
isDisposed = true;
|
|
204
|
+
transports[Symbol.dispose]();
|
|
13
205
|
},
|
|
14
206
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
207
|
+
return ok(sync);
|
|
208
|
+
};
|
|
209
|
+
export const createClock = (deps) => (initialTimestamp = createInitialTimestamp(deps)) => {
|
|
210
|
+
let currentTimestamp = initialTimestamp;
|
|
211
|
+
return {
|
|
212
|
+
get: () => currentTimestamp,
|
|
213
|
+
save: (timestamp) => {
|
|
214
|
+
currentTimestamp = timestamp;
|
|
215
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
216
|
+
update evolu_config
|
|
217
|
+
set "clock" = ${timestampToTimestampBytes(timestamp)};
|
|
218
|
+
`);
|
|
219
|
+
if (!result.ok)
|
|
220
|
+
return result;
|
|
221
|
+
return ok();
|
|
19
222
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
const createClientStorage = (deps) => (config) => {
|
|
226
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps)({
|
|
227
|
+
onStorageError: config.onError,
|
|
228
|
+
isOwnerWithinQuota: constTrue, // Clients don't have quota limits
|
|
229
|
+
});
|
|
230
|
+
// TODO: Mutex per OwnerId
|
|
231
|
+
const mutex = createMutex();
|
|
232
|
+
const storage = {
|
|
233
|
+
...sqliteStorageBase,
|
|
234
|
+
// Not implemented yet.
|
|
235
|
+
validateWriteKey: constFalse,
|
|
236
|
+
setWriteKey: constFalse,
|
|
237
|
+
writeMessages: async (ownerIdBytes, encryptedMessages) => {
|
|
238
|
+
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
239
|
+
// Everything is sync now, but we will need async crypto in the future.
|
|
240
|
+
const writeResult = await mutex.withLock(async () => {
|
|
241
|
+
const owner = deps.getSyncOwner(ownerId);
|
|
242
|
+
// Owner can be removed during syncing.
|
|
243
|
+
// `ok(true)` means success, we just skipped the write.
|
|
244
|
+
if (!owner)
|
|
245
|
+
return ok(true);
|
|
246
|
+
// TODO: Add quota checking for collaborative scenarios.
|
|
247
|
+
// When receiving messages from other owners via relay broadcast,
|
|
248
|
+
// check if this owner is within quota before accepting the data.
|
|
249
|
+
// This prevents an owner from exceeding storage limits when receiving
|
|
250
|
+
// data shared by other collaborators.
|
|
251
|
+
const messages = [];
|
|
252
|
+
for (const message of encryptedMessages) {
|
|
253
|
+
const change = decryptAndDecodeDbChange(deps)(message, owner.encryptionKey);
|
|
254
|
+
if (!change.ok)
|
|
255
|
+
return change;
|
|
256
|
+
messages.push({
|
|
257
|
+
timestamp: message.timestamp,
|
|
258
|
+
change: change.value,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const transaction = deps.sqlite.transaction(() => {
|
|
262
|
+
let clockTimestamp = deps.clock.get();
|
|
263
|
+
for (const message of messages) {
|
|
264
|
+
const nextTimestamp = receiveTimestamp(deps)(clockTimestamp, message.timestamp);
|
|
265
|
+
if (!nextTimestamp.ok)
|
|
266
|
+
return nextTimestamp;
|
|
267
|
+
clockTimestamp = nextTimestamp.value;
|
|
268
|
+
}
|
|
269
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
270
|
+
const applyMessagesResult = applyMessages({ ...deps, storage })(owner.id, messages);
|
|
271
|
+
if (!applyMessagesResult.ok)
|
|
272
|
+
return applyMessagesResult;
|
|
273
|
+
}
|
|
274
|
+
// // Apply local mutations atomically with approved messages
|
|
275
|
+
// for (const change of localMutations) {
|
|
276
|
+
// const result = applyLocalOnlyChange(deps)(change);
|
|
277
|
+
// if (!result.ok) return result;
|
|
278
|
+
// }
|
|
279
|
+
return deps.clock.save(clockTimestamp);
|
|
280
|
+
});
|
|
281
|
+
if (!transaction.ok)
|
|
282
|
+
return transaction;
|
|
283
|
+
return ok(true);
|
|
284
|
+
})();
|
|
285
|
+
if (!writeResult.ok) {
|
|
286
|
+
if (writeResult.error.type !== "AbortError") {
|
|
287
|
+
config.onError(writeResult.error);
|
|
288
|
+
}
|
|
289
|
+
return err({ type: "StorageWriteError", ownerId });
|
|
24
290
|
}
|
|
291
|
+
config.onReceive();
|
|
292
|
+
return ok();
|
|
25
293
|
},
|
|
26
|
-
|
|
27
|
-
|
|
294
|
+
readDbChange: (ownerId, timestamp) => {
|
|
295
|
+
const owner = deps.getSyncOwner(ownerIdBytesToOwnerId(ownerId));
|
|
296
|
+
// Owner can be removed to stop syncing.
|
|
297
|
+
if (!owner)
|
|
298
|
+
return null;
|
|
299
|
+
const result = deps.sqlite.exec(sql `
|
|
300
|
+
select "table", "id", "column", "value"
|
|
301
|
+
from evolu_history
|
|
302
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
303
|
+
`);
|
|
304
|
+
if (!result.ok) {
|
|
305
|
+
config.onError(result.error);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const { rows } = result.value;
|
|
309
|
+
assert(rows.length > 0, "Rows must not be empty");
|
|
310
|
+
const { table, id } = rows[0];
|
|
311
|
+
const values = {};
|
|
312
|
+
let isInsert = false;
|
|
313
|
+
let isDelete = null;
|
|
314
|
+
for (const r of rows) {
|
|
315
|
+
assert(r.table === table, "All rows must have the same table");
|
|
316
|
+
assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
|
|
317
|
+
switch (r.column) {
|
|
318
|
+
case "createdAt":
|
|
319
|
+
isInsert = true;
|
|
320
|
+
break;
|
|
321
|
+
case "isDeleted":
|
|
322
|
+
assert(SqliteBoolean.is(r.value), "isDeleted column must contain a valid SqliteBoolean (0 or 1)");
|
|
323
|
+
isDelete = sqliteBooleanToBoolean(r.value);
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
values[r.column] = r.value;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const message = {
|
|
330
|
+
timestamp: timestampBytesToTimestamp(timestamp),
|
|
331
|
+
change: DbChange.orThrow({
|
|
332
|
+
table: rows[0].table,
|
|
333
|
+
id: idBytesToId(rows[0].id),
|
|
334
|
+
values,
|
|
335
|
+
isInsert,
|
|
336
|
+
isDelete,
|
|
337
|
+
}),
|
|
338
|
+
};
|
|
339
|
+
return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
return ok(storage);
|
|
343
|
+
};
|
|
344
|
+
/** Creates a unique identifier for a transport configuration. */
|
|
345
|
+
const createTransportKey = (transportConfig) => {
|
|
346
|
+
return `${transportConfig.type}:${transportConfig.url}`;
|
|
347
|
+
};
|
|
348
|
+
export const applyLocalOnlyChange = (deps) => (change) => {
|
|
349
|
+
if (change.isDelete) {
|
|
350
|
+
const result = deps.sqlite.exec(sql `
|
|
351
|
+
delete from ${sql.identifier(change.table)}
|
|
352
|
+
where id = ${change.id};
|
|
353
|
+
`);
|
|
354
|
+
if (!result.ok)
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const now = deps.time.nowIso();
|
|
359
|
+
const ownerId = deps.appOwner.id;
|
|
360
|
+
let entries = objectToEntries(change.values);
|
|
361
|
+
if (change.isDelete !== null) {
|
|
362
|
+
entries = [...entries, ["isDeleted", sqliteFalse]];
|
|
363
|
+
}
|
|
364
|
+
for (const [column, value] of entries) {
|
|
365
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
366
|
+
insert into ${sql.identifier(change.table)}
|
|
367
|
+
("ownerId", "id", ${sql.identifier(column)}, createdAt, updatedAt)
|
|
368
|
+
values (${ownerId}, ${change.id}, ${value}, ${now}, ${now})
|
|
369
|
+
on conflict ("ownerId", "id") do update
|
|
370
|
+
set
|
|
371
|
+
${sql.identifier(column)} = ${value},
|
|
372
|
+
updatedAt = ${now};
|
|
373
|
+
`);
|
|
374
|
+
if (!result.ok)
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return ok();
|
|
379
|
+
};
|
|
380
|
+
const applyMessages = (deps) => (ownerId, messages) => {
|
|
381
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
382
|
+
const usageResult = getOwnerUsage(deps)(ownerIdBytes, timestampToTimestampBytes(firstInArray(messages).timestamp));
|
|
383
|
+
if (!usageResult.ok)
|
|
384
|
+
return usageResult;
|
|
385
|
+
let { firstTimestamp, lastTimestamp } = usageResult.value;
|
|
386
|
+
for (const message of messages) {
|
|
387
|
+
const date = timestampToDateIso(message.timestamp);
|
|
388
|
+
const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message, date);
|
|
389
|
+
if (!result1.ok)
|
|
390
|
+
return result1;
|
|
391
|
+
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
392
|
+
let strategy;
|
|
393
|
+
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(timestamp, firstTimestamp, lastTimestamp);
|
|
394
|
+
const result2 = applyMessageToTimestampAndHistoryTables(deps)(ownerIdBytes, message, strategy, date);
|
|
395
|
+
if (!result2.ok)
|
|
396
|
+
return result2;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* TODO: Implement proper storedBytes tracking for client using encrypted
|
|
400
|
+
* message sizes (need to figure out how to reuse received or postpone
|
|
401
|
+
* client...).
|
|
402
|
+
*/
|
|
403
|
+
const updateUsage = updateOwnerUsage(deps)(ownerIdBytes, 1, // Placeholder until proper tracking implemented
|
|
404
|
+
firstTimestamp, lastTimestamp);
|
|
405
|
+
if (!updateUsage.ok)
|
|
406
|
+
return updateUsage;
|
|
407
|
+
return ok();
|
|
408
|
+
};
|
|
409
|
+
const applyMessageToAppTable = (deps) => (ownerIdBytes, message, date) => {
|
|
410
|
+
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
411
|
+
const columns = dbChangeToColumns(message.change, date);
|
|
412
|
+
for (const [column, value] of columns) {
|
|
413
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
414
|
+
with
|
|
415
|
+
existingTimestamp as (
|
|
416
|
+
select 1
|
|
417
|
+
from evolu_history
|
|
418
|
+
where
|
|
419
|
+
"ownerId" = ${ownerIdBytes}
|
|
420
|
+
and "table" = ${message.change.table}
|
|
421
|
+
and "id" = ${idToIdBytes(message.change.id)}
|
|
422
|
+
and "column" = ${column}
|
|
423
|
+
and "timestamp" >= ${timestampToTimestampBytes(message.timestamp)}
|
|
424
|
+
limit 1
|
|
425
|
+
)
|
|
426
|
+
insert into ${sql.identifier(message.change.table)}
|
|
427
|
+
("ownerId", "id", ${sql.identifier(column)}, updatedAt)
|
|
428
|
+
select ${ownerId}, ${message.change.id}, ${value}, ${date}
|
|
429
|
+
where not exists (select 1 from existingTimestamp)
|
|
430
|
+
on conflict ("ownerId", "id") do update
|
|
431
|
+
set
|
|
432
|
+
${sql.identifier(column)} = ${value},
|
|
433
|
+
updatedAt = ${date}
|
|
434
|
+
where not exists (select 1 from existingTimestamp);
|
|
435
|
+
`);
|
|
436
|
+
if (!result.ok)
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
return ok();
|
|
440
|
+
};
|
|
441
|
+
export const applyMessageToTimestampAndHistoryTables = (deps) => (ownerId, message, strategy, date) => {
|
|
442
|
+
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
443
|
+
const id = idToIdBytes(message.change.id);
|
|
444
|
+
const result = deps.storage.insertTimestamp(ownerId, timestamp, strategy);
|
|
445
|
+
if (!result.ok)
|
|
446
|
+
return result;
|
|
447
|
+
const columns = dbChangeToColumns(message.change, date);
|
|
448
|
+
for (const [column, value] of columns) {
|
|
449
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
450
|
+
insert into evolu_history
|
|
451
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
452
|
+
values
|
|
453
|
+
(
|
|
454
|
+
${ownerId},
|
|
455
|
+
${message.change.table},
|
|
456
|
+
${id},
|
|
457
|
+
${column},
|
|
458
|
+
${value},
|
|
459
|
+
${timestamp}
|
|
460
|
+
)
|
|
461
|
+
on conflict do nothing;
|
|
462
|
+
`);
|
|
463
|
+
if (!result.ok)
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
return ok();
|
|
467
|
+
};
|
|
468
|
+
const dbChangeToColumns = (change, date) => {
|
|
469
|
+
const entries = [...objectToEntries(change.values)];
|
|
470
|
+
if (change.isInsert) {
|
|
471
|
+
entries.push(["createdAt", date]);
|
|
472
|
+
}
|
|
473
|
+
if (change.isDelete !== null) {
|
|
474
|
+
entries.push(["isDeleted", booleanToSqliteBoolean(change.isDelete)]);
|
|
475
|
+
}
|
|
476
|
+
return entries;
|
|
28
477
|
};
|
|
29
478
|
export const initialSyncState = { type: "SyncStateInitial" };
|