@syncular/core 0.59.0 → 0.60.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/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/dist/message.js
CHANGED
|
@@ -48,6 +48,38 @@ function validateRoundFailureCode(code, fail) {
|
|
|
48
48
|
fail('ROUND_FAILURE.code must be a lowercase dotted code of at most 64 bytes');
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
/** §1.5, §1.7: a present `outboxId` is non-empty and at most 64 bytes. */
|
|
52
|
+
function checkOutboxId(outboxId, fail) {
|
|
53
|
+
if (outboxId.length === 0 || utf8Encode(outboxId).length > 64) {
|
|
54
|
+
fail('REQ_HEADER.outboxId must be non-empty and at most 64 bytes');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** §4.3, §4.4: a unit id is 32 lowercase hex characters. */
|
|
58
|
+
function checkUnitId(id, frame, fail) {
|
|
59
|
+
if (!/^[0-9a-f]{32}$/.test(id)) {
|
|
60
|
+
fail(`${frame}.id must be 32 lowercase hex characters`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** §4.2, §4.4: a digest is 32 bytes; an echo digest may also be empty. */
|
|
64
|
+
function checkDigest(digest, field, fail) {
|
|
65
|
+
const emptyAllowed = field !== 'PULL_HEADER.registrationDigest';
|
|
66
|
+
if (digest.length !== 32 && !(emptyAllowed && digest.length === 0)) {
|
|
67
|
+
fail(`${field} must be ${emptyAllowed ? '0 or ' : ''}32 bytes`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** §4.2, §4.3: a cursor is `-1` or a commitSeq. */
|
|
71
|
+
function checkCursor(cursor, field, fail) {
|
|
72
|
+
if (cursor < -1)
|
|
73
|
+
fail(`${field} must be >= -1`);
|
|
74
|
+
}
|
|
75
|
+
/** §4.4: fenced records in strictly ascending code-unit order of `table`. */
|
|
76
|
+
function checkFencedOrder(fenced, fail) {
|
|
77
|
+
for (let i = 1; i < fenced.length; i++) {
|
|
78
|
+
if (fenced[i - 1].table >= fenced[i].table) {
|
|
79
|
+
fail('SUB_START.fenced tables must be unique and in ascending order');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
51
83
|
// ---------------------------------------------------------------------------
|
|
52
84
|
// Frame payload decoders (each consumes exactly the frame payload)
|
|
53
85
|
// ---------------------------------------------------------------------------
|
|
@@ -64,11 +96,15 @@ function decodeReqHeader(r) {
|
|
|
64
96
|
invalid('REQ_HEADER.logEpoch must be non-empty');
|
|
65
97
|
return value;
|
|
66
98
|
});
|
|
99
|
+
const outboxId = r.opt(() => r.str());
|
|
100
|
+
if (outboxId !== undefined)
|
|
101
|
+
checkOutboxId(outboxId, invalid);
|
|
67
102
|
return definedObject({
|
|
68
103
|
type: 'REQ_HEADER',
|
|
69
104
|
clientId,
|
|
70
105
|
schemaVersion,
|
|
71
106
|
logEpoch: logEpoch,
|
|
107
|
+
outboxId: outboxId,
|
|
72
108
|
});
|
|
73
109
|
}
|
|
74
110
|
function decodeRoundFailure(r) {
|
|
@@ -81,6 +117,9 @@ function decodePushCommit(r) {
|
|
|
81
117
|
if (clientCommitId.length === 0) {
|
|
82
118
|
invalid('PUSH_COMMIT.clientCommitId must be non-empty');
|
|
83
119
|
}
|
|
120
|
+
const clientSeq = r.i64();
|
|
121
|
+
if (clientSeq < 0)
|
|
122
|
+
invalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
84
123
|
const operationCount = r.u32();
|
|
85
124
|
if (operationCount === 0) {
|
|
86
125
|
throw new DecodeError('sync.empty_commit', 'PUSH_COMMIT must carry at least one operation');
|
|
@@ -113,7 +152,7 @@ function decodePushCommit(r) {
|
|
|
113
152
|
payload: payload,
|
|
114
153
|
}));
|
|
115
154
|
}
|
|
116
|
-
return { type: 'PUSH_COMMIT', clientCommitId, operations };
|
|
155
|
+
return { type: 'PUSH_COMMIT', clientCommitId, clientSeq, operations };
|
|
117
156
|
}
|
|
118
157
|
function decodePullHeader(r) {
|
|
119
158
|
const limitCommits = r.i32();
|
|
@@ -123,27 +162,32 @@ function decodePullHeader(r) {
|
|
|
123
162
|
if ((accept & 0xf0) !== 0) {
|
|
124
163
|
invalid('PULL_HEADER.accept bits 4-7 must be zero');
|
|
125
164
|
}
|
|
165
|
+
const cursor = r.i64();
|
|
166
|
+
checkCursor(cursor, 'PULL_HEADER.cursor', invalid);
|
|
167
|
+
const registrationDigest = r.bytes();
|
|
168
|
+
checkDigest(registrationDigest, 'PULL_HEADER.registrationDigest', invalid);
|
|
169
|
+
const echoDigest = r.bytes();
|
|
170
|
+
checkDigest(echoDigest, 'PULL_HEADER.echoDigest', invalid);
|
|
126
171
|
return {
|
|
127
172
|
type: 'PULL_HEADER',
|
|
128
173
|
limitCommits,
|
|
129
174
|
limitSnapshotRows,
|
|
130
175
|
maxSnapshotPages,
|
|
131
176
|
accept,
|
|
177
|
+
cursor,
|
|
178
|
+
registrationDigest,
|
|
179
|
+
echoDigest,
|
|
132
180
|
};
|
|
133
181
|
}
|
|
134
|
-
function
|
|
182
|
+
function decodeProgress(r) {
|
|
135
183
|
const id = r.str();
|
|
136
|
-
|
|
137
|
-
const scopes = readStringListMap(r);
|
|
138
|
-
const params = r.opt(() => readJson(r, 'SUBSCRIPTION.params'));
|
|
184
|
+
checkUnitId(id, 'PROGRESS', invalid);
|
|
139
185
|
const cursor = r.i64();
|
|
140
|
-
|
|
186
|
+
checkCursor(cursor, 'PROGRESS.cursor', invalid);
|
|
187
|
+
const bootstrapState = r.opt(() => readJson(r, 'PROGRESS.bootstrapState'));
|
|
141
188
|
return definedObject({
|
|
142
|
-
type: '
|
|
189
|
+
type: 'PROGRESS',
|
|
143
190
|
id,
|
|
144
|
-
table,
|
|
145
|
-
scopes,
|
|
146
|
-
params: params,
|
|
147
191
|
cursor,
|
|
148
192
|
bootstrapState: bootstrapState,
|
|
149
193
|
});
|
|
@@ -260,6 +304,7 @@ function decodePushResultDetails(r) {
|
|
|
260
304
|
}
|
|
261
305
|
function decodeSubStart(r) {
|
|
262
306
|
const id = r.str();
|
|
307
|
+
checkUnitId(id, 'SUB_START', invalid);
|
|
263
308
|
const statusByte = r.u8();
|
|
264
309
|
let status;
|
|
265
310
|
if (statusByte === 1)
|
|
@@ -271,16 +316,42 @@ function decodeSubStart(r) {
|
|
|
271
316
|
else
|
|
272
317
|
invalid(`invalid SUB_START status byte ${statusByte}`);
|
|
273
318
|
const reasonCode = r.str();
|
|
274
|
-
const effectiveScopes = readStringListMap(r);
|
|
275
319
|
const bootstrap = r.bool();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
320
|
+
const fencedCount = r.u32();
|
|
321
|
+
const fenced = [];
|
|
322
|
+
for (let i = 0; i < fencedCount; i++) {
|
|
323
|
+
const table = r.str();
|
|
324
|
+
const fencedStatus = r.u8();
|
|
325
|
+
if (fencedStatus !== 1 && fencedStatus !== 2) {
|
|
326
|
+
invalid('invalid fenced record status byte');
|
|
327
|
+
}
|
|
328
|
+
fenced.push({
|
|
329
|
+
table,
|
|
330
|
+
status: fencedStatus === 1 ? 'active' : 'revoked',
|
|
331
|
+
scopes: readStringListMap(r),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
checkFencedOrder(fenced, invalid);
|
|
335
|
+
return { type: 'SUB_START', id, status, reasonCode, bootstrap, fenced };
|
|
336
|
+
}
|
|
337
|
+
function decodeLiveStart(r) {
|
|
338
|
+
const statusByte = r.u8();
|
|
339
|
+
let status;
|
|
340
|
+
if (statusByte === 1)
|
|
341
|
+
status = 'active';
|
|
342
|
+
else if (statusByte === 3)
|
|
343
|
+
status = 'reset';
|
|
344
|
+
else if (statusByte === 4)
|
|
345
|
+
status = 'unregistered';
|
|
346
|
+
else
|
|
347
|
+
invalid('invalid LIVE_START status byte');
|
|
348
|
+
return { type: 'LIVE_START', status, reasonCode: r.str() };
|
|
349
|
+
}
|
|
350
|
+
function decodeLiveEnd(r) {
|
|
351
|
+
const throughSeq = r.i64();
|
|
352
|
+
const echoDigest = r.bytes();
|
|
353
|
+
checkDigest(echoDigest, 'LIVE_END.echoDigest', invalid);
|
|
354
|
+
return { type: 'LIVE_END', throughSeq, echoDigest };
|
|
284
355
|
}
|
|
285
356
|
function decodeCommit(r) {
|
|
286
357
|
const commitSeq = r.i64();
|
|
@@ -306,23 +377,15 @@ function decodeCommit(r) {
|
|
|
306
377
|
op = 'delete';
|
|
307
378
|
else
|
|
308
379
|
invalid(`invalid change op byte ${opByte}`);
|
|
309
|
-
const rowVersion = r.
|
|
380
|
+
const rowVersion = r.i64();
|
|
381
|
+
if (rowVersion < 1)
|
|
382
|
+
invalid('change rowVersion must be at least 1');
|
|
310
383
|
const scopes = readStringMap(r);
|
|
311
384
|
const row = r.opt(() => r.bytes());
|
|
312
|
-
if (op === 'upsert'
|
|
313
|
-
invalid('
|
|
385
|
+
if ((op === 'upsert') !== (row !== undefined)) {
|
|
386
|
+
invalid('change row must be present iff op is upsert');
|
|
314
387
|
}
|
|
315
|
-
|
|
316
|
-
invalid('delete change must not carry rowVersion or row');
|
|
317
|
-
}
|
|
318
|
-
changes.push(definedObject({
|
|
319
|
-
tableIndex,
|
|
320
|
-
rowId,
|
|
321
|
-
op,
|
|
322
|
-
rowVersion: rowVersion,
|
|
323
|
-
scopes,
|
|
324
|
-
row: row,
|
|
325
|
-
}));
|
|
388
|
+
changes.push(definedObject({ tableIndex, rowId, op, rowVersion, scopes, row }));
|
|
326
389
|
}
|
|
327
390
|
return { type: 'COMMIT', commitSeq, createdAtMs, actorId, tables, changes };
|
|
328
391
|
}
|
|
@@ -412,8 +475,11 @@ function decodeRequestFrame(frameType, payload) {
|
|
|
412
475
|
case FrameType.PULL_HEADER:
|
|
413
476
|
frame = decodePullHeader(r);
|
|
414
477
|
break;
|
|
415
|
-
case FrameType.
|
|
416
|
-
frame =
|
|
478
|
+
case FrameType.UNIT:
|
|
479
|
+
frame = { type: 'UNIT', scopes: readStringMap(r) };
|
|
480
|
+
break;
|
|
481
|
+
case FrameType.PROGRESS:
|
|
482
|
+
frame = decodeProgress(r);
|
|
417
483
|
break;
|
|
418
484
|
default:
|
|
419
485
|
if (RESPONSE_FRAME_TYPES.has(frameType)) {
|
|
@@ -455,6 +521,12 @@ function decodeResponseFrame(frameType, payload) {
|
|
|
455
521
|
case FrameType.SUB_END:
|
|
456
522
|
frame = decodeSubEnd(r);
|
|
457
523
|
break;
|
|
524
|
+
case FrameType.LIVE_START:
|
|
525
|
+
frame = decodeLiveStart(r);
|
|
526
|
+
break;
|
|
527
|
+
case FrameType.LIVE_END:
|
|
528
|
+
frame = decodeLiveEnd(r);
|
|
529
|
+
break;
|
|
458
530
|
case FrameType.ERROR:
|
|
459
531
|
frame = decodeErrorFrame(r);
|
|
460
532
|
break;
|
|
@@ -477,6 +549,7 @@ function validateRequestSequence(frames) {
|
|
|
477
549
|
}
|
|
478
550
|
let pushCount = 0;
|
|
479
551
|
let hasPull = false;
|
|
552
|
+
let hasProgress = false;
|
|
480
553
|
let index = 1;
|
|
481
554
|
let sawRoundFailure = false;
|
|
482
555
|
for (const frame of frames.slice(1)) {
|
|
@@ -503,9 +576,16 @@ function validateRequestSequence(frames) {
|
|
|
503
576
|
invalid('duplicate PULL_HEADER frame');
|
|
504
577
|
hasPull = true;
|
|
505
578
|
break;
|
|
506
|
-
case '
|
|
579
|
+
case 'UNIT':
|
|
507
580
|
if (!hasPull)
|
|
508
|
-
invalid('
|
|
581
|
+
invalid('UNIT frame without a PULL_HEADER');
|
|
582
|
+
if (hasProgress)
|
|
583
|
+
invalid('UNIT frame after a PROGRESS frame');
|
|
584
|
+
break;
|
|
585
|
+
case 'PROGRESS':
|
|
586
|
+
if (!hasPull)
|
|
587
|
+
invalid('PROGRESS frame without a PULL_HEADER');
|
|
588
|
+
hasProgress = true;
|
|
509
589
|
break;
|
|
510
590
|
case 'UNKNOWN':
|
|
511
591
|
break;
|
|
@@ -521,12 +601,12 @@ function validateResponseSequence(frames) {
|
|
|
521
601
|
if (first === undefined || first.type !== 'RESP_HEADER') {
|
|
522
602
|
invalid('response must start with a RESP_HEADER frame');
|
|
523
603
|
}
|
|
524
|
-
|
|
525
|
-
let
|
|
604
|
+
// §1.6 sections: unit sections, then at most one live section.
|
|
605
|
+
let section = 'none';
|
|
606
|
+
let sawUnitSection = false;
|
|
607
|
+
let sawLive = false;
|
|
526
608
|
let sawLease = false;
|
|
527
609
|
let sawBody = false;
|
|
528
|
-
let subscriptionHasCommits = false;
|
|
529
|
-
let subscriptionHasSegments = false;
|
|
530
610
|
let errorSeen = false;
|
|
531
611
|
let lastPushResult;
|
|
532
612
|
let lastPushResultHasDetails = false;
|
|
@@ -538,8 +618,7 @@ function validateResponseSequence(frames) {
|
|
|
538
618
|
invalid('duplicate RESP_HEADER frame');
|
|
539
619
|
break;
|
|
540
620
|
case 'LEASE':
|
|
541
|
-
// §7.3.2: at most one LEASE, immediately after RESP_HEADER
|
|
542
|
-
// any PUSH_RESULT / subscription section / ERROR).
|
|
621
|
+
// §7.3.2: at most one LEASE, immediately after RESP_HEADER.
|
|
543
622
|
if (sawLease)
|
|
544
623
|
invalid('duplicate LEASE frame');
|
|
545
624
|
if (sawBody)
|
|
@@ -547,15 +626,16 @@ function validateResponseSequence(frames) {
|
|
|
547
626
|
sawLease = true;
|
|
548
627
|
break;
|
|
549
628
|
case 'PUSH_RESULT':
|
|
550
|
-
if (
|
|
551
|
-
invalid('PUSH_RESULT frame after
|
|
629
|
+
if (sawUnitSection || sawLive) {
|
|
630
|
+
invalid('PUSH_RESULT frame after a unit or live section');
|
|
631
|
+
}
|
|
552
632
|
lastPushResult = frame;
|
|
553
633
|
lastPushResultHasDetails = false;
|
|
554
634
|
sawBody = true;
|
|
555
635
|
break;
|
|
556
636
|
case 'PUSH_RESULT_DETAILS': {
|
|
557
|
-
if (
|
|
558
|
-
invalid('PUSH_RESULT_DETAILS frame after
|
|
637
|
+
if (sawUnitSection || sawLive) {
|
|
638
|
+
invalid('PUSH_RESULT_DETAILS frame after a unit or live section');
|
|
559
639
|
}
|
|
560
640
|
if (lastPushResult === undefined) {
|
|
561
641
|
invalid('PUSH_RESULT_DETAILS without a preceding PUSH_RESULT');
|
|
@@ -576,36 +656,43 @@ function validateResponseSequence(frames) {
|
|
|
576
656
|
break;
|
|
577
657
|
}
|
|
578
658
|
case 'SUB_START':
|
|
579
|
-
if (
|
|
659
|
+
if (section === 'unit')
|
|
580
660
|
invalid('nested SUB_START frame');
|
|
581
|
-
|
|
582
|
-
|
|
661
|
+
if (sawLive)
|
|
662
|
+
invalid('SUB_START frame inside or after the live section');
|
|
663
|
+
section = 'unit';
|
|
664
|
+
sawUnitSection = true;
|
|
583
665
|
sawBody = true;
|
|
584
666
|
lastPushResult = undefined;
|
|
585
|
-
subscriptionHasCommits = false;
|
|
586
|
-
subscriptionHasSegments = false;
|
|
587
|
-
break;
|
|
588
|
-
case 'COMMIT':
|
|
589
|
-
if (!inSubscription)
|
|
590
|
-
invalid('COMMIT frame outside a subscription');
|
|
591
|
-
if (subscriptionHasSegments) {
|
|
592
|
-
invalid('COMMIT and segment frames must not both appear for one subscription');
|
|
593
|
-
}
|
|
594
|
-
subscriptionHasCommits = true;
|
|
595
667
|
break;
|
|
596
668
|
case 'SEGMENT_REF':
|
|
597
669
|
case 'SEGMENT_INLINE':
|
|
598
|
-
if (
|
|
599
|
-
invalid('segment frame outside a
|
|
600
|
-
if (subscriptionHasCommits) {
|
|
601
|
-
invalid('COMMIT and segment frames must not both appear for one subscription');
|
|
602
|
-
}
|
|
603
|
-
subscriptionHasSegments = true;
|
|
670
|
+
if (section !== 'unit')
|
|
671
|
+
invalid('segment frame outside a unit section');
|
|
604
672
|
break;
|
|
605
673
|
case 'SUB_END':
|
|
606
|
-
if (
|
|
674
|
+
if (section !== 'unit')
|
|
607
675
|
invalid('SUB_END frame without an open SUB_START');
|
|
608
|
-
|
|
676
|
+
section = 'none';
|
|
677
|
+
break;
|
|
678
|
+
case 'LIVE_START':
|
|
679
|
+
if (section === 'unit')
|
|
680
|
+
invalid('LIVE_START frame inside a unit section');
|
|
681
|
+
if (sawLive)
|
|
682
|
+
invalid('second LIVE_START frame');
|
|
683
|
+
section = 'live';
|
|
684
|
+
sawLive = true;
|
|
685
|
+
sawBody = true;
|
|
686
|
+
lastPushResult = undefined;
|
|
687
|
+
break;
|
|
688
|
+
case 'COMMIT':
|
|
689
|
+
if (section !== 'live')
|
|
690
|
+
invalid('COMMIT frame outside the live section');
|
|
691
|
+
break;
|
|
692
|
+
case 'LIVE_END':
|
|
693
|
+
if (section !== 'live')
|
|
694
|
+
invalid('LIVE_END frame without an open LIVE_START');
|
|
695
|
+
section = 'none';
|
|
609
696
|
break;
|
|
610
697
|
case 'ERROR':
|
|
611
698
|
errorSeen = true;
|
|
@@ -618,28 +705,33 @@ function validateResponseSequence(frames) {
|
|
|
618
705
|
break;
|
|
619
706
|
}
|
|
620
707
|
}
|
|
621
|
-
if (
|
|
622
|
-
invalid('
|
|
708
|
+
if (section !== 'none' && !errorSeen) {
|
|
709
|
+
invalid('unit or live section not terminated');
|
|
623
710
|
}
|
|
624
711
|
}
|
|
625
|
-
export function validateFrameSequence(message) {
|
|
626
|
-
if (message.msgKind === 'request')
|
|
627
|
-
validateRequestSequence(message.frames);
|
|
628
|
-
else
|
|
629
|
-
validateResponseSequence(message.frames);
|
|
630
|
-
}
|
|
631
712
|
// ---------------------------------------------------------------------------
|
|
632
713
|
// Frame payload encoders
|
|
633
714
|
// ---------------------------------------------------------------------------
|
|
634
715
|
const OP_BYTES = { upsert: 1, delete: 2 };
|
|
635
716
|
const PUSH_STATUS_BYTES = { applied: 1, cached: 2, rejected: 3 };
|
|
636
717
|
const SUB_STATUS_BYTES = { active: 1, revoked: 2, reset: 3 };
|
|
718
|
+
const LIVE_STATUS_BYTES = { active: 1, reset: 3, unregistered: 4 };
|
|
637
719
|
const MEDIA_TYPE_BYTES = { rows: 1, sqlite: 2 };
|
|
638
720
|
function encodeInvalid(message) {
|
|
639
721
|
throw new Error(message);
|
|
640
722
|
}
|
|
641
|
-
function
|
|
642
|
-
|
|
723
|
+
function encodeUnknown(frame, w) {
|
|
724
|
+
if (!Number.isInteger(frame.frameType) ||
|
|
725
|
+
frame.frameType < 0 ||
|
|
726
|
+
frame.frameType > 0xff ||
|
|
727
|
+
KNOWN_FRAME_TYPES.has(frame.frameType)) {
|
|
728
|
+
encodeInvalid(`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`);
|
|
729
|
+
}
|
|
730
|
+
w.raw(frame.payload);
|
|
731
|
+
return frame.frameType;
|
|
732
|
+
}
|
|
733
|
+
/** Writes the payload; returns the frame type byte. */
|
|
734
|
+
function encodeRequestFrame(frame, w) {
|
|
643
735
|
switch (frame.type) {
|
|
644
736
|
case 'REQ_HEADER': {
|
|
645
737
|
if (frame.clientId.length === 0) {
|
|
@@ -654,6 +746,10 @@ function encodeFrame(frame) {
|
|
|
654
746
|
encodeInvalid('REQ_HEADER.logEpoch must be non-empty');
|
|
655
747
|
}
|
|
656
748
|
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
749
|
+
if (frame.outboxId !== undefined) {
|
|
750
|
+
checkOutboxId(frame.outboxId, encodeInvalid);
|
|
751
|
+
}
|
|
752
|
+
w.opt(frame.outboxId, (v) => w.str(v));
|
|
657
753
|
break;
|
|
658
754
|
}
|
|
659
755
|
case 'ROUND_FAILURE': {
|
|
@@ -668,7 +764,11 @@ function encodeFrame(frame) {
|
|
|
668
764
|
if (frame.operations.length === 0) {
|
|
669
765
|
encodeInvalid('PUSH_COMMIT must carry at least one operation');
|
|
670
766
|
}
|
|
767
|
+
if (frame.clientSeq < 0) {
|
|
768
|
+
encodeInvalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
769
|
+
}
|
|
671
770
|
w.str(frame.clientCommitId);
|
|
771
|
+
w.i64(frame.clientSeq);
|
|
672
772
|
w.u32(frame.operations.length);
|
|
673
773
|
for (const operation of frame.operations) {
|
|
674
774
|
if (operation.op === 'upsert' && operation.payload === undefined) {
|
|
@@ -692,18 +792,35 @@ function encodeFrame(frame) {
|
|
|
692
792
|
w.i32(frame.limitCommits);
|
|
693
793
|
w.i32(frame.limitSnapshotRows);
|
|
694
794
|
w.i32(frame.maxSnapshotPages);
|
|
795
|
+
checkCursor(frame.cursor, 'PULL_HEADER.cursor', encodeInvalid);
|
|
796
|
+
checkDigest(frame.registrationDigest, 'PULL_HEADER.registrationDigest', encodeInvalid);
|
|
797
|
+
checkDigest(frame.echoDigest, 'PULL_HEADER.echoDigest', encodeInvalid);
|
|
695
798
|
w.u8(frame.accept);
|
|
799
|
+
w.i64(frame.cursor);
|
|
800
|
+
w.bytes(frame.registrationDigest);
|
|
801
|
+
w.bytes(frame.echoDigest);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
case 'UNIT': {
|
|
805
|
+
writeStringMap(w, frame.scopes);
|
|
696
806
|
break;
|
|
697
807
|
}
|
|
698
|
-
case '
|
|
808
|
+
case 'PROGRESS': {
|
|
809
|
+
checkUnitId(frame.id, 'PROGRESS', encodeInvalid);
|
|
810
|
+
checkCursor(frame.cursor, 'PROGRESS.cursor', encodeInvalid);
|
|
699
811
|
w.str(frame.id);
|
|
700
|
-
w.str(frame.table);
|
|
701
|
-
writeStringListMap(w, frame.scopes);
|
|
702
|
-
w.opt(frame.params, (v) => w.str(v));
|
|
703
812
|
w.i64(frame.cursor);
|
|
704
813
|
w.opt(frame.bootstrapState, (v) => w.str(v));
|
|
705
814
|
break;
|
|
706
815
|
}
|
|
816
|
+
case 'UNKNOWN':
|
|
817
|
+
return encodeUnknown(frame, w);
|
|
818
|
+
}
|
|
819
|
+
return FrameType[frame.type];
|
|
820
|
+
}
|
|
821
|
+
/** Writes the payload; returns the frame type byte. */
|
|
822
|
+
function encodeResponseFrame(frame, w) {
|
|
823
|
+
switch (frame.type) {
|
|
707
824
|
case 'RESP_HEADER': {
|
|
708
825
|
w.opt(frame.requiredSchemaVersion, (v) => w.i32(v));
|
|
709
826
|
w.opt(frame.latestSchemaVersion, (v) => w.i32(v));
|
|
@@ -779,11 +896,29 @@ function encodeFrame(frame) {
|
|
|
779
896
|
break;
|
|
780
897
|
}
|
|
781
898
|
case 'SUB_START': {
|
|
899
|
+
checkUnitId(frame.id, 'SUB_START', encodeInvalid);
|
|
900
|
+
checkFencedOrder(frame.fenced, encodeInvalid);
|
|
782
901
|
w.str(frame.id);
|
|
783
902
|
w.u8(SUB_STATUS_BYTES[frame.status]);
|
|
784
903
|
w.str(frame.reasonCode);
|
|
785
|
-
writeStringListMap(w, frame.effectiveScopes);
|
|
786
904
|
w.bool(frame.bootstrap);
|
|
905
|
+
w.u32(frame.fenced.length);
|
|
906
|
+
for (const record of frame.fenced) {
|
|
907
|
+
w.str(record.table);
|
|
908
|
+
w.u8(SUB_STATUS_BYTES[record.status]);
|
|
909
|
+
writeStringListMap(w, record.scopes);
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case 'LIVE_START': {
|
|
914
|
+
w.u8(LIVE_STATUS_BYTES[frame.status]);
|
|
915
|
+
w.str(frame.reasonCode);
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case 'LIVE_END': {
|
|
919
|
+
checkDigest(frame.echoDigest, 'LIVE_END.echoDigest', encodeInvalid);
|
|
920
|
+
w.i64(frame.throughSeq);
|
|
921
|
+
w.bytes(frame.echoDigest);
|
|
787
922
|
break;
|
|
788
923
|
}
|
|
789
924
|
case 'COMMIT': {
|
|
@@ -798,18 +933,16 @@ function encodeFrame(frame) {
|
|
|
798
933
|
if (change.tableIndex >= frame.tables.length) {
|
|
799
934
|
encodeInvalid(`change tableIndex ${change.tableIndex} out of range`);
|
|
800
935
|
}
|
|
801
|
-
if (change.op === 'upsert'
|
|
802
|
-
(change
|
|
803
|
-
encodeInvalid('upsert change requires rowVersion and row');
|
|
936
|
+
if ((change.op === 'upsert') !== (change.row !== undefined)) {
|
|
937
|
+
encodeInvalid('change row must be present iff op is upsert');
|
|
804
938
|
}
|
|
805
|
-
if (change.
|
|
806
|
-
(change
|
|
807
|
-
encodeInvalid('delete change must not carry rowVersion or row');
|
|
939
|
+
if (change.rowVersion < 1) {
|
|
940
|
+
encodeInvalid('change rowVersion must be at least 1');
|
|
808
941
|
}
|
|
809
942
|
w.u16(change.tableIndex);
|
|
810
943
|
w.str(change.rowId);
|
|
811
944
|
w.u8(OP_BYTES[change.op]);
|
|
812
|
-
w.
|
|
945
|
+
w.i64(change.rowVersion);
|
|
813
946
|
writeStringMap(w, change.scopes);
|
|
814
947
|
w.opt(change.row, (v) => w.bytes(v));
|
|
815
948
|
}
|
|
@@ -833,7 +966,8 @@ function encodeFrame(frame) {
|
|
|
833
966
|
break;
|
|
834
967
|
}
|
|
835
968
|
case 'SEGMENT_INLINE': {
|
|
836
|
-
|
|
969
|
+
w.raw(frame.payload);
|
|
970
|
+
break;
|
|
837
971
|
}
|
|
838
972
|
case 'SUB_END': {
|
|
839
973
|
w.i64(frame.nextCursor);
|
|
@@ -849,33 +983,29 @@ function encodeFrame(frame) {
|
|
|
849
983
|
w.opt(frame.details, (v) => w.str(v));
|
|
850
984
|
break;
|
|
851
985
|
}
|
|
852
|
-
case 'UNKNOWN':
|
|
853
|
-
|
|
854
|
-
frame.frameType < 0 ||
|
|
855
|
-
frame.frameType > 0xff ||
|
|
856
|
-
KNOWN_FRAME_TYPES.has(frame.frameType)) {
|
|
857
|
-
encodeInvalid(`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`);
|
|
858
|
-
}
|
|
859
|
-
return { frameType: frame.frameType, payload: frame.payload };
|
|
860
|
-
}
|
|
986
|
+
case 'UNKNOWN':
|
|
987
|
+
return encodeUnknown(frame, w);
|
|
861
988
|
}
|
|
862
|
-
return
|
|
989
|
+
return FrameType[frame.type];
|
|
863
990
|
}
|
|
864
991
|
// ---------------------------------------------------------------------------
|
|
865
|
-
// Envelope
|
|
992
|
+
// Envelope. The kind-specific codecs let a peer bundle only the direction it
|
|
993
|
+
// speaks; `encodeMessage` and `decodeMessage` dispatch on the kind.
|
|
866
994
|
// ---------------------------------------------------------------------------
|
|
867
|
-
|
|
868
|
-
if (!isSupportedProtocolWireVersion(
|
|
869
|
-
encodeInvalid(`unsupported wireVersion ${
|
|
995
|
+
function encodeEnvelope(wireVersion, kindByte, frames, validate, encode) {
|
|
996
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
997
|
+
encodeInvalid(`unsupported wireVersion ${wireVersion}`);
|
|
870
998
|
}
|
|
871
|
-
|
|
999
|
+
validate(frames);
|
|
872
1000
|
const w = new ByteWriter();
|
|
873
1001
|
w.raw(MAGIC_BYTES);
|
|
874
|
-
w.u16(
|
|
875
|
-
w.u8(
|
|
1002
|
+
w.u16(wireVersion);
|
|
1003
|
+
w.u8(kindByte);
|
|
876
1004
|
w.u8(0x00);
|
|
877
|
-
for (const frame of
|
|
878
|
-
const
|
|
1005
|
+
for (const frame of frames) {
|
|
1006
|
+
const p = new ByteWriter();
|
|
1007
|
+
const frameType = encode(frame, p);
|
|
1008
|
+
const payload = p.finish();
|
|
879
1009
|
w.u8(frameType);
|
|
880
1010
|
w.u32(payload.length);
|
|
881
1011
|
w.raw(payload);
|
|
@@ -884,6 +1014,17 @@ export function encodeMessage(message) {
|
|
|
884
1014
|
w.u32(0);
|
|
885
1015
|
return w.finish();
|
|
886
1016
|
}
|
|
1017
|
+
export function encodeRequest(message) {
|
|
1018
|
+
return encodeEnvelope(message.wireVersion, 0x01, message.frames, validateRequestSequence, encodeRequestFrame);
|
|
1019
|
+
}
|
|
1020
|
+
export function encodeResponse(message) {
|
|
1021
|
+
return encodeEnvelope(message.wireVersion, 0x02, message.frames, validateResponseSequence, encodeResponseFrame);
|
|
1022
|
+
}
|
|
1023
|
+
export function encodeMessage(message) {
|
|
1024
|
+
return message.msgKind === 'request'
|
|
1025
|
+
? encodeRequest(message)
|
|
1026
|
+
: encodeResponse(message);
|
|
1027
|
+
}
|
|
887
1028
|
function readFrames(r, decodeFrame_) {
|
|
888
1029
|
const frames = [];
|
|
889
1030
|
for (;;) {
|
|
@@ -905,7 +1046,7 @@ function readFrames(r, decodeFrame_) {
|
|
|
905
1046
|
frames.push(decodeFrame_(frameType, payload));
|
|
906
1047
|
}
|
|
907
1048
|
}
|
|
908
|
-
|
|
1049
|
+
function decodeEnvelope(bytes) {
|
|
909
1050
|
const r = new ByteReader(bytes);
|
|
910
1051
|
if (r.remaining < 8)
|
|
911
1052
|
invalid('truncated envelope header');
|
|
@@ -927,20 +1068,31 @@ export function decodeMessage(bytes) {
|
|
|
927
1068
|
const flags = r.u8();
|
|
928
1069
|
if (flags !== 0)
|
|
929
1070
|
invalid(`non-zero envelope flags 0x${flags.toString(16)}`);
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
};
|
|
944
|
-
|
|
945
|
-
|
|
1071
|
+
return { r, wireVersion, request: kindByte === 0x01 };
|
|
1072
|
+
}
|
|
1073
|
+
function requestBody(r, wireVersion) {
|
|
1074
|
+
const frames = readFrames(r, decodeRequestFrame);
|
|
1075
|
+
validateRequestSequence(frames);
|
|
1076
|
+
return { wireVersion, msgKind: 'request', frames };
|
|
1077
|
+
}
|
|
1078
|
+
function responseBody(r, wireVersion) {
|
|
1079
|
+
const frames = readFrames(r, decodeResponseFrame);
|
|
1080
|
+
validateResponseSequence(frames);
|
|
1081
|
+
return { wireVersion, msgKind: 'response', frames };
|
|
1082
|
+
}
|
|
1083
|
+
export function decodeRequest(bytes) {
|
|
1084
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1085
|
+
if (!request)
|
|
1086
|
+
invalid('expected a request message');
|
|
1087
|
+
return requestBody(r, wireVersion);
|
|
1088
|
+
}
|
|
1089
|
+
export function decodeResponse(bytes) {
|
|
1090
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1091
|
+
if (request)
|
|
1092
|
+
invalid('expected a response message');
|
|
1093
|
+
return responseBody(r, wireVersion);
|
|
1094
|
+
}
|
|
1095
|
+
export function decodeMessage(bytes) {
|
|
1096
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1097
|
+
return request ? requestBody(r, wireVersion) : responseBody(r, wireVersion);
|
|
946
1098
|
}
|