@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/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 decodeSubscription(r) {
182
+ function decodeProgress(r) {
135
183
  const id = r.str();
136
- const table = r.str();
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
- const bootstrapState = r.opt(() => readJson(r, 'SUBSCRIPTION.bootstrapState'));
186
+ checkCursor(cursor, 'PROGRESS.cursor', invalid);
187
+ const bootstrapState = r.opt(() => readJson(r, 'PROGRESS.bootstrapState'));
141
188
  return definedObject({
142
- type: 'SUBSCRIPTION',
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
- return {
277
- type: 'SUB_START',
278
- id,
279
- status,
280
- reasonCode,
281
- effectiveScopes,
282
- bootstrap,
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.opt(() => r.i64());
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' && (rowVersion === undefined || row === undefined)) {
313
- invalid('upsert change requires rowVersion and row');
385
+ if ((op === 'upsert') !== (row !== undefined)) {
386
+ invalid('change row must be present iff op is upsert');
314
387
  }
315
- if (op === 'delete' && (rowVersion !== undefined || row !== undefined)) {
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.SUBSCRIPTION:
416
- frame = decodeSubscription(r);
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 'SUBSCRIPTION':
579
+ case 'UNIT':
507
580
  if (!hasPull)
508
- invalid('SUBSCRIPTION frame without a PULL_HEADER');
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
- let inSubscription = false;
525
- let sawSubscription = false;
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 (before
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 (sawSubscription)
551
- invalid('PUSH_RESULT frame after SUB_START');
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 (sawSubscription) {
558
- invalid('PUSH_RESULT_DETAILS frame after SUB_START');
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 (inSubscription)
659
+ if (section === 'unit')
580
660
  invalid('nested SUB_START frame');
581
- inSubscription = true;
582
- sawSubscription = true;
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 (!inSubscription)
599
- invalid('segment frame outside a subscription');
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 (!inSubscription)
674
+ if (section !== 'unit')
607
675
  invalid('SUB_END frame without an open SUB_START');
608
- inSubscription = false;
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 (inSubscription && !errorSeen) {
622
- invalid('subscription not terminated by SUB_END');
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 encodeFrame(frame) {
642
- const w = new ByteWriter();
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 'SUBSCRIPTION': {
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.rowVersion === undefined || change.row === undefined)) {
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.op === 'delete' &&
806
- (change.rowVersion !== undefined || change.row !== undefined)) {
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.opt(change.rowVersion, (v) => w.i64(v));
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
- return { frameType: FrameType.SEGMENT_INLINE, payload: frame.payload };
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
- if (!Number.isInteger(frame.frameType) ||
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 { frameType: FrameType[frame.type], payload: w.finish() };
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
- export function encodeMessage(message) {
868
- if (!isSupportedProtocolWireVersion(message.wireVersion)) {
869
- encodeInvalid(`unsupported wireVersion ${message.wireVersion}`);
995
+ function encodeEnvelope(wireVersion, kindByte, frames, validate, encode) {
996
+ if (!isSupportedProtocolWireVersion(wireVersion)) {
997
+ encodeInvalid(`unsupported wireVersion ${wireVersion}`);
870
998
  }
871
- validateFrameSequence(message);
999
+ validate(frames);
872
1000
  const w = new ByteWriter();
873
1001
  w.raw(MAGIC_BYTES);
874
- w.u16(message.wireVersion);
875
- w.u8(message.msgKind === 'request' ? 0x01 : 0x02);
1002
+ w.u16(wireVersion);
1003
+ w.u8(kindByte);
876
1004
  w.u8(0x00);
877
- for (const frame of message.frames) {
878
- const { frameType, payload } = encodeFrame(frame);
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
- export function decodeMessage(bytes) {
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
- if (kindByte === 0x01) {
931
- const message = {
932
- wireVersion,
933
- msgKind: 'request',
934
- frames: readFrames(r, (frameType, payload) => decodeRequestFrame(frameType, payload)),
935
- };
936
- validateRequestSequence(message.frames);
937
- return message;
938
- }
939
- const message = {
940
- wireVersion,
941
- msgKind: 'response',
942
- frames: readFrames(r, (frameType, payload) => decodeResponseFrame(frameType, payload)),
943
- };
944
- validateResponseSequence(message.frames);
945
- return message;
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
  }