@syncular/core 0.59.0 → 0.60.1
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/canonical-json.d.ts +20 -9
- package/dist/canonical-json.js +108 -6
- package/dist/constants.d.ts +5 -5
- package/dist/constants.js +5 -5
- package/dist/frames.d.ts +6 -3
- package/dist/frames.js +10 -4
- package/dist/message.d.ts +54 -13
- package/dist/message.js +279 -127
- package/dist/render.js +34 -11
- package/package.json +1 -1
- package/src/canonical-json.ts +135 -6
- package/src/constants.ts +5 -5
- package/src/frames.ts +10 -4
- package/src/message.ts +389 -155
- package/src/render.ts +34 -11
package/src/message.ts
CHANGED
|
@@ -45,6 +45,9 @@ export interface ReqHeaderFrame {
|
|
|
45
45
|
schemaVersion: number;
|
|
46
46
|
/** Partition log identity; absent requests epoch acquisition. */
|
|
47
47
|
logEpoch?: string;
|
|
48
|
+
/** Identity of the sender's outbox sequence (§6.3.2, §7.1); absent for a
|
|
49
|
+
* sender without an outbox. Non-empty, at most 64 bytes. */
|
|
50
|
+
outboxId?: string;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface PushOperation {
|
|
@@ -60,6 +63,8 @@ export interface PushOperation {
|
|
|
60
63
|
export interface PushCommitFrame {
|
|
61
64
|
type: 'PUSH_COMMIT';
|
|
62
65
|
clientCommitId: string;
|
|
66
|
+
/** Outbox sequence number (§6.3.2, §7.1); `0` without an outbox. */
|
|
67
|
+
clientSeq: number;
|
|
63
68
|
operations: PushOperation[];
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -70,15 +75,26 @@ export interface PullHeaderFrame {
|
|
|
70
75
|
maxSnapshotPages: number;
|
|
71
76
|
/** Bitmask (§4.2): bit 0 inline rows, 1 external rows, 2 sqlite, 3 signed URLs. */
|
|
72
77
|
accept: number;
|
|
78
|
+
/** The replica cursor (§4.5); `-1` when no unit is live. */
|
|
79
|
+
cursor: number;
|
|
80
|
+
/** §4.1 registration digest: exactly 32 bytes. */
|
|
81
|
+
registrationDigest: Uint8Array;
|
|
82
|
+
/** The last processed `LIVE_END.echoDigest`: empty or 32 bytes (§4.2). */
|
|
83
|
+
echoDigest: Uint8Array;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
/** §4.3: one unit of a traveling registration list. */
|
|
87
|
+
export interface UnitFrame {
|
|
88
|
+
type: 'UNIT';
|
|
89
|
+
scopes: Record<string, string>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** §4.3: the progress of one catching-up unit. */
|
|
93
|
+
export interface ProgressFrame {
|
|
94
|
+
type: 'PROGRESS';
|
|
95
|
+
/** The unit id (§4.1): 32 lowercase hex characters. */
|
|
77
96
|
id: string;
|
|
78
|
-
|
|
79
|
-
scopes: ScopeMap;
|
|
80
|
-
/** Host-opaque JSON document, preserved verbatim. */
|
|
81
|
-
params?: string;
|
|
97
|
+
/** `-1`, the §4.7 pin while a resume token is held, or the unit cursor. */
|
|
82
98
|
cursor: number;
|
|
83
99
|
/** Opaque resume token JSON, round-tripped byte-for-byte (§4.7). */
|
|
84
100
|
bootstrapState?: string;
|
|
@@ -104,7 +120,8 @@ export type RequestFrame =
|
|
|
104
120
|
| RoundFailureFrame
|
|
105
121
|
| PushCommitFrame
|
|
106
122
|
| PullHeaderFrame
|
|
107
|
-
|
|
|
123
|
+
| UnitFrame
|
|
124
|
+
| ProgressFrame
|
|
108
125
|
| UnknownFrame;
|
|
109
126
|
|
|
110
127
|
export interface RespHeaderFrame {
|
|
@@ -173,21 +190,47 @@ export interface PushResultDetailsFrame {
|
|
|
173
190
|
entries: Array<{ opIndex: number; details: RejectionDetails }>;
|
|
174
191
|
}
|
|
175
192
|
|
|
193
|
+
/** §4.4: one (unit, table) pair the wildcard fence touched. */
|
|
194
|
+
export interface FencedRecord {
|
|
195
|
+
table: string;
|
|
196
|
+
status: 'active' | 'revoked';
|
|
197
|
+
/** Effective lists of the variables the fence added; empty when revoked. */
|
|
198
|
+
scopes: ScopeMap;
|
|
199
|
+
}
|
|
200
|
+
|
|
176
201
|
export interface SubStartFrame {
|
|
177
202
|
type: 'SUB_START';
|
|
203
|
+
/** The unit id (§4.1): 32 lowercase hex characters. */
|
|
178
204
|
id: string;
|
|
179
205
|
status: 'active' | 'revoked' | 'reset';
|
|
180
206
|
reasonCode: string;
|
|
181
|
-
effectiveScopes: ScopeMap;
|
|
182
207
|
bootstrap: boolean;
|
|
208
|
+
/** The unit's echo, in ascending table order (§4.4). */
|
|
209
|
+
fenced: FencedRecord[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface LiveStartFrame {
|
|
213
|
+
type: 'LIVE_START';
|
|
214
|
+
status: 'active' | 'reset' | 'unregistered';
|
|
215
|
+
reasonCode: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface LiveEndFrame {
|
|
219
|
+
type: 'LIVE_END';
|
|
220
|
+
/** Highest commitSeq the live section is complete through; `-1` for
|
|
221
|
+
* `reset` and `unregistered` (§4.4). */
|
|
222
|
+
throughSeq: number;
|
|
223
|
+
/** Echo digest of the request: empty or 32 bytes (§4.4). */
|
|
224
|
+
echoDigest: Uint8Array;
|
|
183
225
|
}
|
|
184
226
|
|
|
185
227
|
export interface CommitChange {
|
|
186
228
|
tableIndex: number;
|
|
187
229
|
rowId: string;
|
|
188
230
|
op: 'upsert' | 'delete';
|
|
189
|
-
/**
|
|
190
|
-
|
|
231
|
+
/** The row's `server_version` after the write, ≥ 1; a delete's is the
|
|
232
|
+
* version its tombstone records (§2.2, §4.5). */
|
|
233
|
+
rowVersion: number;
|
|
191
234
|
scopes: Record<string, string>;
|
|
192
235
|
/** Generated-row-codec bytes; present iff `op` is `upsert` (§4.5). */
|
|
193
236
|
row?: Uint8Array;
|
|
@@ -227,7 +270,7 @@ export interface SegmentInlineFrame {
|
|
|
227
270
|
export interface SubEndFrame {
|
|
228
271
|
type: 'SUB_END';
|
|
229
272
|
nextCursor: number;
|
|
230
|
-
/** Present iff the bootstrap is incomplete (§4.4). */
|
|
273
|
+
/** Present iff the unit's bootstrap is incomplete (§4.4). */
|
|
231
274
|
bootstrapState?: string;
|
|
232
275
|
}
|
|
233
276
|
|
|
@@ -251,6 +294,8 @@ export type ResponseFrame =
|
|
|
251
294
|
| SegmentRefFrame
|
|
252
295
|
| SegmentInlineFrame
|
|
253
296
|
| SubEndFrame
|
|
297
|
+
| LiveStartFrame
|
|
298
|
+
| LiveEndFrame
|
|
254
299
|
| ErrorFrame
|
|
255
300
|
| UnknownFrame;
|
|
256
301
|
|
|
@@ -315,6 +360,62 @@ function validateRoundFailureCode(
|
|
|
315
360
|
}
|
|
316
361
|
}
|
|
317
362
|
|
|
363
|
+
/** §1.5, §1.7: a present `outboxId` is non-empty and at most 64 bytes. */
|
|
364
|
+
function checkOutboxId(outboxId: string, fail: (message: string) => never) {
|
|
365
|
+
if (outboxId.length === 0 || utf8Encode(outboxId).length > 64) {
|
|
366
|
+
fail('REQ_HEADER.outboxId must be non-empty and at most 64 bytes');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** §4.3, §4.4: a unit id is 32 lowercase hex characters. */
|
|
371
|
+
function checkUnitId(
|
|
372
|
+
id: string,
|
|
373
|
+
frame: 'PROGRESS' | 'SUB_START',
|
|
374
|
+
fail: (message: string) => never,
|
|
375
|
+
) {
|
|
376
|
+
if (!/^[0-9a-f]{32}$/.test(id)) {
|
|
377
|
+
fail(`${frame}.id must be 32 lowercase hex characters`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** §4.2, §4.4: a digest is 32 bytes; an echo digest may also be empty. */
|
|
382
|
+
function checkDigest(
|
|
383
|
+
digest: Uint8Array,
|
|
384
|
+
field:
|
|
385
|
+
| 'PULL_HEADER.registrationDigest'
|
|
386
|
+
| 'PULL_HEADER.echoDigest'
|
|
387
|
+
| 'LIVE_END.echoDigest',
|
|
388
|
+
fail: (message: string) => never,
|
|
389
|
+
) {
|
|
390
|
+
const emptyAllowed = field !== 'PULL_HEADER.registrationDigest';
|
|
391
|
+
if (digest.length !== 32 && !(emptyAllowed && digest.length === 0)) {
|
|
392
|
+
fail(`${field} must be ${emptyAllowed ? '0 or ' : ''}32 bytes`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** §4.2, §4.3: a cursor is `-1` or a commitSeq. */
|
|
397
|
+
function checkCursor(
|
|
398
|
+
cursor: number,
|
|
399
|
+
field: 'PULL_HEADER.cursor' | 'PROGRESS.cursor',
|
|
400
|
+
fail: (message: string) => never,
|
|
401
|
+
) {
|
|
402
|
+
if (cursor < -1) fail(`${field} must be >= -1`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** §4.4: fenced records in strictly ascending code-unit order of `table`. */
|
|
406
|
+
function checkFencedOrder(
|
|
407
|
+
fenced: readonly FencedRecord[],
|
|
408
|
+
fail: (message: string) => never,
|
|
409
|
+
) {
|
|
410
|
+
for (let i = 1; i < fenced.length; i++) {
|
|
411
|
+
if (
|
|
412
|
+
(fenced[i - 1] as FencedRecord).table >= (fenced[i] as FencedRecord).table
|
|
413
|
+
) {
|
|
414
|
+
fail('SUB_START.fenced tables must be unique and in ascending order');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
318
419
|
// ---------------------------------------------------------------------------
|
|
319
420
|
// Frame payload decoders (each consumes exactly the frame payload)
|
|
320
421
|
// ---------------------------------------------------------------------------
|
|
@@ -329,11 +430,14 @@ function decodeReqHeader(r: ByteReader): ReqHeaderFrame {
|
|
|
329
430
|
if (value.length === 0) invalid('REQ_HEADER.logEpoch must be non-empty');
|
|
330
431
|
return value;
|
|
331
432
|
});
|
|
433
|
+
const outboxId = r.opt(() => r.str());
|
|
434
|
+
if (outboxId !== undefined) checkOutboxId(outboxId, invalid);
|
|
332
435
|
return definedObject({
|
|
333
436
|
type: 'REQ_HEADER',
|
|
334
437
|
clientId,
|
|
335
438
|
schemaVersion,
|
|
336
439
|
logEpoch: logEpoch,
|
|
440
|
+
outboxId: outboxId,
|
|
337
441
|
});
|
|
338
442
|
}
|
|
339
443
|
|
|
@@ -348,6 +452,8 @@ function decodePushCommit(r: ByteReader): PushCommitFrame {
|
|
|
348
452
|
if (clientCommitId.length === 0) {
|
|
349
453
|
invalid('PUSH_COMMIT.clientCommitId must be non-empty');
|
|
350
454
|
}
|
|
455
|
+
const clientSeq = r.i64();
|
|
456
|
+
if (clientSeq < 0) invalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
351
457
|
const operationCount = r.u32();
|
|
352
458
|
if (operationCount === 0) {
|
|
353
459
|
throw new DecodeError(
|
|
@@ -382,7 +488,7 @@ function decodePushCommit(r: ByteReader): PushCommitFrame {
|
|
|
382
488
|
}),
|
|
383
489
|
);
|
|
384
490
|
}
|
|
385
|
-
return { type: 'PUSH_COMMIT', clientCommitId, operations };
|
|
491
|
+
return { type: 'PUSH_COMMIT', clientCommitId, clientSeq, operations };
|
|
386
492
|
}
|
|
387
493
|
|
|
388
494
|
function decodePullHeader(r: ByteReader): PullHeaderFrame {
|
|
@@ -393,30 +499,33 @@ function decodePullHeader(r: ByteReader): PullHeaderFrame {
|
|
|
393
499
|
if ((accept & 0xf0) !== 0) {
|
|
394
500
|
invalid('PULL_HEADER.accept bits 4-7 must be zero');
|
|
395
501
|
}
|
|
502
|
+
const cursor = r.i64();
|
|
503
|
+
checkCursor(cursor, 'PULL_HEADER.cursor', invalid);
|
|
504
|
+
const registrationDigest = r.bytes();
|
|
505
|
+
checkDigest(registrationDigest, 'PULL_HEADER.registrationDigest', invalid);
|
|
506
|
+
const echoDigest = r.bytes();
|
|
507
|
+
checkDigest(echoDigest, 'PULL_HEADER.echoDigest', invalid);
|
|
396
508
|
return {
|
|
397
509
|
type: 'PULL_HEADER',
|
|
398
510
|
limitCommits,
|
|
399
511
|
limitSnapshotRows,
|
|
400
512
|
maxSnapshotPages,
|
|
401
513
|
accept,
|
|
514
|
+
cursor,
|
|
515
|
+
registrationDigest,
|
|
516
|
+
echoDigest,
|
|
402
517
|
};
|
|
403
518
|
}
|
|
404
519
|
|
|
405
|
-
function
|
|
520
|
+
function decodeProgress(r: ByteReader): ProgressFrame {
|
|
406
521
|
const id = r.str();
|
|
407
|
-
|
|
408
|
-
const scopes = readStringListMap(r);
|
|
409
|
-
const params = r.opt(() => readJson(r, 'SUBSCRIPTION.params'));
|
|
522
|
+
checkUnitId(id, 'PROGRESS', invalid);
|
|
410
523
|
const cursor = r.i64();
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
);
|
|
524
|
+
checkCursor(cursor, 'PROGRESS.cursor', invalid);
|
|
525
|
+
const bootstrapState = r.opt(() => readJson(r, 'PROGRESS.bootstrapState'));
|
|
414
526
|
return definedObject({
|
|
415
|
-
type: '
|
|
527
|
+
type: 'PROGRESS',
|
|
416
528
|
id,
|
|
417
|
-
table,
|
|
418
|
-
scopes,
|
|
419
|
-
params: params,
|
|
420
529
|
cursor,
|
|
421
530
|
bootstrapState: bootstrapState,
|
|
422
531
|
});
|
|
@@ -532,6 +641,7 @@ function decodePushResultDetails(r: ByteReader): PushResultDetailsFrame {
|
|
|
532
641
|
|
|
533
642
|
function decodeSubStart(r: ByteReader): SubStartFrame {
|
|
534
643
|
const id = r.str();
|
|
644
|
+
checkUnitId(id, 'SUB_START', invalid);
|
|
535
645
|
const statusByte = r.u8();
|
|
536
646
|
let status: 'active' | 'revoked' | 'reset';
|
|
537
647
|
if (statusByte === 1) status = 'active';
|
|
@@ -539,16 +649,40 @@ function decodeSubStart(r: ByteReader): SubStartFrame {
|
|
|
539
649
|
else if (statusByte === 3) status = 'reset';
|
|
540
650
|
else invalid(`invalid SUB_START status byte ${statusByte}`);
|
|
541
651
|
const reasonCode = r.str();
|
|
542
|
-
const effectiveScopes = readStringListMap(r);
|
|
543
652
|
const bootstrap = r.bool();
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
653
|
+
const fencedCount = r.u32();
|
|
654
|
+
const fenced: FencedRecord[] = [];
|
|
655
|
+
for (let i = 0; i < fencedCount; i++) {
|
|
656
|
+
const table = r.str();
|
|
657
|
+
const fencedStatus = r.u8();
|
|
658
|
+
if (fencedStatus !== 1 && fencedStatus !== 2) {
|
|
659
|
+
invalid('invalid fenced record status byte');
|
|
660
|
+
}
|
|
661
|
+
fenced.push({
|
|
662
|
+
table,
|
|
663
|
+
status: fencedStatus === 1 ? 'active' : 'revoked',
|
|
664
|
+
scopes: readStringListMap(r),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
checkFencedOrder(fenced, invalid);
|
|
668
|
+
return { type: 'SUB_START', id, status, reasonCode, bootstrap, fenced };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function decodeLiveStart(r: ByteReader): LiveStartFrame {
|
|
672
|
+
const statusByte = r.u8();
|
|
673
|
+
let status: LiveStartFrame['status'];
|
|
674
|
+
if (statusByte === 1) status = 'active';
|
|
675
|
+
else if (statusByte === 3) status = 'reset';
|
|
676
|
+
else if (statusByte === 4) status = 'unregistered';
|
|
677
|
+
else invalid('invalid LIVE_START status byte');
|
|
678
|
+
return { type: 'LIVE_START', status, reasonCode: r.str() };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function decodeLiveEnd(r: ByteReader): LiveEndFrame {
|
|
682
|
+
const throughSeq = r.i64();
|
|
683
|
+
const echoDigest = r.bytes();
|
|
684
|
+
checkDigest(echoDigest, 'LIVE_END.echoDigest', invalid);
|
|
685
|
+
return { type: 'LIVE_END', throughSeq, echoDigest };
|
|
552
686
|
}
|
|
553
687
|
|
|
554
688
|
function decodeCommit(r: ByteReader): CommitFrame {
|
|
@@ -573,24 +707,15 @@ function decodeCommit(r: ByteReader): CommitFrame {
|
|
|
573
707
|
if (opByte === 1) op = 'upsert';
|
|
574
708
|
else if (opByte === 2) op = 'delete';
|
|
575
709
|
else invalid(`invalid change op byte ${opByte}`);
|
|
576
|
-
const rowVersion = r.
|
|
710
|
+
const rowVersion = r.i64();
|
|
711
|
+
if (rowVersion < 1) invalid('change rowVersion must be at least 1');
|
|
577
712
|
const scopes = readStringMap(r);
|
|
578
713
|
const row = r.opt(() => r.bytes());
|
|
579
|
-
if (op === 'upsert'
|
|
580
|
-
invalid('
|
|
581
|
-
}
|
|
582
|
-
if (op === 'delete' && (rowVersion !== undefined || row !== undefined)) {
|
|
583
|
-
invalid('delete change must not carry rowVersion or row');
|
|
714
|
+
if ((op === 'upsert') !== (row !== undefined)) {
|
|
715
|
+
invalid('change row must be present iff op is upsert');
|
|
584
716
|
}
|
|
585
717
|
changes.push(
|
|
586
|
-
definedObject({
|
|
587
|
-
tableIndex,
|
|
588
|
-
rowId,
|
|
589
|
-
op,
|
|
590
|
-
rowVersion: rowVersion,
|
|
591
|
-
scopes,
|
|
592
|
-
row: row,
|
|
593
|
-
}),
|
|
718
|
+
definedObject({ tableIndex, rowId, op, rowVersion, scopes, row }),
|
|
594
719
|
);
|
|
595
720
|
}
|
|
596
721
|
return { type: 'COMMIT', commitSeq, createdAtMs, actorId, tables, changes };
|
|
@@ -686,8 +811,11 @@ function decodeRequestFrame(
|
|
|
686
811
|
case FrameType.PULL_HEADER:
|
|
687
812
|
frame = decodePullHeader(r);
|
|
688
813
|
break;
|
|
689
|
-
case FrameType.
|
|
690
|
-
frame =
|
|
814
|
+
case FrameType.UNIT:
|
|
815
|
+
frame = { type: 'UNIT', scopes: readStringMap(r) };
|
|
816
|
+
break;
|
|
817
|
+
case FrameType.PROGRESS:
|
|
818
|
+
frame = decodeProgress(r);
|
|
691
819
|
break;
|
|
692
820
|
default:
|
|
693
821
|
if (RESPONSE_FRAME_TYPES.has(frameType)) {
|
|
@@ -735,6 +863,12 @@ function decodeResponseFrame(
|
|
|
735
863
|
case FrameType.SUB_END:
|
|
736
864
|
frame = decodeSubEnd(r);
|
|
737
865
|
break;
|
|
866
|
+
case FrameType.LIVE_START:
|
|
867
|
+
frame = decodeLiveStart(r);
|
|
868
|
+
break;
|
|
869
|
+
case FrameType.LIVE_END:
|
|
870
|
+
frame = decodeLiveEnd(r);
|
|
871
|
+
break;
|
|
738
872
|
case FrameType.ERROR:
|
|
739
873
|
frame = decodeErrorFrame(r);
|
|
740
874
|
break;
|
|
@@ -761,6 +895,7 @@ function validateRequestSequence(frames: readonly RequestFrame[]): void {
|
|
|
761
895
|
}
|
|
762
896
|
let pushCount = 0;
|
|
763
897
|
let hasPull = false;
|
|
898
|
+
let hasProgress = false;
|
|
764
899
|
let index = 1;
|
|
765
900
|
let sawRoundFailure = false;
|
|
766
901
|
for (const frame of frames.slice(1)) {
|
|
@@ -784,8 +919,13 @@ function validateRequestSequence(frames: readonly RequestFrame[]): void {
|
|
|
784
919
|
if (hasPull) invalid('duplicate PULL_HEADER frame');
|
|
785
920
|
hasPull = true;
|
|
786
921
|
break;
|
|
787
|
-
case '
|
|
788
|
-
if (!hasPull) invalid('
|
|
922
|
+
case 'UNIT':
|
|
923
|
+
if (!hasPull) invalid('UNIT frame without a PULL_HEADER');
|
|
924
|
+
if (hasProgress) invalid('UNIT frame after a PROGRESS frame');
|
|
925
|
+
break;
|
|
926
|
+
case 'PROGRESS':
|
|
927
|
+
if (!hasPull) invalid('PROGRESS frame without a PULL_HEADER');
|
|
928
|
+
hasProgress = true;
|
|
789
929
|
break;
|
|
790
930
|
case 'UNKNOWN':
|
|
791
931
|
break;
|
|
@@ -802,12 +942,12 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
802
942
|
if (first === undefined || first.type !== 'RESP_HEADER') {
|
|
803
943
|
invalid('response must start with a RESP_HEADER frame');
|
|
804
944
|
}
|
|
805
|
-
|
|
806
|
-
let
|
|
945
|
+
// §1.6 sections: unit sections, then at most one live section.
|
|
946
|
+
let section: 'none' | 'unit' | 'live' = 'none';
|
|
947
|
+
let sawUnitSection = false;
|
|
948
|
+
let sawLive = false;
|
|
807
949
|
let sawLease = false;
|
|
808
950
|
let sawBody = false;
|
|
809
|
-
let subscriptionHasCommits = false;
|
|
810
|
-
let subscriptionHasSegments = false;
|
|
811
951
|
let errorSeen = false;
|
|
812
952
|
let lastPushResult: PushResultFrame | undefined;
|
|
813
953
|
let lastPushResultHasDetails = false;
|
|
@@ -818,21 +958,22 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
818
958
|
invalid('duplicate RESP_HEADER frame');
|
|
819
959
|
break;
|
|
820
960
|
case 'LEASE':
|
|
821
|
-
// §7.3.2: at most one LEASE, immediately after RESP_HEADER
|
|
822
|
-
// any PUSH_RESULT / subscription section / ERROR).
|
|
961
|
+
// §7.3.2: at most one LEASE, immediately after RESP_HEADER.
|
|
823
962
|
if (sawLease) invalid('duplicate LEASE frame');
|
|
824
963
|
if (sawBody) invalid('LEASE frame must immediately follow RESP_HEADER');
|
|
825
964
|
sawLease = true;
|
|
826
965
|
break;
|
|
827
966
|
case 'PUSH_RESULT':
|
|
828
|
-
if (
|
|
967
|
+
if (sawUnitSection || sawLive) {
|
|
968
|
+
invalid('PUSH_RESULT frame after a unit or live section');
|
|
969
|
+
}
|
|
829
970
|
lastPushResult = frame;
|
|
830
971
|
lastPushResultHasDetails = false;
|
|
831
972
|
sawBody = true;
|
|
832
973
|
break;
|
|
833
974
|
case 'PUSH_RESULT_DETAILS': {
|
|
834
|
-
if (
|
|
835
|
-
invalid('PUSH_RESULT_DETAILS frame after
|
|
975
|
+
if (sawUnitSection || sawLive) {
|
|
976
|
+
invalid('PUSH_RESULT_DETAILS frame after a unit or live section');
|
|
836
977
|
}
|
|
837
978
|
if (lastPushResult === undefined) {
|
|
838
979
|
invalid('PUSH_RESULT_DETAILS without a preceding PUSH_RESULT');
|
|
@@ -861,36 +1002,40 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
861
1002
|
break;
|
|
862
1003
|
}
|
|
863
1004
|
case 'SUB_START':
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
1005
|
+
if (section === 'unit') invalid('nested SUB_START frame');
|
|
1006
|
+
if (sawLive)
|
|
1007
|
+
invalid('SUB_START frame inside or after the live section');
|
|
1008
|
+
section = 'unit';
|
|
1009
|
+
sawUnitSection = true;
|
|
867
1010
|
sawBody = true;
|
|
868
1011
|
lastPushResult = undefined;
|
|
869
|
-
subscriptionHasCommits = false;
|
|
870
|
-
subscriptionHasSegments = false;
|
|
871
|
-
break;
|
|
872
|
-
case 'COMMIT':
|
|
873
|
-
if (!inSubscription) invalid('COMMIT frame outside a subscription');
|
|
874
|
-
if (subscriptionHasSegments) {
|
|
875
|
-
invalid(
|
|
876
|
-
'COMMIT and segment frames must not both appear for one subscription',
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
subscriptionHasCommits = true;
|
|
880
1012
|
break;
|
|
881
1013
|
case 'SEGMENT_REF':
|
|
882
1014
|
case 'SEGMENT_INLINE':
|
|
883
|
-
if (
|
|
884
|
-
if (subscriptionHasCommits) {
|
|
885
|
-
invalid(
|
|
886
|
-
'COMMIT and segment frames must not both appear for one subscription',
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
subscriptionHasSegments = true;
|
|
1015
|
+
if (section !== 'unit') invalid('segment frame outside a unit section');
|
|
890
1016
|
break;
|
|
891
1017
|
case 'SUB_END':
|
|
892
|
-
if (
|
|
893
|
-
|
|
1018
|
+
if (section !== 'unit')
|
|
1019
|
+
invalid('SUB_END frame without an open SUB_START');
|
|
1020
|
+
section = 'none';
|
|
1021
|
+
break;
|
|
1022
|
+
case 'LIVE_START':
|
|
1023
|
+
if (section === 'unit')
|
|
1024
|
+
invalid('LIVE_START frame inside a unit section');
|
|
1025
|
+
if (sawLive) invalid('second LIVE_START frame');
|
|
1026
|
+
section = 'live';
|
|
1027
|
+
sawLive = true;
|
|
1028
|
+
sawBody = true;
|
|
1029
|
+
lastPushResult = undefined;
|
|
1030
|
+
break;
|
|
1031
|
+
case 'COMMIT':
|
|
1032
|
+
if (section !== 'live')
|
|
1033
|
+
invalid('COMMIT frame outside the live section');
|
|
1034
|
+
break;
|
|
1035
|
+
case 'LIVE_END':
|
|
1036
|
+
if (section !== 'live')
|
|
1037
|
+
invalid('LIVE_END frame without an open LIVE_START');
|
|
1038
|
+
section = 'none';
|
|
894
1039
|
break;
|
|
895
1040
|
case 'ERROR':
|
|
896
1041
|
errorSeen = true;
|
|
@@ -903,16 +1048,11 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
903
1048
|
break;
|
|
904
1049
|
}
|
|
905
1050
|
}
|
|
906
|
-
if (
|
|
907
|
-
invalid('
|
|
1051
|
+
if (section !== 'none' && !errorSeen) {
|
|
1052
|
+
invalid('unit or live section not terminated');
|
|
908
1053
|
}
|
|
909
1054
|
}
|
|
910
1055
|
|
|
911
|
-
export function validateFrameSequence(message: SyncMessage): void {
|
|
912
|
-
if (message.msgKind === 'request') validateRequestSequence(message.frames);
|
|
913
|
-
else validateResponseSequence(message.frames);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
1056
|
// ---------------------------------------------------------------------------
|
|
917
1057
|
// Frame payload encoders
|
|
918
1058
|
// ---------------------------------------------------------------------------
|
|
@@ -920,17 +1060,30 @@ export function validateFrameSequence(message: SyncMessage): void {
|
|
|
920
1060
|
const OP_BYTES = { upsert: 1, delete: 2 } as const;
|
|
921
1061
|
const PUSH_STATUS_BYTES = { applied: 1, cached: 2, rejected: 3 } as const;
|
|
922
1062
|
const SUB_STATUS_BYTES = { active: 1, revoked: 2, reset: 3 } as const;
|
|
1063
|
+
const LIVE_STATUS_BYTES = { active: 1, reset: 3, unregistered: 4 } as const;
|
|
923
1064
|
const MEDIA_TYPE_BYTES = { rows: 1, sqlite: 2 } as const;
|
|
924
1065
|
|
|
925
1066
|
function encodeInvalid(message: string): never {
|
|
926
1067
|
throw new Error(message);
|
|
927
1068
|
}
|
|
928
1069
|
|
|
929
|
-
function
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1070
|
+
function encodeUnknown(frame: UnknownFrame, w: ByteWriter): number {
|
|
1071
|
+
if (
|
|
1072
|
+
!Number.isInteger(frame.frameType) ||
|
|
1073
|
+
frame.frameType < 0 ||
|
|
1074
|
+
frame.frameType > 0xff ||
|
|
1075
|
+
KNOWN_FRAME_TYPES.has(frame.frameType)
|
|
1076
|
+
) {
|
|
1077
|
+
encodeInvalid(
|
|
1078
|
+
`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
w.raw(frame.payload);
|
|
1082
|
+
return frame.frameType;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Writes the payload; returns the frame type byte. */
|
|
1086
|
+
function encodeRequestFrame(frame: RequestFrame, w: ByteWriter): number {
|
|
934
1087
|
switch (frame.type) {
|
|
935
1088
|
case 'REQ_HEADER': {
|
|
936
1089
|
if (frame.clientId.length === 0) {
|
|
@@ -945,6 +1098,10 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
945
1098
|
encodeInvalid('REQ_HEADER.logEpoch must be non-empty');
|
|
946
1099
|
}
|
|
947
1100
|
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
1101
|
+
if (frame.outboxId !== undefined) {
|
|
1102
|
+
checkOutboxId(frame.outboxId, encodeInvalid);
|
|
1103
|
+
}
|
|
1104
|
+
w.opt(frame.outboxId, (v) => w.str(v));
|
|
948
1105
|
break;
|
|
949
1106
|
}
|
|
950
1107
|
case 'ROUND_FAILURE': {
|
|
@@ -959,7 +1116,11 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
959
1116
|
if (frame.operations.length === 0) {
|
|
960
1117
|
encodeInvalid('PUSH_COMMIT must carry at least one operation');
|
|
961
1118
|
}
|
|
1119
|
+
if (frame.clientSeq < 0) {
|
|
1120
|
+
encodeInvalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
1121
|
+
}
|
|
962
1122
|
w.str(frame.clientCommitId);
|
|
1123
|
+
w.i64(frame.clientSeq);
|
|
963
1124
|
w.u32(frame.operations.length);
|
|
964
1125
|
for (const operation of frame.operations) {
|
|
965
1126
|
if (operation.op === 'upsert' && operation.payload === undefined) {
|
|
@@ -983,18 +1144,40 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
983
1144
|
w.i32(frame.limitCommits);
|
|
984
1145
|
w.i32(frame.limitSnapshotRows);
|
|
985
1146
|
w.i32(frame.maxSnapshotPages);
|
|
1147
|
+
checkCursor(frame.cursor, 'PULL_HEADER.cursor', encodeInvalid);
|
|
1148
|
+
checkDigest(
|
|
1149
|
+
frame.registrationDigest,
|
|
1150
|
+
'PULL_HEADER.registrationDigest',
|
|
1151
|
+
encodeInvalid,
|
|
1152
|
+
);
|
|
1153
|
+
checkDigest(frame.echoDigest, 'PULL_HEADER.echoDigest', encodeInvalid);
|
|
986
1154
|
w.u8(frame.accept);
|
|
1155
|
+
w.i64(frame.cursor);
|
|
1156
|
+
w.bytes(frame.registrationDigest);
|
|
1157
|
+
w.bytes(frame.echoDigest);
|
|
987
1158
|
break;
|
|
988
1159
|
}
|
|
989
|
-
case '
|
|
1160
|
+
case 'UNIT': {
|
|
1161
|
+
writeStringMap(w, frame.scopes);
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
case 'PROGRESS': {
|
|
1165
|
+
checkUnitId(frame.id, 'PROGRESS', encodeInvalid);
|
|
1166
|
+
checkCursor(frame.cursor, 'PROGRESS.cursor', encodeInvalid);
|
|
990
1167
|
w.str(frame.id);
|
|
991
|
-
w.str(frame.table);
|
|
992
|
-
writeStringListMap(w, frame.scopes);
|
|
993
|
-
w.opt(frame.params, (v) => w.str(v));
|
|
994
1168
|
w.i64(frame.cursor);
|
|
995
1169
|
w.opt(frame.bootstrapState, (v) => w.str(v));
|
|
996
1170
|
break;
|
|
997
1171
|
}
|
|
1172
|
+
case 'UNKNOWN':
|
|
1173
|
+
return encodeUnknown(frame, w);
|
|
1174
|
+
}
|
|
1175
|
+
return FrameType[frame.type];
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/** Writes the payload; returns the frame type byte. */
|
|
1179
|
+
function encodeResponseFrame(frame: ResponseFrame, w: ByteWriter): number {
|
|
1180
|
+
switch (frame.type) {
|
|
998
1181
|
case 'RESP_HEADER': {
|
|
999
1182
|
w.opt(frame.requiredSchemaVersion, (v) => w.i32(v));
|
|
1000
1183
|
w.opt(frame.latestSchemaVersion, (v) => w.i32(v));
|
|
@@ -1068,11 +1251,29 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
1068
1251
|
break;
|
|
1069
1252
|
}
|
|
1070
1253
|
case 'SUB_START': {
|
|
1254
|
+
checkUnitId(frame.id, 'SUB_START', encodeInvalid);
|
|
1255
|
+
checkFencedOrder(frame.fenced, encodeInvalid);
|
|
1071
1256
|
w.str(frame.id);
|
|
1072
1257
|
w.u8(SUB_STATUS_BYTES[frame.status]);
|
|
1073
1258
|
w.str(frame.reasonCode);
|
|
1074
|
-
writeStringListMap(w, frame.effectiveScopes);
|
|
1075
1259
|
w.bool(frame.bootstrap);
|
|
1260
|
+
w.u32(frame.fenced.length);
|
|
1261
|
+
for (const record of frame.fenced) {
|
|
1262
|
+
w.str(record.table);
|
|
1263
|
+
w.u8(SUB_STATUS_BYTES[record.status]);
|
|
1264
|
+
writeStringListMap(w, record.scopes);
|
|
1265
|
+
}
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case 'LIVE_START': {
|
|
1269
|
+
w.u8(LIVE_STATUS_BYTES[frame.status]);
|
|
1270
|
+
w.str(frame.reasonCode);
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
case 'LIVE_END': {
|
|
1274
|
+
checkDigest(frame.echoDigest, 'LIVE_END.echoDigest', encodeInvalid);
|
|
1275
|
+
w.i64(frame.throughSeq);
|
|
1276
|
+
w.bytes(frame.echoDigest);
|
|
1076
1277
|
break;
|
|
1077
1278
|
}
|
|
1078
1279
|
case 'COMMIT': {
|
|
@@ -1086,22 +1287,16 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
1086
1287
|
if (change.tableIndex >= frame.tables.length) {
|
|
1087
1288
|
encodeInvalid(`change tableIndex ${change.tableIndex} out of range`);
|
|
1088
1289
|
}
|
|
1089
|
-
if (
|
|
1090
|
-
change
|
|
1091
|
-
(change.rowVersion === undefined || change.row === undefined)
|
|
1092
|
-
) {
|
|
1093
|
-
encodeInvalid('upsert change requires rowVersion and row');
|
|
1290
|
+
if ((change.op === 'upsert') !== (change.row !== undefined)) {
|
|
1291
|
+
encodeInvalid('change row must be present iff op is upsert');
|
|
1094
1292
|
}
|
|
1095
|
-
if (
|
|
1096
|
-
change
|
|
1097
|
-
(change.rowVersion !== undefined || change.row !== undefined)
|
|
1098
|
-
) {
|
|
1099
|
-
encodeInvalid('delete change must not carry rowVersion or row');
|
|
1293
|
+
if (change.rowVersion < 1) {
|
|
1294
|
+
encodeInvalid('change rowVersion must be at least 1');
|
|
1100
1295
|
}
|
|
1101
1296
|
w.u16(change.tableIndex);
|
|
1102
1297
|
w.str(change.rowId);
|
|
1103
1298
|
w.u8(OP_BYTES[change.op]);
|
|
1104
|
-
w.
|
|
1299
|
+
w.i64(change.rowVersion);
|
|
1105
1300
|
writeStringMap(w, change.scopes);
|
|
1106
1301
|
w.opt(change.row, (v) => w.bytes(v));
|
|
1107
1302
|
}
|
|
@@ -1125,7 +1320,8 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
1125
1320
|
break;
|
|
1126
1321
|
}
|
|
1127
1322
|
case 'SEGMENT_INLINE': {
|
|
1128
|
-
|
|
1323
|
+
w.raw(frame.payload);
|
|
1324
|
+
break;
|
|
1129
1325
|
}
|
|
1130
1326
|
case 'SUB_END': {
|
|
1131
1327
|
w.i64(frame.nextCursor);
|
|
@@ -1141,39 +1337,37 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
1141
1337
|
w.opt(frame.details, (v) => w.str(v));
|
|
1142
1338
|
break;
|
|
1143
1339
|
}
|
|
1144
|
-
case 'UNKNOWN':
|
|
1145
|
-
|
|
1146
|
-
!Number.isInteger(frame.frameType) ||
|
|
1147
|
-
frame.frameType < 0 ||
|
|
1148
|
-
frame.frameType > 0xff ||
|
|
1149
|
-
KNOWN_FRAME_TYPES.has(frame.frameType)
|
|
1150
|
-
) {
|
|
1151
|
-
encodeInvalid(
|
|
1152
|
-
`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`,
|
|
1153
|
-
);
|
|
1154
|
-
}
|
|
1155
|
-
return { frameType: frame.frameType, payload: frame.payload };
|
|
1156
|
-
}
|
|
1340
|
+
case 'UNKNOWN':
|
|
1341
|
+
return encodeUnknown(frame, w);
|
|
1157
1342
|
}
|
|
1158
|
-
return
|
|
1343
|
+
return FrameType[frame.type];
|
|
1159
1344
|
}
|
|
1160
1345
|
|
|
1161
1346
|
// ---------------------------------------------------------------------------
|
|
1162
|
-
// Envelope
|
|
1347
|
+
// Envelope. The kind-specific codecs let a peer bundle only the direction it
|
|
1348
|
+
// speaks; `encodeMessage` and `decodeMessage` dispatch on the kind.
|
|
1163
1349
|
// ---------------------------------------------------------------------------
|
|
1164
1350
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1351
|
+
function encodeEnvelope<F extends RequestFrame | ResponseFrame>(
|
|
1352
|
+
wireVersion: number,
|
|
1353
|
+
kindByte: number,
|
|
1354
|
+
frames: readonly F[],
|
|
1355
|
+
validate: (frames: readonly F[]) => void,
|
|
1356
|
+
encode: (frame: F, w: ByteWriter) => number,
|
|
1357
|
+
): Uint8Array {
|
|
1358
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
1359
|
+
encodeInvalid(`unsupported wireVersion ${wireVersion}`);
|
|
1168
1360
|
}
|
|
1169
|
-
|
|
1361
|
+
validate(frames);
|
|
1170
1362
|
const w = new ByteWriter();
|
|
1171
1363
|
w.raw(MAGIC_BYTES);
|
|
1172
|
-
w.u16(
|
|
1173
|
-
w.u8(
|
|
1364
|
+
w.u16(wireVersion);
|
|
1365
|
+
w.u8(kindByte);
|
|
1174
1366
|
w.u8(0x00);
|
|
1175
|
-
for (const frame of
|
|
1176
|
-
const
|
|
1367
|
+
for (const frame of frames) {
|
|
1368
|
+
const p = new ByteWriter();
|
|
1369
|
+
const frameType = encode(frame, p);
|
|
1370
|
+
const payload = p.finish();
|
|
1177
1371
|
w.u8(frameType);
|
|
1178
1372
|
w.u32(payload.length);
|
|
1179
1373
|
w.raw(payload);
|
|
@@ -1183,6 +1377,32 @@ export function encodeMessage(message: SyncMessage): Uint8Array {
|
|
|
1183
1377
|
return w.finish();
|
|
1184
1378
|
}
|
|
1185
1379
|
|
|
1380
|
+
export function encodeRequest(message: RequestMessage): Uint8Array {
|
|
1381
|
+
return encodeEnvelope(
|
|
1382
|
+
message.wireVersion,
|
|
1383
|
+
0x01,
|
|
1384
|
+
message.frames,
|
|
1385
|
+
validateRequestSequence,
|
|
1386
|
+
encodeRequestFrame,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
export function encodeResponse(message: ResponseMessage): Uint8Array {
|
|
1391
|
+
return encodeEnvelope(
|
|
1392
|
+
message.wireVersion,
|
|
1393
|
+
0x02,
|
|
1394
|
+
message.frames,
|
|
1395
|
+
validateResponseSequence,
|
|
1396
|
+
encodeResponseFrame,
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export function encodeMessage(message: SyncMessage): Uint8Array {
|
|
1401
|
+
return message.msgKind === 'request'
|
|
1402
|
+
? encodeRequest(message)
|
|
1403
|
+
: encodeResponse(message);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1186
1406
|
function readFrames<T>(
|
|
1187
1407
|
r: ByteReader,
|
|
1188
1408
|
decodeFrame_: (frameType: number, payload: Uint8Array) => T,
|
|
@@ -1209,7 +1429,11 @@ function readFrames<T>(
|
|
|
1209
1429
|
}
|
|
1210
1430
|
}
|
|
1211
1431
|
|
|
1212
|
-
|
|
1432
|
+
function decodeEnvelope(bytes: Uint8Array): {
|
|
1433
|
+
r: ByteReader;
|
|
1434
|
+
wireVersion: number;
|
|
1435
|
+
request: boolean;
|
|
1436
|
+
} {
|
|
1213
1437
|
const r = new ByteReader(bytes);
|
|
1214
1438
|
if (r.remaining < 8) invalid('truncated envelope header');
|
|
1215
1439
|
const magic = r.raw(4);
|
|
@@ -1231,24 +1455,34 @@ export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
|
1231
1455
|
}
|
|
1232
1456
|
const flags = r.u8();
|
|
1233
1457
|
if (flags !== 0) invalid(`non-zero envelope flags 0x${flags.toString(16)}`);
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1458
|
+
return { r, wireVersion, request: kindByte === 0x01 };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function requestBody(r: ByteReader, wireVersion: number): RequestMessage {
|
|
1462
|
+
const frames = readFrames(r, decodeRequestFrame);
|
|
1463
|
+
validateRequestSequence(frames);
|
|
1464
|
+
return { wireVersion, msgKind: 'request', frames };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function responseBody(r: ByteReader, wireVersion: number): ResponseMessage {
|
|
1468
|
+
const frames = readFrames(r, decodeResponseFrame);
|
|
1469
|
+
validateResponseSequence(frames);
|
|
1470
|
+
return { wireVersion, msgKind: 'response', frames };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
export function decodeRequest(bytes: Uint8Array): RequestMessage {
|
|
1474
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1475
|
+
if (!request) invalid('expected a request message');
|
|
1476
|
+
return requestBody(r, wireVersion);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
export function decodeResponse(bytes: Uint8Array): ResponseMessage {
|
|
1480
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1481
|
+
if (request) invalid('expected a response message');
|
|
1482
|
+
return responseBody(r, wireVersion);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
1486
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1487
|
+
return request ? requestBody(r, wireVersion) : responseBody(r, wireVersion);
|
|
1254
1488
|
}
|