@syncular/core 0.59.0 → 0.60.1
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/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/frames.d.ts +6 -3
- package/dist/frames.js +10 -4
- package/dist/message.d.ts +54 -13
- package/dist/message.js +279 -127
- package/dist/render.js +34 -11
- package/package.json +1 -1
- package/src/canonical-json.ts +135 -6
- package/src/constants.ts +5 -5
- package/src/frames.ts +10 -4
- package/src/message.ts +389 -155
- package/src/render.ts +34 -11
package/dist/render.js
CHANGED
|
@@ -42,6 +42,13 @@ function renderScopeLists(scopes) {
|
|
|
42
42
|
function renderStringMap(map) {
|
|
43
43
|
return { ...map };
|
|
44
44
|
}
|
|
45
|
+
/** Digest fields render as lowercase hex (Conventions: hashes are hex). */
|
|
46
|
+
function renderDigest(digest) {
|
|
47
|
+
let hex = '';
|
|
48
|
+
for (const byte of digest)
|
|
49
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
50
|
+
return hex;
|
|
51
|
+
}
|
|
45
52
|
function renderOperation(operation) {
|
|
46
53
|
return {
|
|
47
54
|
table: operation.table,
|
|
@@ -83,9 +90,7 @@ function renderChange(change) {
|
|
|
83
90
|
tableIndex: change.tableIndex,
|
|
84
91
|
rowId: change.rowId,
|
|
85
92
|
op: change.op,
|
|
86
|
-
|
|
87
|
-
? { rowVersion: change.rowVersion }
|
|
88
|
-
: {}),
|
|
93
|
+
rowVersion: change.rowVersion,
|
|
89
94
|
scopes: renderStringMap(change.scopes),
|
|
90
95
|
...(change.row !== undefined ? { row: bytesToBase64(change.row) } : {}),
|
|
91
96
|
};
|
|
@@ -98,6 +103,7 @@ function renderFrame(frame) {
|
|
|
98
103
|
clientId: frame.clientId,
|
|
99
104
|
schemaVersion: frame.schemaVersion,
|
|
100
105
|
...(frame.logEpoch !== undefined ? { logEpoch: frame.logEpoch } : {}),
|
|
106
|
+
...(frame.outboxId !== undefined ? { outboxId: frame.outboxId } : {}),
|
|
101
107
|
};
|
|
102
108
|
case 'ROUND_FAILURE':
|
|
103
109
|
return { type: 'ROUND_FAILURE', code: frame.code };
|
|
@@ -105,6 +111,7 @@ function renderFrame(frame) {
|
|
|
105
111
|
return {
|
|
106
112
|
type: 'PUSH_COMMIT',
|
|
107
113
|
clientCommitId: frame.clientCommitId,
|
|
114
|
+
clientSeq: frame.clientSeq,
|
|
108
115
|
operations: frame.operations.map(renderOperation),
|
|
109
116
|
};
|
|
110
117
|
case 'PULL_HEADER':
|
|
@@ -114,16 +121,16 @@ function renderFrame(frame) {
|
|
|
114
121
|
limitSnapshotRows: frame.limitSnapshotRows,
|
|
115
122
|
maxSnapshotPages: frame.maxSnapshotPages,
|
|
116
123
|
accept: frame.accept,
|
|
124
|
+
cursor: frame.cursor,
|
|
125
|
+
registrationDigest: renderDigest(frame.registrationDigest),
|
|
126
|
+
echoDigest: renderDigest(frame.echoDigest),
|
|
117
127
|
};
|
|
118
|
-
case '
|
|
128
|
+
case 'UNIT':
|
|
129
|
+
return { type: 'UNIT', scopes: renderStringMap(frame.scopes) };
|
|
130
|
+
case 'PROGRESS':
|
|
119
131
|
return {
|
|
120
|
-
type: '
|
|
132
|
+
type: 'PROGRESS',
|
|
121
133
|
id: frame.id,
|
|
122
|
-
table: frame.table,
|
|
123
|
-
scopes: renderScopeLists(frame.scopes),
|
|
124
|
-
...(frame.params !== undefined
|
|
125
|
-
? { params: parseJson(frame.params) }
|
|
126
|
-
: {}),
|
|
127
134
|
cursor: frame.cursor,
|
|
128
135
|
...(frame.bootstrapState !== undefined
|
|
129
136
|
? { bootstrapState: parseJson(frame.bootstrapState) }
|
|
@@ -187,8 +194,24 @@ function renderFrame(frame) {
|
|
|
187
194
|
id: frame.id,
|
|
188
195
|
status: frame.status,
|
|
189
196
|
reasonCode: frame.reasonCode,
|
|
190
|
-
effectiveScopes: renderScopeLists(frame.effectiveScopes),
|
|
191
197
|
bootstrap: frame.bootstrap,
|
|
198
|
+
fenced: frame.fenced.map((record) => ({
|
|
199
|
+
table: record.table,
|
|
200
|
+
status: record.status,
|
|
201
|
+
scopes: renderScopeLists(record.scopes),
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
case 'LIVE_START':
|
|
205
|
+
return {
|
|
206
|
+
type: 'LIVE_START',
|
|
207
|
+
status: frame.status,
|
|
208
|
+
reasonCode: frame.reasonCode,
|
|
209
|
+
};
|
|
210
|
+
case 'LIVE_END':
|
|
211
|
+
return {
|
|
212
|
+
type: 'LIVE_END',
|
|
213
|
+
throughSeq: frame.throughSeq,
|
|
214
|
+
echoDigest: renderDigest(frame.echoDigest),
|
|
192
215
|
};
|
|
193
216
|
case 'COMMIT':
|
|
194
217
|
return {
|
package/package.json
CHANGED
package/src/canonical-json.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical JSON for digests (SPEC.md §11.2)
|
|
3
|
-
* implementations: UTF-8, no insignificant whitespace,
|
|
4
|
-
* by code-unit, scope value lists sorted and
|
|
2
|
+
* Canonical JSON for digests (SPEC.md §11.2) and the digests over it —
|
|
3
|
+
* contractual across implementations: UTF-8, no insignificant whitespace,
|
|
4
|
+
* object keys sorted by code-unit, scope value lists sorted and
|
|
5
|
+
* deduplicated, every other array in its stated order.
|
|
5
6
|
*
|
|
6
|
-
* Used for the
|
|
7
|
-
* (§
|
|
8
|
-
* canonical text.
|
|
7
|
+
* Used for the unit id and registration digest (§4.1), the echo digest
|
|
8
|
+
* (§4.4), the scope digest (§3.5) and the `X-Syncular-Scopes` header (§5.5).
|
|
9
9
|
*/
|
|
10
|
+
import { utf8Encode } from './bytes';
|
|
11
|
+
import type { FencedRecord } from './message';
|
|
12
|
+
|
|
13
|
+
/** A unit's scope map: one value per requested variable (§4.1). */
|
|
14
|
+
export type UnitScopes = Readonly<Record<string, string>>;
|
|
15
|
+
|
|
10
16
|
export function canonicalScopeJson(
|
|
11
17
|
scopes: Readonly<Record<string, readonly string[]>>,
|
|
12
18
|
): string {
|
|
@@ -18,3 +24,126 @@ export function canonicalScopeJson(
|
|
|
18
24
|
});
|
|
19
25
|
return `{${parts.join(',')}}`;
|
|
20
26
|
}
|
|
27
|
+
|
|
28
|
+
/** §4.1: the canonical JSON of a unit's scope map. */
|
|
29
|
+
export function canonicalUnitJson(scopes: UnitScopes): string {
|
|
30
|
+
const keys = Object.keys(scopes).sort();
|
|
31
|
+
const parts = keys.map(
|
|
32
|
+
(key) => `${JSON.stringify(key)}:${JSON.stringify(scopes[key])}`,
|
|
33
|
+
);
|
|
34
|
+
return `{${parts.join(',')}}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** §4.1: lowercase hex of the first 16 bytes of SHA-256 over the canonical
|
|
38
|
+
* JSON of the unit's scope map. */
|
|
39
|
+
export function unitId(scopes: UnitScopes): string {
|
|
40
|
+
let hex = '';
|
|
41
|
+
for (const byte of sha256(utf8Encode(canonicalUnitJson(scopes))).subarray(
|
|
42
|
+
0,
|
|
43
|
+
16,
|
|
44
|
+
)) {
|
|
45
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
46
|
+
}
|
|
47
|
+
return hex;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** §4.1: SHA-256 over the canonical JSON array of the units' scope maps in
|
|
51
|
+
* registration order, as 32 raw bytes. */
|
|
52
|
+
export function registrationDigest(units: readonly UnitScopes[]): Uint8Array {
|
|
53
|
+
return sha256(utf8Encode(`[${units.map(canonicalUnitJson).join(',')}]`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One unit of the effective list as the echo digest sees it (§4.4). */
|
|
57
|
+
export interface UnitEcho {
|
|
58
|
+
id: string;
|
|
59
|
+
status: 'active' | 'revoked';
|
|
60
|
+
fenced: readonly FencedRecord[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** §4.4: SHA-256 over the canonical JSON array, in registration order, of
|
|
64
|
+
* `{"fenced":[…],"id":…,"status":…}` per unit, as 32 raw bytes. */
|
|
65
|
+
export function echoDigest(units: readonly UnitEcho[]): Uint8Array {
|
|
66
|
+
const entries = units.map((unit) => {
|
|
67
|
+
const fenced = unit.fenced.map(
|
|
68
|
+
(record) =>
|
|
69
|
+
`{"scopes":${canonicalScopeJson(record.scopes)},"status":${JSON.stringify(record.status)},"table":${JSON.stringify(record.table)}}`,
|
|
70
|
+
);
|
|
71
|
+
return `{"fenced":[${fenced.join(',')}],"id":${JSON.stringify(unit.id)},"status":${JSON.stringify(unit.status)}}`;
|
|
72
|
+
});
|
|
73
|
+
return sha256(utf8Encode(`[${entries.join(',')}]`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// FIPS 180-4 SHA-256. Synchronous so the unit id is a plain function of the
|
|
77
|
+
// scope map on every runtime (WebCrypto digests are async only).
|
|
78
|
+
const K = new Uint32Array([
|
|
79
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
80
|
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
81
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
82
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
83
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
84
|
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
85
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
86
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
87
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
88
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
89
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
function sha256(data: Uint8Array): Uint8Array {
|
|
93
|
+
const padded = new Uint8Array(((data.length + 72) >>> 6) << 6);
|
|
94
|
+
padded.set(data);
|
|
95
|
+
padded[data.length] = 0x80;
|
|
96
|
+
const view = new DataView(padded.buffer);
|
|
97
|
+
view.setUint32(padded.length - 8, Math.floor(data.length / 0x20000000));
|
|
98
|
+
view.setUint32(padded.length - 4, (data.length * 8) >>> 0);
|
|
99
|
+
const h: [number, number, number, number, number, number, number, number] = [
|
|
100
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
101
|
+
0x1f83d9ab, 0x5be0cd19,
|
|
102
|
+
];
|
|
103
|
+
const w = new Uint32Array(64);
|
|
104
|
+
const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n));
|
|
105
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
106
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
107
|
+
for (let i = 16; i < 64; i++) {
|
|
108
|
+
const a = w[i - 15] as number;
|
|
109
|
+
const b = w[i - 2] as number;
|
|
110
|
+
w[i] =
|
|
111
|
+
(rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10)) +
|
|
112
|
+
(w[i - 7] as number) +
|
|
113
|
+
(rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3)) +
|
|
114
|
+
(w[i - 16] as number);
|
|
115
|
+
}
|
|
116
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
117
|
+
for (let i = 0; i < 64; i++) {
|
|
118
|
+
const t1 =
|
|
119
|
+
hh +
|
|
120
|
+
(rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) +
|
|
121
|
+
((e & f) ^ (~e & g)) +
|
|
122
|
+
(K[i] as number) +
|
|
123
|
+
(w[i] as number);
|
|
124
|
+
const t2 =
|
|
125
|
+
(rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) +
|
|
126
|
+
((a & b) ^ (a & c) ^ (b & c));
|
|
127
|
+
hh = g;
|
|
128
|
+
g = f;
|
|
129
|
+
f = e;
|
|
130
|
+
e = (d + t1) | 0;
|
|
131
|
+
d = c;
|
|
132
|
+
c = b;
|
|
133
|
+
b = a;
|
|
134
|
+
a = (t1 + t2) | 0;
|
|
135
|
+
}
|
|
136
|
+
h[0] = (h[0] + a) | 0;
|
|
137
|
+
h[1] = (h[1] + b) | 0;
|
|
138
|
+
h[2] = (h[2] + c) | 0;
|
|
139
|
+
h[3] = (h[3] + d) | 0;
|
|
140
|
+
h[4] = (h[4] + e) | 0;
|
|
141
|
+
h[5] = (h[5] + f) | 0;
|
|
142
|
+
h[6] = (h[6] + g) | 0;
|
|
143
|
+
h[7] = (h[7] + hh) | 0;
|
|
144
|
+
}
|
|
145
|
+
const out = new Uint8Array(32);
|
|
146
|
+
const outView = new DataView(out.buffer);
|
|
147
|
+
h.forEach((word, i) => outView.setUint32(i * 4, word));
|
|
148
|
+
return out;
|
|
149
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** Protocol constants (SPEC.md §1.2, §9 are normative). */
|
|
2
2
|
|
|
3
3
|
/** Latest SSP2 envelope wire version emitted by current clients. */
|
|
4
|
-
export const PROTOCOL_WIRE_VERSION =
|
|
4
|
+
export const PROTOCOL_WIRE_VERSION = 4;
|
|
5
5
|
|
|
6
|
-
/** Oldest SSP2 envelope wire version accepted. Wire version
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
export const MINIMUM_PROTOCOL_WIRE_VERSION =
|
|
6
|
+
/** Oldest SSP2 envelope wire version accepted. Wire version 4 replaced
|
|
7
|
+
* per-table subscriptions with units and the replica cursor (§4); earlier
|
|
8
|
+
* versions are rejected as unknown (§9). */
|
|
9
|
+
export const MINIMUM_PROTOCOL_WIRE_VERSION = 4;
|
|
10
10
|
|
|
11
11
|
/** Wire versions implemented by the reference codec, oldest first. */
|
|
12
12
|
export const SUPPORTED_PROTOCOL_WIRE_VERSIONS: readonly number[] =
|
package/src/frames.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
/** Frame type registry for wire
|
|
1
|
+
/** Frame type registry for wire version 4 (SPEC.md §1.2). */
|
|
2
2
|
export const FrameType = {
|
|
3
3
|
END: 0x00,
|
|
4
4
|
REQ_HEADER: 0x01,
|
|
5
5
|
PUSH_COMMIT: 0x02,
|
|
6
6
|
PULL_HEADER: 0x03,
|
|
7
|
-
|
|
7
|
+
UNIT: 0x04,
|
|
8
8
|
ROUND_FAILURE: 0x05,
|
|
9
|
+
PROGRESS: 0x06,
|
|
9
10
|
RESP_HEADER: 0x10,
|
|
10
11
|
PUSH_RESULT: 0x11,
|
|
11
12
|
SUB_START: 0x12,
|
|
@@ -15,6 +16,8 @@ export const FrameType = {
|
|
|
15
16
|
SUB_END: 0x16,
|
|
16
17
|
LEASE: 0x19,
|
|
17
18
|
PUSH_RESULT_DETAILS: 0x1b,
|
|
19
|
+
LIVE_START: 0x1c,
|
|
20
|
+
LIVE_END: 0x1d,
|
|
18
21
|
ERROR: 0x1f,
|
|
19
22
|
} as const;
|
|
20
23
|
|
|
@@ -25,8 +28,9 @@ export const REQUEST_FRAME_TYPES: ReadonlySet<number> = new Set([
|
|
|
25
28
|
FrameType.REQ_HEADER,
|
|
26
29
|
FrameType.PUSH_COMMIT,
|
|
27
30
|
FrameType.PULL_HEADER,
|
|
28
|
-
FrameType.
|
|
31
|
+
FrameType.UNIT,
|
|
29
32
|
FrameType.ROUND_FAILURE,
|
|
33
|
+
FrameType.PROGRESS,
|
|
30
34
|
]);
|
|
31
35
|
|
|
32
36
|
/** Known response frame types (excluding END). */
|
|
@@ -40,10 +44,12 @@ export const RESPONSE_FRAME_TYPES: ReadonlySet<number> = new Set([
|
|
|
40
44
|
FrameType.SUB_END,
|
|
41
45
|
FrameType.LEASE,
|
|
42
46
|
FrameType.PUSH_RESULT_DETAILS,
|
|
47
|
+
FrameType.LIVE_START,
|
|
48
|
+
FrameType.LIVE_END,
|
|
43
49
|
FrameType.ERROR,
|
|
44
50
|
]);
|
|
45
51
|
|
|
46
|
-
/** Every frame type with a defined layout in wire
|
|
52
|
+
/** Every frame type with a defined layout in wire version 4. */
|
|
47
53
|
export const KNOWN_FRAME_TYPES: ReadonlySet<number> = new Set([
|
|
48
54
|
FrameType.END,
|
|
49
55
|
...REQUEST_FRAME_TYPES,
|