@gmod/tabix 1.5.14 → 1.6.0
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 +9 -5
- package/README.md +182 -35
- package/dist/chunk.js +0 -1
- package/dist/chunk.js.map +1 -1
- package/dist/csi.d.ts +1 -1
- package/dist/csi.js +132 -128
- package/dist/csi.js.map +1 -1
- package/dist/indexFile.js +15 -41
- package/dist/indexFile.js.map +1 -1
- package/dist/tabix-bundle.js +2 -0
- package/dist/tabix-bundle.js.LICENSE.txt +8 -0
- package/dist/tabixIndexedFile.d.ts +49 -25
- package/dist/tabixIndexedFile.js +214 -195
- package/dist/tabixIndexedFile.js.map +1 -1
- package/dist/tbi.js +169 -173
- package/dist/tbi.js.map +1 -1
- package/dist/util.d.ts +7 -6
- package/dist/util.js +9 -19
- package/dist/util.js.map +1 -1
- package/dist/virtualOffset.d.ts +0 -1
- package/dist/virtualOffset.js +0 -13
- package/dist/virtualOffset.js.map +1 -1
- package/esm/chunk.js +0 -1
- package/esm/chunk.js.map +1 -1
- package/esm/csi.d.ts +1 -1
- package/esm/csi.js +4 -4
- package/esm/csi.js.map +1 -1
- package/esm/indexFile.js +1 -1
- package/esm/indexFile.js.map +1 -1
- package/esm/tabixIndexedFile.d.ts +49 -25
- package/esm/tabixIndexedFile.js +134 -90
- package/esm/tabixIndexedFile.js.map +1 -1
- package/esm/tbi.js +6 -5
- package/esm/tbi.js.map +1 -1
- package/esm/util.d.ts +7 -6
- package/esm/util.js +6 -5
- package/esm/util.js.map +1 -1
- package/esm/virtualOffset.d.ts +0 -1
- package/esm/virtualOffset.js +0 -13
- package/esm/virtualOffset.js.map +1 -1
- package/package.json +24 -19
- package/src/chunk.ts +0 -1
- package/src/csi.ts +6 -5
- package/src/indexFile.ts +1 -1
- package/src/tabixIndexedFile.ts +141 -106
- package/src/tbi.ts +7 -6
- package/src/util.ts +7 -6
- package/src/virtualOffset.ts +7 -21
package/src/tabixIndexedFile.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import AbortablePromiseCache from '@gmod/abortable-promise-cache'
|
|
2
2
|
import LRU from 'quick-lru'
|
|
3
3
|
import { Buffer } from 'buffer'
|
|
4
|
-
import { GenericFilehandle, LocalFile } from 'generic-filehandle'
|
|
4
|
+
import { GenericFilehandle, RemoteFile, LocalFile } from 'generic-filehandle'
|
|
5
5
|
import { unzip, unzipChunkSlice } from '@gmod/bgzf-filehandle'
|
|
6
6
|
import { checkAbortSignal } from './util'
|
|
7
7
|
import IndexFile, { Options, IndexData } from './indexFile'
|
|
@@ -10,6 +10,11 @@ import Chunk from './chunk'
|
|
|
10
10
|
import TBI from './tbi'
|
|
11
11
|
import CSI from './csi'
|
|
12
12
|
|
|
13
|
+
function isASCII(str: string) {
|
|
14
|
+
// eslint-disable-next-line no-control-regex
|
|
15
|
+
return /^[\u0000-\u007F]*$/.test(str)
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
type GetLinesCallback = (line: string, fileOffset: number) => void
|
|
14
19
|
|
|
15
20
|
const decoder =
|
|
@@ -27,50 +32,60 @@ interface ReadChunk {
|
|
|
27
32
|
dpositions: number[]
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
function timeout(time: number) {
|
|
31
|
-
return new Promise(resolve => setTimeout(resolve, time))
|
|
32
|
-
}
|
|
33
35
|
export default class TabixIndexedFile {
|
|
34
36
|
private filehandle: GenericFilehandle
|
|
35
37
|
private index: IndexFile
|
|
36
|
-
private chunkSizeLimit: number
|
|
37
|
-
private yieldTime: number
|
|
38
38
|
private renameRefSeq: (n: string) => string
|
|
39
39
|
private chunkCache: AbortablePromiseCache<Chunk, ReadChunk>
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* @param {object} args
|
|
43
|
+
*
|
|
43
44
|
* @param {string} [args.path]
|
|
45
|
+
*
|
|
44
46
|
* @param {filehandle} [args.filehandle]
|
|
47
|
+
*
|
|
45
48
|
* @param {string} [args.tbiPath]
|
|
49
|
+
*
|
|
46
50
|
* @param {filehandle} [args.tbiFilehandle]
|
|
51
|
+
*
|
|
47
52
|
* @param {string} [args.csiPath]
|
|
53
|
+
*
|
|
48
54
|
* @param {filehandle} [args.csiFilehandle]
|
|
49
|
-
*
|
|
50
|
-
* @param {
|
|
51
|
-
*
|
|
52
|
-
*
|
|
55
|
+
*
|
|
56
|
+
* @param {url} [args.url]
|
|
57
|
+
*
|
|
58
|
+
* @param {csiUrl} [args.csiUrl]
|
|
59
|
+
*
|
|
60
|
+
* @param {tbiUrl} [args.tbiUrl]
|
|
61
|
+
*
|
|
62
|
+
* @param {function} [args.renameRefSeqs] optional function with sig `string
|
|
63
|
+
* => string` to transform reference sequence names for the purpose of
|
|
64
|
+
* indexing and querying. note that the data that is returned is not altered,
|
|
65
|
+
* just the names of the reference sequences that are used for querying.
|
|
53
66
|
*/
|
|
54
67
|
constructor({
|
|
55
68
|
path,
|
|
56
69
|
filehandle,
|
|
70
|
+
url,
|
|
57
71
|
tbiPath,
|
|
72
|
+
tbiUrl,
|
|
58
73
|
tbiFilehandle,
|
|
59
74
|
csiPath,
|
|
75
|
+
csiUrl,
|
|
60
76
|
csiFilehandle,
|
|
61
|
-
yieldTime = 500,
|
|
62
|
-
chunkSizeLimit = 50000000,
|
|
63
77
|
renameRefSeqs = n => n,
|
|
64
78
|
chunkCacheSize = 5 * 2 ** 20,
|
|
65
79
|
}: {
|
|
66
80
|
path?: string
|
|
67
81
|
filehandle?: GenericFilehandle
|
|
82
|
+
url?: string
|
|
68
83
|
tbiPath?: string
|
|
84
|
+
tbiUrl?: string
|
|
69
85
|
tbiFilehandle?: GenericFilehandle
|
|
70
86
|
csiPath?: string
|
|
87
|
+
csiUrl?: string
|
|
71
88
|
csiFilehandle?: GenericFilehandle
|
|
72
|
-
yieldTime?: number
|
|
73
|
-
chunkSizeLimit?: number
|
|
74
89
|
renameRefSeqs?: (n: string) => string
|
|
75
90
|
chunkCacheSize?: number
|
|
76
91
|
}) {
|
|
@@ -78,6 +93,8 @@ export default class TabixIndexedFile {
|
|
|
78
93
|
this.filehandle = filehandle
|
|
79
94
|
} else if (path) {
|
|
80
95
|
this.filehandle = new LocalFile(path)
|
|
96
|
+
} else if (url) {
|
|
97
|
+
this.filehandle = new RemoteFile(url)
|
|
81
98
|
} else {
|
|
82
99
|
throw new TypeError('must provide either filehandle or path')
|
|
83
100
|
}
|
|
@@ -107,15 +124,25 @@ export default class TabixIndexedFile {
|
|
|
107
124
|
filehandle: new LocalFile(`${path}.tbi`),
|
|
108
125
|
renameRefSeqs,
|
|
109
126
|
})
|
|
127
|
+
} else if (csiUrl) {
|
|
128
|
+
this.index = new CSI({
|
|
129
|
+
filehandle: new RemoteFile(csiUrl),
|
|
130
|
+
})
|
|
131
|
+
} else if (tbiUrl) {
|
|
132
|
+
this.index = new TBI({
|
|
133
|
+
filehandle: new RemoteFile(tbiUrl),
|
|
134
|
+
})
|
|
135
|
+
} else if (url) {
|
|
136
|
+
this.index = new TBI({
|
|
137
|
+
filehandle: new RemoteFile(`${url}.tbi`),
|
|
138
|
+
})
|
|
110
139
|
} else {
|
|
111
140
|
throw new TypeError(
|
|
112
|
-
'must provide one of tbiFilehandle, tbiPath, csiFilehandle,
|
|
141
|
+
'must provide one of tbiFilehandle, tbiPath, csiFilehandle, csiPath, tbiUrl, csiUrl',
|
|
113
142
|
)
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
this.chunkSizeLimit = chunkSizeLimit
|
|
117
145
|
this.renameRefSeq = renameRefSeqs
|
|
118
|
-
this.yieldTime = yieldTime
|
|
119
146
|
this.chunkCache = new AbortablePromiseCache<Chunk, ReadChunk>({
|
|
120
147
|
cache: new LRU({ maxSize: Math.floor(chunkCacheSize / (1 << 16)) }),
|
|
121
148
|
fill: (args: Chunk, signal?: AbortSignal) =>
|
|
@@ -125,10 +152,16 @@ export default class TabixIndexedFile {
|
|
|
125
152
|
|
|
126
153
|
/**
|
|
127
154
|
* @param refName name of the reference sequence
|
|
155
|
+
*
|
|
128
156
|
* @param start start of the region (in 0-based half-open coordinates)
|
|
157
|
+
*
|
|
129
158
|
* @param end end of the region (in 0-based half-open coordinates)
|
|
130
|
-
*
|
|
131
|
-
* @
|
|
159
|
+
*
|
|
160
|
+
* @param opts callback called for each line in the region. can also pass a
|
|
161
|
+
* object param containing obj.lineCallback, obj.signal, etc
|
|
162
|
+
*
|
|
163
|
+
* @returns promise that is resolved when the whole read is finished,
|
|
164
|
+
* rejected on error
|
|
132
165
|
*/
|
|
133
166
|
async getLines(
|
|
134
167
|
refName: string,
|
|
@@ -139,9 +172,7 @@ export default class TabixIndexedFile {
|
|
|
139
172
|
let signal: AbortSignal | undefined
|
|
140
173
|
let options: Options = {}
|
|
141
174
|
let callback: (line: string, lineOffset: number) => void
|
|
142
|
-
|
|
143
|
-
throw new TypeError('line callback must be provided')
|
|
144
|
-
}
|
|
175
|
+
|
|
145
176
|
if (typeof opts === 'function') {
|
|
146
177
|
callback = opts
|
|
147
178
|
} else {
|
|
@@ -149,12 +180,6 @@ export default class TabixIndexedFile {
|
|
|
149
180
|
callback = opts.lineCallback
|
|
150
181
|
signal = opts.signal
|
|
151
182
|
}
|
|
152
|
-
if (refName === undefined) {
|
|
153
|
-
throw new TypeError('must provide a reference sequence name')
|
|
154
|
-
}
|
|
155
|
-
if (!callback) {
|
|
156
|
-
throw new TypeError('line callback must be provided')
|
|
157
|
-
}
|
|
158
183
|
|
|
159
184
|
const metadata = await this.index.getMetadata(options)
|
|
160
185
|
checkAbortSignal(signal)
|
|
@@ -172,21 +197,8 @@ export default class TabixIndexedFile {
|
|
|
172
197
|
const chunks = await this.index.blocksForRange(refName, start, end, options)
|
|
173
198
|
checkAbortSignal(signal)
|
|
174
199
|
|
|
175
|
-
// check the chunks for any that are over the size limit. if
|
|
176
|
-
// any are, don't fetch any of them
|
|
177
|
-
for (const chunk of chunks) {
|
|
178
|
-
const size = chunk.fetchedSize()
|
|
179
|
-
if (size > this.chunkSizeLimit) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`Too much data. Chunk size ${size.toLocaleString()} bytes exceeds chunkSizeLimit of ${this.chunkSizeLimit.toLocaleString()}.`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
200
|
// now go through each chunk and parse and filter the lines out of it
|
|
187
|
-
let last = Date.now()
|
|
188
201
|
for (const c of chunks) {
|
|
189
|
-
let previousStartCoordinate: number | undefined
|
|
190
202
|
const { buffer, cpositions, dpositions } = await this.chunkCache.get(
|
|
191
203
|
c.toString(),
|
|
192
204
|
c,
|
|
@@ -196,16 +208,33 @@ export default class TabixIndexedFile {
|
|
|
196
208
|
checkAbortSignal(signal)
|
|
197
209
|
let blockStart = 0
|
|
198
210
|
let pos = 0
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
|
|
212
|
+
const str = decoder?.decode(buffer) ?? buffer.toString()
|
|
213
|
+
// fast path, Buffer is just ASCII chars and not gigantor, can be
|
|
214
|
+
// converted to string and processed directly. if it is not ASCII or
|
|
215
|
+
// gigantic (chrome max str len is 512Mb), we have to decode line by line
|
|
216
|
+
const strIsASCII = buffer.length < 500_000_000 && isASCII(str)
|
|
217
|
+
while (blockStart < str.length) {
|
|
218
|
+
let line: string
|
|
219
|
+
let n: number
|
|
220
|
+
if (strIsASCII) {
|
|
221
|
+
n = str.indexOf('\n', blockStart)
|
|
222
|
+
if (n === -1) {
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
line = str.slice(blockStart, n)
|
|
226
|
+
} else {
|
|
227
|
+
n = buffer.indexOf('\n', blockStart)
|
|
228
|
+
if (n === -1) {
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
const b = buffer.slice(blockStart, n)
|
|
232
|
+
line = decoder?.decode(b) ?? b.toString()
|
|
203
233
|
}
|
|
204
|
-
const b = buffer.slice(blockStart, n)
|
|
205
|
-
const line = decoder?.decode(b) ?? b.toString()
|
|
206
234
|
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
207
236
|
if (dpositions) {
|
|
208
|
-
while (blockStart + c.minv.dataPosition >= dpositions[pos++]) {}
|
|
237
|
+
while (blockStart + c.minv.dataPosition >= dpositions[pos++]!) {}
|
|
209
238
|
pos--
|
|
210
239
|
}
|
|
211
240
|
|
|
@@ -218,48 +247,31 @@ export default class TabixIndexedFile {
|
|
|
218
247
|
line,
|
|
219
248
|
)
|
|
220
249
|
|
|
221
|
-
// do a small check just to make sure that the lines are really sorted
|
|
222
|
-
// by start coordinate
|
|
223
|
-
if (
|
|
224
|
-
previousStartCoordinate !== undefined &&
|
|
225
|
-
startCoordinate !== undefined &&
|
|
226
|
-
previousStartCoordinate > startCoordinate
|
|
227
|
-
) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
`Lines not sorted by start coordinate (${previousStartCoordinate} > ${startCoordinate}), this file is not usable with Tabix.`,
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
previousStartCoordinate = startCoordinate
|
|
233
|
-
|
|
234
250
|
if (overlaps) {
|
|
235
251
|
callback(
|
|
236
|
-
line
|
|
237
|
-
// cpositions[pos] refers to actual file offset of a bgzip block
|
|
252
|
+
line,
|
|
253
|
+
// cpositions[pos] refers to actual file offset of a bgzip block
|
|
254
|
+
// boundaries
|
|
238
255
|
//
|
|
239
|
-
// we multiply by (1 <<8) in order to make sure each block has a
|
|
240
|
-
// address space so that data in that block could never
|
|
256
|
+
// we multiply by (1 <<8) in order to make sure each block has a
|
|
257
|
+
// "unique" address space so that data in that block could never
|
|
258
|
+
// overlap
|
|
241
259
|
//
|
|
242
|
-
// then the blockStart-dpositions is an uncompressed file offset
|
|
243
|
-
// that bgzip block boundary, and since the cpositions are
|
|
244
|
-
// (1 << 8) these uncompressed offsets get a unique
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
// then the blockStart-dpositions is an uncompressed file offset
|
|
261
|
+
// from that bgzip block boundary, and since the cpositions are
|
|
262
|
+
// multiplied by (1 << 8) these uncompressed offsets get a unique
|
|
263
|
+
// space
|
|
264
|
+
cpositions[pos]! * (1 << 8) +
|
|
265
|
+
(blockStart - dpositions[pos]!) +
|
|
247
266
|
c.minv.dataPosition +
|
|
248
267
|
1,
|
|
249
268
|
)
|
|
250
269
|
} else if (startCoordinate !== undefined && startCoordinate >= end) {
|
|
251
|
-
// the lines were overlapping the region, but now have stopped, so
|
|
252
|
-
//
|
|
253
|
-
//
|
|
270
|
+
// the lines were overlapping the region, but now have stopped, so we
|
|
271
|
+
// must be at the end of the relevant data and we can stop processing
|
|
272
|
+
// data now
|
|
254
273
|
return
|
|
255
274
|
}
|
|
256
|
-
|
|
257
|
-
// yield if we have emitted beyond the yield limit
|
|
258
|
-
if (this.yieldTime && last - Date.now() > this.yieldTime) {
|
|
259
|
-
last = Date.now()
|
|
260
|
-
checkAbortSignal(signal)
|
|
261
|
-
await timeout(1)
|
|
262
|
-
}
|
|
263
275
|
blockStart = n + 1
|
|
264
276
|
}
|
|
265
277
|
}
|
|
@@ -270,14 +282,16 @@ export default class TabixIndexedFile {
|
|
|
270
282
|
}
|
|
271
283
|
|
|
272
284
|
/**
|
|
273
|
-
* get a buffer containing the "header" region of
|
|
274
|
-
*
|
|
275
|
-
* non-meta line
|
|
285
|
+
* get a buffer containing the "header" region of the file, which are the
|
|
286
|
+
* bytes up to the first non-meta line
|
|
276
287
|
*/
|
|
277
288
|
async getHeaderBuffer(opts: Options = {}) {
|
|
278
289
|
const { firstDataLine, metaChar, maxBlockSize } =
|
|
279
290
|
await this.getMetadata(opts)
|
|
291
|
+
|
|
280
292
|
checkAbortSignal(opts.signal)
|
|
293
|
+
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
281
295
|
const maxFetch = (firstDataLine?.blockPosition || 0) + maxBlockSize
|
|
282
296
|
// TODO: what if we don't have a firstDataLine, and the header
|
|
283
297
|
// actually takes up more than one block? this case is not covered here
|
|
@@ -299,14 +313,14 @@ export default class TabixIndexedFile {
|
|
|
299
313
|
lastNewline = i
|
|
300
314
|
}
|
|
301
315
|
}
|
|
302
|
-
return bytes.
|
|
316
|
+
return bytes.subarray(0, lastNewline + 1)
|
|
303
317
|
}
|
|
304
318
|
return bytes
|
|
305
319
|
}
|
|
306
320
|
|
|
307
321
|
/**
|
|
308
|
-
* get a string containing the "header" region of the
|
|
309
|
-
*
|
|
322
|
+
* get a string containing the "header" region of the file, is the portion up
|
|
323
|
+
* to the first non-meta line
|
|
310
324
|
*
|
|
311
325
|
* @returns {Promise} for a string
|
|
312
326
|
*/
|
|
@@ -316,9 +330,8 @@ export default class TabixIndexedFile {
|
|
|
316
330
|
}
|
|
317
331
|
|
|
318
332
|
/**
|
|
319
|
-
* get an array of reference sequence names, in the order in which
|
|
320
|
-
*
|
|
321
|
-
* to these names.
|
|
333
|
+
* get an array of reference sequence names, in the order in which they occur
|
|
334
|
+
* in the file. reference sequence renaming is not applied to these names.
|
|
322
335
|
*/
|
|
323
336
|
async getReferenceSequenceNames(opts: Options = {}) {
|
|
324
337
|
const metadata = await this.getMetadata(opts)
|
|
@@ -326,12 +339,17 @@ export default class TabixIndexedFile {
|
|
|
326
339
|
}
|
|
327
340
|
|
|
328
341
|
/**
|
|
329
|
-
* @param {object} metadata metadata object from the parsed index,
|
|
330
|
-
*
|
|
342
|
+
* @param {object} metadata metadata object from the parsed index, containing
|
|
343
|
+
* columnNumbers, metaChar, and format
|
|
344
|
+
*
|
|
331
345
|
* @param {string} regionRefName
|
|
346
|
+
*
|
|
332
347
|
* @param {number} regionStart region start coordinate (0-based-half-open)
|
|
348
|
+
*
|
|
333
349
|
* @param {number} regionEnd region end coordinate (0-based-half-open)
|
|
350
|
+
*
|
|
334
351
|
* @param {array[string]} line
|
|
352
|
+
*
|
|
335
353
|
* @returns {object} like `{startCoordinate, overlaps}`. overlaps is boolean,
|
|
336
354
|
* true if line is a data line that overlaps the given region
|
|
337
355
|
*/
|
|
@@ -364,22 +382,25 @@ export default class TabixIndexedFile {
|
|
|
364
382
|
}
|
|
365
383
|
const maxColumn = Math.max(ref, start, end)
|
|
366
384
|
|
|
367
|
-
// this code is kind of complex, but it is fairly fast.
|
|
368
|
-
//
|
|
369
|
-
//
|
|
385
|
+
// this code is kind of complex, but it is fairly fast. basically, we want
|
|
386
|
+
// to avoid doing a split, because if the lines are really long that could
|
|
387
|
+
// lead to us allocating a bunch of extra memory, which is slow
|
|
370
388
|
|
|
371
389
|
let currentColumnNumber = 1 // cols are numbered starting at 1 in the index metadata
|
|
372
390
|
let currentColumnStart = 0
|
|
373
391
|
let refSeq = ''
|
|
374
392
|
let startCoordinate = -Infinity
|
|
375
|
-
|
|
376
|
-
|
|
393
|
+
const l = line.length
|
|
394
|
+
for (let i = 0; i < l + 1; i++) {
|
|
395
|
+
if (line[i] === '\t' || i === l) {
|
|
377
396
|
if (currentColumnNumber === ref) {
|
|
378
397
|
if (
|
|
379
398
|
this.renameRefSeq(line.slice(currentColumnStart, i)) !==
|
|
380
399
|
regionRefName
|
|
381
400
|
) {
|
|
382
|
-
return {
|
|
401
|
+
return {
|
|
402
|
+
overlaps: false,
|
|
403
|
+
}
|
|
383
404
|
}
|
|
384
405
|
} else if (currentColumnNumber === start) {
|
|
385
406
|
startCoordinate = parseInt(line.slice(currentColumnStart, i), 10)
|
|
@@ -388,12 +409,18 @@ export default class TabixIndexedFile {
|
|
|
388
409
|
startCoordinate -= 1
|
|
389
410
|
}
|
|
390
411
|
if (startCoordinate >= regionEnd) {
|
|
391
|
-
return {
|
|
412
|
+
return {
|
|
413
|
+
startCoordinate,
|
|
414
|
+
overlaps: false,
|
|
415
|
+
}
|
|
392
416
|
}
|
|
393
417
|
if (end === 0 || end === start) {
|
|
394
418
|
// if we have no end, we assume the feature is 1 bp long
|
|
395
419
|
if (startCoordinate + 1 <= regionStart) {
|
|
396
|
-
return {
|
|
420
|
+
return {
|
|
421
|
+
startCoordinate,
|
|
422
|
+
overlaps: false,
|
|
423
|
+
}
|
|
397
424
|
}
|
|
398
425
|
}
|
|
399
426
|
} else if (format === 'VCF' && currentColumnNumber === 4) {
|
|
@@ -407,9 +434,11 @@ export default class TabixIndexedFile {
|
|
|
407
434
|
refSeq,
|
|
408
435
|
line.slice(currentColumnStart, i),
|
|
409
436
|
)
|
|
410
|
-
: parseInt(line.slice(currentColumnStart, i), 10)
|
|
437
|
+
: Number.parseInt(line.slice(currentColumnStart, i), 10)
|
|
411
438
|
if (endCoordinate <= regionStart) {
|
|
412
|
-
return {
|
|
439
|
+
return {
|
|
440
|
+
overlaps: false,
|
|
441
|
+
}
|
|
413
442
|
}
|
|
414
443
|
}
|
|
415
444
|
currentColumnStart = i + 1
|
|
@@ -419,7 +448,10 @@ export default class TabixIndexedFile {
|
|
|
419
448
|
}
|
|
420
449
|
}
|
|
421
450
|
}
|
|
422
|
-
return {
|
|
451
|
+
return {
|
|
452
|
+
startCoordinate,
|
|
453
|
+
overlaps: true,
|
|
454
|
+
}
|
|
423
455
|
}
|
|
424
456
|
|
|
425
457
|
_getVcfEnd(startCoordinate: number, refSeq: string, info: any) {
|
|
@@ -450,8 +482,11 @@ export default class TabixIndexedFile {
|
|
|
450
482
|
}
|
|
451
483
|
|
|
452
484
|
/**
|
|
453
|
-
* return the approximate number of data lines in the given reference
|
|
485
|
+
* return the approximate number of data lines in the given reference
|
|
486
|
+
* sequence
|
|
487
|
+
*
|
|
454
488
|
* @param refSeq reference sequence name
|
|
489
|
+
*
|
|
455
490
|
* @returns number of data lines present on that reference sequence
|
|
456
491
|
*/
|
|
457
492
|
async lineCount(refName: string, opts: Options = {}) {
|
|
@@ -468,7 +503,7 @@ export default class TabixIndexedFile {
|
|
|
468
503
|
opts,
|
|
469
504
|
)
|
|
470
505
|
|
|
471
|
-
return buffer.
|
|
506
|
+
return buffer.subarray(0, bytesRead)
|
|
472
507
|
}
|
|
473
508
|
|
|
474
509
|
/**
|
|
@@ -476,8 +511,8 @@ export default class TabixIndexedFile {
|
|
|
476
511
|
* contiguous bgzip blocks) of the file
|
|
477
512
|
*/
|
|
478
513
|
async readChunk(c: Chunk, opts: Options = {}) {
|
|
479
|
-
// fetch the uncompressed data, uncompress carefully a block at a time,
|
|
480
|
-
//
|
|
514
|
+
// fetch the uncompressed data, uncompress carefully a block at a time, and
|
|
515
|
+
// stop when done
|
|
481
516
|
|
|
482
517
|
const data = await this._readRegion(
|
|
483
518
|
c.minv.blockPosition,
|
package/src/tbi.ts
CHANGED
|
@@ -10,7 +10,8 @@ const TBI_MAGIC = 21578324 // TBI\1
|
|
|
10
10
|
const TAD_LIDX_SHIFT = 14
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* calculate the list of bins that may overlap with region [beg,end)
|
|
13
|
+
* calculate the list of bins that may overlap with region [beg,end)
|
|
14
|
+
* (zero-based half-open)
|
|
14
15
|
*/
|
|
15
16
|
function reg2bins(beg: number, end: number) {
|
|
16
17
|
beg += 1 // < convert to 1-based closed
|
|
@@ -22,16 +23,16 @@ function reg2bins(beg: number, end: number) {
|
|
|
22
23
|
[73 + (beg >> 20), 73 + (end >> 20)],
|
|
23
24
|
[585 + (beg >> 17), 585 + (end >> 17)],
|
|
24
25
|
[4681 + (beg >> 14), 4681 + (end >> 14)],
|
|
25
|
-
]
|
|
26
|
+
] as const
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export default class TabixIndex extends IndexFile {
|
|
29
30
|
async lineCount(refName: string, opts: Options = {}) {
|
|
30
31
|
const indexData = await this.parse(opts)
|
|
31
|
-
|
|
32
|
+
const refId = indexData.refNameToId[refName]
|
|
33
|
+
if (refId === undefined) {
|
|
32
34
|
return -1
|
|
33
35
|
}
|
|
34
|
-
const refId = indexData.refNameToId[refName]
|
|
35
36
|
const idx = indexData.indices[refId]
|
|
36
37
|
if (!idx) {
|
|
37
38
|
return -1
|
|
@@ -194,10 +195,10 @@ export default class TabixIndex extends IndexFile {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
const indexData = await this.parse(opts)
|
|
197
|
-
|
|
198
|
+
const refId = indexData.refNameToId[refName]
|
|
199
|
+
if (refId === undefined) {
|
|
198
200
|
return []
|
|
199
201
|
}
|
|
200
|
-
const refId = indexData.refNameToId[refName]
|
|
201
202
|
const ba = indexData.indices[refId]
|
|
202
203
|
if (!ba) {
|
|
203
204
|
return []
|
package/src/util.ts
CHANGED
|
@@ -15,14 +15,15 @@ class AbortError extends Error {
|
|
|
15
15
|
public code: string | undefined
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Properly check if the given AbortSignal is aborted.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* with a `code` attribute set to `ERR_ABORTED`.
|
|
18
|
+
* Properly check if the given AbortSignal is aborted. Per the standard, if the
|
|
19
|
+
* signal reads as aborted, this function throws either a DOMException
|
|
20
|
+
* AbortError, or a regular error with a `code` attribute set to `ERR_ABORTED`.
|
|
22
21
|
*
|
|
23
22
|
* For convenience, passing `undefined` is a no-op
|
|
24
23
|
*
|
|
25
|
-
* @param {AbortSignal} [signal] an AbortSignal, or anything with an `aborted`
|
|
24
|
+
* @param {AbortSignal} [signal] an AbortSignal, or anything with an `aborted`
|
|
25
|
+
* attribute
|
|
26
|
+
*
|
|
26
27
|
* @returns nothing
|
|
27
28
|
*/
|
|
28
29
|
export function checkAbortSignal(signal?: AbortSignal) {
|
|
@@ -59,7 +60,7 @@ export function canMergeBlocks(chunk1: Chunk, chunk2: Chunk) {
|
|
|
59
60
|
)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
export function optimizeChunks(chunks: Chunk[], lowest
|
|
63
|
+
export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
|
|
63
64
|
const mergedChunks: Chunk[] = []
|
|
64
65
|
let lastChunk: Chunk | null = null
|
|
65
66
|
|
package/src/virtualOffset.ts
CHANGED
|
@@ -16,20 +16,6 @@ export default class VirtualOffset {
|
|
|
16
16
|
this.blockPosition - b.blockPosition || this.dataPosition - b.dataPosition
|
|
17
17
|
)
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
static min(...args: VirtualOffset[]) {
|
|
21
|
-
let min
|
|
22
|
-
let i = 0
|
|
23
|
-
for (; !min; i += 1) {
|
|
24
|
-
min = args[i]
|
|
25
|
-
}
|
|
26
|
-
for (; i < args.length; i += 1) {
|
|
27
|
-
if (min.compareTo(args[i]) > 0) {
|
|
28
|
-
min = args[i]
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return min
|
|
32
|
-
}
|
|
33
19
|
}
|
|
34
20
|
export function fromBytes(bytes: Buffer, offset = 0, bigendian = false) {
|
|
35
21
|
if (bigendian) {
|
|
@@ -37,12 +23,12 @@ export function fromBytes(bytes: Buffer, offset = 0, bigendian = false) {
|
|
|
37
23
|
}
|
|
38
24
|
|
|
39
25
|
return new VirtualOffset(
|
|
40
|
-
bytes[offset + 7] * 0x10000000000 +
|
|
41
|
-
bytes[offset + 6] * 0x100000000 +
|
|
42
|
-
bytes[offset + 5] * 0x1000000 +
|
|
43
|
-
bytes[offset + 4] * 0x10000 +
|
|
44
|
-
bytes[offset + 3] * 0x100 +
|
|
45
|
-
bytes[offset + 2]
|
|
46
|
-
(bytes[offset + 1] << 8) | bytes[offset]
|
|
26
|
+
bytes[offset + 7]! * 0x10000000000 +
|
|
27
|
+
bytes[offset + 6]! * 0x100000000 +
|
|
28
|
+
bytes[offset + 5]! * 0x1000000 +
|
|
29
|
+
bytes[offset + 4]! * 0x10000 +
|
|
30
|
+
bytes[offset + 3]! * 0x100 +
|
|
31
|
+
bytes[offset + 2]!,
|
|
32
|
+
(bytes[offset + 1]! << 8) | bytes[offset]!,
|
|
47
33
|
)
|
|
48
34
|
}
|