@evolu/common 6.0.1-preview.27 → 6.0.1-preview.29
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/Array.d.ts +58 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +53 -5
- package/dist/src/Evolu/Evolu.d.ts +7 -12
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +18 -23
- package/dist/src/Evolu/Owner.d.ts +48 -19
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +11 -2
- package/dist/src/Evolu/Protocol.d.ts +31 -31
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +51 -28
- package/dist/src/Evolu/Relay.d.ts +40 -25
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +106 -49
- package/dist/src/Evolu/Storage.d.ts +59 -12
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +77 -50
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +14 -5
- package/dist/src/Evolu/Timestamp.d.ts +25 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +25 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/Sqlite.d.ts +6 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +6 -0
- package/dist/src/Task.d.ts +75 -0
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +29 -6
- package/dist/src/Time.d.ts +7 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +13 -2
- package/dist/src/Type.d.ts +56 -9
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +40 -8
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/package.json +1 -1
- package/src/Array.ts +76 -11
- package/src/Evolu/Evolu.ts +27 -27
- package/src/Evolu/Owner.ts +75 -26
- package/src/Evolu/Protocol.ts +90 -61
- package/src/Evolu/Relay.ts +182 -77
- package/src/Evolu/Storage.ts +157 -67
- package/src/Evolu/Sync.ts +18 -6
- package/src/Evolu/Timestamp.ts +25 -0
- package/src/Instances.ts +90 -0
- package/src/Sqlite.ts +6 -0
- package/src/Task.ts +88 -7
- package/src/Time.ts +13 -2
- package/src/Type.ts +56 -9
- package/src/index.ts +1 -0
|
@@ -85,10 +85,10 @@
|
|
|
85
85
|
*
|
|
86
86
|
* - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
|
|
87
87
|
* - {@link ProtocolWriteError}: A serious relay-side write failure occurred.
|
|
88
|
+
* - {@link ProtocolQuotaError}: Storage or billing quota exceeded.
|
|
88
89
|
* - {@link ProtocolSyncError}: A serious relay-side synchronization failure
|
|
89
90
|
* occurred.
|
|
90
|
-
* - {@link
|
|
91
|
-
* - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
|
|
91
|
+
* - {@link ProtocolVersionError}: Protocol version mismatch.
|
|
92
92
|
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
93
93
|
*
|
|
94
94
|
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
@@ -159,6 +159,18 @@
|
|
|
159
159
|
*
|
|
160
160
|
* @module
|
|
161
161
|
*/
|
|
162
|
+
/**
|
|
163
|
+
* TODO:
|
|
164
|
+
*
|
|
165
|
+
* - The client-relay naming convention in functions like
|
|
166
|
+
* `applyProtocolMessageAsClient` and `applyProtocolMessageAsRelay` is not
|
|
167
|
+
* ideal. In the future, clients will be able to sync directly with each other
|
|
168
|
+
* (P2P), making the current naming misleading. Consider using
|
|
169
|
+
* initiator/non-initiator terminology instead, and consolidate into a single
|
|
170
|
+
* `applyProtocolMessage` function with conditional arguments to reduce code
|
|
171
|
+
* duplication.
|
|
172
|
+
* - ProtocolQuotaError should return storedBytes and actual quota.
|
|
173
|
+
*/
|
|
162
174
|
import { Packr } from "msgpackr";
|
|
163
175
|
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
164
176
|
import { assert } from "../Assert.js";
|
|
@@ -248,10 +260,10 @@ export const ProtocolErrorCode = {
|
|
|
248
260
|
WriteKeyError: 1,
|
|
249
261
|
/** A code for {@link ProtocolWriteError}. */
|
|
250
262
|
WriteError: 2,
|
|
263
|
+
/** A code for {@link ProtocolQuotaError}. */
|
|
264
|
+
QuotaError: 3,
|
|
251
265
|
/** A code for {@link ProtocolSyncError}. */
|
|
252
|
-
SyncError:
|
|
253
|
-
/** A code for {@link ProtocolQuotaExceededError}. */
|
|
254
|
-
QuotaExceededError: 4,
|
|
266
|
+
SyncError: 4,
|
|
255
267
|
};
|
|
256
268
|
/**
|
|
257
269
|
* Creates a {@link ProtocolMessage} from CRDT messages.
|
|
@@ -524,14 +536,15 @@ const createRunLengthEncoder = (encodeValue) => {
|
|
|
524
536
|
};
|
|
525
537
|
export const applyProtocolMessageAsClient = (deps) => async (inputMessage, options = {}) => {
|
|
526
538
|
// try-catch instead of Result for performance and stacktraces
|
|
539
|
+
// DEV: Measure it again, I think we should use Result with new Error.
|
|
527
540
|
try {
|
|
528
541
|
const input = createBuffer(inputMessage);
|
|
529
542
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
530
543
|
const version = options.version ?? protocolVersion;
|
|
531
544
|
if (requestedVersion !== version) {
|
|
532
545
|
return err({
|
|
533
|
-
type: "
|
|
534
|
-
|
|
546
|
+
type: "ProtocolVersionError",
|
|
547
|
+
version: requestedVersion,
|
|
535
548
|
isInitiator: version < requestedVersion,
|
|
536
549
|
ownerId,
|
|
537
550
|
});
|
|
@@ -553,14 +566,14 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
553
566
|
type: "ProtocolWriteError",
|
|
554
567
|
ownerId,
|
|
555
568
|
});
|
|
556
|
-
case ProtocolErrorCode.
|
|
569
|
+
case ProtocolErrorCode.QuotaError:
|
|
557
570
|
return err({
|
|
558
|
-
type: "
|
|
571
|
+
type: "ProtocolQuotaError",
|
|
559
572
|
ownerId,
|
|
560
573
|
});
|
|
561
|
-
case ProtocolErrorCode.
|
|
574
|
+
case ProtocolErrorCode.SyncError:
|
|
562
575
|
return err({
|
|
563
|
-
type: "
|
|
576
|
+
type: "ProtocolSyncError",
|
|
564
577
|
ownerId,
|
|
565
578
|
});
|
|
566
579
|
default:
|
|
@@ -570,9 +583,11 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
570
583
|
}
|
|
571
584
|
const messages = decodeMessages(input);
|
|
572
585
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
573
|
-
if (isNonEmptyReadonlyArray(messages)
|
|
574
|
-
|
|
575
|
-
|
|
586
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
587
|
+
const writeResult = await deps.storage.writeMessages(ownerIdBytes, messages);
|
|
588
|
+
// Errors are handled by the Storage. Here we just stop syncing.
|
|
589
|
+
if (!writeResult.ok)
|
|
590
|
+
return ok({ type: "no-response" });
|
|
576
591
|
}
|
|
577
592
|
// Now: No writeKey, no sync.
|
|
578
593
|
// TODO: Allow to sync SharedReadonlyOwner
|
|
@@ -611,9 +626,10 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
611
626
|
}
|
|
612
627
|
};
|
|
613
628
|
export const applyProtocolMessageAsRelay = (deps) => async (inputMessage, options = {},
|
|
614
|
-
/** For
|
|
629
|
+
/** For tests only. */
|
|
615
630
|
version = protocolVersion) => {
|
|
616
631
|
// try-catch instead of Result for performance and stacktraces
|
|
632
|
+
// DEV: Measure it again, I think we should use Result with new Error.
|
|
617
633
|
try {
|
|
618
634
|
const input = createBuffer(inputMessage);
|
|
619
635
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
@@ -669,17 +685,33 @@ version = protocolVersion) => {
|
|
|
669
685
|
}).unwrap(),
|
|
670
686
|
});
|
|
671
687
|
}
|
|
688
|
+
const writeResult = await deps.storage.writeMessages(ownerIdBytes, messages);
|
|
689
|
+
if (!writeResult.ok) {
|
|
690
|
+
const errorCode = writeResult.error.type === "StorageWriteError"
|
|
691
|
+
? ProtocolErrorCode.WriteError
|
|
692
|
+
: ProtocolErrorCode.QuotaError;
|
|
693
|
+
const message = createProtocolMessageBuffer(ownerId, {
|
|
694
|
+
messageType: MessageType.Response,
|
|
695
|
+
errorCode,
|
|
696
|
+
}).unwrap();
|
|
697
|
+
return ok({ type: "response", message });
|
|
698
|
+
}
|
|
672
699
|
/**
|
|
673
|
-
* Broadcast messages to all subscribed
|
|
700
|
+
* Broadcast messages to all subscribed owners for real-time
|
|
674
701
|
* synchronization between clients.
|
|
675
702
|
*
|
|
703
|
+
* Messages are only broadcasted after successful write to ensure
|
|
704
|
+
* devices that can still sync aren't affected by quota errors, and to
|
|
705
|
+
* prevent using a half-working relay service (broadcasting without
|
|
706
|
+
* persistence).
|
|
707
|
+
*
|
|
676
708
|
* When a relay's database is deleted or clients migrate to a new relay
|
|
677
709
|
* (without data migration), clients will sync their data to the relay,
|
|
678
710
|
* and the relay will broadcast those messages to other connected
|
|
679
711
|
* clients. Those clients may receive messages they already have, but
|
|
680
|
-
* this is safe because
|
|
681
|
-
*
|
|
682
|
-
*
|
|
712
|
+
* this is safe because Evolu sync is idempotent. As the relay becomes
|
|
713
|
+
* more synchronized with clients over time, fewer duplicate messages
|
|
714
|
+
* will be broadcasted.
|
|
683
715
|
*/
|
|
684
716
|
if (options.broadcast) {
|
|
685
717
|
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
@@ -693,15 +725,6 @@ version = protocolVersion) => {
|
|
|
693
725
|
}
|
|
694
726
|
options.broadcast(ownerId, broadcastBuffer.unwrap());
|
|
695
727
|
}
|
|
696
|
-
if (!(await deps.storage.writeMessages(ownerIdBytes, messages))) {
|
|
697
|
-
return ok({
|
|
698
|
-
type: "response",
|
|
699
|
-
message: createProtocolMessageBuffer(ownerId, {
|
|
700
|
-
messageType: MessageType.Response,
|
|
701
|
-
errorCode: ProtocolErrorCode.WriteError,
|
|
702
|
-
}).unwrap(),
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
728
|
}
|
|
706
729
|
const ranges = decodeRanges(input);
|
|
707
730
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
@@ -3,13 +3,12 @@ import { TimingSafeEqualDep } from "../Crypto.js";
|
|
|
3
3
|
import { LazyValue } from "../Function.js";
|
|
4
4
|
import { Result } from "../Result.js";
|
|
5
5
|
import { SqliteDep, SqliteError } from "../Sqlite.js";
|
|
6
|
+
import { MaybeAsync } from "../Task.js";
|
|
6
7
|
import { SimpleName } from "../Type.js";
|
|
7
8
|
import { OwnerId } from "./Owner.js";
|
|
8
9
|
import { ProtocolInvalidDataError } from "./Protocol.js";
|
|
9
|
-
import {
|
|
10
|
-
export interface
|
|
11
|
-
}
|
|
12
|
-
export interface RelayConfig extends ConsoleConfig {
|
|
10
|
+
import { CreateBaseSqliteStorageConfig, SqliteStorageDeps, Storage, StorageConfig } from "./Storage.js";
|
|
11
|
+
export interface RelayConfig extends ConsoleConfig, StorageConfig {
|
|
13
12
|
/**
|
|
14
13
|
* The relay name.
|
|
15
14
|
*
|
|
@@ -18,21 +17,22 @@ export interface RelayConfig extends ConsoleConfig {
|
|
|
18
17
|
*/
|
|
19
18
|
readonly name?: SimpleName;
|
|
20
19
|
/**
|
|
21
|
-
* Optional callback to
|
|
20
|
+
* Optional callback to check if an {@link OwnerId} is allowed to access the
|
|
21
|
+
* relay. If this callback is not provided, all owners are allowed.
|
|
22
22
|
*
|
|
23
|
-
* If
|
|
23
|
+
* If provided, the callback receives the OwnerId and should return a
|
|
24
|
+
* {@link MaybeAsync} boolean: `true` to allow access, or `false` to deny.
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
27
|
+
* asynchronous (for calling remote APIs).
|
|
27
28
|
*
|
|
28
29
|
* The callback returns a boolean rather than an error type because error
|
|
29
|
-
* handling and logging are the responsibility of the callback
|
|
30
|
-
*
|
|
31
|
-
* into the generic relay interface.
|
|
30
|
+
* handling and logging are the responsibility of the callback
|
|
31
|
+
* implementation.
|
|
32
32
|
*
|
|
33
|
-
* OwnerId is used
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* OwnerId is used rather than short-lived tokens because this only controls
|
|
34
|
+
* relay access, not write permissions. Since all data is encrypted on the
|
|
35
|
+
* relay, OwnerId exposure is safe.
|
|
36
36
|
*
|
|
37
37
|
* Owners specify which relays to connect to via {@link OwnerTransport}. In
|
|
38
38
|
* WebSocket-based implementations, this check occurs before accepting the
|
|
@@ -42,20 +42,36 @@ export interface RelayConfig extends ConsoleConfig {
|
|
|
42
42
|
* ### Example
|
|
43
43
|
*
|
|
44
44
|
* ```ts
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
45
|
+
* // Client
|
|
46
|
+
* const transport = createOwnerWebSocketTransport({
|
|
47
|
+
* url: "wss://relay.evolu.dev",
|
|
48
|
+
* ownerId: owner.id,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* const evolu = createEvolu(deps)(Schema, {
|
|
52
|
+
* transports: [transport],
|
|
53
53
|
* });
|
|
54
|
+
*
|
|
55
|
+
*
|
|
56
|
+
* // Relay
|
|
57
|
+
* isOwnerAllowed: (ownerId) =>
|
|
58
|
+
* Promise.resolve(ownerId === "6jy_2F4RT5qqeLgJ14_dnQ"),
|
|
54
59
|
* ```
|
|
55
60
|
*/
|
|
56
|
-
readonly
|
|
61
|
+
readonly isOwnerAllowed?: (ownerId: OwnerId) => MaybeAsync<boolean>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A completely interchangeable server for syncing and backing up encrypted data
|
|
65
|
+
* between Evolu clients.
|
|
66
|
+
*
|
|
67
|
+
* Unlike traditional servers, relays are blind by design—they transmit
|
|
68
|
+
* encrypted data without understanding its shape or meaning. This enables true
|
|
69
|
+
* decentralization and infinite horizontal scalability with minimal
|
|
70
|
+
* infrastructure.
|
|
71
|
+
*/
|
|
72
|
+
export interface Relay extends Disposable {
|
|
57
73
|
}
|
|
58
|
-
export declare const createRelaySqliteStorage: (deps: SqliteStorageDeps & TimingSafeEqualDep) => (
|
|
74
|
+
export declare const createRelaySqliteStorage: (deps: SqliteStorageDeps & TimingSafeEqualDep) => (config: CreateBaseSqliteStorageConfig) => Storage;
|
|
59
75
|
export declare const createRelayStorageTables: (deps: SqliteDep) => Result<void, SqliteError>;
|
|
60
76
|
export interface RelayLogger {
|
|
61
77
|
readonly started: (enableLogging: boolean, port: number) => void;
|
|
@@ -63,7 +79,6 @@ export interface RelayLogger {
|
|
|
63
79
|
readonly upgradeSocketError: (error: Error) => void;
|
|
64
80
|
readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
|
|
65
81
|
readonly unauthorizedOwner: (ownerId: OwnerId) => void;
|
|
66
|
-
readonly authenticateOwnerError: (error: unknown) => void;
|
|
67
82
|
readonly connectionEstablished: (totalConnectionCount: number) => void;
|
|
68
83
|
readonly connectionWebSocketError: (error: Error) => void;
|
|
69
84
|
readonly relayOptionSubscribe: (ownerId: OwnerId, getSubscriberCount: LazyValue<number>) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"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"}
|
package/dist/src/Evolu/Relay.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
1
|
+
import { filterArray, isNonEmptyReadonlyArray, mapArray } from "../Array.js";
|
|
2
|
+
import { createInstances } from "../Instances.js";
|
|
2
3
|
import { err, ok } from "../Result.js";
|
|
3
4
|
import { sql } from "../Sqlite.js";
|
|
5
|
+
import { createMutex, isAsync } from "../Task.js";
|
|
6
|
+
import { PositiveInt } from "../Type.js";
|
|
7
|
+
import { ownerIdBytesToOwnerId, } from "./Owner.js";
|
|
4
8
|
import { createBaseSqliteStorage, } from "./Storage.js";
|
|
5
9
|
import { timestampToTimestampBytes } from "./Timestamp.js";
|
|
6
|
-
export const createRelaySqliteStorage = (deps) => (
|
|
7
|
-
const sqliteStorageBase = createBaseSqliteStorage(deps)(
|
|
10
|
+
export const createRelaySqliteStorage = (deps) => (config) => {
|
|
11
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps)(config);
|
|
12
|
+
/**
|
|
13
|
+
* Mutex instances are cached per OwnerId to prevent concurrent writes for
|
|
14
|
+
* the same owner. Instances are never evicted, causing a memory leak
|
|
15
|
+
* proportional to unique owner count. However, per-instance overhead should
|
|
16
|
+
* be small. Monitor production memory usage to determine if
|
|
17
|
+
* eviction/cleanup is needed.
|
|
18
|
+
*/
|
|
19
|
+
const ownerMutexes = createInstances();
|
|
8
20
|
return {
|
|
9
21
|
...sqliteStorageBase,
|
|
10
22
|
/**
|
|
@@ -23,22 +35,22 @@ export const createRelaySqliteStorage = (deps) => (options) => {
|
|
|
23
35
|
where ownerId = ${ownerId};
|
|
24
36
|
`);
|
|
25
37
|
if (!selectWriteKey.ok) {
|
|
26
|
-
|
|
38
|
+
config.onStorageError(selectWriteKey.error);
|
|
27
39
|
return false;
|
|
28
40
|
}
|
|
29
41
|
const { rows } = selectWriteKey.value;
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
insert into evolu_writeKey (ownerId, writeKey)
|
|
33
|
-
values (${ownerId}, ${writeKey});
|
|
34
|
-
`);
|
|
35
|
-
if (!insertWriteKey.ok) {
|
|
36
|
-
options.onStorageError(insertWriteKey.error);
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
42
|
+
if (isNonEmptyReadonlyArray(rows)) {
|
|
43
|
+
return deps.timingSafeEqual(rows[0].writeKey, writeKey);
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
const insertWriteKey = deps.sqlite.exec(sql `
|
|
46
|
+
insert into evolu_writeKey (ownerId, writeKey)
|
|
47
|
+
values (${ownerId}, ${writeKey});
|
|
48
|
+
`);
|
|
49
|
+
if (!insertWriteKey.ok) {
|
|
50
|
+
config.onStorageError(insertWriteKey.error);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
42
54
|
},
|
|
43
55
|
setWriteKey: (ownerId, writeKey) => {
|
|
44
56
|
const upsertWriteKey = deps.sqlite.exec(sql `
|
|
@@ -48,39 +60,82 @@ export const createRelaySqliteStorage = (deps) => (options) => {
|
|
|
48
60
|
set writeKey = excluded.writeKey;
|
|
49
61
|
`);
|
|
50
62
|
if (!upsertWriteKey.ok) {
|
|
51
|
-
|
|
63
|
+
config.onStorageError(upsertWriteKey.error);
|
|
52
64
|
return false;
|
|
53
65
|
}
|
|
54
66
|
return true;
|
|
55
67
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
68
|
+
writeMessages: async (ownerIdBytes, messages) => {
|
|
69
|
+
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
70
|
+
const messagesWithTimestampBytes = mapArray(messages, (m) => ({
|
|
71
|
+
timestamp: timestampToTimestampBytes(m.timestamp),
|
|
72
|
+
change: m.change,
|
|
73
|
+
}));
|
|
74
|
+
const result = await ownerMutexes
|
|
75
|
+
.ensure(ownerId, createMutex)
|
|
76
|
+
.withLock(async () => {
|
|
77
|
+
const existingTimestampsResult = sqliteStorageBase.getExistingTimestamps(ownerIdBytes, mapArray(messagesWithTimestampBytes, (m) => m.timestamp));
|
|
78
|
+
if (!existingTimestampsResult.ok)
|
|
79
|
+
return existingTimestampsResult;
|
|
80
|
+
const existingTimestampsSet = new Set(existingTimestampsResult.value.map((t) => t.toString()));
|
|
81
|
+
const newMessages = filterArray(messagesWithTimestampBytes, (m) => !existingTimestampsSet.has(m.timestamp.toString()));
|
|
82
|
+
// Nothing to write
|
|
83
|
+
if (!isNonEmptyReadonlyArray(newMessages)) {
|
|
84
|
+
return ok();
|
|
85
|
+
}
|
|
86
|
+
const storedBytesResult = deps.sqlite.exec(sql `
|
|
87
|
+
select storedBytes
|
|
88
|
+
from evolu_usage
|
|
89
|
+
where ownerId = ${ownerIdBytes};
|
|
73
90
|
`);
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
if (!storedBytesResult.ok)
|
|
92
|
+
return storedBytesResult;
|
|
93
|
+
const storedBytes = storedBytesResult.value.rows[0]?.storedBytes ?? 0;
|
|
94
|
+
const incomingBytes = newMessages.reduce((sum, m) => sum + m.change.length, 0);
|
|
95
|
+
const newStoredBytes = PositiveInt.orThrow(storedBytes + incomingBytes);
|
|
96
|
+
if (config.isOwnerWithinQuota) {
|
|
97
|
+
const withinQuotaResult = config.isOwnerWithinQuota(ownerId, newStoredBytes);
|
|
98
|
+
const isWithinQuota = isAsync(withinQuotaResult)
|
|
99
|
+
? await withinQuotaResult
|
|
100
|
+
: withinQuotaResult;
|
|
101
|
+
if (!isWithinQuota) {
|
|
102
|
+
return err({ type: "StorageQuotaError", ownerId });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return deps.sqlite.transaction(() => {
|
|
106
|
+
for (const { timestamp, change } of newMessages) {
|
|
107
|
+
const insertTimestampResult = sqliteStorageBase.insertTimestamp(ownerIdBytes, timestamp);
|
|
108
|
+
if (!insertTimestampResult.ok)
|
|
109
|
+
return insertTimestampResult;
|
|
110
|
+
const insertMessage = deps.sqlite.exec(sql `
|
|
111
|
+
insert into evolu_message ("ownerId", "timestamp", "change")
|
|
112
|
+
values (${ownerIdBytes}, ${timestamp}, ${change})
|
|
113
|
+
on conflict do nothing;
|
|
114
|
+
`);
|
|
115
|
+
if (!insertMessage.ok)
|
|
116
|
+
return insertMessage;
|
|
117
|
+
}
|
|
118
|
+
const updateUsage = deps.sqlite.exec(sql `
|
|
119
|
+
insert into evolu_usage ("ownerId", "storedBytes")
|
|
120
|
+
values (${ownerIdBytes}, ${newStoredBytes})
|
|
121
|
+
on conflict (ownerId) do update
|
|
122
|
+
set storedBytes = ${newStoredBytes};
|
|
123
|
+
`);
|
|
124
|
+
if (!updateUsage.ok)
|
|
125
|
+
return updateUsage;
|
|
126
|
+
return ok();
|
|
127
|
+
});
|
|
128
|
+
})();
|
|
129
|
+
if (!result.ok && result.error.type !== "AbortError") {
|
|
130
|
+
switch (result.error.type) {
|
|
131
|
+
case "SqliteError":
|
|
132
|
+
config.onStorageError(result.error);
|
|
133
|
+
return err({ type: "StorageWriteError", ownerId });
|
|
134
|
+
case "StorageQuotaError":
|
|
135
|
+
return err({ type: "StorageQuotaError", ownerId });
|
|
76
136
|
}
|
|
77
|
-
return ok();
|
|
78
|
-
});
|
|
79
|
-
if (!result.ok) {
|
|
80
|
-
options.onStorageError(result.error);
|
|
81
|
-
return false;
|
|
82
137
|
}
|
|
83
|
-
return
|
|
138
|
+
return ok();
|
|
84
139
|
},
|
|
85
140
|
readDbChange: (ownerId, timestamp) => {
|
|
86
141
|
const result = deps.sqlite.exec(sql `
|
|
@@ -89,13 +144,13 @@ export const createRelaySqliteStorage = (deps) => (options) => {
|
|
|
89
144
|
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
90
145
|
`);
|
|
91
146
|
if (!result.ok) {
|
|
92
|
-
|
|
147
|
+
config.onStorageError(result.error);
|
|
93
148
|
return null;
|
|
94
149
|
}
|
|
95
150
|
return result.value.rows[0]?.change;
|
|
96
151
|
},
|
|
97
152
|
deleteOwner: (ownerId) => {
|
|
98
|
-
const
|
|
153
|
+
const transactionResult = deps.sqlite.transaction(() => {
|
|
99
154
|
const deleteWriteKey = deps.sqlite.exec(sql `
|
|
100
155
|
delete from evolu_writeKey where ownerId = ${ownerId};
|
|
101
156
|
`);
|
|
@@ -106,14 +161,19 @@ export const createRelaySqliteStorage = (deps) => (options) => {
|
|
|
106
161
|
`);
|
|
107
162
|
if (!deleteMessages.ok)
|
|
108
163
|
return deleteMessages;
|
|
164
|
+
const deleteUsage = deps.sqlite.exec(sql `
|
|
165
|
+
delete from evolu_usage where ownerId = ${ownerId};
|
|
166
|
+
`);
|
|
167
|
+
if (!deleteUsage.ok)
|
|
168
|
+
return deleteUsage;
|
|
109
169
|
const deleteBaseOwner = sqliteStorageBase.deleteOwner(ownerId);
|
|
110
170
|
if (!deleteBaseOwner)
|
|
111
171
|
return err(null);
|
|
112
172
|
return ok();
|
|
113
173
|
});
|
|
114
|
-
if (!
|
|
115
|
-
if (
|
|
116
|
-
|
|
174
|
+
if (!transactionResult.ok) {
|
|
175
|
+
if (transactionResult.error)
|
|
176
|
+
config.onStorageError(transactionResult.error);
|
|
117
177
|
return false;
|
|
118
178
|
}
|
|
119
179
|
return true;
|
|
@@ -164,9 +224,6 @@ export const createRelayLogger = (deps) => ({
|
|
|
164
224
|
unauthorizedOwner: (ownerId) => {
|
|
165
225
|
deps.console.warn("[relay]", "unauthorized owner", { ownerId });
|
|
166
226
|
},
|
|
167
|
-
authenticateOwnerError: (error) => {
|
|
168
|
-
deps.console.error("[relay]", "authenticateOwner error", error);
|
|
169
|
-
},
|
|
170
227
|
connectionEstablished: (totalConnectionCount) => {
|
|
171
228
|
deps.console.log("[relay]", "connection", { totalConnectionCount });
|
|
172
229
|
},
|
|
@@ -3,16 +3,50 @@ import { Brand } from "../Brand.js";
|
|
|
3
3
|
import { RandomDep } from "../Random.js";
|
|
4
4
|
import { Result } from "../Result.js";
|
|
5
5
|
import { SqliteDep, SqliteError } from "../Sqlite.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { MaybeAsync } from "../Task.js";
|
|
7
|
+
import { NonNegativeInt, PositiveInt } from "../Type.js";
|
|
8
|
+
import { BaseOwnerError, OwnerId, OwnerIdBytes, OwnerWriteKey } from "./Owner.js";
|
|
8
9
|
import { Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
10
|
+
export interface StorageConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Optional callback to check if an {@link OwnerId} is within their quota for
|
|
13
|
+
* the requested write. If this callback is not provided, all writes are
|
|
14
|
+
* allowed regardless of size.
|
|
15
|
+
*
|
|
16
|
+
* If provided, the callback receives the OwnerId and the number of bytes
|
|
17
|
+
* required for the write, and should return a {@link MaybeAsync} boolean:
|
|
18
|
+
* `true` to allow the write, or `false` to deny it due to quota limits.
|
|
19
|
+
*
|
|
20
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
21
|
+
* asynchronous (for calling remote APIs).
|
|
22
|
+
*
|
|
23
|
+
* The callback returns a boolean rather than an error type because error
|
|
24
|
+
* handling and logging are the responsibility of the callback
|
|
25
|
+
* implementation.
|
|
26
|
+
*
|
|
27
|
+
* ### Example
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* // Client
|
|
31
|
+
* // evolu.subscribeError
|
|
32
|
+
*
|
|
33
|
+
* // Relay
|
|
34
|
+
* isOwnerWithinQuota: (ownerId, requiredBytes) => {
|
|
35
|
+
* console.log(ownerId, requiredBytes);
|
|
36
|
+
* // Check error via evolu.subscribeError
|
|
37
|
+
* return true;
|
|
38
|
+
* };
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
readonly isOwnerWithinQuota?: (ownerId: OwnerId, requiredBytes: PositiveInt) => MaybeAsync<boolean>;
|
|
42
|
+
}
|
|
9
43
|
/**
|
|
10
44
|
* Evolu Storage
|
|
11
45
|
*
|
|
12
|
-
*
|
|
13
|
-
* storage can be plugged in, as long as it implements this
|
|
14
|
-
* Implementations must handle their own errors; return values only
|
|
15
|
-
* overall success or failure.
|
|
46
|
+
* Evolu protocol using Storage is agnostic to storage implementation
|
|
47
|
+
* details—any storage can be plugged in, as long as it implements this
|
|
48
|
+
* interface. Implementations must handle their own errors; return values only
|
|
49
|
+
* indicate overall success or failure.
|
|
16
50
|
*
|
|
17
51
|
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
18
52
|
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
@@ -47,12 +81,12 @@ export interface Storage {
|
|
|
47
81
|
/**
|
|
48
82
|
* Write encrypted {@link CrdtMessage}s to storage.
|
|
49
83
|
*
|
|
50
|
-
* Must use a mutex
|
|
51
|
-
*
|
|
84
|
+
* Must use a mutex per ownerId to ensure sequential processing and proper
|
|
85
|
+
* protocol logic handling during sync operations.
|
|
52
86
|
*
|
|
53
|
-
*
|
|
87
|
+
* TODO: Use MaybeAsync
|
|
54
88
|
*/
|
|
55
|
-
readonly writeMessages: (
|
|
89
|
+
readonly writeMessages: (ownerIdBytes: OwnerIdBytes, messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>) => MaybeAsync<Result<void, StorageWriteError | StorageQuotaError>>;
|
|
56
90
|
/** Read encrypted {@link DbChange}s from storage. */
|
|
57
91
|
readonly readDbChange: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => EncryptedDbChange | null;
|
|
58
92
|
/**
|
|
@@ -65,6 +99,14 @@ export interface Storage {
|
|
|
65
99
|
export interface StorageDep {
|
|
66
100
|
readonly storage: Storage;
|
|
67
101
|
}
|
|
102
|
+
/** Error indicating a serious write failure. */
|
|
103
|
+
export interface StorageWriteError extends BaseOwnerError {
|
|
104
|
+
readonly type: "StorageWriteError";
|
|
105
|
+
}
|
|
106
|
+
/** Error when storage or billing quota is exceeded. */
|
|
107
|
+
export interface StorageQuotaError extends BaseOwnerError {
|
|
108
|
+
readonly type: "StorageQuotaError";
|
|
109
|
+
}
|
|
68
110
|
/**
|
|
69
111
|
* A cryptographic hash used for efficiently comparing collections of
|
|
70
112
|
* {@link TimestampBytes}s.
|
|
@@ -174,15 +216,20 @@ export interface BaseSqliteStorage extends Pick<Storage, "getSize" | "fingerprin
|
|
|
174
216
|
* messages may be received and processed multiple times.
|
|
175
217
|
*/
|
|
176
218
|
readonly insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => Result<void, SqliteError>;
|
|
219
|
+
/**
|
|
220
|
+
* Efficiently checks which timestamps already exist in the database using a
|
|
221
|
+
* single CTE query instead of N individual queries.
|
|
222
|
+
*/
|
|
223
|
+
readonly getExistingTimestamps: (ownerIdBytes: OwnerIdBytes, timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
|
|
177
224
|
}
|
|
178
225
|
export interface BaseSqliteStorageDep {
|
|
179
226
|
readonly storage: BaseSqliteStorage;
|
|
180
227
|
}
|
|
181
228
|
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
182
|
-
export interface
|
|
229
|
+
export interface CreateBaseSqliteStorageConfig extends StorageConfig {
|
|
183
230
|
onStorageError: (error: SqliteError) => void;
|
|
184
231
|
}
|
|
185
|
-
export declare const createBaseSqliteStorage: (deps: SqliteStorageDeps) => (
|
|
232
|
+
export declare const createBaseSqliteStorage: (deps: SqliteStorageDeps) => (config: CreateBaseSqliteStorageConfig) => BaseSqliteStorage;
|
|
186
233
|
export declare const createBaseSqliteStorageTables: (deps: SqliteDep) => Result<void, SqliteError>;
|
|
187
234
|
export declare const timestampBytesToFingerprint: (timestamp: TimestampBytes) => Fingerprint;
|
|
188
235
|
export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: OwnerIdBytes, index: NonNegativeInt) => Result<TimestampBytes, SqliteError>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"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"}
|