@evolu/common 7.4.0 → 8.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
import { appendToArray, firstInArray, isNonEmptyReadonlyArray, } from "../Array.js";
|
|
2
|
-
import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
3
|
-
import { eqArrayNumber } from "../Eq.js";
|
|
4
|
-
import { createTransferableError } from "../Error.js";
|
|
5
|
-
import { constFalse, constTrue } from "../Function.js";
|
|
6
|
-
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
7
|
-
import { createResources } from "../Resources.js";
|
|
8
|
-
import { err, ok } from "../Result.js";
|
|
9
|
-
import { booleanToSqliteBoolean, sql, SqliteBoolean, sqliteBooleanToBoolean, } from "../Sqlite.js";
|
|
10
|
-
import { createMutex } from "../Task.js";
|
|
11
|
-
import { Boolean, 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 { systemColumns } from "./Schema.js";
|
|
15
|
-
import { createBaseSqliteStorage, DbChange, getOwnerUsage, getTimestampInsertStrategy, updateOwnerUsage, } from "./Storage.js";
|
|
16
|
-
import { createInitialTimestamp, receiveTimestamp, sendTimestamp, timestampBytesToTimestamp, timestampToDateIso, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
17
|
-
export const createSync = (deps) => (config) => {
|
|
18
|
-
let isDisposed = false;
|
|
19
|
-
/** Returns owner data only if actively assigned to at least one transport. */
|
|
20
|
-
const getSyncOwner = (ownerId) => {
|
|
21
|
-
if (isDisposed)
|
|
22
|
-
return null;
|
|
23
|
-
return resources.getConsumer(ownerId);
|
|
24
|
-
};
|
|
25
|
-
const storageResult = createClientStorage({
|
|
26
|
-
...deps,
|
|
27
|
-
getSyncOwner,
|
|
28
|
-
})(config);
|
|
29
|
-
if (!storageResult.ok)
|
|
30
|
-
return storageResult;
|
|
31
|
-
const storage = storageResult.value;
|
|
32
|
-
const createResource = (transport) => {
|
|
33
|
-
const transportKey = createTransportKey(transport);
|
|
34
|
-
deps.console.log("[sync]", "createWebSocket", {
|
|
35
|
-
transportKey,
|
|
36
|
-
url: transport.url,
|
|
37
|
-
});
|
|
38
|
-
return deps.createWebSocket(transport.url, {
|
|
39
|
-
binaryType: "arraybuffer",
|
|
40
|
-
onOpen: () => {
|
|
41
|
-
if (isDisposed)
|
|
42
|
-
return;
|
|
43
|
-
const webSocket = resources.getResource(transportKey);
|
|
44
|
-
if (!webSocket)
|
|
45
|
-
return;
|
|
46
|
-
const ownerIds = resources.getConsumersForResource(transportKey);
|
|
47
|
-
deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
|
|
48
|
-
for (const ownerId of ownerIds) {
|
|
49
|
-
const message = createProtocolMessageForSync({ storage })(ownerId, SubscriptionFlags.Subscribe);
|
|
50
|
-
if (!message)
|
|
51
|
-
continue;
|
|
52
|
-
deps.console.log("[sync]", "send", { message });
|
|
53
|
-
webSocket.send(message);
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
onClose: (event) => {
|
|
57
|
-
deps.console.log("[sync]", "onClose", {
|
|
58
|
-
transportKey,
|
|
59
|
-
code: event.code,
|
|
60
|
-
reason: event.reason,
|
|
61
|
-
wasClean: event.wasClean,
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
onError: (error) => {
|
|
65
|
-
deps.console.warn("[sync]", "onError", { transportKey, error });
|
|
66
|
-
},
|
|
67
|
-
onMessage: (data) => {
|
|
68
|
-
// Only handle ArrayBuffer data for sync messages
|
|
69
|
-
if (isDisposed || !(data instanceof ArrayBuffer))
|
|
70
|
-
return;
|
|
71
|
-
const webSocket = resources.getResource(transportKey);
|
|
72
|
-
if (!webSocket)
|
|
73
|
-
return;
|
|
74
|
-
const input = new Uint8Array(data);
|
|
75
|
-
deps.console.log("[sync]", "onMessage", {
|
|
76
|
-
transportKey,
|
|
77
|
-
message: input,
|
|
78
|
-
});
|
|
79
|
-
applyProtocolMessageAsClient({ storage })(input, {
|
|
80
|
-
// No write key, no sync (for a case when an owner was unused).
|
|
81
|
-
getWriteKey: (ownerId) => getSyncOwner(ownerId)?.writeKey ?? null,
|
|
82
|
-
})
|
|
83
|
-
.then((message) => {
|
|
84
|
-
if (!message.ok) {
|
|
85
|
-
config.onError(message.error);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
switch (message.value.type) {
|
|
89
|
-
case "response":
|
|
90
|
-
webSocket.send(message.value.message);
|
|
91
|
-
break;
|
|
92
|
-
case "no-response":
|
|
93
|
-
// Sync complete, no response needed
|
|
94
|
-
break;
|
|
95
|
-
case "broadcast":
|
|
96
|
-
// This was a broadcast message, don't affect sync counter
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
.catch((error) => {
|
|
101
|
-
config.onError(createTransferableError(error));
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
const resources = createResources({
|
|
107
|
-
createResource,
|
|
108
|
-
getResourceKey: createTransportKey,
|
|
109
|
-
getConsumerId: (owner) => owner.id,
|
|
110
|
-
disposalDelay: config.disposalDelayMs ?? 100,
|
|
111
|
-
onConsumerAdded: (owner, webSocket) => {
|
|
112
|
-
deps.console.log("[sync]", "onConsumerAdded", {
|
|
113
|
-
ownerId: owner.id,
|
|
114
|
-
isOpen: webSocket.isOpen(),
|
|
115
|
-
});
|
|
116
|
-
// The onOpen handler will sync it.
|
|
117
|
-
if (!webSocket.isOpen())
|
|
118
|
-
return;
|
|
119
|
-
const message = createProtocolMessageForSync({ storage })(owner.id, SubscriptionFlags.Subscribe);
|
|
120
|
-
if (message)
|
|
121
|
-
webSocket.send(message);
|
|
122
|
-
},
|
|
123
|
-
onConsumerRemoved: (owner, webSocket) => {
|
|
124
|
-
deps.console.log("[sync]", "onConsumerRemoved", {
|
|
125
|
-
ownerId: owner.id,
|
|
126
|
-
isOpen: webSocket.isOpen(),
|
|
127
|
-
});
|
|
128
|
-
const message = createProtocolMessageForUnsubscribe(owner.id);
|
|
129
|
-
webSocket.send(message);
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
const sync = {
|
|
133
|
-
useOwner: (use, owner) => {
|
|
134
|
-
if (isDisposed) {
|
|
135
|
-
deps.console.warn("[sync]", "useOwner called on disposed Sync instance", { owner });
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
deps.console.log("[sync]", "useOwner", { use, owner });
|
|
139
|
-
const transports = owner.transports ?? config.transports;
|
|
140
|
-
if (use) {
|
|
141
|
-
resources.addConsumer(owner, transports);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
const result = resources.removeConsumer(owner, transports);
|
|
145
|
-
if (!result.ok) {
|
|
146
|
-
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
147
|
-
transports,
|
|
148
|
-
ownerId: owner.id,
|
|
149
|
-
error: result.error,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
applyChanges: (changes) => {
|
|
155
|
-
deps.console.log("[sync]", "applyChanges", { changes });
|
|
156
|
-
let clockTimestamp = deps.clock.get();
|
|
157
|
-
const ownerMessages = new Map();
|
|
158
|
-
for (const change of changes) {
|
|
159
|
-
const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
|
|
160
|
-
if (!nextTimestamp.ok)
|
|
161
|
-
return nextTimestamp;
|
|
162
|
-
clockTimestamp = nextTimestamp.value;
|
|
163
|
-
const { ownerId = config.appOwner.id, ...dbChange } = change;
|
|
164
|
-
const message = {
|
|
165
|
-
timestamp: clockTimestamp,
|
|
166
|
-
change: dbChange,
|
|
167
|
-
};
|
|
168
|
-
const messages = ownerMessages.get(ownerId);
|
|
169
|
-
if (messages)
|
|
170
|
-
messages.push(message);
|
|
171
|
-
else
|
|
172
|
-
ownerMessages.set(ownerId, [message]);
|
|
173
|
-
}
|
|
174
|
-
for (const [ownerId, messages] of ownerMessages) {
|
|
175
|
-
const result = applyMessages({ ...deps, storage })(ownerId, messages);
|
|
176
|
-
if (!result.ok)
|
|
177
|
-
return result;
|
|
178
|
-
const owner = getSyncOwner(ownerId);
|
|
179
|
-
if (!owner?.writeKey)
|
|
180
|
-
continue;
|
|
181
|
-
const message = createProtocolMessageFromCrdtMessages(deps)({
|
|
182
|
-
id: owner.id,
|
|
183
|
-
encryptionKey: owner.encryptionKey,
|
|
184
|
-
writeKey: owner.writeKey,
|
|
185
|
-
}, messages);
|
|
186
|
-
const transports = owner.transports ?? config.transports;
|
|
187
|
-
// Send message to all transports for this owner
|
|
188
|
-
for (const transport of transports) {
|
|
189
|
-
const transportKey = createTransportKey(transport);
|
|
190
|
-
const webSocket = resources.getResource(transportKey);
|
|
191
|
-
if (!webSocket)
|
|
192
|
-
continue;
|
|
193
|
-
if (webSocket.isOpen()) {
|
|
194
|
-
deps.console.log("[sync]", "send", { transportKey, message });
|
|
195
|
-
webSocket.send(message);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return deps.clock.save(clockTimestamp);
|
|
200
|
-
},
|
|
201
|
-
[Symbol.dispose]: () => {
|
|
202
|
-
if (isDisposed)
|
|
203
|
-
return;
|
|
204
|
-
isDisposed = true;
|
|
205
|
-
resources[Symbol.dispose]();
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
return ok(sync);
|
|
209
|
-
};
|
|
210
|
-
export const createClock = (deps) => (initialTimestamp = createInitialTimestamp(deps)) => {
|
|
211
|
-
let currentTimestamp = initialTimestamp;
|
|
212
|
-
return {
|
|
213
|
-
get: () => currentTimestamp,
|
|
214
|
-
save: (timestamp) => {
|
|
215
|
-
currentTimestamp = timestamp;
|
|
216
|
-
const result = deps.sqlite.exec(sql.prepared `
|
|
217
|
-
update evolu_config
|
|
218
|
-
set "clock" = ${timestampToTimestampBytes(timestamp)};
|
|
219
|
-
`);
|
|
220
|
-
if (!result.ok)
|
|
221
|
-
return result;
|
|
222
|
-
return ok();
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
};
|
|
226
|
-
const createClientStorage = (deps) => (config) => {
|
|
227
|
-
const sqliteStorageBase = createBaseSqliteStorage(deps)({
|
|
228
|
-
onStorageError: config.onError,
|
|
229
|
-
isOwnerWithinQuota: constTrue, // Clients don't have quota limits
|
|
230
|
-
});
|
|
231
|
-
// TODO: Mutex per OwnerId
|
|
232
|
-
const mutex = createMutex();
|
|
233
|
-
const storage = {
|
|
234
|
-
...sqliteStorageBase,
|
|
235
|
-
// Not implemented yet.
|
|
236
|
-
validateWriteKey: constFalse,
|
|
237
|
-
setWriteKey: constFalse,
|
|
238
|
-
writeMessages: async (ownerIdBytes, encryptedMessages) => {
|
|
239
|
-
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
240
|
-
// Everything is sync now, but we will need async crypto in the future.
|
|
241
|
-
const result = await mutex.withLock(async () => {
|
|
242
|
-
const owner = deps.getSyncOwner(ownerId);
|
|
243
|
-
// Owner can be removed during syncing.
|
|
244
|
-
// `ok(true)` means success, we just skipped the write.
|
|
245
|
-
if (!owner)
|
|
246
|
-
return ok(true);
|
|
247
|
-
// TODO: Add quota checking for collaborative scenarios.
|
|
248
|
-
// When receiving messages from other owners via relay broadcast,
|
|
249
|
-
// check if this owner is within quota before accepting the data.
|
|
250
|
-
// This prevents an owner from exceeding storage limits when receiving
|
|
251
|
-
// data shared by other collaborators.
|
|
252
|
-
const messages = [];
|
|
253
|
-
for (const message of encryptedMessages) {
|
|
254
|
-
const change = decryptAndDecodeDbChange(deps)(message, owner.encryptionKey);
|
|
255
|
-
if (!change.ok)
|
|
256
|
-
return change;
|
|
257
|
-
messages.push({
|
|
258
|
-
timestamp: message.timestamp,
|
|
259
|
-
change: change.value,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
const transaction = deps.sqlite.transaction(() => {
|
|
263
|
-
let clockTimestamp = deps.clock.get();
|
|
264
|
-
for (const message of messages) {
|
|
265
|
-
const nextTimestamp = receiveTimestamp(deps)(clockTimestamp, message.timestamp);
|
|
266
|
-
if (!nextTimestamp.ok)
|
|
267
|
-
return nextTimestamp;
|
|
268
|
-
clockTimestamp = nextTimestamp.value;
|
|
269
|
-
}
|
|
270
|
-
if (isNonEmptyReadonlyArray(messages)) {
|
|
271
|
-
const result = applyMessages({ ...deps, storage })(owner.id, messages);
|
|
272
|
-
if (!result.ok)
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
|
-
return deps.clock.save(clockTimestamp);
|
|
276
|
-
});
|
|
277
|
-
if (!transaction.ok)
|
|
278
|
-
return transaction;
|
|
279
|
-
return ok(true);
|
|
280
|
-
})();
|
|
281
|
-
if (!result.ok) {
|
|
282
|
-
if (result.error.type !== "AbortError") {
|
|
283
|
-
config.onError(result.error);
|
|
284
|
-
}
|
|
285
|
-
return err({ type: "StorageWriteError", ownerId });
|
|
286
|
-
}
|
|
287
|
-
config.onReceive();
|
|
288
|
-
return ok();
|
|
289
|
-
},
|
|
290
|
-
readDbChange: (ownerId, timestamp) => {
|
|
291
|
-
const owner = deps.getSyncOwner(ownerIdBytesToOwnerId(ownerId));
|
|
292
|
-
// Owner can be removed to stop syncing.
|
|
293
|
-
if (!owner)
|
|
294
|
-
return null;
|
|
295
|
-
const result = deps.sqlite.exec(sql `
|
|
296
|
-
select "table", "id", "column", "value"
|
|
297
|
-
from evolu_history
|
|
298
|
-
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
299
|
-
union all
|
|
300
|
-
select "table", "id", "column", "value"
|
|
301
|
-
from evolu_message_quarantine
|
|
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
|
-
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows in evolu_history or quarantine");
|
|
310
|
-
const { table, id } = firstInArray(rows);
|
|
311
|
-
const values = createRecord();
|
|
312
|
-
let isInsert;
|
|
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 "updatedAt":
|
|
322
|
-
isInsert = false;
|
|
323
|
-
break;
|
|
324
|
-
case "isDeleted":
|
|
325
|
-
assert(SqliteBoolean.is(r.value), "isDeleted column must contain a valid SqliteBoolean");
|
|
326
|
-
isDelete = sqliteBooleanToBoolean(r.value);
|
|
327
|
-
break;
|
|
328
|
-
default:
|
|
329
|
-
values[r.column] = r.value;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
assert(Boolean.is(isInsert), "isInsert must be in evolu_history");
|
|
333
|
-
const message = {
|
|
334
|
-
timestamp: timestampBytesToTimestamp(timestamp),
|
|
335
|
-
change: DbChange.orThrow({
|
|
336
|
-
table: rows[0].table,
|
|
337
|
-
id: idBytesToId(rows[0].id),
|
|
338
|
-
values,
|
|
339
|
-
isInsert,
|
|
340
|
-
isDelete,
|
|
341
|
-
}),
|
|
342
|
-
};
|
|
343
|
-
return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
return ok(storage);
|
|
347
|
-
};
|
|
348
|
-
/** Creates a unique identifier for a {@link OwnerTransport}. */
|
|
349
|
-
const createTransportKey = (transport) => {
|
|
350
|
-
return `${transport.type}:${transport.url}`;
|
|
351
|
-
};
|
|
352
|
-
const dbChangeToColumns = (change, now) => {
|
|
353
|
-
let values = objectToEntries(change.values);
|
|
354
|
-
// SystemColumns are not encoded in change.values.
|
|
355
|
-
values = appendToArray(values, [
|
|
356
|
-
change.isInsert ? "createdAt" : "updatedAt",
|
|
357
|
-
now,
|
|
358
|
-
]);
|
|
359
|
-
if (change.isDelete != null) {
|
|
360
|
-
values = appendToArray(values, [
|
|
361
|
-
"isDeleted",
|
|
362
|
-
booleanToSqliteBoolean(change.isDelete),
|
|
363
|
-
]);
|
|
364
|
-
}
|
|
365
|
-
return values;
|
|
366
|
-
};
|
|
367
|
-
export const applyLocalOnlyChange = (deps) => (change) => {
|
|
368
|
-
if (change.isDelete) {
|
|
369
|
-
const result = deps.sqlite.exec(sql `
|
|
370
|
-
delete from ${sql.identifier(change.table)}
|
|
371
|
-
where id = ${change.id};
|
|
372
|
-
`);
|
|
373
|
-
if (!result.ok)
|
|
374
|
-
return result;
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
const ownerId = deps.appOwner.id;
|
|
378
|
-
const columns = dbChangeToColumns(change, deps.time.nowIso());
|
|
379
|
-
for (const [column, value] of columns) {
|
|
380
|
-
const result = deps.sqlite.exec(sql.prepared `
|
|
381
|
-
insert into ${sql.identifier(change.table)}
|
|
382
|
-
("ownerId", "id", ${sql.identifier(column)})
|
|
383
|
-
values (${ownerId}, ${change.id}, ${value})
|
|
384
|
-
on conflict ("ownerId", "id") do update
|
|
385
|
-
set ${sql.identifier(column)} = ${value};
|
|
386
|
-
`);
|
|
387
|
-
if (!result.ok)
|
|
388
|
-
return result;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return ok();
|
|
392
|
-
};
|
|
393
|
-
const applyMessages = (deps) => (ownerId, messages) => {
|
|
394
|
-
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
395
|
-
const usage = getOwnerUsage(deps)(ownerIdBytes, timestampToTimestampBytes(firstInArray(messages).timestamp));
|
|
396
|
-
if (!usage.ok)
|
|
397
|
-
return usage;
|
|
398
|
-
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
399
|
-
for (const { timestamp, change } of messages) {
|
|
400
|
-
const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
|
|
401
|
-
const idBytes = idToIdBytes(change.id);
|
|
402
|
-
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
403
|
-
for (const [column, value] of columns) {
|
|
404
|
-
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
405
|
-
const result = applyColumnChange(deps)(ownerIdBytes, ownerId, change.table, idBytes, change.id, column, value, timestampBytes);
|
|
406
|
-
if (!result.ok)
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
const result = deps.sqlite.exec(sql.prepared `
|
|
411
|
-
insert into evolu_message_quarantine
|
|
412
|
-
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
413
|
-
values
|
|
414
|
-
(
|
|
415
|
-
${ownerIdBytes},
|
|
416
|
-
${timestampBytes},
|
|
417
|
-
${change.table},
|
|
418
|
-
${idBytes},
|
|
419
|
-
${column},
|
|
420
|
-
${value}
|
|
421
|
-
)
|
|
422
|
-
on conflict do nothing;
|
|
423
|
-
`);
|
|
424
|
-
if (!result.ok)
|
|
425
|
-
return result;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
let strategy;
|
|
429
|
-
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(timestampBytes, firstTimestamp, lastTimestamp);
|
|
430
|
-
const result = deps.storage.insertTimestamp(ownerIdBytes, timestampBytes, strategy);
|
|
431
|
-
if (!result.ok)
|
|
432
|
-
return result;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* TODO: Implement proper storedBytes tracking for client using received and
|
|
436
|
-
* sent encrypted message sizes.
|
|
437
|
-
*/
|
|
438
|
-
return updateOwnerUsage(deps)(ownerIdBytes, 1, // Placeholder until proper tracking implemented
|
|
439
|
-
firstTimestamp, lastTimestamp);
|
|
440
|
-
};
|
|
441
|
-
/**
|
|
442
|
-
* System columns that can appear in sync messages. Excludes `ownerId` because
|
|
443
|
-
* it's handled separately (stored per-row, not per-column in messages).
|
|
444
|
-
*/
|
|
445
|
-
const systemColumnsWithoutOwnerId = systemColumns.difference(new Set(["ownerId"]));
|
|
446
|
-
const validateColumnValue = (deps) => (table, column, _value) => {
|
|
447
|
-
const schemaColumns = getProperty(deps.dbSchema.tables, table);
|
|
448
|
-
return (schemaColumns != null &&
|
|
449
|
-
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column)));
|
|
450
|
-
};
|
|
451
|
-
const applyColumnChange = (deps) => (ownerIdBytes, ownerId, table, idBytes, id, column, value, timestampBytes) => {
|
|
452
|
-
const result = deps.sqlite.exec(sql.prepared `
|
|
453
|
-
with
|
|
454
|
-
existingTimestamp as (
|
|
455
|
-
select 1
|
|
456
|
-
from evolu_history
|
|
457
|
-
where
|
|
458
|
-
"ownerId" = ${ownerIdBytes}
|
|
459
|
-
and "table" = ${table}
|
|
460
|
-
and "id" = ${idBytes}
|
|
461
|
-
and "column" = ${column}
|
|
462
|
-
and "timestamp" >= ${timestampBytes}
|
|
463
|
-
limit 1
|
|
464
|
-
)
|
|
465
|
-
insert into ${sql.identifier(table)}
|
|
466
|
-
("ownerId", "id", ${sql.identifier(column)})
|
|
467
|
-
select ${ownerId}, ${id}, ${value}
|
|
468
|
-
where not exists (select 1 from existingTimestamp)
|
|
469
|
-
on conflict ("ownerId", "id") do update
|
|
470
|
-
set ${sql.identifier(column)} = ${value}
|
|
471
|
-
where not exists (select 1 from existingTimestamp);
|
|
472
|
-
`);
|
|
473
|
-
if (!result.ok)
|
|
474
|
-
return result;
|
|
475
|
-
{
|
|
476
|
-
const result = deps.sqlite.exec(sql.prepared `
|
|
477
|
-
insert into evolu_history
|
|
478
|
-
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
479
|
-
values
|
|
480
|
-
(
|
|
481
|
-
${ownerIdBytes},
|
|
482
|
-
${table},
|
|
483
|
-
${idBytes},
|
|
484
|
-
${column},
|
|
485
|
-
${value},
|
|
486
|
-
${timestampBytes}
|
|
487
|
-
)
|
|
488
|
-
on conflict do nothing;
|
|
489
|
-
`);
|
|
490
|
-
if (!result.ok)
|
|
491
|
-
return result;
|
|
492
|
-
}
|
|
493
|
-
return ok();
|
|
494
|
-
};
|
|
495
|
-
/**
|
|
496
|
-
* Attempts to apply quarantined messages that may now be valid after a schema
|
|
497
|
-
* update. Messages are quarantined when they reference tables or columns that
|
|
498
|
-
* don't exist in the current schema (e.g., from a newer app version).
|
|
499
|
-
*/
|
|
500
|
-
export const tryApplyQuarantinedMessages = (deps) => () => {
|
|
501
|
-
const rows = deps.sqlite.exec(sql `
|
|
502
|
-
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
503
|
-
from evolu_message_quarantine;
|
|
504
|
-
`);
|
|
505
|
-
if (!rows.ok)
|
|
506
|
-
return rows;
|
|
507
|
-
for (const row of rows.value.rows) {
|
|
508
|
-
if (!validateColumnValue(deps)(row.table, row.column, row.value))
|
|
509
|
-
continue;
|
|
510
|
-
const result = applyColumnChange(deps)(row.ownerId, ownerIdBytesToOwnerId(row.ownerId), row.table, row.id, idBytesToId(row.id), row.column, row.value, row.timestamp);
|
|
511
|
-
if (!result.ok)
|
|
512
|
-
return result;
|
|
513
|
-
{
|
|
514
|
-
const result = deps.sqlite.exec(sql `
|
|
515
|
-
delete from evolu_message_quarantine
|
|
516
|
-
where
|
|
517
|
-
"ownerId" = ${row.ownerId}
|
|
518
|
-
and "timestamp" = ${row.timestamp}
|
|
519
|
-
and "table" = ${row.table}
|
|
520
|
-
and "id" = ${row.id}
|
|
521
|
-
and "column" = ${row.column};
|
|
522
|
-
`);
|
|
523
|
-
if (!result.ok)
|
|
524
|
-
return result;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return ok();
|
|
528
|
-
};
|
|
529
|
-
export const initialSyncState = { type: "SyncStateInitial" };
|
package/src/Instances.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manages disposable instances by key, ensuring exactly one instance per key.
|
|
3
|
-
*
|
|
4
|
-
* Use cases:
|
|
5
|
-
*
|
|
6
|
-
* - One mutex per key to prevent concurrent writes
|
|
7
|
-
* - Preserving state during hot module reloading
|
|
8
|
-
*
|
|
9
|
-
* **Important:** Do not use this as global shared state. Use it locally or pass
|
|
10
|
-
* it as a dependency instead. The only exception is for hot reloading support,
|
|
11
|
-
* where Evolu uses it to ensure only one instance exists across module reloads
|
|
12
|
-
* (having two Evolu instances with the same name would mean two SQLite
|
|
13
|
-
* connections to the same file, which could corrupt data).
|
|
14
|
-
*/
|
|
15
|
-
export interface Instances<K extends string, T extends Disposable>
|
|
16
|
-
extends Disposable {
|
|
17
|
-
/**
|
|
18
|
-
* Ensures an instance exists for the given key, creating it if necessary. If
|
|
19
|
-
* the instance already exists, the optional `onCacheHit` callback is invoked
|
|
20
|
-
* to update the existing instance.
|
|
21
|
-
*/
|
|
22
|
-
readonly ensure: (
|
|
23
|
-
key: K,
|
|
24
|
-
create: () => T,
|
|
25
|
-
onCacheHit?: (instance: T) => void,
|
|
26
|
-
) => T;
|
|
27
|
-
|
|
28
|
-
/** Gets an instance by key, or returns `null` if it doesn't exist. */
|
|
29
|
-
readonly get: (key: K) => T | null;
|
|
30
|
-
|
|
31
|
-
/** Checks if an instance exists for the given key. */
|
|
32
|
-
readonly has: (key: K) => boolean;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Deletes and disposes an instance by key. Returns `true` if the instance
|
|
36
|
-
* existed and was deleted, `false` otherwise.
|
|
37
|
-
*/
|
|
38
|
-
readonly delete: (key: K) => boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Creates an {@link Instances}. */
|
|
42
|
-
export const createInstances = <
|
|
43
|
-
K extends string,
|
|
44
|
-
T extends Disposable,
|
|
45
|
-
>(): Instances<K, T> => {
|
|
46
|
-
const instances = new Map<K, T>();
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
ensure: (key, create, onCacheHit) => {
|
|
50
|
-
let instance = instances.get(key);
|
|
51
|
-
|
|
52
|
-
if (instance == null) {
|
|
53
|
-
instance = create();
|
|
54
|
-
instances.set(key, instance);
|
|
55
|
-
} else if (onCacheHit) {
|
|
56
|
-
onCacheHit(instance);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return instance;
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
get: (key) => instances.get(key) ?? null,
|
|
63
|
-
|
|
64
|
-
has: (key) => instances.has(key),
|
|
65
|
-
|
|
66
|
-
delete: (key) => {
|
|
67
|
-
const instance = instances.get(key);
|
|
68
|
-
if (instance == null) return false;
|
|
69
|
-
instances.delete(key);
|
|
70
|
-
instance[Symbol.dispose]();
|
|
71
|
-
return true;
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
[Symbol.dispose]: () => {
|
|
75
|
-
const errors: Array<unknown> = [];
|
|
76
|
-
for (const instance of instances.values()) {
|
|
77
|
-
try {
|
|
78
|
-
instance[Symbol.dispose]();
|
|
79
|
-
} catch (error) {
|
|
80
|
-
errors.push(error);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
instances.clear();
|
|
84
|
-
if (errors.length === 1) throw errors[0];
|
|
85
|
-
if (errors.length > 1) {
|
|
86
|
-
throw new AggregateError(errors, "Multiple disposal errors occurred");
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
};
|