@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/CHANGELOG.md +28 -19
- package/dist/csi.d.ts +40 -12
- package/dist/csi.js +14 -18
- package/dist/csi.js.map +1 -1
- package/dist/indexFile.d.ts +28 -8
- package/dist/indexFile.js +5 -14
- package/dist/indexFile.js.map +1 -1
- package/dist/tabixIndexedFile.d.ts +37 -35
- package/dist/tabixIndexedFile.js +50 -54
- package/dist/tabixIndexedFile.js.map +1 -1
- package/dist/tbi.d.ts +4 -4
- package/dist/tbi.js +4 -4
- package/dist/tbi.js.map +1 -1
- package/esm/csi.d.ts +40 -12
- package/esm/csi.js +14 -18
- package/esm/csi.js.map +1 -1
- package/esm/indexFile.d.ts +28 -8
- package/esm/indexFile.js +5 -11
- package/esm/indexFile.js.map +1 -1
- package/esm/tabixIndexedFile.d.ts +37 -35
- package/esm/tabixIndexedFile.js +50 -54
- package/esm/tabixIndexedFile.js.map +1 -1
- package/esm/tbi.d.ts +4 -4
- package/esm/tbi.js +4 -4
- package/esm/tbi.js.map +1 -1
- package/package.json +14 -14
- package/src/csi.ts +16 -28
- package/src/indexFile.ts +17 -16
- package/src/tabixIndexedFile.ts +68 -80
- package/src/tbi.ts +4 -4
- package/src/declare.d.ts +0 -2
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
|
|
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.
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
74
|
+
return this.parseP
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
async hasRefSeq(seqId: number, opts: Options = {}) {
|
package/src/tabixIndexedFile.ts
CHANGED
|
@@ -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:
|
|
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 {
|
|
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.
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
119
|
-
* @param
|
|
120
|
-
* @param
|
|
121
|
-
* @param
|
|
122
|
-
* @returns
|
|
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 =
|
|
200
|
-
let pos
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
|
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) +
|
|
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() >
|
|
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.
|
|
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
|
|
476
|
-
* @returns
|
|
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
|
-
|
|
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
|
-
|
|
478
|
+
b,
|
|
489
479
|
0,
|
|
490
|
-
|
|
491
|
-
|
|
480
|
+
size,
|
|
481
|
+
pos,
|
|
492
482
|
opts,
|
|
493
483
|
)
|
|
494
484
|
|
|
495
|
-
return
|
|
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(
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
496
|
+
const data = await this._readRegion(
|
|
497
|
+
c.minv.blockPosition,
|
|
498
|
+
c.fetchedSize(),
|
|
511
499
|
opts,
|
|
512
500
|
)
|
|
513
501
|
try {
|
|
514
|
-
return unzipChunkSlice(
|
|
502
|
+
return unzipChunkSlice(data, c)
|
|
515
503
|
} catch (e) {
|
|
516
|
-
throw new Error(`error decompressing
|
|
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
|
|
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.
|
|
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.
|
|
158
|
+
bytes.subarray(offset + 16, offset + 24) as unknown as number[],
|
|
159
159
|
true,
|
|
160
160
|
),
|
|
161
161
|
)
|
package/src/declare.d.ts
DELETED