let int32 = new Int32Array(1) let float32 = new Float32Array(int32.buffer) const textDecoder = new TextDecoder() export class ByteBuffer { private _data: Uint8Array private _index: number length: number constructor(data?: Uint8Array) { if (data && !(data instanceof Uint8Array)) { throw new Error('Must initialize a ByteBuffer with a Uint8Array') } this._data = data || new Uint8Array(256) this._index = 0 this.length = data ? data.length : 0 } get offset(): number { return this._index } set offset(value: number) { this._index = value } /** The bytes this buffer reads, as a view rather than a copy. */ get bytes(): Uint8Array { return this._data } /** * Returns a view into the internal buffer, not a copy. * * Consumers transferring this Uint8Array across thread boundaries (e.g. via * postMessage with transferables) MUST copy first: `new Uint8Array(buffer)`. * Otherwise, if multiple Uint8Arrays share the same underlying ArrayBuffer and * one is transferred, all views into that buffer become detached. */ toUint8Array(): Uint8Array { return this._data.subarray(0, this.length) } readByte(): number { return this._data[this._index++] } readByteArray(): Uint8Array { const length = this.readVarUint() const start = this._index this._index = start + length return this._data.slice(start, start + length) } skipByteArray(): void { const length = this.readVarUint() this._index += length } readVarFloat(): number { const index = this._index const data = this._data const first = data[index] if (first === 0) { this._index = index + 1 return 0 } let bits = first | (data[index + 1] << 8) | (data[index + 2] << 16) | (data[index + 3] << 24) this._index = index + 4 bits = (bits << 23) | (bits >>> 9) int32[0] = bits return float32[0] } readVarUint(): number { const data = this._data let i = this._index let b = data[i++] let value = b & 127 if (b < 128) { this._index = i return value } b = data[i++] value |= (b & 127) << 7 if (b < 128) { this._index = i return value } b = data[i++] value |= (b & 127) << 14 if (b < 128) { this._index = i return value } b = data[i++] value |= (b & 127) << 21 if (b < 128) { this._index = i return value } b = data[i++] value |= (b & 127) << 28 this._index = i return value >>> 0 } readVarInt(): number { let value = this.readVarUint() | 0 return value & 1 ? ~(value >>> 1) : value >>> 1 } readVarUint64(): bigint { let value = BigInt(0) let shift = BigInt(0) let seven = BigInt(7) let byte = this.readByte() while (byte & 128 && shift < 56) { value |= BigInt(byte & 127) << shift shift += seven byte = this.readByte() } value |= BigInt(byte) << shift return value } readVarInt64(): bigint { let value = this.readVarUint64() let one = BigInt(1) let sign = value & one value >>= one return sign ? ~value : value } readString(): string { const start = this._index const i = this.findStringTerminator(start) this._index = i + 1 return textDecoder.decode(this._data.subarray(start, i)) } skipString(): void { this._index = this.findStringTerminator(this._index) + 1 } private findStringTerminator(start: number): number { const data = this._data let index = start while (index < data.length && data[index] !== 0) index++ if (index >= data.length) throw new Error('Unterminated string in Kiwi message') return index } private _growBy(amount: number): void { if (this.length + amount > this._data.length) { let data = new Uint8Array((this.length + amount) << 1) data.set(this._data) this._data = data } this.length += amount } writeByte(value: number): void { let index = this.length this._growBy(1) this._data[index] = value } writeByteArray(value: Uint8Array): void { if (!(value instanceof Uint8Array) && !Array.isArray(value)) { throw new Error('Expected byte array') } this.writeVarUint(value.length) let index = this.length this._growBy(value.length) this._data.set(value, index) } writeVarFloat(value: number): void { let index = this.length // Reinterpret as an integer float32[0] = value let bits = int32[0] // Move the exponent to the first 8 bits bits = (bits >>> 23) | (bits << 9) // Optimization: use a single byte to store zero and denormals (check for an exponent of 0) if ((bits & 255) === 0) { this.writeByte(0) return } // Endian-independent 32-bit write this._growBy(4) let data = this._data data[index] = bits data[index + 1] = bits >> 8 data[index + 2] = bits >> 16 data[index + 3] = bits >> 24 } writeVarUint(value: number): void { if (value < 0 || value > 0xffff_ffff) throw new Error('Outside uint range: ' + value) do { let byte = value & 127 value >>>= 7 this.writeByte(value ? byte | 128 : byte) } while (value) } writeVarInt(value: number): void { if (value < -0x8000_0000 || value > 0x7fff_ffff) throw new Error('Outside int range: ' + value) this.writeVarUint(((value << 1) ^ (value >> 31)) >>> 0) } writeVarUint64(value: bigint | string): void { if (typeof value === 'string') value = BigInt(value) else if (typeof value !== 'bigint') throw new Error(`Expected bigint but got ${typeof value}: ${String(value)}`) if (value < 0 || value > BigInt('0xFFFFFFFFFFFFFFFF')) throw new Error('Outside uint64 range: ' + value) let mask = BigInt(127) let seven = BigInt(7) for (let i = 0; value > mask && i < 8; i++) { this.writeByte(Number(value & mask) | 128) value >>= seven } this.writeByte(Number(value)) } writeVarInt64(value: bigint | string): void { if (typeof value === 'string') value = BigInt(value) else if (typeof value !== 'bigint') throw new Error(`Expected bigint but got ${typeof value}: ${String(value)}`) if (value < -BigInt('0x8000000000000000') || value > BigInt('0x7FFFFFFFFFFFFFFF')) throw new Error('Outside int64 range: ' + value) let one = BigInt(1) this.writeVarUint64(value < 0 ? ~(value << one) : value << one) } writeString(value: string): void { let codePoint for (let i = 0; i < value.length; i++) { // Decode UTF-16 let a = value.charCodeAt(i) if (i + 1 === value.length || a < 0xd800 || a >= 0xdc00) { codePoint = a } else { let b = value.charCodeAt(++i) codePoint = (a << 10) + b + (0x10000 - (0xd800 << 10) - 0xdc00) } // Strings are null-terminated if (codePoint === 0) { throw new Error('Cannot encode a string containing the null character') } // Encode UTF-8 if (codePoint < 0x80) { this.writeByte(codePoint) } else { if (codePoint < 0x800) { this.writeByte(((codePoint >> 6) & 0x1f) | 0xc0) } else { if (codePoint < 0x10000) { this.writeByte(((codePoint >> 12) & 0x0f) | 0xe0) } else { this.writeByte(((codePoint >> 18) & 0x07) | 0xf0) this.writeByte(((codePoint >> 12) & 0x3f) | 0x80) } this.writeByte(((codePoint >> 6) & 0x3f) | 0x80) } this.writeByte((codePoint & 0x3f) | 0x80) } } // Strings are null-terminated this.writeByte(0) } }