@gmod/tabix 3.4.1 → 3.4.3
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/dist/chunk.d.ts +2 -1
- package/dist/chunk.js +8 -2
- package/dist/chunk.js.map +1 -1
- package/dist/csi.d.ts +0 -6
- package/dist/csi.js +5 -14
- package/dist/csi.js.map +1 -1
- package/dist/long.d.ts +0 -2
- package/dist/long.js +2 -4
- package/dist/long.js.map +1 -1
- package/dist/tabix-bundle.js +1 -1
- package/dist/tabixIndexedFile.js +49 -35
- package/dist/tabixIndexedFile.js.map +1 -1
- package/dist/tbi.js +22 -23
- package/dist/tbi.js.map +1 -1
- package/dist/util.d.ts +1 -0
- package/dist/util.js +38 -2
- package/dist/util.js.map +1 -1
- package/esm/chunk.d.ts +2 -1
- package/esm/chunk.js +8 -2
- package/esm/chunk.js.map +1 -1
- package/esm/csi.d.ts +0 -6
- package/esm/csi.js +6 -15
- package/esm/csi.js.map +1 -1
- package/esm/long.d.ts +0 -2
- package/esm/long.js +1 -2
- package/esm/long.js.map +1 -1
- package/esm/tabixIndexedFile.js +49 -35
- package/esm/tabixIndexedFile.js.map +1 -1
- package/esm/tbi.js +23 -24
- package/esm/tbi.js.map +1 -1
- package/esm/util.d.ts +1 -0
- package/esm/util.js +37 -2
- package/esm/util.js.map +1 -1
- package/package.json +2 -1
- package/src/chunk.ts +13 -2
- package/src/csi.ts +10 -15
- package/src/long.ts +1 -2
- package/src/tabixIndexedFile.ts +70 -30
- package/src/tbi.ts +38 -25
- package/src/util.ts +45 -1
package/src/tabixIndexedFile.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { GenericFilehandle } from 'generic-filehandle2'
|
|
|
14
14
|
|
|
15
15
|
const TAB = 9
|
|
16
16
|
const NEWLINE = 10
|
|
17
|
+
const CARRIAGE_RETURN = 13
|
|
17
18
|
const SEMICOLON = 59
|
|
18
19
|
|
|
19
20
|
type GetLinesCallback = (
|
|
@@ -59,6 +60,56 @@ function resolveFilehandle(
|
|
|
59
60
|
throw new TypeError('must provide either filehandle, path, or url')
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// An explicitly supplied index always wins over the `<path>.tbi` /
|
|
64
|
+
// `<url>.tbi` names derived from the data file, whichever kind it is.
|
|
65
|
+
function resolveIndex({
|
|
66
|
+
tbiFilehandle,
|
|
67
|
+
csiFilehandle,
|
|
68
|
+
tbiPath,
|
|
69
|
+
csiPath,
|
|
70
|
+
tbiUrl,
|
|
71
|
+
csiUrl,
|
|
72
|
+
path,
|
|
73
|
+
url,
|
|
74
|
+
}: {
|
|
75
|
+
tbiFilehandle?: GenericFilehandle
|
|
76
|
+
csiFilehandle?: GenericFilehandle
|
|
77
|
+
tbiPath?: string
|
|
78
|
+
csiPath?: string
|
|
79
|
+
tbiUrl?: string
|
|
80
|
+
csiUrl?: string
|
|
81
|
+
path?: string
|
|
82
|
+
url?: string
|
|
83
|
+
}) {
|
|
84
|
+
if (tbiFilehandle) {
|
|
85
|
+
return new TBI({ filehandle: tbiFilehandle })
|
|
86
|
+
}
|
|
87
|
+
if (csiFilehandle) {
|
|
88
|
+
return new CSI({ filehandle: csiFilehandle })
|
|
89
|
+
}
|
|
90
|
+
if (tbiPath) {
|
|
91
|
+
return new TBI({ filehandle: new LocalFile(tbiPath) })
|
|
92
|
+
}
|
|
93
|
+
if (csiPath) {
|
|
94
|
+
return new CSI({ filehandle: new LocalFile(csiPath) })
|
|
95
|
+
}
|
|
96
|
+
if (csiUrl) {
|
|
97
|
+
return new CSI({ filehandle: new RemoteFile(csiUrl) })
|
|
98
|
+
}
|
|
99
|
+
if (tbiUrl) {
|
|
100
|
+
return new TBI({ filehandle: new RemoteFile(tbiUrl) })
|
|
101
|
+
}
|
|
102
|
+
if (path) {
|
|
103
|
+
return new TBI({ filehandle: new LocalFile(`${path}.tbi`) })
|
|
104
|
+
}
|
|
105
|
+
if (url) {
|
|
106
|
+
return new TBI({ filehandle: new RemoteFile(`${url}.tbi`) })
|
|
107
|
+
}
|
|
108
|
+
throw new TypeError(
|
|
109
|
+
'must provide one of tbiFilehandle, tbiPath, csiFilehandle, csiPath, tbiUrl, csiUrl',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
62
113
|
function calculateFileOffset(
|
|
63
114
|
cpositions: number[],
|
|
64
115
|
dpositions: number[],
|
|
@@ -172,28 +223,16 @@ export default class TabixIndexedFile {
|
|
|
172
223
|
chunkCacheSize?: number
|
|
173
224
|
}) {
|
|
174
225
|
this.filehandle = resolveFilehandle(filehandle, path, url)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
this.index = new TBI({ filehandle: new LocalFile(`${path}.tbi`) })
|
|
186
|
-
} else if (csiUrl) {
|
|
187
|
-
this.index = new CSI({ filehandle: new RemoteFile(csiUrl) })
|
|
188
|
-
} else if (tbiUrl) {
|
|
189
|
-
this.index = new TBI({ filehandle: new RemoteFile(tbiUrl) })
|
|
190
|
-
} else if (url) {
|
|
191
|
-
this.index = new TBI({ filehandle: new RemoteFile(`${url}.tbi`) })
|
|
192
|
-
} else {
|
|
193
|
-
throw new TypeError(
|
|
194
|
-
'must provide one of tbiFilehandle, tbiPath, csiFilehandle, csiPath, tbiUrl, csiUrl',
|
|
195
|
-
)
|
|
196
|
-
}
|
|
226
|
+
this.index = resolveIndex({
|
|
227
|
+
tbiFilehandle,
|
|
228
|
+
csiFilehandle,
|
|
229
|
+
tbiPath,
|
|
230
|
+
csiPath,
|
|
231
|
+
tbiUrl,
|
|
232
|
+
csiUrl,
|
|
233
|
+
path,
|
|
234
|
+
url,
|
|
235
|
+
})
|
|
197
236
|
|
|
198
237
|
this.chunkCache = new AbortablePromiseCache<Chunk, ReadChunk>({
|
|
199
238
|
cache: new LRU({ maxSize: Math.floor(chunkCacheSize / (1 << 16)) }),
|
|
@@ -318,15 +357,14 @@ export default class TabixIndexedFile {
|
|
|
318
357
|
continue
|
|
319
358
|
}
|
|
320
359
|
|
|
321
|
-
// find tab positions
|
|
360
|
+
// find tab positions. Columns past the end of the line all get `n`
|
|
361
|
+
// rather than breaking out, which would leave stale offsets from the
|
|
362
|
+
// previous line in the tail of the array.
|
|
322
363
|
tabs[0] = blockStart - 1
|
|
323
364
|
for (let i = 0; i < maxColumn; i++) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
break
|
|
328
|
-
}
|
|
329
|
-
tabs[i + 1] = tabPos
|
|
365
|
+
const prev = tabs[i]!
|
|
366
|
+
const tabPos = prev < n ? buffer.indexOf(TAB, prev + 1) : -1
|
|
367
|
+
tabs[i + 1] = tabPos === -1 || tabPos >= n ? n : tabPos
|
|
330
368
|
}
|
|
331
369
|
|
|
332
370
|
// compare ref name bytes directly
|
|
@@ -380,7 +418,9 @@ export default class TabixIndexedFile {
|
|
|
380
418
|
}
|
|
381
419
|
|
|
382
420
|
if (endCoordinate > start) {
|
|
383
|
-
|
|
421
|
+
// trim a CRLF terminator, matching htslib's line reader
|
|
422
|
+
const lineEnd = buffer[n - 1] === CARRIAGE_RETURN ? n - 1 : n
|
|
423
|
+
const line = decoder.decode(buffer.subarray(blockStart, lineEnd))
|
|
384
424
|
callback(
|
|
385
425
|
line,
|
|
386
426
|
calculateFileOffset(
|
package/src/tbi.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { unzip } from '@gmod/bgzf-filehandle'
|
|
|
3
3
|
import Chunk from './chunk.ts'
|
|
4
4
|
import IndexFile from './indexFile.ts'
|
|
5
5
|
import {
|
|
6
|
+
clampChunkEnds,
|
|
6
7
|
findFirstData,
|
|
7
8
|
memoizeByRefId,
|
|
8
9
|
optimizeChunks,
|
|
@@ -16,21 +17,31 @@ import type VirtualOffset from './virtualOffset.ts'
|
|
|
16
17
|
|
|
17
18
|
const TBI_MAGIC = 21_578_324 // TBI\1
|
|
18
19
|
|
|
20
|
+
// TBI is always the fixed depth-5, 14-bit-leaf binning scheme
|
|
21
|
+
const TBI_DEPTH = 5
|
|
22
|
+
const TBI_MAX_REF_LENGTH = 2 ** (14 + TBI_DEPTH * 3)
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* calculate the list of bins that may overlap with region [beg,end)
|
|
21
26
|
* (zero-based half-open)
|
|
22
27
|
*/
|
|
23
28
|
function reg2bins(beg: number, end: number) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Clamp into the binning scheme's range. Past 2**31 the shifts below
|
|
30
|
+
// truncate to int32 and go negative, which would silently yield no bins.
|
|
31
|
+
const b = Math.min(Math.max(beg, 0), TBI_MAX_REF_LENGTH)
|
|
32
|
+
const e = Math.min(end, TBI_MAX_REF_LENGTH) - 1
|
|
33
|
+
const bins: (readonly [number, number])[] = []
|
|
34
|
+
if (b <= e) {
|
|
35
|
+
bins.push(
|
|
36
|
+
[0, 0],
|
|
37
|
+
[1 + (b >> 26), 1 + (e >> 26)],
|
|
38
|
+
[9 + (b >> 23), 9 + (e >> 23)],
|
|
39
|
+
[73 + (b >> 20), 73 + (e >> 20)],
|
|
40
|
+
[585 + (b >> 17), 585 + (e >> 17)],
|
|
41
|
+
[4681 + (b >> 14), 4681 + (e >> 14)],
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return bins
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export default class TabixIndex extends IndexFile {
|
|
@@ -41,16 +52,18 @@ export default class TabixIndex extends IndexFile {
|
|
|
41
52
|
onProgress: opts.onProgress,
|
|
42
53
|
})
|
|
43
54
|
const bytes = (await unzip(buf)) as Uint8Array
|
|
44
|
-
const dataView = new DataView(
|
|
55
|
+
const dataView = new DataView(
|
|
56
|
+
bytes.buffer,
|
|
57
|
+
bytes.byteOffset,
|
|
58
|
+
bytes.byteLength,
|
|
59
|
+
)
|
|
45
60
|
|
|
46
61
|
if (dataView.getUint32(0, true) !== TBI_MAGIC) {
|
|
47
62
|
throw new Error('Not a TBI file')
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
const refCount = dataView.getUint32(4, true)
|
|
51
|
-
const
|
|
52
|
-
const maxBinNumber = ((1 << ((depth + 1) * 3)) - 1) / 7
|
|
53
|
-
const maxRefLength = 2 ** (14 + depth * 3)
|
|
66
|
+
const maxBinNumber = ((1 << ((TBI_DEPTH + 1) * 3)) - 1) / 7
|
|
54
67
|
|
|
55
68
|
// TBI header layout matches CSI aux data; parseAuxData handles both
|
|
56
69
|
const {
|
|
@@ -147,6 +160,10 @@ export default class TabixIndex extends IndexFile {
|
|
|
147
160
|
linearIndex[k] = fromBytes(bytes, pos)
|
|
148
161
|
pos += 8
|
|
149
162
|
}
|
|
163
|
+
clampChunkEnds(
|
|
164
|
+
Object.values(binIndex).flat(),
|
|
165
|
+
linearIndex.map(v => v.blockPosition),
|
|
166
|
+
)
|
|
150
167
|
return { binIndex, linearIndex, stats }
|
|
151
168
|
}
|
|
152
169
|
|
|
@@ -154,7 +171,7 @@ export default class TabixIndex extends IndexFile {
|
|
|
154
171
|
indices: memoizeByRefId(getIndices),
|
|
155
172
|
metaChar,
|
|
156
173
|
maxBinNumber,
|
|
157
|
-
maxRefLength,
|
|
174
|
+
maxRefLength: TBI_MAX_REF_LENGTH,
|
|
158
175
|
skipLines,
|
|
159
176
|
firstDataLine,
|
|
160
177
|
columnNumbers,
|
|
@@ -172,10 +189,6 @@ export default class TabixIndex extends IndexFile {
|
|
|
172
189
|
max: number,
|
|
173
190
|
opts: Options = {},
|
|
174
191
|
) {
|
|
175
|
-
if (min < 0) {
|
|
176
|
-
min = 0
|
|
177
|
-
}
|
|
178
|
-
|
|
179
192
|
const indexData = await this.parse(opts)
|
|
180
193
|
const refId = indexData.refNameToId[refName]
|
|
181
194
|
if (refId === undefined) {
|
|
@@ -186,13 +199,12 @@ export default class TabixIndex extends IndexFile {
|
|
|
186
199
|
return []
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
|
|
190
|
-
// min >= 2**31 overflows int32 and produces a negative >> 14 result —
|
|
191
|
-
// query is well beyond the indexed range
|
|
192
|
-
if (linearIndex.length > 0 && min >= 2 ** 31) {
|
|
202
|
+
if (min > TBI_MAX_REF_LENGTH) {
|
|
193
203
|
console.warn('querying outside of possible tabix range')
|
|
194
204
|
}
|
|
195
|
-
|
|
205
|
+
// clamp before the >> 14 below, which would truncate to int32 and go
|
|
206
|
+
// negative past 2**31
|
|
207
|
+
const beg = Math.min(Math.max(min, 0), TBI_MAX_REF_LENGTH)
|
|
196
208
|
const overlappingBins = reg2bins(min, max) // List of bin #s that overlap min, max
|
|
197
209
|
const chunks: Chunk[] = []
|
|
198
210
|
|
|
@@ -211,7 +223,8 @@ export default class TabixIndex extends IndexFile {
|
|
|
211
223
|
// The linear index is monotonically non-decreasing, so the minimum virtual
|
|
212
224
|
// offset for chunks that could overlap [min, ...) is at index minLin.
|
|
213
225
|
// SYNC: ~/src/gmod/bam-js/src/bai.ts getLowestChunk
|
|
214
|
-
const
|
|
226
|
+
const linearIndex = ba.linearIndex
|
|
227
|
+
const lowest = linearIndex?.[Math.min(beg >> 14, linearIndex.length - 1)]
|
|
215
228
|
|
|
216
229
|
return optimizeChunks(chunks, lowest)
|
|
217
230
|
}
|
package/src/util.ts
CHANGED
|
@@ -65,6 +65,7 @@ export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
|
|
|
65
65
|
lastChunk.minv,
|
|
66
66
|
chunk.maxv,
|
|
67
67
|
lastChunk.bin,
|
|
68
|
+
chunk.endPosition,
|
|
68
69
|
)
|
|
69
70
|
lastMaxBlock = chunkMaxBlock
|
|
70
71
|
}
|
|
@@ -78,6 +79,45 @@ export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
|
|
|
78
79
|
return mergedChunks
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// Tighten each chunk's endPosition (default: a full max-size BGZF block past
|
|
83
|
+
// maxv) down to the next known block boundary. Every chunk min/maxv blockPosition
|
|
84
|
+
// and every extraBoundary (e.g. linear-index entries) is a real BGZF block start;
|
|
85
|
+
// since blocks don't overlap, the first boundary strictly greater than a chunk's
|
|
86
|
+
// maxv.blockPosition is an upper bound on where that final block ends — always at
|
|
87
|
+
// least the true block end, so the clamped fetch still contains the whole block.
|
|
88
|
+
// Shrinks both the byte estimate and the actual fetch with no extra I/O.
|
|
89
|
+
export function clampChunkEnds(
|
|
90
|
+
chunks: Chunk[],
|
|
91
|
+
extraBoundaries: number[] = [],
|
|
92
|
+
) {
|
|
93
|
+
// Array#sort with a comparator beats a Float64Array numeric sort here:
|
|
94
|
+
// extraBoundaries (the linear index) arrives already sorted and TimSort
|
|
95
|
+
// exploits the existing run, which introsort cannot.
|
|
96
|
+
const boundaries = [...extraBoundaries]
|
|
97
|
+
for (const c of chunks) {
|
|
98
|
+
boundaries.push(c.minv.blockPosition, c.maxv.blockPosition)
|
|
99
|
+
}
|
|
100
|
+
boundaries.sort((a, b) => a - b)
|
|
101
|
+
|
|
102
|
+
for (const c of chunks) {
|
|
103
|
+
const max = c.maxv.blockPosition
|
|
104
|
+
// first boundary strictly greater than max
|
|
105
|
+
let lo = 0
|
|
106
|
+
let hi = boundaries.length
|
|
107
|
+
while (lo < hi) {
|
|
108
|
+
const mid = (lo + hi) >>> 1
|
|
109
|
+
if (boundaries[mid]! > max) {
|
|
110
|
+
hi = mid
|
|
111
|
+
} else {
|
|
112
|
+
lo = mid + 1
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (lo < boundaries.length) {
|
|
116
|
+
c.endPosition = Math.min(c.endPosition, boundaries[lo]!)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
export function findFirstData(
|
|
82
122
|
currentFdl: VirtualOffset | undefined,
|
|
83
123
|
virtualOffset: VirtualOffset,
|
|
@@ -116,7 +156,11 @@ const tabixFormats: Record<number, string> = {
|
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
export function parseAuxData(bytes: Uint8Array, offset: number) {
|
|
119
|
-
const dataView = new DataView(
|
|
159
|
+
const dataView = new DataView(
|
|
160
|
+
bytes.buffer,
|
|
161
|
+
bytes.byteOffset,
|
|
162
|
+
bytes.byteLength,
|
|
163
|
+
)
|
|
120
164
|
const formatFlags = dataView.getInt32(offset, true)
|
|
121
165
|
const coordinateType =
|
|
122
166
|
formatFlags & 0x1_00_00 ? 'zero-based-half-open' : '1-based-closed'
|