@syncular/core 0.40.0 → 0.41.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/frames.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare const FrameType: {
5
5
  readonly PUSH_COMMIT: 2;
6
6
  readonly PULL_HEADER: 3;
7
7
  readonly SUBSCRIPTION: 4;
8
+ readonly ROUND_FAILURE: 5;
8
9
  readonly RESP_HEADER: 16;
9
10
  readonly PUSH_RESULT: 17;
10
11
  readonly SUB_START: 18;
package/dist/frames.js CHANGED
@@ -5,6 +5,7 @@ export const FrameType = {
5
5
  PUSH_COMMIT: 0x02,
6
6
  PULL_HEADER: 0x03,
7
7
  SUBSCRIPTION: 0x04,
8
+ ROUND_FAILURE: 0x05,
8
9
  RESP_HEADER: 0x10,
9
10
  PUSH_RESULT: 0x11,
10
11
  SUB_START: 0x12,
@@ -22,6 +23,7 @@ export const REQUEST_FRAME_TYPES = new Set([
22
23
  FrameType.PUSH_COMMIT,
23
24
  FrameType.PULL_HEADER,
24
25
  FrameType.SUBSCRIPTION,
26
+ FrameType.ROUND_FAILURE,
25
27
  ]);
26
28
  /** Known response frame types (excluding END). */
27
29
  export const RESPONSE_FRAME_TYPES = new Set([
package/dist/message.d.ts CHANGED
@@ -45,7 +45,15 @@ export interface UnknownFrame {
45
45
  frameType: number;
46
46
  payload: Uint8Array;
47
47
  }
48
- export type RequestFrame = ReqHeaderFrame | PushCommitFrame | PullHeaderFrame | SubscriptionFrame | UnknownFrame;
48
+ /**
49
+ * §1.5: the code of the client's last round that failed where the server
50
+ * could not see it. Reported once, immediately after `REQ_HEADER`.
51
+ */
52
+ export interface RoundFailureFrame {
53
+ type: 'ROUND_FAILURE';
54
+ code: string;
55
+ }
56
+ export type RequestFrame = ReqHeaderFrame | RoundFailureFrame | PushCommitFrame | PullHeaderFrame | SubscriptionFrame | UnknownFrame;
49
57
  export interface RespHeaderFrame {
50
58
  type: 'RESP_HEADER';
51
59
  requiredSchemaVersion?: number;
@@ -183,6 +191,8 @@ export interface ResponseMessage {
183
191
  frames: ResponseFrame[];
184
192
  }
185
193
  export type SyncMessage = RequestMessage | ResponseMessage;
194
+ /** §1.5: `code` is a legal `ROUND_FAILURE` code (grammar and byte length). */
195
+ export declare function isRoundFailureCode(code: string): boolean;
186
196
  export declare function validateFrameSequence(message: SyncMessage): void;
187
197
  export declare function encodeMessage(message: SyncMessage): Uint8Array;
188
198
  export declare function decodeMessage(bytes: Uint8Array): SyncMessage;
package/dist/message.js CHANGED
@@ -37,6 +37,19 @@ function requireJsonDocument(raw, what) {
37
37
  }
38
38
  return raw;
39
39
  }
40
+ /** §1.5: a `ROUND_FAILURE` code is a lowercase dotted catalog code. */
41
+ const ROUND_FAILURE_CODE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
42
+ /** §1.5: `code` is a legal `ROUND_FAILURE` code (grammar and byte length). */
43
+ export function isRoundFailureCode(code) {
44
+ // At most 64 bytes on the wire (§1.5), so the UTF-8 length decides it.
45
+ return ROUND_FAILURE_CODE.test(code) && utf8Encode(code).length <= 64;
46
+ }
47
+ /** Shared by the frame decoder and encoder (§1.5 payload invariants). */
48
+ function validateRoundFailureCode(code, fail) {
49
+ if (!isRoundFailureCode(code)) {
50
+ fail('ROUND_FAILURE.code must be a lowercase dotted code of at most 64 bytes');
51
+ }
52
+ }
40
53
  // ---------------------------------------------------------------------------
41
54
  // Frame payload decoders (each consumes exactly the frame payload)
42
55
  // ---------------------------------------------------------------------------
@@ -62,6 +75,11 @@ function decodeReqHeader(r, wireVersion) {
62
75
  logEpoch: logEpoch,
63
76
  });
64
77
  }
78
+ function decodeRoundFailure(r) {
79
+ const code = r.str();
80
+ validateRoundFailureCode(code, invalid);
81
+ return { type: 'ROUND_FAILURE', code };
82
+ }
65
83
  function decodePushCommit(r) {
66
84
  const clientCommitId = r.str();
67
85
  if (clientCommitId.length === 0) {
@@ -388,6 +406,9 @@ function decodeRequestFrame(frameType, payload, wireVersion) {
388
406
  case FrameType.REQ_HEADER:
389
407
  frame = decodeReqHeader(r, wireVersion);
390
408
  break;
409
+ case FrameType.ROUND_FAILURE:
410
+ frame = decodeRoundFailure(r);
411
+ break;
391
412
  case FrameType.PUSH_COMMIT:
392
413
  frame = decodePushCommit(r);
393
414
  break;
@@ -459,11 +480,22 @@ function validateRequestSequence(frames) {
459
480
  }
460
481
  let pushCount = 0;
461
482
  let hasPull = false;
483
+ let index = 1;
484
+ let sawRoundFailure = false;
462
485
  for (const frame of frames.slice(1)) {
463
486
  switch (frame.type) {
464
487
  case 'REQ_HEADER':
465
488
  invalid('duplicate REQ_HEADER frame');
466
489
  break;
490
+ case 'ROUND_FAILURE':
491
+ // §1.5: at most one, immediately after REQ_HEADER.
492
+ if (sawRoundFailure)
493
+ invalid('duplicate ROUND_FAILURE frame');
494
+ if (index !== 1) {
495
+ invalid('ROUND_FAILURE frame must immediately follow REQ_HEADER');
496
+ }
497
+ sawRoundFailure = true;
498
+ break;
467
499
  case 'PUSH_COMMIT':
468
500
  if (hasPull)
469
501
  invalid('PUSH_COMMIT frame after PULL_HEADER');
@@ -481,6 +513,7 @@ function validateRequestSequence(frames) {
481
513
  case 'UNKNOWN':
482
514
  break;
483
515
  }
516
+ index += 1;
484
517
  }
485
518
  if (pushCount === 0 && !hasPull) {
486
519
  invalid('request must contain PUSH_COMMIT or PULL_HEADER frames');
@@ -631,6 +664,11 @@ function encodeFrame(frame, wireVersion) {
631
664
  }
632
665
  break;
633
666
  }
667
+ case 'ROUND_FAILURE': {
668
+ validateRoundFailureCode(frame.code, encodeInvalid);
669
+ w.str(frame.code);
670
+ break;
671
+ }
634
672
  case 'PUSH_COMMIT': {
635
673
  if (frame.clientCommitId.length === 0) {
636
674
  encodeInvalid('PUSH_COMMIT.clientCommitId must be non-empty');
package/dist/render.js CHANGED
@@ -99,6 +99,8 @@ function renderFrame(frame) {
99
99
  schemaVersion: frame.schemaVersion,
100
100
  ...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
101
101
  };
102
+ case 'ROUND_FAILURE':
103
+ return { type: 'ROUND_FAILURE', code: frame.code };
102
104
  case 'PUSH_COMMIT':
103
105
  return {
104
106
  type: 'PUSH_COMMIT',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/core",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Core protocol types and shared utilities for the Syncular sync framework",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
package/src/frames.ts CHANGED
@@ -5,6 +5,7 @@ export const FrameType = {
5
5
  PUSH_COMMIT: 0x02,
6
6
  PULL_HEADER: 0x03,
7
7
  SUBSCRIPTION: 0x04,
8
+ ROUND_FAILURE: 0x05,
8
9
  RESP_HEADER: 0x10,
9
10
  PUSH_RESULT: 0x11,
10
11
  SUB_START: 0x12,
@@ -25,6 +26,7 @@ export const REQUEST_FRAME_TYPES: ReadonlySet<number> = new Set([
25
26
  FrameType.PUSH_COMMIT,
26
27
  FrameType.PULL_HEADER,
27
28
  FrameType.SUBSCRIPTION,
29
+ FrameType.ROUND_FAILURE,
28
30
  ]);
29
31
 
30
32
  /** Known response frame types (excluding END). */
package/src/message.ts CHANGED
@@ -89,8 +89,18 @@ export interface UnknownFrame {
89
89
  payload: Uint8Array;
90
90
  }
91
91
 
92
+ /**
93
+ * §1.5: the code of the client's last round that failed where the server
94
+ * could not see it. Reported once, immediately after `REQ_HEADER`.
95
+ */
96
+ export interface RoundFailureFrame {
97
+ type: 'ROUND_FAILURE';
98
+ code: string;
99
+ }
100
+
92
101
  export type RequestFrame =
93
102
  | ReqHeaderFrame
103
+ | RoundFailureFrame
94
104
  | PushCommitFrame
95
105
  | PullHeaderFrame
96
106
  | SubscriptionFrame
@@ -285,6 +295,27 @@ function requireJsonDocument(raw: string, what: string): string {
285
295
  return raw;
286
296
  }
287
297
 
298
+ /** §1.5: a `ROUND_FAILURE` code is a lowercase dotted catalog code. */
299
+ const ROUND_FAILURE_CODE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
300
+
301
+ /** §1.5: `code` is a legal `ROUND_FAILURE` code (grammar and byte length). */
302
+ export function isRoundFailureCode(code: string): boolean {
303
+ // At most 64 bytes on the wire (§1.5), so the UTF-8 length decides it.
304
+ return ROUND_FAILURE_CODE.test(code) && utf8Encode(code).length <= 64;
305
+ }
306
+
307
+ /** Shared by the frame decoder and encoder (§1.5 payload invariants). */
308
+ function validateRoundFailureCode(
309
+ code: string,
310
+ fail: (message: string) => never,
311
+ ): void {
312
+ if (!isRoundFailureCode(code)) {
313
+ fail(
314
+ 'ROUND_FAILURE.code must be a lowercase dotted code of at most 64 bytes',
315
+ );
316
+ }
317
+ }
318
+
288
319
  // ---------------------------------------------------------------------------
289
320
  // Frame payload decoders (each consumes exactly the frame payload)
290
321
  // ---------------------------------------------------------------------------
@@ -311,6 +342,12 @@ function decodeReqHeader(r: ByteReader, wireVersion: number): ReqHeaderFrame {
311
342
  });
312
343
  }
313
344
 
345
+ function decodeRoundFailure(r: ByteReader): RoundFailureFrame {
346
+ const code = r.str();
347
+ validateRoundFailureCode(code, invalid);
348
+ return { type: 'ROUND_FAILURE', code };
349
+ }
350
+
314
351
  function decodePushCommit(r: ByteReader): PushCommitFrame {
315
352
  const clientCommitId = r.str();
316
353
  if (clientCommitId.length === 0) {
@@ -649,6 +686,9 @@ function decodeRequestFrame(
649
686
  case FrameType.REQ_HEADER:
650
687
  frame = decodeReqHeader(r, wireVersion);
651
688
  break;
689
+ case FrameType.ROUND_FAILURE:
690
+ frame = decodeRoundFailure(r);
691
+ break;
652
692
  case FrameType.PUSH_COMMIT:
653
693
  frame = decodePushCommit(r);
654
694
  break;
@@ -731,11 +771,21 @@ function validateRequestSequence(frames: readonly RequestFrame[]): void {
731
771
  }
732
772
  let pushCount = 0;
733
773
  let hasPull = false;
774
+ let index = 1;
775
+ let sawRoundFailure = false;
734
776
  for (const frame of frames.slice(1)) {
735
777
  switch (frame.type) {
736
778
  case 'REQ_HEADER':
737
779
  invalid('duplicate REQ_HEADER frame');
738
780
  break;
781
+ case 'ROUND_FAILURE':
782
+ // §1.5: at most one, immediately after REQ_HEADER.
783
+ if (sawRoundFailure) invalid('duplicate ROUND_FAILURE frame');
784
+ if (index !== 1) {
785
+ invalid('ROUND_FAILURE frame must immediately follow REQ_HEADER');
786
+ }
787
+ sawRoundFailure = true;
788
+ break;
739
789
  case 'PUSH_COMMIT':
740
790
  if (hasPull) invalid('PUSH_COMMIT frame after PULL_HEADER');
741
791
  pushCount += 1;
@@ -750,6 +800,7 @@ function validateRequestSequence(frames: readonly RequestFrame[]): void {
750
800
  case 'UNKNOWN':
751
801
  break;
752
802
  }
803
+ index += 1;
753
804
  }
754
805
  if (pushCount === 0 && !hasPull) {
755
806
  invalid('request must contain PUSH_COMMIT or PULL_HEADER frames');
@@ -913,6 +964,11 @@ function encodeFrame(
913
964
  }
914
965
  break;
915
966
  }
967
+ case 'ROUND_FAILURE': {
968
+ validateRoundFailureCode(frame.code, encodeInvalid);
969
+ w.str(frame.code);
970
+ break;
971
+ }
916
972
  case 'PUSH_COMMIT': {
917
973
  if (frame.clientCommitId.length === 0) {
918
974
  encodeInvalid('PUSH_COMMIT.clientCommitId must be non-empty');
package/src/render.ts CHANGED
@@ -140,6 +140,8 @@ function renderFrame(frame: RequestFrame | ResponseFrame): JsonValue {
140
140
  schemaVersion: frame.schemaVersion,
141
141
  ...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
142
142
  };
143
+ case 'ROUND_FAILURE':
144
+ return { type: 'ROUND_FAILURE', code: frame.code };
143
145
  case 'PUSH_COMMIT':
144
146
  return {
145
147
  type: 'PUSH_COMMIT',