@evolu/common 6.0.1-preview.28 → 6.0.1-preview.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Array.d.ts +58 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +53 -5
- package/dist/src/Evolu/Evolu.d.ts +3 -3
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +3 -3
- package/dist/src/Evolu/Owner.d.ts +48 -19
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +11 -2
- package/dist/src/Evolu/Protocol.d.ts +31 -31
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +51 -28
- package/dist/src/Evolu/Relay.d.ts +40 -25
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +106 -49
- package/dist/src/Evolu/Storage.d.ts +59 -12
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +77 -50
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +14 -5
- package/dist/src/Evolu/Timestamp.d.ts +25 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +25 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/{Multiton.js → Instances.js} +20 -9
- package/dist/src/Sqlite.d.ts +6 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +6 -0
- package/dist/src/Task.d.ts +75 -0
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +29 -6
- package/dist/src/Time.d.ts +7 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +13 -2
- package/dist/src/Type.d.ts +56 -9
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +40 -8
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
- package/src/Array.ts +76 -11
- package/src/Evolu/Evolu.ts +4 -5
- package/src/Evolu/Owner.ts +75 -26
- package/src/Evolu/Protocol.ts +90 -61
- package/src/Evolu/Relay.ts +182 -77
- package/src/Evolu/Storage.ts +157 -67
- package/src/Evolu/Sync.ts +18 -6
- package/src/Evolu/Timestamp.ts +25 -0
- package/src/Instances.ts +90 -0
- package/src/Sqlite.ts +6 -0
- package/src/Task.ts +88 -7
- package/src/Time.ts +13 -2
- package/src/Type.ts +56 -9
- package/src/index.ts +1 -1
- package/dist/src/Multiton.d.ts +0 -50
- package/dist/src/Multiton.d.ts.map +0 -1
- package/src/Multiton.ts +0 -98
package/src/Evolu/Owner.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Entropy32,
|
|
9
9
|
RandomBytesDep,
|
|
10
10
|
} from "../Crypto.js";
|
|
11
|
+
import { getOrNull } from "../Result.js";
|
|
11
12
|
import {
|
|
12
13
|
brand,
|
|
13
14
|
Id,
|
|
@@ -16,12 +17,8 @@ import {
|
|
|
16
17
|
idToIdBytes,
|
|
17
18
|
Mnemonic,
|
|
18
19
|
NonNegativeInt,
|
|
19
|
-
PositiveInt,
|
|
20
20
|
} from "../Type.js";
|
|
21
|
-
import {
|
|
22
|
-
import type { Timestamp } from "./Timestamp.js";
|
|
23
|
-
import { TimestampBytes } from "./Timestamp.js";
|
|
24
|
-
import type { Storage } from "./Storage.js";
|
|
21
|
+
import type { Storage, EncryptedDbChange } from "./Storage.js";
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
24
|
* The Owner represents ownership of data in Evolu. Every database change is
|
|
@@ -159,6 +156,17 @@ export const createOwner = (secret: OwnerSecret): Owner => ({
|
|
|
159
156
|
* preserved because it coordinates deletion information across devices. Other
|
|
160
157
|
* devices need to sync the information that an owner was deleted so they can
|
|
161
158
|
* delete their local data as well.
|
|
159
|
+
*
|
|
160
|
+
* ### Privacy Considerations
|
|
161
|
+
*
|
|
162
|
+
* AppOwner must never be shared with anyone, except for its {@link OwnerId},
|
|
163
|
+
* which can be used for authorization with
|
|
164
|
+
* {@link createOwnerWebSocketTransport}. It's safe because OwnerId is
|
|
165
|
+
* pseudonymous (it can't be assigned to a specific person).
|
|
166
|
+
*
|
|
167
|
+
* For data sharing scenarios, use {@link SharedOwner} and
|
|
168
|
+
* {@link SharedReadonlyOwner} instead, which are designed specifically for
|
|
169
|
+
* collaborative access.
|
|
162
170
|
*/
|
|
163
171
|
export interface AppOwner extends Owner {
|
|
164
172
|
readonly type: "AppOwner";
|
|
@@ -294,7 +302,17 @@ export type OwnerTransport = OwnerWebSocketTransport;
|
|
|
294
302
|
/**
|
|
295
303
|
* WebSocket transport configuration.
|
|
296
304
|
*
|
|
297
|
-
* ### Authentication
|
|
305
|
+
* ### Authentication via URL
|
|
306
|
+
*
|
|
307
|
+
* The {@link OwnerId} is passed as a URL query parameter. While this approach is
|
|
308
|
+
* generally discouraged for authentication tokens (they get logged), it's safe
|
|
309
|
+
* here because OwnerId is pseudonymous and used only for access verification -
|
|
310
|
+
* it provides no ability to read encrypted data or write changes.
|
|
311
|
+
*
|
|
312
|
+
* See: [HTTP headers in Websockets client
|
|
313
|
+
* API](https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api/74564827#74564827)
|
|
314
|
+
*
|
|
315
|
+
* ### Error Handling
|
|
298
316
|
*
|
|
299
317
|
* When a relay rejects a connection (invalid OwnerId, unauthorized owner, or
|
|
300
318
|
* server error), the browser WebSocket API does not expose the specific HTTP
|
|
@@ -317,14 +335,23 @@ export interface OwnerWebSocketTransport {
|
|
|
317
335
|
* Creates an {@link OwnerWebSocketTransport} for the given relay URL and
|
|
318
336
|
* {@link OwnerId}.
|
|
319
337
|
*
|
|
338
|
+
* The URL must be a WebSocket base URL without query parameters or fragments
|
|
339
|
+
* (e.g., `wss://relay.evolu.dev`, not `wss://relay.evolu.dev?foo=bar`). The
|
|
340
|
+
* function appends the `ownerId` as a query parameter.
|
|
341
|
+
*
|
|
320
342
|
* ### Example
|
|
321
343
|
*
|
|
322
344
|
* ```ts
|
|
345
|
+
* // Create transport "wss://relay.evolu.dev?ownerId=..."
|
|
323
346
|
* const transport = createOwnerWebSocketTransport({
|
|
324
347
|
* url: "wss://relay.evolu.dev",
|
|
325
348
|
* ownerId: owner.id,
|
|
326
349
|
* });
|
|
327
|
-
*
|
|
350
|
+
*
|
|
351
|
+
* // Use with createEvolu
|
|
352
|
+
* const evolu = createEvolu(deps)(Schema, {
|
|
353
|
+
* transports: [transport],
|
|
354
|
+
* });
|
|
328
355
|
* ```
|
|
329
356
|
*/
|
|
330
357
|
export const createOwnerWebSocketTransport = (config: {
|
|
@@ -355,6 +382,11 @@ export const parseOwnerIdFromOwnerWebSocketTransportUrl = (
|
|
|
355
382
|
url: string,
|
|
356
383
|
): OwnerId | null => getOrNull(OwnerId.fromUnknown(url.split("=")[1]));
|
|
357
384
|
|
|
385
|
+
/** Base interface for all owner errors. */
|
|
386
|
+
export interface BaseOwnerError {
|
|
387
|
+
readonly ownerId: OwnerId;
|
|
388
|
+
}
|
|
389
|
+
|
|
358
390
|
/**
|
|
359
391
|
* Usage data for an {@link OwnerId}.
|
|
360
392
|
*
|
|
@@ -368,27 +400,44 @@ export interface OwnerUsage {
|
|
|
368
400
|
/** The {@link Owner} this usage data belongs to. */
|
|
369
401
|
readonly ownerId: OwnerIdBytes;
|
|
370
402
|
|
|
371
|
-
/** Total bytes stored in the database. */
|
|
372
|
-
readonly storedBytes: PositiveInt;
|
|
373
|
-
|
|
374
|
-
/** Total bytes received. */
|
|
375
|
-
readonly receivedBytes: number;
|
|
376
|
-
|
|
377
|
-
/** Total bytes sent. */
|
|
378
|
-
readonly sentBytes: number;
|
|
379
|
-
|
|
380
403
|
/**
|
|
381
|
-
*
|
|
404
|
+
* Total logical data bytes stored.
|
|
382
405
|
*
|
|
383
|
-
*
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* The maximum {@link Timestamp}.
|
|
406
|
+
* Measures the size of {@link EncryptedDbChange}s only, excluding
|
|
407
|
+
* {@link Storage} implementation overhead (with SqliteStorage: indexes,
|
|
408
|
+
* skiplist columns, etc.). This provides:
|
|
389
409
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
410
|
+
* - **Predictable measurement** - same data = same byte count across all
|
|
411
|
+
* instances
|
|
412
|
+
* - **Quota enforcement** - consistent billing/limits independent of storage
|
|
413
|
+
* implementation
|
|
414
|
+
* - **Overhead tracking** - actual Storage size can be compared against this to
|
|
415
|
+
* monitor efficiency
|
|
392
416
|
*/
|
|
393
|
-
readonly
|
|
417
|
+
readonly storedBytes: NonNegativeInt;
|
|
418
|
+
|
|
419
|
+
// TODO: Decide how to use receivedBytes and sentBytes.
|
|
420
|
+
// /** Total bytes received. */
|
|
421
|
+
// readonly receivedBytes: NonNegativeInt;
|
|
422
|
+
|
|
423
|
+
// TODO: Decide how to use sentBytes.
|
|
424
|
+
// /** Total bytes sent. */
|
|
425
|
+
// readonly sentBytes: NonNegativeInt;
|
|
426
|
+
|
|
427
|
+
// TODO: Decide how to use firstTimestamp.
|
|
428
|
+
// /**
|
|
429
|
+
// * The minimum {@link Timestamp}.
|
|
430
|
+
// *
|
|
431
|
+
// * Helps {@link Storage} choose faster algorithms.
|
|
432
|
+
// */
|
|
433
|
+
// readonly firstTimestamp: TimestampBytes | null;
|
|
434
|
+
|
|
435
|
+
// TODO: Decide how to use lastTimestamp.
|
|
436
|
+
// /**
|
|
437
|
+
// * The maximum {@link Timestamp}.
|
|
438
|
+
// *
|
|
439
|
+
// * Helps {@link Storage} choose faster algorithms. Free relays can use it to
|
|
440
|
+
// * identify inactive accounts for cleanup or archival.
|
|
441
|
+
// */
|
|
442
|
+
// readonly lastTimestamp: TimestampBytes | null;
|
|
394
443
|
}
|
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -85,10 +85,10 @@
|
|
|
85
85
|
*
|
|
86
86
|
* - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
|
|
87
87
|
* - {@link ProtocolWriteError}: A serious relay-side write failure occurred.
|
|
88
|
+
* - {@link ProtocolQuotaError}: Storage or billing quota exceeded.
|
|
88
89
|
* - {@link ProtocolSyncError}: A serious relay-side synchronization failure
|
|
89
90
|
* occurred.
|
|
90
|
-
* - {@link
|
|
91
|
-
* - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
|
|
91
|
+
* - {@link ProtocolVersionError}: Protocol version mismatch.
|
|
92
92
|
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
93
93
|
*
|
|
94
94
|
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
@@ -160,6 +160,19 @@
|
|
|
160
160
|
* @module
|
|
161
161
|
*/
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* TODO:
|
|
165
|
+
*
|
|
166
|
+
* - The client-relay naming convention in functions like
|
|
167
|
+
* `applyProtocolMessageAsClient` and `applyProtocolMessageAsRelay` is not
|
|
168
|
+
* ideal. In the future, clients will be able to sync directly with each other
|
|
169
|
+
* (P2P), making the current naming misleading. Consider using
|
|
170
|
+
* initiator/non-initiator terminology instead, and consolidate into a single
|
|
171
|
+
* `applyProtocolMessage` function with conditional arguments to reduce code
|
|
172
|
+
* duplication.
|
|
173
|
+
* - ProtocolQuotaError should return storedBytes and actual quota.
|
|
174
|
+
*/
|
|
175
|
+
|
|
163
176
|
import { Packr } from "msgpackr";
|
|
164
177
|
import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
165
178
|
import { assert } from "../Assert.js";
|
|
@@ -204,6 +217,7 @@ import {
|
|
|
204
217
|
} from "../Type.js";
|
|
205
218
|
import { Predicate } from "../Types.js";
|
|
206
219
|
import {
|
|
220
|
+
BaseOwnerError,
|
|
207
221
|
Owner,
|
|
208
222
|
OwnerId,
|
|
209
223
|
OwnerIdBytes,
|
|
@@ -342,36 +356,31 @@ export const ProtocolErrorCode = {
|
|
|
342
356
|
WriteKeyError: 1,
|
|
343
357
|
/** A code for {@link ProtocolWriteError}. */
|
|
344
358
|
WriteError: 2,
|
|
359
|
+
/** A code for {@link ProtocolQuotaError}. */
|
|
360
|
+
QuotaError: 3,
|
|
345
361
|
/** A code for {@link ProtocolSyncError}. */
|
|
346
|
-
SyncError:
|
|
347
|
-
/** A code for {@link ProtocolQuotaExceededError}. */
|
|
348
|
-
QuotaExceededError: 4,
|
|
362
|
+
SyncError: 4,
|
|
349
363
|
} as const;
|
|
350
364
|
|
|
351
365
|
type ProtocolErrorCode =
|
|
352
366
|
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
353
367
|
|
|
354
368
|
export type ProtocolError =
|
|
355
|
-
|
|
|
369
|
+
| ProtocolVersionError
|
|
356
370
|
| ProtocolInvalidDataError
|
|
357
371
|
| ProtocolWriteKeyError
|
|
358
372
|
| ProtocolWriteError
|
|
359
373
|
| ProtocolSyncError
|
|
360
|
-
|
|
|
374
|
+
| ProtocolQuotaError
|
|
361
375
|
| ProtocolTimestampMismatchError;
|
|
362
376
|
|
|
363
|
-
/** Base interface for all protocol errors. */
|
|
364
|
-
export interface ProtocolErrorBase {
|
|
365
|
-
readonly ownerId: OwnerId;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
377
|
/**
|
|
369
378
|
* Represents a version mismatch in the Evolu Protocol. Occurs when the
|
|
370
379
|
* initiator and non-initiator are using incompatible protocol versions.
|
|
371
380
|
*/
|
|
372
|
-
export interface
|
|
373
|
-
readonly type: "
|
|
374
|
-
readonly
|
|
381
|
+
export interface ProtocolVersionError extends BaseOwnerError {
|
|
382
|
+
readonly type: "ProtocolVersionError";
|
|
383
|
+
readonly version: NonNegativeInt;
|
|
375
384
|
/** Indicates which side is obsolete and should update. */
|
|
376
385
|
readonly isInitiator: boolean;
|
|
377
386
|
}
|
|
@@ -384,7 +393,7 @@ export interface ProtocolInvalidDataError {
|
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
/** Error when a {@link OwnerWriteKey} is invalid, missing, or fails validation. */
|
|
387
|
-
export interface ProtocolWriteKeyError extends
|
|
396
|
+
export interface ProtocolWriteKeyError extends BaseOwnerError {
|
|
388
397
|
readonly type: "ProtocolWriteKeyError";
|
|
389
398
|
}
|
|
390
399
|
|
|
@@ -392,27 +401,31 @@ export interface ProtocolWriteKeyError extends ProtocolErrorBase {
|
|
|
392
401
|
* Error indicating a serious relay-side write failure. Clients should log this
|
|
393
402
|
* error and show a generic sync error to the user.
|
|
394
403
|
*/
|
|
395
|
-
export interface ProtocolWriteError extends
|
|
404
|
+
export interface ProtocolWriteError extends BaseOwnerError {
|
|
396
405
|
readonly type: "ProtocolWriteError";
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
/**
|
|
400
|
-
* Error
|
|
401
|
-
*
|
|
409
|
+
* Error when storage or billing quota is exceeded.
|
|
410
|
+
*
|
|
411
|
+
* When relay rejects writes due to quota, the affected device stops syncing
|
|
412
|
+
* because RBSR requires both sides to converge—if the relay won't accept the
|
|
413
|
+
* client's data, they can never reach the same state. Only the device with
|
|
414
|
+
* excess local data is affected. Other devices that haven't exceeded quota can
|
|
415
|
+
* still sync normally.
|
|
416
|
+
*
|
|
417
|
+
* Clients should prompt the user to upgrade their plan.
|
|
402
418
|
*/
|
|
403
|
-
export interface
|
|
404
|
-
readonly type: "
|
|
419
|
+
export interface ProtocolQuotaError extends BaseOwnerError {
|
|
420
|
+
readonly type: "ProtocolQuotaError";
|
|
405
421
|
}
|
|
406
422
|
|
|
407
423
|
/**
|
|
408
|
-
* Error
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* TODO: Add callback to relay config to check quota and return this error when
|
|
412
|
-
* limits are reached.
|
|
424
|
+
* Error indicating a serious relay-side synchronization failure. Clients should
|
|
425
|
+
* log this error and show a generic sync error to the user.
|
|
413
426
|
*/
|
|
414
|
-
export interface
|
|
415
|
-
readonly type: "
|
|
427
|
+
export interface ProtocolSyncError extends BaseOwnerError {
|
|
428
|
+
readonly type: "ProtocolSyncError";
|
|
416
429
|
}
|
|
417
430
|
|
|
418
431
|
/**
|
|
@@ -862,10 +875,10 @@ const createRunLengthEncoder = <T>(
|
|
|
862
875
|
export interface ApplyProtocolMessageAsClientOptions {
|
|
863
876
|
getWriteKey?: (ownerId: OwnerId) => OwnerWriteKey | null;
|
|
864
877
|
|
|
865
|
-
/** For testing purposes only; should not be used in production. */
|
|
866
|
-
version?: NonNegativeInt;
|
|
867
|
-
|
|
868
878
|
rangesMaxSize?: ProtocolMessageRangesMaxSize;
|
|
879
|
+
|
|
880
|
+
/** For tests only. */
|
|
881
|
+
version?: NonNegativeInt;
|
|
869
882
|
}
|
|
870
883
|
|
|
871
884
|
/**
|
|
@@ -887,22 +900,23 @@ export const applyProtocolMessageAsClient =
|
|
|
887
900
|
ApplyProtocolMessageAsClientResult,
|
|
888
901
|
| ProtocolInvalidDataError
|
|
889
902
|
| ProtocolSyncError
|
|
890
|
-
|
|
|
903
|
+
| ProtocolVersionError
|
|
891
904
|
| ProtocolWriteError
|
|
892
905
|
| ProtocolWriteKeyError
|
|
893
|
-
|
|
|
906
|
+
| ProtocolQuotaError
|
|
894
907
|
>
|
|
895
908
|
> => {
|
|
896
909
|
// try-catch instead of Result for performance and stacktraces
|
|
910
|
+
// DEV: Measure it again, I think we should use Result with new Error.
|
|
897
911
|
try {
|
|
898
912
|
const input = createBuffer(inputMessage);
|
|
899
913
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
900
914
|
const version = options.version ?? protocolVersion;
|
|
901
915
|
|
|
902
916
|
if (requestedVersion !== version) {
|
|
903
|
-
return err<
|
|
904
|
-
type: "
|
|
905
|
-
|
|
917
|
+
return err<ProtocolVersionError>({
|
|
918
|
+
type: "ProtocolVersionError",
|
|
919
|
+
version: requestedVersion,
|
|
906
920
|
isInitiator: version < requestedVersion,
|
|
907
921
|
ownerId,
|
|
908
922
|
});
|
|
@@ -929,16 +943,16 @@ export const applyProtocolMessageAsClient =
|
|
|
929
943
|
type: "ProtocolWriteError",
|
|
930
944
|
ownerId,
|
|
931
945
|
});
|
|
946
|
+
case ProtocolErrorCode.QuotaError:
|
|
947
|
+
return err<ProtocolQuotaError>({
|
|
948
|
+
type: "ProtocolQuotaError",
|
|
949
|
+
ownerId,
|
|
950
|
+
});
|
|
932
951
|
case ProtocolErrorCode.SyncError:
|
|
933
952
|
return err<ProtocolSyncError>({
|
|
934
953
|
type: "ProtocolSyncError",
|
|
935
954
|
ownerId,
|
|
936
955
|
});
|
|
937
|
-
case ProtocolErrorCode.QuotaExceededError:
|
|
938
|
-
return err<ProtocolQuotaExceededError>({
|
|
939
|
-
type: "ProtocolQuotaExceededError",
|
|
940
|
-
ownerId,
|
|
941
|
-
});
|
|
942
956
|
default:
|
|
943
957
|
throw new ProtocolDecodeError(
|
|
944
958
|
`Invalid ProtocolErrorCode: ${errorCode}`,
|
|
@@ -950,11 +964,13 @@ export const applyProtocolMessageAsClient =
|
|
|
950
964
|
const messages = decodeMessages(input);
|
|
951
965
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
952
966
|
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
967
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
968
|
+
const writeResult = await deps.storage.writeMessages(
|
|
969
|
+
ownerIdBytes,
|
|
970
|
+
messages,
|
|
971
|
+
);
|
|
972
|
+
// Errors are handled by the Storage. Here we just stop syncing.
|
|
973
|
+
if (!writeResult.ok) return ok({ type: "no-response" });
|
|
958
974
|
}
|
|
959
975
|
|
|
960
976
|
// Now: No writeKey, no sync.
|
|
@@ -1033,12 +1049,13 @@ export const applyProtocolMessageAsRelay =
|
|
|
1033
1049
|
async (
|
|
1034
1050
|
inputMessage: Uint8Array,
|
|
1035
1051
|
options: ApplyProtocolMessageAsRelayOptions = {},
|
|
1036
|
-
/** For
|
|
1052
|
+
/** For tests only. */
|
|
1037
1053
|
version = protocolVersion,
|
|
1038
1054
|
): Promise<
|
|
1039
1055
|
Result<ApplyProtocolMessageAsRelayResult, ProtocolInvalidDataError>
|
|
1040
1056
|
> => {
|
|
1041
1057
|
// try-catch instead of Result for performance and stacktraces
|
|
1058
|
+
// DEV: Measure it again, I think we should use Result with new Error.
|
|
1042
1059
|
try {
|
|
1043
1060
|
const input = createBuffer(inputMessage);
|
|
1044
1061
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
@@ -1104,17 +1121,39 @@ export const applyProtocolMessageAsRelay =
|
|
|
1104
1121
|
});
|
|
1105
1122
|
}
|
|
1106
1123
|
|
|
1124
|
+
const writeResult = await deps.storage.writeMessages(
|
|
1125
|
+
ownerIdBytes,
|
|
1126
|
+
messages,
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
if (!writeResult.ok) {
|
|
1130
|
+
const errorCode =
|
|
1131
|
+
writeResult.error.type === "StorageWriteError"
|
|
1132
|
+
? ProtocolErrorCode.WriteError
|
|
1133
|
+
: ProtocolErrorCode.QuotaError;
|
|
1134
|
+
const message = createProtocolMessageBuffer(ownerId, {
|
|
1135
|
+
messageType: MessageType.Response,
|
|
1136
|
+
errorCode,
|
|
1137
|
+
}).unwrap();
|
|
1138
|
+
return ok({ type: "response", message });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1107
1141
|
/**
|
|
1108
|
-
* Broadcast messages to all subscribed
|
|
1142
|
+
* Broadcast messages to all subscribed owners for real-time
|
|
1109
1143
|
* synchronization between clients.
|
|
1110
1144
|
*
|
|
1145
|
+
* Messages are only broadcasted after successful write to ensure
|
|
1146
|
+
* devices that can still sync aren't affected by quota errors, and to
|
|
1147
|
+
* prevent using a half-working relay service (broadcasting without
|
|
1148
|
+
* persistence).
|
|
1149
|
+
*
|
|
1111
1150
|
* When a relay's database is deleted or clients migrate to a new relay
|
|
1112
1151
|
* (without data migration), clients will sync their data to the relay,
|
|
1113
1152
|
* and the relay will broadcast those messages to other connected
|
|
1114
1153
|
* clients. Those clients may receive messages they already have, but
|
|
1115
|
-
* this is safe because
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1154
|
+
* this is safe because Evolu sync is idempotent. As the relay becomes
|
|
1155
|
+
* more synchronized with clients over time, fewer duplicate messages
|
|
1156
|
+
* will be broadcasted.
|
|
1118
1157
|
*/
|
|
1119
1158
|
if (options.broadcast) {
|
|
1120
1159
|
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
@@ -1128,16 +1167,6 @@ export const applyProtocolMessageAsRelay =
|
|
|
1128
1167
|
}
|
|
1129
1168
|
options.broadcast(ownerId, broadcastBuffer.unwrap());
|
|
1130
1169
|
}
|
|
1131
|
-
|
|
1132
|
-
if (!(await deps.storage.writeMessages(ownerIdBytes, messages))) {
|
|
1133
|
-
return ok({
|
|
1134
|
-
type: "response",
|
|
1135
|
-
message: createProtocolMessageBuffer(ownerId, {
|
|
1136
|
-
messageType: MessageType.Response,
|
|
1137
|
-
errorCode: ProtocolErrorCode.WriteError,
|
|
1138
|
-
}).unwrap(),
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
1170
|
}
|
|
1142
1171
|
|
|
1143
1172
|
const ranges = decodeRanges(input);
|