@syncular/core 0.58.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/blob-ref.d.ts +4 -4
- package/dist/blob-ref.js +21 -38
- package/dist/canonical-json.d.ts +20 -9
- package/dist/canonical-json.js +108 -6
- package/dist/constants.d.ts +5 -5
- package/dist/constants.js +5 -5
- package/dist/crypto.js +2 -1
- package/dist/frames.d.ts +6 -3
- package/dist/frames.js +10 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/json-document.d.ts +11 -0
- package/dist/json-document.js +50 -0
- package/dist/message.d.ts +59 -18
- package/dist/message.js +311 -176
- package/dist/realtime.d.ts +3 -17
- package/dist/realtime.js +2 -37
- package/dist/render.js +34 -11
- package/dist/row-codec.d.ts +4 -1
- package/dist/row-codec.js +29 -7
- package/package.json +1 -1
- package/src/blob-ref.ts +27 -47
- package/src/canonical-json.ts +135 -6
- package/src/constants.ts +5 -5
- package/src/crypto.ts +2 -1
- package/src/frames.ts +10 -4
- package/src/index.ts +1 -0
- package/src/json-document.ts +62 -0
- package/src/message.ts +424 -217
- package/src/realtime.ts +9 -47
- package/src/render.ts +34 -11
- package/src/row-codec.ts +39 -7
package/src/message.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './bytes';
|
|
23
23
|
import { isSupportedProtocolWireVersion, SYNC_PACK_MAGIC } from './constants';
|
|
24
24
|
import { DecodeError } from './errors';
|
|
25
|
+
import { parseJsonDocument } from './json-document';
|
|
25
26
|
import {
|
|
26
27
|
FrameType,
|
|
27
28
|
KNOWN_FRAME_TYPES,
|
|
@@ -42,8 +43,11 @@ export interface ReqHeaderFrame {
|
|
|
42
43
|
type: 'REQ_HEADER';
|
|
43
44
|
clientId: string;
|
|
44
45
|
schemaVersion: number;
|
|
45
|
-
/**
|
|
46
|
+
/** Partition log identity; absent requests epoch acquisition. */
|
|
46
47
|
logEpoch?: string;
|
|
48
|
+
/** Identity of the sender's outbox sequence (§6.3.2, §7.1); absent for a
|
|
49
|
+
* sender without an outbox. Non-empty, at most 64 bytes. */
|
|
50
|
+
outboxId?: string;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
export interface PushOperation {
|
|
@@ -59,6 +63,8 @@ export interface PushOperation {
|
|
|
59
63
|
export interface PushCommitFrame {
|
|
60
64
|
type: 'PUSH_COMMIT';
|
|
61
65
|
clientCommitId: string;
|
|
66
|
+
/** Outbox sequence number (§6.3.2, §7.1); `0` without an outbox. */
|
|
67
|
+
clientSeq: number;
|
|
62
68
|
operations: PushOperation[];
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -69,15 +75,26 @@ export interface PullHeaderFrame {
|
|
|
69
75
|
maxSnapshotPages: number;
|
|
70
76
|
/** Bitmask (§4.2): bit 0 inline rows, 1 external rows, 2 sqlite, 3 signed URLs. */
|
|
71
77
|
accept: number;
|
|
78
|
+
/** The replica cursor (§4.5); `-1` when no unit is live. */
|
|
79
|
+
cursor: number;
|
|
80
|
+
/** §4.1 registration digest: exactly 32 bytes. */
|
|
81
|
+
registrationDigest: Uint8Array;
|
|
82
|
+
/** The last processed `LIVE_END.echoDigest`: empty or 32 bytes (§4.2). */
|
|
83
|
+
echoDigest: Uint8Array;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
/** §4.3: one unit of a traveling registration list. */
|
|
87
|
+
export interface UnitFrame {
|
|
88
|
+
type: 'UNIT';
|
|
89
|
+
scopes: Record<string, string>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** §4.3: the progress of one catching-up unit. */
|
|
93
|
+
export interface ProgressFrame {
|
|
94
|
+
type: 'PROGRESS';
|
|
95
|
+
/** The unit id (§4.1): 32 lowercase hex characters. */
|
|
76
96
|
id: string;
|
|
77
|
-
|
|
78
|
-
scopes: ScopeMap;
|
|
79
|
-
/** Host-opaque JSON document, preserved verbatim. */
|
|
80
|
-
params?: string;
|
|
97
|
+
/** `-1`, the §4.7 pin while a resume token is held, or the unit cursor. */
|
|
81
98
|
cursor: number;
|
|
82
99
|
/** Opaque resume token JSON, round-tripped byte-for-byte (§4.7). */
|
|
83
100
|
bootstrapState?: string;
|
|
@@ -103,17 +120,18 @@ export type RequestFrame =
|
|
|
103
120
|
| RoundFailureFrame
|
|
104
121
|
| PushCommitFrame
|
|
105
122
|
| PullHeaderFrame
|
|
106
|
-
|
|
|
123
|
+
| UnitFrame
|
|
124
|
+
| ProgressFrame
|
|
107
125
|
| UnknownFrame;
|
|
108
126
|
|
|
109
127
|
export interface RespHeaderFrame {
|
|
110
128
|
type: 'RESP_HEADER';
|
|
111
129
|
requiredSchemaVersion?: number;
|
|
112
130
|
latestSchemaVersion?: number;
|
|
113
|
-
/**
|
|
114
|
-
logEpoch
|
|
115
|
-
/**
|
|
116
|
-
resetRequired
|
|
131
|
+
/** Partition log identity, non-empty. */
|
|
132
|
+
logEpoch: string;
|
|
133
|
+
/** True produces a header-only reset response. */
|
|
134
|
+
resetRequired: boolean;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
/** §7.3.2: a server-issued auth lease delivered to the client (opaque). */
|
|
@@ -172,21 +190,47 @@ export interface PushResultDetailsFrame {
|
|
|
172
190
|
entries: Array<{ opIndex: number; details: RejectionDetails }>;
|
|
173
191
|
}
|
|
174
192
|
|
|
193
|
+
/** §4.4: one (unit, table) pair the wildcard fence touched. */
|
|
194
|
+
export interface FencedRecord {
|
|
195
|
+
table: string;
|
|
196
|
+
status: 'active' | 'revoked';
|
|
197
|
+
/** Effective lists of the variables the fence added; empty when revoked. */
|
|
198
|
+
scopes: ScopeMap;
|
|
199
|
+
}
|
|
200
|
+
|
|
175
201
|
export interface SubStartFrame {
|
|
176
202
|
type: 'SUB_START';
|
|
203
|
+
/** The unit id (§4.1): 32 lowercase hex characters. */
|
|
177
204
|
id: string;
|
|
178
205
|
status: 'active' | 'revoked' | 'reset';
|
|
179
206
|
reasonCode: string;
|
|
180
|
-
effectiveScopes: ScopeMap;
|
|
181
207
|
bootstrap: boolean;
|
|
208
|
+
/** The unit's echo, in ascending table order (§4.4). */
|
|
209
|
+
fenced: FencedRecord[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface LiveStartFrame {
|
|
213
|
+
type: 'LIVE_START';
|
|
214
|
+
status: 'active' | 'reset' | 'unregistered';
|
|
215
|
+
reasonCode: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface LiveEndFrame {
|
|
219
|
+
type: 'LIVE_END';
|
|
220
|
+
/** Highest commitSeq the live section is complete through; `-1` for
|
|
221
|
+
* `reset` and `unregistered` (§4.4). */
|
|
222
|
+
throughSeq: number;
|
|
223
|
+
/** Echo digest of the request: empty or 32 bytes (§4.4). */
|
|
224
|
+
echoDigest: Uint8Array;
|
|
182
225
|
}
|
|
183
226
|
|
|
184
227
|
export interface CommitChange {
|
|
185
228
|
tableIndex: number;
|
|
186
229
|
rowId: string;
|
|
187
230
|
op: 'upsert' | 'delete';
|
|
188
|
-
/**
|
|
189
|
-
|
|
231
|
+
/** The row's `server_version` after the write, ≥ 1; a delete's is the
|
|
232
|
+
* version its tombstone records (§2.2, §4.5). */
|
|
233
|
+
rowVersion: number;
|
|
190
234
|
scopes: Record<string, string>;
|
|
191
235
|
/** Generated-row-codec bytes; present iff `op` is `upsert` (§4.5). */
|
|
192
236
|
row?: Uint8Array;
|
|
@@ -226,7 +270,7 @@ export interface SegmentInlineFrame {
|
|
|
226
270
|
export interface SubEndFrame {
|
|
227
271
|
type: 'SUB_END';
|
|
228
272
|
nextCursor: number;
|
|
229
|
-
/** Present iff the bootstrap is incomplete (§4.4). */
|
|
273
|
+
/** Present iff the unit's bootstrap is incomplete (§4.4). */
|
|
230
274
|
bootstrapState?: string;
|
|
231
275
|
}
|
|
232
276
|
|
|
@@ -250,6 +294,8 @@ export type ResponseFrame =
|
|
|
250
294
|
| SegmentRefFrame
|
|
251
295
|
| SegmentInlineFrame
|
|
252
296
|
| SubEndFrame
|
|
297
|
+
| LiveStartFrame
|
|
298
|
+
| LiveEndFrame
|
|
253
299
|
| ErrorFrame
|
|
254
300
|
| UnknownFrame;
|
|
255
301
|
|
|
@@ -286,12 +332,10 @@ function invalid(message: string): never {
|
|
|
286
332
|
throw new DecodeError('sync.invalid_request', message);
|
|
287
333
|
}
|
|
288
334
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
invalid(`${what} is not a valid JSON document`);
|
|
294
|
-
}
|
|
335
|
+
/** A `json` field: a valid JSON document (Conventions), kept as raw text. */
|
|
336
|
+
function readJson(r: ByteReader, what: string): string {
|
|
337
|
+
const raw = r.str();
|
|
338
|
+
parseJsonDocument(raw, what);
|
|
295
339
|
return raw;
|
|
296
340
|
}
|
|
297
341
|
|
|
@@ -316,29 +360,84 @@ function validateRoundFailureCode(
|
|
|
316
360
|
}
|
|
317
361
|
}
|
|
318
362
|
|
|
363
|
+
/** §1.5, §1.7: a present `outboxId` is non-empty and at most 64 bytes. */
|
|
364
|
+
function checkOutboxId(outboxId: string, fail: (message: string) => never) {
|
|
365
|
+
if (outboxId.length === 0 || utf8Encode(outboxId).length > 64) {
|
|
366
|
+
fail('REQ_HEADER.outboxId must be non-empty and at most 64 bytes');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** §4.3, §4.4: a unit id is 32 lowercase hex characters. */
|
|
371
|
+
function checkUnitId(
|
|
372
|
+
id: string,
|
|
373
|
+
frame: 'PROGRESS' | 'SUB_START',
|
|
374
|
+
fail: (message: string) => never,
|
|
375
|
+
) {
|
|
376
|
+
if (!/^[0-9a-f]{32}$/.test(id)) {
|
|
377
|
+
fail(`${frame}.id must be 32 lowercase hex characters`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** §4.2, §4.4: a digest is 32 bytes; an echo digest may also be empty. */
|
|
382
|
+
function checkDigest(
|
|
383
|
+
digest: Uint8Array,
|
|
384
|
+
field:
|
|
385
|
+
| 'PULL_HEADER.registrationDigest'
|
|
386
|
+
| 'PULL_HEADER.echoDigest'
|
|
387
|
+
| 'LIVE_END.echoDigest',
|
|
388
|
+
fail: (message: string) => never,
|
|
389
|
+
) {
|
|
390
|
+
const emptyAllowed = field !== 'PULL_HEADER.registrationDigest';
|
|
391
|
+
if (digest.length !== 32 && !(emptyAllowed && digest.length === 0)) {
|
|
392
|
+
fail(`${field} must be ${emptyAllowed ? '0 or ' : ''}32 bytes`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** §4.2, §4.3: a cursor is `-1` or a commitSeq. */
|
|
397
|
+
function checkCursor(
|
|
398
|
+
cursor: number,
|
|
399
|
+
field: 'PULL_HEADER.cursor' | 'PROGRESS.cursor',
|
|
400
|
+
fail: (message: string) => never,
|
|
401
|
+
) {
|
|
402
|
+
if (cursor < -1) fail(`${field} must be >= -1`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** §4.4: fenced records in strictly ascending code-unit order of `table`. */
|
|
406
|
+
function checkFencedOrder(
|
|
407
|
+
fenced: readonly FencedRecord[],
|
|
408
|
+
fail: (message: string) => never,
|
|
409
|
+
) {
|
|
410
|
+
for (let i = 1; i < fenced.length; i++) {
|
|
411
|
+
if (
|
|
412
|
+
(fenced[i - 1] as FencedRecord).table >= (fenced[i] as FencedRecord).table
|
|
413
|
+
) {
|
|
414
|
+
fail('SUB_START.fenced tables must be unique and in ascending order');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
319
419
|
// ---------------------------------------------------------------------------
|
|
320
420
|
// Frame payload decoders (each consumes exactly the frame payload)
|
|
321
421
|
// ---------------------------------------------------------------------------
|
|
322
422
|
|
|
323
|
-
function decodeReqHeader(r: ByteReader
|
|
423
|
+
function decodeReqHeader(r: ByteReader): ReqHeaderFrame {
|
|
324
424
|
const clientId = r.str();
|
|
325
425
|
if (clientId.length === 0) invalid('REQ_HEADER.clientId must be non-empty');
|
|
326
426
|
const schemaVersion = r.i32();
|
|
327
427
|
if (schemaVersion < 1) invalid('REQ_HEADER.schemaVersion must be >= 1');
|
|
328
|
-
const logEpoch =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
})
|
|
336
|
-
: undefined;
|
|
428
|
+
const logEpoch = r.opt(() => {
|
|
429
|
+
const value = r.str();
|
|
430
|
+
if (value.length === 0) invalid('REQ_HEADER.logEpoch must be non-empty');
|
|
431
|
+
return value;
|
|
432
|
+
});
|
|
433
|
+
const outboxId = r.opt(() => r.str());
|
|
434
|
+
if (outboxId !== undefined) checkOutboxId(outboxId, invalid);
|
|
337
435
|
return definedObject({
|
|
338
436
|
type: 'REQ_HEADER',
|
|
339
437
|
clientId,
|
|
340
438
|
schemaVersion,
|
|
341
439
|
logEpoch: logEpoch,
|
|
440
|
+
outboxId: outboxId,
|
|
342
441
|
});
|
|
343
442
|
}
|
|
344
443
|
|
|
@@ -353,6 +452,8 @@ function decodePushCommit(r: ByteReader): PushCommitFrame {
|
|
|
353
452
|
if (clientCommitId.length === 0) {
|
|
354
453
|
invalid('PUSH_COMMIT.clientCommitId must be non-empty');
|
|
355
454
|
}
|
|
455
|
+
const clientSeq = r.i64();
|
|
456
|
+
if (clientSeq < 0) invalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
356
457
|
const operationCount = r.u32();
|
|
357
458
|
if (operationCount === 0) {
|
|
358
459
|
throw new DecodeError(
|
|
@@ -387,7 +488,7 @@ function decodePushCommit(r: ByteReader): PushCommitFrame {
|
|
|
387
488
|
}),
|
|
388
489
|
);
|
|
389
490
|
}
|
|
390
|
-
return { type: 'PUSH_COMMIT', clientCommitId, operations };
|
|
491
|
+
return { type: 'PUSH_COMMIT', clientCommitId, clientSeq, operations };
|
|
391
492
|
}
|
|
392
493
|
|
|
393
494
|
function decodePullHeader(r: ByteReader): PullHeaderFrame {
|
|
@@ -398,51 +499,52 @@ function decodePullHeader(r: ByteReader): PullHeaderFrame {
|
|
|
398
499
|
if ((accept & 0xf0) !== 0) {
|
|
399
500
|
invalid('PULL_HEADER.accept bits 4-7 must be zero');
|
|
400
501
|
}
|
|
502
|
+
const cursor = r.i64();
|
|
503
|
+
checkCursor(cursor, 'PULL_HEADER.cursor', invalid);
|
|
504
|
+
const registrationDigest = r.bytes();
|
|
505
|
+
checkDigest(registrationDigest, 'PULL_HEADER.registrationDigest', invalid);
|
|
506
|
+
const echoDigest = r.bytes();
|
|
507
|
+
checkDigest(echoDigest, 'PULL_HEADER.echoDigest', invalid);
|
|
401
508
|
return {
|
|
402
509
|
type: 'PULL_HEADER',
|
|
403
510
|
limitCommits,
|
|
404
511
|
limitSnapshotRows,
|
|
405
512
|
maxSnapshotPages,
|
|
406
513
|
accept,
|
|
514
|
+
cursor,
|
|
515
|
+
registrationDigest,
|
|
516
|
+
echoDigest,
|
|
407
517
|
};
|
|
408
518
|
}
|
|
409
519
|
|
|
410
|
-
function
|
|
520
|
+
function decodeProgress(r: ByteReader): ProgressFrame {
|
|
411
521
|
const id = r.str();
|
|
412
|
-
|
|
413
|
-
const scopes = readStringListMap(r);
|
|
414
|
-
const params = r.opt(() =>
|
|
415
|
-
requireJsonDocument(r.str(), 'SUBSCRIPTION.params'),
|
|
416
|
-
);
|
|
522
|
+
checkUnitId(id, 'PROGRESS', invalid);
|
|
417
523
|
const cursor = r.i64();
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
524
|
+
checkCursor(cursor, 'PROGRESS.cursor', invalid);
|
|
525
|
+
const bootstrapState = r.opt(() => readJson(r, 'PROGRESS.bootstrapState'));
|
|
421
526
|
return definedObject({
|
|
422
|
-
type: '
|
|
527
|
+
type: 'PROGRESS',
|
|
423
528
|
id,
|
|
424
|
-
table,
|
|
425
|
-
scopes,
|
|
426
|
-
params: params,
|
|
427
529
|
cursor,
|
|
428
530
|
bootstrapState: bootstrapState,
|
|
429
531
|
});
|
|
430
532
|
}
|
|
431
533
|
|
|
432
|
-
function decodeRespHeader(r: ByteReader
|
|
534
|
+
function decodeRespHeader(r: ByteReader): RespHeaderFrame {
|
|
433
535
|
const requiredSchemaVersion = r.opt(() => r.i32());
|
|
434
536
|
const latestSchemaVersion = r.opt(() => r.i32());
|
|
435
|
-
const logEpoch =
|
|
436
|
-
if (logEpoch
|
|
537
|
+
const logEpoch = r.str();
|
|
538
|
+
if (logEpoch.length === 0) {
|
|
437
539
|
invalid('RESP_HEADER.logEpoch must be non-empty');
|
|
438
540
|
}
|
|
439
|
-
const resetRequired =
|
|
541
|
+
const resetRequired = r.bool();
|
|
440
542
|
return definedObject({
|
|
441
543
|
type: 'RESP_HEADER',
|
|
442
544
|
requiredSchemaVersion: requiredSchemaVersion,
|
|
443
545
|
latestSchemaVersion: latestSchemaVersion,
|
|
444
|
-
logEpoch
|
|
445
|
-
resetRequired
|
|
546
|
+
logEpoch,
|
|
547
|
+
resetRequired,
|
|
446
548
|
});
|
|
447
549
|
}
|
|
448
550
|
|
|
@@ -523,9 +625,10 @@ function decodePushResultDetails(r: ByteReader): PushResultDetailsFrame {
|
|
|
523
625
|
);
|
|
524
626
|
}
|
|
525
627
|
seen.add(opIndex);
|
|
628
|
+
const parsed = parseJsonDocument(r.str(), 'PUSH_RESULT_DETAILS.details');
|
|
526
629
|
let details: RejectionDetails;
|
|
527
630
|
try {
|
|
528
|
-
details = normalizeRejectionDetails(
|
|
631
|
+
details = normalizeRejectionDetails(parsed);
|
|
529
632
|
} catch (error) {
|
|
530
633
|
invalid(
|
|
531
634
|
`invalid PUSH_RESULT_DETAILS.details: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -538,6 +641,7 @@ function decodePushResultDetails(r: ByteReader): PushResultDetailsFrame {
|
|
|
538
641
|
|
|
539
642
|
function decodeSubStart(r: ByteReader): SubStartFrame {
|
|
540
643
|
const id = r.str();
|
|
644
|
+
checkUnitId(id, 'SUB_START', invalid);
|
|
541
645
|
const statusByte = r.u8();
|
|
542
646
|
let status: 'active' | 'revoked' | 'reset';
|
|
543
647
|
if (statusByte === 1) status = 'active';
|
|
@@ -545,16 +649,40 @@ function decodeSubStart(r: ByteReader): SubStartFrame {
|
|
|
545
649
|
else if (statusByte === 3) status = 'reset';
|
|
546
650
|
else invalid(`invalid SUB_START status byte ${statusByte}`);
|
|
547
651
|
const reasonCode = r.str();
|
|
548
|
-
const effectiveScopes = readStringListMap(r);
|
|
549
652
|
const bootstrap = r.bool();
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
653
|
+
const fencedCount = r.u32();
|
|
654
|
+
const fenced: FencedRecord[] = [];
|
|
655
|
+
for (let i = 0; i < fencedCount; i++) {
|
|
656
|
+
const table = r.str();
|
|
657
|
+
const fencedStatus = r.u8();
|
|
658
|
+
if (fencedStatus !== 1 && fencedStatus !== 2) {
|
|
659
|
+
invalid('invalid fenced record status byte');
|
|
660
|
+
}
|
|
661
|
+
fenced.push({
|
|
662
|
+
table,
|
|
663
|
+
status: fencedStatus === 1 ? 'active' : 'revoked',
|
|
664
|
+
scopes: readStringListMap(r),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
checkFencedOrder(fenced, invalid);
|
|
668
|
+
return { type: 'SUB_START', id, status, reasonCode, bootstrap, fenced };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function decodeLiveStart(r: ByteReader): LiveStartFrame {
|
|
672
|
+
const statusByte = r.u8();
|
|
673
|
+
let status: LiveStartFrame['status'];
|
|
674
|
+
if (statusByte === 1) status = 'active';
|
|
675
|
+
else if (statusByte === 3) status = 'reset';
|
|
676
|
+
else if (statusByte === 4) status = 'unregistered';
|
|
677
|
+
else invalid('invalid LIVE_START status byte');
|
|
678
|
+
return { type: 'LIVE_START', status, reasonCode: r.str() };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function decodeLiveEnd(r: ByteReader): LiveEndFrame {
|
|
682
|
+
const throughSeq = r.i64();
|
|
683
|
+
const echoDigest = r.bytes();
|
|
684
|
+
checkDigest(echoDigest, 'LIVE_END.echoDigest', invalid);
|
|
685
|
+
return { type: 'LIVE_END', throughSeq, echoDigest };
|
|
558
686
|
}
|
|
559
687
|
|
|
560
688
|
function decodeCommit(r: ByteReader): CommitFrame {
|
|
@@ -579,24 +707,15 @@ function decodeCommit(r: ByteReader): CommitFrame {
|
|
|
579
707
|
if (opByte === 1) op = 'upsert';
|
|
580
708
|
else if (opByte === 2) op = 'delete';
|
|
581
709
|
else invalid(`invalid change op byte ${opByte}`);
|
|
582
|
-
const rowVersion = r.
|
|
710
|
+
const rowVersion = r.i64();
|
|
711
|
+
if (rowVersion < 1) invalid('change rowVersion must be at least 1');
|
|
583
712
|
const scopes = readStringMap(r);
|
|
584
713
|
const row = r.opt(() => r.bytes());
|
|
585
|
-
if (op === 'upsert'
|
|
586
|
-
invalid('
|
|
587
|
-
}
|
|
588
|
-
if (op === 'delete' && (rowVersion !== undefined || row !== undefined)) {
|
|
589
|
-
invalid('delete change must not carry rowVersion or row');
|
|
714
|
+
if ((op === 'upsert') !== (row !== undefined)) {
|
|
715
|
+
invalid('change row must be present iff op is upsert');
|
|
590
716
|
}
|
|
591
717
|
changes.push(
|
|
592
|
-
definedObject({
|
|
593
|
-
tableIndex,
|
|
594
|
-
rowId,
|
|
595
|
-
op,
|
|
596
|
-
rowVersion: rowVersion,
|
|
597
|
-
scopes,
|
|
598
|
-
row: row,
|
|
599
|
-
}),
|
|
718
|
+
definedObject({ tableIndex, rowId, op, rowVersion, scopes, row }),
|
|
600
719
|
);
|
|
601
720
|
}
|
|
602
721
|
return { type: 'COMMIT', commitSeq, createdAtMs, actorId, tables, changes };
|
|
@@ -647,9 +766,7 @@ function decodeSegmentInline(r: ByteReader): SegmentInlineFrame {
|
|
|
647
766
|
|
|
648
767
|
function decodeSubEnd(r: ByteReader): SubEndFrame {
|
|
649
768
|
const nextCursor = r.i64();
|
|
650
|
-
const bootstrapState = r.opt(() =>
|
|
651
|
-
requireJsonDocument(r.str(), 'SUB_END.bootstrapState'),
|
|
652
|
-
);
|
|
769
|
+
const bootstrapState = r.opt(() => readJson(r, 'SUB_END.bootstrapState'));
|
|
653
770
|
return definedObject({
|
|
654
771
|
type: 'SUB_END',
|
|
655
772
|
nextCursor,
|
|
@@ -663,7 +780,7 @@ function decodeErrorFrame(r: ByteReader): ErrorFrame {
|
|
|
663
780
|
const category = r.str();
|
|
664
781
|
const retryable = r.bool();
|
|
665
782
|
const recommendedAction = r.str();
|
|
666
|
-
const details = r.opt(() =>
|
|
783
|
+
const details = r.opt(() => readJson(r, 'ERROR.details'));
|
|
667
784
|
return definedObject({
|
|
668
785
|
type: 'ERROR',
|
|
669
786
|
code,
|
|
@@ -678,13 +795,12 @@ function decodeErrorFrame(r: ByteReader): ErrorFrame {
|
|
|
678
795
|
function decodeRequestFrame(
|
|
679
796
|
frameType: number,
|
|
680
797
|
payload: Uint8Array,
|
|
681
|
-
wireVersion: number,
|
|
682
798
|
): RequestFrame {
|
|
683
799
|
const r = new ByteReader(payload);
|
|
684
800
|
let frame: RequestFrame;
|
|
685
801
|
switch (frameType) {
|
|
686
802
|
case FrameType.REQ_HEADER:
|
|
687
|
-
frame = decodeReqHeader(r
|
|
803
|
+
frame = decodeReqHeader(r);
|
|
688
804
|
break;
|
|
689
805
|
case FrameType.ROUND_FAILURE:
|
|
690
806
|
frame = decodeRoundFailure(r);
|
|
@@ -695,8 +811,11 @@ function decodeRequestFrame(
|
|
|
695
811
|
case FrameType.PULL_HEADER:
|
|
696
812
|
frame = decodePullHeader(r);
|
|
697
813
|
break;
|
|
698
|
-
case FrameType.
|
|
699
|
-
frame =
|
|
814
|
+
case FrameType.UNIT:
|
|
815
|
+
frame = { type: 'UNIT', scopes: readStringMap(r) };
|
|
816
|
+
break;
|
|
817
|
+
case FrameType.PROGRESS:
|
|
818
|
+
frame = decodeProgress(r);
|
|
700
819
|
break;
|
|
701
820
|
default:
|
|
702
821
|
if (RESPONSE_FRAME_TYPES.has(frameType)) {
|
|
@@ -713,13 +832,12 @@ function decodeRequestFrame(
|
|
|
713
832
|
function decodeResponseFrame(
|
|
714
833
|
frameType: number,
|
|
715
834
|
payload: Uint8Array,
|
|
716
|
-
wireVersion: number,
|
|
717
835
|
): ResponseFrame {
|
|
718
836
|
const r = new ByteReader(payload);
|
|
719
837
|
let frame: ResponseFrame;
|
|
720
838
|
switch (frameType) {
|
|
721
839
|
case FrameType.RESP_HEADER:
|
|
722
|
-
frame = decodeRespHeader(r
|
|
840
|
+
frame = decodeRespHeader(r);
|
|
723
841
|
break;
|
|
724
842
|
case FrameType.LEASE:
|
|
725
843
|
frame = decodeLease(r);
|
|
@@ -745,6 +863,12 @@ function decodeResponseFrame(
|
|
|
745
863
|
case FrameType.SUB_END:
|
|
746
864
|
frame = decodeSubEnd(r);
|
|
747
865
|
break;
|
|
866
|
+
case FrameType.LIVE_START:
|
|
867
|
+
frame = decodeLiveStart(r);
|
|
868
|
+
break;
|
|
869
|
+
case FrameType.LIVE_END:
|
|
870
|
+
frame = decodeLiveEnd(r);
|
|
871
|
+
break;
|
|
748
872
|
case FrameType.ERROR:
|
|
749
873
|
frame = decodeErrorFrame(r);
|
|
750
874
|
break;
|
|
@@ -771,6 +895,7 @@ function validateRequestSequence(frames: readonly RequestFrame[]): void {
|
|
|
771
895
|
}
|
|
772
896
|
let pushCount = 0;
|
|
773
897
|
let hasPull = false;
|
|
898
|
+
let hasProgress = false;
|
|
774
899
|
let index = 1;
|
|
775
900
|
let sawRoundFailure = false;
|
|
776
901
|
for (const frame of frames.slice(1)) {
|
|
@@ -794,8 +919,13 @@ function validateRequestSequence(frames: readonly RequestFrame[]): void {
|
|
|
794
919
|
if (hasPull) invalid('duplicate PULL_HEADER frame');
|
|
795
920
|
hasPull = true;
|
|
796
921
|
break;
|
|
797
|
-
case '
|
|
798
|
-
if (!hasPull) invalid('
|
|
922
|
+
case 'UNIT':
|
|
923
|
+
if (!hasPull) invalid('UNIT frame without a PULL_HEADER');
|
|
924
|
+
if (hasProgress) invalid('UNIT frame after a PROGRESS frame');
|
|
925
|
+
break;
|
|
926
|
+
case 'PROGRESS':
|
|
927
|
+
if (!hasPull) invalid('PROGRESS frame without a PULL_HEADER');
|
|
928
|
+
hasProgress = true;
|
|
799
929
|
break;
|
|
800
930
|
case 'UNKNOWN':
|
|
801
931
|
break;
|
|
@@ -812,12 +942,12 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
812
942
|
if (first === undefined || first.type !== 'RESP_HEADER') {
|
|
813
943
|
invalid('response must start with a RESP_HEADER frame');
|
|
814
944
|
}
|
|
815
|
-
|
|
816
|
-
let
|
|
945
|
+
// §1.6 sections: unit sections, then at most one live section.
|
|
946
|
+
let section: 'none' | 'unit' | 'live' = 'none';
|
|
947
|
+
let sawUnitSection = false;
|
|
948
|
+
let sawLive = false;
|
|
817
949
|
let sawLease = false;
|
|
818
950
|
let sawBody = false;
|
|
819
|
-
let subscriptionHasCommits = false;
|
|
820
|
-
let subscriptionHasSegments = false;
|
|
821
951
|
let errorSeen = false;
|
|
822
952
|
let lastPushResult: PushResultFrame | undefined;
|
|
823
953
|
let lastPushResultHasDetails = false;
|
|
@@ -828,21 +958,22 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
828
958
|
invalid('duplicate RESP_HEADER frame');
|
|
829
959
|
break;
|
|
830
960
|
case 'LEASE':
|
|
831
|
-
// §7.3.2: at most one LEASE, immediately after RESP_HEADER
|
|
832
|
-
// any PUSH_RESULT / subscription section / ERROR).
|
|
961
|
+
// §7.3.2: at most one LEASE, immediately after RESP_HEADER.
|
|
833
962
|
if (sawLease) invalid('duplicate LEASE frame');
|
|
834
963
|
if (sawBody) invalid('LEASE frame must immediately follow RESP_HEADER');
|
|
835
964
|
sawLease = true;
|
|
836
965
|
break;
|
|
837
966
|
case 'PUSH_RESULT':
|
|
838
|
-
if (
|
|
967
|
+
if (sawUnitSection || sawLive) {
|
|
968
|
+
invalid('PUSH_RESULT frame after a unit or live section');
|
|
969
|
+
}
|
|
839
970
|
lastPushResult = frame;
|
|
840
971
|
lastPushResultHasDetails = false;
|
|
841
972
|
sawBody = true;
|
|
842
973
|
break;
|
|
843
974
|
case 'PUSH_RESULT_DETAILS': {
|
|
844
|
-
if (
|
|
845
|
-
invalid('PUSH_RESULT_DETAILS frame after
|
|
975
|
+
if (sawUnitSection || sawLive) {
|
|
976
|
+
invalid('PUSH_RESULT_DETAILS frame after a unit or live section');
|
|
846
977
|
}
|
|
847
978
|
if (lastPushResult === undefined) {
|
|
848
979
|
invalid('PUSH_RESULT_DETAILS without a preceding PUSH_RESULT');
|
|
@@ -871,36 +1002,40 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
871
1002
|
break;
|
|
872
1003
|
}
|
|
873
1004
|
case 'SUB_START':
|
|
874
|
-
if (
|
|
875
|
-
|
|
876
|
-
|
|
1005
|
+
if (section === 'unit') invalid('nested SUB_START frame');
|
|
1006
|
+
if (sawLive)
|
|
1007
|
+
invalid('SUB_START frame inside or after the live section');
|
|
1008
|
+
section = 'unit';
|
|
1009
|
+
sawUnitSection = true;
|
|
877
1010
|
sawBody = true;
|
|
878
1011
|
lastPushResult = undefined;
|
|
879
|
-
subscriptionHasCommits = false;
|
|
880
|
-
subscriptionHasSegments = false;
|
|
881
|
-
break;
|
|
882
|
-
case 'COMMIT':
|
|
883
|
-
if (!inSubscription) invalid('COMMIT frame outside a subscription');
|
|
884
|
-
if (subscriptionHasSegments) {
|
|
885
|
-
invalid(
|
|
886
|
-
'COMMIT and segment frames must not both appear for one subscription',
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
subscriptionHasCommits = true;
|
|
890
1012
|
break;
|
|
891
1013
|
case 'SEGMENT_REF':
|
|
892
1014
|
case 'SEGMENT_INLINE':
|
|
893
|
-
if (
|
|
894
|
-
if (subscriptionHasCommits) {
|
|
895
|
-
invalid(
|
|
896
|
-
'COMMIT and segment frames must not both appear for one subscription',
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
subscriptionHasSegments = true;
|
|
1015
|
+
if (section !== 'unit') invalid('segment frame outside a unit section');
|
|
900
1016
|
break;
|
|
901
1017
|
case 'SUB_END':
|
|
902
|
-
if (
|
|
903
|
-
|
|
1018
|
+
if (section !== 'unit')
|
|
1019
|
+
invalid('SUB_END frame without an open SUB_START');
|
|
1020
|
+
section = 'none';
|
|
1021
|
+
break;
|
|
1022
|
+
case 'LIVE_START':
|
|
1023
|
+
if (section === 'unit')
|
|
1024
|
+
invalid('LIVE_START frame inside a unit section');
|
|
1025
|
+
if (sawLive) invalid('second LIVE_START frame');
|
|
1026
|
+
section = 'live';
|
|
1027
|
+
sawLive = true;
|
|
1028
|
+
sawBody = true;
|
|
1029
|
+
lastPushResult = undefined;
|
|
1030
|
+
break;
|
|
1031
|
+
case 'COMMIT':
|
|
1032
|
+
if (section !== 'live')
|
|
1033
|
+
invalid('COMMIT frame outside the live section');
|
|
1034
|
+
break;
|
|
1035
|
+
case 'LIVE_END':
|
|
1036
|
+
if (section !== 'live')
|
|
1037
|
+
invalid('LIVE_END frame without an open LIVE_START');
|
|
1038
|
+
section = 'none';
|
|
904
1039
|
break;
|
|
905
1040
|
case 'ERROR':
|
|
906
1041
|
errorSeen = true;
|
|
@@ -913,16 +1048,11 @@ function validateResponseSequence(frames: readonly ResponseFrame[]): void {
|
|
|
913
1048
|
break;
|
|
914
1049
|
}
|
|
915
1050
|
}
|
|
916
|
-
if (
|
|
917
|
-
invalid('
|
|
1051
|
+
if (section !== 'none' && !errorSeen) {
|
|
1052
|
+
invalid('unit or live section not terminated');
|
|
918
1053
|
}
|
|
919
1054
|
}
|
|
920
1055
|
|
|
921
|
-
export function validateFrameSequence(message: SyncMessage): void {
|
|
922
|
-
if (message.msgKind === 'request') validateRequestSequence(message.frames);
|
|
923
|
-
else validateResponseSequence(message.frames);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
1056
|
// ---------------------------------------------------------------------------
|
|
927
1057
|
// Frame payload encoders
|
|
928
1058
|
// ---------------------------------------------------------------------------
|
|
@@ -930,20 +1060,30 @@ export function validateFrameSequence(message: SyncMessage): void {
|
|
|
930
1060
|
const OP_BYTES = { upsert: 1, delete: 2 } as const;
|
|
931
1061
|
const PUSH_STATUS_BYTES = { applied: 1, cached: 2, rejected: 3 } as const;
|
|
932
1062
|
const SUB_STATUS_BYTES = { active: 1, revoked: 2, reset: 3 } as const;
|
|
1063
|
+
const LIVE_STATUS_BYTES = { active: 1, reset: 3, unregistered: 4 } as const;
|
|
933
1064
|
const MEDIA_TYPE_BYTES = { rows: 1, sqlite: 2 } as const;
|
|
934
1065
|
|
|
935
1066
|
function encodeInvalid(message: string): never {
|
|
936
1067
|
throw new Error(message);
|
|
937
1068
|
}
|
|
938
1069
|
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1070
|
+
function encodeUnknown(frame: UnknownFrame, w: ByteWriter): number {
|
|
1071
|
+
if (
|
|
1072
|
+
!Number.isInteger(frame.frameType) ||
|
|
1073
|
+
frame.frameType < 0 ||
|
|
1074
|
+
frame.frameType > 0xff ||
|
|
1075
|
+
KNOWN_FRAME_TYPES.has(frame.frameType)
|
|
1076
|
+
) {
|
|
1077
|
+
encodeInvalid(
|
|
1078
|
+
`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
w.raw(frame.payload);
|
|
1082
|
+
return frame.frameType;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Writes the payload; returns the frame type byte. */
|
|
1086
|
+
function encodeRequestFrame(frame: RequestFrame, w: ByteWriter): number {
|
|
947
1087
|
switch (frame.type) {
|
|
948
1088
|
case 'REQ_HEADER': {
|
|
949
1089
|
if (frame.clientId.length === 0) {
|
|
@@ -954,14 +1094,14 @@ function encodeFrame(
|
|
|
954
1094
|
}
|
|
955
1095
|
w.str(frame.clientId);
|
|
956
1096
|
w.i32(frame.schemaVersion);
|
|
957
|
-
if (
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
encodeInvalid('REQ_HEADER.logEpoch requires wireVersion 2');
|
|
1097
|
+
if (frame.logEpoch === '') {
|
|
1098
|
+
encodeInvalid('REQ_HEADER.logEpoch must be non-empty');
|
|
1099
|
+
}
|
|
1100
|
+
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
1101
|
+
if (frame.outboxId !== undefined) {
|
|
1102
|
+
checkOutboxId(frame.outboxId, encodeInvalid);
|
|
964
1103
|
}
|
|
1104
|
+
w.opt(frame.outboxId, (v) => w.str(v));
|
|
965
1105
|
break;
|
|
966
1106
|
}
|
|
967
1107
|
case 'ROUND_FAILURE': {
|
|
@@ -976,7 +1116,11 @@ function encodeFrame(
|
|
|
976
1116
|
if (frame.operations.length === 0) {
|
|
977
1117
|
encodeInvalid('PUSH_COMMIT must carry at least one operation');
|
|
978
1118
|
}
|
|
1119
|
+
if (frame.clientSeq < 0) {
|
|
1120
|
+
encodeInvalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
1121
|
+
}
|
|
979
1122
|
w.str(frame.clientCommitId);
|
|
1123
|
+
w.i64(frame.clientSeq);
|
|
980
1124
|
w.u32(frame.operations.length);
|
|
981
1125
|
for (const operation of frame.operations) {
|
|
982
1126
|
if (operation.op === 'upsert' && operation.payload === undefined) {
|
|
@@ -1000,36 +1144,48 @@ function encodeFrame(
|
|
|
1000
1144
|
w.i32(frame.limitCommits);
|
|
1001
1145
|
w.i32(frame.limitSnapshotRows);
|
|
1002
1146
|
w.i32(frame.maxSnapshotPages);
|
|
1147
|
+
checkCursor(frame.cursor, 'PULL_HEADER.cursor', encodeInvalid);
|
|
1148
|
+
checkDigest(
|
|
1149
|
+
frame.registrationDigest,
|
|
1150
|
+
'PULL_HEADER.registrationDigest',
|
|
1151
|
+
encodeInvalid,
|
|
1152
|
+
);
|
|
1153
|
+
checkDigest(frame.echoDigest, 'PULL_HEADER.echoDigest', encodeInvalid);
|
|
1003
1154
|
w.u8(frame.accept);
|
|
1155
|
+
w.i64(frame.cursor);
|
|
1156
|
+
w.bytes(frame.registrationDigest);
|
|
1157
|
+
w.bytes(frame.echoDigest);
|
|
1004
1158
|
break;
|
|
1005
1159
|
}
|
|
1006
|
-
case '
|
|
1160
|
+
case 'UNIT': {
|
|
1161
|
+
writeStringMap(w, frame.scopes);
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
case 'PROGRESS': {
|
|
1165
|
+
checkUnitId(frame.id, 'PROGRESS', encodeInvalid);
|
|
1166
|
+
checkCursor(frame.cursor, 'PROGRESS.cursor', encodeInvalid);
|
|
1007
1167
|
w.str(frame.id);
|
|
1008
|
-
w.str(frame.table);
|
|
1009
|
-
writeStringListMap(w, frame.scopes);
|
|
1010
|
-
w.opt(frame.params, (v) => w.str(v));
|
|
1011
1168
|
w.i64(frame.cursor);
|
|
1012
1169
|
w.opt(frame.bootstrapState, (v) => w.str(v));
|
|
1013
1170
|
break;
|
|
1014
1171
|
}
|
|
1172
|
+
case 'UNKNOWN':
|
|
1173
|
+
return encodeUnknown(frame, w);
|
|
1174
|
+
}
|
|
1175
|
+
return FrameType[frame.type];
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/** Writes the payload; returns the frame type byte. */
|
|
1179
|
+
function encodeResponseFrame(frame: ResponseFrame, w: ByteWriter): number {
|
|
1180
|
+
switch (frame.type) {
|
|
1015
1181
|
case 'RESP_HEADER': {
|
|
1016
1182
|
w.opt(frame.requiredSchemaVersion, (v) => w.i32(v));
|
|
1017
1183
|
w.opt(frame.latestSchemaVersion, (v) => w.i32(v));
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1020
|
-
encodeInvalid('RESP_HEADER.logEpoch is required and non-empty');
|
|
1021
|
-
}
|
|
1022
|
-
if (frame.resetRequired === undefined) {
|
|
1023
|
-
encodeInvalid('RESP_HEADER.resetRequired is required');
|
|
1024
|
-
}
|
|
1025
|
-
w.str(frame.logEpoch);
|
|
1026
|
-
w.bool(frame.resetRequired);
|
|
1027
|
-
} else if (
|
|
1028
|
-
frame.logEpoch !== undefined ||
|
|
1029
|
-
frame.resetRequired !== undefined
|
|
1030
|
-
) {
|
|
1031
|
-
encodeInvalid('RESP_HEADER epoch fields require wireVersion 2');
|
|
1184
|
+
if (frame.logEpoch.length === 0) {
|
|
1185
|
+
encodeInvalid('RESP_HEADER.logEpoch must be non-empty');
|
|
1032
1186
|
}
|
|
1187
|
+
w.str(frame.logEpoch);
|
|
1188
|
+
w.bool(frame.resetRequired);
|
|
1033
1189
|
break;
|
|
1034
1190
|
}
|
|
1035
1191
|
case 'LEASE': {
|
|
@@ -1095,11 +1251,29 @@ function encodeFrame(
|
|
|
1095
1251
|
break;
|
|
1096
1252
|
}
|
|
1097
1253
|
case 'SUB_START': {
|
|
1254
|
+
checkUnitId(frame.id, 'SUB_START', encodeInvalid);
|
|
1255
|
+
checkFencedOrder(frame.fenced, encodeInvalid);
|
|
1098
1256
|
w.str(frame.id);
|
|
1099
1257
|
w.u8(SUB_STATUS_BYTES[frame.status]);
|
|
1100
1258
|
w.str(frame.reasonCode);
|
|
1101
|
-
writeStringListMap(w, frame.effectiveScopes);
|
|
1102
1259
|
w.bool(frame.bootstrap);
|
|
1260
|
+
w.u32(frame.fenced.length);
|
|
1261
|
+
for (const record of frame.fenced) {
|
|
1262
|
+
w.str(record.table);
|
|
1263
|
+
w.u8(SUB_STATUS_BYTES[record.status]);
|
|
1264
|
+
writeStringListMap(w, record.scopes);
|
|
1265
|
+
}
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case 'LIVE_START': {
|
|
1269
|
+
w.u8(LIVE_STATUS_BYTES[frame.status]);
|
|
1270
|
+
w.str(frame.reasonCode);
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
case 'LIVE_END': {
|
|
1274
|
+
checkDigest(frame.echoDigest, 'LIVE_END.echoDigest', encodeInvalid);
|
|
1275
|
+
w.i64(frame.throughSeq);
|
|
1276
|
+
w.bytes(frame.echoDigest);
|
|
1103
1277
|
break;
|
|
1104
1278
|
}
|
|
1105
1279
|
case 'COMMIT': {
|
|
@@ -1113,22 +1287,16 @@ function encodeFrame(
|
|
|
1113
1287
|
if (change.tableIndex >= frame.tables.length) {
|
|
1114
1288
|
encodeInvalid(`change tableIndex ${change.tableIndex} out of range`);
|
|
1115
1289
|
}
|
|
1116
|
-
if (
|
|
1117
|
-
change
|
|
1118
|
-
(change.rowVersion === undefined || change.row === undefined)
|
|
1119
|
-
) {
|
|
1120
|
-
encodeInvalid('upsert change requires rowVersion and row');
|
|
1290
|
+
if ((change.op === 'upsert') !== (change.row !== undefined)) {
|
|
1291
|
+
encodeInvalid('change row must be present iff op is upsert');
|
|
1121
1292
|
}
|
|
1122
|
-
if (
|
|
1123
|
-
change
|
|
1124
|
-
(change.rowVersion !== undefined || change.row !== undefined)
|
|
1125
|
-
) {
|
|
1126
|
-
encodeInvalid('delete change must not carry rowVersion or row');
|
|
1293
|
+
if (change.rowVersion < 1) {
|
|
1294
|
+
encodeInvalid('change rowVersion must be at least 1');
|
|
1127
1295
|
}
|
|
1128
1296
|
w.u16(change.tableIndex);
|
|
1129
1297
|
w.str(change.rowId);
|
|
1130
1298
|
w.u8(OP_BYTES[change.op]);
|
|
1131
|
-
w.
|
|
1299
|
+
w.i64(change.rowVersion);
|
|
1132
1300
|
writeStringMap(w, change.scopes);
|
|
1133
1301
|
w.opt(change.row, (v) => w.bytes(v));
|
|
1134
1302
|
}
|
|
@@ -1152,7 +1320,8 @@ function encodeFrame(
|
|
|
1152
1320
|
break;
|
|
1153
1321
|
}
|
|
1154
1322
|
case 'SEGMENT_INLINE': {
|
|
1155
|
-
|
|
1323
|
+
w.raw(frame.payload);
|
|
1324
|
+
break;
|
|
1156
1325
|
}
|
|
1157
1326
|
case 'SUB_END': {
|
|
1158
1327
|
w.i64(frame.nextCursor);
|
|
@@ -1168,39 +1337,37 @@ function encodeFrame(
|
|
|
1168
1337
|
w.opt(frame.details, (v) => w.str(v));
|
|
1169
1338
|
break;
|
|
1170
1339
|
}
|
|
1171
|
-
case 'UNKNOWN':
|
|
1172
|
-
|
|
1173
|
-
!Number.isInteger(frame.frameType) ||
|
|
1174
|
-
frame.frameType < 0 ||
|
|
1175
|
-
frame.frameType > 0xff ||
|
|
1176
|
-
KNOWN_FRAME_TYPES.has(frame.frameType)
|
|
1177
|
-
) {
|
|
1178
|
-
encodeInvalid(
|
|
1179
|
-
`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`,
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
return { frameType: frame.frameType, payload: frame.payload };
|
|
1183
|
-
}
|
|
1340
|
+
case 'UNKNOWN':
|
|
1341
|
+
return encodeUnknown(frame, w);
|
|
1184
1342
|
}
|
|
1185
|
-
return
|
|
1343
|
+
return FrameType[frame.type];
|
|
1186
1344
|
}
|
|
1187
1345
|
|
|
1188
1346
|
// ---------------------------------------------------------------------------
|
|
1189
|
-
// Envelope
|
|
1347
|
+
// Envelope. The kind-specific codecs let a peer bundle only the direction it
|
|
1348
|
+
// speaks; `encodeMessage` and `decodeMessage` dispatch on the kind.
|
|
1190
1349
|
// ---------------------------------------------------------------------------
|
|
1191
1350
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1351
|
+
function encodeEnvelope<F extends RequestFrame | ResponseFrame>(
|
|
1352
|
+
wireVersion: number,
|
|
1353
|
+
kindByte: number,
|
|
1354
|
+
frames: readonly F[],
|
|
1355
|
+
validate: (frames: readonly F[]) => void,
|
|
1356
|
+
encode: (frame: F, w: ByteWriter) => number,
|
|
1357
|
+
): Uint8Array {
|
|
1358
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
1359
|
+
encodeInvalid(`unsupported wireVersion ${wireVersion}`);
|
|
1195
1360
|
}
|
|
1196
|
-
|
|
1361
|
+
validate(frames);
|
|
1197
1362
|
const w = new ByteWriter();
|
|
1198
1363
|
w.raw(MAGIC_BYTES);
|
|
1199
|
-
w.u16(
|
|
1200
|
-
w.u8(
|
|
1364
|
+
w.u16(wireVersion);
|
|
1365
|
+
w.u8(kindByte);
|
|
1201
1366
|
w.u8(0x00);
|
|
1202
|
-
for (const frame of
|
|
1203
|
-
const
|
|
1367
|
+
for (const frame of frames) {
|
|
1368
|
+
const p = new ByteWriter();
|
|
1369
|
+
const frameType = encode(frame, p);
|
|
1370
|
+
const payload = p.finish();
|
|
1204
1371
|
w.u8(frameType);
|
|
1205
1372
|
w.u32(payload.length);
|
|
1206
1373
|
w.raw(payload);
|
|
@@ -1210,6 +1377,32 @@ export function encodeMessage(message: SyncMessage): Uint8Array {
|
|
|
1210
1377
|
return w.finish();
|
|
1211
1378
|
}
|
|
1212
1379
|
|
|
1380
|
+
export function encodeRequest(message: RequestMessage): Uint8Array {
|
|
1381
|
+
return encodeEnvelope(
|
|
1382
|
+
message.wireVersion,
|
|
1383
|
+
0x01,
|
|
1384
|
+
message.frames,
|
|
1385
|
+
validateRequestSequence,
|
|
1386
|
+
encodeRequestFrame,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
export function encodeResponse(message: ResponseMessage): Uint8Array {
|
|
1391
|
+
return encodeEnvelope(
|
|
1392
|
+
message.wireVersion,
|
|
1393
|
+
0x02,
|
|
1394
|
+
message.frames,
|
|
1395
|
+
validateResponseSequence,
|
|
1396
|
+
encodeResponseFrame,
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export function encodeMessage(message: SyncMessage): Uint8Array {
|
|
1401
|
+
return message.msgKind === 'request'
|
|
1402
|
+
? encodeRequest(message)
|
|
1403
|
+
: encodeResponse(message);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1213
1406
|
function readFrames<T>(
|
|
1214
1407
|
r: ByteReader,
|
|
1215
1408
|
decodeFrame_: (frameType: number, payload: Uint8Array) => T,
|
|
@@ -1236,7 +1429,11 @@ function readFrames<T>(
|
|
|
1236
1429
|
}
|
|
1237
1430
|
}
|
|
1238
1431
|
|
|
1239
|
-
|
|
1432
|
+
function decodeEnvelope(bytes: Uint8Array): {
|
|
1433
|
+
r: ByteReader;
|
|
1434
|
+
wireVersion: number;
|
|
1435
|
+
request: boolean;
|
|
1436
|
+
} {
|
|
1240
1437
|
const r = new ByteReader(bytes);
|
|
1241
1438
|
if (r.remaining < 8) invalid('truncated envelope header');
|
|
1242
1439
|
const magic = r.raw(4);
|
|
@@ -1258,24 +1455,34 @@ export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
|
1258
1455
|
}
|
|
1259
1456
|
const flags = r.u8();
|
|
1260
1457
|
if (flags !== 0) invalid(`non-zero envelope flags 0x${flags.toString(16)}`);
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1458
|
+
return { r, wireVersion, request: kindByte === 0x01 };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function requestBody(r: ByteReader, wireVersion: number): RequestMessage {
|
|
1462
|
+
const frames = readFrames(r, decodeRequestFrame);
|
|
1463
|
+
validateRequestSequence(frames);
|
|
1464
|
+
return { wireVersion, msgKind: 'request', frames };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function responseBody(r: ByteReader, wireVersion: number): ResponseMessage {
|
|
1468
|
+
const frames = readFrames(r, decodeResponseFrame);
|
|
1469
|
+
validateResponseSequence(frames);
|
|
1470
|
+
return { wireVersion, msgKind: 'response', frames };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
export function decodeRequest(bytes: Uint8Array): RequestMessage {
|
|
1474
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1475
|
+
if (!request) invalid('expected a request message');
|
|
1476
|
+
return requestBody(r, wireVersion);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
export function decodeResponse(bytes: Uint8Array): ResponseMessage {
|
|
1480
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1481
|
+
if (request) invalid('expected a response message');
|
|
1482
|
+
return responseBody(r, wireVersion);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export function decodeMessage(bytes: Uint8Array): SyncMessage {
|
|
1486
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1487
|
+
return request ? requestBody(r, wireVersion) : responseBody(r, wireVersion);
|
|
1281
1488
|
}
|