"use strict"; /* UTILS */ function joinUint8Arrays(a, b) { let c = new Uint8Array(a.length + b.length); c.set(a, 0); c.set(b, a.length); return c; } function safeNumber(bigInt) { if (bigInt > MAX_U53 || bigInt < -MAX_U53) { throw RangeError("BigInt value out of double precision range (53 bits)"); } else { return Number(bigInt); } } const MAX_U32 = 2n ** 32n - 1n; const MAX_U53 = 2n ** 53n - 1n; // same as Number.MAX_SAFE_INTEGER const MAX_U64 = 2n ** 64n - 1n; // this is the maximum string length in spec, // some browsers support less (eg. firefox with 2^30 - 2) const MAX_STRING_LENGTH = MAX_U53; // this is the maximum array length in spec, // used for underlying data structure const MAX_DATA_LENGTH = MAX_U32; const MAX_ARRAY_LENGTH = MAX_U32; const MAX_MAP_LENGTH = MAX_U32; /* PRIMITIVE TYPES */ class BareType { /** * pack(obj): * : a js object compatible with the layout of the class this is called on * returns: the binary representation of the class in a Uint8Array with values inserted from */ static pack(obj) {} /** * unpack(raw): * : a DataView on the Uint8Array of the message bytes, offset to the start of the unprocessed bytes * returns: [an object with its values set according to the class layout, the number of bytes consumed by this operation] */ static unpack(raw) {} } class BarePrimitive extends BareType { static pack(value) {} } class BareUInt extends BarePrimitive { static pack(value) { value = BigInt(value); if (value > MAX_U64) { throw RangeError("Unsigned value out of 64-bit range"); } else if (value < 0) { throw RangeError("Passed signed value to unsigned field"); } let bytes = []; while (value >= 0x80n) { bytes.push(Number((value & 0xFFn) | 0x80n)); value = value >> 7n; } bytes.push(Number(value)); return Uint8Array.from(bytes); } static unpack(raw) { let value = 0n; for (let i = 0;; i++) { let byte = BigInt(raw.getUint8(i)); if (byte < 0x80n) { value += byte << BigInt(7 * i); return [value, i + 1]; } value += (byte & 0x7Fn) << BigInt(7 * i); } } } class BareInt extends BarePrimitive { static pack(value) { value = BigInt(value); if (value < 0) { value = ~(2n * value); } else { value = 2n * value; } if (value > MAX_U64) { throw RangeError("Signed value out of 64-bit range"); } return BareUInt.pack(value); } static unpack(raw) { let [value, length] = BareUInt.unpack(raw); let sign = value % 2n; value = value / 2n; if (sign) { value = -(value + 1n); } return [value, length]; } } class BareU8 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(1); let view = new DataView(bin.buffer); view.setUint8(0, value); return bin; } static unpack(raw) { let value = raw.getUint8(0); return [value, 1]; } } class BareU16 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(2); let view = new DataView(bin.buffer); view.setUint16(0, value, true); return bin; } static unpack(raw) { let value = raw.getUint16(0, true); return [value, 2]; } } class BareU32 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(4); let view = new DataView(bin.buffer); view.setUint32(0, value, true); return bin; } static unpack(raw) { let value = raw.getUint32(0, true); return [value, 4]; } } class BareU64 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(8); let view = new DataView(bin.buffer); view.setBigUint64(0, BigInt(value), true); return bin; } static unpack(raw) { let value = raw.getBigUint64(0, true); return [value, 8]; } } class BareI8 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(1); let view = new DataView(bin.buffer); view.setInt8(0, value); return bin; } static unpack(raw) { let value = raw.getInt8(0); return [value, 1]; } } class BareI16 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(2); let view = new DataView(bin.buffer); view.setInt16(0, value, true); return bin; } static unpack(raw) { let value = raw.getInt16(0, true); return [value, 2]; } } class BareI32 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(4); let view = new DataView(bin.buffer); view.setInt32(0, value, true); return bin; } static unpack(raw) { let value = raw.getInt32(0, true); return [value, 4]; } } class BareI64 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(8); let view = new DataView(bin.buffer); view.setBigInt64(0, BigInt(value), true); return bin; } static unpack(raw) { let value = raw.getBigInt64(0, true); return [value, 8]; } } class BareF32 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(4); let view = new DataView(bin.buffer); view.setFloat32(0, value, true); return bin; } static unpack(raw) { let value = raw.getFloat32(0, true); return [value, 4]; } } class BareF64 extends BarePrimitive { static pack(value) { let bin = new Uint8Array(8); let view = new DataView(bin.buffer); view.setFloat64(0, value, true); return bin; } static unpack(raw) { let value = raw.getFloat64(0, true); return [value, 8]; } } class BareBool extends BarePrimitive { static pack(value) { let bin = new Uint8Array(1); let view = new DataView(bin.buffer); value = value ? 1 : 0; view.setUint8(0, value); return bin; } static unpack(raw) { let value = raw.getUint8(0); value = !!value; return [value, 1]; } } function mapEnum(enumClass, keys) { let entries = Object.entries(keys); for (let i = 0; i < entries.length; i++) { let [key, name] = entries[i]; enumClass[name] = key; } return keys; } class BareEnum extends BarePrimitive { // alternatively an array with gaps is allowed: ['ONE', 'TWO', , , 'FIVE'] // this can also be done by index after the class definition: // Enum.keys[50] = 'SPECIAL'; // although you then have to run mapEnum after that static keys; // = mapEnum(this, {0:'NAME1', 1:'NAME2', 4:'NAME5', ...}) static pack(value) { if (!this.keys[value]) { throw ReferenceError("Invalid enum value"); } let num = BigInt(value); return BareUInt.pack(num); } static unpack(raw) { let [value, bytes] = BareUInt.unpack(raw); if (value > MAX_U32) { throw RangeError("Enum value out of range"); } if (!this.keys[value]) { throw ReferenceError("Invalid enum value"); } value = Number(value); return [value, bytes]; } } const BareUTF8Encoder = new TextEncoder(); const BareUTF8Decoder = new TextDecoder(); class BareString extends BarePrimitive { static pack(value) { let bytes = BareUTF8Encoder.encode(value); let length = BareUInt.pack(BigInt(bytes.length)); return joinUint8Arrays(length, bytes); } static unpack(raw) { let [length, lenBytes] = BareUInt.unpack(raw); if (length > MAX_STRING_LENGTH) { throw RangeError("Invalid string length"); } length = Number(length); let bytes = new DataView(raw.buffer, raw.byteOffset + lenBytes, length); let value = BareUTF8Decoder.decode(bytes); return [value, length + lenBytes]; } } class BareDataFixed extends BarePrimitive { static length; static pack(value) { return value; } static unpack(raw) { let value = raw.buffer.slice(raw.byteOffset, raw.byteOffset + this.length); return [value, this.length]; } } class BareData extends BarePrimitive { static pack(value) { let length = BareUInt.pack(BigInt(value.length)); return joinUint8Arrays(length, value); } static unpack(raw) { let [length, lenBytes] = BareUInt.unpack(raw); if (length > MAX_DATA_LENGTH) { throw RangeError("Invalid array length"); } length = Number(length); let value = raw.buffer.slice(raw.byteOffset + lenBytes, raw.byteOffset + lenBytes + length); return [value, length + lenBytes]; } } class BareVoid extends BarePrimitive { // INVARIANT: may only be used as a member of sets in a tagged union static pack(value) { return new Uint8Array(0); } static unpack(raw) { return [null, 0]; } } /* AGGREGATE TYPES */ class BareOptional extends BareType { static type; static pack(obj) { if (obj === undefined) { return Uint8Array.of(0); } else { let bytes = this.type.pack(obj); let bin = new Uint8Array(bytes.length + 1); bin.set([1], 0); bin.set(bytes, 1); return bin; } } static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element if (!raw instanceof DataView) { raw = new DataView(raw.buffer, raw.byteOffset); } let status = raw.getUint8(0); if (status === 0) { return [undefined, 1]; } else { let [obj, bytes] = this.type.unpack(new DataView(raw.buffer, raw.byteOffset + 1)); return [obj, bytes + 1]; } } } class BareArrayFixed extends BareType { // INVARIANT: length is greater than 0 static length; static type; static pack(obj) { let elements = new Uint8Array(0); for (let i = 0; i < this.length; i++) { let bytes = this.type.pack(obj[i]); elements = joinUint8Arrays(elements, bytes); } return elements; } static unpack(raw) { let obj = []; let length = 0; for (let i = 0; i < this.length; i++) { let view = new DataView(raw.buffer, raw.byteOffset + length); let [elem, bytes] = this.type.unpack(view); obj.push(elem); length += bytes; } return [obj, length]; } } class BareArray extends BareType { static type; static pack(obj) { let bin = BareUInt.pack(BigInt(obj.length)); for (let i = 0; i < obj.length; i++) { let bytes = this.type.pack(obj[i]); bin = joinUint8Arrays(bin, bytes); } return bin; } static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element if (!raw instanceof DataView) { raw = new DataView(raw.buffer, raw.byteOffset); } let obj = []; let [numElements, length] = BareUInt.unpack(raw); if (numElements > MAX_ARRAY_LENGTH) { throw RangeError("Invalid array length"); } numElements = Number(numElements); for (let i = 0; i < numElements; i++) { let view = new DataView(raw.buffer, raw.byteOffset + length); let [elem, bytes] = this.type.unpack(view); length += bytes; obj.push(elem); } return [obj, length]; } } class BareMap extends BareType { // INVARIANT: map key is a primitive data type but not void, data or data static keyType; static valueType; static pack(obj) { let keys = Object.keys(obj); let bin = BareUInt.pack(BigInt(keys.length)); for (let i = 0; i < keys.length; i++) { let keyBytes = this.keyType.pack(keys[i]); bin = joinUint8Arrays(bin, keyBytes); let valueBytes = this.valueType.pack(obj[keys[i]]); bin = joinUint8Arrays(bin, valueBytes); } return bin; } static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element if (!raw instanceof DataView) { raw = new DataView(raw.buffer, raw.byteOffset); } let obj = {}; let [numEntries, length] = BareUInt.unpack(raw); if (numEntries > MAX_MAP_LENGTH) { throw RangeError("Invalid array length"); } numEntries = Number(numEntries); for (let i = 0; i < numEntries; i++) { let keyView = new DataView(raw.buffer, raw.byteOffset + length); let [key, keyBytes] = this.type.unpack(keyView); length += keyBytes; let valueView = new DataView(raw.buffer, raw.byteOffset + length); let [value, valueBytes] = this.valueType.unpack(valueView); length += valueBytes; obj[key] = value; } return [obj, length]; } } function mapUnion(obj) { let entries = Object.entries(obj); let reverse = {}; for (let i = 0; i < entries.length; i++) { let [key, val] = entries[i]; reverse[val] = key; } return reverse; } // TODO how do I store/represent the BARE type of an object for unions??? also; use BigInt class BareUnion extends BareType { // INVARIANT: has at least one type static types; // = twoWayMap([[class, i], ...]) static pack(obj) { let unionIndex = this.types[obj.constructor]; if (!unionIndex) { console.err("The union " + this + " does not support encoding the type " + obj) } let bin = obj.pack(); let index = BareUInt.pack(unionIndex); return joinUint8Arrays(index, bin); } static unpack(raw) { // make sure raw is a DataView, relevant if this is the top level element if (!raw instanceof DataView) { raw = new DataView(raw.buffer, raw.byteOffset); } let [index, length] = BareUInt.unpack(raw); let objType = this.types[index]; let [obj, bytes] = objType.unpack(new DataView(raw.buffer, raw.byteOffset + length)); return [obj, bytes + length]; } } class BareStruct extends BareType { // INVARIANT: has at least one field static entries; // = [['key', type], ...] static pack(obj) { let bin = new Uint8Array(); for (let i = 0; i < this.entries.length; i++) { let [key, type] = this.entries[i]; let bytes = type.pack(obj[key]); bin = joinUint8Arrays(bin, bytes); } return bin; } static unpack(raw) { let obj = {}; let length = 0; for (let i = 0; i < this.entries.length; i++) { let [key, type] = this.entries[i]; let view = new DataView(raw.buffer, raw.byteOffset + length); let [value, bytes] = type.unpack(view); length += bytes; obj[key] = value; } return [obj, length]; } } export { mapEnum, mapUnion, safeNumber, BareUInt, BareInt, BareU8, BareU16, BareU32, BareU64, BareI8, BareI16, BareI32, BareI64, BareF32, BareF64, BareBool, BareEnum, BareString, BareDataFixed, BareData, BareVoid, BareOptional, BareArrayFixed, BareArray, BareMap, BareUnion, BareStruct, }; export default { mapEnum: mapEnum, mapUnion: mapUnion, safeNumber: safeNumber, UInt: BareUInt, Int: BareInt, U8: BareU8, U16: BareU16, U32: BareU32, U64: BareU64, I8: BareI8, I16: BareI16, I32: BareI32, I64: BareI64, F32: BareF32, F64: BareF64, Bool: BareBool, Enum: BareEnum, String: BareString, DataFixed: BareDataFixed, Data: BareData, Void: BareVoid, Optional: BareOptional, ArrayFixed: BareArrayFixed, Array: BareArray, Map: BareMap, Union: BareUnion, Struct: BareStruct, };