@velajs/live-protocol 1.0.0 → 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @velajs/live-protocol
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6e9d70a: Validate live frame schemas, JSON shape, protocol versions, cursor/epoch pairs, and secure default size limits for frames, deltas, and presence metadata. Outbound encoders now reject malformed or oversized frames before serialization.
8
+
9
+ ## 1.0.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 0fcf971: Modernize the package build, validation, and release toolchain.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Zero runtime dependencies. Ships three things:
13
13
 
14
14
  ## Versioning
15
15
 
16
- `LIVE_PROTOCOL` (currently `1`) bumps only on breaking wire changes. Receivers must ignore unknown frame types and unknown fields; additive changes do not bump the version. Any wire change releases in lockstep: live-protocol → `@velajs/vela` → `@velajs/cloudflare` → `@velajs/client`.
16
+ `LIVE_PROTOCOL` is `2`. Every subscription must advertise `v: 2`; omitted or older versions are rejected. Receivers still ignore unknown frame types and unknown fields within the same version. Any wire change releases in lockstep: live-protocol → `@velajs/vela` → `@velajs/cloudflare` → `@velajs/client`.
17
17
 
18
18
  ## Delivery semantics (normative summary)
19
19
 
@@ -23,4 +23,13 @@ Zero runtime dependencies. Ships three things:
23
23
  - **`resume`** means nothing relevant changed while the client was away: keep the cached value, advance the cursor.
24
24
  - Optimistic updates gate on a subscription frame whose `cursor` passes the mutation's `Vela-Commit-Cursor` — never on HTTP response timing, which races the broadcast.
25
25
 
26
+ ## Validation and limits
27
+
28
+ Both endpoints must run the exported frame guards before dispatch. They reject
29
+ non-JSON/prototype-bearing payloads, unsafe or negative cursors, incomplete
30
+ cursor/epoch pairs, unsupported advertised versions, oversized strings, and
31
+ malformed row operations. Defaults are 64 KiB per envelope, 1,000 delta operations,
32
+ and 4 KiB of presence metadata. Clients ignore regressive cursors and cold-resubscribe
33
+ when an epoch or watermark cannot continue safely.
34
+
26
35
  See `vela/LIVE.md` in the main framework repo for the full feature documentation.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,218 @@
