UNPKG

@foxglove/ulog

Version:
247 lines (206 loc) 7.38 kB
import { Filelike } from "./file"; const CHUNK_SIZE = 256 * 1024; /** * ChunkedReader provides functions to read typed data from a Filelike. * * It amortizes the cost loading data from the Filelike by reading chunks (pages) of data into * memory when reading typed data. */ export class ChunkedReader { readonly chunkSize: number; #file: Filelike; #chunk?: Uint8Array; #view?: DataView; /** Position in the file where we read the next chunk */ #fileCursor = 0; /** Position in the current chunk */ #chunkCursor = 0; #textDecoder = new TextDecoder(); constructor(filelike: Filelike, chunkSize = CHUNK_SIZE) { this.#file = filelike; this.chunkSize = chunkSize; } /** * @deprecated You should open the Filelike yourself before using ChunkedReader */ async open(): Promise<number> { return await this.#file.open(); } view(): DataView | undefined { return this.#view; } position(): number { return this.#fileCursor - (this.#chunk?.byteLength ?? 0) + this.#chunkCursor; } size(): number { return this.#file.size(); } remaining(): number { return this.size() - (this.#fileCursor - (this.#chunk?.byteLength ?? 0) + this.#chunkCursor); } seek(relativeByteOffset: number): void { const byteOffset = this.position() + relativeByteOffset; if (byteOffset < 0 || byteOffset > this.size()) { throw new Error(`Cannot seek to ${byteOffset}`); } // If we have a chunk it is more performant to attempt re-using the chunk. So we try to figure // out if our seekTo puts us within the chunk and if so adjust the chunkCursor. if (this.#chunk) { // This is where the chunk starts in the file const chunkStart = this.#fileCursor - this.#chunk.byteLength; if (byteOffset >= chunkStart && byteOffset < this.#fileCursor) { this.#chunkCursor = byteOffset - chunkStart; return; } } this.#fileCursor = byteOffset; this.#chunkCursor = 0; this.#chunk = undefined; this.#view = undefined; } seekTo(byteOffset: number): void { if (byteOffset < 0 || byteOffset > this.size()) { throw new Error(`Cannot seek to ${byteOffset}`); } // If we have a chunk it is more performant to attempt re-using the chunk. So we try to figure // out if our seekTo puts us within the chunk and if so adjust the chunkCursor. if (this.#chunk) { // This is where the chunk starts in the file const chunkStart = this.#fileCursor - this.#chunk.byteLength; if (byteOffset >= chunkStart && byteOffset < this.#fileCursor) { this.#chunkCursor = byteOffset - chunkStart; return; } } this.#fileCursor = byteOffset; this.#chunkCursor = 0; this.#chunk = undefined; this.#view = undefined; } async skip(count: number): Promise<void> { const byteOffset = this.#chunkCursor + count; if (count < 0 || byteOffset < 0 || byteOffset > this.size()) { throw new Error(`Cannot skip ${count} bytes`); } await this.#fetch(count); this.#chunkCursor += count; } async peekUint8(offset: number): Promise<number> { const view = await this.#fetch(offset + 1); return view.getUint8(this.#chunkCursor + offset); } async readBytes(count: number): Promise<Uint8Array> { const view = await this.#fetch(count); const data = new Uint8Array(view.buffer, view.byteOffset + this.#chunkCursor, count); this.#chunkCursor += count; return data; } async readUint8(): Promise<number> { const view = await this.#fetch(1); return view.getUint8(this.#chunkCursor++); } async readInt16(): Promise<number> { const view = await this.#fetch(2); const data = view.getInt16(this.#chunkCursor, true); this.#chunkCursor += 2; return data; } async readUint16(): Promise<number> { const view = await this.#fetch(2); const data = view.getUint16(this.#chunkCursor, true); this.#chunkCursor += 2; return data; } async readInt32(): Promise<number> { const view = await this.#fetch(4); const data = view.getInt32(this.#chunkCursor, true); this.#chunkCursor += 4; return data; } async readUint32(): Promise<number> { const view = await this.#fetch(4); const data = view.getUint32(this.#chunkCursor, true); this.#chunkCursor += 4; return data; } async readFloat32(): Promise<number> { const view = await this.#fetch(4); const data = view.getFloat32(this.#chunkCursor, true); this.#chunkCursor += 4; return data; } async readFloat64(): Promise<number> { const view = await this.#fetch(8); const data = view.getFloat64(this.#chunkCursor, true); this.#chunkCursor += 8; return data; } async readInt64(): Promise<bigint> { const view = await this.#fetch(8); const data = view.getBigInt64(this.#chunkCursor, true); this.#chunkCursor += 8; return data; } async readUint64(): Promise<bigint> { const view = await this.#fetch(8); const data = view.getBigUint64(this.#chunkCursor, true); this.#chunkCursor += 8; return data; } async readString(length: number): Promise<string> { const view = await this.#fetch(length); const data = this.#textDecoder.decode( view.buffer.slice( view.byteOffset + this.#chunkCursor, view.byteOffset + this.#chunkCursor + length, ), ); this.#chunkCursor += length; return data; } async #fetch(bytesRequired: number): Promise<DataView> { if (bytesRequired > this.remaining()) { throw new Error( `Cannot read ${bytesRequired} bytes from ${this.size()} byte source, ${this.remaining()} bytes remaining`, ); } if (!this.#chunk || this.#chunkCursor === this.#chunk.byteLength) { const fileRemaining = this.size() - this.#fileCursor; this.#chunk = await this.#file.read( this.#fileCursor, clamp(this.chunkSize, bytesRequired, fileRemaining), ); this.#view = new DataView(this.#chunk.buffer, this.#chunk.byteOffset, this.#chunk.byteLength); this.#chunkCursor = 0; this.#fileCursor += this.#chunk.byteLength; } let bytesAvailable = this.#chunk.byteLength - this.#chunkCursor; const bytesNeeded = bytesRequired - bytesAvailable; if (bytesAvailable < bytesRequired) { const fileRemaining = this.size() - this.#fileCursor; const curChunk = this.#chunk; const nextChunk = await this.#file.read( this.#fileCursor, clamp(this.chunkSize, bytesNeeded, fileRemaining), ); this.#chunk = concat(curChunk.slice(this.#chunkCursor), nextChunk); this.#view = new DataView(this.#chunk.buffer, this.#chunk.byteOffset, this.#chunk.byteLength); this.#chunkCursor = 0; this.#fileCursor += nextChunk.byteLength; bytesAvailable = this.#chunk.byteLength - this.#chunkCursor; if (bytesAvailable < bytesRequired) { throw new Error(`Requested ${bytesRequired} bytes but ${bytesAvailable} bytes available`); } } return this.#view!; } } function concat(a: Uint8Array, b: Uint8Array): Uint8Array { const c = new Uint8Array(a.byteLength + b.byteLength); c.set(a); c.set(b, a.byteLength); return c; } function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); }