@gmod/tabix 3.2.0 → 3.2.2
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 +5 -1
- package/dist/tabix-bundle.js +1 -1
- package/dist/tabixIndexedFile.d.ts +28 -20
- package/dist/tabixIndexedFile.js +120 -122
- package/dist/tabixIndexedFile.js.map +1 -1
- package/dist/tbi.js +0 -1
- package/dist/tbi.js.map +1 -1
- package/dist/util.d.ts +0 -20
- package/dist/util.js +0 -41
- package/dist/util.js.map +1 -1
- package/esm/tabixIndexedFile.d.ts +28 -20
- package/esm/tabixIndexedFile.js +120 -122
- package/esm/tabixIndexedFile.js.map +1 -1
- package/esm/tbi.js +1 -2
- package/esm/tbi.js.map +1 -1
- package/esm/util.d.ts +0 -20
- package/esm/util.js +0 -39
- package/esm/util.js.map +1 -1
- package/package.json +11 -9
- package/src/tabixIndexedFile.ts +175 -136
- package/src/tbi.ts +1 -2
- package/src/util.ts +0 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmod/tabix",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "Read Tabix-indexed files, supports both .tbi and .csi indexes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"test": "vitest",
|
|
34
|
+
"benchonly": "vitest bench",
|
|
35
|
+
"bench": "./scripts/build-both-branches.sh \"$BRANCH1\" \"$BRANCH2\" && vitest bench",
|
|
34
36
|
"lint": "eslint --report-unused-disable-directives --max-warnings 0",
|
|
35
37
|
"format": "prettier --write .",
|
|
36
38
|
"docs": "documentation readme --shallow src/tabixIndexedFile.ts --section TabixIndexedFile",
|
|
@@ -53,24 +55,24 @@
|
|
|
53
55
|
],
|
|
54
56
|
"dependencies": {
|
|
55
57
|
"@gmod/abortable-promise-cache": "^3.0.1",
|
|
56
|
-
"@gmod/bgzf-filehandle": "^6.0.
|
|
57
|
-
"
|
|
58
|
-
"
|
|
58
|
+
"@gmod/bgzf-filehandle": "^6.0.9",
|
|
59
|
+
"@jbrowse/quick-lru": "^7.3.5",
|
|
60
|
+
"generic-filehandle2": "^2.0.16"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
63
|
"@types/node": "^22.15.3",
|
|
62
|
-
"@vitest/coverage-v8": "^4.0.
|
|
64
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
63
65
|
"documentation": "^14.0.3",
|
|
64
|
-
"eslint": "^9.
|
|
66
|
+
"eslint": "^9.39.2",
|
|
65
67
|
"eslint-plugin-import": "^2.31.0",
|
|
66
68
|
"eslint-plugin-unicorn": "^62.0.0",
|
|
67
69
|
"prettier": "^3.3.3",
|
|
68
70
|
"rimraf": "^6.0.1",
|
|
69
71
|
"standard-changelog": "^7.0.1",
|
|
70
72
|
"typescript": "^5.7.0",
|
|
71
|
-
"typescript-eslint": "^8.
|
|
72
|
-
"vitest": "^4.0.
|
|
73
|
-
"webpack": "^5.
|
|
73
|
+
"typescript-eslint": "^8.50.1",
|
|
74
|
+
"vitest": "^4.0.16",
|
|
75
|
+
"webpack": "^5.104.1",
|
|
74
76
|
"webpack-cli": "^6.0.1"
|
|
75
77
|
},
|
|
76
78
|
"publishConfig": {
|
package/src/tabixIndexedFile.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import AbortablePromiseCache from '@gmod/abortable-promise-cache'
|
|
2
2
|
import { unzip, unzipChunkSlice } from '@gmod/bgzf-filehandle'
|
|
3
|
+
import LRU from '@jbrowse/quick-lru'
|
|
3
4
|
import { LocalFile, RemoteFile } from 'generic-filehandle2'
|
|
4
|
-
import LRU from 'quick-lru'
|
|
5
5
|
|
|
6
6
|
import Chunk from './chunk.ts'
|
|
7
7
|
import CSI from './csi.ts'
|
|
8
|
-
import IndexFile, {
|
|
8
|
+
import IndexFile, { Options } from './indexFile.ts'
|
|
9
9
|
import TBI from './tbi.ts'
|
|
10
|
-
import { checkAbortSignal } from './util.ts'
|
|
11
10
|
|
|
12
11
|
import type { GenericFilehandle } from 'generic-filehandle2'
|
|
13
12
|
|
|
14
|
-
type GetLinesCallback = (
|
|
13
|
+
type GetLinesCallback = (
|
|
14
|
+
line: string,
|
|
15
|
+
fileOffset: number,
|
|
16
|
+
start: number,
|
|
17
|
+
end: number,
|
|
18
|
+
) => void
|
|
15
19
|
|
|
16
20
|
interface GetLinesOpts {
|
|
17
21
|
[key: string]: unknown
|
|
@@ -29,6 +33,7 @@ export default class TabixIndexedFile {
|
|
|
29
33
|
private filehandle: GenericFilehandle
|
|
30
34
|
private index: IndexFile
|
|
31
35
|
private renameRefSeq: (n: string) => string
|
|
36
|
+
private hasCustomRenameRefSeq: boolean
|
|
32
37
|
private chunkCache: AbortablePromiseCache<Chunk, ReadChunk>
|
|
33
38
|
public cache = new LRU<
|
|
34
39
|
string,
|
|
@@ -73,7 +78,7 @@ export default class TabixIndexedFile {
|
|
|
73
78
|
csiPath,
|
|
74
79
|
csiUrl,
|
|
75
80
|
csiFilehandle,
|
|
76
|
-
renameRefSeqs
|
|
81
|
+
renameRefSeqs: renameRefSeqsPre,
|
|
77
82
|
chunkCacheSize = 5 * 2 ** 20,
|
|
78
83
|
}: {
|
|
79
84
|
path?: string
|
|
@@ -88,6 +93,7 @@ export default class TabixIndexedFile {
|
|
|
88
93
|
renameRefSeqs?: (n: string) => string
|
|
89
94
|
chunkCacheSize?: number
|
|
90
95
|
}) {
|
|
96
|
+
const renameRefSeqs = renameRefSeqsPre ?? (arg => arg)
|
|
91
97
|
if (filehandle) {
|
|
92
98
|
this.filehandle = filehandle
|
|
93
99
|
} else if (path) {
|
|
@@ -142,6 +148,7 @@ export default class TabixIndexedFile {
|
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
this.renameRefSeq = renameRefSeqs
|
|
151
|
+
this.hasCustomRenameRefSeq = renameRefSeqsPre !== undefined
|
|
145
152
|
this.chunkCache = new AbortablePromiseCache<Chunk, ReadChunk>({
|
|
146
153
|
cache: new LRU({ maxSize: Math.floor(chunkCacheSize / (1 << 16)) }),
|
|
147
154
|
fill: (args: Chunk, signal?: AbortSignal) =>
|
|
@@ -185,7 +192,7 @@ export default class TabixIndexedFile {
|
|
|
185
192
|
) {
|
|
186
193
|
let signal: AbortSignal | undefined
|
|
187
194
|
let options: Options = {}
|
|
188
|
-
let callback:
|
|
195
|
+
let callback: GetLinesCallback
|
|
189
196
|
|
|
190
197
|
if (typeof opts === 'function') {
|
|
191
198
|
callback = opts
|
|
@@ -196,7 +203,6 @@ export default class TabixIndexedFile {
|
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
const metadata = await this.index.getMetadata(options)
|
|
199
|
-
checkAbortSignal(signal)
|
|
200
206
|
const start = s ?? 0
|
|
201
207
|
const end = e ?? metadata.maxRefLength
|
|
202
208
|
if (!(start <= end)) {
|
|
@@ -209,9 +215,24 @@ export default class TabixIndexedFile {
|
|
|
209
215
|
}
|
|
210
216
|
|
|
211
217
|
const chunks = await this.index.blocksForRange(refName, start, end, options)
|
|
212
|
-
checkAbortSignal(signal)
|
|
213
218
|
const decoder = new TextDecoder('utf8')
|
|
214
219
|
|
|
220
|
+
const isVCF = metadata.format === 'VCF'
|
|
221
|
+
const columnNumbersEffective = {
|
|
222
|
+
ref: metadata.columnNumbers.ref || 0,
|
|
223
|
+
start: metadata.columnNumbers.start || 0,
|
|
224
|
+
end: isVCF ? 8 : metadata.columnNumbers.end || 0,
|
|
225
|
+
}
|
|
226
|
+
const maxColumn = Math.max(
|
|
227
|
+
columnNumbersEffective.ref,
|
|
228
|
+
columnNumbersEffective.start,
|
|
229
|
+
columnNumbersEffective.end,
|
|
230
|
+
)
|
|
231
|
+
const metaCharCode = metadata.metaChar?.charCodeAt(0)
|
|
232
|
+
const coordinateOffset =
|
|
233
|
+
metadata.coordinateType === '1-based-closed' ? -1 : 0
|
|
234
|
+
const isIdentityRename = !this.hasCustomRenameRefSeq
|
|
235
|
+
|
|
215
236
|
// now go through each chunk and parse and filter the lines out of it
|
|
216
237
|
for (const c of chunks) {
|
|
217
238
|
const { buffer, cpositions, dpositions } = await this.chunkCache.get(
|
|
@@ -220,7 +241,6 @@ export default class TabixIndexedFile {
|
|
|
220
241
|
signal,
|
|
221
242
|
)
|
|
222
243
|
|
|
223
|
-
checkAbortSignal(signal)
|
|
224
244
|
let blockStart = 0
|
|
225
245
|
let pos = 0
|
|
226
246
|
|
|
@@ -251,15 +271,24 @@ export default class TabixIndexedFile {
|
|
|
251
271
|
}
|
|
252
272
|
|
|
253
273
|
// filter the line for whether it is within the requested range
|
|
254
|
-
const
|
|
255
|
-
metadata,
|
|
274
|
+
const result = this.checkLine(
|
|
256
275
|
refName,
|
|
257
276
|
start,
|
|
258
277
|
end,
|
|
259
278
|
line,
|
|
279
|
+
columnNumbersEffective.ref,
|
|
280
|
+
columnNumbersEffective.start,
|
|
281
|
+
columnNumbersEffective.end,
|
|
282
|
+
maxColumn,
|
|
283
|
+
metaCharCode,
|
|
284
|
+
coordinateOffset,
|
|
285
|
+
isVCF,
|
|
286
|
+
isIdentityRename,
|
|
260
287
|
)
|
|
261
288
|
|
|
262
|
-
if (
|
|
289
|
+
if (result === null) {
|
|
290
|
+
return
|
|
291
|
+
} else if (result !== undefined) {
|
|
263
292
|
callback(
|
|
264
293
|
line,
|
|
265
294
|
this.calculateFileOffset(
|
|
@@ -269,12 +298,9 @@ export default class TabixIndexedFile {
|
|
|
269
298
|
blockStart,
|
|
270
299
|
c.minv.dataPosition,
|
|
271
300
|
),
|
|
301
|
+
result.start,
|
|
302
|
+
result.end,
|
|
272
303
|
)
|
|
273
|
-
} else if (startCoordinate !== undefined && startCoordinate >= end) {
|
|
274
|
-
// the lines were overlapping the region, but now have stopped, so we
|
|
275
|
-
// must be at the end of the relevant data and we can stop processing
|
|
276
|
-
// data now
|
|
277
|
-
return
|
|
278
304
|
}
|
|
279
305
|
blockStart = n + 1
|
|
280
306
|
}
|
|
@@ -296,15 +322,24 @@ export default class TabixIndexedFile {
|
|
|
296
322
|
}
|
|
297
323
|
|
|
298
324
|
// filter the line for whether it is within the requested range
|
|
299
|
-
const
|
|
300
|
-
metadata,
|
|
325
|
+
const result = this.checkLine(
|
|
301
326
|
refName,
|
|
302
327
|
start,
|
|
303
328
|
end,
|
|
304
329
|
line,
|
|
330
|
+
columnNumbersEffective.ref,
|
|
331
|
+
columnNumbersEffective.start,
|
|
332
|
+
columnNumbersEffective.end,
|
|
333
|
+
maxColumn,
|
|
334
|
+
metaCharCode,
|
|
335
|
+
coordinateOffset,
|
|
336
|
+
isVCF,
|
|
337
|
+
isIdentityRename,
|
|
305
338
|
)
|
|
306
339
|
|
|
307
|
-
if (
|
|
340
|
+
if (result === null) {
|
|
341
|
+
return
|
|
342
|
+
} else if (result !== undefined) {
|
|
308
343
|
callback(
|
|
309
344
|
line,
|
|
310
345
|
this.calculateFileOffset(
|
|
@@ -314,12 +349,9 @@ export default class TabixIndexedFile {
|
|
|
314
349
|
blockStart,
|
|
315
350
|
c.minv.dataPosition,
|
|
316
351
|
),
|
|
352
|
+
result.start,
|
|
353
|
+
result.end,
|
|
317
354
|
)
|
|
318
|
-
} else if (startCoordinate !== undefined && startCoordinate >= end) {
|
|
319
|
-
// the lines were overlapping the region, but now have stopped, so we
|
|
320
|
-
// must be at the end of the relevant data and we can stop processing
|
|
321
|
-
// data now
|
|
322
|
-
return
|
|
323
355
|
}
|
|
324
356
|
blockStart = n + 1
|
|
325
357
|
}
|
|
@@ -339,8 +371,6 @@ export default class TabixIndexedFile {
|
|
|
339
371
|
const { firstDataLine, metaChar, maxBlockSize } =
|
|
340
372
|
await this.getMetadata(opts)
|
|
341
373
|
|
|
342
|
-
checkAbortSignal(opts.signal)
|
|
343
|
-
|
|
344
374
|
const maxFetch = (firstDataLine?.blockPosition || 0) + maxBlockSize
|
|
345
375
|
// TODO: what if we don't have a firstDataLine, and the header actually
|
|
346
376
|
// takes up more than one block? this case is not covered here
|
|
@@ -372,8 +402,6 @@ export default class TabixIndexedFile {
|
|
|
372
402
|
/**
|
|
373
403
|
* get a string containing the "header" region of the file, is the portion up
|
|
374
404
|
* to the first non-meta line
|
|
375
|
-
*
|
|
376
|
-
* @returns {Promise} for a string
|
|
377
405
|
*/
|
|
378
406
|
async getHeader(opts: Options = {}) {
|
|
379
407
|
const decoder = new TextDecoder('utf8')
|
|
@@ -391,141 +419,152 @@ export default class TabixIndexedFile {
|
|
|
391
419
|
}
|
|
392
420
|
|
|
393
421
|
/**
|
|
394
|
-
* @param {object} metadata metadata object from the parsed index, containing
|
|
395
|
-
* columnNumbers, metaChar, and format
|
|
396
|
-
*
|
|
397
422
|
* @param {string} regionRefName
|
|
398
423
|
*
|
|
399
424
|
* @param {number} regionStart region start coordinate (0-based-half-open)
|
|
400
425
|
*
|
|
401
426
|
* @param {number} regionEnd region end coordinate (0-based-half-open)
|
|
402
427
|
*
|
|
403
|
-
* @param {
|
|
428
|
+
* @param {string} line
|
|
429
|
+
*
|
|
430
|
+
* @param {number} refColumn column number for ref
|
|
431
|
+
*
|
|
432
|
+
* @param {number} startColumn column number for start
|
|
433
|
+
*
|
|
434
|
+
* @param {number} endColumn column number for end
|
|
435
|
+
*
|
|
436
|
+
* @param {number} maxColumn pre-calculated max column
|
|
437
|
+
*
|
|
438
|
+
* @param {number} metaCharCode pre-calculated metaChar code
|
|
404
439
|
*
|
|
405
|
-
* @
|
|
406
|
-
*
|
|
440
|
+
* @param {number} coordinateOffset 0 or -1 for coordinate adjustment
|
|
441
|
+
*
|
|
442
|
+
* @param {boolean} isVCF whether this is VCF format
|
|
443
|
+
*
|
|
444
|
+
* @param {boolean} isIdentityRename whether renameRefSeq is the identity function
|
|
445
|
+
*
|
|
446
|
+
* @returns {{ start: number, end: number } | null | undefined} coordinates if overlapping, null if should stop processing, undefined otherwise
|
|
407
447
|
*/
|
|
408
448
|
checkLine(
|
|
409
|
-
metadata: IndexData,
|
|
410
449
|
regionRefName: string,
|
|
411
450
|
regionStart: number,
|
|
412
451
|
regionEnd: number,
|
|
413
452
|
line: string,
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
453
|
+
refColumn: number,
|
|
454
|
+
startColumn: number,
|
|
455
|
+
endColumn: number,
|
|
456
|
+
maxColumn: number,
|
|
457
|
+
metaCharCode: number | undefined,
|
|
458
|
+
coordinateOffset: number,
|
|
459
|
+
isVCF: boolean,
|
|
460
|
+
isIdentityRename: boolean,
|
|
461
|
+
): { start: number; end: number } | null | undefined {
|
|
462
|
+
if (metaCharCode !== undefined && line.charCodeAt(0) === metaCharCode) {
|
|
463
|
+
return
|
|
419
464
|
}
|
|
420
465
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
|
|
466
|
+
// Length-based fast path: split for short lines, indexOf for long lines
|
|
467
|
+
// Split is faster for short lines, but slow for long lines with big attribute columns
|
|
468
|
+
if (line.length < 500) {
|
|
469
|
+
const fields = line.split('\t')
|
|
470
|
+
const ref = fields[refColumn - 1]!
|
|
471
|
+
const refMatch = isIdentityRename
|
|
472
|
+
? ref === regionRefName
|
|
473
|
+
: this.renameRefSeq(ref) === regionRefName
|
|
474
|
+
if (!refMatch) {
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const startCoordinate = +fields[startColumn - 1]! + coordinateOffset
|
|
479
|
+
if (startCoordinate >= regionEnd) {
|
|
480
|
+
return null
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let endCoordinate: number
|
|
484
|
+
if (endColumn === 0 || endColumn === startColumn) {
|
|
485
|
+
endCoordinate = startCoordinate + 1
|
|
486
|
+
} else if (isVCF) {
|
|
487
|
+
endCoordinate = this._getVcfEnd(
|
|
488
|
+
startCoordinate,
|
|
489
|
+
fields[3]!,
|
|
490
|
+
fields[endColumn - 1]!,
|
|
491
|
+
)
|
|
492
|
+
} else {
|
|
493
|
+
endCoordinate = +fields[endColumn - 1]!
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (endCoordinate <= regionStart) {
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
return { start: startCoordinate, end: endCoordinate }
|
|
425
500
|
}
|
|
426
|
-
|
|
427
|
-
|
|
501
|
+
|
|
502
|
+
// Long lines - use indexOf chain (avoids parsing long attribute column)
|
|
503
|
+
let prev = -1
|
|
504
|
+
const tabs = [-1]
|
|
505
|
+
for (let i = 0; i < maxColumn; i++) {
|
|
506
|
+
const pos = line.indexOf('\t', prev + 1)
|
|
507
|
+
if (pos === -1) {
|
|
508
|
+
tabs.push(line.length)
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
tabs.push(pos)
|
|
512
|
+
prev = pos
|
|
428
513
|
}
|
|
429
|
-
|
|
430
|
-
|
|
514
|
+
|
|
515
|
+
const ref = line.slice(tabs[refColumn - 1]! + 1, tabs[refColumn])
|
|
516
|
+
const refMatch = isIdentityRename
|
|
517
|
+
? ref === regionRefName
|
|
518
|
+
: this.renameRefSeq(ref) === regionRefName
|
|
519
|
+
if (!refMatch) {
|
|
520
|
+
return
|
|
431
521
|
}
|
|
432
|
-
|
|
433
|
-
|
|
522
|
+
|
|
523
|
+
const startCoordinate =
|
|
524
|
+
+line.slice(tabs[startColumn - 1]! + 1, tabs[startColumn]) +
|
|
525
|
+
coordinateOffset
|
|
526
|
+
if (startCoordinate >= regionEnd) {
|
|
527
|
+
return null
|
|
434
528
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (line[i] === '\t' || i === l) {
|
|
448
|
-
if (currentColumnNumber === ref) {
|
|
449
|
-
if (
|
|
450
|
-
this.renameRefSeq(line.slice(currentColumnStart, i)) !==
|
|
451
|
-
regionRefName
|
|
452
|
-
) {
|
|
453
|
-
return {
|
|
454
|
-
overlaps: false,
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
} else if (currentColumnNumber === start) {
|
|
458
|
-
startCoordinate = Number.parseInt(
|
|
459
|
-
line.slice(currentColumnStart, i),
|
|
460
|
-
10,
|
|
461
|
-
)
|
|
462
|
-
// we convert to 0-based-half-open
|
|
463
|
-
if (coordinateType === '1-based-closed') {
|
|
464
|
-
startCoordinate -= 1
|
|
465
|
-
}
|
|
466
|
-
if (startCoordinate >= regionEnd) {
|
|
467
|
-
return {
|
|
468
|
-
startCoordinate,
|
|
469
|
-
overlaps: false,
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (
|
|
473
|
-
(end === 0 || end === start) && // if we have no end, we assume the feature is 1 bp long
|
|
474
|
-
startCoordinate + 1 <= regionStart
|
|
475
|
-
) {
|
|
476
|
-
return {
|
|
477
|
-
startCoordinate,
|
|
478
|
-
overlaps: false,
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
} else if (format === 'VCF' && currentColumnNumber === 4) {
|
|
482
|
-
refSeq = line.slice(currentColumnStart, i)
|
|
483
|
-
} else if (currentColumnNumber === end) {
|
|
484
|
-
// this will never match if there is no end column
|
|
485
|
-
const endCoordinate =
|
|
486
|
-
format === 'VCF'
|
|
487
|
-
? this._getVcfEnd(
|
|
488
|
-
startCoordinate,
|
|
489
|
-
refSeq,
|
|
490
|
-
line.slice(currentColumnStart, i),
|
|
491
|
-
)
|
|
492
|
-
: Number.parseInt(line.slice(currentColumnStart, i), 10)
|
|
493
|
-
if (endCoordinate <= regionStart) {
|
|
494
|
-
return {
|
|
495
|
-
overlaps: false,
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (currentColumnNumber === maxColumn) {
|
|
500
|
-
break
|
|
501
|
-
}
|
|
502
|
-
currentColumnStart = i + 1
|
|
503
|
-
currentColumnNumber += 1
|
|
504
|
-
}
|
|
529
|
+
|
|
530
|
+
let endCoordinate: number
|
|
531
|
+
if (endColumn === 0 || endColumn === startColumn) {
|
|
532
|
+
endCoordinate = startCoordinate + 1
|
|
533
|
+
} else if (isVCF) {
|
|
534
|
+
endCoordinate = this._getVcfEnd(
|
|
535
|
+
startCoordinate,
|
|
536
|
+
line.slice(tabs[3]! + 1, tabs[4]),
|
|
537
|
+
line.slice(tabs[endColumn - 1]! + 1, tabs[endColumn]),
|
|
538
|
+
)
|
|
539
|
+
} else {
|
|
540
|
+
endCoordinate = +line.slice(tabs[endColumn - 1]! + 1, tabs[endColumn])
|
|
505
541
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
542
|
+
|
|
543
|
+
if (endCoordinate <= regionStart) {
|
|
544
|
+
return
|
|
509
545
|
}
|
|
546
|
+
|
|
547
|
+
return { start: startCoordinate, end: endCoordinate }
|
|
510
548
|
}
|
|
511
549
|
|
|
512
550
|
_getVcfEnd(startCoordinate: number, refSeq: string, info: any) {
|
|
513
551
|
let endCoordinate = startCoordinate + refSeq.length
|
|
514
|
-
// ignore TRA features as they specify CHR2 and END as being on a different
|
|
515
|
-
// chromosome
|
|
516
|
-
//
|
|
517
|
-
// if CHR2 is on the same chromosome, still ignore it because there should
|
|
518
|
-
// be another pairwise feature at the end of this one
|
|
519
552
|
const isTRA = info.includes('SVTYPE=TRA')
|
|
520
|
-
if (
|
|
521
|
-
const endRegex = /(?:^|;)END=([^;]+)/
|
|
522
|
-
const match = endRegex.exec(info)
|
|
523
|
-
if (match) {
|
|
524
|
-
endCoordinate = Number.parseInt(match[1]!, 10)
|
|
525
|
-
}
|
|
526
|
-
} else if (isTRA) {
|
|
553
|
+
if (isTRA) {
|
|
527
554
|
return startCoordinate + 1
|
|
528
555
|
}
|
|
556
|
+
|
|
557
|
+
if (info[0] !== '.') {
|
|
558
|
+
const endIdx = info.indexOf('END=')
|
|
559
|
+
if (endIdx !== -1 && (endIdx === 0 || info[endIdx - 1] === ';')) {
|
|
560
|
+
const start = endIdx + 4
|
|
561
|
+
let end = info.indexOf(';', start)
|
|
562
|
+
if (end === -1) {
|
|
563
|
+
end = info.length
|
|
564
|
+
}
|
|
565
|
+
endCoordinate = Number.parseInt(info.slice(start, end), 10)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
529
568
|
return endCoordinate
|
|
530
569
|
}
|
|
531
570
|
|
package/src/tbi.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { unzip } from '@gmod/bgzf-filehandle'
|
|
|
3
3
|
import Chunk from './chunk.ts'
|
|
4
4
|
import IndexFile, { Options } from './indexFile.ts'
|
|
5
5
|
import { longFromBytesToUnsigned } from './long.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { optimizeChunks } from './util.ts'
|
|
7
7
|
import VirtualOffset, { fromBytes } from './virtualOffset.ts'
|
|
8
8
|
|
|
9
9
|
const TBI_MAGIC = 21578324 // TBI\1
|
|
@@ -44,7 +44,6 @@ export default class TabixIndex extends IndexFile {
|
|
|
44
44
|
async _parse(opts: Options = {}) {
|
|
45
45
|
const buf = await this.filehandle.readFile(opts)
|
|
46
46
|
const bytes = await unzip(buf)
|
|
47
|
-
checkAbortSignal(opts.signal)
|
|
48
47
|
const dataView = new DataView(bytes.buffer)
|
|
49
48
|
|
|
50
49
|
const magic = dataView.getUint32(0, true)
|
package/src/util.ts
CHANGED
|
@@ -1,48 +1,6 @@
|
|
|
1
1
|
import Chunk from './chunk.ts'
|
|
2
2
|
import VirtualOffset from './virtualOffset.ts'
|
|
3
3
|
|
|
4
|
-
class AbortError extends Error {
|
|
5
|
-
public code: string | undefined
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Properly check if the given AbortSignal is aborted. Per the standard, if the
|
|
9
|
-
* signal reads as aborted, this function throws either a DOMException
|
|
10
|
-
* AbortError, or a regular error with a `code` attribute set to `ERR_ABORTED`.
|
|
11
|
-
*
|
|
12
|
-
* For convenience, passing `undefined` is a no-op
|
|
13
|
-
*
|
|
14
|
-
* @param {AbortSignal} [signal] an AbortSignal, or anything with an `aborted`
|
|
15
|
-
* attribute
|
|
16
|
-
*
|
|
17
|
-
* @returns nothing
|
|
18
|
-
*/
|
|
19
|
-
export function checkAbortSignal(signal?: AbortSignal) {
|
|
20
|
-
if (!signal) {
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (signal.aborted) {
|
|
25
|
-
if (typeof DOMException === 'undefined') {
|
|
26
|
-
const e = new AbortError('aborted')
|
|
27
|
-
e.code = 'ERR_ABORTED'
|
|
28
|
-
throw e
|
|
29
|
-
} else {
|
|
30
|
-
throw new DOMException('aborted', 'AbortError')
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Skips to the next tick, then runs `checkAbortSignal`.
|
|
37
|
-
* Await this to inside an otherwise synchronous loop to
|
|
38
|
-
* provide a place to break when an abort signal is received.
|
|
39
|
-
* @param {AbortSignal} signal
|
|
40
|
-
*/
|
|
41
|
-
export async function abortBreakPoint(signal?: AbortSignal) {
|
|
42
|
-
await Promise.resolve()
|
|
43
|
-
checkAbortSignal(signal)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
4
|
export function canMergeBlocks(chunk1: Chunk, chunk2: Chunk) {
|
|
47
5
|
return (
|
|
48
6
|
chunk2.minv.blockPosition - chunk1.maxv.blockPosition < 65000 &&
|