@betwixtlabs/bebop@3.1.3Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
latest
betwixt-labs/bebopWorks with
•JSR Score100%•This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




Publisheda year ago (3.1.3)
The TypeScript runtime for Bebop, a schema-based binary serialization format.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242import { BinarySchema } from "./binary.ts"; const hexDigits: string = "0123456789abcdef"; const asciiToHex: Array<number> = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]; const guidDelimiter: string = "-"; const ticksBetweenEpochs: bigint = 621355968000000000n; const dateMask: bigint = 0x3fffffffffffffffn; const emptyByteArray: Uint8Array = new Uint8Array(0); const emptyString: string = ""; const byteToHex: Array<string> = []; // A lookup table: ['00', '01', ..., 'ff'] for (const x of hexDigits) { for (const y of hexDigits) { byteToHex.push(x + y); } } // Cache the check for Crypto.getRandomValues const hasCryptoGetRandomValues = typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function'; export class BebopRuntimeError extends Error { constructor(message: string) { super(message); this.name = "BebopRuntimeError"; } } /** * Represents a globally unique identifier (GUID). */ export class Guid { public static readonly empty: Guid = new Guid("00000000-0000-0000-0000-000000000000"); /** * Constructs a new Guid object with the specified value. * @param value The value of the GUID. */ private constructor(private readonly value: string) { } /** * Gets the string value of the Guid. * @returns The string representation of the Guid. */ public toString(): string { return this.value; } /** * Checks if the Guid is empty. * @returns true if the Guid is empty, false otherwise. */ public isEmpty(): boolean { return this.value === Guid.empty.value; } /** * Checks if a value is a Guid. * @param value The value to be checked. * @returns true if the value is a Guid, false otherwise. */ public static isGuid(value: any): value is Guid { return value instanceof Guid; } /** * Parses a string into a Guid. * @param value The string to be parsed. * @returns A new Guid that represents the parsed value. * @throws {BebopRuntimeError} If the input string is not a valid Guid. */ public static parseGuid(value: string): Guid { let cleanedInput = ''; let count = 0; // Iterate through each character in the input for (let i = 0; i < value.length; i++) { let ch = value[i].toLowerCase(); if (hexDigits.indexOf(ch) !== -1) { // If the character is a hexadecimal digit, add it to cleanedInput cleanedInput += ch; count++; } else if (ch !== '-') { // If the character is not a hexadecimal digit or a hyphen, it's invalid throw new BebopRuntimeError(`Invalid GUID: ${value}`); } } // If the count is not 32, the input is not a valid GUID if (count !== 32) { throw new BebopRuntimeError(`Invalid GUID: ${value}`); } // Insert hyphens to make it a 8-4-4-4-12 character pattern const guidString = cleanedInput.slice(0, 8) + '-' + cleanedInput.slice(8, 12) + '-' + cleanedInput.slice(12, 16) + '-' + cleanedInput.slice(16, 20) + '-' + cleanedInput.slice(20); // Construct a new Guid object with the generated string and return it return new Guid(guidString); } /** * Creates a an insecure new Guid using Math.random. * @returns A new Guid. */ public static newGuid(): Guid { let guid = ""; // Obtain a single timestamp to help seed randomness const now = Date.now(); // Iterate through the 36 characters of a UUID for (let i = 0; i < 36; i++) { // Insert hyphens at the appropriate indices (8, 13, 18, 23) if (i === 8 || i === 13 || i === 18 || i === 23) { guid += "-"; } // According to the UUID v4 spec, the 14th character should be '4' else if (i === 14) { guid += "4"; } // According to the UUID v4 spec, the 19th character should be one of '8', '9', 'a', or 'b'. // Here we're using 'a' or 'b' to simplify the code else if (i === 19) { guid += Math.random() > 0.5 ? "a" : "b"; } // Generate the rest of the UUID using random hexadecimal digits else { // Add the current time to the random number to seed it, then modulo by 16 to get a number between 0 and 15 // Use bitwise OR 0 to round the result down to an integer, and get the hexadecimal digit from the lookup table guid += hexDigits[(Math.random() * 16 + now) % 16 | 0]; } } // Construct a new Guid object with the generated string and return it return new Guid(guid); } /** * Creates a new cryptographically secure Guid using Crypto.getRandomValues. * @returns A new secure Guid. * @throws {BebopRuntimeError} If Crypto.getRandomValues is not available. */ public static newSecureGuid(): Guid { if (!hasCryptoGetRandomValues) { throw new BebopRuntimeError( "Crypto.getRandomValues is not available. " + "Please include a polyfill or use in an environment that supports it." ); } const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); // Set the version (4) and variant (RFC4122) bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; return Guid.fromBytes(bytes, 0); } /** * Checks if the Guid is equal to another Guid. * @param other The other Guid to be compared with. * @returns true if the Guids are equal, false otherwise. */ public equals(other: Guid): boolean { // Check if both GUIDs are the same instance if (this === other) { return true; } // Check if the other object is a GUID if (!(other instanceof Guid)) { return false; } // Compare the hexadecimal representations of both GUIDs for (let i = 0; i < this.value.length; i++) { if (this.value[i] !== other.value[i]) { return false; } } // All hexadecimal digits are equal, so the GUIDs are equal return true; } /** * Writes the Guid to a DataView. * @param view The DataView to write to. * @param length The position to start writing at. */ public writeToView(view: DataView, length: number): void { var p = 0, a = 0; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; p += (this.value.charCodeAt(p) === 45) as any; view.setUint32(length, a, true); a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; p += (this.value.charCodeAt(p) === 45) as any; view.setUint16(length + 4, a, true); a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; p += (this.value.charCodeAt(p) === 45) as any; view.setUint16(length + 6, a, true); a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; p += (this.value.charCodeAt(p) === 45) as any; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; view.setUint32(length + 8, a, false); a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)]; view.setUint32(length + 12, a, false); } /** * Creates a Guid from a byte array. * @param buffer The byte array to create the Guid from. * @param index The position in the array to start reading from. * @returns A new Guid that represents the byte array. */ public static fromBytes(buffer: Uint8Array, index: number): Guid { // Order: 3 2 1 0 - 5 4 - 7 6 - 8 9 - a b c d e f var s = byteToHex[buffer[index + 3]]; s += byteToHex[buffer[index + 2]]; s += byteToHex[buffer[index + 1]]; s += byteToHex[buffer[index]]; s += guidDelimiter; s += byteToHex[buffer[index + 5]]; s += byteToHex[buffer[index + 4]]; s += guidDelimiter; s += byteToHex[buffer[index + 7]]; s += byteToHex[buffer[index + 6]]; s += guidDelimiter; s += byteToHex[buffer[index + 8]]; s += byteToHex[buffer[index + 9]]; s += guidDelimiter; s += byteToHex[buffer[index + 10]]; s += byteToHex[buffer[index + 11]]; s += byteToHex[buffer[index + 12]]; s += byteToHex[buffer[index + 13]]; s += byteToHex[buffer[index + 14]]; s += byteToHex[buffer[index + 15]]; return new Guid(s); } /** * Converts the Guid to a string when it's used as a primitive. * @returns The string representation of the Guid. */ [Symbol.toPrimitive](hint: string): string { if (hint === "string" || hint === "default") { return this.toString(); } throw new Error(`Guid cannot be converted to ${hint}`); } } /** * Represents a wrapper around the `Map` class with support for using `Guid` instances as keys. * * This class is designed to provide a 1:1 mapping between `Guid` instances and values, allowing `Guid` instances to be used as keys in the map. * The class handles converting `Guid` instances to their string representation for key storage and retrieval. * @remarks this is required because Javascript lacks true reference equality. Thus two `Guid` instances with the same value are not equal. */ export class GuidMap<TValue> { private readonly map: Map<string, TValue>; /** * Creates a new GuidMap instance. * @param entries - An optional array or iterable containing key-value pairs to initialize the map. */ constructor( entries?: | readonly (readonly [Guid, TValue])[] | null | Iterable<readonly [Guid, TValue]> ) { if (entries instanceof Map) { this.map = new Map<string, TValue>( entries as unknown as Iterable<[string, TValue]> ); } else if (entries && typeof entries[Symbol.iterator] === "function") { this.map = new Map<string, TValue>( [...entries].map(([key, value]) => [key.toString(), value]) ); } else { this.map = new Map<string, TValue>(); } } /** * Sets the value associated with the specified `Guid` key in the map. * @param key The `Guid` key. * @param value The value to be set. * @returns The updated `GuidMap` instance. */ set(key: Guid, value: TValue): this { this.map.set(key.toString(), value); return this; } /** * Retrieves the value associated with the specified `Guid` key from the map. * @param key The `Guid` key. * @returns The associated value, or `undefined` if the key is not found. */ get(key: Guid): TValue | undefined { return this.map.get(key.toString()); } /** * Deletes the value associated with the specified `Guid` key from the map. * @param key The `Guid` key. * @returns `true` if the key was found and deleted, or `false` otherwise. */ delete(key: Guid): boolean { return this.map.delete(key.toString()); } /** * Checks if the map contains the specified `Guid` key. * @param key The `Guid` key. * @returns `true` if the key is found, or `false` otherwise. */ has(key: Guid): boolean { return this.map.has(key.toString()); } /** * Removes all entries from the map. */ clear(): void { this.map.clear(); } /** * Returns the number of entries in the map. * @returns The number of entries in the map. */ get size(): number { return this.map.size; } /** * Executes the provided callback function once for each key-value pair in the map. * @param callbackFn The callback function to execute. */ forEach( callbackFn: (value: TValue, key: Guid, map: GuidMap<TValue>) => void ): void { this.map.forEach((value, keyString) => { callbackFn(value, Guid.parseGuid(keyString), this); }); } /** * Returns an iterator that yields key-value pairs in the map. * @returns An iterator for key-value pairs in the map. */ *entries(): Generator<[Guid, TValue]> { for (const [keyString, value] of this.map.entries()) { yield [Guid.parseGuid(keyString), value]; } } /** * Returns an iterator that yields the keys of the map. * @returns An iterator for the keys of the map. */ *keys(): Generator<Guid> { for (const keyString of this.map.keys()) { yield Guid.parseGuid(keyString); } } /** * Returns an iterator that yields the values in the map. * @returns An iterator for the values in the map. */ *values(): Generator<TValue> { yield* this.map.values() as Generator<TValue>; } /** * Returns an iterator that yields key-value pairs in the map. * This method is invoked when using the spread operator or destructuring the map. * @returns An iterator for key-value pairs in the map. */ [Symbol.iterator](): Generator<[Guid, TValue]> { return this.entries(); } /** * The constructor function used to create derived objects. */ get [Symbol.species](): typeof GuidMap { return GuidMap; } } /** * An interface which all generated Bebop interfaces implement. * @note this interface is not currently used by the runtime; it is reserved for future use. */ export interface BebopRecord { } export class BebopView { private static textDecoder: TextDecoder; private static writeBuffer: Uint8Array = new Uint8Array(256); private static writeBufferView: DataView = new DataView(BebopView.writeBuffer.buffer); private static instance: BebopView; public static getInstance(): BebopView { if (!BebopView.instance) { BebopView.instance = new BebopView(); } return BebopView.instance; } minimumTextDecoderLength: number = 300; private buffer: Uint8Array; private view: DataView; index: number; // read pointer length: number; // write pointer private constructor() { this.buffer = BebopView.writeBuffer; this.view = BebopView.writeBufferView; this.index = 0; this.length = 0; } startReading(buffer: Uint8Array): void { this.buffer = buffer; this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength); this.index = 0; this.length = buffer.length; } startWriting(): void { this.buffer = BebopView.writeBuffer; this.view = BebopView.writeBufferView; this.index = 0; this.length = 0; } private guaranteeBufferLength(length: number): void { if (length > this.buffer.length) { const data = new Uint8Array(length << 1); data.set(this.buffer); this.buffer = data; this.view = new DataView(data.buffer); } } private growBy(amount: number): void { this.length += amount; this.guaranteeBufferLength(this.length); } skip(amount: number) { this.index += amount; } toArray(): Uint8Array { return this.buffer.subarray(0, this.length); } readByte(): number { return this.buffer[this.index++]; } readUint16(): number { const result = this.view.getUint16(this.index, true); this.index += 2; return result; } readInt16(): number { const result = this.view.getInt16(this.index, true); this.index += 2; return result; } readUint32(): number { const result = this.view.getUint32(this.index, true); this.index += 4; return result; } readInt32(): number { const result = this.view.getInt32(this.index, true); this.index += 4; return result; } readUint64(): bigint { const result = this.view.getBigUint64(this.index, true); this.index += 8; return result; } readInt64(): bigint { const result = this.view.getBigInt64(this.index, true); this.index += 8; return result; } readFloat32(): number { const result = this.view.getFloat32(this.index, true); this.index += 4; return result; } readFloat64(): number { const result = this.view.getFloat64(this.index, true); this.index += 8; return result; } writeByte(value: number): void { const index = this.length; this.growBy(1); this.buffer[index] = value; } writeUint16(value: number): void { const index = this.length; this.growBy(2); this.view.setUint16(index, value, true); } writeInt16(value: number): void { const index = this.length; this.growBy(2); this.view.setInt16(index, value, true); } writeUint32(value: number): void { const index = this.length; this.growBy(4); this.view.setUint32(index, value, true); } writeInt32(value: number): void { const index = this.length; this.growBy(4); this.view.setInt32(index, value, true); } writeUint64(value: bigint): void { const index = this.length; this.growBy(8); this.view.setBigUint64(index, value, true); } writeInt64(value: bigint): void { const index = this.length; this.growBy(8); this.view.setBigInt64(index, value, true); } writeFloat32(value: number): void { const index = this.length; this.growBy(4); this.view.setFloat32(index, value, true); } writeFloat64(value: number): void { const index = this.length; this.growBy(8); this.view.setFloat64(index, value, true); } readBytes(): Uint8Array { const length = this.readUint32(); if (length === 0) { return emptyByteArray; } const start = this.index, end = start + length; this.index = end; return this.buffer.subarray(start, end); } writeBytes(value: Uint8Array): void { const byteCount = value.length; this.writeUint32(byteCount); if (byteCount === 0) { return; } const index = this.length; this.growBy(byteCount); this.buffer.set(value, index); } /** * Reads a length-prefixed UTF-8-encoded string. */ readString(): string { const lengthBytes = this.readUint32(); // bail out early on an empty string if (lengthBytes === 0) { return emptyString; } if (lengthBytes >= this.minimumTextDecoderLength) { if (typeof require !== 'undefined') { if (typeof TextDecoder === 'undefined') { throw new BebopRuntimeError("TextDecoder is not defined on 'global'. Please include a polyfill."); } } if (BebopView.textDecoder === undefined) { BebopView.textDecoder = new TextDecoder(); } return BebopView.textDecoder.decode(this.buffer.subarray(this.index, this.index += lengthBytes)); } const end = this.index + lengthBytes; let result = ""; let codePoint: number; while (this.index < end) { // decode UTF-8 const a = this.buffer[this.index++]; if (a < 0xC0) { codePoint = a; } else { const b = this.buffer[this.index++]; if (a < 0xE0) { codePoint = ((a & 0x1F) << 6) | (b & 0x3F); } else { const c = this.buffer[this.index++]; if (a < 0xF0) { codePoint = ((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F); } else { const d = this.buffer[this.index++]; codePoint = ((a & 0x07) << 18) | ((b & 0x3F) << 12) | ((c & 0x3F) << 6) | (d & 0x3F); } } } // encode UTF-16 if (codePoint < 0x10000) { result += String.fromCharCode(codePoint); } else { codePoint -= 0x10000; result += String.fromCharCode((codePoint >> 10) + 0xD800, (codePoint & ((1 << 10) - 1)) + 0xDC00); } } // Damage control, if the input is malformed UTF-8. this.index = end; return result; } /** * Writes a length-prefixed UTF-8-encoded string. */ writeString(value: string): void { // The number of characters in the string const stringLength = value.length; // If the string is empty avoid unnecessary allocations by writing the zero length and returning. if (stringLength === 0) { this.writeUint32(0); return; } // value.length * 3 is an upper limit for the space taken up by the string: // https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto#Buffer_Sizing // We add 4 for our length prefix. const maxBytes = 4 + stringLength * 3; // Reallocate if necessary, then write to this.length + 4. this.guaranteeBufferLength(this.length + maxBytes); // Start writing the string from here: let w = this.length + 4; const start = w; let codePoint: number; for (let i = 0; i < stringLength; i++) { // decode UTF-16 const a = value.charCodeAt(i); if (i + 1 === stringLength || a < 0xD800 || a >= 0xDC00) { codePoint = a; } else { const b = value.charCodeAt(++i); codePoint = (a << 10) + b + (0x10000 - (0xD800 << 10) - 0xDC00); } // encode UTF-8 if (codePoint < 0x80) { this.buffer[w++] = codePoint; } else { if (codePoint < 0x800) { this.buffer[w++] = ((codePoint >> 6) & 0x1F) | 0xC0; } else { if (codePoint < 0x10000) { this.buffer[w++] = ((codePoint >> 12) & 0x0F) | 0xE0; } else { this.buffer[w++] = ((codePoint >> 18) & 0x07) | 0xF0; this.buffer[w++] = ((codePoint >> 12) & 0x3F) | 0x80; } this.buffer[w++] = ((codePoint >> 6) & 0x3F) | 0x80; } this.buffer[w++] = (codePoint & 0x3F) | 0x80; } } // Count how many bytes we wrote. const written = w - start; // Write the length prefix, then skip over it and the written string. this.view.setUint32(this.length, written, true); this.length += 4 + written; } readGuid(): Guid { const guid = Guid.fromBytes(this.buffer, this.index); this.index += 16; return guid; } writeGuid(value: Guid): void { const i = this.length; this.growBy(16); value.writeToView(this.view, i); } // A note on these numbers: // 62135596800000 ms is the difference between the C# epoch (0001-01-01) and the Unix epoch (1970-01-01). // 0.0001 is the number of milliseconds per "tick" (a tick is 100 ns). // 429496.7296 is the number of milliseconds in 2^32 ticks. // 0x3fffffff is a mask to ignore the "Kind" bits of the Date.ToBinary value. // 0x40000000 is a mask to set the "Kind" bits to "DateTimeKind.Utc". readDate(): Date { const ticks = this.readUint64() & dateMask; const ms = (ticks - ticksBetweenEpochs) / 10000n; return new Date(Number(ms)); } writeDate(date: Date) { const ms = BigInt(date.getTime()); const ticks = ms * 10000n + ticksBetweenEpochs; this.writeUint64(ticks & dateMask); } /** * Reserve some space to write a message's length prefix, and return its index. * The length is stored as a little-endian fixed-width unsigned 32-bit integer, so 4 bytes are reserved. */ reserveMessageLength(): number { const i = this.length; this.growBy(4); return i; } /** * Fill in a message's length prefix. */ fillMessageLength(position: number, messageLength: number): void { this.view.setUint32(position, messageLength, true); } /** * Read out a message's length prefix. */ readMessageLength(): number { const result = this.view.getUint32(this.index, true); this.index += 4; return result; } } const typeMarker = '#btype'; const keyMarker = '#ktype'; const mapTag = 1; const dateTag = 2; const uint8ArrayTag = 3; const bigIntTag = 4; const guidTag = 5; const mapGuidTag = 6; const boolTag = 7; const stringTag = 8; const numberTag = 9; const castScalarByTag = (value: any, tag: number): any => { switch (tag) { case bigIntTag: return BigInt(value); case boolTag: return Boolean(value); case stringTag: return value; case numberTag: return Number(value); default: throw new BebopRuntimeError(`Unknown scalar tag: ${tag}`); } }; /** * Determines the tag for the keys of a given map based on the type of the first key. * @param map - The map whose key tag is to be determined. * @returns The tag for the keys of the map. * @throws BebopRuntimeError if the map is empty or if the type of the first key is not a string, number, boolean, or BigInt. */ const getMapKeyTag = (map: Map<unknown, unknown>): number => { if (map.size === 0) { throw new BebopRuntimeError("Cannot determine key type of an empty map."); } const keyType = typeof map.keys().next().value; let keyTag: number; switch (keyType) { case "string": keyTag = stringTag; break; case "number": keyTag = numberTag; break; case "boolean": keyTag = boolTag; break; case "bigint": keyTag = bigIntTag; break; default: throw new BebopRuntimeError(`Not suitable map type tag found. Keys must be strings, numbers, booleans, or BigInts: ${keyType}`); } return keyTag; }; /** * A custom replacer function for JSON.stringify that supports BigInt, Map, * Date, Uint8Array, including BigInt values inside Map and Array. * @param _key - The key of the property being stringified. * @param value - The value of the property being stringified. * @returns The modified value for the property, or the original value if not a BigInt or Map. */ const replacer = (_key: string | number, value: any): any => { if (value === null) return value; switch (typeof value) { case 'bigint': return { [typeMarker]: bigIntTag, value: value.toString() }; case 'string': case 'number': case 'boolean': return value; } if (value instanceof Date) { const ms = BigInt(value.getTime()); const ticks = ms * 10000n + ticksBetweenEpochs; return { [typeMarker]: dateTag, value: (ticks & dateMask).toString() }; } if (value instanceof Uint8Array) { return { [typeMarker]: uint8ArrayTag, value: Array.from(value) }; } if (value instanceof Guid) { return { [typeMarker]: guidTag, value: value.toString() }; } if (value instanceof GuidMap) { const obj: Record<any, any> = {}; for (let [k, v] of value.entries()) { obj[k.toString()] = replacer(_key, v); } return { [typeMarker]: mapGuidTag, value: obj }; } if (value instanceof Map) { const obj: Record<any, any> = {}; let keyTag = getMapKeyTag(value); if (keyTag === undefined) { throw new BebopRuntimeError("Not suitable map key type tag found."); } for (let [k, v] of value.entries()) { obj[k] = replacer(_key, v); } return { [typeMarker]: mapTag, [keyMarker]: keyTag, value: obj }; } if (Array.isArray(value)) { return value.map((v, i) => replacer(i, v)); } if (typeof value === 'object') { const newObj: Record<any, any> = {}; for (let k in value) { newObj[k] = replacer(k, value[k]); } return newObj; } return value; }; /** * A custom reviver function for JSON.parse that supports BigInt, Map, Date, * Uint8Array, including nested values * @param _key - The key of the property being parsed. * @param value - The value of the property being parsed. * @returns The modified value for the property, or the original value if not a marked type. */ const reviver = (_key: string | number, value: any): any => { if (_key === "__proto__" || _key === "prototype" || _key === "constructor") throw new BebopRuntimeError("potential prototype pollution"); if (value && typeof value === "object" && !Array.isArray(value)) { if (value[typeMarker]) { switch (value[typeMarker]) { case bigIntTag: return BigInt(value.value); case dateTag: const ticks = BigInt(value.value) & dateMask; const ms = (ticks - ticksBetweenEpochs) / 10000n; return new Date(Number(ms)); case uint8ArrayTag: return new Uint8Array(value.value); case mapTag: const keyTag = value[keyMarker]; if (keyTag === undefined || keyTag === null) { throw new BebopRuntimeError("Map key type tag not found."); } const map = new Map(); for (let k in value.value) { const trueKey = castScalarByTag(k, keyTag); map.set(trueKey, reviver(k, value.value[k])); } return map; case guidTag: return Guid.parseGuid(value.value); case mapGuidTag: const guidMap = new GuidMap(); for (let k in value.value) { guidMap.set(Guid.parseGuid(k), reviver(k, value.value[k])); } return guidMap; default: throw new BebopRuntimeError(`Unknown type marker: ${value[typeMarker]}`); } } } return value; }; /** * A collection of functions for working with Bebop-encoded JSON. */ export const BebopJson = { /** * A custom replacer function for JSON.stringify that supports BigInt, Map, * Date, Uint8Array, including BigInt values inside Map and Array. * @param _key - The key of the property being stringified. * @param value - The value of the property being stringified. * @returns The modified value for the property, or the original value if not a BigInt or Map. */ replacer, /** * A custom reviver function for JSON.parse that supports BigInt, Map, Date, * Uint8Array, including nested values * @param _key - The key of the property being parsed. * @param value - The value of the property being parsed. * @returns The modified value for the property, or the original value if not a marked type. */ reviver, }; /** * Ensures that the given value is a valid boolean. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid boolean. */ const ensureBoolean = (value: any): void => { if (!(value === false || value === true || value instanceof Boolean || typeof value === "boolean")) { throw new BebopRuntimeError(`Invalid value for Boolean: ${value} / typeof ${typeof value}`); } }; /** * Ensures that the given value is a valid Uint8 number (0 to 255). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint8 number. */ const ensureUint8 = (value: any): void => { if (!Number.isInteger(value) || value < 0 || value > 255) { throw new BebopRuntimeError(`Invalid value for Uint8: ${value}`); } }; /** * Ensures that the given value is a valid Int16 number (-32768 to 32767). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Int16 number. */ const ensureInt16 = (value: any): void => { if (!Number.isInteger(value) || value < -32768 || value > 32767) { throw new BebopRuntimeError(`Invalid value for Int16: ${value}`); } }; /** * Ensures that the given value is a valid Uint16 number (0 to 65535). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint16 number. */ const ensureUint16 = (value: any): void => { if (!Number.isInteger(value) || value < 0 || value > 65535) { throw new BebopRuntimeError(`Invalid value for Uint16: ${value}`); } }; /** * Ensures that the given value is a valid Int32 number (-2147483648 to 2147483647). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Int32 number. */ const ensureInt32 = (value: any): void => { if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) { throw new BebopRuntimeError(`Invalid value for Int32: ${value}`); } }; /** * Ensures that the given value is a valid Uint32 number (0 to 4294967295). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint32 number. */ const ensureUint32 = (value: any): void => { if (!Number.isInteger(value) || value < 0 || value > 4294967295) { throw new BebopRuntimeError(`Invalid value for Uint32: ${value}`); } }; /** * Ensures that the given value is a valid Int64 number (-9223372036854775808 to 9223372036854775807). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Int64 number. */ const ensureInt64 = (value: bigint | number): void => { const min = BigInt("-9223372036854775808"); const max = BigInt("9223372036854775807"); value = BigInt(value); if (value < min || value > max) { throw new BebopRuntimeError(`Invalid value for Int64: ${value}`); } }; /** * Ensures that the given value is a valid Uint64 number (0 to 18446744073709551615). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint64 number. */ const ensureUint64 = (value: bigint | number): void => { const max = BigInt("18446744073709551615"); value = BigInt(value); if (value < BigInt(0) || value > max) { throw new BebopRuntimeError(`Invalid value for Uint64: ${value}`); } }; /** * Ensures that the given value is a valid BigInt number. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid BigInt number. */ const ensureBigInt = (value: any): void => { if (typeof value !== 'bigint') { throw new BebopRuntimeError(`Invalid value for BigInt: ${value}`); } }; /** * Ensures that the given value is a valid float number. * @param value - The value to check. * @throws {Error} - If the value is not a valid float number. */ const ensureFloat = (value: any): void => { if (typeof value !== 'number' || !Number.isFinite(value)) { throw new BebopRuntimeError(`Invalid value for Float: ${value}`); } }; /** * Ensures that the given value is a valid Map object, with keys and values that pass the specified validators. * @param value - The value to check. * @param keyTypeValidator - A function that validates the type of each key in the Map. * @param valueTypeValidator - A function that validates the type of each value in the Map. * @throws {BebopRuntimeError} - If the value is not a valid Map object, or if any key or value fails validation. */ const ensureMap = (value: any, keyTypeValidator: (key: any) => void, valueTypeValidator: (value: any) => void): void => { if (!(value instanceof Map || value instanceof GuidMap)) { throw new BebopRuntimeError(`Invalid value for Map: ${value}`); } for (let [k, v] of value) { keyTypeValidator(k); valueTypeValidator(v); } }; /** * Ensures that the given value is a valid Array object, with elements that pass the specified validator. * @param value - The value to check. * @param elementTypeValidator - A function that validates the type of each element in the Array. * @throws {BebopRuntimeError} - If the value is not a valid Array object, or if any element fails validation. */ const ensureArray = (value: any, elementTypeValidator: (element: any) => void): void => { if (!Array.isArray(value)) { throw new BebopRuntimeError(`Invalid value for Array: ${value}`); } for (let element of value) { elementTypeValidator(element); } }; /** * Ensures that the given value is a valid Date object. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Date object. */ const ensureDate = (value: any): void => { if (!(value instanceof Date)) { throw new BebopRuntimeError(`Invalid value for Date: ${value}`); } }; /** * Ensures that the given value is a valid Uint8Array object. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint8Array object. */ const ensureUint8Array = (value: any): void => { if (!(value instanceof Uint8Array)) { throw new BebopRuntimeError(`Invalid value for Uint8Array: ${value}`); } }; /** * Ensures that the given value is a valid string. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid string. */ const ensureString = (value: any): void => { if (typeof value !== 'string') { throw new BebopRuntimeError(`Invalid value for String: ${value}`); } }; /** * Ensures that the given value is a valid enum value. * @param value - The value to check. * @param enumValue - An object representing the enum values. * @throws {BebopRuntimeError} - If the value is not a valid enum value. */ const ensureEnum = (value: any, enumValue: object): void => { if (!Number.isInteger(value)) { throw new BebopRuntimeError(`Invalid value for enum, not an int: ${value}`); } if (!(value in enumValue)) { throw new BebopRuntimeError(`Invalid value for enum, not in enum: ${value}`); } }; /** * Ensures that the given value is a valid Guid object. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Guid object. */ const ensureGuid = (value: any): void => { if (!(value instanceof Guid)) { throw new BebopRuntimeError(`Invalid value for Guid: ${value}`); } }; /** * This object contains functions for ensuring that values conform to specific types. */ export const BebopTypeGuard = { /** * Ensures that the given value is a valid boolean. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid boolean. */ ensureBoolean, /** * Ensures that the given value is a valid Uint8 number (0 to 255). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint8 number. */ ensureUint8, /** * Ensures that the given value is a valid Int16 number (-32768 to 32767). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Int16 number. */ ensureInt16, /** * Ensures that the given value is a valid Uint16 number (0 to 65535). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint16 number. */ ensureUint16, /** * Ensures that the given value is a valid Int32 number (-2147483648 to 2147483647). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Int32 number. */ ensureInt32, /** * Ensures that the given value is a valid Uint32 number (0 to 4294967295). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint32 number. */ ensureUint32, /** * Ensures that the given value is a valid Int64 number (-9223372036854775808 to 9223372036854775807). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Int64 number. */ ensureInt64, /** * Ensures that the given value is a valid Uint64 number (0 to 18446744073709551615). * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint64 number. */ ensureUint64, /** * Ensures that the given value is a valid BigInt number. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid BigInt number. */ ensureBigInt, /** * Ensures that the given value is a valid float number. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid float number. */ ensureFloat, /** * Ensures that the given value is a valid Map object, with keys and values that pass the specified validators. * @param value - The value to check. * @param keyTypeValidator - A function that validates the type of each key in the Map. * @param valueTypeValidator - A function that validates the type of each value in the Map. * @throws {BebopRuntimeError} - If the value is not a valid Map object, or if any key or value fails validation. */ ensureMap, /** * Ensures that the given value is a valid Array object, with elements that pass the specified validator. * @param value - The value to check. * @param elementTypeValidator - A function that validates the type of each element in the Array. * @throws {BebopRuntimeError} - If the value is not a valid Array object, or if any element fails validation. */ ensureArray, /** * Ensures that the given value is a valid Date object. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Date object. */ ensureDate, /** * Ensures that the given value is a valid Uint8Array object. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid Uint8Array object. */ ensureUint8Array, /** * Ensures that the given value is a valid string. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid string. */ ensureString, /** * Ensures that the given value is a valid enum value. * @param value - The value to check. * @param enumValues - An array of valid enum values. * @throws {BebopRuntimeError} - If the value is not a valid enum value. */ ensureEnum, /** * Ensures that the given value is a valid GUID string. * @param value - The value to check. * @throws {BebopRuntimeError} - If the value is not a valid GUID string. */ ensureGuid }; export { BinarySchema };