@evolu/common 7.4.1 → 8.0.0-next.1
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 +162 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +200 -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 +29 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +329 -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 +2274 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1499 -348
- package/dist/src/Test.d.ts +150 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +97 -0
- package/dist/src/Time.d.ts +202 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +199 -88
- 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 +587 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +370 -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 +130 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +274 -113
- 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 +621 -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 +34 -49
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -177
- 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 +54 -58
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +127 -179
- 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 +37 -24
- 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 +429 -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 +520 -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 +3696 -740
- package/src/Test.ts +202 -0
- package/src/Time.ts +357 -147
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +866 -430
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +404 -167
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +867 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -824
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +441 -307
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +119 -273
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +211 -283
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- 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 -526
- package/src/Instances.ts +0 -92
- 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 -949
package/src/local-first/Relay.ts
CHANGED
|
@@ -1,53 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay server for data synchronization.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import {
|
|
8
|
+
dedupeArray,
|
|
2
9
|
filterArray,
|
|
3
10
|
firstInArray,
|
|
4
|
-
|
|
11
|
+
isNonEmptyArray,
|
|
5
12
|
mapArray,
|
|
6
13
|
} from "../Array.js";
|
|
7
|
-
import {
|
|
8
|
-
import { TimingSafeEqualDep } from "../Crypto.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { assert } from "../Assert.js";
|
|
15
|
+
import type { TimingSafeEqualDep } from "../Crypto.js";
|
|
16
|
+
import { err, ok } from "../Result.js";
|
|
17
|
+
import type { SqliteDep } from "../Sqlite.js";
|
|
18
|
+
import { sql } from "../Sqlite.js";
|
|
19
|
+
import { createMutexByKey } from "../Task.js";
|
|
20
|
+
import { Name, PositiveInt, uint8ArrayToBase64Url } from "../Type.js";
|
|
21
|
+
import { isPromiseLike, type Awaitable } from "../Types.js";
|
|
15
22
|
import {
|
|
16
23
|
OwnerId,
|
|
17
24
|
ownerIdBytesToOwnerId,
|
|
18
|
-
OwnerTransport,
|
|
25
|
+
// OwnerTransport,
|
|
19
26
|
OwnerWriteKey,
|
|
20
27
|
} from "./Owner.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
createBaseSqliteStorage,
|
|
24
|
-
CreateBaseSqliteStorageConfig,
|
|
28
|
+
import type {
|
|
25
29
|
EncryptedDbChange,
|
|
26
|
-
getOwnerUsage,
|
|
27
|
-
getTimestampInsertStrategy,
|
|
28
30
|
SqliteStorageDeps,
|
|
29
31
|
Storage,
|
|
30
32
|
StorageConfig,
|
|
31
33
|
StorageQuotaError,
|
|
34
|
+
} from "./Storage.js";
|
|
35
|
+
import {
|
|
36
|
+
createBaseSqliteStorage,
|
|
37
|
+
getTimestampInsertStrategy,
|
|
38
|
+
readOwnerUsageOrDefault,
|
|
32
39
|
updateOwnerUsage,
|
|
33
40
|
} from "./Storage.js";
|
|
34
41
|
import { timestampToTimestampBytes } from "./Timestamp.js";
|
|
35
42
|
|
|
36
|
-
export interface RelayConfig extends
|
|
43
|
+
export interface RelayConfig extends StorageConfig {
|
|
37
44
|
/**
|
|
38
45
|
* The relay name.
|
|
39
46
|
*
|
|
40
47
|
* Implementations can use this for identification purposes (e.g., database
|
|
41
48
|
* file name, logging).
|
|
42
49
|
*/
|
|
43
|
-
readonly name?:
|
|
50
|
+
readonly name?: Name;
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
53
|
* Optional callback to check if an {@link OwnerId} is allowed to access the
|
|
47
54
|
* relay. If this callback is not provided, all owners are allowed.
|
|
48
55
|
*
|
|
49
|
-
* The callback receives the {@link OwnerId} and
|
|
50
|
-
*
|
|
56
|
+
* The callback receives the {@link OwnerId} and an options object with an
|
|
57
|
+
* abort `AbortSignal`, and returns a {@link Awaitable} boolean: `true` to
|
|
58
|
+
* allow access, or `false` to deny.
|
|
51
59
|
*
|
|
52
60
|
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
53
61
|
* asynchronous (for calling remote APIs).
|
|
@@ -60,11 +68,11 @@ export interface RelayConfig extends ConsoleConfig, StorageConfig {
|
|
|
60
68
|
* relay access, not write permissions. Since all data is encrypted on the
|
|
61
69
|
* relay, OwnerId exposure is safe.
|
|
62
70
|
*
|
|
63
|
-
* Owners specify which relays to connect to via
|
|
71
|
+
* Owners specify which relays to connect to via `OwnerTransport`. In
|
|
64
72
|
* WebSocket-based implementations, this check occurs before accepting the
|
|
65
|
-
* connection, with the OwnerId typically extracted from the URL
|
|
66
|
-
* `ws://localhost:4000
|
|
67
|
-
* correct format for OwnerId extraction.
|
|
73
|
+
* connection, with the OwnerId typically extracted from the URL query string
|
|
74
|
+
* (e.g., `ws://localhost:4000?ownerId=...`). The relay requires the URL to be
|
|
75
|
+
* in the correct format for OwnerId extraction.
|
|
68
76
|
*
|
|
69
77
|
* ### Example
|
|
70
78
|
*
|
|
@@ -81,37 +89,39 @@ export interface RelayConfig extends ConsoleConfig, StorageConfig {
|
|
|
81
89
|
*
|
|
82
90
|
*
|
|
83
91
|
* // Relay
|
|
84
|
-
* isOwnerAllowed: (ownerId) =>
|
|
92
|
+
* isOwnerAllowed: (ownerId, { signal: _signal }) =>
|
|
85
93
|
* Promise.resolve(ownerId === "6jy_2F4RT5qqeLgJ14_dnQ"),
|
|
86
94
|
* ```
|
|
87
95
|
*/
|
|
88
|
-
readonly isOwnerAllowed?: (
|
|
96
|
+
readonly isOwnerAllowed?: (
|
|
97
|
+
ownerId: OwnerId,
|
|
98
|
+
options: {
|
|
99
|
+
/** Aborted when the relay stops waiting for the access check. */
|
|
100
|
+
readonly signal: AbortSignal;
|
|
101
|
+
},
|
|
102
|
+
) => Awaitable<boolean>;
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
/**
|
|
92
|
-
*
|
|
93
|
-
* between Evolu clients.
|
|
106
|
+
* Sync and backup relay for Evolu clients.
|
|
94
107
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
108
|
+
* A relay syncs and backs up encrypted data for Evolu apps. Evolu apps can use
|
|
109
|
+
* multiple relays at the same time, combining self-hosted and cloud relays for
|
|
110
|
+
* resilience. Relays are blind by design: they transmit and store encrypted
|
|
111
|
+
* data without understanding its shape or meaning.
|
|
99
112
|
*/
|
|
100
|
-
export interface Relay extends
|
|
113
|
+
export interface Relay extends AsyncDisposable {
|
|
114
|
+
/** The TCP port actually bound by the relay. */
|
|
115
|
+
readonly port: number;
|
|
116
|
+
}
|
|
101
117
|
|
|
102
118
|
export const createRelaySqliteStorage =
|
|
103
119
|
(deps: SqliteStorageDeps & TimingSafeEqualDep) =>
|
|
104
|
-
(config:
|
|
105
|
-
const sqliteStorageBase = createBaseSqliteStorage(deps)
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
* the same owner. Instances are never evicted, causing a memory leak
|
|
110
|
-
* proportional to unique owner count. However, per-instance overhead should
|
|
111
|
-
* be small. Monitor production memory usage to determine if
|
|
112
|
-
* eviction/cleanup is needed.
|
|
113
|
-
*/
|
|
114
|
-
const ownerMutexes = createInstances<OwnerId, Mutex>();
|
|
120
|
+
(config: StorageConfig): Storage => {
|
|
121
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps);
|
|
122
|
+
|
|
123
|
+
/** Mutex keyed by OwnerId to prevent concurrent writes. */
|
|
124
|
+
const mutexByOwnerId = createMutexByKey<OwnerId>();
|
|
115
125
|
|
|
116
126
|
return {
|
|
117
127
|
...sqliteStorageBase,
|
|
@@ -133,97 +143,91 @@ export const createRelaySqliteStorage =
|
|
|
133
143
|
where ownerId = ${ownerId};
|
|
134
144
|
`,
|
|
135
145
|
);
|
|
136
|
-
|
|
137
|
-
config.onStorageError(selectWriteKey.error);
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const { rows } = selectWriteKey.value;
|
|
146
|
+
const { rows } = selectWriteKey;
|
|
142
147
|
|
|
143
|
-
if (
|
|
148
|
+
if (isNonEmptyArray(rows)) {
|
|
144
149
|
return deps.timingSafeEqual(rows[0].writeKey, writeKey);
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
deps.sqlite.exec(sql`
|
|
148
153
|
insert into evolu_writeKey (ownerId, writeKey)
|
|
149
154
|
values (${ownerId}, ${writeKey});
|
|
150
155
|
`);
|
|
151
|
-
if (!insertWriteKey.ok) {
|
|
152
|
-
config.onStorageError(insertWriteKey.error);
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
156
|
|
|
156
157
|
return true;
|
|
157
158
|
},
|
|
158
159
|
|
|
159
160
|
setWriteKey: (ownerId, writeKey) => {
|
|
160
|
-
|
|
161
|
+
deps.sqlite.exec(sql`
|
|
161
162
|
insert into evolu_writeKey (ownerId, writeKey)
|
|
162
163
|
values (${ownerId}, ${writeKey})
|
|
163
164
|
on conflict (ownerId) do update
|
|
164
165
|
set writeKey = excluded.writeKey;
|
|
165
166
|
`);
|
|
166
|
-
if (!upsertWriteKey.ok) {
|
|
167
|
-
config.onStorageError(upsertWriteKey.error);
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return true;
|
|
172
167
|
},
|
|
173
168
|
|
|
174
|
-
writeMessages:
|
|
169
|
+
writeMessages: (ownerIdBytes, messages) => async (run) => {
|
|
175
170
|
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
const uniqueMessagesWithTimestampBytes = dedupeArray(
|
|
172
|
+
mapArray(messages, (m) => ({
|
|
173
|
+
timestamp: timestampToTimestampBytes(m.timestamp),
|
|
174
|
+
change: m.change,
|
|
175
|
+
})),
|
|
176
|
+
(message) => uint8ArrayToBase64Url(message.timestamp),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return run(
|
|
180
|
+
mutexByOwnerId.withLock(ownerId, async () => {
|
|
184
181
|
const existingTimestampsResult =
|
|
185
182
|
sqliteStorageBase.getExistingTimestamps(
|
|
186
183
|
ownerIdBytes,
|
|
187
|
-
mapArray(
|
|
184
|
+
mapArray(uniqueMessagesWithTimestampBytes, (m) => m.timestamp),
|
|
188
185
|
);
|
|
189
|
-
if (!existingTimestampsResult.ok) return existingTimestampsResult;
|
|
190
186
|
|
|
191
|
-
const
|
|
192
|
-
existingTimestampsResult
|
|
187
|
+
const existingTimestampKeys = new Set(
|
|
188
|
+
mapArray(existingTimestampsResult, uint8ArrayToBase64Url),
|
|
193
189
|
);
|
|
194
190
|
const newMessages = filterArray(
|
|
195
|
-
|
|
196
|
-
(
|
|
191
|
+
uniqueMessagesWithTimestampBytes,
|
|
192
|
+
(message) =>
|
|
193
|
+
!existingTimestampKeys.has(
|
|
194
|
+
uint8ArrayToBase64Url(message.timestamp),
|
|
195
|
+
),
|
|
197
196
|
);
|
|
198
197
|
|
|
199
198
|
// Nothing to write
|
|
200
|
-
if (!
|
|
199
|
+
if (!isNonEmptyArray(newMessages)) {
|
|
201
200
|
return ok();
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
const usage =
|
|
203
|
+
const usage = readOwnerUsageOrDefault(deps)(
|
|
205
204
|
ownerIdBytes,
|
|
206
205
|
firstInArray(newMessages).timestamp,
|
|
207
206
|
);
|
|
208
|
-
if (!usage.ok) return usage;
|
|
209
|
-
|
|
210
|
-
const { storedBytes } = usage.value;
|
|
211
207
|
|
|
212
208
|
const incomingBytes = newMessages.reduce(
|
|
213
209
|
(sum, m) => sum + m.change.length,
|
|
214
210
|
0,
|
|
215
211
|
);
|
|
216
212
|
const newStoredBytes = PositiveInt.orThrow(
|
|
217
|
-
(storedBytes ?? 0) + incomingBytes,
|
|
213
|
+
(usage.storedBytes ?? 0) + incomingBytes,
|
|
218
214
|
);
|
|
219
215
|
|
|
220
|
-
const
|
|
221
|
-
|
|
216
|
+
const quotaResult = config.isOwnerWithinQuota(
|
|
217
|
+
ownerId,
|
|
218
|
+
newStoredBytes,
|
|
219
|
+
);
|
|
220
|
+
const isWithinQuota = isPromiseLike(quotaResult)
|
|
221
|
+
? await quotaResult
|
|
222
|
+
: quotaResult;
|
|
222
223
|
if (!isWithinQuota) {
|
|
223
|
-
return err({
|
|
224
|
+
return err<StorageQuotaError>({
|
|
225
|
+
type: "StorageQuotaError",
|
|
226
|
+
ownerId,
|
|
227
|
+
});
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
let { firstTimestamp, lastTimestamp } = usage
|
|
230
|
+
let { firstTimestamp, lastTimestamp } = usage;
|
|
227
231
|
|
|
228
232
|
return deps.sqlite.transaction(() => {
|
|
229
233
|
for (const { timestamp, change } of newMessages) {
|
|
@@ -235,45 +239,31 @@ export const createRelaySqliteStorage =
|
|
|
235
239
|
lastTimestamp,
|
|
236
240
|
);
|
|
237
241
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
values (${ownerIdBytes}, ${timestamp}, ${change})
|
|
251
|
-
on conflict do nothing;
|
|
252
|
-
`);
|
|
253
|
-
if (!result.ok) return result;
|
|
254
|
-
}
|
|
242
|
+
sqliteStorageBase.insertTimestamp(
|
|
243
|
+
ownerIdBytes,
|
|
244
|
+
timestamp,
|
|
245
|
+
strategy,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
deps.sqlite.exec(sql`
|
|
249
|
+
insert into evolu_message
|
|
250
|
+
("ownerId", "timestamp", "change")
|
|
251
|
+
values (${ownerIdBytes}, ${timestamp}, ${change})
|
|
252
|
+
on conflict do nothing;
|
|
253
|
+
`);
|
|
255
254
|
}
|
|
256
255
|
|
|
257
|
-
|
|
256
|
+
updateOwnerUsage(deps)(
|
|
258
257
|
ownerIdBytes,
|
|
259
258
|
newStoredBytes,
|
|
260
259
|
firstTimestamp,
|
|
261
260
|
lastTimestamp,
|
|
262
261
|
);
|
|
263
|
-
});
|
|
264
|
-
})();
|
|
265
|
-
|
|
266
|
-
if (!result.ok && result.error.type !== "AbortError") {
|
|
267
|
-
switch (result.error.type) {
|
|
268
|
-
case "SqliteError":
|
|
269
|
-
config.onStorageError(result.error);
|
|
270
|
-
return err({ type: "StorageWriteError", ownerId });
|
|
271
|
-
case "StorageQuotaError":
|
|
272
|
-
return err({ type: "StorageQuotaError", ownerId });
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
262
|
|
|
276
|
-
|
|
263
|
+
return ok();
|
|
264
|
+
});
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
277
267
|
},
|
|
278
268
|
|
|
279
269
|
readDbChange: (ownerId, timestamp) => {
|
|
@@ -284,51 +274,35 @@ export const createRelaySqliteStorage =
|
|
|
284
274
|
from evolu_message
|
|
285
275
|
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
286
276
|
`);
|
|
287
|
-
if (!result.ok) {
|
|
288
|
-
config.onStorageError(result.error);
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
277
|
|
|
292
|
-
|
|
278
|
+
const row = result.rows[0];
|
|
279
|
+
assert(row, "Every timestamp must have a change");
|
|
280
|
+
return row.change;
|
|
293
281
|
},
|
|
294
282
|
|
|
295
283
|
deleteOwner: (ownerId) => {
|
|
296
|
-
|
|
297
|
-
|
|
284
|
+
deps.sqlite.transaction(() => {
|
|
285
|
+
deps.sqlite.exec(sql`
|
|
298
286
|
delete from evolu_writeKey where ownerId = ${ownerId};
|
|
299
287
|
`);
|
|
300
|
-
if (!deleteWriteKey.ok) return deleteWriteKey;
|
|
301
288
|
|
|
302
|
-
|
|
289
|
+
deps.sqlite.exec(sql`
|
|
303
290
|
delete from evolu_message where ownerId = ${ownerId};
|
|
304
291
|
`);
|
|
305
|
-
if (!deleteMessages.ok) return deleteMessages;
|
|
306
292
|
|
|
307
|
-
|
|
293
|
+
deps.sqlite.exec(sql`
|
|
308
294
|
delete from evolu_usage where ownerId = ${ownerId};
|
|
309
295
|
`);
|
|
310
|
-
if (!deleteUsage.ok) return deleteUsage;
|
|
311
296
|
|
|
312
|
-
|
|
313
|
-
if (!deleteBaseOwner) return err(null);
|
|
297
|
+
sqliteStorageBase.deleteOwner(ownerId);
|
|
314
298
|
|
|
315
299
|
return ok();
|
|
316
300
|
});
|
|
317
|
-
|
|
318
|
-
if (!transactionResult.ok) {
|
|
319
|
-
if (transactionResult.error)
|
|
320
|
-
config.onStorageError(transactionResult.error);
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return true;
|
|
325
301
|
},
|
|
326
302
|
};
|
|
327
303
|
};
|
|
328
304
|
|
|
329
|
-
export const createRelayStorageTables = (
|
|
330
|
-
deps: SqliteDep,
|
|
331
|
-
): Result<void, SqliteError> => {
|
|
305
|
+
export const createRelayStorageTables = (deps: SqliteDep): void => {
|
|
332
306
|
for (const query of [
|
|
333
307
|
sql`
|
|
334
308
|
create table evolu_writeKey (
|
|
@@ -349,134 +323,6 @@ export const createRelayStorageTables = (
|
|
|
349
323
|
strict;
|
|
350
324
|
`,
|
|
351
325
|
]) {
|
|
352
|
-
|
|
353
|
-
if (!result.ok) return result;
|
|
326
|
+
deps.sqlite.exec(query);
|
|
354
327
|
}
|
|
355
|
-
|
|
356
|
-
return ok();
|
|
357
328
|
};
|
|
358
|
-
|
|
359
|
-
export interface RelayLogger {
|
|
360
|
-
readonly started: (enableLogging: boolean, port: number) => void;
|
|
361
|
-
readonly storageError: (error: unknown) => void;
|
|
362
|
-
readonly upgradeSocketError: (error: Error) => void;
|
|
363
|
-
readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
|
|
364
|
-
readonly unauthorizedOwner: (ownerId: OwnerId) => void;
|
|
365
|
-
readonly connectionEstablished: (totalConnectionCount: number) => void;
|
|
366
|
-
readonly connectionWebSocketError: (error: Error) => void;
|
|
367
|
-
readonly relayOptionSubscribe: (
|
|
368
|
-
ownerId: OwnerId,
|
|
369
|
-
getSubscriberCount: LazyValue<number>,
|
|
370
|
-
) => void;
|
|
371
|
-
readonly relayOptionUnsubscribe: (
|
|
372
|
-
ownerId: OwnerId,
|
|
373
|
-
getSubscriberCount: LazyValue<number>,
|
|
374
|
-
) => void;
|
|
375
|
-
readonly relayOptionBroadcast: (
|
|
376
|
-
ownerId: OwnerId,
|
|
377
|
-
broadcastCount: number,
|
|
378
|
-
subscriberCount: number,
|
|
379
|
-
) => void;
|
|
380
|
-
readonly messageLength: (messageLength: number) => void;
|
|
381
|
-
readonly applyProtocolMessageAsRelayError: (
|
|
382
|
-
error: ProtocolInvalidDataError,
|
|
383
|
-
) => void;
|
|
384
|
-
readonly responseLength: (responseLength: number) => void;
|
|
385
|
-
readonly applyProtocolMessageAsRelayUnknownError: (error: unknown) => void;
|
|
386
|
-
readonly connectionClosed: (totalConnectionCount: number) => void;
|
|
387
|
-
readonly shuttingDown: () => void;
|
|
388
|
-
readonly webSocketServerDisposed: () => void;
|
|
389
|
-
readonly httpServerDisposed: () => void;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export const createRelayLogger = (deps: ConsoleDep): RelayLogger => ({
|
|
393
|
-
started: (enableLogging, port) => {
|
|
394
|
-
deps.console.enabled = true;
|
|
395
|
-
deps.console.log(`Evolu Relay started on port ${port}`);
|
|
396
|
-
deps.console.enabled = enableLogging;
|
|
397
|
-
},
|
|
398
|
-
|
|
399
|
-
storageError: (error) => {
|
|
400
|
-
deps.console.error("[relay]", "storage", error);
|
|
401
|
-
},
|
|
402
|
-
|
|
403
|
-
upgradeSocketError: (error) => {
|
|
404
|
-
deps.console.warn("[relay]", "socket error", { error });
|
|
405
|
-
},
|
|
406
|
-
|
|
407
|
-
invalidOrMissingOwnerIdInUrl: (url) => {
|
|
408
|
-
deps.console.warn("[relay]", "invalid or missing ownerId in URL", { url });
|
|
409
|
-
},
|
|
410
|
-
|
|
411
|
-
unauthorizedOwner: (ownerId) => {
|
|
412
|
-
deps.console.warn("[relay]", "unauthorized owner", { ownerId });
|
|
413
|
-
},
|
|
414
|
-
|
|
415
|
-
connectionEstablished: (totalConnectionCount) => {
|
|
416
|
-
deps.console.log("[relay]", "connection", { totalConnectionCount });
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
connectionWebSocketError: (error) => {
|
|
420
|
-
deps.console.error("[relay]", "error", { error });
|
|
421
|
-
},
|
|
422
|
-
|
|
423
|
-
relayOptionSubscribe: (ownerId, getSubscriberCount) => {
|
|
424
|
-
if (deps.console.enabled)
|
|
425
|
-
deps.console.log("[relay]", "subscribe", {
|
|
426
|
-
ownerId,
|
|
427
|
-
subscriberCount: getSubscriberCount(),
|
|
428
|
-
});
|
|
429
|
-
},
|
|
430
|
-
|
|
431
|
-
relayOptionUnsubscribe: (ownerId, getSubscriberCount) => {
|
|
432
|
-
if (deps.console.enabled)
|
|
433
|
-
deps.console.log("[relay]", "unsubscribe", {
|
|
434
|
-
ownerId,
|
|
435
|
-
subscriberCount: getSubscriberCount(),
|
|
436
|
-
});
|
|
437
|
-
},
|
|
438
|
-
|
|
439
|
-
relayOptionBroadcast: (ownerId, broadcastCount, totalSubscribers) => {
|
|
440
|
-
deps.console.log("[relay]", "broadcast", {
|
|
441
|
-
ownerId,
|
|
442
|
-
broadcastCount,
|
|
443
|
-
totalSubscribers,
|
|
444
|
-
});
|
|
445
|
-
},
|
|
446
|
-
|
|
447
|
-
messageLength: (messageLength) => {
|
|
448
|
-
deps.console.log("[relay]", "on message", { messageLength });
|
|
449
|
-
},
|
|
450
|
-
|
|
451
|
-
applyProtocolMessageAsRelayError: (error) => {
|
|
452
|
-
deps.console.error("[relay]", "applyProtocolMessageAsRelay", error);
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
responseLength: (responseLength) => {
|
|
456
|
-
deps.console.log("[relay]", "responseLength", { responseLength });
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
applyProtocolMessageAsRelayUnknownError: (error) => {
|
|
460
|
-
deps.console.error(
|
|
461
|
-
"[relay]",
|
|
462
|
-
"applyProtocolMessageAsRelayUnknownError",
|
|
463
|
-
error,
|
|
464
|
-
);
|
|
465
|
-
},
|
|
466
|
-
|
|
467
|
-
connectionClosed: (totalConnectionCount) => {
|
|
468
|
-
deps.console.log("[relay]", "close", { totalConnectionCount });
|
|
469
|
-
},
|
|
470
|
-
|
|
471
|
-
shuttingDown: () => {
|
|
472
|
-
deps.console.log("Shutting down Evolu Relay...");
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
webSocketServerDisposed: () => {
|
|
476
|
-
deps.console.log("Evolu Relay WebSocketServer disposed");
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
httpServerDisposed: () => {
|
|
480
|
-
deps.console.log("Evolu Relay HTTP server disposed");
|
|
481
|
-
},
|
|
482
|
-
});
|