Spaces:
Sleeping
Sleeping
File size: 5,638 Bytes
1d7d958 97c59ab 1d7d958 97c59ab 1d7d958 97c59ab 1d7d958 97c59ab 1d7d958 a30d45f 97c59ab a30d45f 97c59ab a30d45f 97c59ab 1d7d958 97c59ab 1d7d958 55da458 1d7d958 55da458 1d7d958 55da458 1d7d958 8c3f6a6 2ec5005 8c3f6a6 1d7d958 2ec5005 1d7d958 2ec5005 1d7d958 97c59ab 8c3f6a6 2ec5005 8c3f6a6 1d7d958 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import fs from 'fs';
import { IOffsetIndexService } from '../interfaces/IOffsetIndexService';
import { RowRangeOffset } from '../../types/dataset.types';
import { CONFIG } from '../../config/app.config';
import { logger } from '../../utils/logger';
export class OffsetIndexService implements IOffsetIndexService {
private idxPath: string;
private csvPath: string;
private fileHandle: fs.promises.FileHandle | null = null;
private isLittleEndian: boolean = true;
private endiannessDetected: boolean = false;
private cachedTotalRows: number | null = null;
constructor(customIdxPath?: string, customCsvPath?: string) {
this.idxPath = customIdxPath || CONFIG.ANNOTATIONS_IDX;
this.csvPath = customCsvPath || CONFIG.ANNOTATIONS_CSV;
}
public async isIndexAvailable(): Promise<boolean> {
try {
if (!fs.existsSync(this.idxPath)) {
return false;
}
const stat = await fs.promises.stat(this.idxPath);
return stat.size >= 8;
} catch {
return false;
}
}
private cachedCsvSize: number | null = null;
private async detectEndianness(handle: fs.promises.FileHandle): Promise<void> {
if (this.endiannessDetected) return;
try {
let csvSize = this.cachedCsvSize || 10 * 1024 * 1024 * 1024 * 1024; // Default fallback upper bound (10 TB)
if (this.cachedCsvSize === null && fs.existsSync(this.csvPath)) {
const stat = await fs.promises.stat(this.csvPath);
if (stat.size > 0) {
this.cachedCsvSize = stat.size;
csvSize = stat.size;
}
}
// Read row 1 offset (at byte position 8) to auto-detect endianness
const buf = Buffer.alloc(8);
const { bytesRead } = await handle.read(buf, 0, 8, 8);
if (bytesRead === 8) {
const valLE = Number(buf.readBigUInt64LE(0));
const valBE = Number(buf.readBigUInt64BE(0));
if (valLE > csvSize && valBE <= csvSize) {
this.isLittleEndian = false;
logger.info(`Auto-detected Big-Endian format for index file '${this.idxPath}'`);
} else if (valBE > csvSize && valLE <= csvSize) {
this.isLittleEndian = true;
logger.info(`Auto-detected Little-Endian format for index file '${this.idxPath}'`);
}
}
} catch (err) {
logger.warn(`Failed to auto-detect endianness for '${this.idxPath}', defaulting to Little-Endian:`, err);
} finally {
this.endiannessDetected = true;
}
}
private async getFileHandle(): Promise<fs.promises.FileHandle | null> {
if (this.fileHandle) return this.fileHandle;
try {
if (await this.isIndexAvailable()) {
this.fileHandle = await fs.promises.open(this.idxPath, 'r');
await this.detectEndianness(this.fileHandle);
return this.fileHandle;
}
} catch (err) {
logger.error(`Failed to open index file at ${this.idxPath}:`, err);
}
return null;
}
public async getTotalIndexedRows(): Promise<number> {
if (this.cachedTotalRows !== null) return this.cachedTotalRows;
if (!fs.existsSync(this.idxPath)) {
const errorMsg = `Index file does not exist at '${this.idxPath}'`;
logger.error(errorMsg);
throw new Error(errorMsg);
}
try {
const stat = await fs.promises.stat(this.idxPath);
if (stat.size < 8) {
const errorMsg = `Index file at '${this.idxPath}' is invalid or empty (size: ${stat.size} bytes, minimum required: 8 bytes)`;
logger.error(errorMsg);
throw new Error(errorMsg);
}
// Each uint64 entry is 8 bytes
this.cachedTotalRows = Math.floor(stat.size / 8);
logger.info(`Successfully retrieved ${this.cachedTotalRows} rows from index file '${this.idxPath}'`);
return this.cachedTotalRows;
} catch (err: any) {
const errorMsg = err.message || String(err);
if (!errorMsg.startsWith('Index file')) {
logger.error(`Failed to load index file '${this.idxPath}': ${errorMsg}`);
}
throw new Error(errorMsg.startsWith('Index file') ? errorMsg : `Failed to load index file '${this.idxPath}': ${errorMsg}`);
}
}
public async getOffsetForRow(rowNumber: number): Promise<number> {
if (!fs.existsSync(this.idxPath)) {
return rowNumber * 128;
}
let handle: fs.promises.FileHandle | null = null;
try {
handle = await fs.promises.open(this.idxPath, 'r');
await this.detectEndianness(handle);
const position = rowNumber * 8;
const buf = Buffer.alloc(8);
const { bytesRead } = await handle.read(buf, 0, 8, position);
if (bytesRead < 8) {
return rowNumber * 128;
}
const valBig = this.isLittleEndian ? buf.readBigUInt64LE(0) : buf.readBigUInt64BE(0);
const val = Number(valBig);
if (!Number.isFinite(val) || val < 0) {
return rowNumber * 128;
}
return val;
} catch (err) {
logger.warn(`Error reading offset for row ${rowNumber}:`, err);
return rowNumber * 128;
} finally {
if (handle) {
try { await handle.close(); } catch {}
}
}
}
public async getOffsetsForRange(startRow: number, endRow: number): Promise<RowRangeOffset> {
const startOffset = await this.getOffsetForRow(startRow);
const endOffset = await this.getOffsetForRow(endRow);
const byteLength = Math.max(0, endOffset - startOffset);
return {
startRow,
endRow,
startOffset,
endOffset,
byteLength,
};
}
public async close(): Promise<void> {
if (this.fileHandle) {
await this.fileHandle.close();
this.fileHandle = null;
}
}
}
|