@interweb-it/jam-codec
Version:
TypeScript implementation of the JAM codec
254 lines (253 loc) • 9.33 kB
JavaScript
const TAG_NULL = 0x00;
const TAG_BOOL = 0x01;
const TAG_INT = 0x02;
const TAG_FLOAT = 0x03;
const TAG_STRING = 0x04;
const TAG_BYTES = 0x05;
const TAG_ARRAY = 0x06;
const TAG_OBJECT = 0x07;
const TAG_ENUM = 0x08;
// ZigZag encode signed integer to unsigned
function zigzagEncode(n) {
const bigN = BigInt(n);
return (bigN << 1n) ^ (bigN >> 63n);
}
// ZigZag decode unsigned integer to signed
function zigzagDecode(n) {
return Number((n >> 1n) ^ (-(n & 1n)));
}
/**
* Write a variable-length integer to the output buffer (BigInt version)
*/
function writeVar(value, output, offset) {
let n = zigzagEncode(value);
let start = offset;
// Write 7 bits at a time, MSB is continuation bit
while (n >= 0x80n) {
output[offset++] = Number((n & 0x7fn) | 0x80n);
n >>= 7n;
}
output[offset++] = Number(n);
return offset;
}
/**
* Read a variable-length integer from the input buffer (BigInt version)
*/
function readVar(input, offset) {
let n = 0n;
let shift = 0n;
let b;
let pos = offset;
do {
b = BigInt(input[pos++]);
n |= (b & 0x7fn) << shift;
shift += 7n;
} while (b & 0x80n);
return [zigzagDecode(n), pos];
}
/**
* Encode a value to binary format
*/
export function encode(value) {
const encoder = new TextEncoder();
let buffer = new Uint8Array(4096);
let offset = 0;
function ensureBuffer(size) {
if (offset + size > buffer.length) {
const newBuffer = new Uint8Array(buffer.length * 2 + size);
newBuffer.set(buffer);
buffer = newBuffer;
}
}
function encodeValue(val, offset) {
ensureBuffer(16);
// Handle EnumType
if (typeof val === 'object' && val !== null && 'type' in val) {
const enumVal = val;
buffer[offset] = TAG_ENUM;
offset++;
switch (enumVal.type) {
case 'A':
buffer[offset] = 15; // index = 15
return offset + 1;
case 'B': {
const [a, b] = enumVal.value;
buffer[offset] = 1; // variant index
offset++;
// Encode u32
const view = new DataView(buffer.buffer, buffer.byteOffset);
view.setUint32(offset, a, true);
offset += 4;
// Encode u64
view.setBigUint64(offset, BigInt(b), true);
return offset + 8;
}
case 'C': {
const { a, b } = enumVal.value;
buffer[offset] = 2; // variant index
offset++;
// Encode u32
const view = new DataView(buffer.buffer, buffer.byteOffset);
view.setUint32(offset, a, true);
offset += 4;
// Encode u64
view.setBigUint64(offset, BigInt(b), true);
return offset + 8;
}
}
}
// Handle regular Value types
if (val === null) {
buffer[offset] = TAG_NULL;
return offset + 1;
}
if (typeof val === 'boolean') {
buffer[offset] = TAG_BOOL;
buffer[offset + 1] = val ? 1 : 0;
return offset + 2;
}
if (typeof val === 'number') {
if (Number.isInteger(val)) {
buffer[offset] = TAG_INT;
return writeVar(val, buffer, offset + 1);
}
else {
buffer[offset] = TAG_FLOAT;
ensureBuffer(8);
const view = new DataView(buffer.buffer, buffer.byteOffset);
view.setFloat64(offset + 1, val, true); // little-endian
return offset + 9;
}
}
if (typeof val === 'string') {
const bytes = encoder.encode(val);
buffer[offset] = TAG_STRING;
let next = writeVar(bytes.length, buffer, offset + 1);
ensureBuffer(bytes.length);
buffer.set(bytes, next);
return next + bytes.length;
}
if (val instanceof Uint8Array) {
buffer[offset] = TAG_BYTES;
let next = writeVar(val.length, buffer, offset + 1);
ensureBuffer(val.length);
buffer.set(val, next);
return next + val.length;
}
if (Array.isArray(val)) {
buffer[offset] = TAG_ARRAY;
let next = writeVar(val.length, buffer, offset + 1);
for (const item of val) {
next = encodeValue(item, next);
}
return next;
}
if (typeof val === 'object') {
const keys = Object.keys(val);
buffer[offset] = TAG_OBJECT;
let next = writeVar(keys.length, buffer, offset + 1);
for (const key of keys) {
const keyBytes = encoder.encode(key);
next = writeVar(keyBytes.length, buffer, next);
ensureBuffer(keyBytes.length);
buffer.set(keyBytes, next);
next += keyBytes.length;
next = encodeValue(val[key], next);
}
return next;
}
throw new Error(`Unsupported value type: ${typeof val}`);
}
offset = encodeValue(value, offset);
return buffer.slice(0, offset);
}
/**
* Decode a binary format to value
*/
export function decode(input) {
const decoder = new TextDecoder();
function decodeValue(offset) {
const tag = input[offset++];
switch (tag) {
case TAG_ENUM: {
const variantIndex = input[offset++];
switch (variantIndex) {
case 15:
return [{ type: 'A' }, offset];
case 1: {
const view = new DataView(input.buffer, input.byteOffset);
const a = view.getUint32(offset, true);
offset += 4;
const b = Number(view.getBigUint64(offset, true));
offset += 8;
return [{ type: 'B', value: [a, b] }, offset];
}
case 2: {
const view = new DataView(input.buffer, input.byteOffset);
const a = view.getUint32(offset, true);
offset += 4;
const b = Number(view.getBigUint64(offset, true));
offset += 8;
return [{ type: 'C', value: { a, b } }, offset];
}
default:
throw new Error(`Unknown enum variant index: ${variantIndex}`);
}
}
case TAG_NULL:
return [null, offset];
case TAG_BOOL:
return [input[offset++] === 1, offset];
case TAG_INT: {
const [value, newOffset] = readVar(input, offset);
return [value, newOffset];
}
case TAG_FLOAT: {
const view = new DataView(input.buffer, input.byteOffset);
const value = view.getFloat64(offset, true); // little-endian
return [value, offset + 8];
}
case TAG_STRING: {
const [length, newOffset] = readVar(input, offset);
const value = decoder.decode(input.slice(newOffset, newOffset + length));
return [value, newOffset + length];
}
case TAG_BYTES: {
const [length, newOffset] = readVar(input, offset);
const value = input.slice(newOffset, newOffset + length);
return [value, newOffset + length];
}
case TAG_ARRAY: {
const [length, newOffset] = readVar(input, offset);
const array = [];
let currentOffset = newOffset;
for (let i = 0; i < length; i++) {
const [value, nextOffset] = decodeValue(currentOffset);
array.push(value);
currentOffset = nextOffset;
}
return [array, currentOffset];
}
case TAG_OBJECT: {
const [length, newOffset] = readVar(input, offset);
const obj = {};
let currentOffset = newOffset;
for (let i = 0; i < length; i++) {
const [keyLength, keyOffset] = readVar(input, currentOffset);
const key = decoder.decode(input.slice(keyOffset, keyOffset + keyLength));
const [value, valueOffset] = decodeValue(keyOffset + keyLength);
obj[key] = value;
currentOffset = valueOffset;
}
return [obj, currentOffset];
}
default:
throw new Error(`Unknown tag: ${tag}`);
}
}
const [value, finalOffset] = decodeValue(0);
if (finalOffset !== input.length) {
throw new Error('Unexpected trailing data');
}
return value;
}