1
- export { LIVE_PROTOCOL } from './version';
2
- export { COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER, LIVE_ERROR_CODES, LIVE_EVENT, RESERVED_EVENT_PREFIX, canonicalLiveFrame, encodeLiveEnvelope, encodeLiveFrame, isClientLiveFrame, isRowOp, isRowOps, isServerLiveFrame, liveEnvelope, readLiveEnvelope, } from './frames';
3
- export type { ClientLiveFrame, LiveErrorCode, LiveFrame, RowOp, ServerLiveFrame } from './frames';
4
- export { DEFAULT_KEY_FIELD, applyListDelta, encodeListDelta } from './delta';
5
- export { runProtocolConformance } from './conformance';
6
- export type { ConformanceReport, DeltaCodec } from './conformance';
7
- export { DELTA_FIXTURES, FRAME_FIXTURES } from './fixtures';
8
- export type { DeltaFixture, FrameFixture } from './fixtures';
1
+ //#region src/version.d.ts
2
+ /**
3
+ * Live-protocol wire version. Bumped ONLY on a breaking wire change (renaming
4
+ * or removing a field, changing a delivery guarantee). Additive changes — new
5
+ * optional fields, new frame types — do NOT bump it: receivers MUST ignore
6
+ * unknown frame `t` values and unknown object fields.
7
+ *
8
+ * A client advertises the version it speaks via the `v` field on its `sub`
9
+ * frame; a server that cannot serve that version replies
10
+ * `{ t: 'error', code: 'unsupported_protocol', fatal: true }`.
11
+ */
12
+ declare const LIVE_PROTOCOL = 2;
13
+ //#endregion
14
+ //#region src/frames.d.ts
15
+ /** The reserved envelope event every live frame rides under. */
16
+ declare const LIVE_EVENT = "$live";
17
+ /**
18
+ * The reserved event-name prefix. The WS dispatcher rejects app gateways that
19
+ * register a `$…` event at bootstrap so live (and future framework) frames can
20
+ * never collide with app events.
21
+ */
22
+ declare const RESERVED_EVENT_PREFIX = "$";
23
+ /**
24
+ * HTTP response headers carrying the commit cursor/epoch of the log scope a
25
+ * mutation's invalidations landed in. The client gates optimistic-layer drops
26
+ * on a subscription frame whose `cursor` passes this value (and whose `epoch`
27
+ * matches) — never on HTTP response timing, which races the broadcast.
28
+ */
29
+ declare const COMMIT_CURSOR_HEADER = "Vela-Commit-Cursor";
30
+ declare const COMMIT_EPOCH_HEADER = "Vela-Commit-Epoch";
31
+ /** Default hard limits shared by every live-protocol endpoint. */
32
+ declare const MAX_LIVE_FRAME_BYTES: number;
33
+ declare const MAX_PRESENCE_METADATA_BYTES: number;
34
+ declare const MAX_DELTA_OPS = 1000;
35
+ /** Well-known `error` frame codes. The code space is open — receivers must tolerate unknown codes. */
36
+ declare const LIVE_ERROR_CODES: {
37
+ readonly UNSUPPORTED_PROTOCOL: 'unsupported_protocol';
38
+ readonly DUPLICATE_SUB: 'duplicate_sub';
39
+ readonly UNKNOWN_QUERY: 'unknown_query';
40
+ readonly FORBIDDEN: 'forbidden';
41
+ readonly BAD_ARGS: 'bad_args';
42
+ readonly LIMIT_EXCEEDED: 'limit_exceeded';
43
+ readonly INTERNAL: 'internal';
44
+ };
45
+ type LiveErrorCode = (typeof LIVE_ERROR_CODES)[keyof typeof LIVE_ERROR_CODES] | (string & {});
46
+ /**
47
+ * One row change inside a `delta` frame. Ops are keyed by the query's key
48
+ * field (default `'id'`); `insert`/`update` carry the full new row, `delete`
49
+ * omits it. An `insert` carries `before` — the key of the row it precedes in
50
+ * the authoritative result (`null` = append) — so the client reconstructs the
51
+ * server's ordering exactly. Application is idempotent: `insert` on an
52
+ * existing key replaces in place, `delete` of an absent key is a no-op.
53
+ */
54
+ type RowOp = {
55
+ op: 'insert';
56
+ key: string;
57
+ row: Record<string, unknown>;
58
+ before: string | null;
59
+ } | {
60
+ op: 'update';
61
+ key: string;
62
+ row: Record<string, unknown>;
63
+ } | {
64
+ op: 'delete';
65
+ key: string;
66
+ };
67
+ /** Client → server frames (the `data` of a `{ event: '$live' }` envelope). */
68
+ type ClientLiveFrame = {
69
+ t: 'sub';
70
+ /** Client-chosen subscription id, unique per socket. */
71
+ sub: string;
72
+ /** The live-query identifier declared by `@LiveQuery(name)`. */
73
+ query: string;
74
+ args?: unknown;
75
+ /** Resume watermark: last observed cursor/epoch. Omitted = cold subscribe. */
76
+ sinceCursor?: number;
77
+ sinceEpoch?: string;
78
+ /** Key-field override for list deltas (default `'id'`). */
79
+ key?: string;
80
+ /** Protocol version the client speaks (see LIVE_PROTOCOL). */
81
+ v: number;
82
+ } | {
83
+ t: 'unsub';
84
+ sub: string;
85
+ } | {
86
+ t: 'presence';
87
+ room: string;
88
+ meta?: unknown;
89
+ };
90
+ /** Server → client frames. `ack` precedes any `data`/`resume` for a sub. */
91
+ type ServerLiveFrame = {
92
+ t: 'ack';
93
+ sub: string;
94
+ } | {
95
+ t: 'data';
96
+ sub: string;
97
+ snapshot: unknown;
98
+ cursor?: number;
99
+ epoch?: string;
100
+ } | {
101
+ t: 'delta';
102
+ sub: string;
103
+ ops: RowOp[];
104
+ cursor?: number;
105
+ epoch?: string;
106
+ } |
107
+ /** Re-run result was byte-identical — no payload, but the cursor still advances (drops optimistic layers). */
108
+ {
109
+ t: 'settled';
110
+ sub: string;
111
+ cursor?: number;
112
+ epoch?: string;
113
+ } |
114
+ /** Resume verdict: nothing relevant changed while away — keep the cached value, advance the cursor. */
115
+ {
116
+ t: 'resume';
117
+ sub: string;
118
+ cursor: number;
119
+ epoch: string;
120
+ } | {
121
+ t: 'error';
122
+ sub?: string;
123
+ code: LiveErrorCode;
124
+ message: string;
125
+ fatal: boolean;
126
+ };
127
+ type LiveFrame = ClientLiveFrame | ServerLiveFrame;
128
+ /** Structural guard for a single {@link RowOp}. Unknown extra fields are tolerated. */
129
+ declare const isRowOp: (value: unknown) => value is RowOp;
130
+ declare const isRowOps: (value: unknown) => value is RowOp[];
131
+ /**
132
+ * Structural guard for a client frame. Frames with an unknown `t` return
133
+ * false — per the forward-compat rule the receiver then ignores the frame.
134
+ */
135
+ declare const isClientLiveFrame: (value: unknown) => value is ClientLiveFrame;
136
+ /** Structural guard for a server frame. Unknown `t` → false (receiver ignores). */
137
+ declare const isServerLiveFrame: (value: unknown) => value is ServerLiveFrame;
138
+ /**
139
+ * Extract the live frame from a parsed WS envelope, or `undefined` when the
140
+ * envelope is not a live envelope. Does NOT validate the frame — pair with
141
+ * {@link isClientLiveFrame} / {@link isServerLiveFrame} on the receiving side.
142
+ */
143
+ declare const readLiveEnvelope: (envelope: unknown) => unknown;
144
+ /** Wrap a frame in the `$live` envelope object. */
145
+ declare const liveEnvelope: (frame: LiveFrame) => {
146
+ event: typeof LIVE_EVENT;
147
+ data: LiveFrame;
148
+ };
149
+ /**
150
+ * Rebuild a frame with the canonical key order, dropping absent optionals.
151
+ * `JSON.stringify` of the result is the frame's canonical wire form — the one
152
+ * the golden fixtures pin byte-for-byte.
153
+ */
154
+ declare const canonicalLiveFrame: (frame: LiveFrame) => Record<string, unknown>;
155
+ /** Canonical JSON encoding of a bare frame (no envelope). */
156
+ declare const encodeLiveFrame: (frame: LiveFrame) => string;
157
+ /** Canonical JSON encoding of the full `$live` envelope — what actually goes on the socket. */
158
+ declare const encodeLiveEnvelope: (frame: LiveFrame) => string;
159
+ //#endregion
160
+ //#region src/delta.d.ts
161
+ /** Default row-identity field. Per-query override rides the `sub` frame's `key`. */
162
+ declare const DEFAULT_KEY_FIELD = "id";
163
+ /**
164
+ * Diff `previous` vs `next` into row ops, or `undefined` when any bail rule
165
+ * holds and the caller must send a full snapshot instead.
166
+ *
167
+ * An empty array is a valid result (no row-level change — typically the server
168
+ * catches byte-identical results earlier and sends `settled` instead).
169
+ */
170
+ declare const encodeListDelta: (previous: unknown, next: unknown, keyField?: string) => RowOp[] | undefined;
171
+ /**
172
+ * Merge row ops into a cached array result, returning a NEW array (the input
173
+ * is never mutated), or `undefined` when the ops cannot be applied cleanly —
174
+ * the caller then falls back to full replacement and lets the next snapshot
175
+ * reconcile.
176
+ *
177
+ * Idempotent by construction: replaying an op after a snapshot already
178
+ * delivered its effect changes nothing.
179
+ */
180
+ declare const applyListDelta: (current: unknown, ops: readonly RowOp[], keyField?: string) => unknown[] | undefined;
181
+ //#endregion
182
+ //#region src/conformance.d.ts
183
+ /** The two halves a wire endpoint must implement compatibly. */
184
+ interface DeltaCodec {
185
+ encodeListDelta: (previous: unknown, next: unknown, keyField?: string) => RowOp[] | undefined;
186
+ applyListDelta: (current: unknown, ops: readonly RowOp[], keyField?: string) => unknown[] | undefined;
187
+ }
188
+ interface ConformanceReport {
189
+ /** Human-readable failure descriptions; empty = conformant. */
190
+ failures: string[];
191
+ checks: number;
192
+ }
193
+ /**
194
+ * Run the full conformance suite against a codec (defaults to the reference
195
+ * codec in this package — the package's own tests run exactly this).
196
+ */
197
+ declare const runProtocolConformance: (codec?: DeltaCodec) => ConformanceReport;
198
+ //#endregion
199
+ //#region src/fixtures.d.ts
200
+ interface FrameFixture {
201
+ name: string;
202
+ frame: LiveFrame;
203
+ /** Exact canonical envelope bytes: `encodeLiveEnvelope(frame)` must equal this. */
204
+ wire: string;
205
+ }
206
+ declare const FRAME_FIXTURES: FrameFixture[];
207
+ interface DeltaFixture {
208
+ name: string;
209
+ previous: unknown;
210
+ next: unknown;
211
+ keyField?: string;
212
+ /** Expected ops, or `null` when the encoder MUST bail to snapshot. */
213
+ expected: RowOp[] | null;
214
+ }
215
+ declare const DELTA_FIXTURES: DeltaFixture[];
216
+ //#endregion
217
+ export { COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER, type ClientLiveFrame, type ConformanceReport, DEFAULT_KEY_FIELD, DELTA_FIXTURES, type DeltaCodec, type DeltaFixture, FRAME_FIXTURES, type FrameFixture, LIVE_ERROR_CODES, LIVE_EVENT, LIVE_PROTOCOL, type LiveErrorCode, type LiveFrame, MAX_DELTA_OPS, MAX_LIVE_FRAME_BYTES, MAX_PRESENCE_METADATA_BYTES, RESERVED_EVENT_PREFIX, type RowOp, type ServerLiveFrame, applyListDelta, canonicalLiveFrame, encodeListDelta, encodeLiveEnvelope, encodeLiveFrame, isClientLiveFrame, isRowOp, isRowOps, isServerLiveFrame, liveEnvelope, readLiveEnvelope, runProtocolConformance };
218
+ //# sourceMappingURL=index.d.ts.map