@evolu/common 6.0.1-preview.9 → 7.0.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 +2 -2
- package/dist/src/Array.d.ts +256 -12
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +215 -9
- 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/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- 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/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +56 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +40 -53
- package/dist/src/Evolu/Db.d.ts +162 -74
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +284 -702
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +213 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +189 -180
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +268 -240
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +595 -447
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
- package/dist/src/Evolu/PublicKysely.js +3 -4
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Relay.d.ts +91 -8
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +214 -88
- package/dist/src/Evolu/Schema.d.ts +125 -47
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +175 -31
- package/dist/src/Evolu/Storage.d.ts +249 -27
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +198 -92
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +469 -20
- package/dist/src/Evolu/Timestamp.d.ts +83 -30
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +79 -34
- 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/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +79 -0
- package/dist/src/Relation.d.ts.map +1 -0
- package/dist/src/Relation.js +127 -0
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +89 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +136 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +676 -343
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +718 -467
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/package.json +14 -13
- package/src/Array.ts +301 -19
- package/src/Assert.ts +0 -21
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -90
- package/src/Evolu/Db.ts +519 -1026
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +476 -360
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +846 -742
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +4 -5
- package/src/Evolu/Query.ts +3 -2
- package/src/Evolu/Relay.ts +406 -103
- package/src/Evolu/Schema.ts +323 -91
- package/src/Evolu/Storage.ts +559 -137
- package/src/Evolu/Sync.ts +819 -36
- package/src/Evolu/Timestamp.ts +90 -58
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/Number.ts +6 -10
- package/src/Object.ts +13 -5
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Relation.ts +234 -0
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +152 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1135 -730
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +8 -3
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/ManyToManyMap.d.ts +0 -26
- package/dist/src/ManyToManyMap.d.ts.map +0 -1
- package/dist/src/ManyToManyMap.js +0 -92
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/ManyToManyMap.ts +0 -140
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Public.ts
CHANGED
|
@@ -5,23 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { createEvolu } from "./Evolu.js";
|
|
8
|
-
export type {
|
|
9
|
-
Evolu,
|
|
10
|
-
EvoluConfigWithInitialData,
|
|
11
|
-
EvoluDeps,
|
|
12
|
-
EvoluError,
|
|
13
|
-
} from "./Evolu.js";
|
|
8
|
+
export type { Evolu, EvoluConfig, EvoluDeps, EvoluError } from "./Evolu.js";
|
|
14
9
|
export * from "./Owner.js";
|
|
15
|
-
export
|
|
16
|
-
export type { BinaryId } from "./Protocol.js";
|
|
10
|
+
export * from "./LocalAuth.js";
|
|
17
11
|
export * as kysely from "./PublicKysely.js";
|
|
18
12
|
export type { InferRow, Query, QueryRows, Row } from "./Query.js";
|
|
19
|
-
export {
|
|
20
|
-
export type { EvoluSchema, ValidMutationSizeError } from "./Schema.js";
|
|
13
|
+
export type { EvoluSchema } from "./Schema.js";
|
|
21
14
|
export type {
|
|
22
15
|
NetworkError,
|
|
23
16
|
PaymentRequiredError,
|
|
24
17
|
ServerError,
|
|
18
|
+
SyncOwner,
|
|
25
19
|
SyncState,
|
|
26
20
|
SyncStateInitial,
|
|
27
21
|
SyncStateIsNotSynced,
|
|
@@ -29,15 +23,14 @@ export type {
|
|
|
29
23
|
SyncStateIsSyncing,
|
|
30
24
|
} from "./Sync.js";
|
|
31
25
|
export {
|
|
32
|
-
binaryTimestampToTimestamp,
|
|
33
26
|
Timestamp,
|
|
34
|
-
|
|
27
|
+
timestampBytesToTimestamp,
|
|
28
|
+
timestampToTimestampBytes,
|
|
35
29
|
} from "./Timestamp.js";
|
|
36
30
|
export type {
|
|
37
|
-
|
|
31
|
+
TimestampBytes,
|
|
38
32
|
TimestampCounterOverflowError,
|
|
39
33
|
TimestampDriftError,
|
|
40
|
-
TimestampDuplicateNodeError,
|
|
41
34
|
TimestampError,
|
|
42
35
|
TimestampTimeOutOfRangeError,
|
|
43
36
|
} from "./Timestamp.js";
|
|
@@ -65,7 +65,7 @@ export type { NotNull } from "kysely";
|
|
|
65
65
|
* from "person"
|
|
66
66
|
* ```
|
|
67
67
|
*/
|
|
68
|
-
//
|
|
68
|
+
// Kysely expects strict AST.
|
|
69
69
|
// prettier-ignore
|
|
70
70
|
export function jsonArrayFrom<O>(
|
|
71
71
|
expr: SelectQueryBuilderExpression<O>,
|
|
@@ -122,7 +122,7 @@ export function jsonArrayFrom<O>(
|
|
|
122
122
|
* from "person";
|
|
123
123
|
* ```
|
|
124
124
|
*/
|
|
125
|
-
//
|
|
125
|
+
// Kysely expects strict AST.
|
|
126
126
|
// prettier-ignore
|
|
127
127
|
export function jsonObjectFrom<O>(
|
|
128
128
|
expr: SelectQueryBuilderExpression<O>,
|
|
@@ -172,7 +172,7 @@ export function jsonObjectFrom<O>(
|
|
|
172
172
|
* from "person"
|
|
173
173
|
* ```
|
|
174
174
|
*/
|
|
175
|
-
//
|
|
175
|
+
// Kysely expects strict AST.
|
|
176
176
|
// prettier-ignore
|
|
177
177
|
export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
|
|
178
178
|
obj: O,
|
|
@@ -209,8 +209,7 @@ export function getJsonObjectArgs(
|
|
|
209
209
|
table: string,
|
|
210
210
|
): Array<Expression<unknown> | string> {
|
|
211
211
|
const args: Array<Expression<unknown> | string> = [];
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
|
|
214
213
|
for (const { selection: s } of node.selections ?? []) {
|
|
215
214
|
if (ReferenceNode.is(s) && ColumnNode.is(s.column)) {
|
|
216
215
|
args.push(
|
package/src/Evolu/Query.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Brand } from "../Brand.js";
|
|
1
2
|
import { bytesToHex, hexToBytes } from "../Buffer.js";
|
|
2
3
|
import { objectToEntries } from "../Object.js";
|
|
3
4
|
import {
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
SqliteValue,
|
|
9
10
|
} from "../Sqlite.js";
|
|
10
11
|
import { Store, StoreSubscribe } from "../Store.js";
|
|
11
|
-
import {
|
|
12
|
+
import { Simplify } from "../Types.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* A type-safe SQL query.
|
|
@@ -51,7 +52,7 @@ export const serializeQuery = <R extends Row>(query: SqliteQuery): Query<R> => {
|
|
|
51
52
|
);
|
|
52
53
|
|
|
53
54
|
const options = query.options
|
|
54
|
-
? objectToEntries(query.options).
|
|
55
|
+
? objectToEntries(query.options).toSorted(([a], [b]) => a.localeCompare(b))
|
|
55
56
|
: [];
|
|
56
57
|
|
|
57
58
|
return JSON.stringify([query.sql, params, options]) as Query<R>;
|
package/src/Evolu/Relay.ts
CHANGED
|
@@ -1,61 +1,123 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
filterArray,
|
|
3
|
+
firstInArray,
|
|
4
|
+
isNonEmptyReadonlyArray,
|
|
5
|
+
mapArray,
|
|
6
|
+
} from "../Array.js";
|
|
7
|
+
import { ConsoleConfig, ConsoleDep } from "../Console.js";
|
|
3
8
|
import { TimingSafeEqualDep } from "../Crypto.js";
|
|
9
|
+
import { LazyValue } from "../Function.js";
|
|
10
|
+
import { createInstances } from "../Instances.js";
|
|
4
11
|
import { err, ok, Result } from "../Result.js";
|
|
5
|
-
import { sql, SqliteError } from "../Sqlite.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
12
|
+
import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
|
|
13
|
+
import { createMutex, isAsync, MaybeAsync, Mutex } from "../Task.js";
|
|
14
|
+
import { PositiveInt, SimpleName } from "../Type.js";
|
|
15
|
+
import {
|
|
16
|
+
OwnerId,
|
|
17
|
+
ownerIdBytesToOwnerId,
|
|
18
|
+
OwnerTransport,
|
|
19
|
+
OwnerWriteKey,
|
|
20
|
+
} from "./Owner.js";
|
|
21
|
+
import { ProtocolInvalidDataError } from "./Protocol.js";
|
|
9
22
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
createBaseSqliteStorage,
|
|
24
|
+
CreateBaseSqliteStorageConfig,
|
|
25
|
+
EncryptedDbChange,
|
|
26
|
+
getOwnerUsage,
|
|
27
|
+
getTimestampInsertStrategy,
|
|
12
28
|
SqliteStorageDeps,
|
|
29
|
+
Storage,
|
|
30
|
+
StorageConfig,
|
|
31
|
+
StorageQuotaError,
|
|
32
|
+
updateOwnerUsage,
|
|
13
33
|
} from "./Storage.js";
|
|
14
|
-
import {
|
|
34
|
+
import { timestampToTimestampBytes } from "./Timestamp.js";
|
|
15
35
|
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
export interface RelayConfig extends ConsoleConfig, StorageConfig {
|
|
37
|
+
/**
|
|
38
|
+
* The relay name.
|
|
39
|
+
*
|
|
40
|
+
* Implementations can use this for identification purposes (e.g., database
|
|
41
|
+
* file name, logging).
|
|
42
|
+
*/
|
|
19
43
|
readonly name?: SimpleName;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Optional callback to check if an {@link OwnerId} is allowed to access the
|
|
47
|
+
* relay. If this callback is not provided, all owners are allowed.
|
|
48
|
+
*
|
|
49
|
+
* The callback receives the {@link OwnerId} and returns a {@link MaybeAsync}
|
|
50
|
+
* boolean: `true` to allow access, or `false` to deny.
|
|
51
|
+
*
|
|
52
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
53
|
+
* asynchronous (for calling remote APIs).
|
|
54
|
+
*
|
|
55
|
+
* The callback returns a boolean rather than an error type because error
|
|
56
|
+
* handling and logging are the responsibility of the callback
|
|
57
|
+
* implementation.
|
|
58
|
+
*
|
|
59
|
+
* OwnerId is used rather than short-lived tokens because this only controls
|
|
60
|
+
* relay access, not write permissions. Since all data is encrypted on the
|
|
61
|
+
* relay, OwnerId exposure is safe.
|
|
62
|
+
*
|
|
63
|
+
* Owners specify which relays to connect to via {@link OwnerTransport}. In
|
|
64
|
+
* WebSocket-based implementations, this check occurs before accepting the
|
|
65
|
+
* connection, with the OwnerId typically extracted from the URL Path (e.g.,
|
|
66
|
+
* `ws://localhost:4000/<ownerId>`). The relay requires the URL to be in the
|
|
67
|
+
* correct format for OwnerId extraction.
|
|
68
|
+
*
|
|
69
|
+
* ### Example
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* // Client
|
|
73
|
+
* const transport = createOwnerWebSocketTransport({
|
|
74
|
+
* url: "wss://relay.evolu.dev",
|
|
75
|
+
* ownerId: owner.id,
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* const evolu = createEvolu(deps)(Schema, {
|
|
79
|
+
* transports: [transport],
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
*
|
|
83
|
+
* // Relay
|
|
84
|
+
* isOwnerAllowed: (ownerId) =>
|
|
85
|
+
* Promise.resolve(ownerId === "6jy_2F4RT5qqeLgJ14_dnQ"),
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
readonly isOwnerAllowed?: (ownerId: OwnerId) => MaybeAsync<boolean>;
|
|
20
89
|
}
|
|
21
90
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
]) {
|
|
50
|
-
const result = deps.sqlite.exec(query);
|
|
51
|
-
if (!result.ok) return result;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return ok({
|
|
55
|
-
...sqliteStorageBase.value,
|
|
91
|
+
/**
|
|
92
|
+
* A completely interchangeable server for syncing and backing up encrypted data
|
|
93
|
+
* between Evolu clients.
|
|
94
|
+
*
|
|
95
|
+
* Unlike traditional servers, relays are blind by design—they transmit
|
|
96
|
+
* encrypted data without understanding its shape or meaning. This enables true
|
|
97
|
+
* decentralization and infinite horizontal scalability with minimal
|
|
98
|
+
* infrastructure.
|
|
99
|
+
*/
|
|
100
|
+
export interface Relay extends Disposable {}
|
|
101
|
+
|
|
102
|
+
export const createRelaySqliteStorage =
|
|
103
|
+
(deps: SqliteStorageDeps & TimingSafeEqualDep) =>
|
|
104
|
+
(config: CreateBaseSqliteStorageConfig): Storage => {
|
|
105
|
+
const sqliteStorageBase = createBaseSqliteStorage(deps)(config);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mutex instances are cached per OwnerId to prevent concurrent writes for
|
|
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>();
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...sqliteStorageBase,
|
|
56
118
|
|
|
57
119
|
/**
|
|
58
|
-
* Lazily authorizes the initiator's {@link
|
|
120
|
+
* Lazily authorizes the initiator's {@link OwnerWriteKey} for the given
|
|
59
121
|
* {@link OwnerId}.
|
|
60
122
|
*
|
|
61
123
|
* - If the {@link OwnerId} does not exist, it is created and associated with
|
|
@@ -64,32 +126,34 @@ export const createRelayStorage =
|
|
|
64
126
|
* stored key.
|
|
65
127
|
*/
|
|
66
128
|
validateWriteKey: (ownerId, writeKey) => {
|
|
67
|
-
const selectWriteKey = deps.sqlite.exec<{ writeKey:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
129
|
+
const selectWriteKey = deps.sqlite.exec<{ writeKey: OwnerWriteKey }>(
|
|
130
|
+
sql`
|
|
131
|
+
select writeKey
|
|
132
|
+
from evolu_writeKey
|
|
133
|
+
where ownerId = ${ownerId};
|
|
134
|
+
`,
|
|
135
|
+
);
|
|
72
136
|
if (!selectWriteKey.ok) {
|
|
73
|
-
|
|
137
|
+
config.onStorageError(selectWriteKey.error);
|
|
74
138
|
return false;
|
|
75
139
|
}
|
|
76
140
|
|
|
77
141
|
const { rows } = selectWriteKey.value;
|
|
78
142
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
values (${ownerId}, ${writeKey});
|
|
83
|
-
`);
|
|
84
|
-
if (!insertWriteKey.ok) {
|
|
85
|
-
options.onStorageError(insertWriteKey.error);
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
143
|
+
if (isNonEmptyReadonlyArray(rows)) {
|
|
144
|
+
return deps.timingSafeEqual(rows[0].writeKey, writeKey);
|
|
145
|
+
}
|
|
88
146
|
|
|
89
|
-
|
|
147
|
+
const insertWriteKey = deps.sqlite.exec(sql`
|
|
148
|
+
insert into evolu_writeKey (ownerId, writeKey)
|
|
149
|
+
values (${ownerId}, ${writeKey});
|
|
150
|
+
`);
|
|
151
|
+
if (!insertWriteKey.ok) {
|
|
152
|
+
config.onStorageError(insertWriteKey.error);
|
|
153
|
+
return false;
|
|
90
154
|
}
|
|
91
155
|
|
|
92
|
-
return
|
|
156
|
+
return true;
|
|
93
157
|
},
|
|
94
158
|
|
|
95
159
|
setWriteKey: (ownerId, writeKey) => {
|
|
@@ -100,44 +164,120 @@ export const createRelayStorage =
|
|
|
100
164
|
set writeKey = excluded.writeKey;
|
|
101
165
|
`);
|
|
102
166
|
if (!upsertWriteKey.ok) {
|
|
103
|
-
|
|
167
|
+
config.onStorageError(upsertWriteKey.error);
|
|
104
168
|
return false;
|
|
105
169
|
}
|
|
106
170
|
|
|
107
171
|
return true;
|
|
108
172
|
},
|
|
109
173
|
|
|
110
|
-
writeMessages: (
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
174
|
+
writeMessages: async (ownerIdBytes, messages) => {
|
|
175
|
+
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
176
|
+
const messagesWithTimestampBytes = mapArray(messages, (m) => ({
|
|
177
|
+
timestamp: timestampToTimestampBytes(m.timestamp),
|
|
178
|
+
change: m.change,
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
const result = await ownerMutexes
|
|
182
|
+
.ensure(ownerId, createMutex)
|
|
183
|
+
.withLock<void, SqliteError | StorageQuotaError>(async () => {
|
|
184
|
+
const existingTimestampsResult =
|
|
185
|
+
sqliteStorageBase.getExistingTimestamps(
|
|
186
|
+
ownerIdBytes,
|
|
187
|
+
mapArray(messagesWithTimestampBytes, (m) => m.timestamp),
|
|
117
188
|
);
|
|
118
|
-
if (!
|
|
119
|
-
|
|
120
|
-
const insertMessage = deps.sqlite.exec(sql`
|
|
121
|
-
insert into evolu_message ("ownerId", "timestamp", "change")
|
|
122
|
-
values
|
|
123
|
-
(
|
|
124
|
-
${ownerId},
|
|
125
|
-
${timestampToBinaryTimestamp(message.timestamp)},
|
|
126
|
-
${message.change}
|
|
127
|
-
)
|
|
128
|
-
on conflict do nothing;
|
|
129
|
-
`);
|
|
130
|
-
if (!insertMessage.ok) return insertMessage;
|
|
131
|
-
}
|
|
132
|
-
return ok();
|
|
133
|
-
});
|
|
189
|
+
if (!existingTimestampsResult.ok) return existingTimestampsResult;
|
|
134
190
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
191
|
+
const existingTimestampsSet = new Set(
|
|
192
|
+
existingTimestampsResult.value.map((t) => t.toString()),
|
|
193
|
+
);
|
|
194
|
+
const newMessages = filterArray(
|
|
195
|
+
messagesWithTimestampBytes,
|
|
196
|
+
(m) => !existingTimestampsSet.has(m.timestamp.toString()),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Nothing to write
|
|
200
|
+
if (!isNonEmptyReadonlyArray(newMessages)) {
|
|
201
|
+
return ok();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const usage = getOwnerUsage(deps)(
|
|
205
|
+
ownerIdBytes,
|
|
206
|
+
firstInArray(newMessages).timestamp,
|
|
207
|
+
);
|
|
208
|
+
if (!usage.ok) return usage;
|
|
209
|
+
|
|
210
|
+
const { storedBytes } = usage.value;
|
|
211
|
+
|
|
212
|
+
const incomingBytes = newMessages.reduce(
|
|
213
|
+
(sum, m) => sum + m.change.length,
|
|
214
|
+
0,
|
|
215
|
+
);
|
|
216
|
+
const newStoredBytes = PositiveInt.orThrow(
|
|
217
|
+
(storedBytes ?? 0) + incomingBytes,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const withinQuotaResult = config.isOwnerWithinQuota(
|
|
221
|
+
ownerId,
|
|
222
|
+
newStoredBytes,
|
|
223
|
+
);
|
|
224
|
+
const isWithinQuota = isAsync(withinQuotaResult)
|
|
225
|
+
? await withinQuotaResult
|
|
226
|
+
: withinQuotaResult;
|
|
227
|
+
if (!isWithinQuota) {
|
|
228
|
+
return err({ type: "StorageQuotaError", ownerId });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
232
|
+
|
|
233
|
+
return deps.sqlite.transaction(() => {
|
|
234
|
+
for (const { timestamp, change } of newMessages) {
|
|
235
|
+
let strategy;
|
|
236
|
+
[strategy, firstTimestamp, lastTimestamp] =
|
|
237
|
+
getTimestampInsertStrategy(
|
|
238
|
+
timestamp,
|
|
239
|
+
firstTimestamp,
|
|
240
|
+
lastTimestamp,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const insertTimestampResult = sqliteStorageBase.insertTimestamp(
|
|
244
|
+
ownerIdBytes,
|
|
245
|
+
timestamp,
|
|
246
|
+
strategy,
|
|
247
|
+
);
|
|
248
|
+
if (!insertTimestampResult.ok) return insertTimestampResult;
|
|
249
|
+
|
|
250
|
+
const insertMessage = deps.sqlite.exec(sql`
|
|
251
|
+
insert into evolu_message ("ownerId", "timestamp", "change")
|
|
252
|
+
values (${ownerIdBytes}, ${timestamp}, ${change})
|
|
253
|
+
on conflict do nothing;
|
|
254
|
+
`);
|
|
255
|
+
if (!insertMessage.ok) return insertMessage;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const updateUsage = updateOwnerUsage(deps)(
|
|
259
|
+
ownerIdBytes,
|
|
260
|
+
newStoredBytes,
|
|
261
|
+
firstTimestamp,
|
|
262
|
+
lastTimestamp,
|
|
263
|
+
);
|
|
264
|
+
if (!updateUsage.ok) return updateUsage;
|
|
265
|
+
|
|
266
|
+
return ok();
|
|
267
|
+
});
|
|
268
|
+
})();
|
|
269
|
+
|
|
270
|
+
if (!result.ok && result.error.type !== "AbortError") {
|
|
271
|
+
switch (result.error.type) {
|
|
272
|
+
case "SqliteError":
|
|
273
|
+
config.onStorageError(result.error);
|
|
274
|
+
return err({ type: "StorageWriteError", ownerId });
|
|
275
|
+
case "StorageQuotaError":
|
|
276
|
+
return err({ type: "StorageQuotaError", ownerId });
|
|
277
|
+
}
|
|
138
278
|
}
|
|
139
279
|
|
|
140
|
-
return
|
|
280
|
+
return ok();
|
|
141
281
|
},
|
|
142
282
|
|
|
143
283
|
readDbChange: (ownerId, timestamp) => {
|
|
@@ -149,7 +289,7 @@ export const createRelayStorage =
|
|
|
149
289
|
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
150
290
|
`);
|
|
151
291
|
if (!result.ok) {
|
|
152
|
-
|
|
292
|
+
config.onStorageError(result.error);
|
|
153
293
|
return null;
|
|
154
294
|
}
|
|
155
295
|
|
|
@@ -157,27 +297,190 @@ export const createRelayStorage =
|
|
|
157
297
|
},
|
|
158
298
|
|
|
159
299
|
deleteOwner: (ownerId) => {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
300
|
+
const transactionResult = deps.sqlite.transaction(() => {
|
|
301
|
+
const deleteWriteKey = deps.sqlite.exec(sql`
|
|
162
302
|
delete from evolu_writeKey where ownerId = ${ownerId};
|
|
163
303
|
`);
|
|
164
|
-
if (!
|
|
304
|
+
if (!deleteWriteKey.ok) return deleteWriteKey;
|
|
165
305
|
|
|
166
|
-
const
|
|
306
|
+
const deleteMessages = deps.sqlite.exec(sql`
|
|
167
307
|
delete from evolu_message where ownerId = ${ownerId};
|
|
168
308
|
`);
|
|
169
|
-
if (!
|
|
309
|
+
if (!deleteMessages.ok) return deleteMessages;
|
|
170
310
|
|
|
171
|
-
const
|
|
172
|
-
|
|
311
|
+
const deleteUsage = deps.sqlite.exec(sql`
|
|
312
|
+
delete from evolu_usage where ownerId = ${ownerId};
|
|
313
|
+
`);
|
|
314
|
+
if (!deleteUsage.ok) return deleteUsage;
|
|
315
|
+
|
|
316
|
+
const deleteBaseOwner = sqliteStorageBase.deleteOwner(ownerId);
|
|
317
|
+
if (!deleteBaseOwner) return err(null);
|
|
173
318
|
|
|
174
319
|
return ok();
|
|
175
320
|
});
|
|
176
|
-
|
|
177
|
-
|
|
321
|
+
|
|
322
|
+
if (!transactionResult.ok) {
|
|
323
|
+
if (transactionResult.error)
|
|
324
|
+
config.onStorageError(transactionResult.error);
|
|
178
325
|
return false;
|
|
179
326
|
}
|
|
327
|
+
|
|
180
328
|
return true;
|
|
181
329
|
},
|
|
182
|
-
}
|
|
330
|
+
};
|
|
183
331
|
};
|
|
332
|
+
|
|
333
|
+
export const createRelayStorageTables = (
|
|
334
|
+
deps: SqliteDep,
|
|
335
|
+
): Result<void, SqliteError> => {
|
|
336
|
+
for (const query of [
|
|
337
|
+
sql`
|
|
338
|
+
create table evolu_writeKey (
|
|
339
|
+
"ownerId" blob not null,
|
|
340
|
+
"writeKey" blob not null,
|
|
341
|
+
primary key ("ownerId")
|
|
342
|
+
)
|
|
343
|
+
strict;
|
|
344
|
+
`,
|
|
345
|
+
|
|
346
|
+
sql`
|
|
347
|
+
create table evolu_message (
|
|
348
|
+
"ownerId" blob not null,
|
|
349
|
+
"timestamp" blob not null,
|
|
350
|
+
"change" blob not null,
|
|
351
|
+
primary key ("ownerId", "timestamp")
|
|
352
|
+
)
|
|
353
|
+
strict;
|
|
354
|
+
`,
|
|
355
|
+
]) {
|
|
356
|
+
const result = deps.sqlite.exec(query);
|
|
357
|
+
if (!result.ok) return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return ok();
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export interface RelayLogger {
|
|
364
|
+
readonly started: (enableLogging: boolean, port: number) => void;
|
|
365
|
+
readonly storageError: (error: unknown) => void;
|
|
366
|
+
readonly upgradeSocketError: (error: Error) => void;
|
|
367
|
+
readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
|
|
368
|
+
readonly unauthorizedOwner: (ownerId: OwnerId) => void;
|
|
369
|
+
readonly connectionEstablished: (totalConnectionCount: number) => void;
|
|
370
|
+
readonly connectionWebSocketError: (error: Error) => void;
|
|
371
|
+
readonly relayOptionSubscribe: (
|
|
372
|
+
ownerId: OwnerId,
|
|
373
|
+
getSubscriberCount: LazyValue<number>,
|
|
374
|
+
) => void;
|
|
375
|
+
readonly relayOptionUnsubscribe: (
|
|
376
|
+
ownerId: OwnerId,
|
|
377
|
+
getSubscriberCount: LazyValue<number>,
|
|
378
|
+
) => void;
|
|
379
|
+
readonly relayOptionBroadcast: (
|
|
380
|
+
ownerId: OwnerId,
|
|
381
|
+
broadcastCount: number,
|
|
382
|
+
subscriberCount: number,
|
|
383
|
+
) => void;
|
|
384
|
+
readonly messageLength: (messageLength: number) => void;
|
|
385
|
+
readonly applyProtocolMessageAsRelayError: (
|
|
386
|
+
error: ProtocolInvalidDataError,
|
|
387
|
+
) => void;
|
|
388
|
+
readonly responseLength: (responseLength: number) => void;
|
|
389
|
+
readonly applyProtocolMessageAsRelayUnknownError: (error: unknown) => void;
|
|
390
|
+
readonly connectionClosed: (totalConnectionCount: number) => void;
|
|
391
|
+
readonly shuttingDown: () => void;
|
|
392
|
+
readonly webSocketServerDisposed: () => void;
|
|
393
|
+
readonly httpServerDisposed: () => void;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const createRelayLogger = (deps: ConsoleDep): RelayLogger => ({
|
|
397
|
+
started: (enableLogging, port) => {
|
|
398
|
+
deps.console.enabled = true;
|
|
399
|
+
deps.console.log(`Evolu Relay started on port ${port}`);
|
|
400
|
+
deps.console.enabled = enableLogging;
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
storageError: (error) => {
|
|
404
|
+
deps.console.error("[relay]", "storage", error);
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
upgradeSocketError: (error) => {
|
|
408
|
+
deps.console.warn("[relay]", "socket error", { error });
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
invalidOrMissingOwnerIdInUrl: (url) => {
|
|
412
|
+
deps.console.warn("[relay]", "invalid or missing ownerId in URL", { url });
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
unauthorizedOwner: (ownerId) => {
|
|
416
|
+
deps.console.warn("[relay]", "unauthorized owner", { ownerId });
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
connectionEstablished: (totalConnectionCount) => {
|
|
420
|
+
deps.console.log("[relay]", "connection", { totalConnectionCount });
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
connectionWebSocketError: (error) => {
|
|
424
|
+
deps.console.error("[relay]", "error", { error });
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
relayOptionSubscribe: (ownerId, getSubscriberCount) => {
|
|
428
|
+
if (deps.console.enabled)
|
|
429
|
+
deps.console.log("[relay]", "subscribe", {
|
|
430
|
+
ownerId,
|
|
431
|
+
subscriberCount: getSubscriberCount(),
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
relayOptionUnsubscribe: (ownerId, getSubscriberCount) => {
|
|
436
|
+
if (deps.console.enabled)
|
|
437
|
+
deps.console.log("[relay]", "unsubscribe", {
|
|
438
|
+
ownerId,
|
|
439
|
+
subscriberCount: getSubscriberCount(),
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
relayOptionBroadcast: (ownerId, broadcastCount, totalSubscribers) => {
|
|
444
|
+
deps.console.log("[relay]", "broadcast", {
|
|
445
|
+
ownerId,
|
|
446
|
+
broadcastCount,
|
|
447
|
+
totalSubscribers,
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
messageLength: (messageLength) => {
|
|
452
|
+
deps.console.log("[relay]", "on message", { messageLength });
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
applyProtocolMessageAsRelayError: (error) => {
|
|
456
|
+
deps.console.error("[relay]", "applyProtocolMessageAsRelay", error);
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
responseLength: (responseLength) => {
|
|
460
|
+
deps.console.log("[relay]", "responseLength", { responseLength });
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
applyProtocolMessageAsRelayUnknownError: (error) => {
|
|
464
|
+
deps.console.error(
|
|
465
|
+
"[relay]",
|
|
466
|
+
"applyProtocolMessageAsRelayUnknownError",
|
|
467
|
+
error,
|
|
468
|
+
);
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
connectionClosed: (totalConnectionCount) => {
|
|
472
|
+
deps.console.log("[relay]", "close", { totalConnectionCount });
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
shuttingDown: () => {
|
|
476
|
+
deps.console.log("Shutting down Evolu Relay...");
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
webSocketServerDisposed: () => {
|
|
480
|
+
deps.console.log("Evolu Relay WebSocketServer disposed");
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
httpServerDisposed: () => {
|
|
484
|
+
deps.console.log("Evolu Relay HTTP server disposed");
|
|
485
|
+
},
|
|
486
|
+
});
|