@syncular/core 0.39.2 → 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 +1 -0
- package/dist/frames.js +2 -0
- package/dist/message.d.ts +11 -1
- package/dist/message.js +38 -0
- package/dist/render.js +2 -0
- package/package.json +1 -1
- package/src/frames.ts +2 -0
- package/src/message.ts +56 -0
- package/src/render.ts +2 -0
package/dist/frames.d.ts
CHANGED
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
|
-
|
|
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
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',
|