@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/version.ts","../src/frames.ts","../src/delta.ts","../src/fixtures.ts","../src/conformance.ts"],"sourcesContent":["/**\n * Live-protocol wire version. Bumped ONLY on a breaking wire change (renaming\n * or removing a field, changing a delivery guarantee). Additive changes — new\n * optional fields, new frame types — do NOT bump it: receivers MUST ignore\n * unknown frame `t` values and unknown object fields.\n *\n * A client advertises the version it speaks via the `v` field on its `sub`\n * frame; a server that cannot serve that version replies\n * `{ t: 'error', code: 'unsupported_protocol', fatal: true }`.\n */\nexport const LIVE_PROTOCOL = 1;\n","/**\n * The normative frame catalog for Vela live queries.\n *\n * Live frames ride Vela's existing WebSocket envelope `{ event, data }` under\n * the single reserved event name `$live`; the frame itself is the envelope's\n * `data`, discriminated on `t`. Classic gateway events, `ping`→`pong`\n * keepalive, and live frames coexist on one socket. The `$` prefix is reserved\n * for the framework: app gateways must never register a `$…` event.\n *\n * Byte-identical encoding matters: golden fixtures pin the exact wire string\n * for every frame shape, and both the server and the client encode through\n * {@link encodeLiveFrame} / {@link encodeLiveEnvelope} so the two sides cannot\n * drift. Canonical key order is the declaration order of each type below;\n * absent optionals are omitted entirely.\n */\n\n/** The reserved envelope event every live frame rides under. */\nexport const LIVE_EVENT = '$live';\n\n/**\n * The reserved event-name prefix. The WS dispatcher rejects app gateways that\n * register a `$…` event at bootstrap so live (and future framework) frames can\n * never collide with app events.\n */\nexport const RESERVED_EVENT_PREFIX = '$';\n\n/**\n * HTTP response headers carrying the commit cursor/epoch of the log scope a\n * mutation's invalidations landed in. The client gates optimistic-layer drops\n * on a subscription frame whose `cursor` passes this value (and whose `epoch`\n * matches) — never on HTTP response timing, which races the broadcast.\n */\nexport const COMMIT_CURSOR_HEADER = 'Vela-Commit-Cursor';\nexport const COMMIT_EPOCH_HEADER = 'Vela-Commit-Epoch';\n\n/** Well-known `error` frame codes. The code space is open — receivers must tolerate unknown codes. */\nexport const LIVE_ERROR_CODES = {\n UNSUPPORTED_PROTOCOL: 'unsupported_protocol',\n DUPLICATE_SUB: 'duplicate_sub',\n UNKNOWN_QUERY: 'unknown_query',\n FORBIDDEN: 'forbidden',\n BAD_ARGS: 'bad_args',\n INTERNAL: 'internal',\n} as const;\n\nexport type LiveErrorCode =\n | (typeof LIVE_ERROR_CODES)[keyof typeof LIVE_ERROR_CODES]\n | (string & {});\n\n/**\n * One row change inside a `delta` frame. Ops are keyed by the query's key\n * field (default `'id'`); `insert`/`update` carry the full new row, `delete`\n * omits it. An `insert` carries `before` — the key of the row it precedes in\n * the authoritative result (`null` = append) — so the client reconstructs the\n * server's ordering exactly. Application is idempotent: `insert` on an\n * existing key replaces in place, `delete` of an absent key is a no-op.\n */\nexport type RowOp =\n | { op: 'insert'; key: string; row: Record<string, unknown>; before: string | null }\n | { op: 'update'; key: string; row: Record<string, unknown> }\n | { op: 'delete'; key: string };\n\n/** Client → server frames (the `data` of a `{ event: '$live' }` envelope). */\nexport type ClientLiveFrame =\n | {\n t: 'sub';\n /** Client-chosen subscription id, unique per socket. */\n sub: string;\n /** The live-query identifier declared by `@LiveQuery(name)`. */\n query: string;\n args?: unknown;\n /** Resume watermark: last observed cursor/epoch. Omitted = cold subscribe. */\n sinceCursor?: number;\n sinceEpoch?: string;\n /** Key-field override for list deltas (default `'id'`). */\n key?: string;\n /** Protocol version the client speaks (see LIVE_PROTOCOL). */\n v?: number;\n }\n | { t: 'unsub'; sub: string }\n | { t: 'presence'; room: string; meta?: unknown };\n\n/** Server → client frames. `ack` precedes any `data`/`resume` for a sub. */\nexport type ServerLiveFrame =\n | { t: 'ack'; sub: string }\n | { t: 'data'; sub: string; snapshot: unknown; cursor?: number; epoch?: string }\n | { t: 'delta'; sub: string; ops: RowOp[]; cursor?: number; epoch?: string }\n /** Re-run result was byte-identical — no payload, but the cursor still advances (drops optimistic layers). */\n | { t: 'settled'; sub: string; cursor?: number; epoch?: string }\n /** Resume verdict: nothing relevant changed while away — keep the cached value, advance the cursor. */\n | { t: 'resume'; sub: string; cursor: number; epoch: string }\n | { t: 'error'; sub?: string; code: LiveErrorCode; message: string; fatal: boolean };\n\nexport type LiveFrame = ClientLiveFrame | ServerLiveFrame;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isOptionalNumber = (value: unknown): value is number | undefined =>\n value === undefined || (typeof value === 'number' && Number.isFinite(value));\n\nconst isOptionalString = (value: unknown): value is string | undefined =>\n value === undefined || typeof value === 'string';\n\n/** Structural guard for a single {@link RowOp}. Unknown extra fields are tolerated. */\nexport const isRowOp = (value: unknown): value is RowOp => {\n if (!isRecord(value) || typeof value['key'] !== 'string') return false;\n const op = value['op'];\n if (op === 'delete') return true;\n if (op !== 'insert' && op !== 'update') return false;\n if (!isRecord(value['row'])) return false;\n if (op === 'insert') {\n const before = value['before'];\n return before === null || typeof before === 'string';\n }\n return true;\n};\n\nexport const isRowOps = (value: unknown): value is RowOp[] =>\n Array.isArray(value) && value.every(isRowOp);\n\n/**\n * Structural guard for a client frame. Frames with an unknown `t` return\n * false — per the forward-compat rule the receiver then ignores the frame.\n */\nexport const isClientLiveFrame = (value: unknown): value is ClientLiveFrame => {\n if (!isRecord(value)) return false;\n switch (value['t']) {\n case 'sub':\n return (\n typeof value['sub'] === 'string' &&\n typeof value['query'] === 'string' &&\n isOptionalNumber(value['sinceCursor']) &&\n isOptionalString(value['sinceEpoch']) &&\n isOptionalString(value['key']) &&\n isOptionalNumber(value['v'])\n );\n case 'unsub':\n return typeof value['sub'] === 'string';\n case 'presence':\n return typeof value['room'] === 'string';\n default:\n return false;\n }\n};\n\n/** Structural guard for a server frame. Unknown `t` → false (receiver ignores). */\nexport const isServerLiveFrame = (value: unknown): value is ServerLiveFrame => {\n if (!isRecord(value)) return false;\n switch (value['t']) {\n case 'ack':\n return typeof value['sub'] === 'string';\n case 'data':\n return (\n typeof value['sub'] === 'string' &&\n 'snapshot' in value &&\n isOptionalNumber(value['cursor']) &&\n isOptionalString(value['epoch'])\n );\n case 'delta':\n return (\n typeof value['sub'] === 'string' &&\n isRowOps(value['ops']) &&\n isOptionalNumber(value['cursor']) &&\n isOptionalString(value['epoch'])\n );\n case 'settled':\n return (\n typeof value['sub'] === 'string' &&\n isOptionalNumber(value['cursor']) &&\n isOptionalString(value['epoch'])\n );\n case 'resume':\n return (\n typeof value['sub'] === 'string' &&\n typeof value['cursor'] === 'number' &&\n Number.isFinite(value['cursor']) &&\n typeof value['epoch'] === 'string'\n );\n case 'error':\n return (\n isOptionalString(value['sub']) &&\n typeof value['code'] === 'string' &&\n typeof value['message'] === 'string' &&\n typeof value['fatal'] === 'boolean'\n );\n default:\n return false;\n }\n};\n\n/**\n * Extract the live frame from a parsed WS envelope, or `undefined` when the\n * envelope is not a live envelope. Does NOT validate the frame — pair with\n * {@link isClientLiveFrame} / {@link isServerLiveFrame} on the receiving side.\n */\nexport const readLiveEnvelope = (envelope: unknown): unknown => {\n if (!isRecord(envelope) || envelope['event'] !== LIVE_EVENT) return undefined;\n return envelope['data'];\n};\n\n/** Wrap a frame in the `$live` envelope object. */\nexport const liveEnvelope = (frame: LiveFrame): { event: typeof LIVE_EVENT; data: LiveFrame } => ({\n event: LIVE_EVENT,\n data: frame,\n});\n\nconst CANONICAL_KEYS: Record<string, readonly string[]> = {\n sub: ['t', 'sub', 'query', 'args', 'sinceCursor', 'sinceEpoch', 'key', 'v'],\n unsub: ['t', 'sub'],\n presence: ['t', 'room', 'meta'],\n ack: ['t', 'sub'],\n data: ['t', 'sub', 'snapshot', 'cursor', 'epoch'],\n delta: ['t', 'sub', 'ops', 'cursor', 'epoch'],\n settled: ['t', 'sub', 'cursor', 'epoch'],\n resume: ['t', 'sub', 'cursor', 'epoch'],\n error: ['t', 'sub', 'code', 'message', 'fatal'],\n};\n\nconst ROW_OP_KEYS = ['op', 'key', 'row', 'before'] as const;\n\nconst canonicalRowOp = (op: RowOp): Record<string, unknown> => {\n const source = op as unknown as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of ROW_OP_KEYS) {\n if (source[key] !== undefined) out[key] = source[key];\n }\n return out;\n};\n\n/**\n * Rebuild a frame with the canonical key order, dropping absent optionals.\n * `JSON.stringify` of the result is the frame's canonical wire form — the one\n * the golden fixtures pin byte-for-byte.\n */\nexport const canonicalLiveFrame = (frame: LiveFrame): Record<string, unknown> => {\n const source = frame as unknown as Record<string, unknown>;\n const keys = CANONICAL_KEYS[frame.t];\n if (keys === undefined) {\n throw new Error(`Unknown live frame type: ${String(frame.t)}`);\n }\n const out: Record<string, unknown> = {};\n for (const key of keys) {\n const value = source[key];\n if (value === undefined) continue;\n out[key] =\n frame.t === 'delta' && key === 'ops' ? (value as RowOp[]).map(canonicalRowOp) : value;\n }\n return out;\n};\n\n/** Canonical JSON encoding of a bare frame (no envelope). */\nexport const encodeLiveFrame = (frame: LiveFrame): string =>\n JSON.stringify(canonicalLiveFrame(frame));\n\n/** Canonical JSON encoding of the full `$live` envelope — what actually goes on the socket. */\nexport const encodeLiveEnvelope = (frame: LiveFrame): string =>\n `{\"event\":${JSON.stringify(LIVE_EVENT)},\"data\":${encodeLiveFrame(frame)}}`;\n","/**\n * The shared keyed list-delta codec — BOTH sides of the wire implement the\n * delta contract through this one module: the server encodes a previous-vs-next\n * query result into `RowOp`s ({@link encodeListDelta}), the client merges them\n * into its cached value ({@link applyListDelta}).\n *\n * Ported from lunora's `subscription-delivery.ts` (encoder) and\n * `delta-merge.ts` (merge), with two deliberate changes:\n *\n * 1. The key field defaults to `'id'` (Vela/CRUD convention, not lunora's\n * `_id`) and is configurable per query.\n * 2. `insert` ops carry an explicit `before` anchor (the key of the row they\n * precede in the authoritative result; `null` = append) instead of\n * approximating position via a `_creationTime` heuristic. Because encoder\n * and merge live in the same package, this buys the exact-reconstruction\n * property the conformance suite enforces: whenever the encoder does not\n * bail, `applyListDelta(previous, encodeListDelta(previous, next))` is\n * deep-equal to `next`, ordering included.\n *\n * Bail-to-snapshot contract (identical on both sides — the server MUST send a\n * full `data` snapshot and the client MUST fall back to full replacement when\n * any of these hold):\n *\n * 1. previous or next is not an array;\n * 2. any row is not a plain object carrying a string key (or the value cannot\n * be JSON-serialized);\n * 3. a duplicate key appears in either array;\n * 4. rows present in BOTH arrays changed relative order (the merge replaces\n * survivors in place and never reorders them);\n * 5. the op count exceeds the next array's length (a near-total change is\n * cheaper as a snapshot).\n *\n * Op ordering inside a delta: deletes first (previous order), then\n * inserts/updates (next order) — the merge never sees a transient over-length\n * list. Merging is idempotent (`insert` on an existing key replaces in place,\n * `delete` of an absent key is a no-op) so at-least-once replay after a\n * reconnect is harmless.\n */\n\nimport type { RowOp } from './frames';\n\n/** Default row-identity field. Per-query override rides the `sub` frame's `key`. */\nexport const DEFAULT_KEY_FIELD = 'id';\n\ntype Row = Record<string, unknown>;\n\ninterface RowIndex {\n byKey: Map<string, Row>;\n order: string[];\n}\n\nconst isPlainObject = (value: unknown): value is Row =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst readRowKey = (row: unknown, keyField: string): string | undefined => {\n if (!isPlainObject(row)) return undefined;\n const key = row[keyField];\n return typeof key === 'string' ? key : undefined;\n};\n\n/**\n * Index rows by key preserving order; `undefined` the moment any row is\n * unkeyable or a key repeats (bail rules 2 and 3 — a duplicated key cannot be\n * expressed as keyed deltas without silently collapsing rows).\n */\nconst indexRows = (rows: unknown[], keyField: string): RowIndex | undefined => {\n const byKey = new Map<string, Row>();\n const order: string[] = [];\n for (const row of rows) {\n const key = readRowKey(row, keyField);\n if (key === undefined || byKey.has(key)) return undefined;\n byKey.set(key, row as Row);\n order.push(key);\n }\n return { byKey, order };\n};\n\n/**\n * True when rows present in BOTH lists keep the same relative order (bail\n * rule 4): the merge updates survivors in place and never reorders them, so a\n * survivor that moved cannot be expressed as deltas.\n */\nconst survivorsKeepOrder = (previous: RowIndex, next: RowIndex): boolean => {\n const survivingPrevious = previous.order.filter((key) => next.byKey.has(key));\n const survivingNext = next.order.filter((key) => previous.byKey.has(key));\n if (survivingPrevious.length !== survivingNext.length) return false;\n return survivingPrevious.every((key, index) => survivingNext[index] === key);\n};\n\n/**\n * Diff `previous` vs `next` into row ops, or `undefined` when any bail rule\n * holds and the caller must send a full snapshot instead.\n *\n * An empty array is a valid result (no row-level change — typically the server\n * catches byte-identical results earlier and sends `settled` instead).\n */\nexport const encodeListDelta = (\n previous: unknown,\n next: unknown,\n keyField: string = DEFAULT_KEY_FIELD,\n): RowOp[] | undefined => {\n try {\n if (!Array.isArray(previous) || !Array.isArray(next)) return undefined;\n\n const previousIndex = indexRows(previous, keyField);\n const nextIndex = indexRows(next, keyField);\n if (previousIndex === undefined || nextIndex === undefined) return undefined;\n if (!survivorsKeepOrder(previousIndex, nextIndex)) return undefined;\n\n const ops: RowOp[] = [];\n\n // Deletes first, in previous order.\n for (const key of previousIndex.order) {\n if (!nextIndex.byKey.has(key)) ops.push({ op: 'delete', key });\n }\n\n // The `before` anchor for an insert at position i is the nearest FOLLOWING\n // survivor in next order (null = append). At merge time, when the insert\n // applies, the list holds exactly the survivors (in order, updates replace\n // in place) plus earlier inserts; splicing sequentially before the anchor\n // therefore reproduces next's ordering exactly — inserts sharing an anchor\n // stack in emission order, trailing inserts append in emission order.\n const followingSurvivor: (string | null)[] = new Array(nextIndex.order.length);\n let anchor: string | null = null;\n for (let index = nextIndex.order.length - 1; index >= 0; index -= 1) {\n followingSurvivor[index] = anchor;\n const key = nextIndex.order[index] as string;\n if (previousIndex.byKey.has(key)) anchor = key;\n }\n\n // Inserts/updates in next order. Each row is fingerprinted with a single\n // JSON.stringify reused for the changed-row compare; an unserializable row\n // throws and the whole encode bails to snapshot (rule 2).\n for (const [index, key] of nextIndex.order.entries()) {\n const nextRow = nextIndex.byKey.get(key) as Row;\n const previousRow = previousIndex.byKey.get(key);\n const nextFingerprint = JSON.stringify(nextRow);\n if (previousRow === undefined) {\n ops.push({ op: 'insert', key, row: nextRow, before: followingSurvivor[index] ?? null });\n continue;\n }\n if (JSON.stringify(previousRow) !== nextFingerprint) {\n ops.push({ op: 'update', key, row: nextRow });\n }\n }\n\n // Bail rule 5: a near-total change is better sent as one snapshot.\n if (ops.length > next.length) return undefined;\n\n return ops;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Merge row ops into a cached array result, returning a NEW array (the input\n * is never mutated), or `undefined` when the ops cannot be applied cleanly —\n * the caller then falls back to full replacement and lets the next snapshot\n * reconcile.\n *\n * Idempotent by construction: replaying an op after a snapshot already\n * delivered its effect changes nothing.\n */\nexport const applyListDelta = (\n current: unknown,\n ops: readonly RowOp[],\n keyField: string = DEFAULT_KEY_FIELD,\n): unknown[] | undefined => {\n if (!Array.isArray(current)) return undefined;\n\n const rows: Row[] = [];\n const seen = new Set<string>();\n for (const element of current) {\n const key = readRowKey(element, keyField);\n if (key === undefined || seen.has(key)) return undefined;\n seen.add(key);\n rows.push(element as Row);\n }\n\n let next = [...rows];\n for (const op of ops) {\n const existingIndex = next.findIndex((row) => row[keyField] === op.key);\n\n if (op.op === 'delete') {\n if (existingIndex !== -1) next.splice(existingIndex, 1);\n continue;\n }\n\n if (existingIndex !== -1) {\n // Present → replace in place. Covers `update`, and an `insert` whose row\n // a snapshot already delivered (replay idempotency).\n next[existingIndex] = op.row;\n continue;\n }\n\n if (op.op === 'insert' && op.before !== null) {\n const anchorIndex = next.findIndex((row) => row[keyField] === op.before);\n if (anchorIndex !== -1) {\n next.splice(anchorIndex, 0, op.row);\n continue;\n }\n }\n\n // `insert` with a null/missing anchor, or an `update` for a row this page\n // never held (degraded replay) → append.\n next = [...next, op.row];\n }\n\n return next;\n};\n","/**\n * Golden wire fixtures — the drift tripwire. The server suite (@velajs/vela)\n * and the client suite (@velajs/client) both run these through\n * `runProtocolConformance`, so an encoding change on either side fails a test\n * instead of surfacing as a production incompatibility.\n *\n * `wire` strings are byte-exact: they pin the canonical key order of\n * `encodeLiveEnvelope`. Do not reformat them.\n */\n\nimport type { LiveFrame, RowOp } from './frames';\n\nexport interface FrameFixture {\n name: string;\n frame: LiveFrame;\n /** Exact canonical envelope bytes: `encodeLiveEnvelope(frame)` must equal this. */\n wire: string;\n}\n\nexport const FRAME_FIXTURES: FrameFixture[] = [\n {\n name: 'sub (full)',\n frame: {\n t: 'sub',\n sub: 's1',\n query: 'todos.list',\n args: { listId: 'l1' },\n sinceCursor: 42,\n sinceEpoch: 'e-1',\n key: 'id',\n v: 1,\n },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"sub\",\"sub\":\"s1\",\"query\":\"todos.list\",\"args\":{\"listId\":\"l1\"},\"sinceCursor\":42,\"sinceEpoch\":\"e-1\",\"key\":\"id\",\"v\":1}}',\n },\n {\n name: 'sub (minimal)',\n frame: { t: 'sub', sub: 's2', query: 'todos.all' },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"sub\",\"sub\":\"s2\",\"query\":\"todos.all\"}}',\n },\n {\n name: 'unsub',\n frame: { t: 'unsub', sub: 's1' },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"unsub\",\"sub\":\"s1\"}}',\n },\n {\n name: 'presence',\n frame: { t: 'presence', room: 'r1', meta: { name: 'kauan' } },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"presence\",\"room\":\"r1\",\"meta\":{\"name\":\"kauan\"}}}',\n },\n {\n name: 'ack',\n frame: { t: 'ack', sub: 's1' },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"ack\",\"sub\":\"s1\"}}',\n },\n {\n name: 'data',\n frame: { t: 'data', sub: 's1', snapshot: [{ id: 'a', text: 'hi' }], cursor: 7, epoch: 'e-1' },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"data\",\"sub\":\"s1\",\"snapshot\":[{\"id\":\"a\",\"text\":\"hi\"}],\"cursor\":7,\"epoch\":\"e-1\"}}',\n },\n {\n name: 'data (cold, no cursor)',\n frame: { t: 'data', sub: 's1', snapshot: null },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"data\",\"sub\":\"s1\",\"snapshot\":null}}',\n },\n {\n name: 'delta',\n frame: {\n t: 'delta',\n sub: 's1',\n ops: [\n { op: 'delete', key: 'a' },\n { op: 'insert', key: 'b', row: { id: 'b' }, before: null },\n { op: 'update', key: 'c', row: { id: 'c', n: 2 } },\n ],\n cursor: 8,\n epoch: 'e-1',\n },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"delta\",\"sub\":\"s1\",\"ops\":[{\"op\":\"delete\",\"key\":\"a\"},{\"op\":\"insert\",\"key\":\"b\",\"row\":{\"id\":\"b\"},\"before\":null},{\"op\":\"update\",\"key\":\"c\",\"row\":{\"id\":\"c\",\"n\":2}}],\"cursor\":8,\"epoch\":\"e-1\"}}',\n },\n {\n name: 'settled',\n frame: { t: 'settled', sub: 's1', cursor: 9, epoch: 'e-1' },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"settled\",\"sub\":\"s1\",\"cursor\":9,\"epoch\":\"e-1\"}}',\n },\n {\n name: 'resume',\n frame: { t: 'resume', sub: 's1', cursor: 42, epoch: 'e-1' },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"resume\",\"sub\":\"s1\",\"cursor\":42,\"epoch\":\"e-1\"}}',\n },\n {\n name: 'error (subscription)',\n frame: { t: 'error', sub: 's1', code: 'forbidden', message: 'nope', fatal: true },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"error\",\"sub\":\"s1\",\"code\":\"forbidden\",\"message\":\"nope\",\"fatal\":true}}',\n },\n {\n name: 'error (connection)',\n frame: { t: 'error', code: 'unsupported_protocol', message: 'v2 required', fatal: true },\n wire: '{\"event\":\"$live\",\"data\":{\"t\":\"error\",\"code\":\"unsupported_protocol\",\"message\":\"v2 required\",\"fatal\":true}}',\n },\n];\n\nexport interface DeltaFixture {\n name: string;\n previous: unknown;\n next: unknown;\n keyField?: string;\n /** Expected ops, or `null` when the encoder MUST bail to snapshot. */\n expected: RowOp[] | null;\n}\n\nexport const DELTA_FIXTURES: DeltaFixture[] = [\n { name: 'noop', previous: [], next: [], expected: [] },\n {\n name: 'insert into empty',\n previous: [],\n next: [{ id: 'a', n: 1 }],\n expected: [{ op: 'insert', key: 'a', row: { id: 'a', n: 1 }, before: null }],\n },\n {\n name: 'insert head',\n previous: [{ id: 'b' }],\n next: [{ id: 'a' }, { id: 'b' }],\n expected: [{ op: 'insert', key: 'a', row: { id: 'a' }, before: 'b' }],\n },\n {\n name: 'insert middle',\n previous: [{ id: 'a' }, { id: 'c' }],\n next: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],\n expected: [{ op: 'insert', key: 'b', row: { id: 'b' }, before: 'c' }],\n },\n {\n name: 'insert tail',\n previous: [{ id: 'a' }],\n next: [{ id: 'a' }, { id: 'b' }],\n expected: [{ op: 'insert', key: 'b', row: { id: 'b' }, before: null }],\n },\n {\n name: 'update in place',\n previous: [{ id: 'a', n: 1 }],\n next: [{ id: 'a', n: 2 }],\n expected: [{ op: 'update', key: 'a', row: { id: 'a', n: 2 } }],\n },\n {\n name: 'delete one',\n previous: [{ id: 'a' }, { id: 'b' }],\n next: [{ id: 'a' }],\n expected: [{ op: 'delete', key: 'b' }],\n },\n {\n name: 'mixed delete+update+insert',\n previous: [\n { id: 'a', n: 1 },\n { id: 'b', n: 1 },\n { id: 'c', n: 1 },\n ],\n next: [\n { id: 'b', n: 2 },\n { id: 'd', n: 1 },\n { id: 'c', n: 1 },\n ],\n expected: [\n { op: 'delete', key: 'a' },\n { op: 'update', key: 'b', row: { id: 'b', n: 2 } },\n { op: 'insert', key: 'd', row: { id: 'd', n: 1 }, before: 'c' },\n ],\n },\n {\n name: 'stacked inserts share an anchor in next order',\n previous: [{ id: 'z' }],\n next: [{ id: 'x' }, { id: 'y' }, { id: 'z' }],\n expected: [\n { op: 'insert', key: 'x', row: { id: 'x' }, before: 'z' },\n { op: 'insert', key: 'y', row: { id: 'y' }, before: 'z' },\n ],\n },\n {\n name: 'custom key field',\n previous: [{ _key: 'a', n: 1 }],\n next: [{ _key: 'a', n: 2 }],\n keyField: '_key',\n expected: [{ op: 'update', key: 'a', row: { _key: 'a', n: 2 } }],\n },\n // ---- bail cases (expected: null → full snapshot) ----\n {\n name: 'bail: clear list (rule 5)',\n previous: [{ id: 'a' }, { id: 'b' }],\n next: [],\n expected: null,\n },\n {\n name: 'bail: near-total change (rule 5)',\n previous: [{ id: 'a' }, { id: 'b' }],\n next: [{ id: 'c' }, { id: 'd' }, { id: 'e' }],\n expected: null,\n },\n {\n name: 'bail: previous not array (rule 1)',\n previous: { id: 'a' },\n next: [{ id: 'a' }],\n expected: null,\n },\n {\n name: 'bail: next not array (rule 1)',\n previous: [{ id: 'a' }],\n next: { id: 'a' },\n expected: null,\n },\n {\n name: 'bail: row missing key (rule 2)',\n previous: [{ id: 'a' }],\n next: [{ text: 'no key' }],\n expected: null,\n },\n {\n name: 'bail: non-string key (rule 2)',\n previous: [{ id: 'a' }],\n next: [{ id: 5 }],\n expected: null,\n },\n { name: 'bail: scalar row (rule 2)', previous: [{ id: 'a' }], next: ['a'], expected: null },\n {\n name: 'bail: duplicate key in previous (rule 3)',\n previous: [{ id: 'a' }, { id: 'a' }],\n next: [{ id: 'a' }],\n expected: null,\n },\n {\n name: 'bail: duplicate key in next (rule 3)',\n previous: [{ id: 'a' }],\n next: [{ id: 'a' }, { id: 'a' }],\n expected: null,\n },\n {\n name: 'bail: survivors reordered (rule 4)',\n previous: [{ id: 'a' }, { id: 'b' }],\n next: [{ id: 'b' }, { id: 'a' }],\n expected: null,\n },\n];\n","/**\n * The protocol conformance runner. Both wire endpoints run this in their own\n * test suites (see `@velajs/testing`'s live harness) so a codec that drifts\n * from the golden fixtures — or from the shared delta semantics — fails a test\n * on the offending side.\n *\n * Checks, in order:\n * 1. Frame encoding: `encodeLiveEnvelope(fixture.frame)` is byte-identical to\n * the pinned wire string, the wire parses back to a guard-recognized frame,\n * and `readLiveEnvelope` extracts it.\n * 2. Delta fixtures: the codec's `encodeListDelta` produces exactly the pinned\n * ops (or bails where the fixture says it must), and for every mergeable\n * fixture `applyListDelta` reconstructs `next` exactly — then reapplying\n * the same ops changes nothing (at-least-once replay idempotency).\n * 3. A seeded randomized sweep of generated list pairs asserting the\n * exact-reconstruction property on cases the fixtures don't enumerate.\n */\n\nimport { DEFAULT_KEY_FIELD, applyListDelta, encodeListDelta } from './delta';\nimport {\n encodeLiveEnvelope,\n isClientLiveFrame,\n isServerLiveFrame,\n readLiveEnvelope,\n} from './frames';\nimport type { RowOp } from './frames';\nimport { DELTA_FIXTURES, FRAME_FIXTURES } from './fixtures';\n\n/** The two halves a wire endpoint must implement compatibly. */\nexport interface DeltaCodec {\n encodeListDelta: (previous: unknown, next: unknown, keyField?: string) => RowOp[] | undefined;\n applyListDelta: (\n current: unknown,\n ops: readonly RowOp[],\n keyField?: string,\n ) => unknown[] | undefined;\n}\n\nconst REFERENCE_CODEC: DeltaCodec = { encodeListDelta, applyListDelta };\n\nconst deepEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;\n return a.every((value, index) => deepEqual(value, b[index]));\n }\n if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(\n (key) =>\n key in (b as Record<string, unknown>) &&\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n }\n return false;\n};\n\n/** Deterministic LCG so the randomized sweep is reproducible (no Math.random). */\nconst makeRandom = (seed: number): (() => number) => {\n let state = seed >>> 0;\n return () => {\n state = (state * 1664525 + 1013904223) >>> 0;\n return state / 0x1_0000_0000;\n };\n};\n\ninterface GeneratedCase {\n previous: Record<string, unknown>[];\n next: Record<string, unknown>[];\n}\n\n/**\n * Generate a mergeable previous/next pair: start from a random keyed list,\n * then delete a random subset, update random payloads, and insert fresh keys\n * at random positions — survivor order is preserved by construction, so the\n * encoder may only bail via the op-count cap (rule 5).\n */\nconst generateCase = (random: () => number, caseIndex: number): GeneratedCase => {\n const previousLength = Math.floor(random() * 8);\n const previous: Record<string, unknown>[] = [];\n for (let index = 0; index < previousLength; index += 1) {\n previous.push({ id: `k${caseIndex}-${index}`, n: Math.floor(random() * 100) });\n }\n\n const next: Record<string, unknown>[] = [];\n for (const row of previous) {\n if (random() < 0.25) continue; // delete\n next.push(random() < 0.4 ? { ...row, n: Math.floor(random() * 100) } : row);\n }\n const insertions = Math.floor(random() * 4);\n for (let index = 0; index < insertions; index += 1) {\n const position = Math.floor(random() * (next.length + 1));\n next.splice(position, 0, { id: `f${caseIndex}-${index}`, n: Math.floor(random() * 100) });\n }\n\n return { previous, next };\n};\n\nexport interface ConformanceReport {\n /** Human-readable failure descriptions; empty = conformant. */\n failures: string[];\n checks: number;\n}\n\n/**\n * Run the full conformance suite against a codec (defaults to the reference\n * codec in this package — the package's own tests run exactly this).\n */\nexport const runProtocolConformance = (codec: DeltaCodec = REFERENCE_CODEC): ConformanceReport => {\n const failures: string[] = [];\n let checks = 0;\n\n for (const fixture of FRAME_FIXTURES) {\n checks += 1;\n const encoded = encodeLiveEnvelope(fixture.frame);\n if (encoded !== fixture.wire) {\n failures.push(\n `frame \"${fixture.name}\": encoded wire differs\\n expected ${fixture.wire}\\n actual ${encoded}`,\n );\n continue;\n }\n const frame = readLiveEnvelope(JSON.parse(fixture.wire));\n if (frame === undefined) {\n failures.push(`frame \"${fixture.name}\": readLiveEnvelope did not recognize the envelope`);\n continue;\n }\n if (!isClientLiveFrame(frame) && !isServerLiveFrame(frame)) {\n failures.push(`frame \"${fixture.name}\": decoded frame not recognized by either guard`);\n }\n }\n\n for (const fixture of DELTA_FIXTURES) {\n checks += 1;\n const keyField = fixture.keyField ?? DEFAULT_KEY_FIELD;\n const ops = codec.encodeListDelta(fixture.previous, fixture.next, keyField);\n\n if (fixture.expected === null) {\n if (ops !== undefined) {\n failures.push(\n `delta \"${fixture.name}\": expected bail-to-snapshot, got ${JSON.stringify(ops)}`,\n );\n }\n continue;\n }\n\n if (ops === undefined) {\n failures.push(\n `delta \"${fixture.name}\": encoder bailed, expected ${JSON.stringify(fixture.expected)}`,\n );\n continue;\n }\n if (!deepEqual(ops, fixture.expected)) {\n failures.push(\n `delta \"${fixture.name}\": ops differ\\n expected ${JSON.stringify(fixture.expected)}\\n actual ${JSON.stringify(ops)}`,\n );\n continue;\n }\n\n const merged = codec.applyListDelta(fixture.previous, ops, keyField);\n if (merged === undefined || !deepEqual(merged, fixture.next)) {\n failures.push(\n `delta \"${fixture.name}\": apply(previous, ops) did not reconstruct next\\n expected ${JSON.stringify(fixture.next)}\\n actual ${JSON.stringify(merged)}`,\n );\n continue;\n }\n\n const replayed = codec.applyListDelta(merged, ops, keyField);\n if (replayed === undefined || !deepEqual(replayed, fixture.next)) {\n failures.push(`delta \"${fixture.name}\": replaying the same ops was not idempotent`);\n }\n }\n\n const random = makeRandom(0x5eed);\n for (let caseIndex = 0; caseIndex < 250; caseIndex += 1) {\n checks += 1;\n const { previous, next } = generateCase(random, caseIndex);\n const ops = codec.encodeListDelta(previous, next);\n\n if (ops === undefined) {\n // Survivor order is preserved by construction, so only rule 5 may bail.\n const referenceOps = encodeListDelta(previous, next);\n if (referenceOps !== undefined) {\n failures.push(`random #${caseIndex}: codec bailed where the reference codec succeeds`);\n }\n continue;\n }\n\n const merged = codec.applyListDelta(previous, ops);\n if (merged === undefined || !deepEqual(merged, next)) {\n failures.push(\n `random #${caseIndex}: apply(previous, ops) != next\\n previous ${JSON.stringify(previous)}\\n next ${JSON.stringify(next)}\\n ops ${JSON.stringify(ops)}\\n merged ${JSON.stringify(merged)}`,\n );\n }\n }\n\n return { failures, checks };\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,55 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/live-protocol",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Normative wire protocol for Vela live queries: frame types, the shared keyed-delta codec, and golden conformance fixtures",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist",
|
|
16
|
-
"README.md",
|
|
17
|
-
"LICENSE",
|
|
18
|
-
"CHANGELOG.md"
|
|
19
|
-
],
|
|
20
|
-
"sideEffects": false,
|
|
21
5
|
"keywords": [
|
|
22
|
-
"
|
|
6
|
+
"delta",
|
|
7
|
+
"framework",
|
|
23
8
|
"live-queries",
|
|
24
9
|
"realtime",
|
|
25
|
-
"
|
|
26
|
-
"delta",
|
|
10
|
+
"vela",
|
|
27
11
|
"websocket",
|
|
28
|
-
"
|
|
12
|
+
"wire-protocol"
|
|
29
13
|
],
|
|
30
|
-
"
|
|
14
|
+
"homepage": "https://github.com/velajs/live-protocol#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/velajs/live-protocol/issues"
|
|
17
|
+
},
|
|
31
18
|
"license": "MIT",
|
|
19
|
+
"author": "ksh",
|
|
32
20
|
"repository": {
|
|
33
21
|
"type": "git",
|
|
34
22
|
"url": "git+https://github.com/velajs/live-protocol.git"
|
|
35
23
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
42
39
|
},
|
|
43
40
|
"devDependencies": {
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
41
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
42
|
+
"@changesets/cli": "^2.31.0",
|
|
43
|
+
"oxfmt": "^0.58.0",
|
|
44
|
+
"oxlint": "^1.73.0",
|
|
45
|
+
"publint": "^0.3.21",
|
|
46
|
+
"tsdown": "^0.22.4",
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"vitest": "^4.1.10"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=24"
|
|
49
52
|
},
|
|
50
53
|
"scripts": {
|
|
51
|
-
"build": "
|
|
54
|
+
"build": "tsdown",
|
|
52
55
|
"test": "vitest run",
|
|
53
|
-
"typecheck": "tsc --noEmit"
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"lint": "oxlint .",
|
|
58
|
+
"format": "oxfmt .",
|
|
59
|
+
"format:check": "oxfmt --check .",
|
|
60
|
+
"publint": "publint",
|
|
61
|
+
"attw": "attw --pack . --profile esm-only",
|
|
62
|
+
"changeset": "changeset",
|
|
63
|
+
"version-packages": "changeset version",
|
|
64
|
+
"release": "pnpm build && changeset publish",
|
|
65
|
+
"verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm publint && pnpm attw"
|
|
54
66
|
}
|
|
55
67
|
}
|
package/dist/conformance.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The protocol conformance runner. Both wire endpoints run this in their own
|
|
3
|
-
* test suites (see `@velajs/testing`'s live harness) so a codec that drifts
|
|
4
|
-
* from the golden fixtures — or from the shared delta semantics — fails a test
|
|
5
|
-
* on the offending side.
|
|
6
|
-
*
|
|
7
|
-
* Checks, in order:
|
|
8
|
-
* 1. Frame encoding: `encodeLiveEnvelope(fixture.frame)` is byte-identical to
|
|
9
|
-
* the pinned wire string, the wire parses back to a guard-recognized frame,
|
|
10
|
-
* and `readLiveEnvelope` extracts it.
|
|
11
|
-
* 2. Delta fixtures: the codec's `encodeListDelta` produces exactly the pinned
|
|
12
|
-
* ops (or bails where the fixture says it must), and for every mergeable
|
|
13
|
-
* fixture `applyListDelta` reconstructs `next` exactly — then reapplying
|
|
14
|
-
* the same ops changes nothing (at-least-once replay idempotency).
|
|
15
|
-
* 3. A seeded randomized sweep of generated list pairs asserting the
|
|
16
|
-
* exact-reconstruction property on cases the fixtures don't enumerate.
|
|
17
|
-
*/
|
|
18
|
-
import type { RowOp } from './frames';
|
|
19
|
-
/** The two halves a wire endpoint must implement compatibly. */
|
|
20
|
-
export interface DeltaCodec {
|
|
21
|
-
encodeListDelta: (previous: unknown, next: unknown, keyField?: string) => RowOp[] | undefined;
|
|
22
|
-
applyListDelta: (current: unknown, ops: readonly RowOp[], keyField?: string) => unknown[] | undefined;
|
|
23
|
-
}
|
|
24
|
-
export interface ConformanceReport {
|
|
25
|
-
/** Human-readable failure descriptions; empty = conformant. */
|
|
26
|
-
failures: string[];
|
|
27
|
-
checks: number;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Run the full conformance suite against a codec (defaults to the reference
|
|
31
|
-
* codec in this package — the package's own tests run exactly this).
|
|
32
|
-
*/
|
|
33
|
-
export declare const runProtocolConformance: (codec?: DeltaCodec) => ConformanceReport;
|
package/dist/conformance.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The protocol conformance runner. Both wire endpoints run this in their own
|
|
3
|
-
* test suites (see `@velajs/testing`'s live harness) so a codec that drifts
|
|
4
|
-
* from the golden fixtures — or from the shared delta semantics — fails a test
|
|
5
|
-
* on the offending side.
|
|
6
|
-
*
|
|
7
|
-
* Checks, in order:
|
|
8
|
-
* 1. Frame encoding: `encodeLiveEnvelope(fixture.frame)` is byte-identical to
|
|
9
|
-
* the pinned wire string, the wire parses back to a guard-recognized frame,
|
|
10
|
-
* and `readLiveEnvelope` extracts it.
|
|
11
|
-
* 2. Delta fixtures: the codec's `encodeListDelta` produces exactly the pinned
|
|
12
|
-
* ops (or bails where the fixture says it must), and for every mergeable
|
|
13
|
-
* fixture `applyListDelta` reconstructs `next` exactly — then reapplying
|
|
14
|
-
* the same ops changes nothing (at-least-once replay idempotency).
|
|
15
|
-
* 3. A seeded randomized sweep of generated list pairs asserting the
|
|
16
|
-
* exact-reconstruction property on cases the fixtures don't enumerate.
|
|
17
|
-
*/ import { DEFAULT_KEY_FIELD, applyListDelta, encodeListDelta } from "./delta.js";
|
|
18
|
-
import { encodeLiveEnvelope, isClientLiveFrame, isServerLiveFrame, readLiveEnvelope } from "./frames.js";
|
|
19
|
-
import { DELTA_FIXTURES, FRAME_FIXTURES } from "./fixtures.js";
|
|
20
|
-
const REFERENCE_CODEC = {
|
|
21
|
-
encodeListDelta,
|
|
22
|
-
applyListDelta
|
|
23
|
-
};
|
|
24
|
-
const deepEqual = (a, b)=>{
|
|
25
|
-
if (a === b) return true;
|
|
26
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
27
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
28
|
-
return a.every((value, index)=>deepEqual(value, b[index]));
|
|
29
|
-
}
|
|
30
|
-
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
31
|
-
const aKeys = Object.keys(a);
|
|
32
|
-
const bKeys = Object.keys(b);
|
|
33
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
34
|
-
return aKeys.every((key)=>key in b && deepEqual(a[key], b[key]));
|
|
35
|
-
}
|
|
36
|
-
return false;
|
|
37
|
-
};
|
|
38
|
-
/** Deterministic LCG so the randomized sweep is reproducible (no Math.random). */ const makeRandom = (seed)=>{
|
|
39
|
-
let state = seed >>> 0;
|
|
40
|
-
return ()=>{
|
|
41
|
-
state = state * 1664525 + 1013904223 >>> 0;
|
|
42
|
-
return state / 0x1_0000_0000;
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* Generate a mergeable previous/next pair: start from a random keyed list,
|
|
47
|
-
* then delete a random subset, update random payloads, and insert fresh keys
|
|
48
|
-
* at random positions — survivor order is preserved by construction, so the
|
|
49
|
-
* encoder may only bail via the op-count cap (rule 5).
|
|
50
|
-
*/ const generateCase = (random, caseIndex)=>{
|
|
51
|
-
const previousLength = Math.floor(random() * 8);
|
|
52
|
-
const previous = [];
|
|
53
|
-
for(let index = 0; index < previousLength; index += 1){
|
|
54
|
-
previous.push({
|
|
55
|
-
id: `k${caseIndex}-${index}`,
|
|
56
|
-
n: Math.floor(random() * 100)
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const next = [];
|
|
60
|
-
for (const row of previous){
|
|
61
|
-
if (random() < 0.25) continue; // delete
|
|
62
|
-
next.push(random() < 0.4 ? {
|
|
63
|
-
...row,
|
|
64
|
-
n: Math.floor(random() * 100)
|
|
65
|
-
} : row);
|
|
66
|
-
}
|
|
67
|
-
const insertions = Math.floor(random() * 4);
|
|
68
|
-
for(let index = 0; index < insertions; index += 1){
|
|
69
|
-
const position = Math.floor(random() * (next.length + 1));
|
|
70
|
-
next.splice(position, 0, {
|
|
71
|
-
id: `f${caseIndex}-${index}`,
|
|
72
|
-
n: Math.floor(random() * 100)
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
previous,
|
|
77
|
-
next
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
/**
|
|
81
|
-
* Run the full conformance suite against a codec (defaults to the reference
|
|
82
|
-
* codec in this package — the package's own tests run exactly this).
|
|
83
|
-
*/ export const runProtocolConformance = (codec = REFERENCE_CODEC)=>{
|
|
84
|
-
const failures = [];
|
|
85
|
-
let checks = 0;
|
|
86
|
-
for (const fixture of FRAME_FIXTURES){
|
|
87
|
-
checks += 1;
|
|
88
|
-
const encoded = encodeLiveEnvelope(fixture.frame);
|
|
89
|
-
if (encoded !== fixture.wire) {
|
|
90
|
-
failures.push(`frame "${fixture.name}": encoded wire differs\n expected ${fixture.wire}\n actual ${encoded}`);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const frame = readLiveEnvelope(JSON.parse(fixture.wire));
|
|
94
|
-
if (frame === undefined) {
|
|
95
|
-
failures.push(`frame "${fixture.name}": readLiveEnvelope did not recognize the envelope`);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (!isClientLiveFrame(frame) && !isServerLiveFrame(frame)) {
|
|
99
|
-
failures.push(`frame "${fixture.name}": decoded frame not recognized by either guard`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
for (const fixture of DELTA_FIXTURES){
|
|
103
|
-
checks += 1;
|
|
104
|
-
const keyField = fixture.keyField ?? DEFAULT_KEY_FIELD;
|
|
105
|
-
const ops = codec.encodeListDelta(fixture.previous, fixture.next, keyField);
|
|
106
|
-
if (fixture.expected === null) {
|
|
107
|
-
if (ops !== undefined) {
|
|
108
|
-
failures.push(`delta "${fixture.name}": expected bail-to-snapshot, got ${JSON.stringify(ops)}`);
|
|
109
|
-
}
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (ops === undefined) {
|
|
113
|
-
failures.push(`delta "${fixture.name}": encoder bailed, expected ${JSON.stringify(fixture.expected)}`);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (!deepEqual(ops, fixture.expected)) {
|
|
117
|
-
failures.push(`delta "${fixture.name}": ops differ\n expected ${JSON.stringify(fixture.expected)}\n actual ${JSON.stringify(ops)}`);
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const merged = codec.applyListDelta(fixture.previous, ops, keyField);
|
|
121
|
-
if (merged === undefined || !deepEqual(merged, fixture.next)) {
|
|
122
|
-
failures.push(`delta "${fixture.name}": apply(previous, ops) did not reconstruct next\n expected ${JSON.stringify(fixture.next)}\n actual ${JSON.stringify(merged)}`);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
const replayed = codec.applyListDelta(merged, ops, keyField);
|
|
126
|
-
if (replayed === undefined || !deepEqual(replayed, fixture.next)) {
|
|
127
|
-
failures.push(`delta "${fixture.name}": replaying the same ops was not idempotent`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const random = makeRandom(0x5eed);
|
|
131
|
-
for(let caseIndex = 0; caseIndex < 250; caseIndex += 1){
|
|
132
|
-
checks += 1;
|
|
133
|
-
const { previous, next } = generateCase(random, caseIndex);
|
|
134
|
-
const ops = codec.encodeListDelta(previous, next);
|
|
135
|
-
if (ops === undefined) {
|
|
136
|
-
// Survivor order is preserved by construction, so only rule 5 may bail.
|
|
137
|
-
const referenceOps = encodeListDelta(previous, next);
|
|
138
|
-
if (referenceOps !== undefined) {
|
|
139
|
-
failures.push(`random #${caseIndex}: codec bailed where the reference codec succeeds`);
|
|
140
|
-
}
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const merged = codec.applyListDelta(previous, ops);
|
|
144
|
-
if (merged === undefined || !deepEqual(merged, next)) {
|
|
145
|
-
failures.push(`random #${caseIndex}: apply(previous, ops) != next\n previous ${JSON.stringify(previous)}\n next ${JSON.stringify(next)}\n ops ${JSON.stringify(ops)}\n merged ${JSON.stringify(merged)}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
failures,
|
|
150
|
-
checks
|
|
151
|
-
};
|
|
152
|
-
};
|
package/dist/delta.d.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared keyed list-delta codec — BOTH sides of the wire implement the
|
|
3
|
-
* delta contract through this one module: the server encodes a previous-vs-next
|
|
4
|
-
* query result into `RowOp`s ({@link encodeListDelta}), the client merges them
|
|
5
|
-
* into its cached value ({@link applyListDelta}).
|
|
6
|
-
*
|
|
7
|
-
* Ported from lunora's `subscription-delivery.ts` (encoder) and
|
|
8
|
-
* `delta-merge.ts` (merge), with two deliberate changes:
|
|
9
|
-
*
|
|
10
|
-
* 1. The key field defaults to `'id'` (Vela/CRUD convention, not lunora's
|
|
11
|
-
* `_id`) and is configurable per query.
|
|
12
|
-
* 2. `insert` ops carry an explicit `before` anchor (the key of the row they
|
|
13
|
-
* precede in the authoritative result; `null` = append) instead of
|
|
14
|
-
* approximating position via a `_creationTime` heuristic. Because encoder
|
|
15
|
-
* and merge live in the same package, this buys the exact-reconstruction
|
|
16
|
-
* property the conformance suite enforces: whenever the encoder does not
|
|
17
|
-
* bail, `applyListDelta(previous, encodeListDelta(previous, next))` is
|
|
18
|
-
* deep-equal to `next`, ordering included.
|
|
19
|
-
*
|
|
20
|
-
* Bail-to-snapshot contract (identical on both sides — the server MUST send a
|
|
21
|
-
* full `data` snapshot and the client MUST fall back to full replacement when
|
|
22
|
-
* any of these hold):
|
|
23
|
-
*
|
|
24
|
-
* 1. previous or next is not an array;
|
|
25
|
-
* 2. any row is not a plain object carrying a string key (or the value cannot
|
|
26
|
-
* be JSON-serialized);
|
|
27
|
-
* 3. a duplicate key appears in either array;
|
|
28
|
-
* 4. rows present in BOTH arrays changed relative order (the merge replaces
|
|
29
|
-
* survivors in place and never reorders them);
|
|
30
|
-
* 5. the op count exceeds the next array's length (a near-total change is
|
|
31
|
-
* cheaper as a snapshot).
|
|
32
|
-
*
|
|
33
|
-
* Op ordering inside a delta: deletes first (previous order), then
|
|
34
|
-
* inserts/updates (next order) — the merge never sees a transient over-length
|
|
35
|
-
* list. Merging is idempotent (`insert` on an existing key replaces in place,
|
|
36
|
-
* `delete` of an absent key is a no-op) so at-least-once replay after a
|
|
37
|
-
* reconnect is harmless.
|
|
38
|
-
*/
|
|
39
|
-
import type { RowOp } from './frames';
|
|
40
|
-
/** Default row-identity field. Per-query override rides the `sub` frame's `key`. */
|
|
41
|
-
export declare const DEFAULT_KEY_FIELD = "id";
|
|
42
|
-
/**
|
|
43
|
-
* Diff `previous` vs `next` into row ops, or `undefined` when any bail rule
|
|
44
|
-
* holds and the caller must send a full snapshot instead.
|
|
45
|
-
*
|
|
46
|
-
* An empty array is a valid result (no row-level change — typically the server
|
|
47
|
-
* catches byte-identical results earlier and sends `settled` instead).
|
|
48
|
-
*/
|
|
49
|
-
export declare const encodeListDelta: (previous: unknown, next: unknown, keyField?: string) => RowOp[] | undefined;
|
|
50
|
-
/**
|
|
51
|
-
* Merge row ops into a cached array result, returning a NEW array (the input
|
|
52
|
-
* is never mutated), or `undefined` when the ops cannot be applied cleanly —
|
|
53
|
-
* the caller then falls back to full replacement and lets the next snapshot
|
|
54
|
-
* reconcile.
|
|
55
|
-
*
|
|
56
|
-
* Idempotent by construction: replaying an op after a snapshot already
|
|
57
|
-
* delivered its effect changes nothing.
|
|
58
|
-
*/
|
|
59
|
-
export declare const applyListDelta: (current: unknown, ops: readonly RowOp[], keyField?: string) => unknown[] | undefined;
|