bebop
Version:
The TypeScript runtime for Bebop, a schema-based binary serialization format.
1 lines • 124 kB
Source Map (JSON)
{"version":3,"sources":["../binary.ts","../index.ts"],"sourcesContent":["import {\n BebopRuntimeError,\n BebopTypeGuard,\n BebopView,\n Guid,\n GuidMap,\n} from \"./index\";\n\nconst decoder = new TextDecoder();\n\ntype FieldTypes =\n | { type: \"scalar\"; }\n | {\n type: \"array\";\n memberTypeId: number;\n depth: number;\n }\n | {\n type: \"map\";\n keyTypeId: number;\n valueTypeId: number;\n nestedType?: FieldTypes;\n };\n\nenum WireMethodType {\n Unary = 0,\n ServerStreaming = 1,\n ClientStreaming = 2,\n DuplexStream = 3,\n}\nenum WireBaseType {\n Bool = -1,\n Byte = -2,\n UInt16 = -3,\n Int16 = -4,\n UInt32 = -5,\n Int32 = -6,\n UInt64 = -7,\n Int64 = -8,\n Float32 = -9,\n Float64 = -10,\n String = -11,\n Guid = -12,\n Date = -13,\n}\n\nenum WireTypeKind {\n Struct = 1,\n Message,\n Union,\n Enum,\n}\n\ntype Decorators = Decorator[];\n\ninterface Decorator {\n identifier: string;\n arguments?: { [identifier: string]: DecoratorArgument; };\n}\n\ninterface DecoratorArgument {\n typeId: number;\n value: string | number | bigint | Guid | null;\n}\n\ninterface EnumMember {\n name: string;\n decorators: Decorators;\n value: number | bigint | null;\n}\n\ninterface Field {\n name: string;\n typeId: number;\n fieldProperties: FieldTypes;\n decorators: Decorators;\n constantValue?: number | null;\n}\n\ninterface Definition {\n index: number;\n name: string;\n kind: WireTypeKind;\n minimalEncodeSize: number;\n decorators: Decorators;\n}\n\ninterface Enum extends Definition {\n baseType: WireBaseType;\n isBitFlags: boolean;\n members: { [name: string]: EnumMember; };\n}\n\ninterface Struct extends Definition {\n isMutable: boolean;\n isFixedSize: boolean;\n fields: { [fieldName: string]: Field; };\n}\n\ninterface Message extends Definition {\n fields: { [fieldName: string]: Field; };\n}\n\ninterface UnionBranch {\n discriminator: number;\n typeId: number;\n}\n\ninterface Union extends Definition {\n branchCount: number;\n branches: UnionBranch[];\n}\n\ninterface Service {\n name: string;\n decorators: Decorators;\n methods: { [methodName: string]: ServiceMethod; };\n}\n\ninterface ServiceMethod {\n name: string;\n decorators: Decorators;\n requestTypeId: number;\n responseTypeId: number;\n methodType: WireMethodType;\n id: number;\n}\n\ninterface SchemaAst {\n bebopVersion: number;\n definitions: { [typeName: string]: Definition; };\n services?: { [serviceName: string]: Service; };\n}\n\n/**\n * A class that can read a buffer containing a Bebop encoded record by utilizing a binary schema.\n */\nexport class RecordReader {\n /**\n * @param schema - BinarySchema object containing metadata about Bebop schemas.\n * @private\n */\n private constructor(private readonly schema: BinarySchema) { }\n\n /**\n * Reads a Bebop encoded record from a buffer.\n *\n * @param definitionName - Name of the definition in the schema for the record to read.\n * @param data - The buffer to read the record from.\n * @returns - The read record as a Record object.\n * @throws - Throws an error if the record cannot be decoded directly.\n * @public\n */\n public read(\n definitionName: string,\n data: Uint8Array\n ): Record<string, unknown> {\n const definition = this.schema.getDefinition(definitionName);\n if (definition.kind === WireTypeKind.Enum) {\n throw new BebopRuntimeError(\"Cannot decode enum directly\");\n }\n const view = BebopView.getInstance();\n view.startReading(data);\n return this.readDefinition(definition, view) as Record<string, unknown>;\n }\n\n private readDefinition(\n definition: Definition,\n view: BebopView\n ): number | bigint | Record<string, unknown> {\n switch (definition.kind) {\n case WireTypeKind.Enum:\n return this.readEnumDefinition(definition as Enum, view);\n case WireTypeKind.Union:\n return this.readUnionDefinition(definition as Union, view);\n case WireTypeKind.Struct:\n return this.readStructDefinition(definition as Struct, view);\n case WireTypeKind.Message:\n return this.readMessageDefinition(definition as Message, view);\n default:\n throw new BebopRuntimeError(`Unknown type kind: ${definition.kind}`);\n }\n }\n\n private readStructDefinition(definition: Struct, view: BebopView) {\n const record = {} as Record<string, unknown>;\n Object.values(definition.fields).forEach((field) => {\n record[field.name] = this.readField(field, view);\n if (!(field.name in record) || record[field.name] === undefined) {\n throw new BebopRuntimeError(`Missing field ${field.name}`);\n }\n });\n if (!definition.isMutable) {\n Object.freeze(record);\n }\n return record;\n }\n\n private readMessageDefinition(definition: Message, view: BebopView) {\n const record = {} as Record<string, unknown>;\n const length = view.readMessageLength();\n const end = view.index + length;\n const fields = Object.values(definition.fields);\n while (true) {\n const discriminator = view.readByte();\n if (discriminator === 0) {\n return record;\n }\n const field = fields.find((f) => f.constantValue === discriminator);\n if (field === undefined) {\n view.index = end;\n return record;\n }\n record[field.name] = this.readField(field, view);\n }\n }\n\n private readField(field: Field, view: BebopView) {\n if (field.typeId >= 0) {\n const definition = this.schema.getDefinition(field.typeId);\n return this.readDefinition(definition, view);\n }\n switch (field.fieldProperties.type) {\n case \"scalar\":\n return this.readScalar(field.typeId, view);\n case \"array\":\n return this.readArray(\n field.fieldProperties,\n field.fieldProperties.depth,\n view\n );\n case \"map\":\n return this.readMap(field.fieldProperties, view);\n default:\n throw new BebopRuntimeError(\n `Unknown field type: ${field.fieldProperties}`\n );\n }\n }\n\n private readScalar(\n typeId: WireBaseType,\n view: BebopView\n ): boolean | number | string | Date | bigint | Guid {\n switch (typeId) {\n case WireBaseType.Bool:\n return !!view.readByte();\n case WireBaseType.Byte:\n return view.readByte();\n case WireBaseType.UInt16:\n return view.readUint16();\n case WireBaseType.Int16:\n return view.readInt16();\n case WireBaseType.UInt32:\n return view.readUint32();\n case WireBaseType.Int32:\n return view.readInt32();\n case WireBaseType.UInt64:\n return view.readUint64();\n case WireBaseType.Int64:\n return view.readInt64();\n case WireBaseType.Float32:\n return view.readFloat32();\n case WireBaseType.Float64:\n return view.readFloat64();\n case WireBaseType.String:\n return view.readString();\n case WireBaseType.Date:\n return view.readDate();\n case WireBaseType.Guid:\n return view.readGuid();\n default:\n throw new BebopRuntimeError(`Unknown scalar type: ${typeId}`);\n }\n }\n\n private readArray(\n field: FieldTypes,\n depth: number,\n view: BebopView\n ): Array<unknown> | Uint8Array {\n if (field.type !== \"array\") {\n throw new BebopRuntimeError(`Expected array field, got ${field.type}`);\n }\n const memberType = field.memberTypeId;\n // Recursive case: there is further nesting.\n if (depth > 0) {\n const length = view.readUint32();\n const array = new Array(length);\n for (let i = 0; i < length; i++) {\n array[i] = this.readArray(field, depth - 1, view);\n }\n return array;\n }\n // Base case: no further nesting. Decode items using the appropriate method.\n if (memberType === WireBaseType.Byte) {\n return view.readBytes();\n }\n let definition;\n if (memberType >= 0) {\n definition = this.schema.getDefinition(memberType);\n }\n const length = view.readUint32();\n const array = new Array(length);\n for (let i = 0; i < length; i++) {\n if (definition !== undefined) {\n array[i] = this.readDefinition(definition, view);\n } else {\n array[i] = this.readScalar(memberType, view);\n }\n }\n return array;\n }\n\n private readMap(\n field: FieldTypes,\n view: BebopView\n ): Map<unknown, unknown> | GuidMap<unknown> {\n if (field.type !== \"map\") {\n throw new BebopRuntimeError(`Expected map field, got ${field.type}`);\n }\n\n const keyType = field.keyTypeId;\n const valueType = field.valueTypeId;\n const map =\n field.keyTypeId === WireBaseType.Guid\n ? new GuidMap<unknown>()\n : new Map<unknown, unknown>();\n const size = view.readUint32();\n let definition;\n if (valueType >= 0) {\n definition = this.schema.getDefinition(valueType);\n }\n for (let i = 0; i < size; i++) {\n const key = this.readScalar(keyType, view);\n let value;\n if (definition !== undefined) {\n value = this.readDefinition(definition, view);\n } else if (field.nestedType !== undefined) {\n const nested = field.nestedType;\n if (nested.type === \"array\") {\n value = this.readArray(nested, nested.depth, view);\n } else if (nested.type === \"map\") {\n value = this.readMap(nested, view);\n }\n } else {\n value = this.readScalar(valueType, view);\n }\n if (value === undefined) {\n throw new BebopRuntimeError(`Error decoding map value for key ${key}`);\n }\n // @ts-ignore\n map.set(key, value);\n }\n return map;\n }\n\n private readEnumDefinition(\n definition: Enum,\n view: BebopView\n ): number | bigint {\n switch (definition.baseType) {\n case WireBaseType.Byte:\n return view.readByte();\n case WireBaseType.UInt16:\n return view.readUint16();\n case WireBaseType.Int16:\n return view.readInt16();\n case WireBaseType.UInt32:\n return view.readUint32();\n case WireBaseType.Int32:\n return view.readInt32();\n case WireBaseType.UInt64:\n return view.readUint64();\n case WireBaseType.Int64:\n return view.readInt64();\n default:\n throw new BebopRuntimeError(\n `Unknown enum base type: ${definition.baseType}`\n );\n }\n }\n\n private readUnionDefinition(definition: Union, view: BebopView) {\n const length = view.readMessageLength();\n const end = view.index + 1 + length;\n const discriminator = view.readByte();\n const branch = definition.branches.find(\n (b) => b.discriminator === discriminator\n );\n if (branch === undefined) {\n view.index = end;\n throw new BebopRuntimeError(`Unknown discriminator: ${discriminator}`);\n }\n return {\n discriminator,\n value: this.readDefinition(\n this.schema.getDefinition(branch.typeId),\n view\n ),\n };\n }\n}\n\n/**\n * A class responsible for writing a dynamic record into a Bebop buffer.\n * The class uses a binary schema provided during instantiation to encode the data.\n *\n * @example\n * const writer = binarySchema.writer;\n * const buffer = writer.write('DefinitionName', record);\n */\nexport class RecordWriter {\n /**\n * @param schema Binary schema used for encoding the data.\n * @private\n */\n private constructor(private schema: BinarySchema) { }\n /**\n * Encodes a given record according to a provided definition name and returns it as a Uint8Array.\n *\n * @param definitionName Name of the definition to be used for encoding.\n * @param record The record to be encoded.\n * @returns Encoded record as a Uint8Array.\n */\n public write(\n definitionName: string,\n record: Record<string, unknown>\n ): Uint8Array {\n const definition = this.schema.getDefinition(definitionName);\n const view = BebopView.getInstance();\n view.startWriting();\n this.writeDefinition(definition, view, record);\n return view.toArray();\n }\n\n private writeDefinition(\n definition: Definition,\n view: BebopView,\n record: unknown\n ): void {\n switch (definition.kind) {\n case WireTypeKind.Enum:\n this.writeEnumDefinition(definition as Enum, view, record);\n break;\n case WireTypeKind.Union:\n this.writeUnionDefinition(definition as Union, view, record);\n break;\n case WireTypeKind.Struct:\n this.writeStructDefinition(definition as Struct, view, record);\n break;\n case WireTypeKind.Message:\n this.writeMessageDefinition(definition as Message, view, record);\n break;\n }\n }\n\n private writeStructDefinition(\n definition: Struct,\n view: BebopView,\n record: unknown\n ): number {\n if (!this.isRecord(record)) {\n throw new BebopRuntimeError(`Expected object, got ${typeof record}`);\n }\n const before = view.length;\n Object.values(definition.fields).forEach((field) => {\n if (!(field.name in record)) {\n throw new BebopRuntimeError(`Missing field: ${field.name}`);\n }\n if (record[field.name] === undefined) {\n throw new BebopRuntimeError(`Field ${field.name} is undefined`);\n }\n this.writeField(field, view, record[field.name]);\n });\n const after = view.length;\n return after - before;\n }\n\n private writeMessageDefinition(\n definition: Message,\n view: BebopView,\n record: unknown\n ) {\n if (!this.isRecord(record)) {\n throw new BebopRuntimeError(`Expected object, got ${typeof record}`);\n }\n const before = view.length;\n const pos = view.reserveMessageLength();\n const start = view.length;\n Object.values(definition.fields).forEach((field) => {\n if (field.constantValue === undefined || field.constantValue === null) {\n throw new BebopRuntimeError(\n `Missing constant value for field: ${field.name}`\n );\n }\n if (typeof field.constantValue !== \"number\") {\n throw new BebopRuntimeError(\n `Expected number, got ${typeof field.constantValue} for field: ${field.name\n }`\n );\n }\n if (field.name in record && record[field.name] !== undefined) {\n view.writeByte(field.constantValue);\n this.writeField(field, view, record[field.name]);\n }\n });\n view.writeByte(0);\n const end = view.length;\n view.fillMessageLength(pos, end - start);\n const after = view.length;\n return after - before;\n }\n\n private writeEnumDefinition(\n definition: Enum,\n view: BebopView,\n value: unknown\n ): void {\n if (typeof value !== \"number\" && typeof value !== \"bigint\") {\n throw new BebopRuntimeError(\n `Expected number or bigint, got ${typeof value}`\n );\n }\n if (\n (definition.baseType === WireBaseType.Int64 ||\n definition.baseType === WireBaseType.UInt64) &&\n typeof value !== \"bigint\"\n ) {\n throw new BebopRuntimeError(`Expected bigint, got ${typeof value}`);\n }\n let valueFound = false;\n for (const member in definition.members) {\n if (definition.members[member].value === value) {\n valueFound = true;\n break;\n }\n }\n if (!valueFound) {\n throw new BebopRuntimeError(\n `Enum '${definition.name}' does not contain value: ${value}`\n );\n }\n switch (definition.baseType) {\n case WireBaseType.Byte:\n BebopTypeGuard.ensureUint8(value);\n view.writeByte(value as number);\n break;\n case WireBaseType.UInt16:\n BebopTypeGuard.ensureUint16(value);\n view.writeUint16(value as number);\n break;\n case WireBaseType.Int16:\n BebopTypeGuard.ensureInt16(value);\n view.writeInt16(value as number);\n break;\n case WireBaseType.UInt32:\n BebopTypeGuard.ensureUint32(value);\n view.writeUint32(value as number);\n break;\n case WireBaseType.Int32:\n BebopTypeGuard.ensureInt32(value);\n view.writeInt32(value as number);\n break;\n case WireBaseType.UInt64:\n BebopTypeGuard.ensureUint64(value);\n view.writeUint64(value as bigint);\n break;\n case WireBaseType.Int64:\n BebopTypeGuard.ensureInt64(value);\n view.writeInt64(value as bigint);\n break;\n default:\n throw new BebopRuntimeError(\n `Unknown enum base type: ${definition.baseType}`\n );\n }\n }\n\n private writeUnionDefinition(\n definition: Union,\n view: BebopView,\n record: unknown\n ): number {\n if (record === null || record === undefined || typeof record !== \"object\") {\n throw new BebopRuntimeError(`Expected non-null object value`);\n }\n if (\n !(\"discriminator\" in record && typeof record.discriminator === \"number\")\n ) {\n throw new BebopRuntimeError(`Expected number 'discriminator' property`);\n }\n if (\n !(\n \"value\" in record &&\n record.value !== null &&\n typeof record.value === \"object\"\n )\n ) {\n throw new BebopRuntimeError(`Expected 'value' property`);\n }\n const branch = definition.branches.find(\n (b) => b.discriminator === record.discriminator\n );\n if (branch === undefined) {\n throw new BebopRuntimeError(\n `No branch found for discriminator: ${record.discriminator}`\n );\n }\n const branchDefinition = this.schema.getDefinition(branch.typeId);\n\n const before = view.length;\n const pos = view.reserveMessageLength();\n const start = view.length + 1;\n view.writeByte(record.discriminator);\n this.writeDefinition(branchDefinition, view, record.value);\n const end = view.length;\n view.fillMessageLength(pos, end - start);\n const after = view.length;\n return after - before;\n }\n\n private writeField(field: Field, view: BebopView, value: unknown): void {\n if (field.typeId >= 0) {\n const definition = this.schema.getDefinition(field.typeId);\n this.writeDefinition(definition, view, value);\n return;\n }\n switch (field.fieldProperties.type) {\n case \"scalar\":\n this.writeScalar(field.typeId, view, value);\n break;\n case \"array\":\n this.writeArray(\n field.fieldProperties,\n field.fieldProperties.depth,\n view,\n value\n );\n break;\n case \"map\":\n this.writeMap(field.fieldProperties, view, value);\n break;\n default:\n throw new BebopRuntimeError(\n `Unknown field type: ${field.fieldProperties}`\n );\n }\n }\n\n private writeArray(\n field: FieldTypes,\n depth: number,\n view: BebopView,\n value: unknown\n ): void {\n if (field.type !== \"array\") {\n throw new BebopRuntimeError(`Expected array field, got ${field.type}`);\n }\n if (!Array.isArray(value) && !(value instanceof Uint8Array)) {\n throw new BebopRuntimeError(`Expected array, got ${typeof value}`);\n }\n if (\n field.memberTypeId === WireBaseType.Byte &&\n !(value instanceof Uint8Array)\n ) {\n throw new BebopRuntimeError(`Expected Uint8Array, got ${typeof value}`);\n }\n\n const memberType = field.memberTypeId;\n const length = value.length;\n // Recursive case: there is further nesting.\n if (depth > 0) {\n view.writeUint32(length);\n for (let i = 0; i < length; i++) {\n this.writeArray(field, depth - 1, view, value[i]);\n }\n return;\n }\n // Base case: no further nesting. Encode items using the appropriate method.\n if (memberType === WireBaseType.Byte) {\n view.writeBytes(value as Uint8Array);\n } else {\n view.writeUint32(length);\n let definition;\n if (memberType >= 0) {\n definition = this.schema.getDefinition(memberType);\n }\n for (let i = 0; i < length; i++) {\n if (definition !== undefined) {\n this.writeDefinition(definition, view, value[i]);\n } else {\n this.writeScalar(memberType, view, value[i]);\n }\n }\n }\n }\n\n private writeMap(field: FieldTypes, view: BebopView, value: unknown): void {\n if (field.type !== \"map\") {\n throw new BebopRuntimeError(`Expected map field, got ${field.type}`);\n }\n if (!(value instanceof Map || value instanceof GuidMap)) {\n throw new BebopRuntimeError(`Expected Map, got ${typeof value}`);\n }\n const keyType = field.keyTypeId;\n const valueType = field.valueTypeId;\n const size = value.size;\n view.writeUint32(size);\n let definition;\n if (valueType >= 0) {\n definition = this.schema.getDefinition(valueType);\n }\n for (const [k, v] of value.entries()) {\n this.writeScalar(keyType, view, k);\n if (definition !== undefined) {\n this.writeDefinition(definition, view, v);\n } else if (field.nestedType !== undefined) {\n const nested = field.nestedType;\n if (nested.type === \"array\") {\n this.writeArray(\n nested,\n nested.depth,\n view,\n v as Array<unknown> | Uint8Array\n );\n } else if (nested.type === \"map\") {\n this.writeMap(\n nested,\n view,\n v as Map<unknown, unknown> | GuidMap<unknown>\n );\n }\n } else {\n this.writeScalar(valueType, view, v);\n }\n }\n }\n\n private writeScalar(typeId: WireBaseType, view: BebopView, value: unknown) {\n switch (typeId) {\n case WireBaseType.Bool:\n BebopTypeGuard.ensureBoolean(value);\n view.writeByte(Number(value));\n break;\n case WireBaseType.Byte:\n BebopTypeGuard.ensureUint8(value);\n view.writeByte(value as number);\n break;\n case WireBaseType.UInt16:\n BebopTypeGuard.ensureUint16(value);\n view.writeUint16(value as number);\n break;\n case WireBaseType.Int16:\n BebopTypeGuard.ensureInt16(value);\n view.writeInt16(value as number);\n break;\n case WireBaseType.UInt32:\n BebopTypeGuard.ensureUint32(value);\n view.writeUint32(value as number);\n break;\n case WireBaseType.Int32:\n BebopTypeGuard.ensureInt32(value);\n view.writeInt32(value as number);\n break;\n case WireBaseType.UInt64:\n BebopTypeGuard.ensureUint64(value as bigint);\n view.writeUint64(value as bigint);\n break;\n case WireBaseType.Int64:\n BebopTypeGuard.ensureInt64(value as bigint);\n view.writeInt64(value as bigint);\n break;\n case WireBaseType.Float32:\n BebopTypeGuard.ensureFloat(value);\n view.writeFloat32(value as number);\n break;\n case WireBaseType.Float64:\n BebopTypeGuard.ensureFloat(value);\n view.writeFloat64(value as number);\n break;\n case WireBaseType.String:\n BebopTypeGuard.ensureString(value);\n view.writeString(value as string);\n break;\n case WireBaseType.Guid:\n BebopTypeGuard.ensureGuid(value);\n view.writeGuid(value as Guid);\n break;\n case WireBaseType.Date:\n BebopTypeGuard.ensureDate(value);\n view.writeDate(value as Date);\n break;\n default:\n throw new BebopRuntimeError(`Unknown scalar type: ${typeId}`);\n }\n }\n\n private isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\";\n }\n}\n\n/**\n * `BinarySchema` represents a class that allows parsing of a Bebop schema in binary form.\n *\n * This class holds the DataView representation of the binary data, its parsing position,\n * and contains methods to get each specific type of Bebop schema structure.\n */\nexport class BinarySchema {\n private readonly view: DataView;\n private readonly dataProxy: Uint8Array;\n private pos: number;\n private readonly ArrayType = -14;\n private readonly MapType = -15;\n private parsedSchema?: SchemaAst;\n private indexToDefinition: { [index: number]: Definition; } = {};\n private nameToDefinition: { [name: string]: Definition; } = {};\n public reader: RecordReader;\n public writer: RecordWriter;\n\n /**\n * Create a new BinarySchema instance.\n * @param data - The binary data array.\n */\n constructor(private readonly data: Uint8Array) {\n // copy the data to prevent modification\n //this.data = data.subarray(0, data.length);\n this.view = new DataView(this.data.buffer);\n this.pos = 0;\n //@ts-expect-error\n this.reader = new RecordReader(this);\n //@ts-expect-error\n this.writer = new RecordWriter(this);\n this.dataProxy = new Proxy(this.data, {\n get: (target: Uint8Array, prop: PropertyKey): any => {\n // If prop is 'length', return the length of the Uint8Array\n if (prop === \"length\") {\n return target.length;\n }\n // If prop is a number-like string, convert it to a number and return the element at that index in the Uint8Array\n if (typeof prop === \"string\" && !isNaN(Number(prop))) {\n return target[Number(prop)];\n }\n // If prop is the name of a method of Uint8Array, return the function\n if (\n typeof prop === \"string\" &&\n typeof (target as any)[prop] === \"function\"\n ) {\n return (target as any)[prop].bind(target);\n }\n // Optionally, you can throw an error or return undefined for all other properties\n throw new BebopRuntimeError(`Cannot access property ${String(prop)}`);\n },\n set: (_: Uint8Array, __: PropertyKey, ___: any): boolean => {\n throw new BebopRuntimeError(\"Cannot modify schema data\");\n },\n });\n }\n\n /**\n * Get the schema.\n * This method should only be called once per instance.\n */\n public get(): void {\n if (this.parsedSchema !== undefined) {\n return;\n }\n\n const schemaVersion = this.getUint8();\n const numDefinedTypes = this.getUint32();\n\n let definedTypes: { [typeName: string]: Definition; } = {};\n for (let i = 0; i < numDefinedTypes; i++) {\n const def = this.getDefinedType(i);\n definedTypes[def.name] = def;\n this.indexToDefinition[i] = def;\n this.nameToDefinition[def.name] = def;\n }\n\n const serviceCount = this.getUint32();\n let services: { [serviceName: string]: Service; } = {};\n\n for (let i = 0; i < serviceCount; i++) {\n const service = this.getServiceDefinition();\n services[service.name] = service;\n }\n this.parsedSchema = {\n bebopVersion: schemaVersion,\n definitions: definedTypes,\n services,\n };\n Object.freeze(this.parsedSchema);\n }\n\n /**\n * Returns the getd schema.\n */\n public get ast(): Readonly<SchemaAst> {\n if (this.parsedSchema === undefined) {\n this.get();\n }\n return this.parsedSchema!;\n }\n /**\n * Returns the raw binary data of the schema wrapped in an immutable Uint8Array.\n */\n public get raw(): Uint8Array {\n return this.dataProxy;\n }\n\n /**\n * Get a Definition by its index or name.\n * @param index - The index or name of the Definition.\n * @returns - The requested Definition.\n * @throws - Will throw an error if no Definition is found at the provided index.\n */\n public getDefinition(index: number | string): Definition {\n const definition =\n typeof index === \"number\"\n ? this.indexToDefinition[index]\n : this.nameToDefinition[index];\n if (!definition) {\n throw new BebopRuntimeError(`No definition found at index: ${index}`);\n }\n return definition;\n }\n\n private getDefinedType(index: number): Definition {\n const name = this.getString();\n const kind = this.getUint8() as WireTypeKind;\n const decorators = this.getDecorators();\n switch (kind) {\n case WireTypeKind.Enum:\n return this.getEnumDefinition(name, kind, decorators, index);\n case WireTypeKind.Union:\n return this.getUnionDefinition(name, kind, decorators, index);\n case WireTypeKind.Struct:\n return this.getStructDefinition(name, kind, decorators, index);\n case WireTypeKind.Message:\n return this.getMessageDefinition(name, kind, decorators, index);\n default:\n throw new BebopRuntimeError(`Unknown type kind: ${kind}`);\n }\n }\n\n private getDecorators(): Decorators {\n const decoratorCount = this.getUint8();\n const decorators: Decorators = [];\n for (let i = 0; i < decoratorCount; i++) {\n const identifier = this.getString();\n decorators.push({\n identifier,\n ...this.getDecorator(),\n });\n }\n return decorators;\n }\n\n private getDecorator(): Omit<Decorator, 'identifier'> {\n const argCount = this.getUint8();\n const args: { [name: string]: DecoratorArgument; } = {};\n for (let i = 0; i < argCount; i++) {\n const identifier = this.getString();\n const typeId = this.getTypeId();\n const argumentValue = this.getConstantValue(typeId);\n args[identifier] = {\n typeId,\n value: argumentValue,\n };\n }\n return { arguments: args };\n }\n\n private getEnumDefinition(\n name: string,\n kind: WireTypeKind,\n decorators: Decorators,\n index: number\n ): Enum {\n const baseType = this.getTypeId();\n const isBitFlags = this.getBool();\n const minimalEncodeSize = this.getInt32();\n const memberCount = this.getUint8();\n const members: { [name: string]: EnumMember; } = {};\n for (let i = 0; i < memberCount; i++) {\n const member = this.getEnumMember(baseType);\n members[member.name] = member;\n }\n return {\n index,\n name: name,\n isBitFlags,\n kind: kind,\n decorators: decorators,\n minimalEncodeSize,\n baseType,\n members,\n };\n }\n\n private getEnumMember(baseType: number): EnumMember {\n const name = this.getString();\n const decorators = this.getDecorators();\n const value = this.getConstantValue(baseType) as number;\n return { name, decorators, value };\n }\n\n private getUnionDefinition(\n name: string,\n kind: WireTypeKind,\n decorators: Decorators,\n index: number\n ): Union {\n const minimalEncodeSize = this.getInt32();\n const branchCount = this.getUint8();\n const branches = new Array(branchCount)\n .fill(null)\n .map(() => this.getUnionBranch());\n return {\n index,\n name: name,\n kind: kind,\n decorators: decorators,\n minimalEncodeSize,\n branchCount,\n branches,\n };\n }\n\n private getUnionBranch(): UnionBranch {\n const discriminator = this.getUint8();\n const typeId = this.getTypeId();\n return { discriminator, typeId };\n }\n\n private getStructDefinition(\n name: string,\n kind: WireTypeKind,\n decorators: Decorators,\n index: number\n ): Struct {\n const isMutable = this.getBool();\n const minimalEncodeSize = this.getInt32();\n const isFixedSize = this.getBool();\n const fields = this.getFields(kind);\n return {\n index,\n name: name,\n kind: kind,\n decorators: decorators,\n isMutable,\n minimalEncodeSize,\n isFixedSize,\n fields,\n };\n }\n\n private getMessageDefinition(\n name: string,\n kind: WireTypeKind,\n decorators: Decorators,\n index: number\n ): Message {\n const minimalEncodeSize = this.getInt32();\n const fields = this.getFields(kind);\n return {\n index,\n minimalEncodeSize,\n name: name,\n kind: kind,\n decorators: decorators,\n fields,\n };\n }\n\n private getFields(parentKind: WireTypeKind): { [name: string]: Field; } {\n const numFields = this.getUint8();\n const fields: { [name: string]: Field; } = {};\n for (let i = 0; i < numFields; i++) {\n const field = this.getField(parentKind);\n fields[field.name] = field;\n }\n return fields;\n }\n\n private getField(parentKind: WireTypeKind): Field {\n const fieldName = this.getString();\n let fieldTypeId = this.getTypeId();\n let fieldProperties: FieldTypes;\n\n if (fieldTypeId === this.ArrayType || fieldTypeId === this.MapType) {\n fieldProperties = this.getNestedType(\n fieldTypeId === this.ArrayType ? \"array\" : \"map\"\n );\n } else {\n fieldProperties = { type: \"scalar\" };\n }\n\n const decorators = this.getDecorators();\n const constantValue = (\n parentKind === WireTypeKind.Message\n ? this.getConstantValue(WireBaseType.Byte)\n : null\n ) as any;\n\n return {\n name: fieldName,\n typeId: fieldTypeId,\n fieldProperties,\n decorators: decorators,\n constantValue,\n };\n }\n\n private getNestedType(parentType: string): FieldTypes {\n if (parentType === \"array\") {\n const depth = this.getUint8();\n const memberTypeId = this.getTypeId();\n return { type: parentType, memberTypeId: memberTypeId, depth };\n }\n\n if (parentType === \"map\") {\n const keyTypeId = this.getTypeId();\n const valueTypeId = this.getTypeId();\n\n let nestedType: FieldTypes | undefined;\n if (valueTypeId === this.ArrayType || valueTypeId === this.MapType) {\n nestedType = this.getNestedType(\n valueTypeId === this.ArrayType ? \"array\" : \"map\"\n );\n }\n return {\n type: parentType,\n keyTypeId,\n valueTypeId: valueTypeId,\n nestedType,\n };\n }\n\n throw new BebopRuntimeError(\"Invalid initial type\");\n }\n\n private getConstantValue(\n typeId: number\n ): string | number | bigint | Guid | null {\n switch (typeId) {\n case WireBaseType.Bool:\n return this.getBool() ? 1 : 0;\n case WireBaseType.Byte:\n return this.getUint8();\n case WireBaseType.UInt16:\n return this.getUint16();\n case WireBaseType.Int16:\n return this.getInt16();\n case WireBaseType.UInt32:\n return this.getUint32();\n case WireBaseType.Int32:\n return this.getInt32();\n case WireBaseType.UInt64:\n return BigInt(this.getUint64()) as bigint;\n case WireBaseType.Int64:\n return BigInt(this.getInt64());\n case WireBaseType.Float32:\n return this.getFloat32();\n case WireBaseType.Float64:\n return this.getFloat64();\n case WireBaseType.String:\n return this.getString();\n case WireBaseType.Guid:\n return Guid.fromBytes(this.getGuid(), 0);\n default:\n throw new BebopRuntimeError(`Unsupported constant type ID: ${typeId}`);\n }\n }\n\n private getServiceDefinition(): Service {\n let name = this.getString();\n let decorators = this.getDecorators();\n let methods: { [name: string]: ServiceMethod; } = {};\n let methodCount = this.getUint32();\n for (let i = 0; i < methodCount; i++) {\n let methodName = this.getString();\n let methodDecorators = this.getDecorators();\n let methodType = this.getUint8() as WireMethodType;\n let requestTypeId = this.getTypeId();\n let responseTypeId = this.getTypeId();\n let id = this.getUint32();\n methods[methodName] = {\n name: methodName,\n decorators: methodDecorators,\n methodType: methodType,\n requestTypeId: requestTypeId,\n responseTypeId: responseTypeId,\n id: id,\n };\n }\n return {\n name: name,\n decorators: decorators,\n methods: methods,\n };\n }\n\n private getString(): string {\n const start = this.pos;\n while (this.pos < this.data.length && this.data[this.pos] !== 0) {\n this.pos++;\n }\n const strBytes = this.data.subarray(start, this.pos);\n // Skip the null terminator\n if (this.pos < this.data.length) {\n this.pos++;\n }\n return decoder.decode(strBytes);\n }\n\n private getUint8() {\n let value = this.view.getUint8(this.pos);\n this.pos++;\n return value;\n }\n\n private getUint16() {\n let value = this.view.getUint16(this.pos, true);\n this.pos += 2;\n return value;\n }\n\n private getInt16() {\n let value = this.view.getInt16(this.pos, true);\n this.pos += 2;\n return value;\n }\n\n private getUint32() {\n let value = this.view.getUint32(this.pos, true);\n this.pos += 4;\n return value;\n }\n\n private getInt32() {\n let value = this.view.getInt32(this.pos, true);\n this.pos += 4;\n return value;\n }\n\n private getUint64() {\n let value = this.view.getBigUint64(this.pos, true);\n this.pos += 8;\n return Number(value);\n }\n\n private getInt64() {\n let value = this.view.getBigInt64(this.pos, true);\n this.pos += 8;\n return Number(value);\n }\n\n private getFloat32() {\n let value = this.view.getFloat32(this.pos, true);\n this.pos += 4;\n return value;\n }\n\n private getFloat64() {\n let value = this.view.getFloat64(this.pos, true);\n this.pos += 8;\n return value;\n }\n\n private getBool() {\n return this.getUint8() !== 0;\n }\n\n private getTypeId() {\n let typeId = this.view.getInt32(this.pos, true);\n this.pos += 4;\n return typeId;\n }\n\n private getGuid() {\n let value = this.data.subarray(this.pos, this.pos + 16);\n this.pos += 16;\n return value;\n }\n}\n","import { BinarySchema } from \"./binary\";\n\nconst hexDigits: string = \"0123456789abcdef\";\nconst asciiToHex: Array<number> = [\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,\n 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\n];\n\nconst guidDelimiter: string = \"-\";\nconst ticksBetweenEpochs: bigint = 621355968000000000n;\nconst dateMask: bigint = 0x3fffffffffffffffn;\nconst emptyByteArray: Uint8Array = new Uint8Array(0);\nconst emptyString: string = \"\";\nconst byteToHex: Array<string> = []; // A lookup table: ['00', '01', ..., 'ff']\nfor (const x of hexDigits) {\n for (const y of hexDigits) {\n byteToHex.push(x + y);\n }\n}\n\n// Cache the check for Crypto.getRandomValues\nconst hasCryptoGetRandomValues = typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function';\n\nexport class BebopRuntimeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BebopRuntimeError\";\n }\n}\n\n\n\n/**\n * Represents a globally unique identifier (GUID).\n */\nexport class Guid {\n public static readonly empty: Guid = new Guid(\"00000000-0000-0000-0000-000000000000\");\n /**\n * Constructs a new Guid object with the specified value.\n * @param value The value of the GUID.\n */\n private constructor(private readonly value: string) { }\n\n /**\n * Gets the string value of the Guid.\n * @returns The string representation of the Guid.\n */\n public toString(): string {\n return this.value;\n }\n\n /**\n * Checks if the Guid is empty.\n * @returns true if the Guid is empty, false otherwise.\n */\n public isEmpty(): boolean {\n return this.value === Guid.empty.value;\n }\n\n /**\n * Checks if a value is a Guid.\n * @param value The value to be checked.\n * @returns true if the value is a Guid, false otherwise.\n */\n public static isGuid(value: any): value is Guid {\n return value instanceof Guid;\n }\n\n /**\n * Parses a string into a Guid.\n * @param value The string to be parsed.\n * @returns A new Guid that represents the parsed value.\n * @throws {BebopRuntimeError} If the input string is not a valid Guid.\n */\n public static parseGuid(value: string): Guid {\n let cleanedInput = '';\n let count = 0;\n\n // Iterate through each character in the input\n for (let i = 0; i < value.length; i++) {\n let ch = value[i].toLowerCase();\n if (hexDigits.indexOf(ch) !== -1) {\n // If the character is a hexadecimal digit, add it to cleanedInput\n cleanedInput += ch;\n count++;\n } else if (ch !== '-') {\n // If the character is not a hexadecimal digit or a hyphen, it's invalid\n throw new BebopRuntimeError(`Invalid GUID: ${value}`);\n }\n }\n // If the count is not 32, the input is not a valid GUID\n if (count !== 32) {\n throw new BebopRuntimeError(`Invalid GUID: ${value}`);\n }\n // Insert hyphens to make it a 8-4-4-4-12 character pattern\n const guidString =\n cleanedInput.slice(0, 8) + '-' +\n cleanedInput.slice(8, 12) + '-' +\n cleanedInput.slice(12, 16) + '-' +\n cleanedInput.slice(16, 20) + '-' +\n cleanedInput.slice(20);\n // Construct a new Guid object with the generated string and return it\n return new Guid(guidString);\n }\n\n /**\n * Creates a an insecure new Guid using Math.random.\n * @returns A new Guid.\n */\n public static newGuid(): Guid {\n let guid = \"\";\n // Obtain a single timestamp to help seed randomness\n const now = Date.now();\n\n // Iterate through the 36 characters of a UUID\n for (let i = 0; i < 36; i++) {\n // Insert hyphens at the appropriate indices (8, 13, 18, 23)\n if (i === 8 || i === 13 || i === 18 || i === 23) {\n guid += \"-\";\n }\n // According to the UUID v4 spec, the 14th character should be '4'\n else if (i === 14) {\n guid += \"4\";\n }\n // According to the UUID v4 spec, the 19th character should be one of '8', '9', 'a', or 'b'.\n // Here we're using 'a' or 'b' to simplify the code\n else if (i === 19) {\n guid += Math.random() > 0.5 ? \"a\" : \"b\";\n }\n // Generate the rest of the UUID using random hexadecimal digits\n else {\n // Add the current time to the random number to seed it, then modulo by 16 to get a number between 0 and 15\n // Use bitwise OR 0 to round the result down to an integer, and get the hexadecimal digit from the lookup table\n guid += hexDigits[(Math.random() * 16 + now) % 16 | 0];\n }\n }\n // Construct a new Guid object with the generated string and return it\n return new Guid(guid);\n }\n\n /**\n * Creates a new cryptographically secure Guid using Crypto.getRandomValues.\n * @returns A new secure Guid.\n * @throws {BebopRuntimeError} If Crypto.getRandomValues is not available.\n */\n public static newSecureGuid(): Guid {\n if (!hasCryptoGetRandomValues) {\n throw new BebopRuntimeError(\n \"Crypto.getRandomValues is not available. \" +\n \"Please include a polyfill or use in an environment that supports it.\"\n );\n }\n\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n\n // Set the version (4) and variant (RFC4122)\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n return Guid.fromBytes(bytes, 0);\n }\n\n /**\n * Checks if the Guid is equal to another Guid.\n * @param other The other Guid to be compared with.\n * @returns true if the Guids are equal, false otherwise.\n */\n public equals(other: Guid): boolean {\n // Check if both GUIDs are the same instance\n if (this === other) {\n return true;\n }\n\n // Check if the other object is a GUID\n if (!(other instanceof Guid)) {\n return false;\n }\n\n // Compare the hexadecimal representations of both GUIDs\n for (let i = 0; i < this.value.length; i++) {\n if (this.value[i] !== other.value[i]) {\n return false;\n }\n }\n // All hexadecimal digits are equal, so the GUIDs are equal\n return true;\n }\n\n /**\n * Writes the Guid to a DataView.\n * @param view The DataView to write to.\n * @param length The position to start writing at.\n */\n public writeToView(view: DataView, length: number): void {\n var p = 0, a = 0;\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n p += (this.value.charCodeAt(p) === 45) as any;\n view.setUint32(length, a, true);\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n p += (this.value.charCodeAt(p) === 45) as any;\n view.setUint16(length + 4, a, true);\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n p += (this.value.charCodeAt(p) === 45) as any;\n view.setUint16(length + 6, a, true);\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n p += (this.value.charCodeAt(p) === 45) as any;\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n view.setUint32(length + 8, a, false);\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];\n view.setUint32(length + 12, a, false);\n }\n\n /**\n * Creates a Guid from a byte array.\n * @param buffer The byte array to create the Guid from.\n * @param index The position in the array to start reading from.\n * @returns A new Guid that represents the byte array.\n */\n public static fromBytes(buffer: Uint8Array, index: number): Guid {\n // Order: 3 2 1 0 - 5 4 - 7 6 - 8 9 - a b c d e f\n var s = byteToHex[buffer[index + 3]];\n s += byteToHex[buffer[index + 2]];\n s += byteToHex[buffer[index + 1]];\n s += byteToHex[buffer[index]];\n s += guidDelimiter;\n s += byteToHex[buffer[index + 5]];\n s += byteToHex[buffer[index + 4]];\n s += guidDelimiter;\n s += byteToHex[buffer[index + 7]];\n s += byteToHex[buffer[index + 6]];\n s += guidDelimiter;\n s += byteToHex[buffer[index + 8]];\n s += byteToHex[buffer[index + 9]];\n s += guidDelimiter;\n s += byteToHex[buffer[index + 10]];\n s += byteToHex[buffer[index + 11]];\n s += byteToHex[buffer[index + 12]];\n s += byteToHex[buffer[index + 13]];\n s += byteToHex[buffer[index + 14]];\n s += byteToHex[buffer[index + 15]];\n return new Guid(s);\n }\n\n /**\n * Converts the Guid to a string when it's used as a primitive.\n * @returns The string representation of the Guid.\n */\n [Symbol.toPrimitive](hint: string): string {\n if (hint === \"string\" || hint === \"default\") {\n return this.toString();\n }\n throw new Error(`Guid cannot be converted to ${hint}`);\n }\n}\n\n\n/**\n * Represents a wrapper around the `Map` class with support for using `Guid` instances as keys.\n *\n * 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.\n * The class handles converting `Guid` instances to their string representation for key storage and retrieval.\n * @remarks this is required because Javascript lacks true reference equality. Thus two `Guid` instances with the same value are not