@syncular/core 0.1.3 → 0.2.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/README.md +5 -19
- package/dist/blob-ref.d.ts +21 -0
- package/dist/blob-ref.js +97 -0
- package/dist/bytes.d.ts +45 -0
- package/dist/bytes.js +242 -0
- package/dist/canonical-json.d.ts +10 -0
- package/dist/canonical-json.js +18 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/crypto.d.ts +60 -0
- package/dist/crypto.js +251 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/frames.d.ts +24 -0
- package/dist/frames.js +42 -0
- package/dist/index.d.ts +18 -26
- package/dist/index.js +18 -43
- package/dist/message.d.ts +157 -0
- package/dist/message.js +786 -0
- package/dist/realtime.d.ts +92 -0
- package/dist/realtime.js +222 -0
- package/dist/render.d.ts +22 -0
- package/dist/render.js +273 -0
- package/dist/row-codec.d.ts +39 -0
- package/dist/row-codec.js +176 -0
- package/dist/segment.d.ts +18 -0
- package/dist/segment.js +105 -0
- package/dist/stream.d.ts +20 -0
- package/dist/stream.js +102 -0
- package/package.json +7 -59
- package/src/blob-ref.ts +135 -0
- package/src/bytes.ts +301 -0
- package/src/canonical-json.ts +20 -0
- package/src/constants.ts +7 -0
- package/src/crypto.ts +335 -0
- package/src/errors.ts +18 -0
- package/src/frames.ts +47 -0
- package/src/index.ts +18 -43
- package/src/message.ts +1024 -0
- package/src/realtime.ts +340 -0
- package/src/render.ts +325 -0
- package/src/row-codec.ts +265 -0
- package/src/segment.ts +150 -0
- package/src/stream.ts +128 -0
- package/dist/blobs.d.ts +0 -153
- package/dist/blobs.d.ts.map +0 -1
- package/dist/blobs.js +0 -47
- package/dist/blobs.js.map +0 -1
- package/dist/column-codecs.d.ts +0 -59
- package/dist/column-codecs.d.ts.map +0 -1
- package/dist/column-codecs.js +0 -159
- package/dist/column-codecs.js.map +0 -1
- package/dist/conflict.d.ts +0 -22
- package/dist/conflict.d.ts.map +0 -1
- package/dist/conflict.js +0 -81
- package/dist/conflict.js.map +0 -1
- package/dist/database.d.ts +0 -7
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js +0 -14
- package/dist/database.js.map +0 -1
- package/dist/dialect.d.ts +0 -7
- package/dist/dialect.d.ts.map +0 -1
- package/dist/dialect.js +0 -2
- package/dist/dialect.js.map +0 -1
- package/dist/error-responses.d.ts +0 -393
- package/dist/error-responses.d.ts.map +0 -1
- package/dist/error-responses.js +0 -393
- package/dist/error-responses.js.map +0 -1
- package/dist/http/api-client.d.ts +0 -11
- package/dist/http/api-client.d.ts.map +0 -1
- package/dist/http/api-client.js +0 -30
- package/dist/http/api-client.js.map +0 -1
- package/dist/http/blob.d.ts +0 -6
- package/dist/http/blob.d.ts.map +0 -1
- package/dist/http/blob.js +0 -46
- package/dist/http/blob.js.map +0 -1
- package/dist/http/generated/api.d.ts +0 -5241
- package/dist/http/generated/api.d.ts.map +0 -1
- package/dist/http/generated/api.js +0 -6
- package/dist/http/generated/api.js.map +0 -1
- package/dist/http/index.d.ts +0 -18
- package/dist/http/index.d.ts.map +0 -1
- package/dist/http/index.js +0 -212
- package/dist/http/index.js.map +0 -1
- package/dist/http/shared.d.ts +0 -45
- package/dist/http/shared.d.ts.map +0 -1
- package/dist/http/shared.js +0 -122
- package/dist/http/shared.js.map +0 -1
- package/dist/http/transport-client.d.ts +0 -31
- package/dist/http/transport-client.d.ts.map +0 -1
- package/dist/http/transport-client.js +0 -193
- package/dist/http/transport-client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/kysely-column-codecs.d.ts +0 -31
- package/dist/kysely-column-codecs.d.ts.map +0 -1
- package/dist/kysely-column-codecs.js +0 -452
- package/dist/kysely-column-codecs.js.map +0 -1
- package/dist/logger.d.ts +0 -29
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -26
- package/dist/logger.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -54
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -7
- package/dist/proxy/index.js.map +0 -1
- package/dist/schemas/blobs.d.ts +0 -82
- package/dist/schemas/blobs.d.ts.map +0 -1
- package/dist/schemas/blobs.js +0 -64
- package/dist/schemas/blobs.js.map +0 -1
- package/dist/schemas/common.d.ts +0 -100
- package/dist/schemas/common.d.ts.map +0 -1
- package/dist/schemas/common.js +0 -64
- package/dist/schemas/common.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -7
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -7
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/sync.d.ts +0 -1065
- package/dist/schemas/sync.d.ts.map +0 -1
- package/dist/schemas/sync.js +0 -361
- package/dist/schemas/sync.js.map +0 -1
- package/dist/scopes/index.d.ts +0 -113
- package/dist/scopes/index.d.ts.map +0 -1
- package/dist/scopes/index.js +0 -103
- package/dist/scopes/index.js.map +0 -1
- package/dist/sentry.d.ts +0 -31
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js +0 -147
- package/dist/sentry.js.map +0 -1
- package/dist/snapshot-chunks.d.ts +0 -110
- package/dist/snapshot-chunks.d.ts.map +0 -1
- package/dist/snapshot-chunks.js +0 -682
- package/dist/snapshot-chunks.js.map +0 -1
- package/dist/sync-packs.d.ts +0 -33
- package/dist/sync-packs.d.ts.map +0 -1
- package/dist/sync-packs.js +0 -821
- package/dist/sync-packs.js.map +0 -1
- package/dist/telemetry.d.ts +0 -114
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -114
- package/dist/telemetry.js.map +0 -1
- package/dist/transforms.d.ts +0 -146
- package/dist/transforms.d.ts.map +0 -1
- package/dist/transforms.js +0 -155
- package/dist/transforms.js.map +0 -1
- package/dist/types.d.ts +0 -210
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -20
- package/dist/types.js.map +0 -1
- package/dist/utils/bytes.d.ts +0 -4
- package/dist/utils/bytes.d.ts.map +0 -1
- package/dist/utils/bytes.js +0 -66
- package/dist/utils/bytes.js.map +0 -1
- package/dist/utils/compression.d.ts +0 -22
- package/dist/utils/compression.d.ts.map +0 -1
- package/dist/utils/compression.js +0 -129
- package/dist/utils/compression.js.map +0 -1
- package/dist/utils/crypto.d.ts +0 -13
- package/dist/utils/crypto.d.ts.map +0 -1
- package/dist/utils/crypto.js +0 -107
- package/dist/utils/crypto.js.map +0 -1
- package/dist/utils/id.d.ts +0 -3
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -25
- package/dist/utils/id.js.map +0 -1
- package/dist/utils/index.d.ts +0 -10
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -10
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/internal-runtime.d.ts +0 -16
- package/dist/utils/internal-runtime.d.ts.map +0 -1
- package/dist/utils/internal-runtime.js +0 -14
- package/dist/utils/internal-runtime.js.map +0 -1
- package/dist/utils/object.d.ts +0 -2
- package/dist/utils/object.d.ts.map +0 -1
- package/dist/utils/object.js +0 -4
- package/dist/utils/object.js.map +0 -1
- package/dist/utils/realtime-change-index.d.ts +0 -9
- package/dist/utils/realtime-change-index.d.ts.map +0 -1
- package/dist/utils/realtime-change-index.js +0 -36
- package/dist/utils/realtime-change-index.js.map +0 -1
- package/dist/utils/realtime-connection-registry.d.ts +0 -40
- package/dist/utils/realtime-connection-registry.d.ts.map +0 -1
- package/dist/utils/realtime-connection-registry.js +0 -237
- package/dist/utils/realtime-connection-registry.js.map +0 -1
- package/dist/utils/table-registry.d.ts +0 -10
- package/dist/utils/table-registry.d.ts.map +0 -1
- package/dist/utils/table-registry.js +0 -54
- package/dist/utils/table-registry.js.map +0 -1
- package/dist/utils/url.d.ts +0 -3
- package/dist/utils/url.d.ts.map +0 -1
- package/dist/utils/url.js +0 -20
- package/dist/utils/url.js.map +0 -1
- package/src/blobs.ts +0 -219
- package/src/column-codecs.ts +0 -262
- package/src/conflict.ts +0 -92
- package/src/database.ts +0 -21
- package/src/dialect.ts +0 -8
- package/src/error-responses.ts +0 -423
- package/src/http/api-client.ts +0 -39
- package/src/http/blob.ts +0 -95
- package/src/http/generated/api.ts +0 -5241
- package/src/http/index.ts +0 -365
- package/src/http/shared.ts +0 -220
- package/src/http/transport-client.ts +0 -314
- package/src/kysely-column-codecs.ts +0 -585
- package/src/logger.ts +0 -38
- package/src/proxy/index.ts +0 -57
- package/src/schemas/blobs.ts +0 -110
- package/src/schemas/common.ts +0 -89
- package/src/schemas/index.ts +0 -7
- package/src/schemas/sync.ts +0 -517
- package/src/scopes/index.ts +0 -237
- package/src/sentry.ts +0 -200
- package/src/snapshot-chunks.ts +0 -964
- package/src/sync-packs.ts +0 -1096
- package/src/telemetry.ts +0 -243
- package/src/transforms.ts +0 -256
- package/src/types.ts +0 -267
- package/src/utils/bytes.ts +0 -75
- package/src/utils/compression.ts +0 -187
- package/src/utils/crypto.ts +0 -146
- package/src/utils/id.ts +0 -34
- package/src/utils/index.ts +0 -9
- package/src/utils/internal-runtime.ts +0 -39
- package/src/utils/object.ts +0 -3
- package/src/utils/realtime-change-index.ts +0 -44
- package/src/utils/realtime-connection-registry.ts +0 -286
- package/src/utils/table-registry.ts +0 -67
- package/src/utils/url.ts +0 -29
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** §8.7 channel tag: a standalone SSP2 response message — a delta. */
|
|
2
|
+
export declare const REALTIME_TAG_DELTA = 0;
|
|
3
|
+
/** §8.7 channel tag: one chunk of the in-flight sync round's byte
|
|
4
|
+
* stream (request client→server, response server→client). */
|
|
5
|
+
export declare const REALTIME_TAG_ROUND = 1;
|
|
6
|
+
export interface RealtimeHelloEvent {
|
|
7
|
+
event: 'hello';
|
|
8
|
+
data: {
|
|
9
|
+
protocolVersion: number;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
actorId: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
cursor: number;
|
|
14
|
+
latestCursor: number;
|
|
15
|
+
requiresSync: boolean;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export type WakeReason = 'delta-too-large' | 'catchup-required' | 'reset-required';
|
|
20
|
+
export interface RealtimeSyncEvent {
|
|
21
|
+
event: 'sync';
|
|
22
|
+
data: {
|
|
23
|
+
cursor: number;
|
|
24
|
+
requiresPull: true;
|
|
25
|
+
reason: WakeReason;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface RealtimeHeartbeatEvent {
|
|
30
|
+
event: 'heartbeat';
|
|
31
|
+
data: {
|
|
32
|
+
timestamp: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** §8.6.2 presence fanout kind — a closed set of three. */
|
|
36
|
+
export type PresenceKind = 'join' | 'update' | 'leave';
|
|
37
|
+
/**
|
|
38
|
+
* Server → client presence fanout (§8.6.2): a scope-mate's presence for a
|
|
39
|
+
* key the receiver holds changed. `doc` is the peer's document for
|
|
40
|
+
* `join`/`update` and `null` for `leave`. An `error` variant carries a
|
|
41
|
+
* client-runtime `presence.*` code back to the publisher (size cap /
|
|
42
|
+
* forbidden, §8.6.2); it never fans out to peers.
|
|
43
|
+
*/
|
|
44
|
+
export interface RealtimePresenceEvent {
|
|
45
|
+
event: 'presence';
|
|
46
|
+
data: {
|
|
47
|
+
scopeKey: string;
|
|
48
|
+
kind?: PresenceKind;
|
|
49
|
+
actorId?: string;
|
|
50
|
+
clientId?: string;
|
|
51
|
+
doc?: Record<string, unknown> | null;
|
|
52
|
+
error?: string;
|
|
53
|
+
timestamp?: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export type RealtimeServerEvent = RealtimeHelloEvent | RealtimeSyncEvent | RealtimeHeartbeatEvent | RealtimePresenceEvent;
|
|
57
|
+
/** Client → server delta acknowledgement (§8.2). */
|
|
58
|
+
export interface RealtimeAck {
|
|
59
|
+
type: 'ack';
|
|
60
|
+
cursor: number;
|
|
61
|
+
}
|
|
62
|
+
/** Client → server presence publish/leave (§8.6.2). `doc: null` = leave. */
|
|
63
|
+
export interface RealtimePresencePublish {
|
|
64
|
+
event: 'presence';
|
|
65
|
+
data: {
|
|
66
|
+
scopeKey: string;
|
|
67
|
+
doc: Record<string, unknown> | null;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export type ParsedRealtimeEvent = {
|
|
71
|
+
known: true;
|
|
72
|
+
event: RealtimeServerEvent;
|
|
73
|
+
} | {
|
|
74
|
+
known: false;
|
|
75
|
+
eventName: string;
|
|
76
|
+
};
|
|
77
|
+
/** Serialize a client→server presence publish/leave (§8.6.2). */
|
|
78
|
+
export declare function encodePresencePublish(scopeKey: string, doc: Record<string, unknown> | null): string;
|
|
79
|
+
/** Serialize a server→client presence fanout event (§8.6.2). */
|
|
80
|
+
export declare function encodePresenceFanout(scopeKey: string, kind: PresenceKind, actorId: string, clientId: string, doc: Record<string, unknown> | null, timestamp: number): string;
|
|
81
|
+
/** Serialize a server→client presence error directed at the publisher
|
|
82
|
+
* (§8.6.2 `presence.too_large` / `presence.forbidden`). */
|
|
83
|
+
export declare function encodePresenceError(scopeKey: string, error: string, timestamp: number): string;
|
|
84
|
+
export declare function parseRealtimeServerEvent(text: string): ParsedRealtimeEvent;
|
|
85
|
+
/**
|
|
86
|
+
* Parse a client → server presence publish/leave (§8.6.2). Used by the
|
|
87
|
+
* server to validate inbound `presence` control messages. A malformed
|
|
88
|
+
* message (missing/empty `scopeKey`, or a `doc` that is neither an object
|
|
89
|
+
* nor `null`) throws a `DecodeError` — a known event with wrong-shape data
|
|
90
|
+
* is never a tolerated variant (§8.1).
|
|
91
|
+
*/
|
|
92
|
+
export declare function parseRealtimePresencePublish(text: string): RealtimePresencePublish;
|
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime JSON control messages and channel tags (SPEC.md §8).
|
|
3
|
+
*
|
|
4
|
+
* Control messages are JSON text frames; binary frames carry a one-byte
|
|
5
|
+
* channel tag (§8.7) followed by either a complete SSP2 response message
|
|
6
|
+
* (deltas) or a chunk of an in-flight sync round's byte stream, handled
|
|
7
|
+
* by the message codec / stream scanner. Per §8.1 a client MUST tolerate
|
|
8
|
+
* and ignore unknown JSON control events, so parsing returns
|
|
9
|
+
* `known: false` for unrecognized event names instead of failing.
|
|
10
|
+
*/
|
|
11
|
+
import { DecodeError } from './errors.js';
|
|
12
|
+
/** §8.7 channel tag: a standalone SSP2 response message — a delta. */
|
|
13
|
+
export const REALTIME_TAG_DELTA = 0x00;
|
|
14
|
+
/** §8.7 channel tag: one chunk of the in-flight sync round's byte
|
|
15
|
+
* stream (request client→server, response server→client). */
|
|
16
|
+
export const REALTIME_TAG_ROUND = 0x01;
|
|
17
|
+
const PRESENCE_KINDS = ['join', 'update', 'leave'];
|
|
18
|
+
/** Serialize a client→server presence publish/leave (§8.6.2). */
|
|
19
|
+
export function encodePresencePublish(scopeKey, doc) {
|
|
20
|
+
return JSON.stringify({ event: 'presence', data: { scopeKey, doc } });
|
|
21
|
+
}
|
|
22
|
+
/** Serialize a server→client presence fanout event (§8.6.2). */
|
|
23
|
+
export function encodePresenceFanout(scopeKey, kind, actorId, clientId, doc, timestamp) {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
event: 'presence',
|
|
26
|
+
data: { scopeKey, kind, actorId, clientId, doc, timestamp },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Serialize a server→client presence error directed at the publisher
|
|
30
|
+
* (§8.6.2 `presence.too_large` / `presence.forbidden`). */
|
|
31
|
+
export function encodePresenceError(scopeKey, error, timestamp) {
|
|
32
|
+
return JSON.stringify({
|
|
33
|
+
event: 'presence',
|
|
34
|
+
data: { scopeKey, error, timestamp },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function malformed(what) {
|
|
38
|
+
throw new DecodeError('sync.invalid_request', `malformed realtime control message: ${what}`);
|
|
39
|
+
}
|
|
40
|
+
function requireObject(value, what) {
|
|
41
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
42
|
+
malformed(`${what} must be a JSON object`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function requireNumber(value, what) {
|
|
47
|
+
// Realtime numeric fields are integers within the ±(2^53−1) i64 contract
|
|
48
|
+
// (SPEC.md §8.1); fractional or non-finite numbers are malformed.
|
|
49
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
|
|
50
|
+
malformed(`${what} must be an integer within the i64 safe range`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function requireString(value, what) {
|
|
55
|
+
if (typeof value !== 'string')
|
|
56
|
+
malformed(`${what} must be a string`);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function requireBoolean(value, what) {
|
|
60
|
+
if (typeof value !== 'boolean')
|
|
61
|
+
malformed(`${what} must be a boolean`);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const WAKE_REASONS = [
|
|
65
|
+
'delta-too-large',
|
|
66
|
+
'catchup-required',
|
|
67
|
+
'reset-required',
|
|
68
|
+
];
|
|
69
|
+
export function parseRealtimeServerEvent(text) {
|
|
70
|
+
let value;
|
|
71
|
+
try {
|
|
72
|
+
value = JSON.parse(text);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
malformed('not valid JSON');
|
|
76
|
+
}
|
|
77
|
+
const root = requireObject(value, 'control message');
|
|
78
|
+
const event = requireString(root.event, 'event');
|
|
79
|
+
if (event === 'hello') {
|
|
80
|
+
const data = requireObject(root.data, 'hello.data');
|
|
81
|
+
return {
|
|
82
|
+
known: true,
|
|
83
|
+
event: {
|
|
84
|
+
event: 'hello',
|
|
85
|
+
data: {
|
|
86
|
+
protocolVersion: requireNumber(data.protocolVersion, 'hello.data.protocolVersion'),
|
|
87
|
+
sessionId: requireString(data.sessionId, 'hello.data.sessionId'),
|
|
88
|
+
actorId: requireString(data.actorId, 'hello.data.actorId'),
|
|
89
|
+
clientId: requireString(data.clientId, 'hello.data.clientId'),
|
|
90
|
+
cursor: requireNumber(data.cursor, 'hello.data.cursor'),
|
|
91
|
+
latestCursor: requireNumber(data.latestCursor, 'hello.data.latestCursor'),
|
|
92
|
+
requiresSync: requireBoolean(data.requiresSync, 'hello.data.requiresSync'),
|
|
93
|
+
timestamp: requireNumber(data.timestamp, 'hello.data.timestamp'),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (event === 'sync') {
|
|
99
|
+
const data = requireObject(root.data, 'sync.data');
|
|
100
|
+
const reason = requireString(data.reason, 'sync.data.reason');
|
|
101
|
+
if (!WAKE_REASONS.includes(reason)) {
|
|
102
|
+
malformed(`unknown wake reason ${JSON.stringify(reason)}`);
|
|
103
|
+
}
|
|
104
|
+
if (data.requiresPull !== true) {
|
|
105
|
+
malformed('sync.data.requiresPull must be true');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
known: true,
|
|
109
|
+
event: {
|
|
110
|
+
event: 'sync',
|
|
111
|
+
data: {
|
|
112
|
+
cursor: requireNumber(data.cursor, 'sync.data.cursor'),
|
|
113
|
+
requiresPull: true,
|
|
114
|
+
reason: reason,
|
|
115
|
+
timestamp: requireNumber(data.timestamp, 'sync.data.timestamp'),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (event === 'heartbeat') {
|
|
121
|
+
const data = requireObject(root.data, 'heartbeat.data');
|
|
122
|
+
return {
|
|
123
|
+
known: true,
|
|
124
|
+
event: {
|
|
125
|
+
event: 'heartbeat',
|
|
126
|
+
data: {
|
|
127
|
+
timestamp: requireNumber(data.timestamp, 'heartbeat.data.timestamp'),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (event === 'presence') {
|
|
133
|
+
// §8.6.2: validate the presence shape but carry `data` through
|
|
134
|
+
// verbatim so re-encoding is byte-identical (host-defined `doc`). A
|
|
135
|
+
// `presence` message is recognized in both directions: a fanout (has
|
|
136
|
+
// `kind`), a publisher-directed error (has `error`), or a client→server
|
|
137
|
+
// publish (neither — just `scopeKey`/`doc`, §8.6.2).
|
|
138
|
+
const data = requireObject(root.data, 'presence.data');
|
|
139
|
+
validatePresenceScopeKey(data.scopeKey);
|
|
140
|
+
if ('error' in data) {
|
|
141
|
+
// The publisher-directed error variant (`presence.too_large` /
|
|
142
|
+
// `presence.forbidden`, §8.6.2) — a string code, no `kind`/`doc`.
|
|
143
|
+
requireString(data.error, 'presence.data.error');
|
|
144
|
+
}
|
|
145
|
+
else if ('kind' in data) {
|
|
146
|
+
validatePresenceKind(data.kind);
|
|
147
|
+
requireString(data.actorId, 'presence.data.actorId');
|
|
148
|
+
requireString(data.clientId, 'presence.data.clientId');
|
|
149
|
+
validatePresenceFanoutDoc(data.kind, data.doc);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Client→server publish/leave: scopeKey + doc (object or null).
|
|
153
|
+
if (data.doc !== null)
|
|
154
|
+
requirePresenceDocObject(data.doc);
|
|
155
|
+
}
|
|
156
|
+
if (data.timestamp !== undefined) {
|
|
157
|
+
requireNumber(data.timestamp, 'presence.data.timestamp');
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
known: true,
|
|
161
|
+
event: { event: 'presence', data: data },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { known: false, eventName: event };
|
|
165
|
+
}
|
|
166
|
+
function validatePresenceScopeKey(value) {
|
|
167
|
+
const key = requireString(value, 'presence.data.scopeKey');
|
|
168
|
+
if (key.length === 0)
|
|
169
|
+
malformed('presence.data.scopeKey must be non-empty');
|
|
170
|
+
return key;
|
|
171
|
+
}
|
|
172
|
+
function validatePresenceKind(value) {
|
|
173
|
+
const kind = requireString(value, 'presence.data.kind');
|
|
174
|
+
if (!PRESENCE_KINDS.includes(kind)) {
|
|
175
|
+
malformed(`unknown presence kind ${JSON.stringify(kind)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** §8.6.2 `doc`-present-iff-not-`leave`: a leave carries null, a
|
|
179
|
+
* join/update carries an object. */
|
|
180
|
+
function validatePresenceFanoutDoc(kind, doc) {
|
|
181
|
+
if (kind === 'leave') {
|
|
182
|
+
if (doc !== null && doc !== undefined) {
|
|
183
|
+
malformed('presence leave must carry doc: null');
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
requirePresenceDocObject(doc);
|
|
188
|
+
}
|
|
189
|
+
function requirePresenceDocObject(value) {
|
|
190
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
191
|
+
malformed('presence.data.doc must be a JSON object');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Parse a client → server presence publish/leave (§8.6.2). Used by the
|
|
196
|
+
* server to validate inbound `presence` control messages. A malformed
|
|
197
|
+
* message (missing/empty `scopeKey`, or a `doc` that is neither an object
|
|
198
|
+
* nor `null`) throws a `DecodeError` — a known event with wrong-shape data
|
|
199
|
+
* is never a tolerated variant (§8.1).
|
|
200
|
+
*/
|
|
201
|
+
export function parseRealtimePresencePublish(text) {
|
|
202
|
+
let value;
|
|
203
|
+
try {
|
|
204
|
+
value = JSON.parse(text);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
malformed('not valid JSON');
|
|
208
|
+
}
|
|
209
|
+
const root = requireObject(value, 'control message');
|
|
210
|
+
if (root.event !== 'presence') {
|
|
211
|
+
malformed('not a presence control message');
|
|
212
|
+
}
|
|
213
|
+
const data = requireObject(root.data, 'presence.data');
|
|
214
|
+
const scopeKey = validatePresenceScopeKey(data.scopeKey);
|
|
215
|
+
const doc = data.doc;
|
|
216
|
+
if (doc !== null)
|
|
217
|
+
requirePresenceDocObject(doc);
|
|
218
|
+
return {
|
|
219
|
+
event: 'presence',
|
|
220
|
+
data: { scopeKey, doc: doc },
|
|
221
|
+
};
|
|
222
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON debug rendering (SPEC.md §11).
|
|
3
|
+
*
|
|
4
|
+
* Non-contractual for the wire; contractual for golden vectors. Rendering
|
|
5
|
+
* rules: frames in wire order with END omitted, field order per the SPEC
|
|
6
|
+
* field tables, absent opt() fields omitted, binary fields as standard
|
|
7
|
+
* base64, enums by their spec names, i64 as JSON numbers, json-typed
|
|
8
|
+
* fields embedded as parsed JSON.
|
|
9
|
+
*/
|
|
10
|
+
import type { SyncMessage } from './message.js';
|
|
11
|
+
import type { RowsSegment } from './segment.js';
|
|
12
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
13
|
+
[key: string]: JsonValue;
|
|
14
|
+
};
|
|
15
|
+
/** Standard base64 (§11.1 rule 4). Dependency- and runtime-agnostic. */
|
|
16
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
17
|
+
export declare function renderMessageValue(message: SyncMessage): JsonValue;
|
|
18
|
+
/** `render(bytes) → json` for SSP2 messages (§11). */
|
|
19
|
+
export declare function renderMessage(bytes: Uint8Array): JsonValue;
|
|
20
|
+
export declare function renderRowsSegmentValue(segment: RowsSegment): JsonValue;
|
|
21
|
+
/** `render(bytes) → json` for standalone SSG2 rows segments (§11 rule 8). */
|
|
22
|
+
export declare function renderRowsSegment(bytes: Uint8Array): JsonValue;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { decodeMessage } from './message.js';
|
|
2
|
+
import { decodeRowsSegment, ROWS_SEGMENT_FORMAT_VERSION } from './segment.js';
|
|
3
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
4
|
+
/** Standard base64 (§11.1 rule 4). Dependency- and runtime-agnostic. */
|
|
5
|
+
export function bytesToBase64(bytes) {
|
|
6
|
+
let out = '';
|
|
7
|
+
let i = 0;
|
|
8
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
9
|
+
const chunk = ((bytes[i] ?? 0) << 16) |
|
|
10
|
+
((bytes[i + 1] ?? 0) << 8) |
|
|
11
|
+
(bytes[i + 2] ?? 0);
|
|
12
|
+
out +=
|
|
13
|
+
BASE64_ALPHABET.charAt((chunk >> 18) & 63) +
|
|
14
|
+
BASE64_ALPHABET.charAt((chunk >> 12) & 63) +
|
|
15
|
+
BASE64_ALPHABET.charAt((chunk >> 6) & 63) +
|
|
16
|
+
BASE64_ALPHABET.charAt(chunk & 63);
|
|
17
|
+
}
|
|
18
|
+
const rest = bytes.length - i;
|
|
19
|
+
if (rest === 1) {
|
|
20
|
+
const chunk = (bytes[i] ?? 0) << 16;
|
|
21
|
+
out +=
|
|
22
|
+
BASE64_ALPHABET.charAt((chunk >> 18) & 63) +
|
|
23
|
+
BASE64_ALPHABET.charAt((chunk >> 12) & 63) +
|
|
24
|
+
'==';
|
|
25
|
+
}
|
|
26
|
+
else if (rest === 2) {
|
|
27
|
+
const chunk = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8);
|
|
28
|
+
out +=
|
|
29
|
+
BASE64_ALPHABET.charAt((chunk >> 18) & 63) +
|
|
30
|
+
BASE64_ALPHABET.charAt((chunk >> 12) & 63) +
|
|
31
|
+
BASE64_ALPHABET.charAt((chunk >> 6) & 63) +
|
|
32
|
+
'=';
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function parseJson(raw) {
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
function renderScopeLists(scopes) {
|
|
40
|
+
return Object.fromEntries(Object.entries(scopes).map(([key, values]) => [key, [...values]]));
|
|
41
|
+
}
|
|
42
|
+
function renderStringMap(map) {
|
|
43
|
+
return { ...map };
|
|
44
|
+
}
|
|
45
|
+
function renderOperation(operation) {
|
|
46
|
+
return {
|
|
47
|
+
table: operation.table,
|
|
48
|
+
rowId: operation.rowId,
|
|
49
|
+
op: operation.op,
|
|
50
|
+
...(operation.baseVersion !== undefined
|
|
51
|
+
? { baseVersion: operation.baseVersion }
|
|
52
|
+
: {}),
|
|
53
|
+
...(operation.payload !== undefined
|
|
54
|
+
? { payload: bytesToBase64(operation.payload) }
|
|
55
|
+
: {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function renderResult(result) {
|
|
59
|
+
if (result.status === 'applied') {
|
|
60
|
+
return { opIndex: result.opIndex, status: 'applied' };
|
|
61
|
+
}
|
|
62
|
+
if (result.status === 'conflict') {
|
|
63
|
+
return {
|
|
64
|
+
opIndex: result.opIndex,
|
|
65
|
+
status: 'conflict',
|
|
66
|
+
code: result.code,
|
|
67
|
+
message: result.message,
|
|
68
|
+
serverVersion: result.serverVersion,
|
|
69
|
+
serverRow: bytesToBase64(result.serverRow),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
opIndex: result.opIndex,
|
|
74
|
+
status: 'error',
|
|
75
|
+
code: result.code,
|
|
76
|
+
message: result.message,
|
|
77
|
+
retryable: result.retryable,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function renderChange(change) {
|
|
81
|
+
return {
|
|
82
|
+
tableIndex: change.tableIndex,
|
|
83
|
+
rowId: change.rowId,
|
|
84
|
+
op: change.op,
|
|
85
|
+
...(change.rowVersion !== undefined
|
|
86
|
+
? { rowVersion: change.rowVersion }
|
|
87
|
+
: {}),
|
|
88
|
+
scopes: renderStringMap(change.scopes),
|
|
89
|
+
...(change.row !== undefined ? { row: bytesToBase64(change.row) } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function renderFrame(frame) {
|
|
93
|
+
switch (frame.type) {
|
|
94
|
+
case 'REQ_HEADER':
|
|
95
|
+
return {
|
|
96
|
+
type: 'REQ_HEADER',
|
|
97
|
+
clientId: frame.clientId,
|
|
98
|
+
schemaVersion: frame.schemaVersion,
|
|
99
|
+
};
|
|
100
|
+
case 'PUSH_COMMIT':
|
|
101
|
+
return {
|
|
102
|
+
type: 'PUSH_COMMIT',
|
|
103
|
+
clientCommitId: frame.clientCommitId,
|
|
104
|
+
operations: frame.operations.map(renderOperation),
|
|
105
|
+
};
|
|
106
|
+
case 'PULL_HEADER':
|
|
107
|
+
return {
|
|
108
|
+
type: 'PULL_HEADER',
|
|
109
|
+
limitCommits: frame.limitCommits,
|
|
110
|
+
limitSnapshotRows: frame.limitSnapshotRows,
|
|
111
|
+
maxSnapshotPages: frame.maxSnapshotPages,
|
|
112
|
+
accept: frame.accept,
|
|
113
|
+
};
|
|
114
|
+
case 'SUBSCRIPTION':
|
|
115
|
+
return {
|
|
116
|
+
type: 'SUBSCRIPTION',
|
|
117
|
+
id: frame.id,
|
|
118
|
+
table: frame.table,
|
|
119
|
+
scopes: renderScopeLists(frame.scopes),
|
|
120
|
+
...(frame.params !== undefined
|
|
121
|
+
? { params: parseJson(frame.params) }
|
|
122
|
+
: {}),
|
|
123
|
+
cursor: frame.cursor,
|
|
124
|
+
...(frame.bootstrapState !== undefined
|
|
125
|
+
? { bootstrapState: parseJson(frame.bootstrapState) }
|
|
126
|
+
: {}),
|
|
127
|
+
};
|
|
128
|
+
case 'RESP_HEADER':
|
|
129
|
+
return {
|
|
130
|
+
type: 'RESP_HEADER',
|
|
131
|
+
...(frame.requiredSchemaVersion !== undefined
|
|
132
|
+
? { requiredSchemaVersion: frame.requiredSchemaVersion }
|
|
133
|
+
: {}),
|
|
134
|
+
...(frame.latestSchemaVersion !== undefined
|
|
135
|
+
? { latestSchemaVersion: frame.latestSchemaVersion }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
138
|
+
case 'LEASE':
|
|
139
|
+
return {
|
|
140
|
+
type: 'LEASE',
|
|
141
|
+
leaseId: frame.leaseId,
|
|
142
|
+
expiresAtMs: frame.expiresAtMs,
|
|
143
|
+
};
|
|
144
|
+
case 'PUSH_RESULT':
|
|
145
|
+
return {
|
|
146
|
+
type: 'PUSH_RESULT',
|
|
147
|
+
clientCommitId: frame.clientCommitId,
|
|
148
|
+
status: frame.status,
|
|
149
|
+
...(frame.commitSeq !== undefined
|
|
150
|
+
? { commitSeq: frame.commitSeq }
|
|
151
|
+
: {}),
|
|
152
|
+
results: frame.results.map(renderResult),
|
|
153
|
+
};
|
|
154
|
+
case 'SUB_START':
|
|
155
|
+
return {
|
|
156
|
+
type: 'SUB_START',
|
|
157
|
+
id: frame.id,
|
|
158
|
+
status: frame.status,
|
|
159
|
+
reasonCode: frame.reasonCode,
|
|
160
|
+
effectiveScopes: renderScopeLists(frame.effectiveScopes),
|
|
161
|
+
bootstrap: frame.bootstrap,
|
|
162
|
+
};
|
|
163
|
+
case 'COMMIT':
|
|
164
|
+
return {
|
|
165
|
+
type: 'COMMIT',
|
|
166
|
+
commitSeq: frame.commitSeq,
|
|
167
|
+
createdAtMs: frame.createdAtMs,
|
|
168
|
+
actorId: frame.actorId,
|
|
169
|
+
tables: [...frame.tables],
|
|
170
|
+
changes: frame.changes.map(renderChange),
|
|
171
|
+
};
|
|
172
|
+
case 'SEGMENT_REF':
|
|
173
|
+
return {
|
|
174
|
+
type: 'SEGMENT_REF',
|
|
175
|
+
segmentId: frame.segmentId,
|
|
176
|
+
mediaType: frame.mediaType,
|
|
177
|
+
table: frame.table,
|
|
178
|
+
byteLength: frame.byteLength,
|
|
179
|
+
rowCount: frame.rowCount,
|
|
180
|
+
asOfCommitSeq: frame.asOfCommitSeq,
|
|
181
|
+
scopeDigest: frame.scopeDigest,
|
|
182
|
+
...(frame.rowCursor !== undefined
|
|
183
|
+
? { rowCursor: frame.rowCursor }
|
|
184
|
+
: {}),
|
|
185
|
+
...(frame.nextRowCursor !== undefined
|
|
186
|
+
? { nextRowCursor: frame.nextRowCursor }
|
|
187
|
+
: {}),
|
|
188
|
+
...(frame.url !== undefined ? { url: frame.url } : {}),
|
|
189
|
+
...(frame.urlExpiresAtMs !== undefined
|
|
190
|
+
? { urlExpiresAtMs: frame.urlExpiresAtMs }
|
|
191
|
+
: {}),
|
|
192
|
+
};
|
|
193
|
+
case 'SEGMENT_INLINE':
|
|
194
|
+
return {
|
|
195
|
+
type: 'SEGMENT_INLINE',
|
|
196
|
+
payload: bytesToBase64(frame.payload),
|
|
197
|
+
};
|
|
198
|
+
case 'SUB_END':
|
|
199
|
+
return {
|
|
200
|
+
type: 'SUB_END',
|
|
201
|
+
nextCursor: frame.nextCursor,
|
|
202
|
+
...(frame.bootstrapState !== undefined
|
|
203
|
+
? { bootstrapState: parseJson(frame.bootstrapState) }
|
|
204
|
+
: {}),
|
|
205
|
+
};
|
|
206
|
+
case 'ERROR':
|
|
207
|
+
return {
|
|
208
|
+
type: 'ERROR',
|
|
209
|
+
code: frame.code,
|
|
210
|
+
message: frame.message,
|
|
211
|
+
category: frame.category,
|
|
212
|
+
retryable: frame.retryable,
|
|
213
|
+
recommendedAction: frame.recommendedAction,
|
|
214
|
+
...(frame.details !== undefined
|
|
215
|
+
? { details: parseJson(frame.details) }
|
|
216
|
+
: {}),
|
|
217
|
+
};
|
|
218
|
+
case 'UNKNOWN':
|
|
219
|
+
return {
|
|
220
|
+
type: 'UNKNOWN',
|
|
221
|
+
frameType: frame.frameType,
|
|
222
|
+
payload: bytesToBase64(frame.payload),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export function renderMessageValue(message) {
|
|
227
|
+
return {
|
|
228
|
+
magic: 'SSP2',
|
|
229
|
+
wireVersion: message.wireVersion,
|
|
230
|
+
msgKind: message.msgKind,
|
|
231
|
+
frames: message.frames.map((frame) => renderFrame(frame)),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** `render(bytes) → json` for SSP2 messages (§11). */
|
|
235
|
+
export function renderMessage(bytes) {
|
|
236
|
+
return renderMessageValue(decodeMessage(bytes));
|
|
237
|
+
}
|
|
238
|
+
function renderRowValue(column, value) {
|
|
239
|
+
if (value === null)
|
|
240
|
+
return null;
|
|
241
|
+
if (value instanceof Uint8Array)
|
|
242
|
+
return bytesToBase64(value);
|
|
243
|
+
if ((column.type === 'json' || column.type === 'blob_ref') &&
|
|
244
|
+
typeof value === 'string') {
|
|
245
|
+
// §11: json and blob_ref (tag 7) render as embedded parsed JSON.
|
|
246
|
+
return parseJson(value);
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
export function renderRowsSegmentValue(segment) {
|
|
251
|
+
return {
|
|
252
|
+
magic: 'SSG2',
|
|
253
|
+
formatVersion: ROWS_SEGMENT_FORMAT_VERSION,
|
|
254
|
+
table: segment.table,
|
|
255
|
+
schemaVersion: segment.schemaVersion,
|
|
256
|
+
columns: segment.columns.map((column) => ({
|
|
257
|
+
name: column.name,
|
|
258
|
+
type: column.type,
|
|
259
|
+
nullable: column.nullable,
|
|
260
|
+
})),
|
|
261
|
+
blocks: segment.blocks.map((block) => block.map((row) => ({
|
|
262
|
+
serverVersion: row.serverVersion,
|
|
263
|
+
values: Object.fromEntries(segment.columns.map((column, i) => [
|
|
264
|
+
column.name,
|
|
265
|
+
renderRowValue(column, row.values[i] ?? null),
|
|
266
|
+
])),
|
|
267
|
+
}))),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** `render(bytes) → json` for standalone SSG2 rows segments (§11 rule 8). */
|
|
271
|
+
export function renderRowsSegment(bytes) {
|
|
272
|
+
return renderRowsSegmentValue(decodeRowsSegment(bytes));
|
|
273
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ByteReader, ByteWriter } from './bytes.js';
|
|
2
|
+
export type ColumnType = 'string' | 'integer' | 'float' | 'boolean' | 'json' | 'bytes' | 'blob_ref' | 'crdt';
|
|
3
|
+
export interface RowColumn {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly type: ColumnType;
|
|
6
|
+
readonly nullable: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* For a `crdt` column (§2.4 tag 8, §5.10.1): the named merger the server
|
|
9
|
+
* selects (this rung defines exactly `'yjs-doc'`). Schema-IR metadata
|
|
10
|
+
* ONLY — never on the wire (the SSG2 column table carries name/type/
|
|
11
|
+
* nullable, and `crdt` shares the `bytes` tag). Ignored for other types.
|
|
12
|
+
*/
|
|
13
|
+
readonly crdtType?: string;
|
|
14
|
+
/**
|
|
15
|
+
* §5.11: this column is encrypted end-to-end. When set, `type` is `bytes`
|
|
16
|
+
* (the wire/stored type — the ciphertext envelope rides the bytes machinery)
|
|
17
|
+
* and `declaredType` is the pre-flip app type. Both are schema-IR metadata,
|
|
18
|
+
* never on the wire (like `crdtType`). The row codec ignores these — it
|
|
19
|
+
* only ever sees a `bytes` value; encryption/decryption is the client
|
|
20
|
+
* encode/apply seam (§5.11).
|
|
21
|
+
*/
|
|
22
|
+
readonly encrypted?: boolean;
|
|
23
|
+
/** §5.11: the app-side type of an encrypted column (its type before the
|
|
24
|
+
* wire flip to `bytes`). Present iff `encrypted` is set. */
|
|
25
|
+
readonly declaredType?: ColumnType;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `integer` is `i64` within the ±(2^53−1) contract; `json` is the raw JSON
|
|
29
|
+
* document string, preserved byte-for-byte on round-trip.
|
|
30
|
+
*/
|
|
31
|
+
export type RowValue = string | number | boolean | Uint8Array | null;
|
|
32
|
+
export declare function columnTypeTag(type: ColumnType): number;
|
|
33
|
+
export declare function columnTypeFromTag(tag: number): ColumnType;
|
|
34
|
+
export declare function writeRow(writer: ByteWriter, columns: readonly RowColumn[], values: readonly RowValue[]): void;
|
|
35
|
+
export declare function readRow(reader: ByteReader, columns: readonly RowColumn[]): RowValue[];
|
|
36
|
+
/** Encode one standalone row (e.g. a push payload or conflict serverRow). */
|
|
37
|
+
export declare function encodeRow(columns: readonly RowColumn[], values: readonly RowValue[]): Uint8Array;
|
|
38
|
+
/** Decode one standalone row; the bytes must contain exactly one row. */
|
|
39
|
+
export declare function decodeRow(columns: readonly RowColumn[], bytes: Uint8Array): RowValue[];
|