@evolu/common 6.0.1-preview.1 → 6.0.1-preview.10
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/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -3
- package/dist/src/Crypto.d.ts +11 -0
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Evolu/Config.d.ts +17 -4
- package/dist/src/Evolu/Config.d.ts.map +1 -1
- package/dist/src/Evolu/Config.js +1 -1
- package/dist/src/Evolu/Db.d.ts +21 -13
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +81 -64
- package/dist/src/Evolu/Evolu.d.ts +6 -6
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +19 -24
- package/dist/src/Evolu/Protocol.d.ts +79 -37
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +167 -50
- package/dist/src/Evolu/Relay.d.ts +3 -1
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +39 -3
- package/dist/src/Evolu/Schema.d.ts +24 -40
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +13 -72
- package/dist/src/Evolu/Storage.d.ts +1 -0
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +10 -0
- package/package.json +3 -3
- package/src/Assert.ts +6 -3
- package/src/Crypto.ts +13 -0
- package/src/Evolu/Config.ts +19 -5
- package/src/Evolu/Db.ts +92 -76
- package/src/Evolu/Evolu.ts +38 -35
- package/src/Evolu/Protocol.ts +205 -79
- package/src/Evolu/Relay.ts +45 -4
- package/src/Evolu/Schema.ts +102 -130
- package/src/Evolu/Storage.ts +12 -0
package/src/Evolu/Evolu.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
-
import { assertNonEmptyArray } from "../Assert.js";
|
|
2
|
+
import { assert, assertNonEmptyArray } from "../Assert.js";
|
|
3
3
|
import { createCallbacks } from "../Callbacks.js";
|
|
4
4
|
import { ConsoleDep } from "../Console.js";
|
|
5
5
|
import { SymmetricCryptoDecryptError } from "../Crypto.js";
|
|
@@ -44,9 +44,9 @@ import {
|
|
|
44
44
|
SubscribedQueries,
|
|
45
45
|
} from "./Query.js";
|
|
46
46
|
import {
|
|
47
|
-
assertValidEvoluSchema,
|
|
48
47
|
CreateQuery,
|
|
49
48
|
EvoluSchema,
|
|
49
|
+
evoluSchemaToDbSchema,
|
|
50
50
|
insertable,
|
|
51
51
|
Mutation,
|
|
52
52
|
MutationKind,
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
MutationOptions,
|
|
55
55
|
updateable,
|
|
56
56
|
upsertable,
|
|
57
|
-
|
|
57
|
+
ValidateSchema,
|
|
58
58
|
ValidMutationSize,
|
|
59
59
|
ValidMutationSizeError,
|
|
60
60
|
} from "./Schema.js";
|
|
@@ -88,7 +88,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
88
88
|
* All this function does is compile the Kysely query and serialize it into a
|
|
89
89
|
* unique string. Both operations are fast and cheap.
|
|
90
90
|
*
|
|
91
|
-
* For mutations, use {@link Evolu
|
|
91
|
+
* For mutations, use {@link Evolu#insert} and {@link Evolu#update}.
|
|
92
92
|
*
|
|
93
93
|
* ### Example
|
|
94
94
|
*
|
|
@@ -111,7 +111,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
111
111
|
* A returned promise always resolves successfully because there is no reason
|
|
112
112
|
* why loading should fail. All data are local, and the query is typed. A
|
|
113
113
|
* serious unexpected Evolu error shall be handled with
|
|
114
|
-
* {@link Evolu
|
|
114
|
+
* {@link Evolu#subscribeError}.
|
|
115
115
|
*
|
|
116
116
|
* Loading is batched, and returned promises are cached, so there is no need
|
|
117
117
|
* for an additional cache. Evolu's internal cache is invalidated on
|
|
@@ -144,7 +144,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
144
144
|
* If you are curious why Evolu does not do that for all queries by default,
|
|
145
145
|
* the answer is simple: performance. Tracking changes is costly and
|
|
146
146
|
* meaningful only for visible (hence subscribed) queries anyway. To subscribe
|
|
147
|
-
* to a query, use {@link Evolu
|
|
147
|
+
* to a query, use {@link Evolu#subscribeQuery}.
|
|
148
148
|
*
|
|
149
149
|
* ### Example
|
|
150
150
|
*
|
|
@@ -300,7 +300,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
300
300
|
|
|
301
301
|
/**
|
|
302
302
|
* Restore {@link AppOwner} with all their synced data. It uses
|
|
303
|
-
* {@link Evolu
|
|
303
|
+
* {@link Evolu#resetAppOwner}, so be careful.
|
|
304
304
|
*/
|
|
305
305
|
readonly restoreAppOwner: (
|
|
306
306
|
mnemonic: Mnemonic,
|
|
@@ -431,9 +431,7 @@ let tabId: Id | null = null;
|
|
|
431
431
|
export const createEvolu =
|
|
432
432
|
(deps: EvoluDeps) =>
|
|
433
433
|
<S extends EvoluSchema>(
|
|
434
|
-
|
|
435
|
-
// with type errors messages as we had it in the old Evolu.
|
|
436
|
-
schema: S,
|
|
434
|
+
schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
|
|
437
435
|
partialConfig: Partial<EvoluConfigWithInitialData<S>> = {},
|
|
438
436
|
): Evolu<S> => {
|
|
439
437
|
const config = { ...defaultConfig, ...partialConfig };
|
|
@@ -441,11 +439,14 @@ export const createEvolu =
|
|
|
441
439
|
let evolu = evoluInstances.get(config.name);
|
|
442
440
|
|
|
443
441
|
if (evolu == null) {
|
|
444
|
-
evolu = createEvoluInstance(deps)(
|
|
442
|
+
evolu = createEvoluInstance(deps)(
|
|
443
|
+
schema as EvoluSchema,
|
|
444
|
+
config as IntentionalNever,
|
|
445
|
+
);
|
|
445
446
|
evoluInstances.set(config.name, evolu);
|
|
446
447
|
} else {
|
|
447
448
|
// Hot reloading. Note that indexes are intentionally omitted.
|
|
448
|
-
evolu.ensureSchema(schema);
|
|
449
|
+
evolu.ensureSchema(schema as EvoluSchema);
|
|
449
450
|
}
|
|
450
451
|
|
|
451
452
|
return evolu as IntentionalNever;
|
|
@@ -554,10 +555,7 @@ const createEvoluInstance =
|
|
|
554
555
|
}
|
|
555
556
|
});
|
|
556
557
|
|
|
557
|
-
const dbSchema =
|
|
558
|
-
assertValidEvoluSchema(schema),
|
|
559
|
-
indexes,
|
|
560
|
-
);
|
|
558
|
+
const dbSchema = evoluSchemaToDbSchema(schema, indexes);
|
|
561
559
|
|
|
562
560
|
const mutationTypesCache = new Map<
|
|
563
561
|
MutationKind,
|
|
@@ -592,21 +590,17 @@ const createEvoluInstance =
|
|
|
592
590
|
if (initialData)
|
|
593
591
|
initialData({
|
|
594
592
|
insert: (table, props) => {
|
|
595
|
-
const Type = getMutationType(table, "insert");
|
|
596
593
|
const id = createId(deps);
|
|
594
|
+
const values = getMutationType(table, "insert").fromUnknown(props);
|
|
597
595
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
initialDataDbChanges.push(
|
|
602
|
-
id,
|
|
603
|
-
table,
|
|
604
|
-
values: result.value,
|
|
605
|
-
} as unknown as DbChange);
|
|
596
|
+
if (values.ok) {
|
|
597
|
+
const dbChange = { table, id, values: values.value };
|
|
598
|
+
assertValidDbChange(dbChange);
|
|
599
|
+
initialDataDbChanges.push(dbChange);
|
|
606
600
|
return ok({ id });
|
|
607
601
|
}
|
|
608
602
|
|
|
609
|
-
return
|
|
603
|
+
return values;
|
|
610
604
|
},
|
|
611
605
|
});
|
|
612
606
|
|
|
@@ -654,14 +648,14 @@ const createEvoluInstance =
|
|
|
654
648
|
} else {
|
|
655
649
|
// Remove `id` from values.
|
|
656
650
|
const { id: _id, ...values } = result.value;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
mutateMicrotaskQueue.push([
|
|
651
|
+
const dbChange = { table, id, values };
|
|
652
|
+
assertValidDbChange(dbChange);
|
|
653
|
+
mutateMicrotaskQueue.push([dbChange, options?.onComplete]);
|
|
660
654
|
}
|
|
661
655
|
|
|
662
656
|
if (mutateMicrotaskQueue.length === 1)
|
|
663
657
|
queueMicrotask(() => {
|
|
664
|
-
const changes = [];
|
|
658
|
+
const changes: Array<DbChange> = [];
|
|
665
659
|
const onCompletes = [];
|
|
666
660
|
|
|
667
661
|
for (const [change, onComplete] of mutateMicrotaskQueue) {
|
|
@@ -807,6 +801,7 @@ const createEvoluInstance =
|
|
|
807
801
|
const onCompleteId = callbacks.register(() => {
|
|
808
802
|
resolve();
|
|
809
803
|
});
|
|
804
|
+
|
|
810
805
|
dbWorker.postMessage({
|
|
811
806
|
type: "reset",
|
|
812
807
|
onCompleteId,
|
|
@@ -822,11 +817,8 @@ const createEvoluInstance =
|
|
|
822
817
|
|
|
823
818
|
ensureSchema: (schema) => {
|
|
824
819
|
mutationTypesCache.clear();
|
|
825
|
-
const
|
|
826
|
-
dbWorker.postMessage({
|
|
827
|
-
type: "ensureDbSchema",
|
|
828
|
-
dbSchema: validEvoluSchemaToDbSchema(validSchema),
|
|
829
|
-
});
|
|
820
|
+
const dbSchema = evoluSchemaToDbSchema(schema);
|
|
821
|
+
dbWorker.postMessage({ type: "ensureDbSchema", dbSchema });
|
|
830
822
|
},
|
|
831
823
|
|
|
832
824
|
exportDatabase: () => {
|
|
@@ -945,3 +937,14 @@ const createLoadingPromises = (
|
|
|
945
937
|
|
|
946
938
|
return loadingPromises;
|
|
947
939
|
};
|
|
940
|
+
|
|
941
|
+
const assertValidDbChange: (dbChange: {
|
|
942
|
+
table: string;
|
|
943
|
+
id: Id;
|
|
944
|
+
values: unknown;
|
|
945
|
+
}) => asserts dbChange is DbChange = (dbChange) => {
|
|
946
|
+
assert(
|
|
947
|
+
DbChange.is(dbChange),
|
|
948
|
+
`Failed to create DbChange for table "${dbChange.table}". If you see this message, you either disabled EvoluSchema validation or Evolu has a bug - please report it.`,
|
|
949
|
+
);
|
|
950
|
+
};
|
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -15,21 +15,39 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ### Message Structure
|
|
17
17
|
*
|
|
18
|
-
* | Field | Notes
|
|
19
|
-
* | :----------------------------- |
|
|
20
|
-
* | **Header** |
|
|
21
|
-
* | - {@link protocolVersion} |
|
|
22
|
-
* | - {@link OwnerId} |
|
|
23
|
-
* |
|
|
24
|
-
* |
|
|
25
|
-
* | - {@link
|
|
26
|
-
* | - {@link
|
|
27
|
-
* | -
|
|
28
|
-
* |
|
|
29
|
-
* |
|
|
30
|
-
* | - {@link
|
|
18
|
+
* | Field | Notes |
|
|
19
|
+
* | :----------------------------- | :------------------------ |
|
|
20
|
+
* | **Header** | |
|
|
21
|
+
* | - {@link protocolVersion} | |
|
|
22
|
+
* | - {@link OwnerId} | {@link Owner} |
|
|
23
|
+
* | **Initiator** | |
|
|
24
|
+
* | - {@link WriteKeyMode} | |
|
|
25
|
+
* | - {@link WriteKey} | If WriteKeyMode >= 1 |
|
|
26
|
+
* | - {@link WriteKey} | If WriteKeyMode = 2 (new) |
|
|
27
|
+
* | **Non-initiator** | |
|
|
28
|
+
* | - {@link ProtocolErrorCode} | |
|
|
29
|
+
* | **Messages** | |
|
|
30
|
+
* | - {@link NonNegativeInt} | A number of messages. |
|
|
31
|
+
* | - {@link EncryptedCrdtMessage} | |
|
|
32
|
+
* | **Ranges** | |
|
|
33
|
+
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
34
|
+
* | - {@link Range} | |
|
|
31
35
|
*
|
|
32
|
-
*
|
|
36
|
+
* ### WriteKey Validation
|
|
37
|
+
*
|
|
38
|
+
* The initiator sends WriteKeyMode and optionally one or two WriteKeys. One key
|
|
39
|
+
* for write operations and two for key rotation (current and new). Note that
|
|
40
|
+
* it's ok to not send any key if initiator is going to be synced with readonly
|
|
41
|
+
* owner. The non-initiator validates them immediately after parsing the
|
|
42
|
+
* initiator header, before processing any messages or ranges.
|
|
43
|
+
*
|
|
44
|
+
* ### WriteKey Rotation
|
|
45
|
+
*
|
|
46
|
+
* When initiator's {@link WriteKeyMode} is `Rotation`, two WriteKeys are
|
|
47
|
+
* present:
|
|
48
|
+
*
|
|
49
|
+
* 1. Current WriteKey (for validation)
|
|
50
|
+
* 2. New WriteKey (to be stored)
|
|
33
51
|
*
|
|
34
52
|
* ### Synchronization
|
|
35
53
|
*
|
|
@@ -127,10 +145,8 @@ import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
|
127
145
|
import { assert } from "../Assert.js";
|
|
128
146
|
import {
|
|
129
147
|
Buffer,
|
|
130
|
-
BufferError,
|
|
131
148
|
bytesToHex,
|
|
132
149
|
bytesToUtf8,
|
|
133
|
-
concatBytes,
|
|
134
150
|
createBuffer,
|
|
135
151
|
hexToBytes,
|
|
136
152
|
utf8ToBytes,
|
|
@@ -144,7 +160,7 @@ import {
|
|
|
144
160
|
} from "../Crypto.js";
|
|
145
161
|
import { eqArrayNumber } from "../Eq.js";
|
|
146
162
|
import { computeBalancedBuckets } from "../Number.js";
|
|
147
|
-
import { objectToEntries
|
|
163
|
+
import { objectToEntries } from "../Object.js";
|
|
148
164
|
import { err, ok, Result } from "../Result.js";
|
|
149
165
|
import { SqliteValue } from "../Sqlite.js";
|
|
150
166
|
import {
|
|
@@ -157,11 +173,12 @@ import {
|
|
|
157
173
|
NanoId,
|
|
158
174
|
NonNegativeInt,
|
|
159
175
|
Number,
|
|
176
|
+
object,
|
|
160
177
|
PositiveInt,
|
|
178
|
+
record,
|
|
161
179
|
} from "../Type.js";
|
|
162
180
|
import { Brand, Predicate } from "../Types.js";
|
|
163
181
|
import {
|
|
164
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
165
182
|
Owner,
|
|
166
183
|
OwnerId,
|
|
167
184
|
OwnerWithWriteAccess,
|
|
@@ -203,6 +220,14 @@ export const ProtocolErrorCode = {
|
|
|
203
220
|
type ProtocolErrorCode =
|
|
204
221
|
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
205
222
|
|
|
223
|
+
export const WriteKeyMode = {
|
|
224
|
+
None: 0,
|
|
225
|
+
Single: 1,
|
|
226
|
+
Rotation: 2,
|
|
227
|
+
} as const;
|
|
228
|
+
|
|
229
|
+
type WriteKeyMode = (typeof WriteKeyMode)[keyof typeof WriteKeyMode];
|
|
230
|
+
|
|
206
231
|
/**
|
|
207
232
|
* Evolu Protocol Storage
|
|
208
233
|
*
|
|
@@ -247,17 +272,15 @@ export interface Storage {
|
|
|
247
272
|
callback: (timestamp: BinaryTimestamp, index: NonNegativeInt) => boolean,
|
|
248
273
|
) => void;
|
|
249
274
|
|
|
250
|
-
/**
|
|
251
|
-
* Authorizes the initiator's {@link WriteKey} for the given
|
|
252
|
-
* {@link BinaryOwnerId}.
|
|
253
|
-
*
|
|
254
|
-
* For a client that does not expect foreign writes, return `false`.
|
|
255
|
-
*/
|
|
275
|
+
/** Validates the {@link WriteKey} for the given {@link Owner}. */
|
|
256
276
|
readonly validateWriteKey: (
|
|
257
277
|
ownerId: BinaryOwnerId,
|
|
258
278
|
writeKey: WriteKey,
|
|
259
279
|
) => boolean;
|
|
260
280
|
|
|
281
|
+
/** Sets the {@link WriteKey} for the given {@link Owner}. */
|
|
282
|
+
readonly setWriteKey: (ownerId: BinaryOwnerId, writeKey: WriteKey) => boolean;
|
|
283
|
+
|
|
261
284
|
/** Write encrypted {@link CrdtMessage}s to storage. */
|
|
262
285
|
readonly writeMessages: (
|
|
263
286
|
ownerId: BinaryOwnerId,
|
|
@@ -269,6 +292,9 @@ export interface Storage {
|
|
|
269
292
|
ownerId: BinaryOwnerId,
|
|
270
293
|
timestamp: BinaryTimestamp,
|
|
271
294
|
) => EncryptedDbChange | null;
|
|
295
|
+
|
|
296
|
+
/** Delete all data for the given {@link Owner}. */
|
|
297
|
+
readonly deleteOwner: (ownerId: BinaryOwnerId) => boolean;
|
|
272
298
|
}
|
|
273
299
|
|
|
274
300
|
export interface StorageDep {
|
|
@@ -293,15 +319,23 @@ export interface CrdtMessage {
|
|
|
293
319
|
readonly change: DbChange;
|
|
294
320
|
}
|
|
295
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
324
|
+
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
325
|
+
*/
|
|
326
|
+
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
327
|
+
export type Base64Url256 = typeof Base64Url256.Type;
|
|
328
|
+
|
|
296
329
|
/**
|
|
297
330
|
* A DbChange is a change to a table row. Together with a unique
|
|
298
331
|
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
299
332
|
*/
|
|
300
|
-
export
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
333
|
+
export const DbChange = object({
|
|
334
|
+
table: Base64Url256,
|
|
335
|
+
id: Id,
|
|
336
|
+
values: record(Base64Url256, SqliteValue),
|
|
337
|
+
});
|
|
338
|
+
export type DbChange = typeof DbChange.Type;
|
|
305
339
|
|
|
306
340
|
export const RangeType = {
|
|
307
341
|
Fingerprint: 1,
|
|
@@ -425,6 +459,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
425
459
|
maxSize?: PositiveInt,
|
|
426
460
|
): ProtocolMessage => {
|
|
427
461
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
462
|
+
type: "initiator",
|
|
428
463
|
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
429
464
|
writeKey: owner.writeKey,
|
|
430
465
|
});
|
|
@@ -480,7 +515,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
480
515
|
export const createProtocolMessageForSync =
|
|
481
516
|
(deps: StorageDep) =>
|
|
482
517
|
(ownerId: OwnerId): ProtocolMessage | null => {
|
|
483
|
-
const buffer = createProtocolMessageBuffer(ownerId);
|
|
518
|
+
const buffer = createProtocolMessageBuffer(ownerId, { type: "initiator" });
|
|
484
519
|
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
485
520
|
|
|
486
521
|
const size = deps.storage.getSize(binaryOwnerId);
|
|
@@ -498,6 +533,19 @@ export const createProtocolMessageForSync =
|
|
|
498
533
|
return buffer.unwrap();
|
|
499
534
|
};
|
|
500
535
|
|
|
536
|
+
/** Creates a ProtocolMessage for {@link WriteKey} rotation. */
|
|
537
|
+
export const createProtocolMessageForWriteKeyRotation = (
|
|
538
|
+
ownerId: OwnerId,
|
|
539
|
+
currentWriteKey: WriteKey,
|
|
540
|
+
newWriteKey: WriteKey,
|
|
541
|
+
): ProtocolMessage => {
|
|
542
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
543
|
+
type: "initiator",
|
|
544
|
+
writeKey: [currentWriteKey, newWriteKey],
|
|
545
|
+
});
|
|
546
|
+
return buffer.unwrap();
|
|
547
|
+
};
|
|
548
|
+
|
|
501
549
|
/**
|
|
502
550
|
* Mutable builder for constructing {@link ProtocolMessage} respecting size
|
|
503
551
|
* limits.
|
|
@@ -525,16 +573,22 @@ export interface ProtocolMessageBuffer {
|
|
|
525
573
|
export const createProtocolMessageBuffer = (
|
|
526
574
|
ownerId: OwnerId,
|
|
527
575
|
options: {
|
|
528
|
-
readonly errorCode?: ProtocolErrorCode;
|
|
529
|
-
readonly writeKey?: WriteKey;
|
|
530
576
|
readonly totalMaxSize?: PositiveInt | undefined;
|
|
531
577
|
readonly rangesMaxSize?: PositiveInt | undefined;
|
|
532
578
|
readonly version?: NonNegativeInt;
|
|
533
|
-
}
|
|
579
|
+
} & (
|
|
580
|
+
| {
|
|
581
|
+
readonly type: "initiator";
|
|
582
|
+
/** Single key or [current, new] for rotation. */
|
|
583
|
+
readonly writeKey?: WriteKey | readonly [WriteKey, WriteKey];
|
|
584
|
+
}
|
|
585
|
+
| {
|
|
586
|
+
readonly type: "non-initiator";
|
|
587
|
+
readonly errorCode: ProtocolErrorCode;
|
|
588
|
+
}
|
|
589
|
+
),
|
|
534
590
|
): ProtocolMessageBuffer => {
|
|
535
591
|
const {
|
|
536
|
-
errorCode,
|
|
537
|
-
writeKey,
|
|
538
592
|
totalMaxSize = maxProtocolMessageSize,
|
|
539
593
|
rangesMaxSize = maxProtocolMessageRangesSize,
|
|
540
594
|
version = protocolVersion,
|
|
@@ -555,7 +609,21 @@ export const createProtocolMessageBuffer = (
|
|
|
555
609
|
|
|
556
610
|
encodeNonNegativeInt(buffers.header, version);
|
|
557
611
|
buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
|
|
558
|
-
|
|
612
|
+
|
|
613
|
+
if (options.type === "initiator") {
|
|
614
|
+
if (!options.writeKey) {
|
|
615
|
+
buffers.header.extend([WriteKeyMode.None]);
|
|
616
|
+
} else if (!Array.isArray(options.writeKey)) {
|
|
617
|
+
buffers.header.extend([WriteKeyMode.Single]);
|
|
618
|
+
buffers.header.extend(options.writeKey as WriteKey);
|
|
619
|
+
} else {
|
|
620
|
+
buffers.header.extend([WriteKeyMode.Rotation]);
|
|
621
|
+
buffers.header.extend(options.writeKey[0] as WriteKey); // current
|
|
622
|
+
buffers.header.extend(options.writeKey[1] as WriteKey); // new
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
buffers.header.extend([options.errorCode]);
|
|
626
|
+
}
|
|
559
627
|
|
|
560
628
|
let isLastRangeInfinite = false;
|
|
561
629
|
|
|
@@ -567,10 +635,7 @@ export const createProtocolMessageBuffer = (
|
|
|
567
635
|
const getHeaderAndMessagesSize = () =>
|
|
568
636
|
buffers.header.getLength() +
|
|
569
637
|
buffers.messages.timestamps.getLength() +
|
|
570
|
-
buffers.messages.dbChanges.getLength()
|
|
571
|
-
(buffers.messages.timestamps.getCount() > 0 && writeKey
|
|
572
|
-
? writeKeyLength
|
|
573
|
-
: 0);
|
|
638
|
+
buffers.messages.dbChanges.getLength();
|
|
574
639
|
|
|
575
640
|
const getRangesSize = () =>
|
|
576
641
|
buffers.ranges.timestamps.getCount() > 0
|
|
@@ -685,8 +750,6 @@ export const createProtocolMessageBuffer = (
|
|
|
685
750
|
|
|
686
751
|
buffers.messages.timestamps.append(buffers.header);
|
|
687
752
|
buffers.header.extend(buffers.messages.dbChanges.unwrap());
|
|
688
|
-
if (buffers.messages.timestamps.getCount() > 0 && writeKey)
|
|
689
|
-
buffers.header.extend(writeKey);
|
|
690
753
|
|
|
691
754
|
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
692
755
|
buffers.ranges.timestamps.append(buffers.header);
|
|
@@ -836,8 +899,6 @@ export const applyProtocolMessageAsClient =
|
|
|
836
899
|
});
|
|
837
900
|
}
|
|
838
901
|
|
|
839
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
840
|
-
|
|
841
902
|
const errorCode = input.shift() as ProtocolErrorCode;
|
|
842
903
|
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
843
904
|
switch (errorCode) {
|
|
@@ -864,6 +925,7 @@ export const applyProtocolMessageAsClient =
|
|
|
864
925
|
}
|
|
865
926
|
|
|
866
927
|
const messages = decodeMessages(input);
|
|
928
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
867
929
|
|
|
868
930
|
if (
|
|
869
931
|
isNonEmptyReadonlyArray(messages) &&
|
|
@@ -877,6 +939,7 @@ export const applyProtocolMessageAsClient =
|
|
|
877
939
|
if (writeKey == null) return ok(null);
|
|
878
940
|
|
|
879
941
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
942
|
+
type: "initiator",
|
|
880
943
|
writeKey,
|
|
881
944
|
totalMaxSize,
|
|
882
945
|
rangesMaxSize,
|
|
@@ -911,8 +974,7 @@ export const applyProtocolMessageAsRelay =
|
|
|
911
974
|
version = protocolVersion,
|
|
912
975
|
): Result<ProtocolMessage | null, ProtocolInvalidDataError> =>
|
|
913
976
|
tryDecodeProtocolData(inputMessage, (input) => {
|
|
914
|
-
const requestedVersion =
|
|
915
|
-
const ownerId = decodeOwnerId(input);
|
|
977
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
916
978
|
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
917
979
|
|
|
918
980
|
if (requestedVersion !== version) {
|
|
@@ -925,44 +987,89 @@ export const applyProtocolMessageAsRelay =
|
|
|
925
987
|
|
|
926
988
|
subscribe?.(ownerId);
|
|
927
989
|
|
|
928
|
-
const
|
|
990
|
+
const writeKeyMode = input.shift() as WriteKeyMode;
|
|
991
|
+
let writeKey: WriteKey | undefined;
|
|
992
|
+
let newWriteKey: WriteKey | undefined;
|
|
993
|
+
|
|
994
|
+
if (writeKeyMode !== WriteKeyMode.None) {
|
|
995
|
+
writeKey = input.shiftN(writeKeyLength) as WriteKey;
|
|
996
|
+
switch (writeKeyMode) {
|
|
997
|
+
case WriteKeyMode.Single:
|
|
998
|
+
break;
|
|
999
|
+
case WriteKeyMode.Rotation:
|
|
1000
|
+
newWriteKey = input.shiftN(writeKeyLength) as WriteKey;
|
|
1001
|
+
break;
|
|
1002
|
+
default:
|
|
1003
|
+
throw new ProtocolDecodeError(
|
|
1004
|
+
`Invalid WriteKeyMode: ${writeKeyMode}`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
929
1008
|
|
|
930
|
-
if (
|
|
931
|
-
const
|
|
932
|
-
|
|
1009
|
+
if (writeKey) {
|
|
1010
|
+
const isValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
1011
|
+
if (!isValid) {
|
|
1012
|
+
return ok(
|
|
1013
|
+
createProtocolMessageBuffer(ownerId, {
|
|
1014
|
+
type: "non-initiator",
|
|
1015
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1016
|
+
}).unwrap(),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
933
1019
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1020
|
+
if (newWriteKey) {
|
|
1021
|
+
const rotationSuccess = deps.storage.setWriteKey(
|
|
1022
|
+
binaryOwnerId,
|
|
1023
|
+
newWriteKey,
|
|
1024
|
+
);
|
|
1025
|
+
if (!rotationSuccess) {
|
|
1026
|
+
return ok(
|
|
1027
|
+
createProtocolMessageBuffer(ownerId, {
|
|
1028
|
+
type: "non-initiator",
|
|
1029
|
+
errorCode: ProtocolErrorCode.WriteError,
|
|
1030
|
+
}).unwrap(),
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const messages = decodeMessages(input);
|
|
938
1037
|
|
|
939
|
-
|
|
1038
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
1039
|
+
if (!writeKey)
|
|
940
1040
|
return ok(
|
|
941
1041
|
createProtocolMessageBuffer(ownerId, {
|
|
1042
|
+
type: "non-initiator",
|
|
942
1043
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
943
1044
|
}).unwrap(),
|
|
944
1045
|
);
|
|
945
1046
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1047
|
+
// Only broadcast if there's no ranges.
|
|
1048
|
+
if (broadcast && input.getLength() === 0) {
|
|
1049
|
+
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
1050
|
+
type: "non-initiator",
|
|
1051
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
1052
|
+
totalMaxSize,
|
|
1053
|
+
rangesMaxSize,
|
|
1054
|
+
version,
|
|
1055
|
+
});
|
|
1056
|
+
for (const message of messages) {
|
|
1057
|
+
broadcastBuffer.addMessage(message);
|
|
1058
|
+
}
|
|
1059
|
+
broadcast(ownerId, broadcastBuffer.unwrap());
|
|
955
1060
|
}
|
|
956
1061
|
|
|
957
1062
|
if (!deps.storage.writeMessages(binaryOwnerId, messages))
|
|
958
1063
|
return ok(
|
|
959
1064
|
createProtocolMessageBuffer(ownerId, {
|
|
1065
|
+
type: "non-initiator",
|
|
960
1066
|
errorCode: ProtocolErrorCode.WriteError,
|
|
961
1067
|
}).unwrap(),
|
|
962
1068
|
);
|
|
963
1069
|
}
|
|
964
1070
|
|
|
965
1071
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
1072
|
+
type: "non-initiator",
|
|
966
1073
|
errorCode: ProtocolErrorCode.NoError,
|
|
967
1074
|
totalMaxSize,
|
|
968
1075
|
rangesMaxSize,
|
|
@@ -983,18 +1090,19 @@ const tryDecodeProtocolData = <T, E>(
|
|
|
983
1090
|
try {
|
|
984
1091
|
return callback(createBuffer(data));
|
|
985
1092
|
} catch (error: unknown) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
throw error;
|
|
1093
|
+
return err<ProtocolInvalidDataError>({
|
|
1094
|
+
type: "ProtocolInvalidDataError",
|
|
1095
|
+
data,
|
|
1096
|
+
error,
|
|
1097
|
+
});
|
|
994
1098
|
}
|
|
995
1099
|
};
|
|
996
1100
|
|
|
997
1101
|
const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
1102
|
+
// This structure must never change across protocol versions. The version
|
|
1103
|
+
// and owner ID must always be the first two fields in every protocol message
|
|
1104
|
+
// to enable version negotiation and owner identification before any other
|
|
1105
|
+
// processing occurs.
|
|
998
1106
|
const version = decodeNonNegativeInt(input);
|
|
999
1107
|
const ownerId = decodeOwnerId(input);
|
|
1000
1108
|
return [version, ownerId];
|
|
@@ -1052,6 +1160,7 @@ const sync =
|
|
|
1052
1160
|
return ok(null);
|
|
1053
1161
|
}
|
|
1054
1162
|
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
1163
|
+
type: "non-initiator",
|
|
1055
1164
|
errorCode: ProtocolErrorCode.SyncError,
|
|
1056
1165
|
});
|
|
1057
1166
|
return ok(message.unwrap());
|
|
@@ -1434,13 +1543,6 @@ export const ownerIdToBinaryOwnerId = (ownerId: OwnerId): BinaryOwnerId =>
|
|
|
1434
1543
|
export const binaryOwnerIdToOwnerId = (binaryOwnerId: BinaryOwnerId): OwnerId =>
|
|
1435
1544
|
decodeOwnerId(createBuffer(binaryOwnerId));
|
|
1436
1545
|
|
|
1437
|
-
/**
|
|
1438
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
1439
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
1440
|
-
*/
|
|
1441
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
1442
|
-
export type Base64Url256 = typeof Base64Url256.Type;
|
|
1443
|
-
|
|
1444
1546
|
/**
|
|
1445
1547
|
* Union type for all variants of Base64Url strings with limited length. All
|
|
1446
1548
|
* these types use Base64Url alphabet and are < 256 characters.
|
|
@@ -1895,3 +1997,27 @@ export const decodeSqliteValue = (buffer: Buffer): SqliteValue => {
|
|
|
1895
1997
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1896
1998
|
}
|
|
1897
1999
|
};
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Decodes a ProtocolMessage into a readable JSON object for debugging.
|
|
2003
|
+
*
|
|
2004
|
+
* Note: This is a stub for future implementation. It should use:
|
|
2005
|
+
*
|
|
2006
|
+
* - DecodeVersionAndOwner
|
|
2007
|
+
* - DecodeError or decodeWriteKeys (depending on context)
|
|
2008
|
+
* - DecodeMessages
|
|
2009
|
+
* - DecodeRanges
|
|
2010
|
+
*
|
|
2011
|
+
* If you want to help, please contribute to this function.
|
|
2012
|
+
*/
|
|
2013
|
+
export const decodeProtocolMessageToJson = (
|
|
2014
|
+
_protocolMessage: ProtocolMessage,
|
|
2015
|
+
_isInitiator: boolean,
|
|
2016
|
+
): unknown => {
|
|
2017
|
+
// TODO: Implement using
|
|
2018
|
+
// - decodeVersionAndOwner
|
|
2019
|
+
// -- decodeError or decodeWriteKeys (should be refactored out),
|
|
2020
|
+
// -- decodeMessages, and decodeRanges.
|
|
2021
|
+
// This is a stub for PRs and community contributions.
|
|
2022
|
+
throw new Error("decodeProtocolMessageToJson is not implemented yet.");
|
|
2023
|
+
};
|