UNPKG

@openmeteo/file-reader

Version:

JavaScript reader for the om file format using WebAssembly

1 lines 131 kB
{"version":3,"file":"index.browser.cjs","sources":["../../src/lib/types.ts","../../src/lib/utils.ts","../../src/lib/wasm.ts","../../src/lib/OmFileReader.ts","../../src/lib/backends/FileBackend.ts","../../src/lib/backends/MemoryHttpBackend.ts","../../src/lib/backends/BlockCacheBackend.ts","../../src/lib/backends/OmHttpBackend.ts","../../src/lib/BrowserBlockCache.ts","../../src/lib/BlockCache.ts"],"sourcesContent":["export interface Range {\n start: number;\n end: number;\n}\n\nexport interface OffsetSize {\n offset: number;\n size: number;\n}\n\nexport interface OmFilePrefetchReadOptions {\n ranges: Range[];\n prefetchConcurrency?: number;\n ioSizeMax?: bigint;\n ioSizeMerge?: bigint;\n signal?: AbortSignal;\n}\n\nexport interface OmFileReadBaseOptions<T extends keyof OmDataTypeToTypedArray> extends OmFilePrefetchReadOptions {\n type: T;\n prefetch?: boolean;\n}\n\nexport interface OmFileReadOptions<T extends keyof OmDataTypeToTypedArray> extends OmFileReadBaseOptions<T> {\n intoSAB?: boolean;\n}\n\nexport interface OmFileReadIntoOptions<T extends keyof OmDataTypeToTypedArray> extends OmFileReadBaseOptions<T> {\n output: OmDataTypeToTypedArray[T];\n}\n\nexport enum CompressionType {\n /// Lossy compression using 2D delta coding and scale-factor.\n /// Only supports float and scales to 16-bit signed integer.\n PforDelta2dInt16 = 0,\n /// Lossless float/double compression using 2D xor coding.\n FpxXor2d = 1,\n /// PFor integer compression.\n /// f32 values are scaled to u32, f64 are scaled to u64.\n PforDelta2d = 2,\n /// Similar to `PforDelta2dInt16` but applies `log10(1+x)` before.\n PforDelta2dInt16Logarithmic = 3,\n None = 4,\n}\n\nexport enum OmDataType {\n None = 0,\n Int8 = 1,\n Uint8 = 2,\n Int16 = 3,\n Uint16 = 4,\n Int32 = 5,\n Uint32 = 6,\n Int64 = 7,\n Uint64 = 8,\n Float = 9,\n Double = 10,\n String = 11,\n Int8Array = 12,\n Uint8Array = 13,\n Int16Array = 14,\n Uint16Array = 15,\n Int32Array = 16,\n Uint32Array = 17,\n Int64Array = 18,\n Uint64Array = 19,\n FloatArray = 20,\n DoubleArray = 21,\n StringArray = 22,\n}\n\nexport type TypedArray =\n | Int8Array\n | Uint8Array\n | Int16Array\n | Uint16Array\n | Int32Array\n | Uint32Array\n | BigInt64Array\n | BigUint64Array\n | Float32Array\n | Float64Array;\n\nexport interface OmDataTypeToTypedArray {\n [OmDataType.Int8Array]: Int8Array;\n [OmDataType.Uint8Array]: Uint8Array;\n [OmDataType.Int16Array]: Int16Array;\n [OmDataType.Uint16Array]: Uint16Array;\n [OmDataType.Int32Array]: Int32Array;\n [OmDataType.Uint32Array]: Uint32Array;\n [OmDataType.Int64Array]: BigInt64Array;\n [OmDataType.Uint64Array]: BigUint64Array;\n [OmDataType.FloatArray]: Float32Array;\n [OmDataType.DoubleArray]: Float64Array;\n}\n\nexport type FileSource = string | File | Blob | Uint8Array | ArrayBuffer;\n","/**\n * Throws the signal's abort reason if the signal has been aborted.\n * Uses the standard DOMException with name \"AbortError\" as fallback.\n */\nexport function throwIfAborted(signal?: AbortSignal): void {\n if (signal?.throwIfAborted) {\n signal.throwIfAborted();\n } else if (signal?.aborted) {\n throw signal.reason ?? new DOMException(\"The operation was aborted\", \"AbortError\");\n }\n}\n\n/**\n * FNV-1a 64-bit hash implementation\n */\nexport function fnv1aHash64(str: string): bigint {\n const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\n const FNV_PRIME = 0x100000001b3n;\n\n let hash = FNV_OFFSET_BASIS;\n const bytes = new TextEncoder().encode(str);\n\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = (hash * FNV_PRIME) & 0xffffffffffffffffn;\n }\n\n return hash;\n}\n\n/**\n * Fetch with exponential backoff retry on server-side errors (HTTP 5xx) and per-attempt timeout.\n */\nexport async function fetchRetry(\n input: RequestInfo,\n init?: RequestInit,\n timeoutMs = 5000,\n retries = 3,\n signal?: AbortSignal\n): Promise<Response> {\n let lastError: Error;\n\n function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return Promise.race([\n promise,\n new Promise<T>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)),\n ]);\n }\n\n for (let attempt = 0; attempt < retries; attempt++) {\n throwIfAborted(signal);\n try {\n const mergedInit: RequestInit = { ...init };\n if (signal) {\n mergedInit.signal = signal;\n }\n const response = await withTimeout(fetch(input, mergedInit), timeoutMs);\n if (response.status >= 500 && response.status < 600) {\n throw new Error(`Server error: ${response.status}`);\n }\n return response;\n } catch (error) {\n // If the signal was aborted, re-throw immediately without retrying\n throwIfAborted(signal);\n lastError = error instanceof Error ? error : new Error(String(error));\n if (attempt < retries - 1) {\n const delay = Math.min(500 * Math.pow(2, attempt), 5000);\n //console.debug(`Attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message}`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n throw lastError!;\n}\n\nexport async function runLimited<T>(tasks: (() => Promise<T>)[], limit: number, signal?: AbortSignal): Promise<T[]> {\n const results: T[] = new Array(tasks.length) as T[];\n\n for (let i = 0; i < tasks.length; i += limit) {\n throwIfAborted(signal);\n const batch = tasks.slice(i, i + limit);\n const batchResults = await Promise.all(batch.map((task) => task()));\n\n for (let j = 0; j < batchResults.length; j++) {\n results[i + j] = batchResults[j];\n }\n }\n\n return results;\n}\n","export interface WasmModule {\n _malloc(size: number): number;\n _free(ptr: number): void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setValue(ptr: number, value: any, type: string): void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n getValue(ptr: number, type: string): any;\n HEAPU8: Uint8Array;\n\n // C-API functions\n om_header_size(): number;\n om_header_type(ptr: number): number;\n om_trailer_size(): number;\n om_trailer_read(trailerPtr: number, offsetPtr: number, sizePtr: number): boolean;\n om_variable_init(dataPtr: number): number;\n om_variable_get_type(variable: number): number;\n om_variable_get_compression(variable: number): number;\n om_variable_get_scale_factor(variable: number): number;\n om_variable_get_add_offset(variable: number): number;\n om_variable_get_dimensions_count(variable: number): number;\n om_variable_get_dimensions_ptr(variable: number): number;\n om_variable_get_chunks_ptr(variable: number): number;\n om_variable_get_name_ptr(variable: number, length: number): number;\n om_variable_get_children_count(variable: number): number;\n om_variable_get_children(variable: number, index: number, count: number, offsetPtr: number, sizePtr: number): boolean;\n om_variable_get_scalar(variable: number, ptrPtr: number, sizePtr: number): number;\n om_decoder_init(\n decoderPtr: number,\n variable: number,\n nDims: bigint,\n readOffsetPtr: number,\n readCountPtr: number,\n intoCubeOffsetPtr: number,\n intoCubeDimensionPtr: number,\n ioSizeMerge: bigint,\n ioSizeMax: bigint\n ): number;\n om_decoder_init_index_read(decoder: number, indexReadPtr: number): void;\n om_decoder_init_data_read(dataReadPtr: number, indexReadPtr: number): void;\n om_decoder_read_buffer_size(decoderPtr: number): number;\n om_decoder_next_index_read(decoder: number, indexRead: number): boolean;\n om_decoder_next_data_read(\n decoder: number,\n dataRead: number,\n indexData: number,\n indexCount: bigint,\n error: number\n ): boolean;\n om_decoder_decode_chunks(\n decoder: number,\n chunkIndex: number,\n data: number,\n count: bigint,\n output: number,\n chunkBuffer: number,\n error: number\n ): boolean;\n\n // Constants\n OM_HEADER_INVALID: number;\n OM_HEADER_LEGACY: number;\n OM_HEADER_READ_TRAILER: number;\n ERROR_OK: number;\n DATA_TYPE_INT8_ARRAY: number;\n DATA_TYPE_UINT8_ARRAY: number;\n DATA_TYPE_INT16_ARRAY: number;\n DATA_TYPE_UINT16_ARRAY: number;\n DATA_TYPE_INT32_ARRAY: number;\n DATA_TYPE_UINT32_ARRAY: number;\n DATA_TYPE_INT64_ARRAY: number;\n DATA_TYPE_UINT64_ARRAY: number;\n DATA_TYPE_FLOAT_ARRAY: number;\n DATA_TYPE_DOUBLE_ARRAY: number;\n\n // Additional info\n sizeof_decoder: number;\n}\n\n// Constants mapping\nconst DATA_TYPES = {\n DATA_TYPE_NONE: 0,\n DATA_TYPE_INT8: 1,\n DATA_TYPE_UINT8: 2,\n DATA_TYPE_INT16: 3,\n DATA_TYPE_UINT16: 4,\n DATA_TYPE_INT32: 5,\n DATA_TYPE_UINT32: 6,\n DATA_TYPE_INT64: 7,\n DATA_TYPE_UINT64: 8,\n DATA_TYPE_FLOAT: 9,\n DATA_TYPE_DOUBLE: 10,\n DATA_TYPE_STRING: 11,\n DATA_TYPE_INT8_ARRAY: 12,\n DATA_TYPE_UINT8_ARRAY: 13,\n DATA_TYPE_INT16_ARRAY: 14,\n DATA_TYPE_UINT16_ARRAY: 15,\n DATA_TYPE_INT32_ARRAY: 16,\n DATA_TYPE_UINT32_ARRAY: 17,\n DATA_TYPE_INT64_ARRAY: 18,\n DATA_TYPE_UINT64_ARRAY: 19,\n DATA_TYPE_FLOAT_ARRAY: 20,\n DATA_TYPE_DOUBLE_ARRAY: 21,\n DATA_TYPE_STRING_ARRAY: 22,\n};\n\nconst HEADER_TYPES = {\n OM_HEADER_INVALID: 0,\n OM_HEADER_LEGACY: 1,\n OM_HEADER_READ_TRAILER: 2,\n};\n\nconst ERROR_CODES = {\n ERROR_OK: 0,\n};\n\n// Size of the decoder structure\nconst SIZEOF_DECODER = 104;\n\nlet wasmModuleWrapped: WasmModule | null = null;\n\nexport async function initWasm(): Promise<WasmModule> {\n if (wasmModuleWrapped) return wasmModuleWrapped;\n\n try {\n const OmFileFormat = await import(\"@openmeteo/file-format-wasm\");\n const wasmModuleRaw = await OmFileFormat.default();\n\n // Create wrapped module with the expected interface\n wasmModuleWrapped = createWrappedModule(wasmModuleRaw);\n\n return wasmModuleWrapped;\n } catch (error) {\n throw new Error(\"Failed to initialize WASM module\", {\n cause: error,\n });\n }\n}\n\nfunction createWrappedModule(rawModule: EmscriptenModule): WasmModule {\n return {\n // Memory management functions\n _malloc: rawModule._malloc,\n _free: rawModule._free,\n setValue: rawModule.setValue,\n getValue: rawModule.getValue,\n HEAPU8: rawModule.HEAPU8,\n\n // Map all the C functions to their prefixed versions\n om_header_size: rawModule._om_header_size,\n om_header_type: rawModule._om_header_type,\n om_trailer_size: rawModule._om_trailer_size,\n om_trailer_read: rawModule._om_trailer_read,\n om_variable_init: rawModule._om_variable_init,\n om_variable_get_type: rawModule._om_variable_get_type,\n om_variable_get_compression: rawModule._om_variable_get_compression,\n om_variable_get_scale_factor: rawModule._om_variable_get_scale_factor,\n om_variable_get_add_offset: rawModule._om_variable_get_add_offset,\n om_variable_get_dimensions_count: rawModule._om_variable_get_dimensions_count,\n om_variable_get_dimensions_ptr: rawModule._om_variable_get_dimensions,\n om_variable_get_chunks_ptr: rawModule._om_variable_get_chunks,\n om_variable_get_name_ptr: rawModule._om_variable_get_name,\n om_variable_get_children_count: rawModule._om_variable_get_children_count,\n om_variable_get_children: rawModule._om_variable_get_children,\n om_variable_get_scalar: rawModule._om_variable_get_scalar,\n om_decoder_init: rawModule._om_decoder_init,\n om_decoder_init_index_read: rawModule._om_decoder_init_index_read,\n om_decoder_init_data_read: rawModule._om_decoder_init_data_read,\n om_decoder_read_buffer_size: rawModule._om_decoder_read_buffer_size,\n om_decoder_next_index_read: rawModule._om_decoder_next_index_read,\n om_decoder_next_data_read: rawModule._om_decoder_next_data_read,\n om_decoder_decode_chunks: rawModule._om_decoder_decode_chunks,\n\n // Constants\n ...HEADER_TYPES,\n ...ERROR_CODES,\n ...DATA_TYPES,\n\n // Additional info\n sizeof_decoder: SIZEOF_DECODER,\n };\n}\n\nexport function getWasmModule() {\n if (!wasmModuleWrapped) {\n throw new Error(\"WASM module not initialized. Call initWasm() first.\");\n }\n return wasmModuleWrapped;\n}\n","import { OmFileReaderBackend } from \"./backends/OmFileReaderBackend\";\nimport {\n OffsetSize,\n OmDataType,\n TypedArray,\n OmDataTypeToTypedArray,\n OmFileReadOptions,\n OmFileReadIntoOptions,\n Range,\n OmFilePrefetchReadOptions,\n} from \"./types\";\nimport { runLimited, throwIfAborted } from \"./utils\";\nimport { WasmModule, initWasm, getWasmModule } from \"./wasm\";\n\nexport class OmFileReader {\n private backend: OmFileReaderBackend;\n private wasm: WasmModule;\n private variable: number | null;\n private variableDataPtr: number | null;\n private metadataCache: Map<string, OffsetSize | null>;\n\n constructor(backend: OmFileReaderBackend, wasm?: WasmModule) {\n this.backend = backend;\n this.wasm = wasm ?? getWasmModule();\n this.variable = null;\n this.variableDataPtr = null;\n this.metadataCache = new Map();\n }\n\n /**\n * Static factory method to create and initialize an OmFileReader\n */\n static async create(backend: OmFileReaderBackend): Promise<OmFileReader> {\n // Make sure WASM is initialized\n const wasm = await initWasm();\n const reader = new OmFileReader(backend, wasm);\n await reader.initialize();\n return reader;\n }\n\n async initialize(): Promise<OmFileReader> {\n let variableData: Uint8Array | undefined;\n\n // First, try to read the trailer\n const trailerSize = this.wasm.om_trailer_size();\n\n const fileSize = await this.backend.count();\n if (fileSize < trailerSize) {\n throw new Error(\"File too small to contain trailer\");\n }\n\n if (fileSize >= trailerSize) {\n const trailerOffset = fileSize - trailerSize;\n const trailerPtr = await this._readDataBlock(trailerOffset, trailerSize);\n\n const offsetPtr = this.wasm._malloc(8); // 64-bit value\n const sizePtr = this.wasm._malloc(8);\n\n try {\n const success = this.wasm.om_trailer_read(trailerPtr, offsetPtr, sizePtr);\n\n if (success) {\n const offset = Number(this.wasm.getValue(offsetPtr, \"i64\"));\n const size = Number(this.wasm.getValue(sizePtr, \"i64\"));\n variableData = await this.backend.getBytes(offset, size);\n }\n } finally {\n this.wasm._free(trailerPtr);\n this.wasm._free(offsetPtr);\n this.wasm._free(sizePtr);\n }\n }\n\n // Fallback to legacy header if trailer reading fails\n if (!variableData) {\n const headerSize = this.wasm.om_header_size();\n const headerData = await this.backend.getBytes(0, headerSize);\n const headerPtr = this.wasm._malloc(headerData.length);\n this.wasm.HEAPU8.set(headerData, headerPtr);\n\n try {\n const headerType = this.wasm.om_header_type(headerPtr);\n if (headerType === this.wasm.OM_HEADER_LEGACY) {\n variableData = headerData;\n }\n } finally {\n this.wasm._free(headerPtr);\n }\n }\n\n if (!variableData) {\n throw new Error(\"Not a valid OM file\");\n }\n\n // Initialize variable\n const variableDataPtr = this.wasm._malloc(variableData.length);\n this.wasm.HEAPU8.set(variableData, variableDataPtr);\n this.variable = this.wasm.om_variable_init(variableDataPtr);\n\n if (!this.variable) {\n this.wasm._free(variableDataPtr);\n throw new Error(\"Failed to initialize variable\");\n }\n\n this.variableDataPtr = variableDataPtr;\n\n return this;\n }\n\n // Helper method to convert C strings to JS strings\n private _getString(strPtr: number, strLen: number): string {\n const bytes = this.wasm.HEAPU8.subarray(strPtr, strPtr + strLen);\n return new TextDecoder(\"utf8\").decode(bytes);\n }\n\n dataType(): number {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n return this.wasm.om_variable_get_type(this.variable);\n }\n\n compression(): number {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n return this.wasm.om_variable_get_compression(this.variable);\n }\n\n scaleFactor(): number {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n return this.wasm.om_variable_get_scale_factor(this.variable);\n }\n\n addOffset(): number {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n return this.wasm.om_variable_get_add_offset(this.variable);\n }\n\n getDimensions(): number[] {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n const count = Number(this.wasm.om_variable_get_dimensions_count(this.variable));\n const dimensionsPtr = this.wasm.om_variable_get_dimensions_ptr(this.variable);\n\n // Create view directly into WASM memory\n const int64View = new BigInt64Array(this.wasm.HEAPU8.buffer, dimensionsPtr, count);\n return Array.from(int64View, (bigIntVal) => Number(bigIntVal));\n }\n\n getChunkDimensions(): number[] {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n const count = Number(this.wasm.om_variable_get_dimensions_count(this.variable));\n const chunksPtr = this.wasm.om_variable_get_chunks_ptr(this.variable);\n\n // Create view directly into WASM memory\n const int64View = new BigInt64Array(this.wasm.HEAPU8.buffer, chunksPtr, count);\n return Array.from(int64View, (bigIntVal) => Number(bigIntVal));\n }\n\n getName(): string | null {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n // string length is i16\n const lengthPtr = this.wasm._malloc(2);\n const valuePtr = this.wasm.om_variable_get_name_ptr(this.variable, lengthPtr);\n const size = this.wasm.getValue(lengthPtr, \"i16\") as number;\n this.wasm._free(lengthPtr);\n if (size === 0 || valuePtr === 0) {\n return null;\n }\n return this._getString(valuePtr, size);\n }\n\n numberOfChildren(): number {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n return this.wasm.om_variable_get_children_count(this.variable);\n }\n\n async getChild(index: number): Promise<OmFileReader | null> {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n // Allocate memory for the output parameters\n const offsetPtr = this.wasm._malloc(8);\n const sizePtr = this.wasm._malloc(8);\n\n const success = this.wasm.om_variable_get_children(this.variable, index, 1, offsetPtr, sizePtr);\n\n if (!success) {\n this.wasm._free(offsetPtr);\n this.wasm._free(sizePtr);\n return null;\n }\n\n const offset = Number(this.wasm.getValue(offsetPtr, \"i64\"));\n const size = Number(this.wasm.getValue(sizePtr, \"i64\"));\n\n this.wasm._free(offsetPtr);\n this.wasm._free(sizePtr);\n\n return this.initChildFromOffsetSize({ offset, size });\n }\n\n /**\n * Searches direct children by name. Does not search recursively.\n */\n async getChildByName(name: string): Promise<OmFileReader | null> {\n // Check cache first\n const cachedMetadata = this.metadataCache.get(name);\n if (cachedMetadata === null) {\n return null;\n }\n if (cachedMetadata) {\n return await this.initChildFromOffsetSize(cachedMetadata);\n }\n\n // Search through children and cache metadata\n const numChildren = this.numberOfChildren();\n for (let i = 0; i < numChildren; i++) {\n const metadata = this._getChildMetadata(i);\n if (metadata) {\n const child = await this.initChildFromOffsetSize(metadata);\n const childName = child.getName();\n if (childName) {\n this.metadataCache.set(childName, metadata);\n if (childName === name) {\n return child; // keep this one\n }\n }\n child.dispose();\n }\n }\n // also remember invalid names\n this.metadataCache.set(name, null);\n return null;\n }\n\n async initChildFromOffsetSize(offsetSize: OffsetSize): Promise<OmFileReader> {\n const childDataPtr = await this._readDataBlock(offsetSize.offset, offsetSize.size);\n\n const childReader = new OmFileReader(this.backend, this.wasm);\n\n childReader.variable = this.wasm.om_variable_init(childDataPtr);\n childReader.variableDataPtr = childDataPtr;\n\n return childReader;\n }\n\n /**\n * Get child metadata by index.\n */\n private _getChildMetadata(index: number): OffsetSize | null {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n // Allocate memory for the output parameters\n const offsetPtr = this.wasm._malloc(8);\n const sizePtr = this.wasm._malloc(8);\n\n const success = this.wasm.om_variable_get_children(this.variable, index, 1, offsetPtr, sizePtr);\n\n if (!success) {\n this.wasm._free(offsetPtr);\n this.wasm._free(sizePtr);\n return null;\n }\n\n const offset = Number(this.wasm.getValue(offsetPtr, \"i64\"));\n const size = Number(this.wasm.getValue(sizePtr, \"i64\"));\n\n this.wasm._free(offsetPtr);\n this.wasm._free(sizePtr);\n\n return { offset, size };\n }\n\n /**\n * Find a variable by its path (e.g., \"parent/child/grandchild\")\n */\n async findByPath(path: string): Promise<OmFileReader | null> {\n const parts = path.split(\"/\").filter((s) => s.length > 0);\n return await this.navigatePath(parts);\n }\n\n /**\n * Navigate through a path recursively\n */\n async navigatePath(parts: string[]): Promise<OmFileReader | null> {\n if (parts.length === 0) {\n return null;\n }\n\n const child = await this.getChildByName(parts[0]);\n if (child) {\n if (parts.length === 1) {\n return child;\n } else {\n return await child.navigatePath(parts.slice(1));\n }\n }\n return null;\n }\n\n // Method to read scalar values\n readScalar<T>(dataType: OmDataType): T | null {\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n if (this.dataType() !== dataType.valueOf()) {\n return null;\n }\n\n // Allocate memory for output parameters\n const ptrPtr = this.wasm._malloc(4); // pointer to pointer\n const sizePtr = this.wasm._malloc(8); // u64\n\n try {\n const error = this.wasm.om_variable_get_scalar(this.variable, ptrPtr, sizePtr);\n\n if (error !== this.wasm.ERROR_OK) {\n return null;\n }\n\n const dataPtr = this.wasm.getValue(ptrPtr, \"*\") as number;\n\n if (dataPtr === 0) {\n return null;\n }\n\n // Read data based on type\n let result: T | null;\n\n // TODO: Support Int64 and Uint64\n switch (dataType) {\n case OmDataType.Int8:\n result = this.wasm.getValue(dataPtr, \"i8\") as T;\n break;\n case OmDataType.Uint8:\n result = (this.wasm.getValue(dataPtr, \"i8\") & 0xff) as T;\n break;\n case OmDataType.Int16:\n result = this.wasm.getValue(dataPtr, \"i16\") as T;\n break;\n case OmDataType.Uint16:\n result = (this.wasm.getValue(dataPtr, \"i16\") & 0xffff) as T;\n break;\n case OmDataType.Int32:\n result = this.wasm.getValue(dataPtr, \"i32\") as T;\n break;\n case OmDataType.Uint32:\n result = (this.wasm.getValue(dataPtr, \"i32\") >>> 0) as T;\n break;\n case OmDataType.Int64:\n result = this.wasm.getValue(dataPtr, \"i64\") as T;\n break;\n case OmDataType.Uint64:\n // convert to unsigned BigInt\n {\n const val = this.wasm.getValue(dataPtr, \"i64\") as bigint;\n result = (val & BigInt(\"0xFFFFFFFFFFFFFFFF\")) as T;\n }\n break;\n case OmDataType.Float:\n result = this.wasm.getValue(dataPtr, \"float\") as T;\n break;\n case OmDataType.Double:\n result = this.wasm.getValue(dataPtr, \"double\") as T;\n break;\n case OmDataType.String:\n {\n const size = Number(this.wasm.getValue(sizePtr, \"i64\"));\n if (size === 0) {\n return null;\n }\n result = this._getString(dataPtr, size) as T;\n }\n break;\n default:\n result = null;\n }\n return result;\n } finally {\n this.wasm._free(ptrPtr);\n this.wasm._free(sizePtr);\n }\n }\n\n private newIndexRead(decoderPtr: number): number {\n // Size of OmDecoder_indexRead_t\n const sizeOfRange = 16; // 8 bytes for lowerBound + 8 bytes for upperBound\n const sizeOfIndexRead = 8 + 8 + sizeOfRange * 3; // offset + count + 3 range structs\n\n // Allocate the memory\n const indexReadPtr = this.wasm._malloc(sizeOfIndexRead);\n this.wasm.om_decoder_init_index_read(decoderPtr, indexReadPtr);\n return indexReadPtr;\n }\n\n private newDataRead(indexReadPtr: number): number {\n // Size of OmDecoder_dataRead_t\n const sizeOfRange = 16; // 8 bytes for lowerBound + 8 bytes for upperBound\n const sizeOfDataRead = 8 + 8 + sizeOfRange * 3; // offset + count + 3 range structs\n\n // Allocate the memory\n const dataReadPtr = this.wasm._malloc(sizeOfDataRead);\n this.wasm.om_decoder_init_data_read(dataReadPtr, indexReadPtr);\n return dataReadPtr;\n }\n\n private allocateTypedArray<T extends keyof OmDataTypeToTypedArray>(\n dataType: T,\n size: number,\n useSharedBuffer = false\n ): OmDataTypeToTypedArray[T] {\n if (useSharedBuffer && typeof SharedArrayBuffer === \"undefined\") {\n throw new Error(\"SharedArrayBuffer is not available in this environment\");\n }\n\n // Type-safe mapping of data types to their constructors and byte sizes\n const typeInfo = {\n [this.wasm.DATA_TYPE_INT8_ARRAY]: { constructor: Int8Array<ArrayBufferLike>, bytes: 1 },\n [this.wasm.DATA_TYPE_UINT8_ARRAY]: { constructor: Uint8Array<ArrayBufferLike>, bytes: 1 },\n [this.wasm.DATA_TYPE_INT16_ARRAY]: { constructor: Int16Array<ArrayBufferLike>, bytes: 2 },\n [this.wasm.DATA_TYPE_UINT16_ARRAY]: { constructor: Uint16Array<ArrayBufferLike>, bytes: 2 },\n [this.wasm.DATA_TYPE_INT32_ARRAY]: { constructor: Int32Array<ArrayBufferLike>, bytes: 4 },\n [this.wasm.DATA_TYPE_UINT32_ARRAY]: { constructor: Uint32Array<ArrayBufferLike>, bytes: 4 },\n [this.wasm.DATA_TYPE_INT64_ARRAY]: { constructor: BigInt64Array<ArrayBufferLike>, bytes: 8 },\n [this.wasm.DATA_TYPE_UINT64_ARRAY]: { constructor: BigUint64Array<ArrayBufferLike>, bytes: 8 },\n [this.wasm.DATA_TYPE_FLOAT_ARRAY]: { constructor: Float32Array<ArrayBufferLike>, bytes: 4 },\n [this.wasm.DATA_TYPE_DOUBLE_ARRAY]: { constructor: Float64Array<ArrayBufferLike>, bytes: 8 },\n } as const;\n\n const info = typeInfo[dataType];\n const byteLength = size * info.bytes;\n\n if (useSharedBuffer) {\n // In browsers, crossOriginIsolated must be true; in Node, it's undefined (so skip check)\n if (\n typeof SharedArrayBuffer === \"undefined\" ||\n (typeof crossOriginIsolated !== \"undefined\" && !crossOriginIsolated)\n ) {\n throw new Error(\"SharedArrayBuffer is not available in this environment\");\n }\n const sharedBuffer = new SharedArrayBuffer(byteLength);\n return new info.constructor(sharedBuffer) as OmDataTypeToTypedArray[T];\n } else {\n const normalBuffer = new ArrayBuffer(byteLength);\n return new info.constructor(normalBuffer) as OmDataTypeToTypedArray[T];\n }\n }\n\n /**\n * Reads data from the file and returns a new TypedArray of the requested type.\n *\n * @param options Options for reading, including:\n * - type: The data type to read.\n * - ranges: Array of dimension ranges to read.\n * - prefetch: Whether to prefetch data (default: true).\n * - intoSAB: Use SharedArrayBuffer for output (default: false).\n * - ioSizeMax: Maximum I/O size (default: 65536).\n * - ioSizeMerge: Merge threshold for I/O operations (default: 2048).\n */\n async read<T extends keyof OmDataTypeToTypedArray>(\n options: OmFileReadOptions<T>\n ): Promise<OmDataTypeToTypedArray[T]> {\n const {\n type,\n ranges,\n prefetch = true,\n prefetchConcurrency = 10,\n intoSAB = false,\n ioSizeMax = BigInt(65536),\n ioSizeMerge = BigInt(2048),\n signal,\n } = options;\n\n // Calculate output dimensions\n const outDims = ranges.map((range) => Number(range.end - range.start));\n const totalSize = outDims.reduce((a, b) => a * b, 1);\n\n const output = this.allocateTypedArray(type, totalSize, intoSAB);\n\n await this.readInto({ type, output, ranges, ioSizeMax, ioSizeMerge, prefetch, prefetchConcurrency, signal });\n return output;\n }\n\n /**\n * Reads data into an existing TypedArray with specified dimension ranges.\n *\n * @param options Options for reading, including:\n * - type: The data type to read.\n * - output: The TypedArray to read data into.\n * - ranges: Array of dimension ranges to read.\n * - prefetch: Whether to prefetch data (default: true).\n * - ioSizeMax: Maximum I/O size (default: 65536).\n * - ioSizeMerge: Merge threshold for I/O operations (default: 2048).\n */\n async readInto<T extends keyof OmDataTypeToTypedArray>(options: OmFileReadIntoOptions<T>): Promise<void> {\n const {\n type,\n output,\n ranges,\n prefetch = true,\n prefetchConcurrency = 10,\n ioSizeMax = BigInt(65536),\n ioSizeMerge = BigInt(2048),\n signal,\n } = options;\n\n if (this.dataType() !== type) {\n throw new Error(`Invalid data type: expected ${this.dataType()}, got ${type}`);\n }\n\n const nDims = ranges.length;\n const fileDims = this.getDimensions();\n\n // Validate dimension counts\n if (fileDims.length !== nDims) {\n throw new Error(`Mismatched dimensions: file has ${fileDims.length}, request has ${nDims}`);\n }\n\n // Verify output size before starting\n const totalElements = ranges.reduce((acc, r) => acc * Number(r.end - r.start), 1);\n if (output.length < totalElements) {\n throw new Error(`Output array is too small: needs ${totalElements} elements, has ${output.length}`);\n }\n\n await this._runWithDecoder(\n ranges,\n ioSizeMax,\n ioSizeMerge,\n async (decoderPtr) => {\n if (prefetch) {\n await this.decodePrefetch(decoderPtr, prefetchConcurrency, signal);\n }\n await this.decode(decoderPtr, output, signal);\n },\n signal\n );\n }\n\n /**\n * Warms up the backend cache by requesting the necessary data blocks\n * without decoding them or copying them to a TypedArray.\n */\n async readPrefetch(options: OmFilePrefetchReadOptions): Promise<void> {\n const { ranges, prefetchConcurrency = 20, ioSizeMax = BigInt(65536), ioSizeMerge = BigInt(2048), signal } = options;\n\n await this._runWithDecoder(\n ranges,\n ioSizeMax,\n ioSizeMerge,\n async (decoderPtr) => {\n await this.decodePrefetch(decoderPtr, prefetchConcurrency, signal);\n },\n signal\n );\n }\n\n private async decodePrefetch(decoderPtr: number, concurrency = 10, signal?: AbortSignal): Promise<void> {\n if (!this.backend.collectPrefetchTasks) return;\n\n const allTasks: (() => Promise<void>)[] = [];\n\n await this._iterateDataBlocks(\n decoderPtr,\n async (dataReadPtr) => {\n const dataOffset = Number(this.wasm.getValue(dataReadPtr, \"i64\"));\n const dataCount = Number(this.wasm.getValue(dataReadPtr + 8, \"i64\"));\n\n const tasks = await this.backend.collectPrefetchTasks!(dataOffset, dataCount, signal);\n allTasks.push(...tasks);\n },\n signal\n );\n\n if (allTasks.length > 0) {\n await runLimited(allTasks, concurrency, signal);\n }\n }\n\n private async decode(decoderPtr: number, outputArray: TypedArray, signal?: AbortSignal): Promise<void> {\n const outputPtr = this.wasm._malloc(outputArray.byteLength);\n const chunkBufferSize = Number(this.wasm.om_decoder_read_buffer_size(decoderPtr));\n const chunkBufferPtr = this.wasm._malloc(chunkBufferSize);\n const errorPtr = this.wasm._malloc(4); // Separate error ptr for the decode step\n this.wasm.setValue(errorPtr, this.wasm.ERROR_OK, \"i32\");\n\n try {\n await this._iterateDataBlocks(\n decoderPtr,\n async (dataReadPtr) => {\n const dataOffset = Number(this.wasm.getValue(dataReadPtr, \"i64\"));\n const dataCount = Number(this.wasm.getValue(dataReadPtr + 8, \"i64\"));\n const chunkIndexPtr = dataReadPtr + 32; // offset(8), count(8), indexRange(16)\n\n // Get the compressed data from the backend\n const dataBlockPtr = await this._readDataBlock(dataOffset, dataCount, signal);\n\n try {\n const success = this.wasm.om_decoder_decode_chunks(\n decoderPtr,\n chunkIndexPtr,\n dataBlockPtr,\n BigInt(dataCount),\n outputPtr,\n chunkBufferPtr,\n errorPtr\n );\n\n if (!success) {\n throw new Error(`Decoder failed: error ${this.wasm.getValue(errorPtr, \"i32\")}`);\n }\n } finally {\n this.wasm._free(dataBlockPtr);\n }\n },\n signal\n );\n\n this.copyToTypedArray(outputPtr, outputArray);\n } finally {\n this.wasm._free(errorPtr);\n this.wasm._free(chunkBufferPtr);\n this.wasm._free(outputPtr);\n }\n }\n\n /**\n * Internal helper to set up the decoder and execute a task.\n * Handles memory allocation and cleanup for ranges and the decoder.\n */\n private async _runWithDecoder(\n ranges: Range[],\n ioSizeMax: bigint,\n ioSizeMerge: bigint,\n task: (decoderPtr: number) => Promise<void>,\n signal?: AbortSignal\n ): Promise<void> {\n throwIfAborted(signal);\n if (this.variable === null) throw new Error(\"Reader not initialized\");\n\n const nDims = ranges.length;\n const fileDims = this.getDimensions();\n\n if (fileDims.length !== nDims) {\n throw new Error(`Mismatched dimensions: file has ${fileDims.length}, request has ${nDims}`);\n }\n\n const outDims = ranges.map((range) => range.end - range.start);\n\n // Allocate memory for dimension arrays\n const readOffsetPtr = this.wasm._malloc(nDims * 8);\n const readCountPtr = this.wasm._malloc(nDims * 8);\n const intoCubeOffsetPtr = this.wasm._malloc(nDims * 8);\n const intoCubeDimensionPtr = this.wasm._malloc(nDims * 8);\n\n try {\n for (let i = 0; i < nDims; i++) {\n if (ranges[i].start < 0 || ranges[i].end > fileDims[i] || ranges[i].start >= ranges[i].end) {\n throw new Error(`Invalid range for dimension ${i}: ${JSON.stringify(ranges[i])}`);\n }\n this.wasm.setValue(readOffsetPtr + i * 8, BigInt(ranges[i].start), \"i64\");\n this.wasm.setValue(readCountPtr + i * 8, BigInt(outDims[i]), \"i64\");\n this.wasm.setValue(intoCubeOffsetPtr + i * 8, BigInt(0), \"i64\");\n this.wasm.setValue(intoCubeDimensionPtr + i * 8, BigInt(outDims[i]), \"i64\");\n }\n\n const decoderPtr = this.wasm._malloc(this.wasm.sizeof_decoder);\n try {\n const error = this.wasm.om_decoder_init(\n decoderPtr,\n this.variable,\n BigInt(nDims),\n readOffsetPtr,\n readCountPtr,\n intoCubeOffsetPtr,\n intoCubeDimensionPtr,\n ioSizeMerge,\n ioSizeMax\n );\n\n if (error !== this.wasm.ERROR_OK) {\n throw new Error(`Decoder initialization failed: error code ${error}`);\n }\n\n // Run the specific work (decode or prefetch)\n await task(decoderPtr);\n } finally {\n this.wasm._free(decoderPtr);\n }\n } finally {\n this.wasm._free(readOffsetPtr);\n this.wasm._free(readCountPtr);\n this.wasm._free(intoCubeOffsetPtr);\n this.wasm._free(intoCubeDimensionPtr);\n }\n }\n\n private async _iterateDataBlocks(\n decoderPtr: number,\n callback: (dataReadPtr: number, indexDataPtr: number, indexCount: bigint) => Promise<void>,\n signal?: AbortSignal\n ): Promise<void> {\n const errorPtr = this.wasm._malloc(4);\n this.wasm.setValue(errorPtr, this.wasm.ERROR_OK, \"i32\");\n\n const indexReadPtr = this.newIndexRead(decoderPtr);\n\n try {\n // Loop over index blocks\n while (this.wasm.om_decoder_next_index_read(decoderPtr, indexReadPtr)) {\n throwIfAborted(signal);\n const indexOffset = Number(this.wasm.getValue(indexReadPtr, \"i64\"));\n const indexCount = Number(this.wasm.getValue(indexReadPtr + 8, \"i64\"));\n\n // Get bytes for index-read\n const indexDataPtr = await this._readDataBlock(indexOffset, indexCount, signal);\n const dataReadPtr = this.newDataRead(indexReadPtr);\n\n try {\n // Loop over data blocks described by this index block\n while (\n this.wasm.om_decoder_next_data_read(decoderPtr, dataReadPtr, indexDataPtr, BigInt(indexCount), errorPtr)\n ) {\n throwIfAborted(signal);\n await callback(dataReadPtr, indexDataPtr, BigInt(indexCount));\n }\n\n // Check for errors after the data_read loop finishes for this index block\n const error = this.wasm.getValue(errorPtr, \"i32\") as number;\n if (error !== this.wasm.ERROR_OK) {\n throw new Error(`Data read iteration error: ${error}`);\n }\n } finally {\n this.wasm._free(dataReadPtr);\n this.wasm._free(indexDataPtr);\n }\n }\n } finally {\n this.wasm._free(indexReadPtr);\n this.wasm._free(errorPtr);\n }\n }\n\n private async _readDataBlock(offset: number, size: number, signal?: AbortSignal): Promise<number> {\n const data = await this.backend.getBytes(offset, size, signal);\n const ptr = this.wasm._malloc(data.length);\n this.wasm.HEAPU8.set(data, ptr);\n return ptr;\n }\n\n /**\n * Helper method to copy data from WASM memory to a TypedArray with the correct type\n */\n private copyToTypedArray(sourcePtr: number, targetArray: TypedArray): void {\n switch (targetArray.constructor) {\n case Float32Array:\n (targetArray as Float32Array).set(new Float32Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Float64Array:\n (targetArray as Float64Array).set(new Float64Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Int8Array:\n (targetArray as Int8Array).set(new Int8Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Uint8Array:\n (targetArray as Uint8Array).set(new Uint8Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Int16Array:\n (targetArray as Int16Array).set(new Int16Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Uint16Array:\n (targetArray as Uint16Array).set(new Uint16Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Int32Array:\n (targetArray as Int32Array).set(new Int32Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case Uint32Array:\n (targetArray as Uint32Array).set(new Uint32Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case BigInt64Array:\n (targetArray as BigInt64Array).set(new BigInt64Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n case BigUint64Array:\n (targetArray as BigUint64Array).set(new BigUint64Array(this.wasm.HEAPU8.buffer, sourcePtr, targetArray.length));\n break;\n default:\n throw new Error(\"Unsupported TypedArray type in copyToTypedArray\");\n }\n }\n\n // Clean up resources when done\n dispose(): void {\n if (this.variableDataPtr !== null) {\n this.wasm._free(this.variableDataPtr);\n this.variableDataPtr = null;\n }\n this.variable = null;\n }\n}\n","import { FileSource } from \"../types\";\nimport { OmFileReaderBackend } from \"./OmFileReaderBackend\";\n\nexport class FileBackend implements OmFileReaderBackend {\n private fileObj: File | Blob | null = null;\n private memory: Uint8Array | null = null;\n private fileSize = 0;\n\n constructor(source: FileSource) {\n if (typeof File !== \"undefined\" && source instanceof File) {\n this.fileObj = source;\n this.fileSize = source.size;\n } else if (typeof Blob !== \"undefined\" && source instanceof Blob) {\n this.fileObj = source;\n this.fileSize = source.size;\n } else if (source instanceof ArrayBuffer) {\n this.memory = new Uint8Array(source);\n this.fileSize = this.memory.length;\n } else if (source instanceof Uint8Array) {\n this.memory = source;\n this.fileSize = source.length;\n } else {\n throw new Error(\"Unsupported file source type for browser FileBackend\");\n }\n }\n\n async count(): Promise<number> {\n return Promise.resolve(this.fileSize);\n }\n\n async getBytes(offset: number, size: number): Promise<Uint8Array> {\n if (this.memory) {\n return this.memory.slice(offset, offset + size);\n }\n if (this.fileObj) {\n const blob = this.fileObj.slice(offset, offset + size);\n const buffer = await blob.arrayBuffer();\n return new Uint8Array(buffer);\n }\n throw new Error(\"No file or memory buffer available\");\n }\n\n // No collectPrefetchTasks - prefetching has minor effect\n\n async close(): Promise<void> {\n // Nothing to clean up in browser\n }\n}\n","import { OmFileReaderBackend } from \"./OmFileReaderBackend\";\n\nexport class MemoryHttpBackend implements OmFileReaderBackend {\n private url: string;\n private fileSize: number | null = null;\n private fileData: Uint8Array | null = null;\n private loadPromise: Promise<void> | null = null;\n private countPromise: Promise<number> | null = null;\n private maxFileSize: number;\n private onProgress?: (loaded: number, total: number) => void;\n private debug: boolean;\n\n /**\n * Create a new MemoryHttpBackend\n * @param options Configuration options\n */\n constructor(options: {\n url: string;\n maxFileSize?: number;\n onProgress?: (loaded: number, total: number) => void;\n debug?: boolean;\n }) {\n this.url = options.url;\n this.maxFileSize = options.maxFileSize ?? 200 * 1024 * 1024; // 200 MB default\n this.onProgress = options.onProgress;\n this.debug = options.debug ?? false;\n\n // Start loading the file in the background\n this.loadFile().catch((err) => {\n if (this.debug) console.error(\"Background file load failed:\", err);\n });\n }\n\n /**\n * Get the total size of the file\n */\n async count(): Promise<number> {\n if (this.fileSize !== null) {\n return this.fileSize;\n }\n\n if (!this.countPromise) {\n if (this.debug) console.log(`Making HEAD request to ${this.url}`);\n\n this.countPromise = (async () => {\n try {\n const response = await fetch(this.url, {\n method: \"HEAD\",\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error: ${response.status}`);\n }\n\n const contentLength = response.headers.get(\"content-length\");\n if (!contentLength) {\n throw new Error(\"Content-Length header not available\");\n }\n\n this.fileSize = parseInt(contentLength, 10);\n\n if (this.fileSize > this.maxFileSize) {\n throw new Error(\n `File size (${this.fileSize} bytes) exceeds maximum allowed size (${this.maxFileSize} bytes)`\n );\n }\n\n if (this.debug) console.log(`File size: ${this.fileSize} bytes`);\n return this.fileSize;\n } catch (error) {\n this.countPromise = null;\n throw new Error(\"Failed to get file size\", {\n cause: error,\n });\n }\n })();\n }\n\n return this.countPromise;\n }\n\n /**\n * Load the entire file into memory\n */\n async loadFile(): Promise<void> {\n // If already loaded or loading, return that promise\n if (this.fileData) {\n return Promise.resolve();\n }\n\n if (this.loadPromise) {\n return this.loadPromise;\n }\n\n this.loadPromise = (async () => {\n try {\n // First get the file size\n const size = await this.count();\n\n if (this.debug) console.log(`Fetching entire file (${size} bytes) from ${this.url}`);\n\n // Use fetch with streaming and progress tracking\n const response = await fetch(this.url);\n\n if (!response.ok) {\n throw new Error(`HTTP error: ${response.status}`);\n }\n\n // Check if ReadableStream is supported and if progress tracking is needed\n if (this.onProgress && response.body && \"getReader\" in response.body) {\n // Stream the response with progress tracking\n const contentLength = Number(response.headers.get(\"content-length\") ?? size);\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n\n let receivedLength = 0;\n let lastProgressUpdate = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n chunks.push(value);\n receivedLength += value.length;\n\n // Don't update progress too frequently (throttle updates)\n const now = Date.now();\n if (now - lastProgressUpdate > 100) {\n // update every 100ms\n this.onProgress(receivedLength, contentLength);\n lastProgressUpdate = now;\n }\n }\n\n // Concatenate chunks into a single Uint8Array\n this.fileData = new Uint8Array(receivedLength);\n let position = 0;\n for (const chunk of chunks) {\n this.fileData.set(chunk, position);\n position += chunk.length;\n }\n\n // Final progress update\n this.onProgress(receivedLength, contentLength);\n } else {\n // Simple approach without streaming\n const buffer = await response.arrayBuffer();\n this.fileData = new Uint8Array(buffer);\n\n if (this.onProgress) {\n this.onProgress(this.fileData.length, size);\n }\n }\n\n if (this.debug) console.log(`File loaded successfully (${this.fileData.length} bytes)`);\n return;\n } catch (error) {\n this.loadPromise = null;\n throw new Error(\"Failed to load file\", {\n cause: error,\n });\n }\n })();\n\n return this.loadPromise;\n }\n\n /**\n * Get bytes from the file\n * @param offset The starting position in the file\n * @param size The number of bytes to read\n */\n async getBytes(offset: number, size: number): Promise<Uint8Array> {\n try {\n // Make sure the file is loaded\n if (!this.fileData) {\n if (this.debug) console.log(`getBytes(${offset}, ${size}): Waiting for file to load...`);\n await this.loadFile();\n if (this.debug) console.log(`getBytes(${offset}, ${size}): File loaded`);\n }\n\n // At this point, fileData should be available\n if (!this.fileData) {\n throw new Error(\"File data is not available after load\");\n }\n\n // Bounds check\n if (offset < 0 || offset + size > this.fileData.length) {\n throw new Error(`Requested range (${offset}:${offset + size}) is out of bounds (0:${this.fileData.length})`);\n }\n\n if (this.debug) console.log(`Serving ${size} bytes from offset ${offset} from memory`);\n\n // Return the requested slice of data\n return this.fileData.slice(offset, offset + size);\n } catch (error) {\n throw new Error(\"Error in getBytes\", {\n cause: error,\n });\n }\n }\n\n /**\n * Check if the file is fully loaded\n */\n isLoaded(): boolean {\n return !!this.fileData;\n }\n\n /**\n * Get the current loaded data or null if not loaded\n */\n getFileData(): Uint8Array | null {\n return this.fileData;\n }\n\n /**\n * Force a reload of the file\n */\n async reload(): Promise<void> {\n this.fileData = null;\n this.loadPromise = null;\n return this.loadFile();\n }\n\n // No collectPrefetchTasks - prefetching has minor effect\n\n /**\n * Close the backend and release any resources\n */\n async close(): Promise<void> {\n this.fileData = null;\n this.loadPromise = null;\n return Promise.resolve();\n }\n}\n","import { BlockCache } from \"../BlockCache\";\nimport { throwIfAborted } from \"../utils\";\nimport { OmFileReaderBackend } from \"./OmFileReaderBackend\";\n\n/**\n * Wraps a backend for caching blocks of data.\n */\nexport class BlockCacheBackend<K> implements OmFileReaderBackend {\n private readonly backend: OmFileReaderBackend;\n private readonly cache: BlockCache<K>;\n private readonly baseKey: K;\n private readonly keyBuilder: (baseKey: K, blockIdx: number) => K;\n private cachedCount: number | null = null;\n\n constructor(\n backend: OmFileReaderBackend,\n cache: BlockCache<K>,\n baseKey: K,\n keyBuilder: (baseKey: K, blockIdx: number) => K\n ) {\n this.backend = backend;\n this.cache