UNPKG

@hpx7/delta-pack

Version:

A TypeScript code generator and runtime for binary serialization based on schemas.

557 lines (556 loc) 16.3 kB
import { Writer, Reader } from "bin-serde"; import utf8Size from "utf8-buffer-size"; import { rleDecode, rleEncode } from "./rle"; const FLOAT_EPSILON = 0.001; export class Tracker { bitsIdx = 0; dict = []; data = []; bits; reader; constructor(bits = [], reader = new Reader(new Uint8Array())) { this.bits = bits; this.reader = reader; } static parse(buf) { const reader = new Reader(buf); const numBits = reader.readUVarint(); const rleBits = reader.readBits(numBits); const bits = rleDecode(rleBits); return new Tracker(bits, reader); } pushString(val) { if (val === "") { this.data.push({ type: "int", val: 0 }); return; } const idx = this.dict.indexOf(val); if (idx < 0) { this.dict.push(val); const len = utf8Size(val); this.data.push({ type: "string", val, len }); } else { this.data.push({ type: "int", val: -idx - 1 }); } } pushInt(val) { this.data.push({ type: "int", val }); } pushUInt(val) { this.data.push({ type: "uint", val }); } pushFloat(val) { this.data.push({ type: "float", val }); } pushFloatQuantized(val, precision) { this.pushInt(Math.round(val / precision)); } pushBoolean(val) { this.bits.push(val); } pushOptional(val, innerWrite) { this.pushBoolean(val != null); if (val != null) { innerWrite(val); } } pushArray(val, innerWrite) { this.pushUInt(val.length); for (const item of val) { innerWrite(item); } } pushRecord(val, innerKeyWrite, innerValWrite) { this.pushUInt(val.size); for (const [key, value] of val) { innerKeyWrite(key); innerValWrite(value); } } pushStringDiff(a, b) { if (!this.dict.includes(a)) { this.dict.push(a); } this.pushBoolean(a !== b); if (a !== b) { this.pushString(b); } } pushIntDiff(a, b) { this.pushBoolean(a !== b); if (a !== b) { this.pushInt(b); } } pushUIntDiff(a, b) { this.pushBoolean(a !== b); if (a !== b) { this.pushUInt(b); } } pushFloatDiff(a, b) { const changed = !equalsFloat(a, b); this.pushBoolean(changed); if (changed) { this.pushFloat(b); } } pushFloatQuantizedDiff(a, b, precision) { this.pushIntDiff(Math.round(a / precision), Math.round(b / precision)); } pushBooleanDiff(a, b) { this.pushBoolean(a !== b); } pushOptionalDiffPrimitive(a, b, encode) { if (a == null) { this.pushBoolean(b != null); if (b != null) { // null → value encode(b); } // else null → null } else { const changed = b == null || a !== b; this.pushBoolean(changed); if (changed) { this.pushBoolean(b != null); if (b != null) { // value → value encode(b); } // else value → null } } } pushOptionalDiff(a, b, encode, encodeDiff) { if (a == null) { this.pushBoolean(b != null); if (b != null) { // null → value encode(b); } // else null → null } else { this.pushBoolean(b != null); if (b != null) { // value → value encodeDiff(a, b); } // else value → null } } pushArrayDiff(a, b, equals, encode, encodeDiff) { const dirty = b._dirty; const changed = dirty != null ? dirty.size > 0 || a.length !== b.length : !equalsArray(a, b, equals); this.pushBoolean(changed); if (!changed) { return; } this.pushUInt(b.length); const minLen = Math.min(a.length, b.length); for (let i = 0; i < minLen; i++) { const elementChanged = dirty != null ? dirty.has(i) : !equals(a[i], b[i]); this.pushBoolean(elementChanged); if (elementChanged) { encodeDiff(a[i], b[i]); } } for (let i = a.length; i < b.length; i++) { encode(b[i]); } } pushRecordDiff(a, b, equals, encodeKey, encodeVal, encodeDiff) { const dirty = b._dirty; const changed = dirty != null ? dirty.size > 0 : !equalsRecord(a, b, (x, y) => x === y, equals); this.pushBoolean(changed); if (!changed) { return; } const orderedKeys = [...a.keys()].sort(); const updates = []; const deletions = []; const additions = []; if (dirty != null) { // With dirty tracking: only process dirty keys dirty.forEach((dirtyKey) => { if (a.has(dirtyKey) && b.has(dirtyKey)) { // Key exists in both - it's an update const idx = orderedKeys.indexOf(dirtyKey); updates.push(idx); } else if (!a.has(dirtyKey) && b.has(dirtyKey)) { // Key not in a - it's an addition additions.push([dirtyKey, b.get(dirtyKey)]); } else if (a.has(dirtyKey) && !b.has(dirtyKey)) { // Key in a but not in b - it's a deletion const idx = orderedKeys.indexOf(dirtyKey); deletions.push(idx); } }); } else { // Without dirty tracking: check all keys orderedKeys.forEach((aKey, i) => { if (b.has(aKey)) { if (!equals(a.get(aKey), b.get(aKey))) { updates.push(i); } } else { deletions.push(i); } }); b.forEach((bVal, bKey) => { if (!a.has(bKey)) { additions.push([bKey, bVal]); } }); } if (a.size > 0) { this.pushUInt(deletions.length); deletions.forEach((idx) => { this.pushUInt(idx); }); this.pushUInt(updates.length); updates.forEach((idx) => { this.pushUInt(idx); const key = orderedKeys[idx]; encodeDiff(a.get(key), b.get(key)); }); } this.pushUInt(additions.length); additions.forEach(([key, val]) => { encodeKey(key); encodeVal(val); }); } nextString() { const lenOrIdx = this.reader.readVarint(); if (lenOrIdx === 0) { return ""; } if (lenOrIdx > 0) { const str = this.reader.readStringUtf8(lenOrIdx); this.dict.push(str); return str; } return this.dict[-lenOrIdx - 1]; } nextInt() { return this.reader.readVarint(); } nextUInt() { return this.reader.readUVarint(); } nextFloat() { return this.reader.readFloat(); } nextFloatQuantized(precision) { return this.nextInt() * precision; } nextBoolean() { return this.bits[this.bitsIdx++]; } nextOptional(innerRead) { return this.nextBoolean() ? innerRead() : undefined; } nextArray(innerRead) { const len = this.nextUInt(); const arr = new Array(len); for (let i = 0; i < len; i++) { arr[i] = innerRead(); } return arr; } nextRecord(innerKeyRead, innerValRead) { const len = this.nextUInt(); const obj = new Map(); for (let i = 0; i < len; i++) { obj.set(innerKeyRead(), innerValRead()); } return obj; } nextStringDiff(a) { if (!this.dict.includes(a)) { this.dict.push(a); } const changed = this.nextBoolean(); return changed ? this.nextString() : a; } nextIntDiff(a) { const changed = this.nextBoolean(); return changed ? this.nextInt() : a; } nextUIntDiff(a) { const changed = this.nextBoolean(); return changed ? this.nextUInt() : a; } nextFloatDiff(a) { const changed = this.nextBoolean(); return changed ? this.nextFloat() : a; } nextFloatQuantizedDiff(a, precision) { const changed = this.nextBoolean(); return changed ? this.nextFloatQuantized(precision) : a; } nextBooleanDiff(a) { const changed = this.nextBoolean(); return changed ? !a : a; } nextOptionalDiffPrimitive(obj, decode) { if (obj == null) { const present = this.nextBoolean(); return present ? decode() : undefined; } else { const changed = this.nextBoolean(); if (!changed) { return obj; } const present = this.nextBoolean(); return present ? decode() : undefined; } } nextOptionalDiff(obj, decode, decodeDiff) { if (obj == null) { const present = this.nextBoolean(); return present ? decode() : undefined; } else { const present = this.nextBoolean(); return present ? decodeDiff(obj) : undefined; } } nextArrayDiff(arr, decode, decodeDiff) { const changed = this.nextBoolean(); if (!changed) { return arr; } const newLen = this.nextUInt(); const newArr = new Array(newLen); const minLen = Math.min(arr.length, newLen); for (let i = 0; i < minLen; i++) { const changed = this.nextBoolean(); newArr[i] = changed ? decodeDiff(arr[i]) : arr[i]; } for (let i = arr.length; i < newLen; i++) { newArr[i] = decode(); } return newArr; } nextRecordDiff(obj, decodeKey, decodeVal, decodeDiff) { const changed = this.nextBoolean(); if (!changed) { return obj; } const result = new Map(obj); const orderedKeys = [...obj.keys()].sort(); if (obj.size > 0) { const numDeletions = this.nextUInt(); for (let i = 0; i < numDeletions; i++) { const key = orderedKeys[this.nextUInt()]; result.delete(key); } const numUpdates = this.nextUInt(); for (let i = 0; i < numUpdates; i++) { const key = orderedKeys[this.nextUInt()]; result.set(key, decodeDiff(result.get(key))); } } const numAdditions = this.nextUInt(); for (let i = 0; i < numAdditions; i++) { const key = decodeKey(); const val = decodeVal(); result.set(key, val); } return result; } toBuffer() { const buf = new Writer(); const rleBits = rleEncode(this.bits); buf.writeUVarint(rleBits.length); buf.writeBits(rleBits); this.data.forEach((x) => { if (x.type === "string") { buf.writeVarint(x.len); buf.writeStringUtf8(x.val, x.len); } else if (x.type === "int") { buf.writeVarint(x.val); } else if (x.type === "uint") { buf.writeUVarint(x.val); } else if (x.type === "float") { buf.writeFloat(x.val); } }); return buf.toBuffer(); } } export function parseString(x) { if (typeof x !== "string") { throw new Error(`Invalid string: ${x}`); } return x; } export function parseInt(x) { if (typeof x === "string") { x = Number(x); } if (typeof x !== "number" || !Number.isInteger(x)) { throw new Error(`Invalid int: ${x}`); } return x; } export function parseUInt(x) { if (typeof x === "string") { x = Number(x); } if (typeof x !== "number" || !Number.isInteger(x) || x < 0) { throw new Error(`Invalid uint: ${x}`); } return x; } export function parseFloat(x) { if (typeof x === "string") { x = Number(x); } if (typeof x !== "number" || Number.isNaN(x) || !Number.isFinite(x)) { throw new Error(`Invalid float: ${x}`); } return x; } export function parseBoolean(x) { if (x === "true") { return true; } if (x === "false") { return false; } if (typeof x !== "boolean") { throw new Error(`Invalid boolean: ${x}`); } return x; } export function parseEnum(x, enumObj) { if (typeof x !== "string" || !(x in enumObj)) { throw new Error(`Invalid enum: ${x}`); } return x; } export function parseOptional(x, innerParse) { if (x == null) { return undefined; } try { return innerParse(x); } catch (err) { throw new Error(`Invalid optional: ${x}`, { cause: err }); } } export function parseArray(x, innerParse) { if (!Array.isArray(x)) { throw new Error(`Invalid array, got ${typeof x}`); } return x.map((y, i) => { try { return innerParse(y); } catch (err) { throw new Error(`Invalid array element at index ${i}: ${y}`, { cause: err }); } }); } export function parseRecord(x, innerKeyParse, innerValParse) { if (typeof x !== "object" || x == null) { throw new Error(`Invalid record, got ${typeof x}`); } const proto = Object.getPrototypeOf(x); if (proto === Object.prototype || proto === null) { x = new Map(Object.entries(x)); } if (!(x instanceof Map)) { throw new Error(`Invalid record, got ${typeof x}`); } const result = new Map(); for (const [key, val] of x) { try { result.set(innerKeyParse(key), innerValParse(val)); } catch (err) { throw new Error(`Invalid record element (${key}, ${val})`, { cause: err }); } } return result; } export function tryParseField(parseFn, key) { try { return parseFn(); } catch (err) { throw new Error(`Invalid field ${key}`, { cause: err }); } } export function equalsFloat(a, b) { return Math.abs(a - b) < FLOAT_EPSILON; } export function equalsFloatQuantized(a, b, precision) { return Math.round(a / precision) === Math.round(b / precision); } export function equalsOptional(a, b, equals) { if (a == null && b == null) { return true; } if (a != null && b != null) { return equals(a, b); } return false; } export function equalsArray(a, b, equals) { if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (!equals(a[i], b[i])) { return false; } } return true; } export function equalsRecord(a, b, keyEquals, valueEquals) { if (a.size !== b.size) { return false; } for (const [aKey, aVal] of a) { let found = false; for (const [bKey, bVal] of b) { if (keyEquals(aKey, bKey)) { if (!valueEquals(aVal, bVal)) { return false; } found = true; break; } } if (!found) { return false; } } return true; } export function mapValues(obj, fn) { return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value, key)])); } export function mapToObject(map, valueToObject) { const obj = {}; map.forEach((value, key) => { obj[String(key)] = valueToObject(value); }); return obj; }