@hpx7/delta-pack
Version:
A TypeScript code generator and runtime for binary serialization based on schemas.
557 lines (556 loc) • 16.3 kB
JavaScript
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;
}