@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.
@@ -1,5 +1,11 @@
1
1
  /** Protocol constants (SPEC.md §1.2, §9 are normative). */
2
- /** SSP2 envelope wire version this codec implements. */
3
- export declare const PROTOCOL_WIRE_VERSION = 1;
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 this codec implements. */
3
- export const PROTOCOL_WIRE_VERSION = 1;
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 version 1 (SPEC.md §1.2). */
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 version 1. */
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 version 1 (SPEC.md §1.2). */
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 version 1. */
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 { PROTOCOL_WIRE_VERSION, SYNC_PACK_MAGIC } from './constants.js';
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
- return { type: 'REQ_HEADER', clientId, schemaVersion };
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 !== PROTOCOL_WIRE_VERSION) {
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 !== PROTOCOL_WIRE_VERSION) {
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 { PROTOCOL_WIRE_VERSION, SYNC_PACK_MAGIC } from './constants.js';
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 !== PROTOCOL_WIRE_VERSION) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/core",
3
- "version": "0.15.46",
3
+ "version": "0.15.48",
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/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 this codec implements. */
4
- export const PROTOCOL_WIRE_VERSION = 1;
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 version 1 (SPEC.md §1.2). */
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 version 1. */
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 { PROTOCOL_WIRE_VERSION, SYNC_PACK_MAGIC } from './constants';
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
- return { type: 'REQ_HEADER', clientId, schemaVersion };
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(frame: RequestFrame | ResponseFrame): {
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 !== PROTOCOL_WIRE_VERSION) {
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 !== PROTOCOL_WIRE_VERSION) {
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, decodeRequestFrame),
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, decodeResponseFrame),
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 { PROTOCOL_WIRE_VERSION, SYNC_PACK_MAGIC } from './constants';
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 !== PROTOCOL_WIRE_VERSION) {
109
+ if (!isSupportedProtocolWireVersion(wireVersion)) {
110
110
  throw new DecodeError(
111
111
  'sync.invalid_request',
112
112
  `unsupported wireVersion ${wireVersion}`,