Skip to main content
Home

Built and signed on GitHub Actions

Works with
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score100%
Publisheda year ago (3.1.3)

The TypeScript runtime for Bebop, a schema-based binary serialization format.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242
import { 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 };