@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/dist/message.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { ByteReader, ByteWriter, readStringListMap, readStringMap, utf8Encode, writeStringListMap, writeStringMap, } from './bytes.js';
|
|
15
15
|
import { isSupportedProtocolWireVersion, SYNC_PACK_MAGIC } from './constants.js';
|
|
16
16
|
import { DecodeError } from './errors.js';
|
|
17
|
+
import { parseJsonDocument } from './json-document.js';
|
|
17
18
|
import { FrameType, KNOWN_FRAME_TYPES, REQUEST_FRAME_TYPES, RESPONSE_FRAME_TYPES, } from './frames.js';
|
|
18
19
|
import { normalizeRejectionDetails, } from './rejection-details.js';
|
|
19
20
|
import { decodeRowsSegment } from './segment.js';
|
|
@@ -28,13 +29,10 @@ function definedObject(record) {
|
|
|
28
29
|
function invalid(message) {
|
|
29
30
|
throw new DecodeError('sync.invalid_request', message);
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
catch {
|
|
36
|
-
invalid(`${what} is not a valid JSON document`);
|
|
37
|
-
}
|
|
32
|
+
/** A `json` field: a valid JSON document (Conventions), kept as raw text. */
|
|
33
|
+
function readJson(r, what) {
|
|
34
|
+
const raw = r.str();
|
|
35
|
+
parseJsonDocument(raw, what);
|
|
38
36
|
return raw;
|
|
39
37
|
}
|
|
40
38
|
/** §1.5: a `ROUND_FAILURE` code is a lowercase dotted catalog code. */
|
|
@@ -50,29 +48,63 @@ function validateRoundFailureCode(code, fail) {
|
|
|
50
48
|
fail('ROUND_FAILURE.code must be a lowercase dotted code of at most 64 bytes');
|
|
51
49
|
}
|
|
52
50
|
}
|
|
51
|
+
/** §1.5, §1.7: a present `outboxId` is non-empty and at most 64 bytes. */
|
|
52
|
+
function checkOutboxId(outboxId, fail) {
|
|
53
|
+
if (outboxId.length === 0 || utf8Encode(outboxId).length > 64) {
|
|
54
|
+
fail('REQ_HEADER.outboxId must be non-empty and at most 64 bytes');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** §4.3, §4.4: a unit id is 32 lowercase hex characters. */
|
|
58
|
+
function checkUnitId(id, frame, fail) {
|
|
59
|
+
if (!/^[0-9a-f]{32}$/.test(id)) {
|
|
60
|
+
fail(`${frame}.id must be 32 lowercase hex characters`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** §4.2, §4.4: a digest is 32 bytes; an echo digest may also be empty. */
|
|
64
|
+
function checkDigest(digest, field, fail) {
|
|
65
|
+
const emptyAllowed = field !== 'PULL_HEADER.registrationDigest';
|
|
66
|
+
if (digest.length !== 32 && !(emptyAllowed && digest.length === 0)) {
|
|
67
|
+
fail(`${field} must be ${emptyAllowed ? '0 or ' : ''}32 bytes`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** §4.2, §4.3: a cursor is `-1` or a commitSeq. */
|
|
71
|
+
function checkCursor(cursor, field, fail) {
|
|
72
|
+
if (cursor < -1)
|
|
73
|
+
fail(`${field} must be >= -1`);
|
|
74
|
+
}
|
|
75
|
+
/** §4.4: fenced records in strictly ascending code-unit order of `table`. */
|
|
76
|
+
function checkFencedOrder(fenced, fail) {
|
|
77
|
+
for (let i = 1; i < fenced.length; i++) {
|
|
78
|
+
if (fenced[i - 1].table >= fenced[i].table) {
|
|
79
|
+
fail('SUB_START.fenced tables must be unique and in ascending order');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
53
83
|
// ---------------------------------------------------------------------------
|
|
54
84
|
// Frame payload decoders (each consumes exactly the frame payload)
|
|
55
85
|
// ---------------------------------------------------------------------------
|
|
56
|
-
function decodeReqHeader(r
|
|
86
|
+
function decodeReqHeader(r) {
|
|
57
87
|
const clientId = r.str();
|
|
58
88
|
if (clientId.length === 0)
|
|
59
89
|
invalid('REQ_HEADER.clientId must be non-empty');
|
|
60
90
|
const schemaVersion = r.i32();
|
|
61
91
|
if (schemaVersion < 1)
|
|
62
92
|
invalid('REQ_HEADER.schemaVersion must be >= 1');
|
|
63
|
-
const logEpoch =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
const logEpoch = r.opt(() => {
|
|
94
|
+
const value = r.str();
|
|
95
|
+
if (value.length === 0)
|
|
96
|
+
invalid('REQ_HEADER.logEpoch must be non-empty');
|
|
97
|
+
return value;
|
|
98
|
+
});
|
|
99
|
+
const outboxId = r.opt(() => r.str());
|
|
100
|
+
if (outboxId !== undefined)
|
|
101
|
+
checkOutboxId(outboxId, invalid);
|
|
71
102
|
return definedObject({
|
|
72
103
|
type: 'REQ_HEADER',
|
|
73
104
|
clientId,
|
|
74
105
|
schemaVersion,
|
|
75
106
|
logEpoch: logEpoch,
|
|
107
|
+
outboxId: outboxId,
|
|
76
108
|
});
|
|
77
109
|
}
|
|
78
110
|
function decodeRoundFailure(r) {
|
|
@@ -85,6 +117,9 @@ function decodePushCommit(r) {
|
|
|
85
117
|
if (clientCommitId.length === 0) {
|
|
86
118
|
invalid('PUSH_COMMIT.clientCommitId must be non-empty');
|
|
87
119
|
}
|
|
120
|
+
const clientSeq = r.i64();
|
|
121
|
+
if (clientSeq < 0)
|
|
122
|
+
invalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
88
123
|
const operationCount = r.u32();
|
|
89
124
|
if (operationCount === 0) {
|
|
90
125
|
throw new DecodeError('sync.empty_commit', 'PUSH_COMMIT must carry at least one operation');
|
|
@@ -117,7 +152,7 @@ function decodePushCommit(r) {
|
|
|
117
152
|
payload: payload,
|
|
118
153
|
}));
|
|
119
154
|
}
|
|
120
|
-
return { type: 'PUSH_COMMIT', clientCommitId, operations };
|
|
155
|
+
return { type: 'PUSH_COMMIT', clientCommitId, clientSeq, operations };
|
|
121
156
|
}
|
|
122
157
|
function decodePullHeader(r) {
|
|
123
158
|
const limitCommits = r.i32();
|
|
@@ -127,45 +162,50 @@ function decodePullHeader(r) {
|
|
|
127
162
|
if ((accept & 0xf0) !== 0) {
|
|
128
163
|
invalid('PULL_HEADER.accept bits 4-7 must be zero');
|
|
129
164
|
}
|
|
165
|
+
const cursor = r.i64();
|
|
166
|
+
checkCursor(cursor, 'PULL_HEADER.cursor', invalid);
|
|
167
|
+
const registrationDigest = r.bytes();
|
|
168
|
+
checkDigest(registrationDigest, 'PULL_HEADER.registrationDigest', invalid);
|
|
169
|
+
const echoDigest = r.bytes();
|
|
170
|
+
checkDigest(echoDigest, 'PULL_HEADER.echoDigest', invalid);
|
|
130
171
|
return {
|
|
131
172
|
type: 'PULL_HEADER',
|
|
132
173
|
limitCommits,
|
|
133
174
|
limitSnapshotRows,
|
|
134
175
|
maxSnapshotPages,
|
|
135
176
|
accept,
|
|
177
|
+
cursor,
|
|
178
|
+
registrationDigest,
|
|
179
|
+
echoDigest,
|
|
136
180
|
};
|
|
137
181
|
}
|
|
138
|
-
function
|
|
182
|
+
function decodeProgress(r) {
|
|
139
183
|
const id = r.str();
|
|
140
|
-
|
|
141
|
-
const scopes = readStringListMap(r);
|
|
142
|
-
const params = r.opt(() => requireJsonDocument(r.str(), 'SUBSCRIPTION.params'));
|
|
184
|
+
checkUnitId(id, 'PROGRESS', invalid);
|
|
143
185
|
const cursor = r.i64();
|
|
144
|
-
|
|
186
|
+
checkCursor(cursor, 'PROGRESS.cursor', invalid);
|
|
187
|
+
const bootstrapState = r.opt(() => readJson(r, 'PROGRESS.bootstrapState'));
|
|
145
188
|
return definedObject({
|
|
146
|
-
type: '
|
|
189
|
+
type: 'PROGRESS',
|
|
147
190
|
id,
|
|
148
|
-
table,
|
|
149
|
-
scopes,
|
|
150
|
-
params: params,
|
|
151
191
|
cursor,
|
|
152
192
|
bootstrapState: bootstrapState,
|
|
153
193
|
});
|
|
154
194
|
}
|
|
155
|
-
function decodeRespHeader(r
|
|
195
|
+
function decodeRespHeader(r) {
|
|
156
196
|
const requiredSchemaVersion = r.opt(() => r.i32());
|
|
157
197
|
const latestSchemaVersion = r.opt(() => r.i32());
|
|
158
|
-
const logEpoch =
|
|
159
|
-
if (logEpoch
|
|
198
|
+
const logEpoch = r.str();
|
|
199
|
+
if (logEpoch.length === 0) {
|
|
160
200
|
invalid('RESP_HEADER.logEpoch must be non-empty');
|
|
161
201
|
}
|
|
162
|
-
const resetRequired =
|
|
202
|
+
const resetRequired = r.bool();
|
|
163
203
|
return definedObject({
|
|
164
204
|
type: 'RESP_HEADER',
|
|
165
205
|
requiredSchemaVersion: requiredSchemaVersion,
|
|
166
206
|
latestSchemaVersion: latestSchemaVersion,
|
|
167
|
-
logEpoch
|
|
168
|
-
resetRequired
|
|
207
|
+
logEpoch,
|
|
208
|
+
resetRequired,
|
|
169
209
|
});
|
|
170
210
|
}
|
|
171
211
|
function decodeLease(r) {
|
|
@@ -250,9 +290,10 @@ function decodePushResultDetails(r) {
|
|
|
250
290
|
invalid('PUSH_RESULT_DETAILS opIndex values must be unique and non-negative');
|
|
251
291
|
}
|
|
252
292
|
seen.add(opIndex);
|
|
293
|
+
const parsed = parseJsonDocument(r.str(), 'PUSH_RESULT_DETAILS.details');
|
|
253
294
|
let details;
|
|
254
295
|
try {
|
|
255
|
-
details = normalizeRejectionDetails(
|
|
296
|
+
details = normalizeRejectionDetails(parsed);
|
|
256
297
|
}
|
|
257
298
|
catch (error) {
|
|
258
299
|
invalid(`invalid PUSH_RESULT_DETAILS.details: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -263,6 +304,7 @@ function decodePushResultDetails(r) {
|
|
|
263
304
|
}
|
|
264
305
|
function decodeSubStart(r) {
|
|
265
306
|
const id = r.str();
|
|
307
|
+
checkUnitId(id, 'SUB_START', invalid);
|
|
266
308
|
const statusByte = r.u8();
|
|
267
309
|
let status;
|
|
268
310
|
if (statusByte === 1)
|
|
@@ -274,16 +316,42 @@ function decodeSubStart(r) {
|
|
|
274
316
|
else
|
|
275
317
|
invalid(`invalid SUB_START status byte ${statusByte}`);
|
|
276
318
|
const reasonCode = r.str();
|
|
277
|
-
const effectiveScopes = readStringListMap(r);
|
|
278
319
|
const bootstrap = r.bool();
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
320
|
+
const fencedCount = r.u32();
|
|
321
|
+
const fenced = [];
|
|
322
|
+
for (let i = 0; i < fencedCount; i++) {
|
|
323
|
+
const table = r.str();
|
|
324
|
+
const fencedStatus = r.u8();
|
|
325
|
+
if (fencedStatus !== 1 && fencedStatus !== 2) {
|
|
326
|
+
invalid('invalid fenced record status byte');
|
|
327
|
+
}
|
|
328
|
+
fenced.push({
|
|
329
|
+
table,
|
|
330
|
+
status: fencedStatus === 1 ? 'active' : 'revoked',
|
|
331
|
+
scopes: readStringListMap(r),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
checkFencedOrder(fenced, invalid);
|
|
335
|
+
return { type: 'SUB_START', id, status, reasonCode, bootstrap, fenced };
|
|
336
|
+
}
|
|
337
|
+
function decodeLiveStart(r) {
|
|
338
|
+
const statusByte = r.u8();
|
|
339
|
+
let status;
|
|
340
|
+
if (statusByte === 1)
|
|
341
|
+
status = 'active';
|
|
342
|
+
else if (statusByte === 3)
|
|
343
|
+
status = 'reset';
|
|
344
|
+
else if (statusByte === 4)
|
|
345
|
+
status = 'unregistered';
|
|
346
|
+
else
|
|
347
|
+
invalid('invalid LIVE_START status byte');
|
|
348
|
+
return { type: 'LIVE_START', status, reasonCode: r.str() };
|
|
349
|
+
}
|
|
350
|
+
function decodeLiveEnd(r) {
|
|
351
|
+
const throughSeq = r.i64();
|
|
352
|
+
const echoDigest = r.bytes();
|
|
353
|
+
checkDigest(echoDigest, 'LIVE_END.echoDigest', invalid);
|
|
354
|
+
return { type: 'LIVE_END', throughSeq, echoDigest };
|
|
287
355
|
}
|
|
288
356
|
function decodeCommit(r) {
|
|
289
357
|
const commitSeq = r.i64();
|
|
@@ -309,23 +377,15 @@ function decodeCommit(r) {
|
|
|
309
377
|
op = 'delete';
|
|
310
378
|
else
|
|
311
379
|
invalid(`invalid change op byte ${opByte}`);
|
|
312
|
-
const rowVersion = r.
|
|
380
|
+
const rowVersion = r.i64();
|
|
381
|
+
if (rowVersion < 1)
|
|
382
|
+
invalid('change rowVersion must be at least 1');
|
|
313
383
|
const scopes = readStringMap(r);
|
|
314
384
|
const row = r.opt(() => r.bytes());
|
|
315
|
-
if (op === 'upsert'
|
|
316
|
-
invalid('
|
|
317
|
-
}
|
|
318
|
-
if (op === 'delete' && (rowVersion !== undefined || row !== undefined)) {
|
|
319
|
-
invalid('delete change must not carry rowVersion or row');
|
|
385
|
+
if ((op === 'upsert') !== (row !== undefined)) {
|
|
386
|
+
invalid('change row must be present iff op is upsert');
|
|
320
387
|
}
|
|
321
|
-
changes.push(definedObject({
|
|
322
|
-
tableIndex,
|
|
323
|
-
rowId,
|
|
324
|
-
op,
|
|
325
|
-
rowVersion: rowVersion,
|
|
326
|
-
scopes,
|
|
327
|
-
row: row,
|
|
328
|
-
}));
|
|
388
|
+
changes.push(definedObject({ tableIndex, rowId, op, rowVersion, scopes, row }));
|
|
329
389
|
}
|
|
330
390
|
return { type: 'COMMIT', commitSeq, createdAtMs, actorId, tables, changes };
|
|
331
391
|
}
|
|
@@ -375,7 +435,7 @@ function decodeSegmentInline(r) {
|
|
|
375
435
|
}
|
|
376
436
|
function decodeSubEnd(r) {
|
|
377
437
|
const nextCursor = r.i64();
|
|
378
|
-
const bootstrapState = r.opt(() =>
|
|
438
|
+
const bootstrapState = r.opt(() => readJson(r, 'SUB_END.bootstrapState'));
|
|
379
439
|
return definedObject({
|
|
380
440
|
type: 'SUB_END',
|
|
381
441
|
nextCursor,
|
|
@@ -388,7 +448,7 @@ function decodeErrorFrame(r) {
|
|
|
388
448
|
const category = r.str();
|
|
389
449
|
const retryable = r.bool();
|
|
390
450
|
const recommendedAction = r.str();
|
|
391
|
-
const details = r.opt(() =>
|
|
451
|
+
const details = r.opt(() => readJson(r, 'ERROR.details'));
|
|
392
452
|
return definedObject({
|
|
393
453
|
type: 'ERROR',
|
|
394
454
|
code,
|
|
@@ -399,12 +459,12 @@ function decodeErrorFrame(r) {
|
|
|
399
459
|
details: details,
|
|
400
460
|
});
|
|
401
461
|
}
|
|
402
|
-
function decodeRequestFrame(frameType, payload
|
|
462
|
+
function decodeRequestFrame(frameType, payload) {
|
|
403
463
|
const r = new ByteReader(payload);
|
|
404
464
|
let frame;
|
|
405
465
|
switch (frameType) {
|
|
406
466
|
case FrameType.REQ_HEADER:
|
|
407
|
-
frame = decodeReqHeader(r
|
|
467
|
+
frame = decodeReqHeader(r);
|
|
408
468
|
break;
|
|
409
469
|
case FrameType.ROUND_FAILURE:
|
|
410
470
|
frame = decodeRoundFailure(r);
|
|
@@ -415,8 +475,11 @@ function decodeRequestFrame(frameType, payload, wireVersion) {
|
|
|
415
475
|
case FrameType.PULL_HEADER:
|
|
416
476
|
frame = decodePullHeader(r);
|
|
417
477
|
break;
|
|
418
|
-
case FrameType.
|
|
419
|
-
frame =
|
|
478
|
+
case FrameType.UNIT:
|
|
479
|
+
frame = { type: 'UNIT', scopes: readStringMap(r) };
|
|
480
|
+
break;
|
|
481
|
+
case FrameType.PROGRESS:
|
|
482
|
+
frame = decodeProgress(r);
|
|
420
483
|
break;
|
|
421
484
|
default:
|
|
422
485
|
if (RESPONSE_FRAME_TYPES.has(frameType)) {
|
|
@@ -427,12 +490,12 @@ function decodeRequestFrame(frameType, payload, wireVersion) {
|
|
|
427
490
|
r.expectFullyConsumed(`${frame.type} frame payload`);
|
|
428
491
|
return frame;
|
|
429
492
|
}
|
|
430
|
-
function decodeResponseFrame(frameType, payload
|
|
493
|
+
function decodeResponseFrame(frameType, payload) {
|
|
431
494
|
const r = new ByteReader(payload);
|
|
432
495
|
let frame;
|
|
433
496
|
switch (frameType) {
|
|
434
497
|
case FrameType.RESP_HEADER:
|
|
435
|
-
frame = decodeRespHeader(r
|
|
498
|
+
frame = decodeRespHeader(r);
|
|
436
499
|
break;
|
|
437
500
|
case FrameType.LEASE:
|
|
438
501
|
frame = decodeLease(r);
|
|
@@ -458,6 +521,12 @@ function decodeResponseFrame(frameType, payload, wireVersion) {
|
|
|
458
521
|
case FrameType.SUB_END:
|
|
459
522
|
frame = decodeSubEnd(r);
|
|
460
523
|
break;
|
|
524
|
+
case FrameType.LIVE_START:
|
|
525
|
+
frame = decodeLiveStart(r);
|
|
526
|
+
break;
|
|
527
|
+
case FrameType.LIVE_END:
|
|
528
|
+
frame = decodeLiveEnd(r);
|
|
529
|
+
break;
|
|
461
530
|
case FrameType.ERROR:
|
|
462
531
|
frame = decodeErrorFrame(r);
|
|
463
532
|
break;
|
|
@@ -480,6 +549,7 @@ function validateRequestSequence(frames) {
|
|
|
480
549
|
}
|
|
481
550
|
let pushCount = 0;
|
|
482
551
|
let hasPull = false;
|
|
552
|
+
let hasProgress = false;
|
|
483
553
|
let index = 1;
|
|
484
554
|
let sawRoundFailure = false;
|
|
485
555
|
for (const frame of frames.slice(1)) {
|
|
@@ -506,9 +576,16 @@ function validateRequestSequence(frames) {
|
|
|
506
576
|
invalid('duplicate PULL_HEADER frame');
|
|
507
577
|
hasPull = true;
|
|
508
578
|
break;
|
|
509
|
-
case '
|
|
579
|
+
case 'UNIT':
|
|
510
580
|
if (!hasPull)
|
|
511
|
-
invalid('
|
|
581
|
+
invalid('UNIT frame without a PULL_HEADER');
|
|
582
|
+
if (hasProgress)
|
|
583
|
+
invalid('UNIT frame after a PROGRESS frame');
|
|
584
|
+
break;
|
|
585
|
+
case 'PROGRESS':
|
|
586
|
+
if (!hasPull)
|
|
587
|
+
invalid('PROGRESS frame without a PULL_HEADER');
|
|
588
|
+
hasProgress = true;
|
|
512
589
|
break;
|
|
513
590
|
case 'UNKNOWN':
|
|
514
591
|
break;
|
|
@@ -524,12 +601,12 @@ function validateResponseSequence(frames) {
|
|
|
524
601
|
if (first === undefined || first.type !== 'RESP_HEADER') {
|
|
525
602
|
invalid('response must start with a RESP_HEADER frame');
|
|
526
603
|
}
|
|
527
|
-
|
|
528
|
-
let
|
|
604
|
+
// §1.6 sections: unit sections, then at most one live section.
|
|
605
|
+
let section = 'none';
|
|
606
|
+
let sawUnitSection = false;
|
|
607
|
+
let sawLive = false;
|
|
529
608
|
let sawLease = false;
|
|
530
609
|
let sawBody = false;
|
|
531
|
-
let subscriptionHasCommits = false;
|
|
532
|
-
let subscriptionHasSegments = false;
|
|
533
610
|
let errorSeen = false;
|
|
534
611
|
let lastPushResult;
|
|
535
612
|
let lastPushResultHasDetails = false;
|
|
@@ -541,8 +618,7 @@ function validateResponseSequence(frames) {
|
|
|
541
618
|
invalid('duplicate RESP_HEADER frame');
|
|
542
619
|
break;
|
|
543
620
|
case 'LEASE':
|
|
544
|
-
// §7.3.2: at most one LEASE, immediately after RESP_HEADER
|
|
545
|
-
// any PUSH_RESULT / subscription section / ERROR).
|
|
621
|
+
// §7.3.2: at most one LEASE, immediately after RESP_HEADER.
|
|
546
622
|
if (sawLease)
|
|
547
623
|
invalid('duplicate LEASE frame');
|
|
548
624
|
if (sawBody)
|
|
@@ -550,15 +626,16 @@ function validateResponseSequence(frames) {
|
|
|
550
626
|
sawLease = true;
|
|
551
627
|
break;
|
|
552
628
|
case 'PUSH_RESULT':
|
|
553
|
-
if (
|
|
554
|
-
invalid('PUSH_RESULT frame after
|
|
629
|
+
if (sawUnitSection || sawLive) {
|
|
630
|
+
invalid('PUSH_RESULT frame after a unit or live section');
|
|
631
|
+
}
|
|
555
632
|
lastPushResult = frame;
|
|
556
633
|
lastPushResultHasDetails = false;
|
|
557
634
|
sawBody = true;
|
|
558
635
|
break;
|
|
559
636
|
case 'PUSH_RESULT_DETAILS': {
|
|
560
|
-
if (
|
|
561
|
-
invalid('PUSH_RESULT_DETAILS frame after
|
|
637
|
+
if (sawUnitSection || sawLive) {
|
|
638
|
+
invalid('PUSH_RESULT_DETAILS frame after a unit or live section');
|
|
562
639
|
}
|
|
563
640
|
if (lastPushResult === undefined) {
|
|
564
641
|
invalid('PUSH_RESULT_DETAILS without a preceding PUSH_RESULT');
|
|
@@ -579,36 +656,43 @@ function validateResponseSequence(frames) {
|
|
|
579
656
|
break;
|
|
580
657
|
}
|
|
581
658
|
case 'SUB_START':
|
|
582
|
-
if (
|
|
659
|
+
if (section === 'unit')
|
|
583
660
|
invalid('nested SUB_START frame');
|
|
584
|
-
|
|
585
|
-
|
|
661
|
+
if (sawLive)
|
|
662
|
+
invalid('SUB_START frame inside or after the live section');
|
|
663
|
+
section = 'unit';
|
|
664
|
+
sawUnitSection = true;
|
|
586
665
|
sawBody = true;
|
|
587
666
|
lastPushResult = undefined;
|
|
588
|
-
subscriptionHasCommits = false;
|
|
589
|
-
subscriptionHasSegments = false;
|
|
590
|
-
break;
|
|
591
|
-
case 'COMMIT':
|
|
592
|
-
if (!inSubscription)
|
|
593
|
-
invalid('COMMIT frame outside a subscription');
|
|
594
|
-
if (subscriptionHasSegments) {
|
|
595
|
-
invalid('COMMIT and segment frames must not both appear for one subscription');
|
|
596
|
-
}
|
|
597
|
-
subscriptionHasCommits = true;
|
|
598
667
|
break;
|
|
599
668
|
case 'SEGMENT_REF':
|
|
600
669
|
case 'SEGMENT_INLINE':
|
|
601
|
-
if (
|
|
602
|
-
invalid('segment frame outside a
|
|
603
|
-
if (subscriptionHasCommits) {
|
|
604
|
-
invalid('COMMIT and segment frames must not both appear for one subscription');
|
|
605
|
-
}
|
|
606
|
-
subscriptionHasSegments = true;
|
|
670
|
+
if (section !== 'unit')
|
|
671
|
+
invalid('segment frame outside a unit section');
|
|
607
672
|
break;
|
|
608
673
|
case 'SUB_END':
|
|
609
|
-
if (
|
|
674
|
+
if (section !== 'unit')
|
|
610
675
|
invalid('SUB_END frame without an open SUB_START');
|
|
611
|
-
|
|
676
|
+
section = 'none';
|
|
677
|
+
break;
|
|
678
|
+
case 'LIVE_START':
|
|
679
|
+
if (section === 'unit')
|
|
680
|
+
invalid('LIVE_START frame inside a unit section');
|
|
681
|
+
if (sawLive)
|
|
682
|
+
invalid('second LIVE_START frame');
|
|
683
|
+
section = 'live';
|
|
684
|
+
sawLive = true;
|
|
685
|
+
sawBody = true;
|
|
686
|
+
lastPushResult = undefined;
|
|
687
|
+
break;
|
|
688
|
+
case 'COMMIT':
|
|
689
|
+
if (section !== 'live')
|
|
690
|
+
invalid('COMMIT frame outside the live section');
|
|
691
|
+
break;
|
|
692
|
+
case 'LIVE_END':
|
|
693
|
+
if (section !== 'live')
|
|
694
|
+
invalid('LIVE_END frame without an open LIVE_START');
|
|
695
|
+
section = 'none';
|
|
612
696
|
break;
|
|
613
697
|
case 'ERROR':
|
|
614
698
|
errorSeen = true;
|
|
@@ -621,28 +705,33 @@ function validateResponseSequence(frames) {
|
|
|
621
705
|
break;
|
|
622
706
|
}
|
|
623
707
|
}
|
|
624
|
-
if (
|
|
625
|
-
invalid('
|
|
708
|
+
if (section !== 'none' && !errorSeen) {
|
|
709
|
+
invalid('unit or live section not terminated');
|
|
626
710
|
}
|
|
627
711
|
}
|
|
628
|
-
export function validateFrameSequence(message) {
|
|
629
|
-
if (message.msgKind === 'request')
|
|
630
|
-
validateRequestSequence(message.frames);
|
|
631
|
-
else
|
|
632
|
-
validateResponseSequence(message.frames);
|
|
633
|
-
}
|
|
634
712
|
// ---------------------------------------------------------------------------
|
|
635
713
|
// Frame payload encoders
|
|
636
714
|
// ---------------------------------------------------------------------------
|
|
637
715
|
const OP_BYTES = { upsert: 1, delete: 2 };
|
|
638
716
|
const PUSH_STATUS_BYTES = { applied: 1, cached: 2, rejected: 3 };
|
|
639
717
|
const SUB_STATUS_BYTES = { active: 1, revoked: 2, reset: 3 };
|
|
718
|
+
const LIVE_STATUS_BYTES = { active: 1, reset: 3, unregistered: 4 };
|
|
640
719
|
const MEDIA_TYPE_BYTES = { rows: 1, sqlite: 2 };
|
|
641
720
|
function encodeInvalid(message) {
|
|
642
721
|
throw new Error(message);
|
|
643
722
|
}
|
|
644
|
-
function
|
|
645
|
-
|
|
723
|
+
function encodeUnknown(frame, w) {
|
|
724
|
+
if (!Number.isInteger(frame.frameType) ||
|
|
725
|
+
frame.frameType < 0 ||
|
|
726
|
+
frame.frameType > 0xff ||
|
|
727
|
+
KNOWN_FRAME_TYPES.has(frame.frameType)) {
|
|
728
|
+
encodeInvalid(`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`);
|
|
729
|
+
}
|
|
730
|
+
w.raw(frame.payload);
|
|
731
|
+
return frame.frameType;
|
|
732
|
+
}
|
|
733
|
+
/** Writes the payload; returns the frame type byte. */
|
|
734
|
+
function encodeRequestFrame(frame, w) {
|
|
646
735
|
switch (frame.type) {
|
|
647
736
|
case 'REQ_HEADER': {
|
|
648
737
|
if (frame.clientId.length === 0) {
|
|
@@ -653,15 +742,14 @@ function encodeFrame(frame, wireVersion) {
|
|
|
653
742
|
}
|
|
654
743
|
w.str(frame.clientId);
|
|
655
744
|
w.i32(frame.schemaVersion);
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
encodeInvalid('REQ_HEADER.logEpoch must be non-empty');
|
|
659
|
-
}
|
|
660
|
-
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
745
|
+
if (frame.logEpoch === '') {
|
|
746
|
+
encodeInvalid('REQ_HEADER.logEpoch must be non-empty');
|
|
661
747
|
}
|
|
662
|
-
|
|
663
|
-
|
|
748
|
+
w.opt(frame.logEpoch, (v) => w.str(v));
|
|
749
|
+
if (frame.outboxId !== undefined) {
|
|
750
|
+
checkOutboxId(frame.outboxId, encodeInvalid);
|
|
664
751
|
}
|
|
752
|
+
w.opt(frame.outboxId, (v) => w.str(v));
|
|
665
753
|
break;
|
|
666
754
|
}
|
|
667
755
|
case 'ROUND_FAILURE': {
|
|
@@ -676,7 +764,11 @@ function encodeFrame(frame, wireVersion) {
|
|
|
676
764
|
if (frame.operations.length === 0) {
|
|
677
765
|
encodeInvalid('PUSH_COMMIT must carry at least one operation');
|
|
678
766
|
}
|
|
767
|
+
if (frame.clientSeq < 0) {
|
|
768
|
+
encodeInvalid('PUSH_COMMIT.clientSeq must be >= 0');
|
|
769
|
+
}
|
|
679
770
|
w.str(frame.clientCommitId);
|
|
771
|
+
w.i64(frame.clientSeq);
|
|
680
772
|
w.u32(frame.operations.length);
|
|
681
773
|
for (const operation of frame.operations) {
|
|
682
774
|
if (operation.op === 'upsert' && operation.payload === undefined) {
|
|
@@ -700,35 +792,43 @@ function encodeFrame(frame, wireVersion) {
|
|
|
700
792
|
w.i32(frame.limitCommits);
|
|
701
793
|
w.i32(frame.limitSnapshotRows);
|
|
702
794
|
w.i32(frame.maxSnapshotPages);
|
|
795
|
+
checkCursor(frame.cursor, 'PULL_HEADER.cursor', encodeInvalid);
|
|
796
|
+
checkDigest(frame.registrationDigest, 'PULL_HEADER.registrationDigest', encodeInvalid);
|
|
797
|
+
checkDigest(frame.echoDigest, 'PULL_HEADER.echoDigest', encodeInvalid);
|
|
703
798
|
w.u8(frame.accept);
|
|
799
|
+
w.i64(frame.cursor);
|
|
800
|
+
w.bytes(frame.registrationDigest);
|
|
801
|
+
w.bytes(frame.echoDigest);
|
|
704
802
|
break;
|
|
705
803
|
}
|
|
706
|
-
case '
|
|
804
|
+
case 'UNIT': {
|
|
805
|
+
writeStringMap(w, frame.scopes);
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
case 'PROGRESS': {
|
|
809
|
+
checkUnitId(frame.id, 'PROGRESS', encodeInvalid);
|
|
810
|
+
checkCursor(frame.cursor, 'PROGRESS.cursor', encodeInvalid);
|
|
707
811
|
w.str(frame.id);
|
|
708
|
-
w.str(frame.table);
|
|
709
|
-
writeStringListMap(w, frame.scopes);
|
|
710
|
-
w.opt(frame.params, (v) => w.str(v));
|
|
711
812
|
w.i64(frame.cursor);
|
|
712
813
|
w.opt(frame.bootstrapState, (v) => w.str(v));
|
|
713
814
|
break;
|
|
714
815
|
}
|
|
816
|
+
case 'UNKNOWN':
|
|
817
|
+
return encodeUnknown(frame, w);
|
|
818
|
+
}
|
|
819
|
+
return FrameType[frame.type];
|
|
820
|
+
}
|
|
821
|
+
/** Writes the payload; returns the frame type byte. */
|
|
822
|
+
function encodeResponseFrame(frame, w) {
|
|
823
|
+
switch (frame.type) {
|
|
715
824
|
case 'RESP_HEADER': {
|
|
716
825
|
w.opt(frame.requiredSchemaVersion, (v) => w.i32(v));
|
|
717
826
|
w.opt(frame.latestSchemaVersion, (v) => w.i32(v));
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
encodeInvalid('RESP_HEADER.logEpoch is required and non-empty');
|
|
721
|
-
}
|
|
722
|
-
if (frame.resetRequired === undefined) {
|
|
723
|
-
encodeInvalid('RESP_HEADER.resetRequired is required');
|
|
724
|
-
}
|
|
725
|
-
w.str(frame.logEpoch);
|
|
726
|
-
w.bool(frame.resetRequired);
|
|
727
|
-
}
|
|
728
|
-
else if (frame.logEpoch !== undefined ||
|
|
729
|
-
frame.resetRequired !== undefined) {
|
|
730
|
-
encodeInvalid('RESP_HEADER epoch fields require wireVersion 2');
|
|
827
|
+
if (frame.logEpoch.length === 0) {
|
|
828
|
+
encodeInvalid('RESP_HEADER.logEpoch must be non-empty');
|
|
731
829
|
}
|
|
830
|
+
w.str(frame.logEpoch);
|
|
831
|
+
w.bool(frame.resetRequired);
|
|
732
832
|
break;
|
|
733
833
|
}
|
|
734
834
|
case 'LEASE': {
|
|
@@ -796,11 +896,29 @@ function encodeFrame(frame, wireVersion) {
|
|
|
796
896
|
break;
|
|
797
897
|
}
|
|
798
898
|
case 'SUB_START': {
|
|
899
|
+
checkUnitId(frame.id, 'SUB_START', encodeInvalid);
|
|
900
|
+
checkFencedOrder(frame.fenced, encodeInvalid);
|
|
799
901
|
w.str(frame.id);
|
|
800
902
|
w.u8(SUB_STATUS_BYTES[frame.status]);
|
|
801
903
|
w.str(frame.reasonCode);
|
|
802
|
-
writeStringListMap(w, frame.effectiveScopes);
|
|
803
904
|
w.bool(frame.bootstrap);
|
|
905
|
+
w.u32(frame.fenced.length);
|
|
906
|
+
for (const record of frame.fenced) {
|
|
907
|
+
w.str(record.table);
|
|
908
|
+
w.u8(SUB_STATUS_BYTES[record.status]);
|
|
909
|
+
writeStringListMap(w, record.scopes);
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case 'LIVE_START': {
|
|
914
|
+
w.u8(LIVE_STATUS_BYTES[frame.status]);
|
|
915
|
+
w.str(frame.reasonCode);
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case 'LIVE_END': {
|
|
919
|
+
checkDigest(frame.echoDigest, 'LIVE_END.echoDigest', encodeInvalid);
|
|
920
|
+
w.i64(frame.throughSeq);
|
|
921
|
+
w.bytes(frame.echoDigest);
|
|
804
922
|
break;
|
|
805
923
|
}
|
|
806
924
|
case 'COMMIT': {
|
|
@@ -815,18 +933,16 @@ function encodeFrame(frame, wireVersion) {
|
|
|
815
933
|
if (change.tableIndex >= frame.tables.length) {
|
|
816
934
|
encodeInvalid(`change tableIndex ${change.tableIndex} out of range`);
|
|
817
935
|
}
|
|
818
|
-
if (change.op === 'upsert'
|
|
819
|
-
(change
|
|
820
|
-
encodeInvalid('upsert change requires rowVersion and row');
|
|
936
|
+
if ((change.op === 'upsert') !== (change.row !== undefined)) {
|
|
937
|
+
encodeInvalid('change row must be present iff op is upsert');
|
|
821
938
|
}
|
|
822
|
-
if (change.
|
|
823
|
-
(change
|
|
824
|
-
encodeInvalid('delete change must not carry rowVersion or row');
|
|
939
|
+
if (change.rowVersion < 1) {
|
|
940
|
+
encodeInvalid('change rowVersion must be at least 1');
|
|
825
941
|
}
|
|
826
942
|
w.u16(change.tableIndex);
|
|
827
943
|
w.str(change.rowId);
|
|
828
944
|
w.u8(OP_BYTES[change.op]);
|
|
829
|
-
w.
|
|
945
|
+
w.i64(change.rowVersion);
|
|
830
946
|
writeStringMap(w, change.scopes);
|
|
831
947
|
w.opt(change.row, (v) => w.bytes(v));
|
|
832
948
|
}
|
|
@@ -850,7 +966,8 @@ function encodeFrame(frame, wireVersion) {
|
|
|
850
966
|
break;
|
|
851
967
|
}
|
|
852
968
|
case 'SEGMENT_INLINE': {
|
|
853
|
-
|
|
969
|
+
w.raw(frame.payload);
|
|
970
|
+
break;
|
|
854
971
|
}
|
|
855
972
|
case 'SUB_END': {
|
|
856
973
|
w.i64(frame.nextCursor);
|
|
@@ -866,33 +983,29 @@ function encodeFrame(frame, wireVersion) {
|
|
|
866
983
|
w.opt(frame.details, (v) => w.str(v));
|
|
867
984
|
break;
|
|
868
985
|
}
|
|
869
|
-
case 'UNKNOWN':
|
|
870
|
-
|
|
871
|
-
frame.frameType < 0 ||
|
|
872
|
-
frame.frameType > 0xff ||
|
|
873
|
-
KNOWN_FRAME_TYPES.has(frame.frameType)) {
|
|
874
|
-
encodeInvalid(`UNKNOWN frame type ${frame.frameType} collides with a defined frame type`);
|
|
875
|
-
}
|
|
876
|
-
return { frameType: frame.frameType, payload: frame.payload };
|
|
877
|
-
}
|
|
986
|
+
case 'UNKNOWN':
|
|
987
|
+
return encodeUnknown(frame, w);
|
|
878
988
|
}
|
|
879
|
-
return
|
|
989
|
+
return FrameType[frame.type];
|
|
880
990
|
}
|
|
881
991
|
// ---------------------------------------------------------------------------
|
|
882
|
-
// Envelope
|
|
992
|
+
// Envelope. The kind-specific codecs let a peer bundle only the direction it
|
|
993
|
+
// speaks; `encodeMessage` and `decodeMessage` dispatch on the kind.
|
|
883
994
|
// ---------------------------------------------------------------------------
|
|
884
|
-
|
|
885
|
-
if (!isSupportedProtocolWireVersion(
|
|
886
|
-
encodeInvalid(`unsupported wireVersion ${
|
|
995
|
+
function encodeEnvelope(wireVersion, kindByte, frames, validate, encode) {
|
|
996
|
+
if (!isSupportedProtocolWireVersion(wireVersion)) {
|
|
997
|
+
encodeInvalid(`unsupported wireVersion ${wireVersion}`);
|
|
887
998
|
}
|
|
888
|
-
|
|
999
|
+
validate(frames);
|
|
889
1000
|
const w = new ByteWriter();
|
|
890
1001
|
w.raw(MAGIC_BYTES);
|
|
891
|
-
w.u16(
|
|
892
|
-
w.u8(
|
|
1002
|
+
w.u16(wireVersion);
|
|
1003
|
+
w.u8(kindByte);
|
|
893
1004
|
w.u8(0x00);
|
|
894
|
-
for (const frame of
|
|
895
|
-
const
|
|
1005
|
+
for (const frame of frames) {
|
|
1006
|
+
const p = new ByteWriter();
|
|
1007
|
+
const frameType = encode(frame, p);
|
|
1008
|
+
const payload = p.finish();
|
|
896
1009
|
w.u8(frameType);
|
|
897
1010
|
w.u32(payload.length);
|
|
898
1011
|
w.raw(payload);
|
|
@@ -901,6 +1014,17 @@ export function encodeMessage(message) {
|
|
|
901
1014
|
w.u32(0);
|
|
902
1015
|
return w.finish();
|
|
903
1016
|
}
|
|
1017
|
+
export function encodeRequest(message) {
|
|
1018
|
+
return encodeEnvelope(message.wireVersion, 0x01, message.frames, validateRequestSequence, encodeRequestFrame);
|
|
1019
|
+
}
|
|
1020
|
+
export function encodeResponse(message) {
|
|
1021
|
+
return encodeEnvelope(message.wireVersion, 0x02, message.frames, validateResponseSequence, encodeResponseFrame);
|
|
1022
|
+
}
|
|
1023
|
+
export function encodeMessage(message) {
|
|
1024
|
+
return message.msgKind === 'request'
|
|
1025
|
+
? encodeRequest(message)
|
|
1026
|
+
: encodeResponse(message);
|
|
1027
|
+
}
|
|
904
1028
|
function readFrames(r, decodeFrame_) {
|
|
905
1029
|
const frames = [];
|
|
906
1030
|
for (;;) {
|
|
@@ -922,7 +1046,7 @@ function readFrames(r, decodeFrame_) {
|
|
|
922
1046
|
frames.push(decodeFrame_(frameType, payload));
|
|
923
1047
|
}
|
|
924
1048
|
}
|
|
925
|
-
|
|
1049
|
+
function decodeEnvelope(bytes) {
|
|
926
1050
|
const r = new ByteReader(bytes);
|
|
927
1051
|
if (r.remaining < 8)
|
|
928
1052
|
invalid('truncated envelope header');
|
|
@@ -944,20 +1068,31 @@ export function decodeMessage(bytes) {
|
|
|
944
1068
|
const flags = r.u8();
|
|
945
1069
|
if (flags !== 0)
|
|
946
1070
|
invalid(`non-zero envelope flags 0x${flags.toString(16)}`);
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
};
|
|
961
|
-
|
|
962
|
-
|
|
1071
|
+
return { r, wireVersion, request: kindByte === 0x01 };
|
|
1072
|
+
}
|
|
1073
|
+
function requestBody(r, wireVersion) {
|
|
1074
|
+
const frames = readFrames(r, decodeRequestFrame);
|
|
1075
|
+
validateRequestSequence(frames);
|
|
1076
|
+
return { wireVersion, msgKind: 'request', frames };
|
|
1077
|
+
}
|
|
1078
|
+
function responseBody(r, wireVersion) {
|
|
1079
|
+
const frames = readFrames(r, decodeResponseFrame);
|
|
1080
|
+
validateResponseSequence(frames);
|
|
1081
|
+
return { wireVersion, msgKind: 'response', frames };
|
|
1082
|
+
}
|
|
1083
|
+
export function decodeRequest(bytes) {
|
|
1084
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1085
|
+
if (!request)
|
|
1086
|
+
invalid('expected a request message');
|
|
1087
|
+
return requestBody(r, wireVersion);
|
|
1088
|
+
}
|
|
1089
|
+
export function decodeResponse(bytes) {
|
|
1090
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1091
|
+
if (request)
|
|
1092
|
+
invalid('expected a response message');
|
|
1093
|
+
return responseBody(r, wireVersion);
|
|
1094
|
+
}
|
|
1095
|
+
export function decodeMessage(bytes) {
|
|
1096
|
+
const { r, wireVersion, request } = decodeEnvelope(bytes);
|
|
1097
|
+
return request ? requestBody(r, wireVersion) : responseBody(r, wireVersion);
|
|
963
1098
|
}
|