@syncular/core 0.15.46 → 0.15.48
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/constants.d.ts +8 -2
- package/dist/constants.js +14 -2
- package/dist/frames.d.ts +2 -2
- package/dist/frames.js +2 -2
- package/dist/message.d.ts +6 -0
- package/dist/message.js +57 -14
- package/dist/render.js +5 -0
- package/dist/stream.js +2 -2
- package/package.json +1 -1
- package/src/constants.ts +25 -2
- package/src/frames.ts +2 -2
- package/src/message.ts +71 -12
- package/src/render.ts +5 -0
- package/src/stream.ts +2 -2
package/dist/constants.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
|
-
/** SSP2 envelope wire version
|
|
3
|
-
export declare const PROTOCOL_WIRE_VERSION =
|
|
2
|
+
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
3
|
+
export declare const PROTOCOL_WIRE_VERSION = 2;
|
|
4
|
+
/** Oldest SSP2 envelope wire version accepted during the epoch rollout. */
|
|
5
|
+
export declare const MINIMUM_PROTOCOL_WIRE_VERSION = 1;
|
|
6
|
+
/** Wire versions implemented by the reference codec, oldest first. */
|
|
7
|
+
export declare const SUPPORTED_PROTOCOL_WIRE_VERSIONS: readonly number[];
|
|
8
|
+
/** Whether the reference codec implements this exact wire version. */
|
|
9
|
+
export declare function isSupportedProtocolWireVersion(wireVersion: number): boolean;
|
|
4
10
|
/** Magic bytes opening every SSP2 sync envelope (SPEC.md §1.2). */
|
|
5
11
|
export declare const SYNC_PACK_MAGIC = "SSP2";
|
package/dist/constants.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
|
-
/** SSP2 envelope wire version
|
|
3
|
-
export const PROTOCOL_WIRE_VERSION =
|
|
2
|
+
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
3
|
+
export const PROTOCOL_WIRE_VERSION = 2;
|
|
4
|
+
/** Oldest SSP2 envelope wire version accepted during the epoch rollout. */
|
|
5
|
+
export const MINIMUM_PROTOCOL_WIRE_VERSION = 1;
|
|
6
|
+
/** Wire versions implemented by the reference codec, oldest first. */
|
|
7
|
+
export const SUPPORTED_PROTOCOL_WIRE_VERSIONS = Object.freeze(Array.from({
|
|
8
|
+
length: PROTOCOL_WIRE_VERSION - MINIMUM_PROTOCOL_WIRE_VERSION + 1,
|
|
9
|
+
}, (_, index) => MINIMUM_PROTOCOL_WIRE_VERSION + index));
|
|
10
|
+
/** Whether the reference codec implements this exact wire version. */
|
|
11
|
+
export function isSupportedProtocolWireVersion(wireVersion) {
|
|
12
|
+
return (Number.isInteger(wireVersion) &&
|
|
13
|
+
wireVersion >= MINIMUM_PROTOCOL_WIRE_VERSION &&
|
|
14
|
+
wireVersion <= PROTOCOL_WIRE_VERSION);
|
|
15
|
+
}
|
|
4
16
|
/** Magic bytes opening every SSP2 sync envelope (SPEC.md §1.2). */
|
|
5
17
|
export const SYNC_PACK_MAGIC = 'SSP2';
|
package/dist/frames.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Frame type registry for wire
|
|
1
|
+
/** Frame type registry for wire versions 1 and 2 (SPEC.md §1.2). */
|
|
2
2
|
export declare const FrameType: {
|
|
3
3
|
readonly END: 0;
|
|
4
4
|
readonly REQ_HEADER: 1;
|
|
@@ -21,5 +21,5 @@ export type FrameTypeName = keyof typeof FrameType;
|
|
|
21
21
|
export declare const REQUEST_FRAME_TYPES: ReadonlySet<number>;
|
|
22
22
|
/** Known response frame types (excluding END). */
|
|
23
23
|
export declare const RESPONSE_FRAME_TYPES: ReadonlySet<number>;
|
|
24
|
-
/** Every frame type with a defined layout in wire
|
|
24
|
+
/** Every frame type with a defined layout in wire versions 1 and 2. */
|
|
25
25
|
export declare const KNOWN_FRAME_TYPES: ReadonlySet<number>;
|
package/dist/frames.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Frame type registry for wire
|
|
1
|
+
/** Frame type registry for wire versions 1 and 2 (SPEC.md §1.2). */
|
|
2
2
|
export const FrameType = {
|
|
3
3
|
END: 0x00,
|
|
4
4
|
REQ_HEADER: 0x01,
|
|
@@ -36,7 +36,7 @@ export const RESPONSE_FRAME_TYPES = new Set([
|
|
|
36
36
|
FrameType.PUSH_RESULT_DETAILS,
|
|
37
37
|
FrameType.ERROR,
|
|
38
38
|
]);
|
|
39
|
-
/** Every frame type with a defined layout in wire
|
|
39
|
+
/** Every frame type with a defined layout in wire versions 1 and 2. */
|
|
40
40
|
export const KNOWN_FRAME_TYPES = new Set([
|
|
41
41
|
FrameType.END,
|
|
42
42
|
...REQUEST_FRAME_TYPES,
|
package/dist/message.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface ReqHeaderFrame {
|
|
|
4
4
|
type: 'REQ_HEADER';
|
|
5
5
|
clientId: string;
|
|
6
6
|
schemaVersion: number;
|
|
7
|
+
/** Wire v2 partition log identity; absent requests epoch acquisition. */
|
|
8
|
+
logEpoch?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface PushOperation {
|
|
9
11
|
table: string;
|
|
@@ -48,6 +50,10 @@ export interface RespHeaderFrame {
|
|
|
48
50
|
type: 'RESP_HEADER';
|
|
49
51
|
requiredSchemaVersion?: number;
|
|
50
52
|
latestSchemaVersion?: number;
|
|
53
|
+
/** Required and non-empty on wire v2. */
|
|
54
|
+
logEpoch?: string;
|
|
55
|
+
/** Required on wire v2. True produces a header-only reset response. */
|
|
56
|
+
resetRequired?: boolean;
|
|
51
57
|
}
|
|
52
58
|
/** §7.3.2: a server-issued auth lease delivered to the client (opaque). */
|
|
53
59
|
export interface LeaseFrame {
|
package/dist/message.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* grammar, retained so canonical re-encoding stays byte-exact).
|
|
13
13
|
*/
|
|
14
14
|
import { ByteReader, ByteWriter, readStringListMap, readStringMap, utf8Encode, writeStringListMap, writeStringMap, } from './bytes.js';
|
|
15
|
-
import {
|
|
15
|
+
import { isSupportedProtocolWireVersion, SYNC_PACK_MAGIC } from './constants.js';
|
|
16
16
|
import { DecodeError } from './errors.js';
|
|
17
17
|
import { FrameType, KNOWN_FRAME_TYPES, REQUEST_FRAME_TYPES, RESPONSE_FRAME_TYPES, } from './frames.js';
|
|
18
18
|
import { normalizeRejectionDetails, } from './rejection-details.js';
|
|
@@ -33,14 +33,27 @@ function requireJsonDocument(raw, what) {
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
// Frame payload decoders (each consumes exactly the frame payload)
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
function decodeReqHeader(r) {
|
|
36
|
+
function decodeReqHeader(r, wireVersion) {
|
|
37
37
|
const clientId = r.str();
|
|
38
38
|
if (clientId.length === 0)
|
|
39
39
|
invalid('REQ_HEADER.clientId must be non-empty');
|
|
40
40
|
const schemaVersion = r.i32();
|
|
41
41
|
if (schemaVersion < 1)
|
|
42
42
|
invalid('REQ_HEADER.schemaVersion must be >= 1');
|
|
43
|
-
|
|
43
|
+
const logEpoch = wireVersion >= 2
|
|
44
|
+
? r.opt(() => {
|
|
45
|
+
const value = r.str();
|
|
46
|
+
if (value.length === 0)
|
|
47
|
+
invalid('REQ_HEADER.logEpoch must be non-empty');
|
|
48
|
+
return value;
|
|
49
|
+
})
|
|
50
|
+
: undefined;
|
|
51
|
+
return {
|
|
52
|
+
type: 'REQ_HEADER',
|
|
53
|
+
clientId,
|
|
54
|
+
schemaVersion,
|
|
55
|
+
...(logEpoch !== undefined ? { logEpoch } : {}),
|
|
56
|
+
};
|
|
44
57
|
}
|
|
45
58
|
function decodePushCommit(r) {
|
|
46
59
|
const clientCommitId = r.str();
|
|
@@ -114,13 +127,20 @@ function decodeSubscription(r) {
|
|
|
114
127
|
...(bootstrapState !== undefined ? { bootstrapState } : {}),
|
|
115
128
|
};
|
|
116
129
|
}
|
|
117
|
-
function decodeRespHeader(r) {
|
|
130
|
+
function decodeRespHeader(r, wireVersion) {
|
|
118
131
|
const requiredSchemaVersion = r.opt(() => r.i32());
|
|
119
132
|
const latestSchemaVersion = r.opt(() => r.i32());
|
|
133
|
+
const logEpoch = wireVersion >= 2 ? r.str() : undefined;
|
|
134
|
+
if (logEpoch !== undefined && logEpoch.length === 0) {
|
|
135
|
+
invalid('RESP_HEADER.logEpoch must be non-empty');
|
|
136
|
+
}
|
|
137
|
+
const resetRequired = wireVersion >= 2 ? r.bool() : undefined;
|
|
120
138
|
return {
|
|
121
139
|
type: 'RESP_HEADER',
|
|
122
140
|
...(requiredSchemaVersion !== undefined ? { requiredSchemaVersion } : {}),
|
|
123
141
|
...(latestSchemaVersion !== undefined ? { latestSchemaVersion } : {}),
|
|
142
|
+
...(logEpoch !== undefined ? { logEpoch } : {}),
|
|
143
|
+
...(resetRequired !== undefined ? { resetRequired } : {}),
|
|
124
144
|
};
|
|
125
145
|
}
|
|
126
146
|
function decodeLease(r) {
|
|
@@ -353,12 +373,12 @@ function decodeErrorFrame(r) {
|
|
|
353
373
|
...(details !== undefined ? { details } : {}),
|
|
354
374
|
};
|
|
355
375
|
}
|
|
356
|
-
function decodeRequestFrame(frameType, payload) {
|
|
376
|
+
function decodeRequestFrame(frameType, payload, wireVersion) {
|
|
357
377
|
const r = new ByteReader(payload);
|
|
358
378
|
let frame;
|
|
359
379
|
switch (frameType) {
|
|
360
380
|
case FrameType.REQ_HEADER:
|
|
361
|
-
frame = decodeReqHeader(r);
|
|
381
|
+
frame = decodeReqHeader(r, wireVersion);
|
|
362
382
|
break;
|
|
363
383
|
case FrameType.PUSH_COMMIT:
|
|
364
384
|
frame = decodePushCommit(r);
|
|
@@ -378,12 +398,12 @@ function decodeRequestFrame(frameType, payload) {
|
|
|
378
398
|
r.expectFullyConsumed(`${frame.type} frame payload`);
|
|
379
399
|
return frame;
|
|
380
400
|
}
|
|
381
|
-
function decodeResponseFrame(frameType, payload) {
|
|
401
|
+
function decodeResponseFrame(frameType, payload, wireVersion) {
|
|
382
402
|
const r = new ByteReader(payload);
|
|
383
403
|
let frame;
|
|
384
404
|
switch (frameType) {
|
|
385
405
|
case FrameType.RESP_HEADER:
|
|
386
|
-
frame = decodeRespHeader(r);
|
|
406
|
+
frame = decodeRespHeader(r, wireVersion);
|
|
387
407
|
break;
|
|
388
408
|
case FrameType.LEASE:
|
|
389
409
|
frame = decodeLease(r);
|
|
@@ -574,7 +594,7 @@ const OP_BYTES = { upsert: 1, delete: 2 };
|
|
|
574
594
|
const PUSH_STATUS_BYTES = { applied: 1, cached: 2, rejected: 3 };
|
|
575
595
|
const SUB_STATUS_BYTES = { active: 1, revoked: 2, reset: 3 };
|
|
576
596
|
const MEDIA_TYPE_BYTES = { rows: 1, sqlite: 2 };
|
|
577
|
-
function encodeFrame(frame) {
|
|
597
|
+
function encodeFrame(frame, wireVersion) {
|
|
578
598
|
const w = new ByteWriter();
|
|
579
599
|
switch (frame.type) {
|
|
580
600
|
case 'REQ_HEADER': {
|
|
@@ -586,6 +606,15 @@ function encodeFrame(frame) {
|
|
|
586
606
|
}
|
|
587
607
|
w.str(frame.clientId);
|
|
588
608
|
w.i32(frame.schemaVersion);
|
|
609
|
+
if (wireVersion >= 2) {
|
|
610
|
+
if (frame.logEpoch === '') {
|
|
611
|
+
throw new Error('REQ_HEADER.logEpoch must be non-empty');
|
|
612
|
+
}
|
|
613
|
+
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
614
|
+
}
|
|
615
|
+
else if (frame.logEpoch !== undefined) {
|
|
616
|
+
throw new Error('REQ_HEADER.logEpoch requires wireVersion 2');
|
|
617
|
+
}
|
|
589
618
|
return { frameType: FrameType.REQ_HEADER, payload: w.finish() };
|
|
590
619
|
}
|
|
591
620
|
case 'PUSH_COMMIT': {
|
|
@@ -634,6 +663,20 @@ function encodeFrame(frame) {
|
|
|
634
663
|
case 'RESP_HEADER': {
|
|
635
664
|
w.opt(frame.requiredSchemaVersion, (v) => w.i32(v));
|
|
636
665
|
w.opt(frame.latestSchemaVersion, (v) => w.i32(v));
|
|
666
|
+
if (wireVersion >= 2) {
|
|
667
|
+
if (frame.logEpoch === undefined || frame.logEpoch.length === 0) {
|
|
668
|
+
throw new Error('RESP_HEADER.logEpoch is required and non-empty');
|
|
669
|
+
}
|
|
670
|
+
if (frame.resetRequired === undefined) {
|
|
671
|
+
throw new Error('RESP_HEADER.resetRequired is required');
|
|
672
|
+
}
|
|
673
|
+
w.str(frame.logEpoch);
|
|
674
|
+
w.bool(frame.resetRequired);
|
|
675
|
+
}
|
|
676
|
+
else if (frame.logEpoch !== undefined ||
|
|
677
|
+
frame.resetRequired !== undefined) {
|
|
678
|
+
throw new Error('RESP_HEADER epoch fields require wireVersion 2');
|
|
679
|
+
}
|
|
637
680
|
return { frameType: FrameType.RESP_HEADER, payload: w.finish() };
|
|
638
681
|
}
|
|
639
682
|
case 'LEASE': {
|
|
@@ -788,7 +831,7 @@ function encodeFrame(frame) {
|
|
|
788
831
|
// Envelope
|
|
789
832
|
// ---------------------------------------------------------------------------
|
|
790
833
|
export function encodeMessage(message) {
|
|
791
|
-
if (message.wireVersion
|
|
834
|
+
if (!isSupportedProtocolWireVersion(message.wireVersion)) {
|
|
792
835
|
throw new Error(`unsupported wireVersion ${message.wireVersion}`);
|
|
793
836
|
}
|
|
794
837
|
validateFrameSequence(message);
|
|
@@ -798,7 +841,7 @@ export function encodeMessage(message) {
|
|
|
798
841
|
w.u8(message.msgKind === 'request' ? 0x01 : 0x02);
|
|
799
842
|
w.u8(0x00);
|
|
800
843
|
for (const frame of message.frames) {
|
|
801
|
-
const { frameType, payload } = encodeFrame(frame);
|
|
844
|
+
const { frameType, payload } = encodeFrame(frame, message.wireVersion);
|
|
802
845
|
w.u8(frameType);
|
|
803
846
|
w.u32(payload.length);
|
|
804
847
|
w.raw(payload);
|
|
@@ -840,7 +883,7 @@ export function decodeMessage(bytes) {
|
|
|
840
883
|
invalid('bad envelope magic');
|
|
841
884
|
}
|
|
842
885
|
const wireVersion = r.u16();
|
|
843
|
-
if (wireVersion
|
|
886
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
844
887
|
invalid(`unsupported wireVersion ${wireVersion}`);
|
|
845
888
|
}
|
|
846
889
|
const kindByte = r.u8();
|
|
@@ -854,7 +897,7 @@ export function decodeMessage(bytes) {
|
|
|
854
897
|
const message = {
|
|
855
898
|
wireVersion,
|
|
856
899
|
msgKind: 'request',
|
|
857
|
-
frames: readFrames(r, decodeRequestFrame),
|
|
900
|
+
frames: readFrames(r, (frameType, payload) => decodeRequestFrame(frameType, payload, wireVersion)),
|
|
858
901
|
};
|
|
859
902
|
validateRequestSequence(message.frames);
|
|
860
903
|
return message;
|
|
@@ -862,7 +905,7 @@ export function decodeMessage(bytes) {
|
|
|
862
905
|
const message = {
|
|
863
906
|
wireVersion,
|
|
864
907
|
msgKind: 'response',
|
|
865
|
-
frames: readFrames(r, decodeResponseFrame),
|
|
908
|
+
frames: readFrames(r, (frameType, payload) => decodeResponseFrame(frameType, payload, wireVersion)),
|
|
866
909
|
};
|
|
867
910
|
validateResponseSequence(message.frames);
|
|
868
911
|
return message;
|
package/dist/render.js
CHANGED
|
@@ -96,6 +96,7 @@ function renderFrame(frame) {
|
|
|
96
96
|
type: 'REQ_HEADER',
|
|
97
97
|
clientId: frame.clientId,
|
|
98
98
|
schemaVersion: frame.schemaVersion,
|
|
99
|
+
...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
|
|
99
100
|
};
|
|
100
101
|
case 'PUSH_COMMIT':
|
|
101
102
|
return {
|
|
@@ -134,6 +135,10 @@ function renderFrame(frame) {
|
|
|
134
135
|
...(frame.latestSchemaVersion !== undefined
|
|
135
136
|
? { latestSchemaVersion: frame.latestSchemaVersion }
|
|
136
137
|
: {}),
|
|
138
|
+
...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
|
|
139
|
+
...(frame.resetRequired !== undefined
|
|
140
|
+
? { resetRequired: frame.resetRequired }
|
|
141
|
+
: {}),
|
|
137
142
|
};
|
|
138
143
|
case 'LEASE':
|
|
139
144
|
return {
|
package/dist/stream.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `decodeMessage`.
|
|
16
16
|
*/
|
|
17
17
|
import { utf8Encode } from './bytes.js';
|
|
18
|
-
import {
|
|
18
|
+
import { isSupportedProtocolWireVersion, SYNC_PACK_MAGIC } from './constants.js';
|
|
19
19
|
import { DecodeError } from './errors.js';
|
|
20
20
|
const MAGIC = utf8Encode(SYNC_PACK_MAGIC);
|
|
21
21
|
/** `END` frame type byte (§1.2). */
|
|
@@ -89,7 +89,7 @@ export class MessageStreamScanner {
|
|
|
89
89
|
throw new DecodeError('sync.invalid_request', 'bad envelope magic');
|
|
90
90
|
}
|
|
91
91
|
const wireVersion = b[4] | (b[5] << 8);
|
|
92
|
-
if (wireVersion
|
|
92
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
93
93
|
throw new DecodeError('sync.invalid_request', `unsupported wireVersion ${wireVersion}`);
|
|
94
94
|
}
|
|
95
95
|
if (b[6] !== 0x01 && b[6] !== 0x02) {
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
2
|
|
|
3
|
-
/** SSP2 envelope wire version
|
|
4
|
-
export const PROTOCOL_WIRE_VERSION =
|
|
3
|
+
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
4
|
+
export const PROTOCOL_WIRE_VERSION = 2;
|
|
5
|
+
|
|
6
|
+
/** Oldest SSP2 envelope wire version accepted during the epoch rollout. */
|
|
7
|
+
export const MINIMUM_PROTOCOL_WIRE_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
/** Wire versions implemented by the reference codec, oldest first. */
|
|
10
|
+
export const SUPPORTED_PROTOCOL_WIRE_VERSIONS: readonly number[] =
|
|
11
|
+
Object.freeze(
|
|
12
|
+
Array.from(
|
|
13
|
+
{
|
|
14
|
+
length: PROTOCOL_WIRE_VERSION - MINIMUM_PROTOCOL_WIRE_VERSION + 1,
|
|
15
|
+
},
|
|
16
|
+
(_, index) => MINIMUM_PROTOCOL_WIRE_VERSION + index,
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/** Whether the reference codec implements this exact wire version. */
|
|
21
|
+
export function isSupportedProtocolWireVersion(wireVersion: number): boolean {
|
|
22
|
+
return (
|
|
23
|
+
Number.isInteger(wireVersion) &&
|
|
24
|
+
wireVersion >= MINIMUM_PROTOCOL_WIRE_VERSION &&
|
|
25
|
+
wireVersion <= PROTOCOL_WIRE_VERSION
|
|
26
|
+
);
|
|
27
|
+
}
|
|
5
28
|
|
|
6
29
|
/** Magic bytes opening every SSP2 sync envelope (SPEC.md §1.2). */
|
|
7
30
|
export const SYNC_PACK_MAGIC = 'SSP2';
|
package/src/frames.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Frame type registry for wire
|
|
1
|
+
/** Frame type registry for wire versions 1 and 2 (SPEC.md §1.2). */
|
|
2
2
|
export const FrameType = {
|
|
3
3
|
END: 0x00,
|
|
4
4
|
REQ_HEADER: 0x01,
|
|
@@ -41,7 +41,7 @@ export const RESPONSE_FRAME_TYPES: ReadonlySet<number> = new Set([
|
|
|
41
41
|
FrameType.ERROR,
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
-
/** Every frame type with a defined layout in wire
|
|
44
|
+
/** Every frame type with a defined layout in wire versions 1 and 2. */
|
|
45
45
|
export const KNOWN_FRAME_TYPES: ReadonlySet<number> = new Set([
|
|
46
46
|
FrameType.END,
|
|
47
47
|
...REQUEST_FRAME_TYPES,
|
package/src/message.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
writeStringListMap,
|
|
21
21
|
writeStringMap,
|
|
22
22
|
} from './bytes';
|
|
23
|
-
import {
|
|
23
|
+
import { isSupportedProtocolWireVersion, SYNC_PACK_MAGIC } from './constants';
|
|
24
24
|
import { DecodeError } from './errors';
|
|
25
25
|
import {
|
|
26
26
|
FrameType,
|
|
@@ -42,6 +42,8 @@ export interface ReqHeaderFrame {
|
|
|
42
42
|
type: 'REQ_HEADER';
|
|
43
43
|
clientId: string;
|
|
44
44
|
schemaVersion: number;
|
|
45
|
+
/** Wire v2 partition log identity; absent requests epoch acquisition. */
|
|
46
|
+
logEpoch?: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface PushOperation {
|
|
@@ -98,6 +100,10 @@ export interface RespHeaderFrame {
|
|
|
98
100
|
type: 'RESP_HEADER';
|
|
99
101
|
requiredSchemaVersion?: number;
|
|
100
102
|
latestSchemaVersion?: number;
|
|
103
|
+
/** Required and non-empty on wire v2. */
|
|
104
|
+
logEpoch?: string;
|
|
105
|
+
/** Required on wire v2. True produces a header-only reset response. */
|
|
106
|
+
resetRequired?: boolean;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
/** §7.3.2: a server-issued auth lease delivered to the client (opaque). */
|
|
@@ -261,12 +267,26 @@ function requireJsonDocument(raw: string, what: string): string {
|
|
|
261
267
|
// Frame payload decoders (each consumes exactly the frame payload)
|
|
262
268
|
// ---------------------------------------------------------------------------
|
|
263
269
|
|
|
264
|
-
function decodeReqHeader(r: ByteReader): ReqHeaderFrame {
|
|
270
|
+
function decodeReqHeader(r: ByteReader, wireVersion: number): ReqHeaderFrame {
|
|
265
271
|
const clientId = r.str();
|
|
266
272
|
if (clientId.length === 0) invalid('REQ_HEADER.clientId must be non-empty');
|
|
267
273
|
const schemaVersion = r.i32();
|
|
268
274
|
if (schemaVersion < 1) invalid('REQ_HEADER.schemaVersion must be >= 1');
|
|
269
|
-
|
|
275
|
+
const logEpoch =
|
|
276
|
+
wireVersion >= 2
|
|
277
|
+
? r.opt(() => {
|
|
278
|
+
const value = r.str();
|
|
279
|
+
if (value.length === 0)
|
|
280
|
+
invalid('REQ_HEADER.logEpoch must be non-empty');
|
|
281
|
+
return value;
|
|
282
|
+
})
|
|
283
|
+
: undefined;
|
|
284
|
+
return {
|
|
285
|
+
type: 'REQ_HEADER',
|
|
286
|
+
clientId,
|
|
287
|
+
schemaVersion,
|
|
288
|
+
...(logEpoch !== undefined ? { logEpoch } : {}),
|
|
289
|
+
};
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
function decodePushCommit(r: ByteReader): PushCommitFrame {
|
|
@@ -348,13 +368,20 @@ function decodeSubscription(r: ByteReader): SubscriptionFrame {
|
|
|
348
368
|
};
|
|
349
369
|
}
|
|
350
370
|
|
|
351
|
-
function decodeRespHeader(r: ByteReader): RespHeaderFrame {
|
|
371
|
+
function decodeRespHeader(r: ByteReader, wireVersion: number): RespHeaderFrame {
|
|
352
372
|
const requiredSchemaVersion = r.opt(() => r.i32());
|
|
353
373
|
const latestSchemaVersion = r.opt(() => r.i32());
|
|
374
|
+
const logEpoch = wireVersion >= 2 ? r.str() : undefined;
|
|
375
|
+
if (logEpoch !== undefined && logEpoch.length === 0) {
|
|
376
|
+
invalid('RESP_HEADER.logEpoch must be non-empty');
|
|
377
|
+
}
|
|
378
|
+
const resetRequired = wireVersion >= 2 ? r.bool() : undefined;
|
|
354
379
|
return {
|
|
355
380
|
type: 'RESP_HEADER',
|
|
356
381
|
...(requiredSchemaVersion !== undefined ? { requiredSchemaVersion } : {}),
|
|
357
382
|
...(latestSchemaVersion !== undefined ? { latestSchemaVersion } : {}),
|
|
383
|
+
...(logEpoch !== undefined ? { logEpoch } : {}),
|
|
384
|
+
...(resetRequired !== undefined ? { resetRequired } : {}),
|
|
358
385
|
};
|
|
359
386
|
}
|
|
360
387
|
|
|
@@ -587,12 +614,13 @@ function decodeErrorFrame(r: ByteReader): ErrorFrame {
|
|
|
587
614
|
function decodeRequestFrame(
|
|
588
615
|
frameType: number,
|
|
589
616
|
payload: Uint8Array,
|
|
617
|
+
wireVersion: number,
|
|
590
618
|
): RequestFrame {
|
|
591
619
|
const r = new ByteReader(payload);
|
|
592
620
|
let frame: RequestFrame;
|
|
593
621
|
switch (frameType) {
|
|
594
622
|
case FrameType.REQ_HEADER:
|
|
595
|
-
frame = decodeReqHeader(r);
|
|
623
|
+
frame = decodeReqHeader(r, wireVersion);
|
|
596
624
|
break;
|
|
597
625
|
case FrameType.PUSH_COMMIT:
|
|
598
626
|
frame = decodePushCommit(r);
|
|
@@ -618,12 +646,13 @@ function decodeRequestFrame(
|
|
|
618
646
|
function decodeResponseFrame(
|
|
619
647
|
frameType: number,
|
|
620
648
|
payload: Uint8Array,
|
|
649
|
+
wireVersion: number,
|
|
621
650
|
): ResponseFrame {
|
|
622
651
|
const r = new ByteReader(payload);
|
|
623
652
|
let frame: ResponseFrame;
|
|
624
653
|
switch (frameType) {
|
|
625
654
|
case FrameType.RESP_HEADER:
|
|
626
|
-
frame = decodeRespHeader(r);
|
|
655
|
+
frame = decodeRespHeader(r, wireVersion);
|
|
627
656
|
break;
|
|
628
657
|
case FrameType.LEASE:
|
|
629
658
|
frame = decodeLease(r);
|
|
@@ -822,7 +851,10 @@ const PUSH_STATUS_BYTES = { applied: 1, cached: 2, rejected: 3 } as const;
|
|
|
822
851
|
const SUB_STATUS_BYTES = { active: 1, revoked: 2, reset: 3 } as const;
|
|
823
852
|
const MEDIA_TYPE_BYTES = { rows: 1, sqlite: 2 } as const;
|
|
824
853
|
|
|
825
|
-
function encodeFrame(
|
|
854
|
+
function encodeFrame(
|
|
855
|
+
frame: RequestFrame | ResponseFrame,
|
|
856
|
+
wireVersion: number,
|
|
857
|
+
): {
|
|
826
858
|
frameType: number;
|
|
827
859
|
payload: Uint8Array;
|
|
828
860
|
} {
|
|
@@ -837,6 +869,14 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
837
869
|
}
|
|
838
870
|
w.str(frame.clientId);
|
|
839
871
|
w.i32(frame.schemaVersion);
|
|
872
|
+
if (wireVersion >= 2) {
|
|
873
|
+
if (frame.logEpoch === '') {
|
|
874
|
+
throw new Error('REQ_HEADER.logEpoch must be non-empty');
|
|
875
|
+
}
|
|
876
|
+
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
877
|
+
} else if (frame.logEpoch !== undefined) {
|
|
878
|
+
throw new Error('REQ_HEADER.logEpoch requires wireVersion 2');
|
|
879
|
+
}
|
|
840
880
|
return { frameType: FrameType.REQ_HEADER, payload: w.finish() };
|
|
841
881
|
}
|
|
842
882
|
case 'PUSH_COMMIT': {
|
|
@@ -885,6 +925,21 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
885
925
|
case 'RESP_HEADER': {
|
|
886
926
|
w.opt(frame.requiredSchemaVersion, (v) => w.i32(v));
|
|
887
927
|
w.opt(frame.latestSchemaVersion, (v) => w.i32(v));
|
|
928
|
+
if (wireVersion >= 2) {
|
|
929
|
+
if (frame.logEpoch === undefined || frame.logEpoch.length === 0) {
|
|
930
|
+
throw new Error('RESP_HEADER.logEpoch is required and non-empty');
|
|
931
|
+
}
|
|
932
|
+
if (frame.resetRequired === undefined) {
|
|
933
|
+
throw new Error('RESP_HEADER.resetRequired is required');
|
|
934
|
+
}
|
|
935
|
+
w.str(frame.logEpoch);
|
|
936
|
+
w.bool(frame.resetRequired);
|
|
937
|
+
} else if (
|
|
938
|
+
frame.logEpoch !== undefined ||
|
|
939
|
+
frame.resetRequired !== undefined
|
|
940
|
+
) {
|
|
941
|
+
throw new Error('RESP_HEADER epoch fields require wireVersion 2');
|
|
942
|
+
}
|
|
888
943
|
return { frameType: FrameType.RESP_HEADER, payload: w.finish() };
|
|
889
944
|
}
|
|
890
945
|
case 'LEASE': {
|
|
@@ -1050,7 +1105,7 @@ function encodeFrame(frame: RequestFrame | ResponseFrame): {
|
|
|
1050
1105
|
// ---------------------------------------------------------------------------
|
|
1051
1106
|
|
|
1052
1107
|
export function encodeMessage(message: SyncMessage): Uint8Array {
|
|
1053
|
-
if (message.wireVersion
|
|
1108
|
+
if (!isSupportedProtocolWireVersion(message.wireVersion)) {
|
|
1054
1109
|
throw new Error(`unsupported wireVersion ${message.wireVersion}`);
|
|
1055
1110
|
}
|
|
1056
1111
|
validateFrameSequence(message);
|
|
@@ -1060,7 +1115,7 @@ export function encodeMessage(message: SyncMessage): Uint8Array {
|
|
|
1060
1115
|
w.u8(message.msgKind === 'request' ? 0x01 : 0x02);
|
|
1061
1116
|
w.u8(0x00);
|
|
1062
1117
|
for (const frame of message.frames) {
|
|
1063
|
-
const { frameType, payload } = encodeFrame(frame);
|
|
1118
|
+
const { frameType, payload } = encodeFrame(frame, message.wireVersion);
|
|
1064
1119
|
w.u8(frameType);
|
|
1065
1120
|
w.u32(payload.length);
|
|
1066
1121
|
w.raw(payload);
|
|
@@ -1109,7 +1164,7 @@ export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
|
1109
1164
|
invalid('bad envelope magic');
|
|
1110
1165
|
}
|
|
1111
1166
|
const wireVersion = r.u16();
|
|
1112
|
-
if (wireVersion
|
|
1167
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
1113
1168
|
invalid(`unsupported wireVersion ${wireVersion}`);
|
|
1114
1169
|
}
|
|
1115
1170
|
const kindByte = r.u8();
|
|
@@ -1122,7 +1177,9 @@ export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
|
1122
1177
|
const message: RequestMessage = {
|
|
1123
1178
|
wireVersion,
|
|
1124
1179
|
msgKind: 'request',
|
|
1125
|
-
frames: readFrames(r,
|
|
1180
|
+
frames: readFrames(r, (frameType, payload) =>
|
|
1181
|
+
decodeRequestFrame(frameType, payload, wireVersion),
|
|
1182
|
+
),
|
|
1126
1183
|
};
|
|
1127
1184
|
validateRequestSequence(message.frames);
|
|
1128
1185
|
return message;
|
|
@@ -1130,7 +1187,9 @@ export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
|
1130
1187
|
const message: ResponseMessage = {
|
|
1131
1188
|
wireVersion,
|
|
1132
1189
|
msgKind: 'response',
|
|
1133
|
-
frames: readFrames(r,
|
|
1190
|
+
frames: readFrames(r, (frameType, payload) =>
|
|
1191
|
+
decodeResponseFrame(frameType, payload, wireVersion),
|
|
1192
|
+
),
|
|
1134
1193
|
};
|
|
1135
1194
|
validateResponseSequence(message.frames);
|
|
1136
1195
|
return message;
|
package/src/render.ts
CHANGED
|
@@ -137,6 +137,7 @@ function renderFrame(frame: RequestFrame | ResponseFrame): JsonValue {
|
|
|
137
137
|
type: 'REQ_HEADER',
|
|
138
138
|
clientId: frame.clientId,
|
|
139
139
|
schemaVersion: frame.schemaVersion,
|
|
140
|
+
...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
|
|
140
141
|
};
|
|
141
142
|
case 'PUSH_COMMIT':
|
|
142
143
|
return {
|
|
@@ -175,6 +176,10 @@ function renderFrame(frame: RequestFrame | ResponseFrame): JsonValue {
|
|
|
175
176
|
...(frame.latestSchemaVersion !== undefined
|
|
176
177
|
? { latestSchemaVersion: frame.latestSchemaVersion }
|
|
177
178
|
: {}),
|
|
179
|
+
...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
|
|
180
|
+
...(frame.resetRequired !== undefined
|
|
181
|
+
? { resetRequired: frame.resetRequired }
|
|
182
|
+
: {}),
|
|
178
183
|
};
|
|
179
184
|
case 'LEASE':
|
|
180
185
|
return {
|
package/src/stream.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `decodeMessage`.
|
|
16
16
|
*/
|
|
17
17
|
import { utf8Encode } from './bytes';
|
|
18
|
-
import {
|
|
18
|
+
import { isSupportedProtocolWireVersion, SYNC_PACK_MAGIC } from './constants';
|
|
19
19
|
import { DecodeError } from './errors';
|
|
20
20
|
|
|
21
21
|
const MAGIC = utf8Encode(SYNC_PACK_MAGIC);
|
|
@@ -106,7 +106,7 @@ export class MessageStreamScanner {
|
|
|
106
106
|
throw new DecodeError('sync.invalid_request', 'bad envelope magic');
|
|
107
107
|
}
|
|
108
108
|
const wireVersion = (b[4] as number) | ((b[5] as number) << 8);
|
|
109
|
-
if (wireVersion
|
|
109
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
110
110
|
throw new DecodeError(
|
|
111
111
|
'sync.invalid_request',
|
|
112
112
|
`unsupported wireVersion ${wireVersion}`,
|