@evolu/common 6.0.1-preview.20 → 6.0.1-preview.21
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/Evolu/Db.d.ts +23 -1
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.d.ts +60 -0
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +33 -0
- package/dist/src/Evolu/Protocol.d.ts +91 -30
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +80 -25
- package/dist/src/Evolu/Relay.d.ts +70 -3
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +74 -1
- package/dist/src/Evolu/Sync.d.ts +2 -11
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +7 -46
- package/dist/src/Result.d.ts +45 -16
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +23 -0
- package/dist/src/Task.js +1 -1
- package/dist/src/Type.d.ts +12 -0
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +12 -0
- package/package.json +1 -1
- package/src/Evolu/Db.ts +24 -1
- package/src/Evolu/Owner.ts +70 -1
- package/src/Evolu/Protocol.ts +117 -36
- package/src/Evolu/Relay.ts +180 -6
- package/src/Evolu/Sync.ts +7 -57
- package/src/Result.ts +47 -16
- package/src/Task.ts +3 -3
- package/src/Type.ts +12 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable jsdoc/no-undefined-types */
|
|
2
1
|
/**
|
|
3
2
|
* Evolu Protocol
|
|
4
3
|
*
|
|
@@ -21,7 +20,7 @@
|
|
|
21
20
|
* transfer, ownership, real-time broadcasting, request-response semantics, and
|
|
22
21
|
* error handling.
|
|
23
22
|
*
|
|
24
|
-
* ### Message
|
|
23
|
+
* ### Message structure
|
|
25
24
|
*
|
|
26
25
|
* | Field | Notes |
|
|
27
26
|
* | :----------------------------- | :------------------------ |
|
|
@@ -44,7 +43,7 @@
|
|
|
44
43
|
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
45
44
|
* | - {@link Range} | |
|
|
46
45
|
*
|
|
47
|
-
* ### WriteKey
|
|
46
|
+
* ### WriteKey validation
|
|
48
47
|
*
|
|
49
48
|
* The initiator sends a hasWriteKey flag and optionally a WriteKey. The
|
|
50
49
|
* WriteKey is required when sending messages as a secure token proving the
|
|
@@ -80,33 +79,33 @@
|
|
|
80
79
|
* initiator. In relay-to-relay or P2P sync, both sides may require the
|
|
81
80
|
* {@link OwnerWriteKey} depending on who is the initiator.
|
|
82
81
|
*
|
|
83
|
-
* ### Protocol
|
|
82
|
+
* ### Protocol errors
|
|
84
83
|
*
|
|
85
84
|
* The protocol uses error codes in the header to signal issues:
|
|
86
85
|
*
|
|
87
86
|
* - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
|
|
88
|
-
* - {@link ProtocolWriteError}: A write
|
|
89
|
-
*
|
|
90
|
-
* - {@link ProtocolSyncError}: A generic or unexpected synchronization failure
|
|
87
|
+
* - {@link ProtocolWriteError}: A serious relay-side write failure occurred.
|
|
88
|
+
* - {@link ProtocolSyncError}: A serious relay-side synchronization failure
|
|
91
89
|
* occurred.
|
|
90
|
+
* - {@link ProtocolQuotaExceededError}: Storage or billing quota exceeded.
|
|
92
91
|
* - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
|
|
93
92
|
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
94
93
|
*
|
|
95
94
|
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
96
95
|
* to allow clients to associate errors with the correct owner.
|
|
97
96
|
*
|
|
98
|
-
* ### Message
|
|
97
|
+
* ### Message size limit
|
|
99
98
|
*
|
|
100
99
|
* The protocol enforces a strict maximum size for all messages, defined by
|
|
101
|
-
* {@link
|
|
100
|
+
* {@link ProtocolMessageMaxSize}. This ensures every {@link ProtocolMessage} is
|
|
102
101
|
* less than or equal to this limit, enabling stateless transports, simplified
|
|
103
102
|
* relay implementation, and predictable memory usage. When all messages don't
|
|
104
103
|
* fit within the limit, the protocol automatically continues synchronization in
|
|
105
104
|
* subsequent rounds using range-based reconciliation.
|
|
106
105
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* {@link
|
|
106
|
+
* Database mutations are limited to 640KB, which is smaller than the protocol
|
|
107
|
+
* message limit to ensure efficient sync with
|
|
108
|
+
* {@link defaultProtocolMessageRangesMaxSize}.
|
|
110
109
|
*
|
|
111
110
|
* ### Why Binary?
|
|
112
111
|
*
|
|
@@ -149,6 +148,15 @@
|
|
|
149
148
|
* Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
|
|
150
149
|
* over time and provide clear feedback about version mismatches.
|
|
151
150
|
*
|
|
151
|
+
* ### Credible exit
|
|
152
|
+
*
|
|
153
|
+
* The protocol specification is intentionally non-configurable to ensure
|
|
154
|
+
* universal compatibility. This design allows applications (users) to switch
|
|
155
|
+
* between any compliant relay without negotiation or compatibility checks
|
|
156
|
+
* beyond version matching. Relays are generic infrastructure that any
|
|
157
|
+
* application can use interchangeably making exit from any single provider
|
|
158
|
+
* technically feasible and economically viable.
|
|
159
|
+
*
|
|
152
160
|
* @module
|
|
153
161
|
*/
|
|
154
162
|
import { Packr } from "msgpackr";
|
|
@@ -160,21 +168,62 @@ import { eqArrayNumber } from "../Eq.js";
|
|
|
160
168
|
import { computeBalancedBuckets } from "../Number.js";
|
|
161
169
|
import { objectToEntries } from "../Object.js";
|
|
162
170
|
import { err, ok } from "../Result.js";
|
|
163
|
-
import { Base64Url, base64UrlToUint8Array, DateIso, Id, idBytesToId, idBytesTypeValueLength, idToIdBytes, Json, jsonToJsonValue, NonNegativeInt, Number, uint8ArrayToBase64Url, } from "../Type.js";
|
|
171
|
+
import { Base64Url, base64UrlToUint8Array, between, DateIso, Id, idBytesToId, idBytesTypeValueLength, idToIdBytes, Int, Json, jsonToJsonValue, NonNegativeInt, Number, uint8ArrayToBase64Url, } from "../Type.js";
|
|
164
172
|
import { ownerIdToOwnerIdBytes, ownerWriteKeyLength, } from "./Owner.js";
|
|
165
173
|
import { fingerprintSize, InfiniteUpperBound, RangeType, } from "./Storage.js";
|
|
166
174
|
import { Counter, eqTimestamp, Millis, timestampBytesLength, timestampBytesToTimestamp, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
167
175
|
/**
|
|
168
|
-
* MessagePack
|
|
176
|
+
* Evolu uses MessagePack for numbers and JSONs.
|
|
169
177
|
*
|
|
170
178
|
* - `variableMapSize: true` - More compact maps, ~5-10% slower encoding
|
|
171
179
|
* - `useRecords: false` - Standard MessagePack without extensions
|
|
172
180
|
*/
|
|
173
181
|
const packr = new Packr({ variableMapSize: true, useRecords: false });
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
|
|
182
|
+
const minProtocolMessageMaxSize = 1_000_000;
|
|
183
|
+
const maxProtocolMessageMaxSize = 100_000_000;
|
|
184
|
+
/**
|
|
185
|
+
* Protocol message maximum size.
|
|
186
|
+
*
|
|
187
|
+
* Defines the upper limit for how large a single protocol message can be.
|
|
188
|
+
* Implementations must enforce a maximum size between 1MB and 100MB to ensure
|
|
189
|
+
* compatibility across all Evolu implementations (the maximum size of mutation
|
|
190
|
+
* change is hardcoded and enforced hence the maximum size can't be smaller).
|
|
191
|
+
*
|
|
192
|
+
* Larger maximum sizes can be configured by relays to reduce roundtrips. For
|
|
193
|
+
* example, a dedicated relay with ample resources could configure a 100MB
|
|
194
|
+
* maximum to minimize roundtrips for large syncs.
|
|
195
|
+
*
|
|
196
|
+
* Only relays can safely configure larger sizes, as clients will handle them.
|
|
197
|
+
* Increasing this value on the client side would break compatibility with
|
|
198
|
+
* relays that enforce smaller limits.
|
|
199
|
+
*/
|
|
200
|
+
export const ProtocolMessageMaxSize = between(minProtocolMessageMaxSize, maxProtocolMessageMaxSize)(Int);
|
|
201
|
+
/**
|
|
202
|
+
* Default {@link ProtocolMessageMaxSize} (1MB).
|
|
203
|
+
*
|
|
204
|
+
* The standard size used across Evolu implementations. Relays with more
|
|
205
|
+
* resources can configure larger sizes to reduce roundtrips.
|
|
206
|
+
*/
|
|
207
|
+
export const defaultProtocolMessageMaxSize = minProtocolMessageMaxSize;
|
|
208
|
+
/**
|
|
209
|
+
* Protocol message ranges maximum size.
|
|
210
|
+
*
|
|
211
|
+
* Defines the upper limit for how large the ranges section of a protocol
|
|
212
|
+
* message can be. Implementations must enforce a maximum size between 3KB and
|
|
213
|
+
* 100KB to ensure compatibility.
|
|
214
|
+
*
|
|
215
|
+
* The upper bound is set to ensure ranges fit within the default 1MB
|
|
216
|
+
* {@link defaultProtocolMessageMaxSize}, maintaining compatibility between all
|
|
217
|
+
* clients and relays.
|
|
218
|
+
*/
|
|
219
|
+
export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
|
|
220
|
+
/**
|
|
221
|
+
* Default {@link ProtocolMessageRangesMaxSize} (30KB).
|
|
222
|
+
*
|
|
223
|
+
* The standard size used across Evolu implementations. Relays with more
|
|
224
|
+
* resources can configure larger sizes to reduce roundtrips.
|
|
225
|
+
*/
|
|
226
|
+
export const defaultProtocolMessageRangesMaxSize = 30_000;
|
|
178
227
|
/** Evolu Protocol version. */
|
|
179
228
|
export const protocolVersion = 0;
|
|
180
229
|
export const MessageType = {
|
|
@@ -201,18 +250,20 @@ export const ProtocolErrorCode = {
|
|
|
201
250
|
WriteError: 2,
|
|
202
251
|
/** A code for {@link ProtocolSyncError}. */
|
|
203
252
|
SyncError: 3,
|
|
253
|
+
/** A code for {@link ProtocolQuotaExceededError}. */
|
|
254
|
+
QuotaExceededError: 4,
|
|
204
255
|
};
|
|
205
256
|
/**
|
|
206
257
|
* Creates a {@link ProtocolMessage} from CRDT messages.
|
|
207
258
|
*
|
|
208
|
-
* If the message size would exceed {@link
|
|
209
|
-
* ensures all messages will be sent in the next round(s) even over
|
|
259
|
+
* If the message size would exceed {@link defaultProtocolMessageMaxSize}, the
|
|
260
|
+
* protocol ensures all messages will be sent in the next round(s) even over
|
|
210
261
|
* unidirectional and stateless transports.
|
|
211
262
|
*/
|
|
212
263
|
export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
|
|
213
264
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
214
265
|
messageType: MessageType.Request,
|
|
215
|
-
totalMaxSize: maxSize ??
|
|
266
|
+
totalMaxSize: maxSize ?? defaultProtocolMessageMaxSize,
|
|
216
267
|
writeKey: owner.writeKey,
|
|
217
268
|
});
|
|
218
269
|
let notAllMessagesSent = false;
|
|
@@ -236,8 +287,8 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
|
|
|
236
287
|
*
|
|
237
288
|
* The ideal approach would be to send three ranges (skip, fingerprint,
|
|
238
289
|
* skip) where the fingerprint of unsent messages would act as narrow sync
|
|
239
|
-
* probe. I think we can send
|
|
240
|
-
*
|
|
290
|
+
* probe. I think we can send `zeroFingerprint` which can be interpreted
|
|
291
|
+
* as an indication that the other side should reply with
|
|
241
292
|
* {@link TimestampsRange}, so no need to restart syncing.
|
|
242
293
|
*
|
|
243
294
|
* For now, using a random fingerprint avoids extra complexity and is good
|
|
@@ -272,7 +323,7 @@ export const createProtocolMessageForUnsubscribe = (ownerId) => createProtocolMe
|
|
|
272
323
|
subscriptionFlag: SubscriptionFlags.Unsubscribe,
|
|
273
324
|
}).unwrap();
|
|
274
325
|
export const createProtocolMessageBuffer = (ownerId, options) => {
|
|
275
|
-
const { totalMaxSize =
|
|
326
|
+
const { totalMaxSize = defaultProtocolMessageMaxSize, rangesMaxSize = defaultProtocolMessageRangesMaxSize, version = protocolVersion, } = options;
|
|
276
327
|
const buffers = {
|
|
277
328
|
header: createBuffer(),
|
|
278
329
|
messages: {
|
|
@@ -507,6 +558,11 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
507
558
|
type: "ProtocolSyncError",
|
|
508
559
|
ownerId,
|
|
509
560
|
});
|
|
561
|
+
case ProtocolErrorCode.QuotaExceededError:
|
|
562
|
+
return err({
|
|
563
|
+
type: "ProtocolQuotaExceededError",
|
|
564
|
+
ownerId,
|
|
565
|
+
});
|
|
510
566
|
default:
|
|
511
567
|
throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
|
|
512
568
|
}
|
|
@@ -537,7 +593,6 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
537
593
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
538
594
|
messageType: MessageType.Request,
|
|
539
595
|
writeKey,
|
|
540
|
-
totalMaxSize: options.totalMaxSize,
|
|
541
596
|
rangesMaxSize: options.rangesMaxSize,
|
|
542
597
|
});
|
|
543
598
|
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
@@ -1,14 +1,81 @@
|
|
|
1
|
-
import { ConsoleConfig } from "../Console.js";
|
|
1
|
+
import { ConsoleConfig, ConsoleDep } from "../Console.js";
|
|
2
2
|
import { TimingSafeEqualDep } from "../Crypto.js";
|
|
3
|
+
import { LazyValue } from "../Function.js";
|
|
3
4
|
import { Result } from "../Result.js";
|
|
4
5
|
import { SqliteError } from "../Sqlite.js";
|
|
5
6
|
import { SimpleName } from "../Type.js";
|
|
7
|
+
import { OwnerId } from "./Owner.js";
|
|
8
|
+
import { ProtocolInvalidDataError } from "./Protocol.js";
|
|
6
9
|
import { CreateSqliteStorageBaseOptions, SqliteStorageDeps, Storage } from "./Storage.js";
|
|
7
10
|
export interface Relay extends Disposable {
|
|
8
11
|
}
|
|
9
12
|
export interface RelayConfig extends ConsoleConfig {
|
|
13
|
+
/**
|
|
14
|
+
* The relay name.
|
|
15
|
+
*
|
|
16
|
+
* Implementations can use this for identification purposes (e.g., database
|
|
17
|
+
* file name, logging).
|
|
18
|
+
*/
|
|
10
19
|
readonly name?: SimpleName;
|
|
20
|
+
/**
|
|
21
|
+
* Optional callback to authenticate an {@link OwnerId} with the relay.
|
|
22
|
+
*
|
|
23
|
+
* If this callback is not provided, all owners are allowed.
|
|
24
|
+
*
|
|
25
|
+
* If provided, the callback receives the OwnerId and should return a promise
|
|
26
|
+
* that resolves to `true` to allow access, or `false` to deny.
|
|
27
|
+
*
|
|
28
|
+
* The callback returns a boolean rather than an error type because error
|
|
29
|
+
* handling and logging are the responsibility of the callback implementation,
|
|
30
|
+
* not the relay. This prevents leaking authentication implementation details
|
|
31
|
+
* into the generic relay interface.
|
|
32
|
+
*
|
|
33
|
+
* OwnerId is used for authentication rather than short-lived tokens because
|
|
34
|
+
* this only controls relay access, not write permissions. Since all data is
|
|
35
|
+
* encrypted on the relay, OwnerId exposure is safe.
|
|
36
|
+
*
|
|
37
|
+
* Owners specify which relays to connect to via {@link TransportConfig}. In
|
|
38
|
+
* WebSocket-based implementations, this check occurs before accepting the
|
|
39
|
+
* connection, with the OwnerId typically extracted from the URL path (e.g.,
|
|
40
|
+
* `ws://localhost:4000/<ownerId>`).
|
|
41
|
+
*
|
|
42
|
+
* ### Example
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const relay = await createNodeJsRelay(deps)({
|
|
46
|
+
* authenticateOwner: async (ownerId) => {
|
|
47
|
+
* const isRegistered = await db.checkOwner(ownerId);
|
|
48
|
+
* if (!isRegistered) {
|
|
49
|
+
* logger.warn("Unauthorized access attempt", { ownerId });
|
|
50
|
+
* }
|
|
51
|
+
* return isRegistered;
|
|
52
|
+
* },
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
readonly authenticateOwner?: (ownerId: OwnerId) => Promise<boolean>;
|
|
11
57
|
}
|
|
12
|
-
export
|
|
13
|
-
export
|
|
58
|
+
export declare const createRelaySqliteStorage: (deps: SqliteStorageDeps & TimingSafeEqualDep) => (options: CreateSqliteStorageBaseOptions) => Result<Storage, SqliteError>;
|
|
59
|
+
export interface RelayLogger {
|
|
60
|
+
readonly started: (enableLogging: boolean, port: number) => void;
|
|
61
|
+
readonly storageError: (error: unknown) => void;
|
|
62
|
+
readonly upgradeSocketError: (error: Error) => void;
|
|
63
|
+
readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
|
|
64
|
+
readonly unauthorizedOwner: (ownerId: OwnerId) => void;
|
|
65
|
+
readonly authenticateOwnerError: (error: unknown) => void;
|
|
66
|
+
readonly connectionEstablished: (totalConnectionCount: number) => void;
|
|
67
|
+
readonly connectionWebSocketError: (error: Error) => void;
|
|
68
|
+
readonly relayOptionSubscribe: (ownerId: OwnerId, getSubscriberCount: LazyValue<number>) => void;
|
|
69
|
+
readonly relayOptionUnsubscribe: (ownerId: OwnerId, getSubscriberCount: LazyValue<number>) => void;
|
|
70
|
+
readonly relayOptionBroadcast: (ownerId: OwnerId, broadcastCount: number, subscriberCount: number) => void;
|
|
71
|
+
readonly messageLength: (messageLength: number) => void;
|
|
72
|
+
readonly applyProtocolMessageAsRelayError: (error: ProtocolInvalidDataError) => void;
|
|
73
|
+
readonly responseLength: (responseLength: number) => void;
|
|
74
|
+
readonly applyProtocolMessageAsRelayUnknownError: (error: unknown) => void;
|
|
75
|
+
readonly connectionClosed: (totalConnectionCount: number) => void;
|
|
76
|
+
readonly shuttingDown: () => void;
|
|
77
|
+
readonly webSocketServerDisposed: () => void;
|
|
78
|
+
readonly httpServerDisposed: () => void;
|
|
79
|
+
}
|
|
80
|
+
export declare const createRelayLogger: (deps: ConsoleDep) => RelayLogger;
|
|
14
81
|
//# sourceMappingURL=Relay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,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
|
@@ -3,7 +3,7 @@ import { err, ok } from "../Result.js";
|
|
|
3
3
|
import { sql } from "../Sqlite.js";
|
|
4
4
|
import { createSqliteStorageBase, } from "./Storage.js";
|
|
5
5
|
import { timestampToTimestampBytes } from "./Timestamp.js";
|
|
6
|
-
export const
|
|
6
|
+
export const createRelaySqliteStorage = (deps) => (options) => {
|
|
7
7
|
const sqliteStorageBase = createSqliteStorageBase(deps)(options);
|
|
8
8
|
if (!sqliteStorageBase.ok)
|
|
9
9
|
return sqliteStorageBase;
|
|
@@ -145,3 +145,76 @@ export const createRelayStorage = (deps) => (options) => {
|
|
|
145
145
|
},
|
|
146
146
|
});
|
|
147
147
|
};
|
|
148
|
+
export const createRelayLogger = (deps) => ({
|
|
149
|
+
started: (enableLogging, port) => {
|
|
150
|
+
deps.console.enabled = true;
|
|
151
|
+
deps.console.log(`Evolu Relay started on port ${port}`);
|
|
152
|
+
deps.console.enabled = enableLogging;
|
|
153
|
+
},
|
|
154
|
+
storageError: (error) => {
|
|
155
|
+
deps.console.error("[relay]", "storage", error);
|
|
156
|
+
},
|
|
157
|
+
upgradeSocketError: (error) => {
|
|
158
|
+
deps.console.warn("[relay]", "socket error", { error });
|
|
159
|
+
},
|
|
160
|
+
invalidOrMissingOwnerIdInUrl: (url) => {
|
|
161
|
+
deps.console.warn("[relay]", "invalid or missing ownerId in URL", { url });
|
|
162
|
+
},
|
|
163
|
+
unauthorizedOwner: (ownerId) => {
|
|
164
|
+
deps.console.warn("[relay]", "unauthorized owner", { ownerId });
|
|
165
|
+
},
|
|
166
|
+
authenticateOwnerError: (error) => {
|
|
167
|
+
deps.console.error("[relay]", "authenticateOwner error", error);
|
|
168
|
+
},
|
|
169
|
+
connectionEstablished: (totalConnectionCount) => {
|
|
170
|
+
deps.console.log("[relay]", "connection", { totalConnectionCount });
|
|
171
|
+
},
|
|
172
|
+
connectionWebSocketError: (error) => {
|
|
173
|
+
deps.console.error("[relay]", "error", { error });
|
|
174
|
+
},
|
|
175
|
+
relayOptionSubscribe: (ownerId, getSubscriberCount) => {
|
|
176
|
+
if (deps.console.enabled)
|
|
177
|
+
deps.console.log("[relay]", "subscribe", {
|
|
178
|
+
ownerId,
|
|
179
|
+
subscriberCount: getSubscriberCount(),
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
relayOptionUnsubscribe: (ownerId, getSubscriberCount) => {
|
|
183
|
+
if (deps.console.enabled)
|
|
184
|
+
deps.console.log("[relay]", "unsubscribe", {
|
|
185
|
+
ownerId,
|
|
186
|
+
subscriberCount: getSubscriberCount(),
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
relayOptionBroadcast: (ownerId, broadcastCount, totalSubscribers) => {
|
|
190
|
+
deps.console.log("[relay]", "broadcast", {
|
|
191
|
+
ownerId,
|
|
192
|
+
broadcastCount,
|
|
193
|
+
totalSubscribers,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
messageLength: (messageLength) => {
|
|
197
|
+
deps.console.log("[relay]", "on message", { messageLength });
|
|
198
|
+
},
|
|
199
|
+
applyProtocolMessageAsRelayError: (error) => {
|
|
200
|
+
deps.console.error("[relay]", "applyProtocolMessageAsRelay", error);
|
|
201
|
+
},
|
|
202
|
+
responseLength: (responseLength) => {
|
|
203
|
+
deps.console.log("[relay]", "responseLength", { responseLength });
|
|
204
|
+
},
|
|
205
|
+
applyProtocolMessageAsRelayUnknownError: (error) => {
|
|
206
|
+
deps.console.error("[relay]", "applyProtocolMessageAsRelayUnknownError", error);
|
|
207
|
+
},
|
|
208
|
+
connectionClosed: (totalConnectionCount) => {
|
|
209
|
+
deps.console.log("[relay]", "close", { totalConnectionCount });
|
|
210
|
+
},
|
|
211
|
+
shuttingDown: () => {
|
|
212
|
+
deps.console.log("Shutting down Evolu Relay...");
|
|
213
|
+
},
|
|
214
|
+
webSocketServerDisposed: () => {
|
|
215
|
+
deps.console.log("Evolu Relay WebSocketServer disposed");
|
|
216
|
+
},
|
|
217
|
+
httpServerDisposed: () => {
|
|
218
|
+
deps.console.log("Evolu Relay HTTP server disposed");
|
|
219
|
+
},
|
|
220
|
+
});
|
package/dist/src/Evolu/Sync.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { AppOwner, OwnerEncryptionKey, OwnerId, OwnerIdBytes, OwnerWriteKey, Tra
|
|
|
12
12
|
import { ProtocolError, ProtocolInvalidDataError, ProtocolTimestampMismatchError } from "./Protocol.js";
|
|
13
13
|
import { MutationChange } from "./Schema.js";
|
|
14
14
|
import { CrdtMessage, SqliteStorageBase, Storage } from "./Storage.js";
|
|
15
|
-
import { Millis, Timestamp,
|
|
15
|
+
import { Millis, Timestamp, TimestampConfigDep, TimestampCounterOverflowError, TimestampDriftError, TimestampTimeOutOfRangeError } from "./Timestamp.js";
|
|
16
16
|
export interface Sync extends Disposable {
|
|
17
17
|
/**
|
|
18
18
|
* Assigns or removes an owner to/from transports with reference counting.
|
|
@@ -68,6 +68,7 @@ export interface ClientStorageDep {
|
|
|
68
68
|
readonly storage: ClientStorage;
|
|
69
69
|
}
|
|
70
70
|
export declare const applyLocalOnlyChange: (deps: SqliteDep & TimeDep) => (change: MutationChange) => Result<void, SqliteError>;
|
|
71
|
+
export declare const applyMessages: (deps: ClientStorageDep & ClockDep & RandomDep & SqliteDep) => (ownerId: OwnerId, messages: ReadonlyArray<CrdtMessage>) => Result<void, SqliteError>;
|
|
71
72
|
export declare const applyMessageToTimestampAndHistoryTables: (deps: ClientStorageDep & SqliteDep) => (ownerId: OwnerIdBytes, message: CrdtMessage) => Result<void, SqliteError>;
|
|
72
73
|
/**
|
|
73
74
|
* TODO: Rework for the new owners API.
|
|
@@ -110,14 +111,4 @@ export interface PaymentRequiredError {
|
|
|
110
111
|
readonly type: "PaymentRequiredError";
|
|
111
112
|
}
|
|
112
113
|
export declare const initialSyncState: SyncStateInitial;
|
|
113
|
-
/**
|
|
114
|
-
* Efficiently checks which binary timestamps already exist in the database
|
|
115
|
-
* using a single CTE query instead of N individual queries. Crucial for WASM
|
|
116
|
-
* SQLite performance where JS↔WASM boundary crossings are expensive.
|
|
117
|
-
*
|
|
118
|
-
* Used for fast idempotency detection in writeMessages before onMessage
|
|
119
|
-
* validation. While applyMessages ensures internal idempotency, this pre-check
|
|
120
|
-
* is faster and required for main thread message validation.
|
|
121
|
-
*/
|
|
122
|
-
export declare const getExistingTimestamps: (deps: SqliteDep) => (ownerIdBytes: OwnerIdBytes, timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
|
|
123
114
|
//# sourceMappingURL=Sync.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sync.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,qBAAqB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"Sync.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Sync.ts"],"names":[],"mappings":"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"}
|
package/dist/src/Evolu/Sync.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { assert } from "../Assert.js";
|
|
2
|
-
import { concatBytes } from "../Buffer.js";
|
|
3
2
|
import { eqArrayNumber } from "../Eq.js";
|
|
4
3
|
import { createTransferableError } from "../Error.js";
|
|
5
4
|
import { constFalse } from "../Function.js";
|
|
@@ -351,7 +350,7 @@ export const applyLocalOnlyChange = (deps) => (change) => {
|
|
|
351
350
|
}
|
|
352
351
|
return ok();
|
|
353
352
|
};
|
|
354
|
-
const applyMessages = (deps) => (ownerId, messages) => {
|
|
353
|
+
export const applyMessages = (deps) => (ownerId, messages) => {
|
|
355
354
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
356
355
|
for (const message of messages) {
|
|
357
356
|
const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message);
|
|
@@ -369,30 +368,26 @@ const applyMessageToAppTable = (deps) => (ownerId, message) => {
|
|
|
369
368
|
for (const [column, value] of objectToEntries(message.change.values)) {
|
|
370
369
|
const result = deps.sqlite.exec(sql.prepared `
|
|
371
370
|
with
|
|
372
|
-
|
|
373
|
-
select
|
|
371
|
+
existingTimestamp as (
|
|
372
|
+
select 1
|
|
374
373
|
from evolu_history
|
|
375
374
|
where
|
|
376
375
|
"ownerId" = ${ownerId}
|
|
377
376
|
and "table" = ${message.change.table}
|
|
378
|
-
and "id" = ${message.change.id}
|
|
377
|
+
and "id" = ${idToIdBytes(message.change.id)}
|
|
379
378
|
and "column" = ${column}
|
|
380
|
-
|
|
379
|
+
and "timestamp" >= ${timestamp}
|
|
381
380
|
limit 1
|
|
382
381
|
)
|
|
383
382
|
insert into ${sql.identifier(message.change.table)}
|
|
384
383
|
("id", ${sql.identifier(column)}, updatedAt)
|
|
385
384
|
select ${message.change.id}, ${value}, ${updatedAt}
|
|
386
|
-
where
|
|
387
|
-
(select "timestamp" from lastTimestamp) is null
|
|
388
|
-
or (select "timestamp" from lastTimestamp) < ${timestamp}
|
|
385
|
+
where not exists (select 1 from existingTimestamp)
|
|
389
386
|
on conflict ("id") do update
|
|
390
387
|
set
|
|
391
388
|
${sql.identifier(column)} = ${value},
|
|
392
389
|
updatedAt = ${updatedAt}
|
|
393
|
-
where
|
|
394
|
-
(select "timestamp" from lastTimestamp) is null
|
|
395
|
-
or (select "timestamp" from lastTimestamp) < ${timestamp};
|
|
390
|
+
where not exists (select 1 from existingTimestamp);
|
|
396
391
|
`);
|
|
397
392
|
if (!result.ok)
|
|
398
393
|
return result;
|
|
@@ -426,37 +421,3 @@ export const applyMessageToTimestampAndHistoryTables = (deps) => (ownerId, messa
|
|
|
426
421
|
return ok();
|
|
427
422
|
};
|
|
428
423
|
export const initialSyncState = { type: "SyncStateInitial" };
|
|
429
|
-
/**
|
|
430
|
-
* Efficiently checks which binary timestamps already exist in the database
|
|
431
|
-
* using a single CTE query instead of N individual queries. Crucial for WASM
|
|
432
|
-
* SQLite performance where JS↔WASM boundary crossings are expensive.
|
|
433
|
-
*
|
|
434
|
-
* Used for fast idempotency detection in writeMessages before onMessage
|
|
435
|
-
* validation. While applyMessages ensures internal idempotency, this pre-check
|
|
436
|
-
* is faster and required for main thread message validation.
|
|
437
|
-
*/
|
|
438
|
-
export const getExistingTimestamps = (deps) => (ownerIdBytes, timestampsBytes) => {
|
|
439
|
-
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
440
|
-
const result = deps.sqlite.exec(sql `
|
|
441
|
-
with recursive
|
|
442
|
-
split_timestamps(timestampBytes, pos) as (
|
|
443
|
-
select
|
|
444
|
-
substr(${concatenatedTimestamps}, 1, 16),
|
|
445
|
-
17 as pos
|
|
446
|
-
union all
|
|
447
|
-
select
|
|
448
|
-
substr(${concatenatedTimestamps}, pos, 16),
|
|
449
|
-
pos + 16
|
|
450
|
-
from split_timestamps
|
|
451
|
-
where pos <= length(${concatenatedTimestamps})
|
|
452
|
-
)
|
|
453
|
-
select s.timestampBytes
|
|
454
|
-
from
|
|
455
|
-
split_timestamps s
|
|
456
|
-
join evolu_timestamp t
|
|
457
|
-
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
458
|
-
`);
|
|
459
|
-
if (!result.ok)
|
|
460
|
-
return result;
|
|
461
|
-
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
462
|
-
};
|
package/dist/src/Result.d.ts
CHANGED
|
@@ -228,23 +228,26 @@
|
|
|
228
228
|
*
|
|
229
229
|
* ### FAQ
|
|
230
230
|
*
|
|
231
|
-
* ####
|
|
231
|
+
* #### When should a function return a plain value instead of `Result<T, E>`?
|
|
232
232
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
233
|
+
* Use `Result<T, E>` only when a function can fail with **known, expected
|
|
234
|
+
* errors** that callers need to handle. If a function cannot fail with a known
|
|
235
|
+
* error, return the value directly.
|
|
236
|
+
*
|
|
237
|
+
* - ✅ Return `Result<User, UserNotFoundError>` - can fail with a known error
|
|
238
|
+
* - ✅ Return `User` - cannot fail with a known error
|
|
239
|
+
* - ❌ Don't return `Result<User, never>` - unnecessary wrapper
|
|
237
240
|
*
|
|
238
|
-
*
|
|
241
|
+
* This keeps the codebase clean and makes error handling intentional. The type
|
|
242
|
+
* system communicates which operations can fail and which cannot.
|
|
239
243
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* without error handling.
|
|
244
|
+
* Unsafe code from external libraries (not under our control) should be wrapped
|
|
245
|
+
* with `trySync` or `tryAsync` at the boundaries. Once wrapped, if the error is
|
|
246
|
+
* not important to callers, functions can safely return plain values. If the
|
|
247
|
+
* error matters, use `Result` with a typed error.
|
|
245
248
|
*
|
|
246
249
|
* ```ts
|
|
247
|
-
* // ✅ Safe to return void -
|
|
250
|
+
* // ✅ Safe to return void - unsafe code is wrapped and error is handled
|
|
248
251
|
* const processData = (data: string): void => {
|
|
249
252
|
* const parseResult = trySync(
|
|
250
253
|
* () => JSON.parse(data),
|
|
@@ -252,7 +255,7 @@
|
|
|
252
255
|
* );
|
|
253
256
|
*
|
|
254
257
|
* if (!parseResult.ok) {
|
|
255
|
-
* logError(parseResult.error);
|
|
258
|
+
* logError(parseResult.error);
|
|
256
259
|
* return;
|
|
257
260
|
* }
|
|
258
261
|
*
|
|
@@ -263,9 +266,12 @@
|
|
|
263
266
|
* processData(jsonString);
|
|
264
267
|
* ```
|
|
265
268
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
+
* #### What if my function doesn't return a value on success?
|
|
270
|
+
*
|
|
271
|
+
* If your function performs an operation but doesn't need to return a value on
|
|
272
|
+
* success, you can use `Result<void, E>`. Using `Result<void, E>` is clearer
|
|
273
|
+
* than using `Result<true, E>` or `Result<null, E>` because it communicates
|
|
274
|
+
* that the function doesn't produce a value but can produce errors.
|
|
269
275
|
*
|
|
270
276
|
* #### How do I short-circuit processing of an array on the first error?
|
|
271
277
|
*
|
|
@@ -441,6 +447,29 @@ export declare const err: <E>(error: E) => Err<E>;
|
|
|
441
447
|
* Throws: `Error` with the original error attached as `cause`.
|
|
442
448
|
*/
|
|
443
449
|
export declare const getOrThrow: <T, E>(result: Result<T, E>) => T;
|
|
450
|
+
/**
|
|
451
|
+
* Extracts the value from a {@link Result} if it is an `Ok`, or returns `null`
|
|
452
|
+
* if it is an `Err`.
|
|
453
|
+
*
|
|
454
|
+
* **Intended usage:**
|
|
455
|
+
*
|
|
456
|
+
* - When you need to convert a `Result` to a nullable value for APIs that expect
|
|
457
|
+
* `T | null`.
|
|
458
|
+
* - When the error is not important and you just want the value or nothing.
|
|
459
|
+
*
|
|
460
|
+
* ### Example
|
|
461
|
+
*
|
|
462
|
+
* ```ts
|
|
463
|
+
* const parseResult = parseJson('{"key": "value"}');
|
|
464
|
+
* const value = getOrNull(parseResult);
|
|
465
|
+
* // value is unknown | null
|
|
466
|
+
*
|
|
467
|
+
* if (value != null) {
|
|
468
|
+
* console.log("Parsed value:", value);
|
|
469
|
+
* }
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
export declare const getOrNull: <T, E>(result: Result<T, E>) => T | null;
|
|
444
473
|
/**
|
|
445
474
|
* Wraps synchronous functions that may throw exceptions, returning a
|
|
446
475
|
* {@link Result}.
|
package/dist/src/Result.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Result.d.ts","sourceRoot":"","sources":["../../src/Result.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Result.d.ts","sourceRoot":"","sources":["../../src/Result.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyUG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAE1C,mCAAmC;AACnC,MAAM,WAAW,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAC5C,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAC7C,CAAC,SAAS,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAErC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;AAC/B,2DAA2D;AAC3D,wBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAKvC;;;;;;;;;GASG;AACH,eAAO,MAAM,GAAG,GAAI,CAAC,EAAE,OAAO,CAAC,KAAG,GAAG,CAAC,CAAC,CAA2B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,CAMvD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,CAAC,GAAG,IAC1B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,EAC1B,IAAI,MAAM,CAAC,EACX,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC9B,MAAM,CAAC,CAAC,EAAE,CAAC,CAMb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,QAAQ,GAAU,CAAC,EAAE,CAAC,EACjC,WAAW,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC9B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAIpB,CAAC"}
|