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;
    }
  }
}