@velajs/live-protocol 1.0.0 → 1.0.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/CHANGELOG.md +7 -0
- package/dist/index.d.ts +228 -8
- package/dist/index.js +867 -5
- package/dist/index.js.map +1 -0
- package/package.json +47 -35
- package/dist/conformance.d.ts +0 -33
- package/dist/conformance.js +0 -152
- package/dist/delta.d.ts +0 -59
- package/dist/delta.js +0 -186
- package/dist/fixtures.d.ts +0 -26
- package/dist/fixtures.js +0 -592
- package/dist/frames.d.ts +0 -154
- package/dist/frames.js +0 -190
- package/dist/version.d.ts +0 -11
- package/dist/version.js +0 -10
package/dist/frames.d.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The normative frame catalog for Vela live queries.
|
|
3
|
-
*
|
|
4
|
-
* Live frames ride Vela's existing WebSocket envelope `{ event, data }` under
|
|
5
|
-
* the single reserved event name `$live`; the frame itself is the envelope's
|
|
6
|
-
* `data`, discriminated on `t`. Classic gateway events, `ping`→`pong`
|
|
7
|
-
* keepalive, and live frames coexist on one socket. The `$` prefix is reserved
|
|
8
|
-
* for the framework: app gateways must never register a `$…` event.
|
|
9
|
-
*
|
|
10
|
-
* Byte-identical encoding matters: golden fixtures pin the exact wire string
|
|
11
|
-
* for every frame shape, and both the server and the client encode through
|
|
12
|
-
* {@link encodeLiveFrame} / {@link encodeLiveEnvelope} so the two sides cannot
|
|
13
|
-
* drift. Canonical key order is the declaration order of each type below;
|
|
14
|
-
* absent optionals are omitted entirely.
|
|
15
|
-
*/
|
|
16
|
-
/** The reserved envelope event every live frame rides under. */
|
|
17
|
-
export declare const LIVE_EVENT = "$live";
|
|
18
|
-
/**
|
|
19
|
-
* The reserved event-name prefix. The WS dispatcher rejects app gateways that
|
|
20
|
-
* register a `$…` event at bootstrap so live (and future framework) frames can
|
|
21
|
-
* never collide with app events.
|
|
22
|
-
*/
|
|
23
|
-
export declare const RESERVED_EVENT_PREFIX = "$";
|
|
24
|
-
/**
|
|
25
|
-
* HTTP response headers carrying the commit cursor/epoch of the log scope a
|
|
26
|
-
* mutation's invalidations landed in. The client gates optimistic-layer drops
|
|
27
|
-
* on a subscription frame whose `cursor` passes this value (and whose `epoch`
|
|
28
|
-
* matches) — never on HTTP response timing, which races the broadcast.
|
|
29
|
-
*/
|
|
30
|
-
export declare const COMMIT_CURSOR_HEADER = "Vela-Commit-Cursor";
|
|
31
|
-
export declare const COMMIT_EPOCH_HEADER = "Vela-Commit-Epoch";
|
|
32
|
-
/** Well-known `error` frame codes. The code space is open — receivers must tolerate unknown codes. */
|
|
33
|
-
export declare const LIVE_ERROR_CODES: {
|
|
34
|
-
readonly UNSUPPORTED_PROTOCOL: "unsupported_protocol";
|
|
35
|
-
readonly DUPLICATE_SUB: "duplicate_sub";
|
|
36
|
-
readonly UNKNOWN_QUERY: "unknown_query";
|
|
37
|
-
readonly FORBIDDEN: "forbidden";
|
|
38
|
-
readonly BAD_ARGS: "bad_args";
|
|
39
|
-
readonly INTERNAL: "internal";
|
|
40
|
-
};
|
|
41
|
-
export type LiveErrorCode = (typeof LIVE_ERROR_CODES)[keyof typeof LIVE_ERROR_CODES] | (string & {});
|
|
42
|
-
/**
|
|
43
|
-
* One row change inside a `delta` frame. Ops are keyed by the query's key
|
|
44
|
-
* field (default `'id'`); `insert`/`update` carry the full new row, `delete`
|
|
45
|
-
* omits it. An `insert` carries `before` — the key of the row it precedes in
|
|
46
|
-
* the authoritative result (`null` = append) — so the client reconstructs the
|
|
47
|
-
* server's ordering exactly. Application is idempotent: `insert` on an
|
|
48
|
-
* existing key replaces in place, `delete` of an absent key is a no-op.
|
|
49
|
-
*/
|
|
50
|
-
export type RowOp = {
|
|
51
|
-
op: 'insert';
|
|
52
|
-
key: string;
|
|
53
|
-
row: Record<string, unknown>;
|
|
54
|
-
before: string | null;
|
|
55
|
-
} | {
|
|
56
|
-
op: 'update';
|
|
57
|
-
key: string;
|
|
58
|
-
row: Record<string, unknown>;
|
|
59
|
-
} | {
|
|
60
|
-
op: 'delete';
|
|
61
|
-
key: string;
|
|
62
|
-
};
|
|
63
|
-
/** Client → server frames (the `data` of a `{ event: '$live' }` envelope). */
|
|
64
|
-
export type ClientLiveFrame = {
|
|
65
|
-
t: 'sub';
|
|
66
|
-
/** Client-chosen subscription id, unique per socket. */
|
|
67
|
-
sub: string;
|
|
68
|
-
/** The live-query identifier declared by `@LiveQuery(name)`. */
|
|
69
|
-
query: string;
|
|
70
|
-
args?: unknown;
|
|
71
|
-
/** Resume watermark: last observed cursor/epoch. Omitted = cold subscribe. */
|
|
72
|
-
sinceCursor?: number;
|
|
73
|
-
sinceEpoch?: string;
|
|
74
|
-
/** Key-field override for list deltas (default `'id'`). */
|
|
75
|
-
key?: string;
|
|
76
|
-
/** Protocol version the client speaks (see LIVE_PROTOCOL). */
|
|
77
|
-
v?: number;
|
|
78
|
-
} | {
|
|
79
|
-
t: 'unsub';
|
|
80
|
-
sub: string;
|
|
81
|
-
} | {
|
|
82
|
-
t: 'presence';
|
|
83
|
-
room: string;
|
|
84
|
-
meta?: unknown;
|
|
85
|
-
};
|
|
86
|
-
/** Server → client frames. `ack` precedes any `data`/`resume` for a sub. */
|
|
87
|
-
export type ServerLiveFrame = {
|
|
88
|
-
t: 'ack';
|
|
89
|
-
sub: string;
|
|
90
|
-
} | {
|
|
91
|
-
t: 'data';
|
|
92
|
-
sub: string;
|
|
93
|
-
snapshot: unknown;
|
|
94
|
-
cursor?: number;
|
|
95
|
-
epoch?: string;
|
|
96
|
-
} | {
|
|
97
|
-
t: 'delta';
|
|
98
|
-
sub: string;
|
|
99
|
-
ops: RowOp[];
|
|
100
|
-
cursor?: number;
|
|
101
|
-
epoch?: string;
|
|
102
|
-
}
|
|
103
|
-
/** Re-run result was byte-identical — no payload, but the cursor still advances (drops optimistic layers). */
|
|
104
|
-
| {
|
|
105
|
-
t: 'settled';
|
|
106
|
-
sub: string;
|
|
107
|
-
cursor?: number;
|
|
108
|
-
epoch?: string;
|
|
109
|
-
}
|
|
110
|
-
/** Resume verdict: nothing relevant changed while away — keep the cached value, advance the cursor. */
|
|
111
|
-
| {
|
|
112
|
-
t: 'resume';
|
|
113
|
-
sub: string;
|
|
114
|
-
cursor: number;
|
|
115
|
-
epoch: string;
|
|
116
|
-
} | {
|
|
117
|
-
t: 'error';
|
|
118
|
-
sub?: string;
|
|
119
|
-
code: LiveErrorCode;
|
|
120
|
-
message: string;
|
|
121
|
-
fatal: boolean;
|
|
122
|
-
};
|
|
123
|
-
export type LiveFrame = ClientLiveFrame | ServerLiveFrame;
|
|
124
|
-
/** Structural guard for a single {@link RowOp}. Unknown extra fields are tolerated. */
|
|
125
|
-
export declare const isRowOp: (value: unknown) => value is RowOp;
|
|
126
|
-
export declare const isRowOps: (value: unknown) => value is RowOp[];
|
|
127
|
-
/**
|
|
128
|
-
* Structural guard for a client frame. Frames with an unknown `t` return
|
|
129
|
-
* false — per the forward-compat rule the receiver then ignores the frame.
|
|
130
|
-
*/
|
|
131
|
-
export declare const isClientLiveFrame: (value: unknown) => value is ClientLiveFrame;
|
|
132
|
-
/** Structural guard for a server frame. Unknown `t` → false (receiver ignores). */
|
|
133
|
-
export declare const isServerLiveFrame: (value: unknown) => value is ServerLiveFrame;
|
|
134
|
-
/**
|
|
135
|
-
* Extract the live frame from a parsed WS envelope, or `undefined` when the
|
|
136
|
-
* envelope is not a live envelope. Does NOT validate the frame — pair with
|
|
137
|
-
* {@link isClientLiveFrame} / {@link isServerLiveFrame} on the receiving side.
|
|
138
|
-
*/
|
|
139
|
-
export declare const readLiveEnvelope: (envelope: unknown) => unknown;
|
|
140
|
-
/** Wrap a frame in the `$live` envelope object. */
|
|
141
|
-
export declare const liveEnvelope: (frame: LiveFrame) => {
|
|
142
|
-
event: typeof LIVE_EVENT;
|
|
143
|
-
data: LiveFrame;
|
|
144
|
-
};
|
|
145
|
-
/**
|
|
146
|
-
* Rebuild a frame with the canonical key order, dropping absent optionals.
|
|
147
|
-
* `JSON.stringify` of the result is the frame's canonical wire form — the one
|
|
148
|
-
* the golden fixtures pin byte-for-byte.
|
|
149
|
-
*/
|
|
150
|
-
export declare const canonicalLiveFrame: (frame: LiveFrame) => Record<string, unknown>;
|
|
151
|
-
/** Canonical JSON encoding of a bare frame (no envelope). */
|
|
152
|
-
export declare const encodeLiveFrame: (frame: LiveFrame) => string;
|
|
153
|
-
/** Canonical JSON encoding of the full `$live` envelope — what actually goes on the socket. */
|
|
154
|
-
export declare const encodeLiveEnvelope: (frame: LiveFrame) => string;
|
package/dist/frames.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The normative frame catalog for Vela live queries.
|
|
3
|
-
*
|
|
4
|
-
* Live frames ride Vela's existing WebSocket envelope `{ event, data }` under
|
|
5
|
-
* the single reserved event name `$live`; the frame itself is the envelope's
|
|
6
|
-
* `data`, discriminated on `t`. Classic gateway events, `ping`→`pong`
|
|
7
|
-
* keepalive, and live frames coexist on one socket. The `$` prefix is reserved
|
|
8
|
-
* for the framework: app gateways must never register a `$…` event.
|
|
9
|
-
*
|
|
10
|
-
* Byte-identical encoding matters: golden fixtures pin the exact wire string
|
|
11
|
-
* for every frame shape, and both the server and the client encode through
|
|
12
|
-
* {@link encodeLiveFrame} / {@link encodeLiveEnvelope} so the two sides cannot
|
|
13
|
-
* drift. Canonical key order is the declaration order of each type below;
|
|
14
|
-
* absent optionals are omitted entirely.
|
|
15
|
-
*/ /** The reserved envelope event every live frame rides under. */ export const LIVE_EVENT = '$live';
|
|
16
|
-
/**
|
|
17
|
-
* The reserved event-name prefix. The WS dispatcher rejects app gateways that
|
|
18
|
-
* register a `$…` event at bootstrap so live (and future framework) frames can
|
|
19
|
-
* never collide with app events.
|
|
20
|
-
*/ export const RESERVED_EVENT_PREFIX = '$';
|
|
21
|
-
/**
|
|
22
|
-
* HTTP response headers carrying the commit cursor/epoch of the log scope a
|
|
23
|
-
* mutation's invalidations landed in. The client gates optimistic-layer drops
|
|
24
|
-
* on a subscription frame whose `cursor` passes this value (and whose `epoch`
|
|
25
|
-
* matches) — never on HTTP response timing, which races the broadcast.
|
|
26
|
-
*/ export const COMMIT_CURSOR_HEADER = 'Vela-Commit-Cursor';
|
|
27
|
-
export const COMMIT_EPOCH_HEADER = 'Vela-Commit-Epoch';
|
|
28
|
-
/** Well-known `error` frame codes. The code space is open — receivers must tolerate unknown codes. */ export const LIVE_ERROR_CODES = {
|
|
29
|
-
UNSUPPORTED_PROTOCOL: 'unsupported_protocol',
|
|
30
|
-
DUPLICATE_SUB: 'duplicate_sub',
|
|
31
|
-
UNKNOWN_QUERY: 'unknown_query',
|
|
32
|
-
FORBIDDEN: 'forbidden',
|
|
33
|
-
BAD_ARGS: 'bad_args',
|
|
34
|
-
INTERNAL: 'internal'
|
|
35
|
-
};
|
|
36
|
-
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
-
const isOptionalNumber = (value)=>value === undefined || typeof value === 'number' && Number.isFinite(value);
|
|
38
|
-
const isOptionalString = (value)=>value === undefined || typeof value === 'string';
|
|
39
|
-
/** Structural guard for a single {@link RowOp}. Unknown extra fields are tolerated. */ export const isRowOp = (value)=>{
|
|
40
|
-
if (!isRecord(value) || typeof value['key'] !== 'string') return false;
|
|
41
|
-
const op = value['op'];
|
|
42
|
-
if (op === 'delete') return true;
|
|
43
|
-
if (op !== 'insert' && op !== 'update') return false;
|
|
44
|
-
if (!isRecord(value['row'])) return false;
|
|
45
|
-
if (op === 'insert') {
|
|
46
|
-
const before = value['before'];
|
|
47
|
-
return before === null || typeof before === 'string';
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
};
|
|
51
|
-
export const isRowOps = (value)=>Array.isArray(value) && value.every(isRowOp);
|
|
52
|
-
/**
|
|
53
|
-
* Structural guard for a client frame. Frames with an unknown `t` return
|
|
54
|
-
* false — per the forward-compat rule the receiver then ignores the frame.
|
|
55
|
-
*/ export const isClientLiveFrame = (value)=>{
|
|
56
|
-
if (!isRecord(value)) return false;
|
|
57
|
-
switch(value['t']){
|
|
58
|
-
case 'sub':
|
|
59
|
-
return typeof value['sub'] === 'string' && typeof value['query'] === 'string' && isOptionalNumber(value['sinceCursor']) && isOptionalString(value['sinceEpoch']) && isOptionalString(value['key']) && isOptionalNumber(value['v']);
|
|
60
|
-
case 'unsub':
|
|
61
|
-
return typeof value['sub'] === 'string';
|
|
62
|
-
case 'presence':
|
|
63
|
-
return typeof value['room'] === 'string';
|
|
64
|
-
default:
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
/** Structural guard for a server frame. Unknown `t` → false (receiver ignores). */ export const isServerLiveFrame = (value)=>{
|
|
69
|
-
if (!isRecord(value)) return false;
|
|
70
|
-
switch(value['t']){
|
|
71
|
-
case 'ack':
|
|
72
|
-
return typeof value['sub'] === 'string';
|
|
73
|
-
case 'data':
|
|
74
|
-
return typeof value['sub'] === 'string' && 'snapshot' in value && isOptionalNumber(value['cursor']) && isOptionalString(value['epoch']);
|
|
75
|
-
case 'delta':
|
|
76
|
-
return typeof value['sub'] === 'string' && isRowOps(value['ops']) && isOptionalNumber(value['cursor']) && isOptionalString(value['epoch']);
|
|
77
|
-
case 'settled':
|
|
78
|
-
return typeof value['sub'] === 'string' && isOptionalNumber(value['cursor']) && isOptionalString(value['epoch']);
|
|
79
|
-
case 'resume':
|
|
80
|
-
return typeof value['sub'] === 'string' && typeof value['cursor'] === 'number' && Number.isFinite(value['cursor']) && typeof value['epoch'] === 'string';
|
|
81
|
-
case 'error':
|
|
82
|
-
return isOptionalString(value['sub']) && typeof value['code'] === 'string' && typeof value['message'] === 'string' && typeof value['fatal'] === 'boolean';
|
|
83
|
-
default:
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* Extract the live frame from a parsed WS envelope, or `undefined` when the
|
|
89
|
-
* envelope is not a live envelope. Does NOT validate the frame — pair with
|
|
90
|
-
* {@link isClientLiveFrame} / {@link isServerLiveFrame} on the receiving side.
|
|
91
|
-
*/ export const readLiveEnvelope = (envelope)=>{
|
|
92
|
-
if (!isRecord(envelope) || envelope['event'] !== LIVE_EVENT) return undefined;
|
|
93
|
-
return envelope['data'];
|
|
94
|
-
};
|
|
95
|
-
/** Wrap a frame in the `$live` envelope object. */ export const liveEnvelope = (frame)=>({
|
|
96
|
-
event: LIVE_EVENT,
|
|
97
|
-
data: frame
|
|
98
|
-
});
|
|
99
|
-
const CANONICAL_KEYS = {
|
|
100
|
-
sub: [
|
|
101
|
-
't',
|
|
102
|
-
'sub',
|
|
103
|
-
'query',
|
|
104
|
-
'args',
|
|
105
|
-
'sinceCursor',
|
|
106
|
-
'sinceEpoch',
|
|
107
|
-
'key',
|
|
108
|
-
'v'
|
|
109
|
-
],
|
|
110
|
-
unsub: [
|
|
111
|
-
't',
|
|
112
|
-
'sub'
|
|
113
|
-
],
|
|
114
|
-
presence: [
|
|
115
|
-
't',
|
|
116
|
-
'room',
|
|
117
|
-
'meta'
|
|
118
|
-
],
|
|
119
|
-
ack: [
|
|
120
|
-
't',
|
|
121
|
-
'sub'
|
|
122
|
-
],
|
|
123
|
-
data: [
|
|
124
|
-
't',
|
|
125
|
-
'sub',
|
|
126
|
-
'snapshot',
|
|
127
|
-
'cursor',
|
|
128
|
-
'epoch'
|
|
129
|
-
],
|
|
130
|
-
delta: [
|
|
131
|
-
't',
|
|
132
|
-
'sub',
|
|
133
|
-
'ops',
|
|
134
|
-
'cursor',
|
|
135
|
-
'epoch'
|
|
136
|
-
],
|
|
137
|
-
settled: [
|
|
138
|
-
't',
|
|
139
|
-
'sub',
|
|
140
|
-
'cursor',
|
|
141
|
-
'epoch'
|
|
142
|
-
],
|
|
143
|
-
resume: [
|
|
144
|
-
't',
|
|
145
|
-
'sub',
|
|
146
|
-
'cursor',
|
|
147
|
-
'epoch'
|
|
148
|
-
],
|
|
149
|
-
error: [
|
|
150
|
-
't',
|
|
151
|
-
'sub',
|
|
152
|
-
'code',
|
|
153
|
-
'message',
|
|
154
|
-
'fatal'
|
|
155
|
-
]
|
|
156
|
-
};
|
|
157
|
-
const ROW_OP_KEYS = [
|
|
158
|
-
'op',
|
|
159
|
-
'key',
|
|
160
|
-
'row',
|
|
161
|
-
'before'
|
|
162
|
-
];
|
|
163
|
-
const canonicalRowOp = (op)=>{
|
|
164
|
-
const source = op;
|
|
165
|
-
const out = {};
|
|
166
|
-
for (const key of ROW_OP_KEYS){
|
|
167
|
-
if (source[key] !== undefined) out[key] = source[key];
|
|
168
|
-
}
|
|
169
|
-
return out;
|
|
170
|
-
};
|
|
171
|
-
/**
|
|
172
|
-
* Rebuild a frame with the canonical key order, dropping absent optionals.
|
|
173
|
-
* `JSON.stringify` of the result is the frame's canonical wire form — the one
|
|
174
|
-
* the golden fixtures pin byte-for-byte.
|
|
175
|
-
*/ export const canonicalLiveFrame = (frame)=>{
|
|
176
|
-
const source = frame;
|
|
177
|
-
const keys = CANONICAL_KEYS[frame.t];
|
|
178
|
-
if (keys === undefined) {
|
|
179
|
-
throw new Error(`Unknown live frame type: ${String(frame.t)}`);
|
|
180
|
-
}
|
|
181
|
-
const out = {};
|
|
182
|
-
for (const key of keys){
|
|
183
|
-
const value = source[key];
|
|
184
|
-
if (value === undefined) continue;
|
|
185
|
-
out[key] = frame.t === 'delta' && key === 'ops' ? value.map(canonicalRowOp) : value;
|
|
186
|
-
}
|
|
187
|
-
return out;
|
|
188
|
-
};
|
|
189
|
-
/** Canonical JSON encoding of a bare frame (no envelope). */ export const encodeLiveFrame = (frame)=>JSON.stringify(canonicalLiveFrame(frame));
|
|
190
|
-
/** Canonical JSON encoding of the full `$live` envelope — what actually goes on the socket. */ export const encodeLiveEnvelope = (frame)=>`{"event":${JSON.stringify(LIVE_EVENT)},"data":${encodeLiveFrame(frame)}}`;
|
package/dist/version.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-protocol wire version. Bumped ONLY on a breaking wire change (renaming
|
|
3
|
-
* or removing a field, changing a delivery guarantee). Additive changes — new
|
|
4
|
-
* optional fields, new frame types — do NOT bump it: receivers MUST ignore
|
|
5
|
-
* unknown frame `t` values and unknown object fields.
|
|
6
|
-
*
|
|
7
|
-
* A client advertises the version it speaks via the `v` field on its `sub`
|
|
8
|
-
* frame; a server that cannot serve that version replies
|
|
9
|
-
* `{ t: 'error', code: 'unsupported_protocol', fatal: true }`.
|
|
10
|
-
*/
|
|
11
|
-
export declare const LIVE_PROTOCOL = 1;
|
package/dist/version.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-protocol wire version. Bumped ONLY on a breaking wire change (renaming
|
|
3
|
-
* or removing a field, changing a delivery guarantee). Additive changes — new
|
|
4
|
-
* optional fields, new frame types — do NOT bump it: receivers MUST ignore
|
|
5
|
-
* unknown frame `t` values and unknown object fields.
|
|
6
|
-
*
|
|
7
|
-
* A client advertises the version it speaks via the `v` field on its `sub`
|
|
8
|
-
* frame; a server that cannot serve that version replies
|
|
9
|
-
* `{ t: 'error', code: 'unsupported_protocol', fatal: true }`.
|
|
10
|
-
*/ export const LIVE_PROTOCOL = 1;
|