@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
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/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- 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 +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- 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 +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- 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 +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- 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 +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- 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.js +3 -3
- package/dist/src/Evolu/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- 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 +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -82
- 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/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/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -97
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Evolu/Sync.js
CHANGED
|
@@ -1,29 +1,462 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { assert } from "../Assert.js";
|
|
2
|
+
import { concatBytes } from "../Buffer.js";
|
|
3
|
+
import { eqArrayNumber } from "../Eq.js";
|
|
4
|
+
import { createTransferableError } from "../Error.js";
|
|
5
|
+
import { constFalse } from "../Function.js";
|
|
6
|
+
import { objectToEntries } from "../Object.js";
|
|
7
|
+
import { createRefCountedResourceManager } from "../RefCountedResourceManager.js";
|
|
8
|
+
import { ok } from "../Result.js";
|
|
9
|
+
import { sql } 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 { createSqliteStorageBase, } from "./Storage.js";
|
|
15
|
+
import { createInitialTimestamp, receiveTimestamp, sendTimestamp, timestampBytesToTimestamp, timestampToTimestampBytes, timestampToTimestampString, } 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 = createRefCountedResourceManager({
|
|
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 = { timestamp: clockTimestamp, change: dbChange };
|
|
164
|
+
const messages = ownerMessages.get(ownerId);
|
|
165
|
+
if (messages)
|
|
166
|
+
messages.push(message);
|
|
167
|
+
else
|
|
168
|
+
ownerMessages.set(ownerId, [message]);
|
|
169
|
+
}
|
|
170
|
+
for (const [ownerId, messages] of ownerMessages) {
|
|
171
|
+
const result = applyMessages({ ...deps, storage })(ownerId, messages);
|
|
172
|
+
if (!result.ok)
|
|
173
|
+
return result;
|
|
174
|
+
const owner = getSyncOwner(ownerId);
|
|
175
|
+
if (!owner?.writeKey)
|
|
176
|
+
continue;
|
|
177
|
+
const message = createProtocolMessageFromCrdtMessages(deps)({
|
|
178
|
+
id: owner.id,
|
|
179
|
+
encryptionKey: owner.encryptionKey,
|
|
180
|
+
writeKey: owner.writeKey,
|
|
181
|
+
}, messages);
|
|
182
|
+
const transportsToUse = owner.transports ?? config.transports;
|
|
183
|
+
// Send message to all transports for this owner
|
|
184
|
+
for (const transportConfig of transportsToUse) {
|
|
185
|
+
const transportKey = createTransportKey(transportConfig);
|
|
186
|
+
const webSocket = transports.getResource(transportKey);
|
|
187
|
+
if (!webSocket)
|
|
188
|
+
continue;
|
|
189
|
+
if (webSocket.isOpen()) {
|
|
190
|
+
deps.console.log("[sync]", "send", { transportKey, message });
|
|
191
|
+
webSocket.send(message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return deps.clock.save(clockTimestamp);
|
|
196
|
+
},
|
|
197
|
+
[Symbol.dispose]: () => {
|
|
198
|
+
if (isDisposed)
|
|
199
|
+
return;
|
|
200
|
+
isDisposed = true;
|
|
201
|
+
transports[Symbol.dispose]();
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
return ok(sync);
|
|
205
|
+
};
|
|
206
|
+
export const createClock = (deps) => (initialTimestamp = createInitialTimestamp(deps)) => {
|
|
207
|
+
let currentTimestamp = initialTimestamp;
|
|
208
|
+
return {
|
|
209
|
+
get: () => currentTimestamp,
|
|
210
|
+
save: (timestamp) => {
|
|
211
|
+
currentTimestamp = timestamp;
|
|
212
|
+
const timestampString = timestampToTimestampString(timestamp);
|
|
213
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
214
|
+
update evolu_config set "clock" = ${timestampString};
|
|
215
|
+
`);
|
|
216
|
+
if (!result.ok)
|
|
217
|
+
return result;
|
|
218
|
+
return ok();
|
|
13
219
|
},
|
|
14
220
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
221
|
+
};
|
|
222
|
+
const createClientStorage = (deps) => (config) => {
|
|
223
|
+
const sqliteStorageBase = createSqliteStorageBase(deps)({
|
|
224
|
+
onStorageError: config.onError,
|
|
225
|
+
});
|
|
226
|
+
if (!sqliteStorageBase.ok)
|
|
227
|
+
return sqliteStorageBase;
|
|
228
|
+
const mutex = createMutex();
|
|
229
|
+
const storage = {
|
|
230
|
+
...sqliteStorageBase.value,
|
|
231
|
+
validateWriteKey: constFalse,
|
|
232
|
+
setWriteKey: constFalse,
|
|
233
|
+
writeMessages: async (ownerIdBytes, encryptedMessages) => {
|
|
234
|
+
const writeResult = await mutex.withLock(async () => {
|
|
235
|
+
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
236
|
+
const owner = deps.getSyncOwner(ownerId);
|
|
237
|
+
// Owner can be removed during syncing.
|
|
238
|
+
if (!owner)
|
|
239
|
+
return ok(false);
|
|
240
|
+
const messages = [];
|
|
241
|
+
for (const message of encryptedMessages) {
|
|
242
|
+
const change = decryptAndDecodeDbChange(deps)(message, owner.encryptionKey);
|
|
243
|
+
if (!change.ok)
|
|
244
|
+
return change;
|
|
245
|
+
messages.push({
|
|
246
|
+
timestamp: message.timestamp,
|
|
247
|
+
change: change.value,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const transaction = deps.sqlite.transaction(() => {
|
|
251
|
+
let clockTimestamp = deps.clock.get();
|
|
252
|
+
for (const message of messages) {
|
|
253
|
+
const nextTimestamp = receiveTimestamp(deps)(clockTimestamp, message.timestamp);
|
|
254
|
+
if (!nextTimestamp.ok)
|
|
255
|
+
return nextTimestamp;
|
|
256
|
+
clockTimestamp = nextTimestamp.value;
|
|
257
|
+
}
|
|
258
|
+
const applyMessagesResult = applyMessages({ ...deps, storage })(owner.id, messages);
|
|
259
|
+
if (!applyMessagesResult.ok)
|
|
260
|
+
return applyMessagesResult;
|
|
261
|
+
// // Apply local mutations atomically with approved messages
|
|
262
|
+
// for (const change of localMutations) {
|
|
263
|
+
// const result = applyLocalOnlyChange(deps)(change);
|
|
264
|
+
// if (!result.ok) return result;
|
|
265
|
+
// }
|
|
266
|
+
return deps.clock.save(clockTimestamp);
|
|
267
|
+
});
|
|
268
|
+
if (!transaction.ok)
|
|
269
|
+
return transaction;
|
|
270
|
+
return ok(true);
|
|
271
|
+
})();
|
|
272
|
+
if (!writeResult.ok) {
|
|
273
|
+
if (writeResult.error.type !== "AbortError") {
|
|
274
|
+
config.onError(writeResult.error);
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
config.onReceive();
|
|
279
|
+
return true;
|
|
19
280
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
281
|
+
readDbChange: (ownerId, timestamp) => {
|
|
282
|
+
const owner = deps.getSyncOwner(ownerIdBytesToOwnerId(ownerId));
|
|
283
|
+
// Owner can be removed to stop syncing.
|
|
284
|
+
if (!owner)
|
|
285
|
+
return null;
|
|
286
|
+
const result = deps.sqlite.exec(sql `
|
|
287
|
+
select "table", "id", "column", "value"
|
|
288
|
+
from evolu_history
|
|
289
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
290
|
+
`);
|
|
291
|
+
if (!result.ok) {
|
|
292
|
+
config.onError(result.error);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const { rows } = result.value;
|
|
296
|
+
assert(rows.length > 0, "Rows must not be empty");
|
|
297
|
+
const { table, id } = rows[0];
|
|
298
|
+
const values = {};
|
|
299
|
+
for (const r of rows) {
|
|
300
|
+
assert(r.table === table, "All rows must have the same table");
|
|
301
|
+
assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
|
|
302
|
+
values[r.column] = r.value;
|
|
24
303
|
}
|
|
304
|
+
const message = {
|
|
305
|
+
timestamp: timestampBytesToTimestamp(timestamp),
|
|
306
|
+
change: {
|
|
307
|
+
table: rows[0].table,
|
|
308
|
+
id: idBytesToId(rows[0].id),
|
|
309
|
+
values,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
|
|
25
313
|
},
|
|
26
|
-
}
|
|
27
|
-
return
|
|
314
|
+
};
|
|
315
|
+
return ok(storage);
|
|
316
|
+
};
|
|
317
|
+
/** Creates a unique identifier for a transport configuration. */
|
|
318
|
+
const createTransportKey = (transportConfig) => {
|
|
319
|
+
return `${transportConfig.type}:${transportConfig.url}`;
|
|
320
|
+
};
|
|
321
|
+
export const applyLocalOnlyChange = (deps) => (change) => {
|
|
322
|
+
const dbChange = {
|
|
323
|
+
table: change.table,
|
|
324
|
+
id: change.id,
|
|
325
|
+
values: change.values,
|
|
326
|
+
};
|
|
327
|
+
const isDeletion = "isDeleted" in dbChange.values && dbChange.values.isDeleted === 1;
|
|
328
|
+
if (isDeletion) {
|
|
329
|
+
const result = deps.sqlite.exec(sql `
|
|
330
|
+
delete from ${sql.identifier(dbChange.table)}
|
|
331
|
+
where id = ${dbChange.id};
|
|
332
|
+
`);
|
|
333
|
+
if (!result.ok)
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const date = new Date(deps.time.now()).toISOString();
|
|
338
|
+
for (const [column, value] of objectToEntries(dbChange.values)) {
|
|
339
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
340
|
+
insert into ${sql.identifier(dbChange.table)}
|
|
341
|
+
("id", ${sql.identifier(column)}, createdAt, updatedAt)
|
|
342
|
+
values (${dbChange.id}, ${value}, ${date}, ${date})
|
|
343
|
+
on conflict ("id") do update
|
|
344
|
+
set
|
|
345
|
+
${sql.identifier(column)} = ${value},
|
|
346
|
+
updatedAt = ${date};
|
|
347
|
+
`);
|
|
348
|
+
if (!result.ok)
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return ok();
|
|
353
|
+
};
|
|
354
|
+
const applyMessages = (deps) => (ownerId, messages) => {
|
|
355
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
356
|
+
for (const message of messages) {
|
|
357
|
+
const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message);
|
|
358
|
+
if (!result1.ok)
|
|
359
|
+
return result1;
|
|
360
|
+
const result2 = applyMessageToTimestampAndHistoryTables(deps)(ownerIdBytes, message);
|
|
361
|
+
if (!result2.ok)
|
|
362
|
+
return result2;
|
|
363
|
+
}
|
|
364
|
+
return ok();
|
|
365
|
+
};
|
|
366
|
+
const applyMessageToAppTable = (deps) => (ownerId, message) => {
|
|
367
|
+
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
368
|
+
const updatedAt = new Date(message.timestamp.millis).toISOString();
|
|
369
|
+
for (const [column, value] of objectToEntries(message.change.values)) {
|
|
370
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
371
|
+
with
|
|
372
|
+
lastTimestamp as (
|
|
373
|
+
select "timestamp"
|
|
374
|
+
from evolu_history
|
|
375
|
+
where
|
|
376
|
+
"ownerId" = ${ownerId}
|
|
377
|
+
and "table" = ${message.change.table}
|
|
378
|
+
and "id" = ${message.change.id}
|
|
379
|
+
and "column" = ${column}
|
|
380
|
+
order by "timestamp" desc
|
|
381
|
+
limit 1
|
|
382
|
+
)
|
|
383
|
+
insert into ${sql.identifier(message.change.table)}
|
|
384
|
+
("id", ${sql.identifier(column)}, updatedAt)
|
|
385
|
+
select ${message.change.id}, ${value}, ${updatedAt}
|
|
386
|
+
where
|
|
387
|
+
(select "timestamp" from lastTimestamp) is null
|
|
388
|
+
or (select "timestamp" from lastTimestamp) < ${timestamp}
|
|
389
|
+
on conflict ("id") do update
|
|
390
|
+
set
|
|
391
|
+
${sql.identifier(column)} = ${value},
|
|
392
|
+
updatedAt = ${updatedAt}
|
|
393
|
+
where
|
|
394
|
+
(select "timestamp" from lastTimestamp) is null
|
|
395
|
+
or (select "timestamp" from lastTimestamp) < ${timestamp};
|
|
396
|
+
`);
|
|
397
|
+
if (!result.ok)
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
return ok();
|
|
401
|
+
};
|
|
402
|
+
export const applyMessageToTimestampAndHistoryTables = (deps) => (ownerId, message) => {
|
|
403
|
+
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
404
|
+
const id = idToIdBytes(message.change.id);
|
|
405
|
+
const result = deps.storage.insertTimestamp(ownerId, timestamp);
|
|
406
|
+
if (!result.ok)
|
|
407
|
+
return result;
|
|
408
|
+
for (const [column, value] of Object.entries(message.change.values)) {
|
|
409
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
410
|
+
insert into evolu_history
|
|
411
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
412
|
+
values
|
|
413
|
+
(
|
|
414
|
+
${ownerId},
|
|
415
|
+
${message.change.table},
|
|
416
|
+
${id},
|
|
417
|
+
${column},
|
|
418
|
+
${value},
|
|
419
|
+
${timestamp}
|
|
420
|
+
)
|
|
421
|
+
on conflict do nothing;
|
|
422
|
+
`);
|
|
423
|
+
if (!result.ok)
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
return ok();
|
|
28
427
|
};
|
|
29
428
|
export const initialSyncState = { type: "SyncStateInitial" };
|
|
429
|
+
/**
|
|
430
|
+
* Efficiently checks which binary timestamps already exist in the database
|
|
431
|
+
* using a single CTE query instead of N individual queries. Crucial for WASM
|
|
432
|
+
* SQLite performance where JS↔WASM boundary crossings are expensive.
|
|
433
|
+
*
|
|
434
|
+
* Used for fast idempotency detection in writeMessages before onMessage
|
|
435
|
+
* validation. While applyMessages ensures internal idempotency, this pre-check
|
|
436
|
+
* is faster and required for main thread message validation.
|
|
437
|
+
*/
|
|
438
|
+
export const getExistingTimestamps = (deps) => (ownerIdBytes, timestampsBytes) => {
|
|
439
|
+
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
440
|
+
const result = deps.sqlite.exec(sql `
|
|
441
|
+
with recursive
|
|
442
|
+
split_timestamps(timestampBytes, pos) as (
|
|
443
|
+
select
|
|
444
|
+
substr(${concatenatedTimestamps}, 1, 16),
|
|
445
|
+
17 as pos
|
|
446
|
+
union all
|
|
447
|
+
select
|
|
448
|
+
substr(${concatenatedTimestamps}, pos, 16),
|
|
449
|
+
pos + 16
|
|
450
|
+
from split_timestamps
|
|
451
|
+
where pos <= length(${concatenatedTimestamps})
|
|
452
|
+
)
|
|
453
|
+
select s.timestampBytes
|
|
454
|
+
from
|
|
455
|
+
split_timestamps s
|
|
456
|
+
join evolu_timestamp t
|
|
457
|
+
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
458
|
+
`);
|
|
459
|
+
if (!result.ok)
|
|
460
|
+
return result;
|
|
461
|
+
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
462
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Brand } from "../Brand.js";
|
|
2
|
+
import { RandomBytesDep } from "../Crypto.js";
|
|
2
3
|
import { Order } from "../Order.js";
|
|
3
4
|
import { Result } from "../Result.js";
|
|
4
5
|
import { TimeDep } from "../Time.js";
|
|
5
|
-
import {
|
|
6
|
+
import { InferType } from "../Type.js";
|
|
6
7
|
export interface TimestampConfig {
|
|
7
8
|
/**
|
|
8
9
|
* Maximum physical clock drift allowed in ms.
|
|
@@ -14,7 +15,7 @@ export interface TimestampConfig {
|
|
|
14
15
|
export interface TimestampConfigDep {
|
|
15
16
|
readonly timestampConfig: TimestampConfig;
|
|
16
17
|
}
|
|
17
|
-
export type TimestampError = TimestampDriftError | TimestampCounterOverflowError |
|
|
18
|
+
export type TimestampError = TimestampDriftError | TimestampCounterOverflowError | TimestampTimeOutOfRangeError;
|
|
18
19
|
export interface TimestampDriftError {
|
|
19
20
|
readonly type: "TimestampDriftError";
|
|
20
21
|
readonly next: Millis;
|
|
@@ -23,10 +24,6 @@ export interface TimestampDriftError {
|
|
|
23
24
|
export interface TimestampCounterOverflowError {
|
|
24
25
|
readonly type: "TimestampCounterOverflowError";
|
|
25
26
|
}
|
|
26
|
-
export interface TimestampDuplicateNodeError {
|
|
27
|
-
readonly type: "TimestampDuplicateNodeError";
|
|
28
|
-
readonly nodeId: NodeId;
|
|
29
|
-
}
|
|
30
27
|
export interface TimestampTimeOutOfRangeError {
|
|
31
28
|
readonly type: "TimestampTimeOutOfRangeError";
|
|
32
29
|
}
|
|
@@ -42,11 +39,11 @@ export interface TimestampTimeOutOfRangeError {
|
|
|
42
39
|
*
|
|
43
40
|
* `new Date(281474976710654).toString()` = Tue Aug 02 10889 07:31:49
|
|
44
41
|
*/
|
|
45
|
-
export declare const Millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").
|
|
42
|
+
export declare const Millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, `LessThanOrEqualTo${number}`, import("../Type.js").LessThanOrEqualToError<number>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Millis", import("../Type.js").BrandWithoutRefineError<"Millis", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<number>>, never>;
|
|
46
43
|
export type Millis = typeof Millis.Type;
|
|
47
44
|
export declare const minMillis: Millis;
|
|
48
45
|
export declare const maxMillis: Millis;
|
|
49
|
-
export declare const Counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").
|
|
46
|
+
export declare const Counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, "LessThanOrEqualTo65535", import("../Type.js").LessThanOrEqualToError<65535>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Counter", import("../Type.js").BrandWithoutRefineError<"Counter", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<65535>>, never>;
|
|
50
47
|
export type Counter = typeof Counter.Type;
|
|
51
48
|
export declare const minCounter: Counter;
|
|
52
49
|
export declare const maxCounter: Counter;
|
|
@@ -63,14 +60,17 @@ export declare const maxCounter: Counter;
|
|
|
63
60
|
*
|
|
64
61
|
* https://lemire.me/blog/2019/12/12/are-64-bit-random-identifiers-free-from-collision
|
|
65
62
|
*
|
|
66
|
-
* What
|
|
63
|
+
* What happens if different devices generate the same NodeId?
|
|
67
64
|
*
|
|
68
|
-
* If the
|
|
69
|
-
* different owner have different owner IDs. Timestamps are partitioned by
|
|
70
|
-
* OwnerId.
|
|
65
|
+
* If devices with the same NodeId use different owners, no issues occur.
|
|
71
66
|
*
|
|
72
|
-
* If the
|
|
73
|
-
*
|
|
67
|
+
* If devices with the same NodeId use the same owner, problems only arise when
|
|
68
|
+
* they generate CRDT messages with identical timestamps (same millis, counter,
|
|
69
|
+
* and NodeId). In this case, the protocol sync algorithm treats them as the
|
|
70
|
+
* same message: the first will be synced with the relay, while the affected
|
|
71
|
+
* message will not be delivered. The affected devices will see different data
|
|
72
|
+
* yet they will think they are synced. This is extremely rare and can be
|
|
73
|
+
* resolved by resetting one device to generate a new NodeId.
|
|
74
74
|
*/
|
|
75
75
|
export declare const NodeId: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "NodeId", import("../Type.js").RegexError<"NodeId">, import("../Type.js").StringError>;
|
|
76
76
|
export type NodeId = typeof NodeId.Type;
|
|
@@ -84,11 +84,12 @@ export declare const maxNodeId: NodeId;
|
|
|
84
84
|
* - https://jaredforsyth.com/posts/hybrid-logical-clocks/
|
|
85
85
|
*/
|
|
86
86
|
export declare const Timestamp: import("../Type.js").ObjectType<{
|
|
87
|
-
millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").
|
|
88
|
-
counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").
|
|
87
|
+
millis: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, `LessThanOrEqualTo${number}`, import("../Type.js").LessThanOrEqualToError<number>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Millis", import("../Type.js").BrandWithoutRefineError<"Millis", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<number>>, never>;
|
|
88
|
+
counter: import("../Type.js").BrandType<import("../Type.js").BrandType<import("../Type.js").Type<"Brand", number & Brand<"Int"> & Brand<"NonNegative">, number, import("../Type.js").NonNegativeError, number & Brand<"Int">, import("../Type.js").IntError | import("../Type.js").NumberError>, "LessThanOrEqualTo65535", import("../Type.js").LessThanOrEqualToError<65535>, import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError>, "Counter", import("../Type.js").BrandWithoutRefineError<"Counter", import("../Type.js").NonNegativeError | import("../Type.js").IntError | import("../Type.js").NumberError | import("../Type.js").LessThanOrEqualToError<65535>>, never>;
|
|
89
89
|
nodeId: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "NodeId", import("../Type.js").RegexError<"NodeId">, import("../Type.js").StringError>;
|
|
90
90
|
}>;
|
|
91
|
-
export
|
|
91
|
+
export interface Timestamp extends InferType<typeof Timestamp> {
|
|
92
|
+
}
|
|
92
93
|
/** Equality function for comparing {@link Timestamp}. */
|
|
93
94
|
export declare const eqTimestamp: import("../Eq.js").Eq<{
|
|
94
95
|
readonly millis: number & Brand<"Int"> & Brand<"NonNegative"> & Brand<`LessThanOrEqualTo${number}`> & Brand<"Millis">;
|
|
@@ -96,17 +97,18 @@ export declare const eqTimestamp: import("../Eq.js").Eq<{
|
|
|
96
97
|
readonly nodeId: string & Brand<"NodeId">;
|
|
97
98
|
}>;
|
|
98
99
|
export declare const createTimestamp: ({ millis, counter, nodeId, }?: Partial<Timestamp>) => Timestamp;
|
|
99
|
-
export declare const createInitialTimestamp: (deps:
|
|
100
|
-
/**
|
|
100
|
+
export declare const createInitialTimestamp: (deps: RandomBytesDep) => Timestamp;
|
|
101
|
+
/** Sortable string representation of {@link Timestamp}. */
|
|
101
102
|
export type TimestampString = string & Brand<"TimestampString">;
|
|
102
103
|
export declare const timestampToTimestampString: (t: Timestamp) => TimestampString;
|
|
103
104
|
export declare const timestampStringToTimestamp: (timestampString: TimestampString) => Timestamp;
|
|
104
105
|
export declare const sendTimestamp: (deps: TimeDep & TimestampConfigDep) => (timestamp: Timestamp) => Result<Timestamp, TimestampDriftError | TimestampCounterOverflowError | TimestampTimeOutOfRangeError>;
|
|
105
|
-
export declare const receiveTimestamp: (deps: TimeDep & TimestampConfigDep) => (local: Timestamp, remote: Timestamp) => Result<Timestamp, TimestampDriftError | TimestampCounterOverflowError |
|
|
106
|
-
/**
|
|
107
|
-
export
|
|
108
|
-
export
|
|
109
|
-
export declare const
|
|
110
|
-
export declare const
|
|
111
|
-
export declare const
|
|
106
|
+
export declare const receiveTimestamp: (deps: TimeDep & TimestampConfigDep) => (local: Timestamp, remote: Timestamp) => Result<Timestamp, TimestampDriftError | TimestampCounterOverflowError | TimestampTimeOutOfRangeError>;
|
|
107
|
+
/** Sortable bytes representation of {@link Timestamp}. */
|
|
108
|
+
export declare const TimestampBytes: import("../Type.js").BrandType<import("../Type.js").Type<"Uint8Array", Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError>, "TimestampBytes", import("../Type.js").BrandWithoutRefineError<"TimestampBytes", import("../Type.js").Uint8ArrayError>, never>;
|
|
109
|
+
export type TimestampBytes = typeof TimestampBytes.Type;
|
|
110
|
+
export declare const timestampBytesLength: number & Brand<"Int"> & Brand<"NonNegative">;
|
|
111
|
+
export declare const timestampToTimestampBytes: (timestamp: Timestamp) => TimestampBytes;
|
|
112
|
+
export declare const timestampBytesToTimestamp: (timestamp: TimestampBytes) => Timestamp;
|
|
113
|
+
export declare const orderTimestampBytes: Order<TimestampBytes>;
|
|
112
114
|
//# sourceMappingURL=Timestamp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Timestamp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Timestamp.ts"],"names":[],"mappings":"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"}
|