@evolu/common 8.10.0 → 8.12.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/dist/src/Bytes.d.ts +39 -2
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +50 -2
- package/dist/src/Config.d.ts +22 -22
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +92 -18
- package/dist/src/Fs.d.ts.map +1 -1
- package/dist/src/Fs.js +2 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +63 -9
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +106 -9
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +55 -4
- package/dist/src/Type.d.ts +1455 -1310
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1274 -517
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +6 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +412 -213
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +181 -18
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +106 -19
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +162 -60
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +346 -23
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +195 -17
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +85 -22
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +403 -81
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- package/src/Config.test.ts +2 -6
- package/src/Config.ts +133 -133
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.ts +92 -18
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Object.test.ts +27 -12
- package/src/Object.ts +5 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +137 -17
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.ts +106 -9
- package/src/Type.test.ts +946 -1028
- package/src/Type.ts +4195 -3136
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +20 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +994 -22
- package/src/local-first/Evolu.ts +625 -232
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +634 -10
- package/src/local-first/Protocol.ts +255 -109
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +376 -26
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +224 -36
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +434 -118
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- package/src/local-first/LocalAuth.ts +0 -457
|
@@ -103,9 +103,9 @@
|
|
|
103
103
|
* fit within the limit, the protocol automatically continues synchronization in
|
|
104
104
|
* subsequent rounds using range-based reconciliation.
|
|
105
105
|
*
|
|
106
|
-
*
|
|
107
|
-
* message
|
|
108
|
-
*
|
|
106
|
+
* Each mutation is limited to {@link maxMutationSize}, so every change fits one
|
|
107
|
+
* message of {@link defaultProtocolMessageMaxSize} next to the largest ranges
|
|
108
|
+
* section.
|
|
109
109
|
*
|
|
110
110
|
* ## Why Binary?
|
|
111
111
|
*
|
|
@@ -182,6 +182,7 @@ import {
|
|
|
182
182
|
} from "../Array.ts";
|
|
183
183
|
import {
|
|
184
184
|
assert,
|
|
185
|
+
assertNonEmptyArray,
|
|
185
186
|
assertNonNullable,
|
|
186
187
|
assertNotUndefined,
|
|
187
188
|
assertSame,
|
|
@@ -189,6 +190,7 @@ import {
|
|
|
189
190
|
import type { Brand } from "../Brand.ts";
|
|
190
191
|
import {
|
|
191
192
|
type Buffer,
|
|
193
|
+
BufferError,
|
|
192
194
|
createBuffer,
|
|
193
195
|
createRunLengthEncoder,
|
|
194
196
|
decodeFlags,
|
|
@@ -247,6 +249,7 @@ import {
|
|
|
247
249
|
zeroNonNegativeInt,
|
|
248
250
|
} from "../Type.ts";
|
|
249
251
|
import type { Predicate } from "../Types.ts";
|
|
252
|
+
import type { Evolu, maxMutationSize } from "./Evolu.ts";
|
|
250
253
|
import {
|
|
251
254
|
type Owner,
|
|
252
255
|
type OwnerError,
|
|
@@ -271,6 +274,7 @@ import {
|
|
|
271
274
|
type RangeUpperBound,
|
|
272
275
|
type SkipRange,
|
|
273
276
|
type StorageDep,
|
|
277
|
+
type StorageWriteMessagesError,
|
|
274
278
|
type TimestampsRange,
|
|
275
279
|
} from "./Storage.ts";
|
|
276
280
|
import {
|
|
@@ -367,18 +371,27 @@ export const MessageType = {
|
|
|
367
371
|
|
|
368
372
|
export type MessageType = (typeof MessageType)[keyof typeof MessageType];
|
|
369
373
|
|
|
370
|
-
/**
|
|
374
|
+
/**
|
|
375
|
+
* The routing prefix of a {@link ProtocolMessage}.
|
|
376
|
+
*
|
|
377
|
+
* The version and the {@link OwnerId} begin every message of every protocol
|
|
378
|
+
* version. The message type follows them only in messages of this peer's
|
|
379
|
+
* version; the layout after the prefix is unknown for any other version.
|
|
380
|
+
*/
|
|
371
381
|
export interface ProtocolHeader extends Typed<"ProtocolHeader"> {
|
|
372
|
-
readonly version:
|
|
382
|
+
readonly version: NonNegativeInt;
|
|
373
383
|
readonly ownerId: OwnerId;
|
|
374
|
-
|
|
384
|
+
/** Absent when `version` differs from {@link protocolVersion}. */
|
|
385
|
+
readonly messageType?: MessageType;
|
|
375
386
|
}
|
|
376
387
|
|
|
377
388
|
/**
|
|
378
|
-
* Parses the
|
|
389
|
+
* Parses the {@link ProtocolHeader} a transport needs to route a message.
|
|
379
390
|
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
391
|
+
* Parsing succeeds for versions this peer cannot process, because a
|
|
392
|
+
* non-initiator answers a version mismatch with only its version and the owner
|
|
393
|
+
* ID. Rejecting that reply here would drop it before
|
|
394
|
+
* {@link applyProtocolMessageAsClient} can report {@link ProtocolVersionError}.
|
|
382
395
|
*/
|
|
383
396
|
export const parseProtocolHeader = (
|
|
384
397
|
inputMessage: Uint8Array,
|
|
@@ -468,8 +481,9 @@ export interface ProtocolWriteError
|
|
|
468
481
|
* still sync normally.
|
|
469
482
|
*
|
|
470
483
|
* Clients should prompt the user to contact the relay provider or upgrade their
|
|
471
|
-
* plan. Quota monitoring and management is the relay provider's
|
|
472
|
-
*
|
|
484
|
+
* plan. Quota monitoring and management is the relay provider's responsibility.
|
|
485
|
+
* After additional quota is available, call {@link Evolu.requestSync} with the
|
|
486
|
+
* affected owner's ID to retry locally stored changes.
|
|
473
487
|
*/
|
|
474
488
|
export interface ProtocolQuotaError
|
|
475
489
|
extends OwnerError, Typed<"ProtocolQuotaError"> {}
|
|
@@ -558,9 +572,87 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
558
572
|
return buffer.unwrap();
|
|
559
573
|
};
|
|
560
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Creates size-limited broadcast {@link ProtocolMessage}s containing every
|
|
577
|
+
* supplied {@link CrdtMessage}.
|
|
578
|
+
*
|
|
579
|
+
* Broadcasts contain no synchronization ranges or write key. Unlike
|
|
580
|
+
* {@link createProtocolMessageFromCrdtMessages}, this function splits all
|
|
581
|
+
* messages into complete frames rather than relying on later synchronization
|
|
582
|
+
* rounds to deliver messages that do not fit. Each individual message must fit
|
|
583
|
+
* the configured size limit.
|
|
584
|
+
*
|
|
585
|
+
* ### Example
|
|
586
|
+
*
|
|
587
|
+
* ```ts
|
|
588
|
+
* import {
|
|
589
|
+
* assertSame,
|
|
590
|
+
* createId,
|
|
591
|
+
* getOrThrow,
|
|
592
|
+
* testCreateDeps,
|
|
593
|
+
* } from "@evolu/common";
|
|
594
|
+
* import {
|
|
595
|
+
* createProtocolBroadcastMessagesFromCrdtMessages,
|
|
596
|
+
* MessageType,
|
|
597
|
+
* parseProtocolHeader,
|
|
598
|
+
* testAppOwner,
|
|
599
|
+
* testCreateCrdtMessage,
|
|
600
|
+
* } from "@evolu/common/local-first";
|
|
601
|
+
*
|
|
602
|
+
* const deps = testCreateDeps();
|
|
603
|
+
* const broadcasts = createProtocolBroadcastMessagesFromCrdtMessages(deps)(
|
|
604
|
+
* testAppOwner,
|
|
605
|
+
* [testCreateCrdtMessage(createId(deps), 1, "Ada")],
|
|
606
|
+
* );
|
|
607
|
+
* assertSame(broadcasts.length, 1);
|
|
608
|
+
* assertSame(
|
|
609
|
+
* getOrThrow(parseProtocolHeader(broadcasts[0])).messageType,
|
|
610
|
+
* MessageType.Broadcast,
|
|
611
|
+
* );
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
export const createProtocolBroadcastMessagesFromCrdtMessages =
|
|
615
|
+
(deps: RandomBytesDep) =>
|
|
616
|
+
(
|
|
617
|
+
owner: Owner,
|
|
618
|
+
messages: NonEmptyReadonlyArray<CrdtMessage>,
|
|
619
|
+
maxSize: ProtocolMessageMaxSize = defaultProtocolMessageMaxSize,
|
|
620
|
+
): NonEmptyReadonlyArray<ProtocolMessage> => {
|
|
621
|
+
const broadcasts: Array<ProtocolMessage> = [];
|
|
622
|
+
let buffer = createProtocolMessageBuffer(owner.id, {
|
|
623
|
+
messageType: MessageType.Broadcast,
|
|
624
|
+
totalMaxSize: maxSize,
|
|
625
|
+
});
|
|
626
|
+
for (const message of messages) {
|
|
627
|
+
const encryptedMessage = {
|
|
628
|
+
timestamp: message.timestamp,
|
|
629
|
+
change: encodeAndEncryptDbChange(deps)(message, owner.encryptionKey),
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
if (!buffer.canAddMessage(encryptedMessage)) {
|
|
633
|
+
const nextBuffer = createProtocolMessageBuffer(owner.id, {
|
|
634
|
+
messageType: MessageType.Broadcast,
|
|
635
|
+
totalMaxSize: maxSize,
|
|
636
|
+
});
|
|
637
|
+
assert(
|
|
638
|
+
nextBuffer.canAddMessage(encryptedMessage),
|
|
639
|
+
"the message is too big",
|
|
640
|
+
);
|
|
641
|
+
broadcasts.push(buffer.unwrap());
|
|
642
|
+
buffer = nextBuffer;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
buffer.addMessage(encryptedMessage);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
broadcasts.push(buffer.unwrap());
|
|
649
|
+
assertNonEmptyArray(broadcasts);
|
|
650
|
+
return broadcasts;
|
|
651
|
+
};
|
|
652
|
+
|
|
561
653
|
/** Creates a {@link ProtocolMessage} for sync. */
|
|
562
654
|
export const createProtocolMessageForSync =
|
|
563
|
-
(deps: StorageDep
|
|
655
|
+
(deps: StorageDep) =>
|
|
564
656
|
(ownerId: OwnerId, subscriptionFlag?: SubscriptionFlag): ProtocolMessage => {
|
|
565
657
|
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
566
658
|
messageType: MessageType.Request,
|
|
@@ -902,21 +994,53 @@ export interface ApplyProtocolMessageAsClientOptions {
|
|
|
902
994
|
}
|
|
903
995
|
|
|
904
996
|
/**
|
|
905
|
-
* Result
|
|
906
|
-
*
|
|
997
|
+
* Result of {@link applyProtocolMessageAsClient}: a continuation for the
|
|
998
|
+
* non-initiator.
|
|
907
999
|
*/
|
|
908
1000
|
export interface ApplyProtocolMessageAsClientResponse extends Typed<"Response"> {
|
|
909
1001
|
readonly message: ProtocolMessage;
|
|
1002
|
+
/** The uploaded messages, if any, for delivery to other local databases. */
|
|
1003
|
+
readonly broadcast?: ProtocolMessage;
|
|
910
1004
|
}
|
|
911
1005
|
|
|
912
|
-
|
|
1006
|
+
/**
|
|
1007
|
+
* Result of {@link applyProtocolMessageAsClient}: the response needed nothing
|
|
1008
|
+
* more, because its ranges all matched or it carried none.
|
|
1009
|
+
*/
|
|
1010
|
+
export interface ApplyProtocolMessageAsClientConverged extends Typed<"Converged"> {}
|
|
913
1011
|
|
|
1012
|
+
/**
|
|
1013
|
+
* Result of {@link applyProtocolMessageAsClient}: a Broadcast message whose
|
|
1014
|
+
* messages were written.
|
|
1015
|
+
*/
|
|
914
1016
|
export interface ApplyProtocolMessageAsClientBroadcast extends Typed<"Broadcast"> {}
|
|
915
1017
|
|
|
1018
|
+
/**
|
|
1019
|
+
* Result of {@link applyProtocolMessageAsClient}: the messages were written, but
|
|
1020
|
+
* without a write key no ranges are reconciled.
|
|
1021
|
+
*/
|
|
1022
|
+
export interface ApplyProtocolMessageAsClientReadonly extends Typed<"Readonly"> {}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Result of {@link applyProtocolMessageAsClient}: the protocol logged an
|
|
1026
|
+
* exception thrown by calling the storage's `writeMessages` (`Write`) or a
|
|
1027
|
+
* failed range reconciliation (`Sync`) to the console. An exception while the
|
|
1028
|
+
* returned Task runs, as the built-in storages throw, is a defect that aborts
|
|
1029
|
+
* the Run instead. Expected write rejections return the original
|
|
1030
|
+
* {@link StorageWriteMessagesError} through {@link Result}. Reconciliation can
|
|
1031
|
+
* fail after messages have been committed; that failure does not roll back the
|
|
1032
|
+
* write.
|
|
1033
|
+
*/
|
|
1034
|
+
export interface ApplyProtocolMessageAsClientFailed extends Typed<"Failed"> {
|
|
1035
|
+
readonly cause: "Write" | "Sync";
|
|
1036
|
+
}
|
|
1037
|
+
|
|
916
1038
|
export type ApplyProtocolMessageAsClientResult =
|
|
917
1039
|
| ApplyProtocolMessageAsClientResponse
|
|
918
|
-
|
|
|
919
|
-
| ApplyProtocolMessageAsClientBroadcast
|
|
1040
|
+
| ApplyProtocolMessageAsClientConverged
|
|
1041
|
+
| ApplyProtocolMessageAsClientBroadcast
|
|
1042
|
+
| ApplyProtocolMessageAsClientReadonly
|
|
1043
|
+
| ApplyProtocolMessageAsClientFailed;
|
|
920
1044
|
|
|
921
1045
|
export const applyProtocolMessageAsClient =
|
|
922
1046
|
(
|
|
@@ -924,12 +1048,7 @@ export const applyProtocolMessageAsClient =
|
|
|
924
1048
|
options: ApplyProtocolMessageAsClientOptions = {},
|
|
925
1049
|
): Task<
|
|
926
1050
|
ApplyProtocolMessageAsClientResult,
|
|
927
|
-
|
|
|
928
|
-
| ProtocolSyncError
|
|
929
|
-
| ProtocolVersionError
|
|
930
|
-
| ProtocolWriteError
|
|
931
|
-
| ProtocolWriteKeyError
|
|
932
|
-
| ProtocolQuotaError,
|
|
1051
|
+
ProtocolError | StorageWriteMessagesError,
|
|
933
1052
|
StorageDep
|
|
934
1053
|
> =>
|
|
935
1054
|
async (run) => {
|
|
@@ -995,15 +1114,18 @@ export const applyProtocolMessageAsClient =
|
|
|
995
1114
|
const result = await run(
|
|
996
1115
|
storage.writeMessages(ownerIdBytes, messages),
|
|
997
1116
|
);
|
|
998
|
-
|
|
999
|
-
if (!result.ok) return ok({ type: "NoResponse" });
|
|
1117
|
+
if (!result.ok) return result;
|
|
1000
1118
|
} catch (error) {
|
|
1001
1119
|
if (AbortError.is(error)) throw error;
|
|
1002
1120
|
run.deps.console.error(error);
|
|
1003
|
-
return ok({ type: "
|
|
1121
|
+
return ok({ type: "Failed", cause: "Write" });
|
|
1004
1122
|
}
|
|
1005
1123
|
}
|
|
1006
1124
|
|
|
1125
|
+
if (messageType === MessageType.Broadcast) {
|
|
1126
|
+
return ok({ type: "Broadcast" });
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1007
1129
|
// Now: No writeKey, no sync.
|
|
1008
1130
|
// TODO: Allow to sync SharedReadonlyOwner
|
|
1009
1131
|
// Without local changes, writeKey will not be required.
|
|
@@ -1011,17 +1133,13 @@ export const applyProtocolMessageAsClient =
|
|
|
1011
1133
|
// the sync will stop.
|
|
1012
1134
|
const writeKey = options.writeKey;
|
|
1013
1135
|
if (writeKey == null) {
|
|
1014
|
-
return ok({ type: "
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (messageType === MessageType.Broadcast) {
|
|
1018
|
-
return ok({ type: "Broadcast" });
|
|
1136
|
+
return ok({ type: "Readonly" });
|
|
1019
1137
|
}
|
|
1020
1138
|
|
|
1021
1139
|
const ranges = decodeRanges(input);
|
|
1022
1140
|
|
|
1023
1141
|
if (!isNonEmptyArray(ranges)) {
|
|
1024
|
-
return ok({ type: "
|
|
1142
|
+
return ok({ type: "Converged" });
|
|
1025
1143
|
}
|
|
1026
1144
|
|
|
1027
1145
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
@@ -1030,14 +1148,23 @@ export const applyProtocolMessageAsClient =
|
|
|
1030
1148
|
rangesMaxSize: options.rangesMaxSize,
|
|
1031
1149
|
});
|
|
1032
1150
|
|
|
1033
|
-
|
|
1151
|
+
let broadcast: ProtocolMessageBuffer | undefined;
|
|
1152
|
+
const result = sync(run.deps)(ranges, output, ownerIdBytes, (message) => {
|
|
1153
|
+
broadcast ??= createProtocolMessageBuffer(ownerId, {
|
|
1154
|
+
messageType: MessageType.Broadcast,
|
|
1155
|
+
});
|
|
1156
|
+
broadcast.addMessage(message);
|
|
1157
|
+
});
|
|
1034
1158
|
|
|
1035
|
-
//
|
|
1036
|
-
if (!result.ok
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1159
|
+
// A failure was logged by sync.
|
|
1160
|
+
if (!result.ok) return ok({ type: "Failed", cause: "Sync" });
|
|
1161
|
+
if (!result.value) return ok({ type: "Converged" });
|
|
1039
1162
|
|
|
1040
|
-
return ok({
|
|
1163
|
+
return ok({
|
|
1164
|
+
type: "Response",
|
|
1165
|
+
message: output.unwrap(),
|
|
1166
|
+
...(broadcast && { broadcast: broadcast.unwrap() }),
|
|
1167
|
+
});
|
|
1041
1168
|
} catch (error) {
|
|
1042
1169
|
if (AbortError.is(error)) throw error;
|
|
1043
1170
|
return err<ProtocolInvalidDataError>({
|
|
@@ -1159,9 +1286,13 @@ export const applyProtocolMessageAsRelay =
|
|
|
1159
1286
|
);
|
|
1160
1287
|
|
|
1161
1288
|
if (!result.ok) {
|
|
1289
|
+
const isQuotaError = result.error.type === "StorageQuotaError";
|
|
1290
|
+
if (!isQuotaError) run.deps.console.error(result.error);
|
|
1162
1291
|
const message = createProtocolMessageBuffer(ownerId, {
|
|
1163
1292
|
messageType: MessageType.Response,
|
|
1164
|
-
errorCode:
|
|
1293
|
+
errorCode: isQuotaError
|
|
1294
|
+
? ProtocolErrorCode.QuotaError
|
|
1295
|
+
: ProtocolErrorCode.WriteError,
|
|
1165
1296
|
}).unwrap();
|
|
1166
1297
|
return ok({ type: "Response", message });
|
|
1167
1298
|
}
|
|
@@ -1256,7 +1387,7 @@ const parseProtocolHeaderFromBuffer = (input: Buffer): ProtocolHeader => {
|
|
|
1256
1387
|
const [version, ownerId] = decodeVersionAndOwner(input);
|
|
1257
1388
|
|
|
1258
1389
|
if (version !== protocolVersion) {
|
|
1259
|
-
|
|
1390
|
+
return { type: "ProtocolHeader", version, ownerId };
|
|
1260
1391
|
}
|
|
1261
1392
|
|
|
1262
1393
|
const messageTypeValue = input.shift();
|
|
@@ -1276,12 +1407,7 @@ const parseProtocolHeaderFromBuffer = (input: Buffer): ProtocolHeader => {
|
|
|
1276
1407
|
throw new ProtocolDecodeError("Invalid MessageType");
|
|
1277
1408
|
}
|
|
1278
1409
|
|
|
1279
|
-
return {
|
|
1280
|
-
type: "ProtocolHeader",
|
|
1281
|
-
version: 1,
|
|
1282
|
-
ownerId,
|
|
1283
|
-
messageType,
|
|
1284
|
-
};
|
|
1410
|
+
return { type: "ProtocolHeader", version, ownerId, messageType };
|
|
1285
1411
|
};
|
|
1286
1412
|
|
|
1287
1413
|
/**
|
|
@@ -1319,6 +1445,7 @@ const sync =
|
|
|
1319
1445
|
ranges: NonEmptyReadonlyArray<Range>,
|
|
1320
1446
|
output: ProtocolMessageBuffer,
|
|
1321
1447
|
ownerIdBytes: OwnerIdBytes,
|
|
1448
|
+
onMessage?: (message: EncryptedCrdtMessage) => void,
|
|
1322
1449
|
): Result<boolean, typeof ProtocolErrorCode.SyncError> => {
|
|
1323
1450
|
const outputInitialSize = output.getSize();
|
|
1324
1451
|
let storageSize: NonNegativeInt;
|
|
@@ -1427,13 +1554,19 @@ const sync =
|
|
|
1427
1554
|
skipRange(range);
|
|
1428
1555
|
} else if (output.canSplitRange()) {
|
|
1429
1556
|
coalesceSkipsBeforeAdd();
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1557
|
+
try {
|
|
1558
|
+
splitRange(deps)(
|
|
1559
|
+
ownerIdBytes,
|
|
1560
|
+
lower,
|
|
1561
|
+
upper,
|
|
1562
|
+
currentUpperBound,
|
|
1563
|
+
output,
|
|
1564
|
+
);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
if (AbortError.is(error)) throw error;
|
|
1567
|
+
deps.console.error(error);
|
|
1568
|
+
return err(ProtocolErrorCode.SyncError);
|
|
1569
|
+
}
|
|
1437
1570
|
} else {
|
|
1438
1571
|
return addFingerprintForRemainingRange(upper)
|
|
1439
1572
|
? ok(true)
|
|
@@ -1495,7 +1628,10 @@ const sync =
|
|
|
1495
1628
|
}
|
|
1496
1629
|
|
|
1497
1630
|
ourTimestamps.add(timestampBinary);
|
|
1498
|
-
if (message)
|
|
1631
|
+
if (message) {
|
|
1632
|
+
output.addMessage(message);
|
|
1633
|
+
onMessage?.(message);
|
|
1634
|
+
}
|
|
1499
1635
|
return true;
|
|
1500
1636
|
},
|
|
1501
1637
|
);
|
|
@@ -1547,7 +1683,7 @@ const sync =
|
|
|
1547
1683
|
};
|
|
1548
1684
|
|
|
1549
1685
|
const splitRange =
|
|
1550
|
-
(deps: StorageDep
|
|
1686
|
+
(deps: StorageDep) =>
|
|
1551
1687
|
(
|
|
1552
1688
|
ownerId: OwnerIdBytes,
|
|
1553
1689
|
lower: NonNegativeInt,
|
|
@@ -1565,15 +1701,10 @@ const splitRange =
|
|
|
1565
1701
|
timestamps: createTimestampsBuffer(),
|
|
1566
1702
|
};
|
|
1567
1703
|
|
|
1568
|
-
deps.storage.iterate(
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
(timestamp) => {
|
|
1573
|
-
range.timestamps.add(timestampBytesToTimestamp(timestamp));
|
|
1574
|
-
return true;
|
|
1575
|
-
},
|
|
1576
|
-
);
|
|
1704
|
+
deps.storage.iterate(ownerId, lower, upper, (timestamp) => {
|
|
1705
|
+
range.timestamps.add(timestampBytesToTimestamp(timestamp));
|
|
1706
|
+
return true;
|
|
1707
|
+
});
|
|
1577
1708
|
|
|
1578
1709
|
buffer.addRange(range);
|
|
1579
1710
|
return;
|
|
@@ -1588,17 +1719,11 @@ const splitRange =
|
|
|
1588
1719
|
...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
|
|
1589
1720
|
];
|
|
1590
1721
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
upperBound,
|
|
1597
|
-
);
|
|
1598
|
-
} catch (error) {
|
|
1599
|
-
deps.console.error(error);
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1722
|
+
const fingerprintRanges = deps.storage.fingerprintRanges(
|
|
1723
|
+
ownerId,
|
|
1724
|
+
fingerprintRangesBuckets,
|
|
1725
|
+
upperBound,
|
|
1726
|
+
);
|
|
1602
1727
|
|
|
1603
1728
|
const rangesToUse =
|
|
1604
1729
|
lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
|
|
@@ -1731,30 +1856,7 @@ export const encodeAndEncryptDbChange =
|
|
|
1731
1856
|
(message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
|
|
1732
1857
|
const buffer = createBuffer();
|
|
1733
1858
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
// Encode the timestamp to prevent tampering (e.g., a malicious relay
|
|
1737
|
-
// assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
|
|
1738
|
-
buffer.extend(timestampToTimestampBytes(message.timestamp));
|
|
1739
|
-
|
|
1740
|
-
encodeFlags(buffer, [
|
|
1741
|
-
message.change.isInsert,
|
|
1742
|
-
// Encode nullable boolean as two flags: presence + value.
|
|
1743
|
-
message.change.isDelete != null,
|
|
1744
|
-
message.change.isDelete ?? false,
|
|
1745
|
-
]);
|
|
1746
|
-
|
|
1747
|
-
encodeString(buffer, message.change.table);
|
|
1748
|
-
buffer.extend(idToIdBytes(message.change.id));
|
|
1749
|
-
|
|
1750
|
-
const entries = objectToEntries(message.change.values);
|
|
1751
|
-
|
|
1752
|
-
encodeLength(buffer, entries);
|
|
1753
|
-
for (const [column, value] of entries) {
|
|
1754
|
-
assertNotUndefined(value);
|
|
1755
|
-
encodeString(buffer, column);
|
|
1756
|
-
encodeSqliteValue(buffer, value);
|
|
1757
|
-
}
|
|
1859
|
+
encodeDbChange(buffer, message);
|
|
1758
1860
|
|
|
1759
1861
|
// Add PADMÉ padding (ignored during decoding)
|
|
1760
1862
|
buffer.extend(createPadmePadding(buffer.getLength()));
|
|
@@ -1772,6 +1874,41 @@ export const encodeAndEncryptDbChange =
|
|
|
1772
1874
|
return buffer.unwrap() as EncryptedDbChange;
|
|
1773
1875
|
};
|
|
1774
1876
|
|
|
1877
|
+
/**
|
|
1878
|
+
* Encodes a {@link CrdtMessage} as {@link encodeAndEncryptDbChange} does before
|
|
1879
|
+
* padding and encryption.
|
|
1880
|
+
*
|
|
1881
|
+
* {@link Evolu.getMutationSize} measures mutations with this encoding, so
|
|
1882
|
+
* {@link maxMutationSize} limits exactly what is encoded, and every change
|
|
1883
|
+
* within it fits one protocol message.
|
|
1884
|
+
*/
|
|
1885
|
+
export const encodeDbChange = (buffer: Buffer, message: CrdtMessage): void => {
|
|
1886
|
+
encodeNonNegativeInt(buffer, protocolVersion);
|
|
1887
|
+
|
|
1888
|
+
// Encode the timestamp to prevent tampering (e.g., a malicious relay
|
|
1889
|
+
// assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
|
|
1890
|
+
buffer.extend(timestampToTimestampBytes(message.timestamp));
|
|
1891
|
+
|
|
1892
|
+
encodeFlags(buffer, [
|
|
1893
|
+
message.change.isInsert,
|
|
1894
|
+
// Encode nullable boolean as two flags: presence + value.
|
|
1895
|
+
message.change.isDelete != null,
|
|
1896
|
+
message.change.isDelete ?? false,
|
|
1897
|
+
]);
|
|
1898
|
+
|
|
1899
|
+
encodeString(buffer, message.change.table);
|
|
1900
|
+
buffer.extend(idToIdBytes(message.change.id));
|
|
1901
|
+
|
|
1902
|
+
const entries = objectToEntries(message.change.values);
|
|
1903
|
+
|
|
1904
|
+
encodeLength(buffer, entries);
|
|
1905
|
+
for (const [column, value] of entries) {
|
|
1906
|
+
assertNotUndefined(value);
|
|
1907
|
+
encodeString(buffer, column);
|
|
1908
|
+
encodeSqliteValue(buffer, value);
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1775
1912
|
/**
|
|
1776
1913
|
* Decrypts and decodes an {@link EncryptedCrdtMessage} using the provided
|
|
1777
1914
|
* owner's encryption key. Verifies that the embedded timestamp matches the
|
|
@@ -1945,21 +2082,30 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
1945
2082
|
}
|
|
1946
2083
|
|
|
1947
2084
|
const json = Json.from.parent(value);
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
// which would cause data corruption if we don't verify round-trip safety.
|
|
1951
|
-
if (json.ok && JSON.stringify(jsonToJsonValue(json.value)) === value) {
|
|
2085
|
+
if (json.ok) {
|
|
2086
|
+
const jsonValue = jsonToJsonValue(json.value);
|
|
1952
2087
|
jsonBuffer.reset();
|
|
1953
2088
|
try {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2089
|
+
// Encoding first rejects nesting deeper than decoding allows, before
|
|
2090
|
+
// the recursive JSON.stringify below could overflow the stack. Such
|
|
2091
|
+
// a value is encoded as a plain string.
|
|
2092
|
+
encodeJsonValue(jsonBuffer, jsonValue);
|
|
2093
|
+
// Only encode as Json if it survives JSON.parse/JSON.stringify
|
|
2094
|
+
// round-trip. Some valid JSON strings like "-0E0" get normalized to
|
|
2095
|
+
// "0" during parsing, which would cause data corruption if we don't
|
|
2096
|
+
// verify round-trip safety.
|
|
2097
|
+
if (JSON.stringify(jsonValue) === value) {
|
|
2098
|
+
const jsonBytes = jsonBuffer.unwrap();
|
|
2099
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Json);
|
|
2100
|
+
encodeLength(buffer, jsonBytes);
|
|
2101
|
+
buffer.extend(jsonBytes);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
if (!(error instanceof BufferError)) throw error;
|
|
1959
2106
|
} finally {
|
|
1960
2107
|
jsonBuffer.reset();
|
|
1961
2108
|
}
|
|
1962
|
-
return;
|
|
1963
2109
|
}
|
|
1964
2110
|
|
|
1965
2111
|
const base64Url = Base64Url.from.parent(value);
|
package/src/local-first/Query.ts
CHANGED
|
@@ -43,25 +43,22 @@ export type { NotNull as KyselyNotNull } from "kysely";
|
|
|
43
43
|
* import {
|
|
44
44
|
* assertType,
|
|
45
45
|
* createQueryBuilder,
|
|
46
|
-
*
|
|
46
|
+
* testEvoluSchema,
|
|
47
|
+
* type TestEvoluSchema,
|
|
47
48
|
* NonEmptyTrimmedString100,
|
|
48
49
|
* type Query,
|
|
50
|
+
* type TestTodoId,
|
|
49
51
|
* } from "@evolu/common";
|
|
50
52
|
*
|
|
51
|
-
* const
|
|
52
|
-
* type TodoId = typeof TodoId.Output;
|
|
53
|
-
* const Schema = {
|
|
54
|
-
* todo: { id: TodoId, title: NonEmptyTrimmedString100 },
|
|
55
|
-
* };
|
|
56
|
-
* const createQuery = createQueryBuilder(Schema);
|
|
53
|
+
* const createQuery = createQueryBuilder(testEvoluSchema);
|
|
57
54
|
* const allTodos = createQuery((db) => db.selectFrom("todo").selectAll());
|
|
58
55
|
*
|
|
59
56
|
* type AllTodosRow = typeof allTodos.Row;
|
|
60
57
|
* assertType<
|
|
61
|
-
* typeof allTodos extends Query<
|
|
58
|
+
* typeof allTodos extends Query<TestEvoluSchema> ? true : false,
|
|
62
59
|
* true
|
|
63
60
|
* >();
|
|
64
|
-
* assertType<AllTodosRow["id"],
|
|
61
|
+
* assertType<AllTodosRow["id"], TestTodoId>();
|
|
65
62
|
* assertType<AllTodosRow["title"], NonEmptyTrimmedString100 | null>();
|
|
66
63
|
* ```
|
|
67
64
|
*/
|
|
@@ -82,15 +79,11 @@ export type Query<
|
|
|
82
79
|
* import {
|
|
83
80
|
* assertType,
|
|
84
81
|
* createQueryBuilder,
|
|
85
|
-
*
|
|
82
|
+
* testEvoluSchema,
|
|
86
83
|
* type InferRow,
|
|
87
|
-
* NonEmptyTrimmedString100,
|
|
88
84
|
* } from "@evolu/common";
|
|
89
85
|
*
|
|
90
|
-
* const
|
|
91
|
-
* todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
|
|
92
|
-
* };
|
|
93
|
-
* const createQuery = createQueryBuilder(Schema);
|
|
86
|
+
* const createQuery = createQueryBuilder(testEvoluSchema);
|
|
94
87
|
* const allTodos = createQuery((db) =>
|
|
95
88
|
* db.selectFrom("todo").selectAll(),
|
|
96
89
|
* );
|
package/src/local-first/Relay.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { err, ok } from "../Result.ts";
|
|
|
17
17
|
import type { SqliteDep } from "../Sqlite.ts";
|
|
18
18
|
import { sql } from "../Sqlite.ts";
|
|
19
19
|
import { createMutexByKey } from "../Task.ts";
|
|
20
|
-
import { Name,
|
|
20
|
+
import { Name, NonNegativeInt, uint8ArrayToBase64Url } from "../Type.ts";
|
|
21
21
|
import { isPromiseLike, type Awaitable } from "../Types.ts";
|
|
22
22
|
import {
|
|
23
23
|
OwnerId,
|
|
@@ -246,7 +246,9 @@ export const createRelaySqliteStorage =
|
|
|
246
246
|
(sum, m) => sum + m.change.length,
|
|
247
247
|
0,
|
|
248
248
|
);
|
|
249
|
-
|
|
249
|
+
// A sum of lengths can be zero. Throwing here would panic the
|
|
250
|
+
// relay's shared Run.
|
|
251
|
+
const newStoredBytes = NonNegativeInt.orThrow(
|
|
250
252
|
(usage.storedBytes ?? 0) + incomingBytes,
|
|
251
253
|
);
|
|
252
254
|
|