@gmod/tabix 1.5.5 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/indexFile.ts CHANGED
@@ -1,5 +1,3 @@
1
- import AbortablePromiseCache from 'abortable-promise-cache'
2
- import QuickLRU from 'quick-lru'
3
1
  import { GenericFilehandle } from 'generic-filehandle'
4
2
  import VirtualOffset from './virtualOffset'
5
3
  import Chunk from './chunk'
@@ -10,15 +8,21 @@ export interface Options {
10
8
  signal?: AbortSignal
11
9
  }
12
10
 
11
+ export interface IndexData {
12
+ refNameToId: { [key: string]: number }
13
+ refIdToName: string[]
14
+ metaChar: string | null
15
+ columnNumbers: { ref: number; start: number; end: number }
16
+ coordinateType: string
17
+ format: string
18
+ [key: string]: any
19
+ }
20
+
13
21
  export default abstract class IndexFile {
14
22
  public filehandle: GenericFilehandle
15
23
  public renameRefSeq: (arg0: string) => string
16
- private _parseCache: any
24
+ private parseP?: Promise<IndexData>
17
25
 
18
- /**
19
- * @param {filehandle} filehandle
20
- * @param {function} [renameRefSeqs]
21
- */
22
26
  constructor({
23
27
  filehandle,
24
28
  renameRefSeqs = (n: string) => n,
@@ -32,10 +36,7 @@ export default abstract class IndexFile {
32
36
 
33
37
  public abstract lineCount(refName: string, args: Options): Promise<number>
34
38
 
35
- protected abstract _parse(opts: Options): Promise<{
36
- refNameToId: { [key: string]: number }
37
- refIdToName: string[]
38
- }>
39
+ protected abstract _parse(opts: Options): Promise<IndexData>
39
40
 
40
41
  public async getMetadata(opts: Options = {}) {
41
42
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -64,13 +65,13 @@ export default abstract class IndexFile {
64
65
  }
65
66
 
66
67
  async parse(opts: Options = {}) {
67
- if (!this._parseCache) {
68
- this._parseCache = new AbortablePromiseCache({
69
- cache: new QuickLRU({ maxSize: 1 }),
70
- fill: () => this._parse(opts),
68
+ if (!this.parseP) {
69
+ this.parseP = this._parse(opts).catch(e => {
70
+ this.parseP = undefined
71
+ throw e
71
72
  })
72
73
  }
73
- return this._parseCache.get('index', null, undefined)
74
+ return this.parseP
74
75
  }
75
76
 
76
77
  async hasRefSeq(seqId: number, opts: Options = {}) {
@@ -3,7 +3,7 @@ import LRU from 'quick-lru'
3
3
  import { GenericFilehandle, LocalFile } from 'generic-filehandle'
4
4
  import { unzip, unzipChunkSlice } from '@gmod/bgzf-filehandle'
5
5
  import { checkAbortSignal } from './util'
6
- import IndexFile, { Options } from './indexFile'
6
+ import IndexFile, { Options, IndexData } from './indexFile'
7
7
 
8
8
  import Chunk from './chunk'
9
9
  import TBI from './tbi'
@@ -11,23 +11,32 @@ import CSI from './csi'
11
11
 
12
12
  type GetLinesCallback = (line: string, fileOffset: number) => void
13
13
 
14
+ const decoder =
15
+ typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8') : undefined
16
+
14
17
  interface GetLinesOpts {
15
18
  [key: string]: unknown
16
19
  signal?: AbortSignal
17
20
  lineCallback: GetLinesCallback
18
21
  }
19
22
 
23
+ interface ReadChunk {
24
+ buffer: Buffer
25
+ cpositions: number[]
26
+ dpositions: number[]
27
+ }
28
+
20
29
  function timeout(time: number) {
21
- return new Promise(resolve => {
22
- setTimeout(resolve, time)
23
- })
30
+ return new Promise(resolve => setTimeout(resolve, time))
24
31
  }
25
32
  export default class TabixIndexedFile {
26
33
  private filehandle: GenericFilehandle
27
34
  private index: IndexFile
28
35
  private chunkSizeLimit: number
36
+ private yieldTime: number
29
37
  private renameRefSeq: (n: string) => string
30
- private chunkCache: any
38
+ private chunkCache: AbortablePromiseCache<Chunk, ReadChunk>
39
+
31
40
  /**
32
41
  * @param {object} args
33
42
  * @param {string} [args.path]
@@ -36,12 +45,10 @@ export default class TabixIndexedFile {
36
45
  * @param {filehandle} [args.tbiFilehandle]
37
46
  * @param {string} [args.csiPath]
38
47
  * @param {filehandle} [args.csiFilehandle]
39
- * @param {chunkSizeLimit} default 50MiB
48
+ * @param {number} [args.yieldTime] yield to main thread after N milliseconds if reading features is taking a long time to avoid hanging main thread
40
49
  * @param {function} [args.renameRefSeqs] optional function with sig `string => string` to transform
41
50
  * reference sequence names for the purpose of indexing and querying. note that the data that is returned is
42
51
  * not altered, just the names of the reference sequences that are used for querying.
43
- * @param {number} [args.chunkCacheSize] maximum size in bytes of the chunk cache. default 5MB
44
- * @param {number} [args.blockCacheSize] maximum size in bytes of the block cache. default 5MB
45
52
  */
46
53
  constructor({
47
54
  path,
@@ -50,6 +57,7 @@ export default class TabixIndexedFile {
50
57
  tbiFilehandle,
51
58
  csiPath,
52
59
  csiFilehandle,
60
+ yieldTime = 500,
53
61
  chunkSizeLimit = 50000000,
54
62
  renameRefSeqs = n => n,
55
63
  chunkCacheSize = 5 * 2 ** 20,
@@ -60,6 +68,7 @@ export default class TabixIndexedFile {
60
68
  tbiFilehandle?: GenericFilehandle
61
69
  csiPath?: string
62
70
  csiFilehandle?: GenericFilehandle
71
+ yieldTime: number
63
72
  chunkSizeLimit?: number
64
73
  renameRefSeqs?: (n: string) => string
65
74
  chunkCacheSize?: number
@@ -105,21 +114,20 @@ export default class TabixIndexedFile {
105
114
 
106
115
  this.chunkSizeLimit = chunkSizeLimit
107
116
  this.renameRefSeq = renameRefSeqs
108
- this.chunkCache = new AbortablePromiseCache({
109
- cache: new LRU({
110
- maxSize: Math.floor(chunkCacheSize / (1 << 16)),
111
- }),
112
-
113
- fill: this.readChunk.bind(this),
117
+ this.yieldTime = yieldTime
118
+ this.chunkCache = new AbortablePromiseCache<Chunk, ReadChunk>({
119
+ cache: new LRU({ maxSize: Math.floor(chunkCacheSize / (1 << 16)) }),
120
+ fill: (args: Chunk, signal?: AbortSignal) =>
121
+ this.readChunk(args, { signal }),
114
122
  })
115
123
  }
116
124
 
117
125
  /**
118
- * @param {string} refName name of the reference sequence
119
- * @param {number} start start of the region (in 0-based half-open coordinates)
120
- * @param {number} end end of the region (in 0-based half-open coordinates)
121
- * @param {function|object} lineCallback callback called for each line in the region. can also pass a object param containing obj.lineCallback, obj.signal, etc
122
- * @returns {Promise} resolved when the whole read is finished, rejected on error
126
+ * @param refName name of the reference sequence
127
+ * @param start start of the region (in 0-based half-open coordinates)
128
+ * @param end end of the region (in 0-based half-open coordinates)
129
+ * @param opts callback called for each line in the region. can also pass a object param containing obj.lineCallback, obj.signal, etc
130
+ * @returns promise that is resolved when the whole read is finished, rejected on error
123
131
  */
124
132
  async getLines(
125
133
  refName: string,
@@ -185,24 +193,23 @@ export default class TabixIndexedFile {
185
193
  const { buffer, cpositions, dpositions } = await this.chunkCache.get(
186
194
  c.toString(),
187
195
  c,
188
- signal,
189
196
  )
190
197
 
191
- const lines = (
192
- typeof TextDecoder !== 'undefined'
193
- ? new TextDecoder('utf-8').decode(buffer)
194
- : buffer.toString()
195
- ).split('\n')
196
- lines.pop()
197
-
198
198
  checkAbortSignal(signal)
199
- let blockStart = c.minv.dataPosition
200
- let pos
201
-
202
- for (let i = 0; i < lines.length; i += 1) {
203
- const line = lines[i]
199
+ let blockStart = 0
200
+ let pos = 0
201
+ while (blockStart < buffer.length) {
202
+ const n = buffer.indexOf('\n', blockStart)
203
+ if (n === -1) {
204
+ break
205
+ }
206
+ const b = buffer.subarray(blockStart, n)
207
+ const line = decoder?.decode(b) || b.toString()
204
208
 
205
- for (pos = 0; blockStart >= dpositions[pos]; pos += 1) {}
209
+ if (dpositions) {
210
+ while (blockStart + c.minv.dataPosition >= dpositions[pos++]) {}
211
+ pos--
212
+ }
206
213
 
207
214
  // filter the line for whether it is within the requested range
208
215
  const { startCoordinate, overlaps } = this.checkLine(
@@ -213,7 +220,8 @@ export default class TabixIndexedFile {
213
220
  line,
214
221
  )
215
222
 
216
- // do a small check just to make sure that the lines are really sorted by start coordinate
223
+ // do a small check just to make sure that the lines are really sorted
224
+ // by start coordinate
217
225
  if (
218
226
  previousStartCoordinate !== undefined &&
219
227
  startCoordinate !== undefined &&
@@ -236,7 +244,10 @@ export default class TabixIndexedFile {
236
244
  // then the blockStart-dpositions is an uncompressed file offset from
237
245
  // that bgzip block boundary, and since the cpositions are multiplied by
238
246
  // (1 << 8) these uncompressed offsets get a unique space
239
- cpositions[pos] * (1 << 8) + (blockStart - dpositions[pos]),
247
+ cpositions[pos] * (1 << 8) +
248
+ (blockStart - dpositions[pos]) +
249
+ c.minv.dataPosition +
250
+ 1,
240
251
  )
241
252
  } else if (startCoordinate !== undefined && startCoordinate >= end) {
242
253
  // the lines were overlapping the region, but now have stopped, so
@@ -244,14 +255,14 @@ export default class TabixIndexedFile {
244
255
  // processing data now
245
256
  return
246
257
  }
247
- blockStart += line.length + 1
248
258
 
249
259
  // yield if we have emitted beyond the yield limit
250
- if (last - Date.now() > 500) {
260
+ if (this.yieldTime && last - Date.now() > this.yieldTime) {
251
261
  last = Date.now()
252
262
  checkAbortSignal(signal)
253
263
  await timeout(1)
254
264
  }
265
+ blockStart = n + 1
255
266
  }
256
267
  }
257
268
  }
@@ -264,18 +275,13 @@ export default class TabixIndexedFile {
264
275
  * get a buffer containing the "header" region of
265
276
  * the file, which are the bytes up to the first
266
277
  * non-meta line
267
- *
268
- * @returns {Promise} for a buffer
269
278
  */
270
279
  async getHeaderBuffer(opts: Options = {}) {
271
280
  const { firstDataLine, metaChar, maxBlockSize } = await this.getMetadata(
272
281
  opts,
273
282
  )
274
283
  checkAbortSignal(opts.signal)
275
- const maxFetch =
276
- firstDataLine && firstDataLine.blockPosition
277
- ? firstDataLine.blockPosition + maxBlockSize
278
- : maxBlockSize
284
+ const maxFetch = (firstDataLine?.blockPosition || 0) + maxBlockSize
279
285
  // TODO: what if we don't have a firstDataLine, and the header
280
286
  // actually takes up more than one block? this case is not covered here
281
287
 
@@ -305,7 +311,7 @@ export default class TabixIndexedFile {
305
311
  lastNewline = i
306
312
  }
307
313
  }
308
- bytes = bytes.slice(0, lastNewline + 1)
314
+ bytes = bytes.subarray(0, lastNewline + 1)
309
315
  }
310
316
  return bytes
311
317
  }
@@ -318,17 +324,13 @@ export default class TabixIndexedFile {
318
324
  */
319
325
  async getHeader(opts: Options = {}) {
320
326
  const bytes = await this.getHeaderBuffer(opts)
321
- checkAbortSignal(opts.signal)
322
327
  return bytes.toString('utf8')
323
328
  }
324
329
 
325
330
  /**
326
331
  * get an array of reference sequence names, in the order in which
327
- * they occur in the file.
328
- *
329
- * reference sequence renaming is not applied to these names.
330
- *
331
- * @returns {Promise} for an array of string sequence names
332
+ * they occur in the file. reference sequence renaming is not applied
333
+ * to these names.
332
334
  */
333
335
  async getReferenceSequenceNames(opts: Options = {}) {
334
336
  const metadata = await this.getMetadata(opts)
@@ -346,22 +348,13 @@ export default class TabixIndexedFile {
346
348
  * true if line is a data line that overlaps the given region
347
349
  */
348
350
  checkLine(
349
- {
350
- columnNumbers,
351
- metaChar,
352
- coordinateType,
353
- format,
354
- }: {
355
- columnNumbers: { ref: number; start: number; end: number }
356
- metaChar: string
357
- coordinateType: string
358
- format: string
359
- },
351
+ metadata: IndexData,
360
352
  regionRefName: string,
361
353
  regionStart: number,
362
354
  regionEnd: number,
363
355
  line: string,
364
356
  ) {
357
+ const { columnNumbers, metaChar, coordinateType, format } = metadata
365
358
  // skip meta lines
366
359
  if (line.charAt(0) === metaChar) {
367
360
  return { overlaps: false }
@@ -472,48 +465,43 @@ export default class TabixIndexedFile {
472
465
 
473
466
  /**
474
467
  * return the approximate number of data lines in the given reference sequence
475
- * @param {string} refSeq reference sequence name
476
- * @returns {Promise} for number of data lines present on that reference sequence
468
+ * @param refSeq reference sequence name
469
+ * @returns number of data lines present on that reference sequence
477
470
  */
478
471
  async lineCount(refName: string, opts: Options = {}) {
479
472
  return this.index.lineCount(refName, opts)
480
473
  }
481
474
 
482
- async _readRegion(
483
- position: number,
484
- compressedSize: number,
485
- opts: Options = {},
486
- ) {
475
+ async _readRegion(pos: number, size: number, opts: Options = {}) {
476
+ const b = Buffer.alloc(size)
487
477
  const { bytesRead, buffer } = await this.filehandle.read(
488
- Buffer.alloc(compressedSize),
478
+ b,
489
479
  0,
490
- compressedSize,
491
- position,
480
+ size,
481
+ pos,
492
482
  opts,
493
483
  )
494
484
 
495
- return bytesRead < compressedSize ? buffer.slice(0, bytesRead) : buffer
485
+ return buffer.subarray(0, bytesRead)
496
486
  }
497
487
 
498
488
  /**
499
489
  * read and uncompress the data in a chunk (composed of one or more
500
490
  * contiguous bgzip blocks) of the file
501
- * @param {Chunk} chunk
502
- * @returns {Promise} for a string chunk of the file
503
491
  */
504
- async readChunk(chunk: Chunk, opts: Options = {}) {
492
+ async readChunk(c: Chunk, opts: Options = {}) {
505
493
  // fetch the uncompressed data, uncompress carefully a block at a time,
506
494
  // and stop when done
507
495
 
508
- const compressedData = await this._readRegion(
509
- chunk.minv.blockPosition,
510
- chunk.fetchedSize(),
496
+ const data = await this._readRegion(
497
+ c.minv.blockPosition,
498
+ c.fetchedSize(),
511
499
  opts,
512
500
  )
513
501
  try {
514
- return unzipChunkSlice(compressedData, chunk)
502
+ return unzipChunkSlice(data, c)
515
503
  } catch (e) {
516
- throw new Error(`error decompressing chunk ${chunk.toString()} ${e}`)
504
+ throw new Error(`error decompressing c ${c.toString()} ${e}`)
517
505
  }
518
506
  }
519
507
  }
package/src/tbi.ts CHANGED
@@ -42,10 +42,10 @@ export default class TabixIndex extends IndexFile {
42
42
  return -1
43
43
  }
44
44
 
45
- // memoize
46
45
  // fetch and parse the index
47
46
  async _parse(opts: Options = {}) {
48
- const bytes = await unzip((await this.filehandle.readFile(opts)) as Buffer)
47
+ const buf = await this.filehandle.readFile(opts)
48
+ const bytes = await unzip(buf)
49
49
  checkAbortSignal(opts.signal)
50
50
 
51
51
  // check TBI magic numbers
@@ -83,7 +83,7 @@ export default class TabixIndex extends IndexFile {
83
83
  // read sequence dictionary
84
84
  const nameSectionLength = bytes.readInt32LE(32)
85
85
  const { refNameToId, refIdToName } = this._parseNameBytes(
86
- bytes.slice(36, 36 + nameSectionLength),
86
+ bytes.subarray(36, 36 + nameSectionLength),
87
87
  )
88
88
 
89
89
  // read the indexes for each reference sequence
@@ -155,7 +155,7 @@ export default class TabixIndex extends IndexFile {
155
155
  parsePseudoBin(bytes: Buffer, offset: number) {
156
156
  const lineCount = longToNumber(
157
157
  Long.fromBytesLE(
158
- bytes.slice(offset + 16, offset + 24) as unknown as number[],
158
+ bytes.subarray(offset + 16, offset + 24) as unknown as number[],
159
159
  true,
160
160
  ),
161
161
  )
package/src/declare.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare module 'abortable-promise-cache'
2
- declare module '@gmod/bgzf-filehandle'