@evolu/common 8.11.0 → 8.13.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/Callbacks.d.ts +12 -1
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +3 -0
- package/dist/src/Error.d.ts +10 -4
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +10 -4
- package/dist/src/Object.d.ts +65 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +142 -0
- package/dist/src/Resource.d.ts +0 -5
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +6 -13
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +7 -0
- package/dist/src/Task.d.ts +10 -8
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -5
- package/dist/src/Worker.d.ts +3 -3
- 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/dist/src/local-first/Db.d.ts +8 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +56 -19
- package/dist/src/local-first/Evolu.d.ts +142 -24
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +109 -8
- package/dist/src/local-first/Owner.d.ts +9 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +9 -0
- package/dist/src/local-first/Protocol.d.ts +18 -7
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +45 -28
- 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 +18 -3
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +523 -133
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +696 -256
- package/dist/src/local-first/Storage.d.ts +19 -15
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +4 -2
- package/package.json +1 -1
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- package/src/Callbacks.test.ts +20 -0
- package/src/Callbacks.ts +17 -1
- package/src/Error.ts +10 -4
- package/src/Object.test.ts +296 -0
- package/src/Object.ts +163 -0
- package/src/Resource.test.ts +20 -16
- package/src/Resource.ts +6 -20
- package/src/Sqlite.ts +7 -0
- package/src/Task.test.ts +233 -62
- package/src/Task.ts +47 -13
- package/src/Worker.ts +3 -3
- package/src/index.ts +6 -1
- package/src/local-first/Db.ts +88 -18
- package/src/local-first/Evolu.test.ts +589 -2
- package/src/local-first/Evolu.ts +285 -36
- package/src/local-first/Owner.ts +9 -0
- package/src/local-first/Protocol.test.ts +59 -60
- package/src/local-first/Protocol.ts +67 -47
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.ts +20 -3
- package/src/local-first/Shared.test.ts +2633 -646
- package/src/local-first/Shared.ts +1192 -364
- package/src/local-first/Storage.ts +24 -23
package/src/local-first/Evolu.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
assertNonEmptyReadonlyArray,
|
|
16
16
|
assertNotUndefined,
|
|
17
17
|
} from "../Assert.ts";
|
|
18
|
+
import { createBuffer } from "../Bytes.ts";
|
|
18
19
|
import { createCallbacks } from "../Callbacks.ts";
|
|
19
20
|
import type { ConsoleDep } from "../Console.ts";
|
|
20
21
|
import { createConsole } from "../Console.ts";
|
|
@@ -28,11 +29,22 @@ import {
|
|
|
28
29
|
type LockManagerDep,
|
|
29
30
|
} from "../LockManager.ts";
|
|
30
31
|
import { createMicrotaskBatch } from "../Microtask.ts";
|
|
32
|
+
import {
|
|
33
|
+
createMutableRecord,
|
|
34
|
+
isPlainObject,
|
|
35
|
+
objectToEntries,
|
|
36
|
+
shareStructure,
|
|
37
|
+
type ReadonlyRecord,
|
|
38
|
+
} from "../Object.ts";
|
|
31
39
|
import type { FlushSyncDep, ReloadAppDep } from "../Platform.ts";
|
|
32
40
|
import { createRefCountByKey } from "../RefCount.ts";
|
|
33
41
|
import { err, ok } from "../Result.ts";
|
|
34
42
|
import { isNonEmptySet } from "../Set.ts";
|
|
35
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
SqliteBoolean,
|
|
45
|
+
sqliteBooleanToBoolean,
|
|
46
|
+
type SqliteValue,
|
|
47
|
+
} from "../Sqlite.ts";
|
|
36
48
|
import type { Listener, ReadonlyStore, Unsubscribe } from "../Store.ts";
|
|
37
49
|
import { createStore } from "../Store.ts";
|
|
38
50
|
import type { Task } from "../Task.ts";
|
|
@@ -41,8 +53,9 @@ import {
|
|
|
41
53
|
createId,
|
|
42
54
|
createIdFromString,
|
|
43
55
|
type ExtractTyped,
|
|
44
|
-
|
|
56
|
+
Id,
|
|
45
57
|
Name,
|
|
58
|
+
PositiveInt,
|
|
46
59
|
type TypeError,
|
|
47
60
|
UrlSafeString,
|
|
48
61
|
} from "../Type.ts";
|
|
@@ -61,7 +74,14 @@ import type {
|
|
|
61
74
|
SyncOwner,
|
|
62
75
|
} from "./Owner.ts";
|
|
63
76
|
import { createOwnerWebSocketTransport } from "./Owner.ts";
|
|
64
|
-
import
|
|
77
|
+
import {
|
|
78
|
+
encodeDbChange,
|
|
79
|
+
type ProtocolError,
|
|
80
|
+
type ProtocolInvalidDataError,
|
|
81
|
+
type ProtocolQuotaError,
|
|
82
|
+
type ProtocolTimestampMismatchError,
|
|
83
|
+
type ProtocolVersionError,
|
|
84
|
+
} from "./Protocol.ts";
|
|
65
85
|
import type {
|
|
66
86
|
Queries,
|
|
67
87
|
QueriesToQueryRowsPromises,
|
|
@@ -77,21 +97,25 @@ import type {
|
|
|
77
97
|
Mutation,
|
|
78
98
|
MutationChange,
|
|
79
99
|
MutationOptions,
|
|
100
|
+
MutationValues,
|
|
80
101
|
ValidateSchema,
|
|
81
102
|
} from "./Schema.ts";
|
|
82
|
-
import { evoluSchemaToSqliteSchema } from "./Schema.ts";
|
|
103
|
+
import { evoluSchemaToSqliteSchema, isLocalOnlyTable } from "./Schema.ts";
|
|
83
104
|
import type {
|
|
84
105
|
ConsoleEntryOrError,
|
|
85
106
|
EvoluInput,
|
|
86
107
|
EvoluOutput,
|
|
87
108
|
OtherBuildRunningError,
|
|
109
|
+
OwnerSyncStatus,
|
|
110
|
+
PendingSyncRoute,
|
|
88
111
|
SharedWorkerDep,
|
|
89
112
|
SyncState,
|
|
90
|
-
|
|
113
|
+
syncStateToOwnerSyncStatus,
|
|
114
|
+
syncStateToRelaySyncStates,
|
|
91
115
|
} from "./Shared.ts";
|
|
92
116
|
import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.ts";
|
|
93
|
-
import { DbChange
|
|
94
|
-
import type
|
|
117
|
+
import { DbChange } from "./Storage.ts";
|
|
118
|
+
import { createTimestamp, type Timestamp } from "./Timestamp.ts";
|
|
95
119
|
|
|
96
120
|
/**
|
|
97
121
|
* Configuration for {@link createEvolu}.
|
|
@@ -371,8 +395,10 @@ export interface Evolu<
|
|
|
371
395
|
*
|
|
372
396
|
* Pass `onComplete` when follow-up work must wait until the mutation is
|
|
373
397
|
* stored and subscribed queries reflect it. It never runs when the database
|
|
374
|
-
* is unavailable; see {@link Evolu.loadQuery}.
|
|
375
|
-
*
|
|
398
|
+
* is unavailable; see {@link Evolu.loadQuery}. It also never runs when the
|
|
399
|
+
* mutation could not be stored, which {@link EvoluErrorDep.evoluError}
|
|
400
|
+
* reports. A stored change is not always visible; see
|
|
401
|
+
* {@link MutationOptions.onComplete}.
|
|
376
402
|
*
|
|
377
403
|
* ### Example
|
|
378
404
|
*
|
|
@@ -492,6 +518,41 @@ export interface Evolu<
|
|
|
492
518
|
*/
|
|
493
519
|
readonly upsert: Mutation<S, "upsert">;
|
|
494
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Returns the size of a mutation of `table` with `values`, measured as
|
|
523
|
+
* {@link maxMutationSize} describes.
|
|
524
|
+
*
|
|
525
|
+
* Use it to check values that column Types do not bound before mutating, or
|
|
526
|
+
* to show how much of the limit a mutation uses. It accepts any of the
|
|
527
|
+
* table's columns, so the values of an insert, update, or upsert fit, and it
|
|
528
|
+
* ignores `id` and `isDeleted`, which are not columns. Mutations of
|
|
529
|
+
* local-only tables are measured too, although they are exempt from the
|
|
530
|
+
* limit.
|
|
531
|
+
*
|
|
532
|
+
* ### Example
|
|
533
|
+
*
|
|
534
|
+
* ```ts
|
|
535
|
+
* import {
|
|
536
|
+
* assertType,
|
|
537
|
+
* type Evolu,
|
|
538
|
+
* maxMutationSize,
|
|
539
|
+
* type NonEmptyTrimmedString100,
|
|
540
|
+
* type TestEvoluSchema,
|
|
541
|
+
* } from "@evolu/common";
|
|
542
|
+
*
|
|
543
|
+
* const fitsTodo = (
|
|
544
|
+
* evolu: Evolu<TestEvoluSchema>,
|
|
545
|
+
* title: NonEmptyTrimmedString100,
|
|
546
|
+
* ) => evolu.getMutationSize("todo", { title }) <= maxMutationSize;
|
|
547
|
+
*
|
|
548
|
+
* assertType<ReturnType<typeof fitsTodo>, boolean>();
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
readonly getMutationSize: <TableName extends keyof S>(
|
|
552
|
+
table: TableName,
|
|
553
|
+
values: Partial<MutationValues<S[TableName], "update">>,
|
|
554
|
+
) => PositiveInt;
|
|
555
|
+
|
|
495
556
|
/**
|
|
496
557
|
* Load {@link Query} and return a promise with {@link QueryRows}.
|
|
497
558
|
*
|
|
@@ -655,6 +716,9 @@ export interface Evolu<
|
|
|
655
716
|
/**
|
|
656
717
|
* Exports the SQLite database file.
|
|
657
718
|
*
|
|
719
|
+
* The exported file is not encrypted, even when the local database is. Any
|
|
720
|
+
* SQLite tool can read all local data from it, so treat it as plaintext.
|
|
721
|
+
*
|
|
658
722
|
* Exports are sequential: concurrent calls share one pending export instead
|
|
659
723
|
* of starting parallel exports.
|
|
660
724
|
*
|
|
@@ -766,9 +830,12 @@ export interface Evolu<
|
|
|
766
830
|
*
|
|
767
831
|
* Reconciles locally stored changes, including writes previously rejected
|
|
768
832
|
* with {@link ProtocolQuotaError}, through the owner's active transports. Call
|
|
769
|
-
* this after the relay provider confirms additional quota is available.
|
|
770
|
-
*
|
|
771
|
-
*
|
|
833
|
+
* this after the relay provider confirms additional quota is available. It
|
|
834
|
+
* also checks again the changes this database skipped from the owner's
|
|
835
|
+
* relays, and sends such a relay the changes received from other relays,
|
|
836
|
+
* which otherwise wait for its next sync, such as after a reconnect. Existing
|
|
837
|
+
* connections and {@link Evolu.useOwner} registrations are retained, including
|
|
838
|
+
* registrations shared by multiple instances or tabs.
|
|
772
839
|
*
|
|
773
840
|
* The owner must have an active writable registration in this database.
|
|
774
841
|
* Unregistered or read-only owners are ignored. Requests are skipped while
|
|
@@ -776,8 +843,9 @@ export interface Evolu<
|
|
|
776
843
|
* reopen. Disposal drops locally buffered requests. Calling a disposed
|
|
777
844
|
* instance throws, like other Evolu operations.
|
|
778
845
|
*
|
|
779
|
-
* Returns immediately, without waiting for synchronization to complete.
|
|
780
|
-
*
|
|
846
|
+
* Returns immediately, without waiting for synchronization to complete. The
|
|
847
|
+
* owner's routes in sync state show a failure, such as a quota rejection, as
|
|
848
|
+
* `failure`, and a skipped change as `skippedError`.
|
|
781
849
|
*
|
|
782
850
|
* ### Example
|
|
783
851
|
*
|
|
@@ -806,17 +874,112 @@ export interface Evolu<
|
|
|
806
874
|
export type UnuseOwner = () => void;
|
|
807
875
|
|
|
808
876
|
/**
|
|
809
|
-
* Represents errors that can occur in {@link Evolu}.
|
|
877
|
+
* Represents app-level errors that can occur in {@link Evolu}.
|
|
878
|
+
*
|
|
879
|
+
* Apps show them from {@link EvoluErrorDep.evoluError}. Problems a relay causes
|
|
880
|
+
* while syncing an owner are not EvoluErrors; see below. An unexpected failure
|
|
881
|
+
* while syncing is an {@link UnknownError}.
|
|
882
|
+
*
|
|
883
|
+
* An error that leaves the app unusable deserves a modal dialog, which moves
|
|
884
|
+
* focus into itself and restores it when closed. Any other error is a status
|
|
885
|
+
* message: show it in a region with `role="alert"`, which screen readers
|
|
886
|
+
* announce without moving focus, so the user keeps working and a background tab
|
|
887
|
+
* shows it when the user returns. Avoid `alert()`, which blocks the page, once
|
|
888
|
+
* in every open tab.
|
|
889
|
+
*
|
|
890
|
+
* - {@link UnsupportedDbVersionError} blocks the app: the local data needs a newer
|
|
891
|
+
* version of it. Ask the user to update the app or close all its tabs.
|
|
892
|
+
* - {@link OtherBuildRunningError} blocks the app while it lasts: another version
|
|
893
|
+
* of the app holds the local data. Ask the user to close the app's other
|
|
894
|
+
* tabs. It clears when the wait ends.
|
|
895
|
+
* - {@link UnknownError} does not block the app: Evolu logged an unexpected
|
|
896
|
+
* failure, such as a mutation that could not be stored. Show a generic
|
|
897
|
+
* message.
|
|
898
|
+
*
|
|
899
|
+
* A problem syncing an owner through a relay belongs to that relay and often
|
|
900
|
+
* repeats in every round, so sync state shows it on the relay's route, as its
|
|
901
|
+
* {@link PendingSyncRoute.failure} with its details, and apps show it as the
|
|
902
|
+
* owner's `Error` {@link OwnerSyncStatus}. When the relay rejected or failed a
|
|
903
|
+
* request, or sent a frame that could not be decoded, the route shows a
|
|
904
|
+
* {@link ProtocolError}. A {@link ProtocolQuotaError} needs more relay quota,
|
|
905
|
+
* then {@link Evolu.requestSync}, and a {@link ProtocolVersionError} needs an app
|
|
906
|
+
* or relay update.
|
|
907
|
+
*
|
|
908
|
+
* A received change that was not created with the owner's encryption key, was
|
|
909
|
+
* altered afterwards, or cannot be decoded by this app version is skipped,
|
|
910
|
+
* while everything else still syncs. The relay offers it again in every round,
|
|
911
|
+
* so the route shows it as its `skippedError`: the
|
|
912
|
+
* {@link DecryptWithXChaCha20Poly1305Error},
|
|
913
|
+
* {@link ProtocolTimestampMismatchError}, or {@link ProtocolInvalidDataError} of
|
|
914
|
+
* the first change skipped in a reply, with its details. The owner's status is
|
|
915
|
+
* `Error` with it too, unless a relay has a failure. Once this database stores
|
|
916
|
+
* a valid change with that timestamp, for example from another relay, the relay
|
|
917
|
+
* no longer offers its copy, and the route completes with its next sync, such
|
|
918
|
+
* as after a reconnect or {@link Evolu.requestSync}, during which no changes
|
|
919
|
+
* arrive from other relays. If you don't trust that relay, stop using it for
|
|
920
|
+
* the owner. For owners that use the default transports, such as
|
|
921
|
+
* {@link Evolu.appOwner} when {@link EvoluConfig.transports} is not empty,
|
|
922
|
+
* replace the relay there; an empty list stops syncing the app owner and makes
|
|
923
|
+
* {@link Evolu.useOwner} require explicit transports. For an owner you passed
|
|
924
|
+
* explicit transports to {@link Evolu.useOwner}, call its {@link UnuseOwner} and
|
|
925
|
+
* use it again without that relay. Do this wherever the owner is used, such as
|
|
926
|
+
* in every tab, because the owner syncs through every transport any of its uses
|
|
927
|
+
* claims. If every route of the owner shows
|
|
928
|
+
* {@link DecryptWithXChaCha20Poly1305Error} and your code creates or shares the
|
|
929
|
+
* owner, check the owner's keys.
|
|
810
930
|
*
|
|
811
931
|
* @group Core
|
|
812
932
|
*/
|
|
813
933
|
export type EvoluError =
|
|
814
|
-
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
934
|
+
OtherBuildRunningError | UnknownError | UnsupportedDbVersionError;
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* The largest {@link Mutation}, in bytes.
|
|
938
|
+
*
|
|
939
|
+
* A mutation's size is its change as encoded for sync: the table name, the ID,
|
|
940
|
+
* and every column name and value, with a few bytes of overhead. A string takes
|
|
941
|
+
* at most three bytes per UTF-16 code unit. Every mutation within the limit
|
|
942
|
+
* fits one protocol message, so it can always sync. A larger mutation throws
|
|
943
|
+
* and is not saved. Mutations of local-only tables are exempt, because they
|
|
944
|
+
* never sync. Measure a mutation in advance with {@link Evolu.getMutationSize}.
|
|
945
|
+
*
|
|
946
|
+
* @group Core
|
|
947
|
+
*/
|
|
948
|
+
export const maxMutationSize: PositiveInt =
|
|
949
|
+
/*#__PURE__*/ PositiveInt.orThrow(640_000);
|
|
950
|
+
|
|
951
|
+
/** Measures a mutation as {@link maxMutationSize} describes. */
|
|
952
|
+
const measureMutation = (
|
|
953
|
+
table: string,
|
|
954
|
+
values: ReadonlyRecord<string, SqliteValue | undefined>,
|
|
955
|
+
): PositiveInt => {
|
|
956
|
+
const columns = createMutableRecord<string, SqliteValue>();
|
|
957
|
+
for (const [column, value] of objectToEntries(values)) {
|
|
958
|
+
if (value !== undefined && column !== "id" && column !== "isDeleted") {
|
|
959
|
+
columns[column] = value;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Measuring with the protocol's encoding keeps the limit and the message size
|
|
964
|
+
// from disagreeing. A separate formula would save the tab about 3 KB
|
|
965
|
+
// compressed, but it would be a second encoding to keep in sync, and it
|
|
966
|
+
// would have to overcount.
|
|
967
|
+
const buffer = createBuffer();
|
|
968
|
+
encodeDbChange(buffer, {
|
|
969
|
+
// Every timestamp and ID takes 16 bytes, and the flags always take one.
|
|
970
|
+
timestamp: createTimestamp(),
|
|
971
|
+
change: DbChange.orThrow({
|
|
972
|
+
table,
|
|
973
|
+
id: mutationSizeId,
|
|
974
|
+
values: columns,
|
|
975
|
+
isInsert: true,
|
|
976
|
+
isDelete: null,
|
|
977
|
+
}),
|
|
978
|
+
});
|
|
979
|
+
return buffer.getLength() as PositiveInt;
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const mutationSizeId = /*#__PURE__*/ Id.orThrow("A".repeat(22));
|
|
820
983
|
|
|
821
984
|
/**
|
|
822
985
|
* Dependency wrapper for the shared {@link EvoluError} store.
|
|
@@ -845,6 +1008,13 @@ export interface EvoluErrorDep {
|
|
|
845
1008
|
* instead; see {@link UnsupportedDbVersionError}. Show that blocking message
|
|
846
1009
|
* outside any query-loading boundary, so pending queries do not hide it.
|
|
847
1010
|
*
|
|
1011
|
+
* An {@link UnknownError} leaves Evolu in an unknown state, so the app can
|
|
1012
|
+
* only ask the user to close the tab. A failed shared worker closes itself,
|
|
1013
|
+
* so the app opened again starts a new one. Some errors reach every tab, such
|
|
1014
|
+
* as an unexpected failure of the shared worker. An app that forwards this
|
|
1015
|
+
* store to an error tracker from each tab then reports such an error once per
|
|
1016
|
+
* tab.
|
|
1017
|
+
*
|
|
848
1018
|
* ### Example
|
|
849
1019
|
*
|
|
850
1020
|
* ```ts
|
|
@@ -858,14 +1028,13 @@ export interface EvoluErrorDep {
|
|
|
858
1028
|
* // The message for the current error, or null for none.
|
|
859
1029
|
* const errorMessage = (error: EvoluError | null): string | null => {
|
|
860
1030
|
* if (!error) return null;
|
|
861
|
-
* // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- The default handles every other EvoluError.
|
|
862
1031
|
* switch (error.type) {
|
|
863
1032
|
* case "UnsupportedDbVersionError":
|
|
864
1033
|
* return "Your data requires a newer version of this app. Please update it.";
|
|
865
1034
|
* case "OtherBuildRunningError":
|
|
866
1035
|
* return "This app is open in another tab with a different version. Close that tab to continue.";
|
|
867
|
-
*
|
|
868
|
-
* return "Something went wrong. Please
|
|
1036
|
+
* case "UnknownError":
|
|
1037
|
+
* return "Something went wrong. Please close this tab and open the app again.";
|
|
869
1038
|
* }
|
|
870
1039
|
* };
|
|
871
1040
|
*
|
|
@@ -901,8 +1070,15 @@ export interface SyncStateDep {
|
|
|
901
1070
|
/**
|
|
902
1071
|
* {@link ReadonlyStore} of the latest {@link SyncState} shared by all
|
|
903
1072
|
* {@link Evolu} instances, or null before the shared worker sends its first
|
|
904
|
-
* snapshot.
|
|
905
|
-
*
|
|
1073
|
+
* snapshot. It lists every database the shared worker serves, including other
|
|
1074
|
+
* tabs', so an app finds its own by {@link Evolu.name}. Apps show users an
|
|
1075
|
+
* owner's {@link OwnerSyncStatus}, from {@link syncStateToOwnerSyncStatus} or a
|
|
1076
|
+
* framework binding's `useOwnerSyncStatus`; views of each relay use
|
|
1077
|
+
* {@link syncStateToRelaySyncStates}.
|
|
1078
|
+
*
|
|
1079
|
+
* Each snapshot keeps the previous snapshot's object for every part that did
|
|
1080
|
+
* not change, and a snapshot equal to the previous one leaves the store
|
|
1081
|
+
* unchanged, so parts and derived statuses can be compared with `===`.
|
|
906
1082
|
*
|
|
907
1083
|
* ### Example
|
|
908
1084
|
*
|
|
@@ -911,6 +1087,7 @@ export interface SyncStateDep {
|
|
|
911
1087
|
* assertEqual,
|
|
912
1088
|
* createId,
|
|
913
1089
|
* createStore,
|
|
1090
|
+
* Millis,
|
|
914
1091
|
* testCreateDeps,
|
|
915
1092
|
* } from "@evolu/common";
|
|
916
1093
|
* import type {
|
|
@@ -920,7 +1097,7 @@ export interface SyncStateDep {
|
|
|
920
1097
|
*
|
|
921
1098
|
* const openRelayLabels = (deps: SyncStateDep): ReadonlyArray<string> =>
|
|
922
1099
|
* (deps.syncState.get()?.transports ?? [])
|
|
923
|
-
* .filter(({
|
|
1100
|
+
* .filter(({ connection }) => connection.type === "Open")
|
|
924
1101
|
* .map(({ label }) => label);
|
|
925
1102
|
*
|
|
926
1103
|
* using syncState = createStore<SyncState | null>(null);
|
|
@@ -930,12 +1107,14 @@ export interface SyncStateDep {
|
|
|
930
1107
|
* syncState.set({
|
|
931
1108
|
* transports: [
|
|
932
1109
|
* {
|
|
1110
|
+
* type: "WebSocket",
|
|
933
1111
|
* id: createId<"SyncTransport">(deps),
|
|
934
1112
|
* label: "wss://relay.example",
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
1113
|
+
* connection: {
|
|
1114
|
+
* type: "Open",
|
|
1115
|
+
* openedAt: Millis.orThrow(1000),
|
|
1116
|
+
* error: null,
|
|
1117
|
+
* },
|
|
939
1118
|
* },
|
|
940
1119
|
* ],
|
|
941
1120
|
* tenants: [],
|
|
@@ -1025,11 +1204,15 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
|
1025
1204
|
}
|
|
1026
1205
|
break;
|
|
1027
1206
|
|
|
1028
|
-
case "Error":
|
|
1029
|
-
|
|
1207
|
+
case "Error": {
|
|
1208
|
+
// Every build shares this channel, so another build's worker can post
|
|
1209
|
+
// an error type this build does not post, such as an older build's
|
|
1210
|
+
// sync error. That build's own tabs show it, so this tab only logs it.
|
|
1211
|
+
if (message.error.type === "UnknownError") setEvoluError(message.error);
|
|
1030
1212
|
// Keep typed errors visible in logs as operational failures.
|
|
1031
1213
|
console.error(message.error);
|
|
1032
1214
|
break;
|
|
1215
|
+
}
|
|
1033
1216
|
|
|
1034
1217
|
default:
|
|
1035
1218
|
exhaustiveCheck(message);
|
|
@@ -1043,8 +1226,9 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
|
1043
1226
|
break;
|
|
1044
1227
|
|
|
1045
1228
|
case "Error":
|
|
1046
|
-
// Sent to this tab only: its database refused startup,
|
|
1047
|
-
// still waits for another
|
|
1229
|
+
// Sent to this tab only: its database refused startup, a mutation it
|
|
1230
|
+
// made could not be stored, or its worker still waits for another
|
|
1231
|
+
// build.
|
|
1048
1232
|
setEvoluError(message.error);
|
|
1049
1233
|
console.error(message.error);
|
|
1050
1234
|
break;
|
|
@@ -1067,7 +1251,15 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
|
1067
1251
|
message.syncStateChannelName,
|
|
1068
1252
|
);
|
|
1069
1253
|
syncStateBroadcastChannel.onMessage = (state) => {
|
|
1070
|
-
|
|
1254
|
+
// Every snapshot arrives as a new structured clone, so its unchanged
|
|
1255
|
+
// parts get the previous snapshot's objects back, and an unchanged
|
|
1256
|
+
// snapshot leaves the store as it is.
|
|
1257
|
+
const previous = syncState.get();
|
|
1258
|
+
syncState.set(
|
|
1259
|
+
previous
|
|
1260
|
+
? shareStructure(previous, state, syncStateItemToKey)
|
|
1261
|
+
: state,
|
|
1262
|
+
);
|
|
1071
1263
|
};
|
|
1072
1264
|
// Asking only after listening misses no snapshot.
|
|
1073
1265
|
sharedWorker.port.postMessage({ type: "RequestSyncState" });
|
|
@@ -1105,6 +1297,14 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
|
|
|
1105
1297
|
);
|
|
1106
1298
|
};
|
|
1107
1299
|
|
|
1300
|
+
// Transports, tenants, owners, and routes come and go, so each is compared
|
|
1301
|
+
// with its previous self by its ID: a transport's id, a tenant's name, an
|
|
1302
|
+
// owner's ownerId, or a route's transportId. A wrong key only loses sharing.
|
|
1303
|
+
const syncStateItemToKey = (item: unknown): unknown =>
|
|
1304
|
+
isPlainObject(item)
|
|
1305
|
+
? (item.id ?? item.name ?? item.ownerId ?? item.transportId)
|
|
1306
|
+
: undefined;
|
|
1307
|
+
|
|
1108
1308
|
/**
|
|
1109
1309
|
* Creates an {@link Evolu} instance from {@link EvoluSchema} and
|
|
1110
1310
|
* {@link EvoluConfig}.
|
|
@@ -1304,6 +1504,14 @@ export const createEvolu =
|
|
|
1304
1504
|
break;
|
|
1305
1505
|
}
|
|
1306
1506
|
|
|
1507
|
+
case "OnMutateFailed": {
|
|
1508
|
+
// The tab reports the failure, and these callbacks never run.
|
|
1509
|
+
for (const onCompleteId of message.onCompleteIds) {
|
|
1510
|
+
onMutateCompleteCallbacks.unregister(onCompleteId);
|
|
1511
|
+
}
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1307
1515
|
default:
|
|
1308
1516
|
exhaustiveCheck(message);
|
|
1309
1517
|
}
|
|
@@ -1396,6 +1604,41 @@ export const createEvolu =
|
|
|
1396
1604
|
`Invalid DbChange for table '${String(table)}'.`,
|
|
1397
1605
|
);
|
|
1398
1606
|
|
|
1607
|
+
// Copy binary values, so what is saved is what was passed: the caller
|
|
1608
|
+
// can change a Uint8Array before the batch is sent, a view of a
|
|
1609
|
+
// resizable buffer can grow, and a view of a shared buffer cannot be
|
|
1610
|
+
// sent to a SharedWorker at all. Node's Buffer passes validation and
|
|
1611
|
+
// its slice shares memory, so only the constructor copies reliably.
|
|
1612
|
+
for (const [column, value] of objectToEntries(dbChange.values)) {
|
|
1613
|
+
if (typeof value === "object" && value !== null) {
|
|
1614
|
+
changeValues[column] = new Uint8Array(value);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// A change over maxMutationSize could never sync, and a shared worker
|
|
1619
|
+
// hosting two databases would stop all work of that database. Correct
|
|
1620
|
+
// apps bound column Types, so it is a programmer error and throws like
|
|
1621
|
+
// the DbChange check above, before batching: it gets no timestamp, the
|
|
1622
|
+
// database worker never sees it, and the code after the call does not
|
|
1623
|
+
// run, so a form keeps its input and no row refers to the missing one.
|
|
1624
|
+
//
|
|
1625
|
+
// Rejected: reporting it through evoluError, which lets that code run
|
|
1626
|
+
// as if the mutation was saved; checking in the database worker, which
|
|
1627
|
+
// gets it already batched and cannot reach the call site; quarantine,
|
|
1628
|
+
// which holds changes stored for sync; and skipping it in sync, which
|
|
1629
|
+
// keeps range fingerprints disagreeing. Received changes are not
|
|
1630
|
+
// checked, and oversized changes stored before this limit are not
|
|
1631
|
+
// recovered, which needs a history-aware design.
|
|
1632
|
+
//
|
|
1633
|
+
// Local-only tables never sync, so they have no limit.
|
|
1634
|
+
if (!isLocalOnlyTable(dbChange.table)) {
|
|
1635
|
+
const size = measureMutation(dbChange.table, dbChange.values);
|
|
1636
|
+
assert(
|
|
1637
|
+
size <= maxMutationSize,
|
|
1638
|
+
`The mutation of table '${dbChange.table}' is ${size} bytes, over maxMutationSize (${maxMutationSize}). Bound the Types of its columns or check it with evolu.getMutationSize.`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1399
1642
|
mutateBatch.push({
|
|
1400
1643
|
change: { ...dbChange, ownerId: options?.ownerId ?? appOwner.id },
|
|
1401
1644
|
onComplete: options?.onComplete,
|
|
@@ -1484,6 +1727,12 @@ export const createEvolu =
|
|
|
1484
1727
|
update: createMutation("update"),
|
|
1485
1728
|
upsert: createMutation("upsert"),
|
|
1486
1729
|
|
|
1730
|
+
getMutationSize: (table, values) =>
|
|
1731
|
+
measureMutation(
|
|
1732
|
+
String(table),
|
|
1733
|
+
values as ReadonlyRecord<string, SqliteValue | undefined>,
|
|
1734
|
+
),
|
|
1735
|
+
|
|
1487
1736
|
loadQuery,
|
|
1488
1737
|
loadQueries: <Q extends Queries<S>>(
|
|
1489
1738
|
queries: [...Q],
|
package/src/local-first/Owner.ts
CHANGED
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
* - {@link OwnerEncryptionKey}: the symmetric key that protects the data.
|
|
29
29
|
* - {@link OwnerWriteKey}: the rotatable token that authorizes writes.
|
|
30
30
|
*
|
|
31
|
+
* Only holders of the encryption key can create an owner's changes. Each change
|
|
32
|
+
* is encrypted and authenticated together with its timestamp, so a relay, or
|
|
33
|
+
* anyone who can write to a relay for the owner, can store bytes for the owner
|
|
34
|
+
* but cannot forge or move a change. A client stores only the changes it can
|
|
35
|
+
* decrypt, verify, and decode, and syncs only what it stored. It skips any
|
|
36
|
+
* other change, which sync state shows on the route of the relay that holds it,
|
|
37
|
+
* and the relay offers it again on each sync until the client stores a valid
|
|
38
|
+
* change with that timestamp.
|
|
39
|
+
*
|
|
31
40
|
* @module
|
|
32
41
|
*/
|
|
33
42
|
|