@evolu/common 6.0.1-preview.25 → 6.0.1-preview.26
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 +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- 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/Evolu/Db.d.ts +6 -6
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +4 -0
- package/dist/src/Evolu/LocalAuth.d.ts +2 -2
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.d.ts +116 -86
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +48 -45
- package/dist/src/Evolu/Relay.d.ts +6 -5
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +40 -39
- package/dist/src/Evolu/Storage.d.ts +6 -11
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +30 -9
- package/dist/src/Evolu/Sync.d.ts +5 -5
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +3 -5
- package/dist/src/Evolu/Timestamp.d.ts +24 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +24 -0
- 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/ManyToManyMap.d.ts +0 -3
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/Result.d.ts +13 -6
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Sqlite.d.ts +36 -1
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +56 -3
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +36 -0
- package/dist/src/Type.d.ts +5 -8
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +10 -10
- package/dist/src/Types.d.ts +1 -1
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +2 -7
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/package.json +1 -1
- package/src/Assert.ts +0 -19
- package/src/Cache.ts +85 -0
- package/src/Evolu/Db.ts +11 -7
- package/src/Evolu/LocalAuth.ts +19 -7
- package/src/Evolu/Owner.ts +140 -103
- package/src/Evolu/Relay.ts +52 -48
- package/src/Evolu/Storage.ts +47 -23
- package/src/Evolu/Sync.ts +11 -12
- package/src/Evolu/Timestamp.ts +24 -0
- package/src/Identicon.ts +197 -0
- package/src/ManyToManyMap.ts +0 -3
- package/src/Result.ts +13 -6
- package/src/Sqlite.ts +68 -5
- package/src/Task.ts +41 -0
- package/src/Type.ts +11 -14
- package/src/Types.ts +1 -1
- package/src/WebSocket.ts +6 -10
- package/src/index.ts +2 -0
package/src/Evolu/Owner.ts
CHANGED
|
@@ -16,29 +16,12 @@ import {
|
|
|
16
16
|
idToIdBytes,
|
|
17
17
|
Mnemonic,
|
|
18
18
|
NonNegativeInt,
|
|
19
|
+
PositiveInt,
|
|
19
20
|
} from "../Type.js";
|
|
20
21
|
import { getOrNull } from "../Result.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
* Can be created using {@link createOwnerSecret} or converted from a
|
|
26
|
-
* {@link Mnemonic} using {@link mnemonicToOwnerSecret}.
|
|
27
|
-
*/
|
|
28
|
-
export const OwnerSecret = brand("OwnerSecret", Entropy32);
|
|
29
|
-
export type OwnerSecret = typeof OwnerSecret.Type;
|
|
30
|
-
|
|
31
|
-
/** Creates a {@link OwnerSecret}. */
|
|
32
|
-
export const createOwnerSecret = (deps: RandomBytesDep): OwnerSecret =>
|
|
33
|
-
deps.randomBytes.create(32) as OwnerSecret;
|
|
34
|
-
|
|
35
|
-
/** Converts an {@link OwnerSecret} to a {@link Mnemonic}. */
|
|
36
|
-
export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
|
|
37
|
-
bip39.entropyToMnemonic(secret, wordlist) as Mnemonic;
|
|
38
|
-
|
|
39
|
-
/** Converts a {@link Mnemonic} to an {@link OwnerSecret}. */
|
|
40
|
-
export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
|
|
41
|
-
bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
|
|
22
|
+
import type { Timestamp } from "./Timestamp.js";
|
|
23
|
+
import { TimestampBytes } from "./Timestamp.js";
|
|
24
|
+
import type { Storage } from "./Storage.js";
|
|
42
25
|
|
|
43
26
|
/**
|
|
44
27
|
* The Owner represents ownership of data in Evolu. Every database change is
|
|
@@ -105,6 +88,27 @@ export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
|
|
|
105
88
|
export const OwnerWriteKey = brand("OwnerWriteKey", Entropy16);
|
|
106
89
|
export type OwnerWriteKey = typeof OwnerWriteKey.Type;
|
|
107
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
|
|
93
|
+
*
|
|
94
|
+
* Can be created using {@link createOwnerSecret} or converted from a
|
|
95
|
+
* {@link Mnemonic} using {@link mnemonicToOwnerSecret}.
|
|
96
|
+
*/
|
|
97
|
+
export const OwnerSecret = brand("OwnerSecret", Entropy32);
|
|
98
|
+
export type OwnerSecret = typeof OwnerSecret.Type;
|
|
99
|
+
|
|
100
|
+
/** Creates a {@link OwnerSecret}. */
|
|
101
|
+
export const createOwnerSecret = (deps: RandomBytesDep): OwnerSecret =>
|
|
102
|
+
deps.randomBytes.create(32) as OwnerSecret;
|
|
103
|
+
|
|
104
|
+
/** Converts an {@link OwnerSecret} to a {@link Mnemonic}. */
|
|
105
|
+
export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
|
|
106
|
+
bip39.entropyToMnemonic(secret, wordlist) as Mnemonic;
|
|
107
|
+
|
|
108
|
+
/** Converts a {@link Mnemonic} to an {@link OwnerSecret}. */
|
|
109
|
+
export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
|
|
110
|
+
bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
|
|
111
|
+
|
|
108
112
|
/** Creates a randomly generated {@link OwnerWriteKey}. */
|
|
109
113
|
export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
|
|
110
114
|
deps.randomBytes.create(16) as OwnerWriteKey;
|
|
@@ -174,82 +178,6 @@ export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
|
|
|
174
178
|
...createOwner(secret),
|
|
175
179
|
});
|
|
176
180
|
|
|
177
|
-
/**
|
|
178
|
-
* Transport configuration for connecting to relays.
|
|
179
|
-
*
|
|
180
|
-
* Each {@link Owner} can specify one or more transports to connect to different
|
|
181
|
-
* relays for data synchronization. Currently supports WebSocket transport, with
|
|
182
|
-
* future support planned for Bluetooth, LocalNetwork, and other protocols.
|
|
183
|
-
*/
|
|
184
|
-
export type TransportConfig = WebSocketTransportConfig;
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* WebSocket transport configuration for relay connections.
|
|
188
|
-
*
|
|
189
|
-
* Use {@link createWebSocketTransportConfig} to create a properly formatted URL
|
|
190
|
-
* with {@link OwnerId}. The relay uses {@link parseOwnerIdFromUrl} to extract the
|
|
191
|
-
* OwnerId from the query string.
|
|
192
|
-
*
|
|
193
|
-
* ### Authentication and Error Handling
|
|
194
|
-
*
|
|
195
|
-
* When a relay rejects a connection (invalid OwnerId, unauthorized owner, or
|
|
196
|
-
* server error), the browser WebSocket API does not expose the specific HTTP
|
|
197
|
-
* status code or reason - it only reports a generic connection failure. The
|
|
198
|
-
* client automatically retries with exponential backoff and jitter, eventually
|
|
199
|
-
* succeeding once the configuration or server issue is resolved.
|
|
200
|
-
*
|
|
201
|
-
* Legitimate clients will be properly configured with valid credentials, so
|
|
202
|
-
* automatic retry is appropriate.
|
|
203
|
-
*
|
|
204
|
-
* @see {@link createWebSocketTransportConfig}
|
|
205
|
-
* @see {@link parseOwnerIdFromUrl}
|
|
206
|
-
*/
|
|
207
|
-
export interface WebSocketTransportConfig {
|
|
208
|
-
readonly type: "WebSocket";
|
|
209
|
-
readonly url: string;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Creates a {@link WebSocketTransportConfig} for the given relay URL and
|
|
214
|
-
* {@link OwnerId}.
|
|
215
|
-
*
|
|
216
|
-
* ### Example
|
|
217
|
-
*
|
|
218
|
-
* ```ts
|
|
219
|
-
* const transport = createWebSocketTransportConfig({
|
|
220
|
-
* relayUrl: "wss://relay.evolu.dev",
|
|
221
|
-
* ownerId: owner.id,
|
|
222
|
-
* });
|
|
223
|
-
* // Result: { type: "WebSocket", url: "wss://relay.evolu.dev?ownerId=..." }
|
|
224
|
-
* ```
|
|
225
|
-
*/
|
|
226
|
-
export const createWebSocketTransportConfig = ({
|
|
227
|
-
relayUrl,
|
|
228
|
-
ownerId,
|
|
229
|
-
}: {
|
|
230
|
-
readonly relayUrl: string;
|
|
231
|
-
readonly ownerId: OwnerId;
|
|
232
|
-
}): WebSocketTransportConfig => ({
|
|
233
|
-
type: "WebSocket",
|
|
234
|
-
url: `${relayUrl}?ownerId=${ownerId}`,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Extracts {@link OwnerId} from a URL query string.
|
|
239
|
-
*
|
|
240
|
-
* Parses the query string `?ownerId=...` and validates that the extracted value
|
|
241
|
-
* is a valid {@link OwnerId}.
|
|
242
|
-
*
|
|
243
|
-
* ### Example
|
|
244
|
-
*
|
|
245
|
-
* ```ts
|
|
246
|
-
* parseOwnerIdFromUrl("/sync?ownerId=_12345678abcdefgh");
|
|
247
|
-
* // Returns: OwnerId or null
|
|
248
|
-
* ```
|
|
249
|
-
*/
|
|
250
|
-
export const parseOwnerIdFromUrl = (url: string | undefined): OwnerId | null =>
|
|
251
|
-
getOrNull(OwnerId.fromUnknown(url?.split("=")[1]));
|
|
252
|
-
|
|
253
181
|
/**
|
|
254
182
|
* An {@link Owner} for sharding data.
|
|
255
183
|
*
|
|
@@ -263,13 +191,13 @@ export const parseOwnerIdFromUrl = (url: string | undefined): OwnerId | null =>
|
|
|
263
191
|
*/
|
|
264
192
|
export interface ShardOwner extends Owner {
|
|
265
193
|
readonly type: "ShardOwner";
|
|
266
|
-
readonly transports?: ReadonlyArray<
|
|
194
|
+
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
267
195
|
}
|
|
268
196
|
|
|
269
197
|
/** Creates a {@link ShardOwner} from an {@link OwnerSecret}. */
|
|
270
198
|
export const createShardOwner = (
|
|
271
199
|
secret: OwnerSecret,
|
|
272
|
-
transports?: ReadonlyArray<
|
|
200
|
+
transports?: ReadonlyArray<OwnerTransport>,
|
|
273
201
|
): ShardOwner => {
|
|
274
202
|
return {
|
|
275
203
|
type: "ShardOwner",
|
|
@@ -299,7 +227,7 @@ export const createShardOwner = (
|
|
|
299
227
|
export const deriveShardOwner = (
|
|
300
228
|
owner: AppOwner,
|
|
301
229
|
path: NonEmptyReadonlyArray<string | number>,
|
|
302
|
-
transports?: ReadonlyArray<
|
|
230
|
+
transports?: ReadonlyArray<OwnerTransport>,
|
|
303
231
|
): ShardOwner => {
|
|
304
232
|
const secret = createSlip21(owner.encryptionKey, path) as OwnerSecret;
|
|
305
233
|
|
|
@@ -313,7 +241,7 @@ export const deriveShardOwner = (
|
|
|
313
241
|
/** An {@link Owner} for collaborative data with write access. */
|
|
314
242
|
export interface SharedOwner extends Owner {
|
|
315
243
|
readonly type: "SharedOwner";
|
|
316
|
-
readonly transports?: ReadonlyArray<
|
|
244
|
+
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
317
245
|
}
|
|
318
246
|
|
|
319
247
|
/**
|
|
@@ -325,7 +253,7 @@ export interface SharedOwner extends Owner {
|
|
|
325
253
|
*/
|
|
326
254
|
export const createSharedOwner = (
|
|
327
255
|
secret: OwnerSecret,
|
|
328
|
-
transports?: ReadonlyArray<
|
|
256
|
+
transports?: ReadonlyArray<OwnerTransport>,
|
|
329
257
|
): SharedOwner => {
|
|
330
258
|
return {
|
|
331
259
|
type: "SharedOwner",
|
|
@@ -343,7 +271,7 @@ export interface SharedReadonlyOwner {
|
|
|
343
271
|
readonly type: "SharedReadonlyOwner";
|
|
344
272
|
readonly id: OwnerId;
|
|
345
273
|
readonly encryptionKey: EncryptionKey;
|
|
346
|
-
readonly transports?: ReadonlyArray<
|
|
274
|
+
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
347
275
|
}
|
|
348
276
|
|
|
349
277
|
/** Creates a {@link SharedReadonlyOwner} from a {@link SharedOwner}. */
|
|
@@ -355,3 +283,112 @@ export const createSharedReadonlyOwner = (
|
|
|
355
283
|
encryptionKey: sharedOwner.encryptionKey,
|
|
356
284
|
...(sharedOwner.transports && { transports: sharedOwner.transports }),
|
|
357
285
|
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Transport configuration for connecting to relays.
|
|
289
|
+
*
|
|
290
|
+
* Currently only WebSocket, in the future Bluetooth, LocalNetwork, etc.
|
|
291
|
+
*/
|
|
292
|
+
export type OwnerTransport = OwnerWebSocketTransport;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* WebSocket transport configuration.
|
|
296
|
+
*
|
|
297
|
+
* ### Authentication and Error Handling
|
|
298
|
+
*
|
|
299
|
+
* When a relay rejects a connection (invalid OwnerId, unauthorized owner, or
|
|
300
|
+
* server error), the browser WebSocket API does not expose the specific HTTP
|
|
301
|
+
* status code or reason - it only reports a generic connection failure. The
|
|
302
|
+
* client automatically retries with exponential backoff and jitter, eventually
|
|
303
|
+
* succeeding once the configuration or server issue is resolved.
|
|
304
|
+
*
|
|
305
|
+
* Legitimate clients will be properly configured with valid credentials, so
|
|
306
|
+
* automatic retry is OK.
|
|
307
|
+
*
|
|
308
|
+
* @see {@link createOwnerWebSocketTransport}
|
|
309
|
+
* @see {@link parseOwnerIdFromOwnerWebSocketTransportUrl}
|
|
310
|
+
*/
|
|
311
|
+
export interface OwnerWebSocketTransport {
|
|
312
|
+
readonly type: "WebSocket";
|
|
313
|
+
readonly url: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Creates an {@link OwnerWebSocketTransport} for the given relay URL and
|
|
318
|
+
* {@link OwnerId}.
|
|
319
|
+
*
|
|
320
|
+
* ### Example
|
|
321
|
+
*
|
|
322
|
+
* ```ts
|
|
323
|
+
* const transport = createOwnerWebSocketTransport({
|
|
324
|
+
* url: "wss://relay.evolu.dev",
|
|
325
|
+
* ownerId: owner.id,
|
|
326
|
+
* });
|
|
327
|
+
* // Result: { type: "WebSocket", url: "wss://relay.evolu.dev?ownerId=..." }
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export const createOwnerWebSocketTransport = (config: {
|
|
331
|
+
readonly url: string;
|
|
332
|
+
readonly ownerId: OwnerId;
|
|
333
|
+
}): OwnerWebSocketTransport => ({
|
|
334
|
+
type: "WebSocket",
|
|
335
|
+
url: `${config.url}?ownerId=${config.ownerId}`,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Extracts {@link OwnerId} from an {@link OwnerWebSocketTransport} URL query
|
|
340
|
+
* string.
|
|
341
|
+
*
|
|
342
|
+
* Parses the query string `?ownerId=...` and validates that the extracted value
|
|
343
|
+
* is a valid {@link OwnerId}.
|
|
344
|
+
*
|
|
345
|
+
* ### Example
|
|
346
|
+
*
|
|
347
|
+
* ```ts
|
|
348
|
+
* parseOwnerIdFromOwnerWebSocketTransportUrl(
|
|
349
|
+
* "/sync?ownerId=_12345678abcdefgh",
|
|
350
|
+
* );
|
|
351
|
+
* // Returns: OwnerId or null
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
export const parseOwnerIdFromOwnerWebSocketTransportUrl = (
|
|
355
|
+
url: string,
|
|
356
|
+
): OwnerId | null => getOrNull(OwnerId.fromUnknown(url.split("=")[1]));
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Usage data for an {@link OwnerId}.
|
|
360
|
+
*
|
|
361
|
+
* Tracks data consumption to monitor usage patterns and enforce quotas if
|
|
362
|
+
* needed. Used by both relays and clients.
|
|
363
|
+
*
|
|
364
|
+
* Relays and clients must handle rate limiting, connection limits, and request
|
|
365
|
+
* throttling separately with in-memory state.
|
|
366
|
+
*/
|
|
367
|
+
export interface OwnerUsage {
|
|
368
|
+
/** The {@link Owner} this usage data belongs to. */
|
|
369
|
+
readonly ownerId: OwnerIdBytes;
|
|
370
|
+
|
|
371
|
+
/** Total bytes stored in the database. */
|
|
372
|
+
readonly storedBytes: PositiveInt;
|
|
373
|
+
|
|
374
|
+
/** Total bytes received. */
|
|
375
|
+
readonly receivedBytes: number;
|
|
376
|
+
|
|
377
|
+
/** Total bytes sent. */
|
|
378
|
+
readonly sentBytes: number;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The minimum {@link Timestamp}.
|
|
382
|
+
*
|
|
383
|
+
* Helps {@link Storage} choose faster algorithms.
|
|
384
|
+
*/
|
|
385
|
+
readonly firstTimestamp: TimestampBytes | null;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The maximum {@link Timestamp}.
|
|
389
|
+
*
|
|
390
|
+
* Helps {@link Storage} choose faster algorithms. Free relays can use it to
|
|
391
|
+
* identify inactive accounts for cleanup or archival.
|
|
392
|
+
*/
|
|
393
|
+
readonly lastTimestamp: TimestampBytes | null;
|
|
394
|
+
}
|
package/src/Evolu/Relay.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { ConsoleConfig, ConsoleDep } from "../Console.js";
|
|
|
3
3
|
import { TimingSafeEqualDep } from "../Crypto.js";
|
|
4
4
|
import { LazyValue } from "../Function.js";
|
|
5
5
|
import { err, ok, Result } from "../Result.js";
|
|
6
|
-
import { sql, SqliteError } from "../Sqlite.js";
|
|
6
|
+
import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
|
|
7
7
|
import { SimpleName } from "../Type.js";
|
|
8
|
-
import { OwnerId,
|
|
8
|
+
import { OwnerId, OwnerTransport, OwnerWriteKey } from "./Owner.js";
|
|
9
9
|
import { ProtocolInvalidDataError } from "./Protocol.js";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
createBaseSqliteStorage,
|
|
12
|
+
CreateBaseSqliteStorageOptions,
|
|
13
13
|
EncryptedDbChange,
|
|
14
14
|
SqliteStorageDeps,
|
|
15
15
|
Storage,
|
|
@@ -44,9 +44,9 @@ export interface RelayConfig extends ConsoleConfig {
|
|
|
44
44
|
* this only controls relay access, not write permissions. Since all data is
|
|
45
45
|
* encrypted on the relay, OwnerId exposure is safe.
|
|
46
46
|
*
|
|
47
|
-
* Owners specify which relays to connect to via {@link
|
|
47
|
+
* Owners specify which relays to connect to via {@link OwnerTransport}. In
|
|
48
48
|
* WebSocket-based implementations, this check occurs before accepting the
|
|
49
|
-
* connection, with the OwnerId typically extracted from the URL
|
|
49
|
+
* connection, with the OwnerId typically extracted from the URL Path (e.g.,
|
|
50
50
|
* `ws://localhost:4000/<ownerId>`).
|
|
51
51
|
*
|
|
52
52
|
* ### Example
|
|
@@ -68,36 +68,11 @@ export interface RelayConfig extends ConsoleConfig {
|
|
|
68
68
|
|
|
69
69
|
export const createRelaySqliteStorage =
|
|
70
70
|
(deps: SqliteStorageDeps & TimingSafeEqualDep) =>
|
|
71
|
-
(options:
|
|
72
|
-
const sqliteStorageBase =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
sql`
|
|
77
|
-
create table if not exists evolu_writeKey (
|
|
78
|
-
"ownerId" blob not null,
|
|
79
|
-
"writeKey" blob not null,
|
|
80
|
-
primary key ("ownerId")
|
|
81
|
-
)
|
|
82
|
-
strict;
|
|
83
|
-
`,
|
|
84
|
-
|
|
85
|
-
sql`
|
|
86
|
-
create table if not exists evolu_message (
|
|
87
|
-
"ownerId" blob not null,
|
|
88
|
-
"timestamp" blob not null,
|
|
89
|
-
"change" blob not null,
|
|
90
|
-
primary key ("ownerId", "timestamp")
|
|
91
|
-
)
|
|
92
|
-
strict;
|
|
93
|
-
`,
|
|
94
|
-
]) {
|
|
95
|
-
const result = deps.sqlite.exec(query);
|
|
96
|
-
if (!result.ok) return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return ok({
|
|
100
|
-
...sqliteStorageBase.value,
|
|
71
|
+
(options: CreateBaseSqliteStorageOptions): Storage => {
|
|
72
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps)(options);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...sqliteStorageBase,
|
|
101
76
|
|
|
102
77
|
/**
|
|
103
78
|
* Lazily authorizes the initiator's {@link OwnerWriteKey} for the given
|
|
@@ -159,11 +134,10 @@ export const createRelaySqliteStorage =
|
|
|
159
134
|
writeMessages: async (ownerId, messages) => {
|
|
160
135
|
const result = deps.sqlite.transaction(() => {
|
|
161
136
|
for (const message of messages) {
|
|
162
|
-
const insertTimestampResult =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
137
|
+
const insertTimestampResult = sqliteStorageBase.insertTimestamp(
|
|
138
|
+
ownerId,
|
|
139
|
+
timestampToTimestampBytes(message.timestamp),
|
|
140
|
+
);
|
|
167
141
|
if (!insertTimestampResult.ok) return insertTimestampResult;
|
|
168
142
|
|
|
169
143
|
const insertMessage = deps.sqlite.exec(sql`
|
|
@@ -207,18 +181,18 @@ export const createRelaySqliteStorage =
|
|
|
207
181
|
|
|
208
182
|
deleteOwner: (ownerId) => {
|
|
209
183
|
const result = deps.sqlite.transaction(() => {
|
|
210
|
-
const
|
|
184
|
+
const deleteWriteKey = deps.sqlite.exec(sql`
|
|
211
185
|
delete from evolu_writeKey where ownerId = ${ownerId};
|
|
212
186
|
`);
|
|
213
|
-
if (!
|
|
187
|
+
if (!deleteWriteKey.ok) return deleteWriteKey;
|
|
214
188
|
|
|
215
|
-
const
|
|
189
|
+
const deleteMessages = deps.sqlite.exec(sql`
|
|
216
190
|
delete from evolu_message where ownerId = ${ownerId};
|
|
217
191
|
`);
|
|
218
|
-
if (!
|
|
192
|
+
if (!deleteMessages.ok) return deleteMessages;
|
|
219
193
|
|
|
220
|
-
const
|
|
221
|
-
if (!
|
|
194
|
+
const deleteBaseOwner = sqliteStorageBase.deleteOwner(ownerId);
|
|
195
|
+
if (!deleteBaseOwner) return err(null);
|
|
222
196
|
|
|
223
197
|
return ok();
|
|
224
198
|
});
|
|
@@ -228,9 +202,39 @@ export const createRelaySqliteStorage =
|
|
|
228
202
|
}
|
|
229
203
|
return true;
|
|
230
204
|
},
|
|
231
|
-
}
|
|
205
|
+
};
|
|
232
206
|
};
|
|
233
207
|
|
|
208
|
+
export const createRelayStorageTables = (
|
|
209
|
+
deps: SqliteDep,
|
|
210
|
+
): Result<void, SqliteError> => {
|
|
211
|
+
for (const query of [
|
|
212
|
+
sql`
|
|
213
|
+
create table evolu_writeKey (
|
|
214
|
+
"ownerId" blob not null,
|
|
215
|
+
"writeKey" blob not null,
|
|
216
|
+
primary key ("ownerId")
|
|
217
|
+
)
|
|
218
|
+
strict;
|
|
219
|
+
`,
|
|
220
|
+
|
|
221
|
+
sql`
|
|
222
|
+
create table evolu_message (
|
|
223
|
+
"ownerId" blob not null,
|
|
224
|
+
"timestamp" blob not null,
|
|
225
|
+
"change" blob not null,
|
|
226
|
+
primary key ("ownerId", "timestamp")
|
|
227
|
+
)
|
|
228
|
+
strict;
|
|
229
|
+
`,
|
|
230
|
+
]) {
|
|
231
|
+
const result = deps.sqlite.exec(query);
|
|
232
|
+
if (!result.ok) return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ok();
|
|
236
|
+
};
|
|
237
|
+
|
|
234
238
|
export interface RelayLogger {
|
|
235
239
|
readonly started: (enableLogging: boolean, port: number) => void;
|
|
236
240
|
readonly storageError: (error: unknown) => void;
|
package/src/Evolu/Storage.ts
CHANGED
|
@@ -240,7 +240,16 @@ export type DbChange = typeof DbChange.Type;
|
|
|
240
240
|
* users, and when it goes down, nothing happens, because it will be
|
|
241
241
|
* synchronized later.
|
|
242
242
|
*/
|
|
243
|
-
export interface
|
|
243
|
+
export interface BaseSqliteStorage
|
|
244
|
+
extends Pick<
|
|
245
|
+
Storage,
|
|
246
|
+
| "getSize"
|
|
247
|
+
| "fingerprint"
|
|
248
|
+
| "fingerprintRanges"
|
|
249
|
+
| "findLowerBound"
|
|
250
|
+
| "iterate"
|
|
251
|
+
| "deleteOwner"
|
|
252
|
+
> {
|
|
244
253
|
/**
|
|
245
254
|
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
246
255
|
*
|
|
@@ -252,33 +261,22 @@ export interface SqliteStorageBase {
|
|
|
252
261
|
ownerId: OwnerIdBytes,
|
|
253
262
|
timestamp: TimestampBytes,
|
|
254
263
|
) => Result<void, SqliteError>;
|
|
255
|
-
|
|
256
|
-
readonly getSize: Storage["getSize"];
|
|
257
|
-
readonly fingerprint: Storage["fingerprint"];
|
|
258
|
-
readonly fingerprintRanges: Storage["fingerprintRanges"];
|
|
259
|
-
readonly findLowerBound: Storage["findLowerBound"];
|
|
260
|
-
readonly iterate: Storage["iterate"];
|
|
261
|
-
readonly deleteOwner: Storage["deleteOwner"];
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
export interface
|
|
265
|
-
readonly storage:
|
|
266
|
+
export interface BaseSqliteStorageDep {
|
|
267
|
+
readonly storage: BaseSqliteStorage;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
269
271
|
|
|
270
|
-
export interface
|
|
272
|
+
export interface CreateBaseSqliteStorageOptions {
|
|
271
273
|
onStorageError: (error: SqliteError) => void;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
export const
|
|
276
|
+
export const createBaseSqliteStorage =
|
|
275
277
|
(deps: SqliteStorageDeps) =>
|
|
276
|
-
(
|
|
277
|
-
|
|
278
|
-
): Result<SqliteStorageBase, SqliteError> => {
|
|
279
|
-
const createTablesResult = createTables(deps);
|
|
280
|
-
if (!createTablesResult.ok) return createTablesResult;
|
|
281
|
-
|
|
278
|
+
(options: CreateBaseSqliteStorageOptions): BaseSqliteStorage => {
|
|
279
|
+
// TODO: Use OwnerUsage table.
|
|
282
280
|
const ownerStats = new Map<
|
|
283
281
|
OwnerId,
|
|
284
282
|
{
|
|
@@ -287,7 +285,7 @@ export const createSqliteStorageBase =
|
|
|
287
285
|
}
|
|
288
286
|
>();
|
|
289
287
|
|
|
290
|
-
return
|
|
288
|
+
return {
|
|
291
289
|
insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
|
|
292
290
|
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
293
291
|
const level = randomSkiplistLevel(deps);
|
|
@@ -422,14 +420,16 @@ export const createSqliteStorageBase =
|
|
|
422
420
|
}
|
|
423
421
|
return true;
|
|
424
422
|
},
|
|
425
|
-
}
|
|
423
|
+
};
|
|
426
424
|
};
|
|
427
425
|
|
|
428
426
|
const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
|
|
429
427
|
assert(begin <= end, "invalid begin or end");
|
|
430
428
|
};
|
|
431
429
|
|
|
432
|
-
const
|
|
430
|
+
export const createBaseSqliteStorageTables = (
|
|
431
|
+
deps: SqliteDep,
|
|
432
|
+
): Result<void, SqliteError> => {
|
|
433
433
|
for (const query of [
|
|
434
434
|
/**
|
|
435
435
|
* Creates the `evolu_timestamp` table for storing timestamps of multiple
|
|
@@ -454,7 +454,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
454
454
|
* enough even without it.
|
|
455
455
|
*/
|
|
456
456
|
sql`
|
|
457
|
-
create table
|
|
457
|
+
create table evolu_timestamp (
|
|
458
458
|
"ownerId" blob not null,
|
|
459
459
|
"t" blob not null,
|
|
460
460
|
"h1" integer,
|
|
@@ -467,7 +467,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
467
467
|
`,
|
|
468
468
|
|
|
469
469
|
sql`
|
|
470
|
-
create index
|
|
470
|
+
create index evolu_timestamp_index on evolu_timestamp (
|
|
471
471
|
"ownerId",
|
|
472
472
|
"l",
|
|
473
473
|
"t",
|
|
@@ -476,6 +476,30 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
476
476
|
"c"
|
|
477
477
|
);
|
|
478
478
|
`,
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Creates the `evolu_usage` table for tracking data consumption per owner.
|
|
482
|
+
*
|
|
483
|
+
* Columns:
|
|
484
|
+
*
|
|
485
|
+
* - `ownerId` – OwnerIdBytes (primary key)
|
|
486
|
+
* - `storedBytes` – total bytes stored in database
|
|
487
|
+
* - `receivedBytes` – total bytes received from clients
|
|
488
|
+
* - `sentBytes` – total bytes sent to clients
|
|
489
|
+
* - `firstTimestamp` – minimum timestamp (nullable)
|
|
490
|
+
* - `lastTimestamp` – maximum timestamp (nullable)
|
|
491
|
+
*/
|
|
492
|
+
sql`
|
|
493
|
+
create table evolu_usage (
|
|
494
|
+
"ownerId" blob primary key,
|
|
495
|
+
"storedBytes" integer not null,
|
|
496
|
+
"receivedBytes" integer not null,
|
|
497
|
+
"sentBytes" integer not null,
|
|
498
|
+
"firstTimestamp" blob,
|
|
499
|
+
"lastTimestamp" blob
|
|
500
|
+
)
|
|
501
|
+
strict;
|
|
502
|
+
`,
|
|
479
503
|
]) {
|
|
480
504
|
const result = deps.sqlite.exec(query);
|
|
481
505
|
if (!result.ok) return result;
|
package/src/Evolu/Sync.ts
CHANGED
|
@@ -27,11 +27,11 @@ import {
|
|
|
27
27
|
OwnerIdBytes,
|
|
28
28
|
ownerIdBytesToOwnerId,
|
|
29
29
|
ownerIdToOwnerIdBytes,
|
|
30
|
+
OwnerTransport,
|
|
30
31
|
OwnerWriteKey,
|
|
31
32
|
ShardOwner,
|
|
32
33
|
SharedOwner,
|
|
33
34
|
SharedReadonlyOwner,
|
|
34
|
-
TransportConfig,
|
|
35
35
|
} from "./Owner.js";
|
|
36
36
|
import {
|
|
37
37
|
applyProtocolMessageAsClient,
|
|
@@ -47,10 +47,10 @@ import {
|
|
|
47
47
|
} from "./Protocol.js";
|
|
48
48
|
import { MutationChange } from "./Schema.js";
|
|
49
49
|
import {
|
|
50
|
+
BaseSqliteStorage,
|
|
51
|
+
createBaseSqliteStorage,
|
|
50
52
|
CrdtMessage,
|
|
51
|
-
createSqliteStorageBase,
|
|
52
53
|
DbChange,
|
|
53
|
-
SqliteStorageBase,
|
|
54
54
|
Storage,
|
|
55
55
|
} from "./Storage.js";
|
|
56
56
|
import {
|
|
@@ -106,13 +106,13 @@ export interface SyncOwner {
|
|
|
106
106
|
readonly encryptionKey: OwnerEncryptionKey;
|
|
107
107
|
/** Optional for read-only owners like {@link SharedReadonlyOwner}. */
|
|
108
108
|
readonly writeKey?: OwnerWriteKey;
|
|
109
|
-
readonly transports?: ReadonlyArray<
|
|
109
|
+
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
export interface SyncConfig {
|
|
113
113
|
readonly appOwner: AppOwner;
|
|
114
114
|
|
|
115
|
-
readonly transports: ReadonlyArray<
|
|
115
|
+
readonly transports: ReadonlyArray<OwnerTransport>;
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* Delay in milliseconds before disposing unused WebSocket connections.
|
|
@@ -166,7 +166,7 @@ export const createSync =
|
|
|
166
166
|
if (!storageResult.ok) return storageResult;
|
|
167
167
|
const storage = storageResult.value;
|
|
168
168
|
|
|
169
|
-
const createResource = (transportConfig:
|
|
169
|
+
const createResource = (transportConfig: OwnerTransport): WebSocket => {
|
|
170
170
|
const transportKey = createTransportKey(transportConfig);
|
|
171
171
|
|
|
172
172
|
deps.console.log("[sync]", "createWebSocket", {
|
|
@@ -255,7 +255,7 @@ export const createSync =
|
|
|
255
255
|
const transports = createRefCountedResourceManager<
|
|
256
256
|
WebSocket,
|
|
257
257
|
TransportKey,
|
|
258
|
-
|
|
258
|
+
OwnerTransport,
|
|
259
259
|
SyncOwner,
|
|
260
260
|
OwnerId
|
|
261
261
|
>({
|
|
@@ -418,7 +418,7 @@ interface GetSyncOwnerDep {
|
|
|
418
418
|
readonly getSyncOwner: (ownerId: OwnerId) => SyncOwner | null;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
export interface ClientStorage extends
|
|
421
|
+
export interface ClientStorage extends Storage, BaseSqliteStorage {}
|
|
422
422
|
|
|
423
423
|
export interface ClientStorageDep {
|
|
424
424
|
readonly storage: ClientStorage;
|
|
@@ -448,15 +448,14 @@ const createClientStorage =
|
|
|
448
448
|
) => void;
|
|
449
449
|
onReceive: () => void;
|
|
450
450
|
}): Result<ClientStorage, SqliteError> => {
|
|
451
|
-
const sqliteStorageBase =
|
|
451
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps)({
|
|
452
452
|
onStorageError: config.onError,
|
|
453
453
|
});
|
|
454
|
-
if (!sqliteStorageBase.ok) return sqliteStorageBase;
|
|
455
454
|
|
|
456
455
|
const mutex = createMutex();
|
|
457
456
|
|
|
458
457
|
const storage: ClientStorage = {
|
|
459
|
-
...sqliteStorageBase
|
|
458
|
+
...sqliteStorageBase,
|
|
460
459
|
|
|
461
460
|
validateWriteKey: constFalse,
|
|
462
461
|
setWriteKey: constFalse,
|
|
@@ -590,7 +589,7 @@ const createClientStorage =
|
|
|
590
589
|
type TransportKey = string & Brand<"TransportKey">;
|
|
591
590
|
|
|
592
591
|
/** Creates a unique identifier for a transport configuration. */
|
|
593
|
-
const createTransportKey = (transportConfig:
|
|
592
|
+
const createTransportKey = (transportConfig: OwnerTransport): TransportKey => {
|
|
594
593
|
return `${transportConfig.type}:${transportConfig.url}` as TransportKey;
|
|
595
594
|
};
|
|
596
595
|
|