@gmod/tabix 3.0.5 → 3.1.1

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/chunk.ts CHANGED
@@ -11,7 +11,7 @@ export default class Chunk {
11
11
  minv: VirtualOffset,
12
12
  maxv: VirtualOffset,
13
13
  bin: number,
14
- fetchedSize = undefined,
14
+ fetchedSize?: number,
15
15
  ) {
16
16
  this.minv = minv
17
17
  this.maxv = maxv
package/src/csi.ts CHANGED
@@ -68,7 +68,7 @@ export default class CSI extends IndexFile {
68
68
  end: dataView.getInt32(offset + 12, true),
69
69
  }
70
70
  const metaValue = dataView.getInt32(offset + 16, true)
71
- const metaChar = metaValue ? String.fromCharCode(metaValue) : null
71
+ const metaChar = metaValue ? String.fromCharCode(metaValue) : undefined
72
72
  const skipLines = dataView.getInt32(offset + 20, true)
73
73
  const nameSectionLength = dataView.getInt32(offset + 24, true)
74
74
 
@@ -87,33 +87,6 @@ export default class CSI extends IndexFile {
87
87
  }
88
88
  }
89
89
 
90
- _parseNameBytes(namesBytes: Uint8Array) {
91
- let currRefId = 0
92
- let currNameStart = 0
93
- const refIdToName = []
94
- const refNameToId: Record<string, number> = {}
95
- const decoder = new TextDecoder('utf8')
96
- for (let i = 0; i < namesBytes.length; i += 1) {
97
- if (!namesBytes[i]) {
98
- if (currNameStart < i) {
99
- const refName = this.renameRefSeq(
100
- decoder.decode(namesBytes.subarray(currNameStart, i)),
101
- )
102
- refIdToName[currRefId] = refName
103
- refNameToId[refName] = currRefId
104
- }
105
- currNameStart = i + 1
106
- currRefId += 1
107
- }
108
- }
109
- return {
110
- refNameToId,
111
- refIdToName,
112
- }
113
- }
114
-
115
- // fetch and parse the index
116
-
117
90
  async _parse(opts: Options = {}) {
118
91
  const bytes = await unzip(await this.filehandle.readFile(opts))
119
92
  const dataView = new DataView(bytes.buffer)
@@ -139,7 +112,7 @@ export default class CSI extends IndexFile {
139
112
  : {
140
113
  refIdToName: [],
141
114
  refNameToId: {},
142
- metaChar: null,
115
+ metaChar: undefined,
143
116
  columnNumbers: { ref: 0, start: 1, end: 2 },
144
117
  coordinateType: 'zero-based-half-open',
145
118
  format: 'generic',
package/src/indexFile.ts CHANGED
@@ -10,7 +10,7 @@ export interface Options {
10
10
  export interface IndexData {
11
11
  refNameToId: Record<string, number>
12
12
  refIdToName: string[]
13
- metaChar: string | null
13
+ metaChar: string | undefined
14
14
  columnNumbers: { ref: number; start: number; end: number }
15
15
  coordinateType: string
16
16
  format: string
@@ -64,9 +64,9 @@ export default abstract class IndexFile {
64
64
 
65
65
  async parse(opts: Options = {}) {
66
66
  if (!this.parseP) {
67
- this.parseP = this._parse(opts).catch((e: unknown) => {
67
+ this.parseP = this._parse(opts).catch((error: unknown) => {
68
68
  this.parseP = undefined
69
- throw e
69
+ throw error
70
70
  })
71
71
  }
72
72
  return this.parseP
@@ -76,4 +76,29 @@ export default abstract class IndexFile {
76
76
  const idx = await this.parse(opts)
77
77
  return !!idx.indices[seqId]?.binIndex
78
78
  }
79
+
80
+ _parseNameBytes(namesBytes: Uint8Array) {
81
+ let currRefId = 0
82
+ let currNameStart = 0
83
+ const refIdToName: string[] = []
84
+ const refNameToId: Record<string, number> = {}
85
+ const decoder = new TextDecoder('utf8')
86
+ for (let i = 0; i < namesBytes.length; i += 1) {
87
+ if (!namesBytes[i]) {
88
+ if (currNameStart < i) {
89
+ const refName = this.renameRefSeq(
90
+ decoder.decode(namesBytes.subarray(currNameStart, i)),
91
+ )
92
+ refIdToName[currRefId] = refName
93
+ refNameToId[refName] = currRefId
94
+ }
95
+ currNameStart = i + 1
96
+ currRefId += 1
97
+ }
98
+ }
99
+ return {
100
+ refNameToId,
101
+ refIdToName,
102
+ }
103
+ }
79
104
  }
@@ -11,11 +11,6 @@ import { checkAbortSignal } from './util.ts'
11
11
 
12
12
  import type { GenericFilehandle } from 'generic-filehandle2'
13
13
 
14
- function isASCII(str: string) {
15
- // eslint-disable-next-line no-control-regex
16
- return /^[\u0000-\u007F]*$/.test(str)
17
- }
18
-
19
14
  type GetLinesCallback = (line: string, fileOffset: number) => void
20
15
 
21
16
  interface GetLinesOpts {
@@ -35,6 +30,9 @@ export default class TabixIndexedFile {
35
30
  private index: IndexFile
36
31
  private renameRefSeq: (n: string) => string
37
32
  private chunkCache: AbortablePromiseCache<Chunk, ReadChunk>
33
+ public cache = new LRU<string, { buffer: Uint8Array; nextIn: number }>({
34
+ maxSize: 1000,
35
+ })
38
36
 
39
37
  /**
40
38
  * @param {object} args
@@ -161,6 +159,21 @@ export default class TabixIndexedFile {
161
159
  * @returns promise that is resolved when the whole read is finished,
162
160
  * rejected on error
163
161
  */
162
+ private calculateFileOffset(
163
+ cpositions: number[],
164
+ dpositions: number[],
165
+ pos: number,
166
+ blockStart: number,
167
+ minvDataPosition: number,
168
+ ) {
169
+ return (
170
+ cpositions[pos]! * (1 << 8) +
171
+ (blockStart - dpositions[pos]!) +
172
+ minvDataPosition +
173
+ 1
174
+ )
175
+ }
176
+
164
177
  async getLines(
165
178
  refName: string,
166
179
  s: number | undefined,
@@ -209,69 +222,104 @@ export default class TabixIndexedFile {
209
222
  let pos = 0
210
223
 
211
224
  // fast path, Buffer is just ASCII chars and not gigantor, can be
212
- // converted to string and processed directly. if it is not ASCII or
213
- // gigantic (chrome max str len is 512Mb), we have to decode line by line
225
+ // converted to string and processed directly.
226
+ //
227
+ // if it is not ASCII or, we have to decode line by line, as it is
228
+ // otherwise hard to get the right 'fileOffset' based feature IDs
229
+ //
230
+ // we use a basic check for isASCII: string length equals buffer length
231
+ // if it is ASCII...no multi-byte decodings
214
232
  const str = decoder.decode(buffer)
215
- const strIsASCII = isASCII(str)
216
- while (blockStart < str.length) {
217
- let line: string
218
- let n: number
219
- if (strIsASCII) {
220
- n = str.indexOf('\n', blockStart)
233
+ const strIsASCII = buffer.length == str.length
234
+ if (strIsASCII) {
235
+ while (blockStart < str.length) {
236
+ const n = str.indexOf('\n', blockStart)
221
237
  if (n === -1) {
222
238
  break
223
239
  }
224
- line = str.slice(blockStart, n)
225
- } else {
226
- n = buffer.indexOf('\n'.charCodeAt(0), blockStart)
240
+ const line = str.slice(blockStart, n)
241
+
242
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
243
+ if (dpositions) {
244
+ const target = blockStart + c.minv.dataPosition
245
+ while (pos < dpositions.length && target >= dpositions[pos]!) {
246
+ pos++
247
+ }
248
+ }
249
+
250
+ // filter the line for whether it is within the requested range
251
+ const { startCoordinate, overlaps } = this.checkLine(
252
+ metadata,
253
+ refName,
254
+ start,
255
+ end,
256
+ line,
257
+ )
258
+
259
+ if (overlaps) {
260
+ callback(
261
+ line,
262
+ this.calculateFileOffset(
263
+ cpositions,
264
+ dpositions,
265
+ pos,
266
+ blockStart,
267
+ c.minv.dataPosition,
268
+ ),
269
+ )
270
+ } else if (startCoordinate !== undefined && startCoordinate >= end) {
271
+ // the lines were overlapping the region, but now have stopped, so we
272
+ // must be at the end of the relevant data and we can stop processing
273
+ // data now
274
+ return
275
+ }
276
+ blockStart = n + 1
277
+ }
278
+ } else {
279
+ while (blockStart < buffer.length) {
280
+ const n = buffer.indexOf('\n'.charCodeAt(0), blockStart)
227
281
  if (n === -1) {
228
282
  break
229
283
  }
230
284
  const b = buffer.slice(blockStart, n)
231
- line = decoder.decode(b)
232
- }
285
+ const line = decoder.decode(b)
233
286
 
234
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
235
- if (dpositions) {
236
- while (blockStart + c.minv.dataPosition >= dpositions[pos++]!) {}
237
- pos--
238
- }
287
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
288
+ if (dpositions) {
289
+ const target = blockStart + c.minv.dataPosition
290
+ while (pos < dpositions.length && target >= dpositions[pos]!) {
291
+ pos++
292
+ }
293
+ }
239
294
 
240
- // filter the line for whether it is within the requested range
241
- const { startCoordinate, overlaps } = this.checkLine(
242
- metadata,
243
- refName,
244
- start,
245
- end,
246
- line,
247
- )
248
-
249
- if (overlaps) {
250
- callback(
295
+ // filter the line for whether it is within the requested range
296
+ const { startCoordinate, overlaps } = this.checkLine(
297
+ metadata,
298
+ refName,
299
+ start,
300
+ end,
251
301
  line,
252
- // cpositions[pos] refers to actual file offset of a bgzip block
253
- // boundaries
254
- //
255
- // we multiply by (1 <<8) in order to make sure each block has a
256
- // "unique" address space so that data in that block could never
257
- // overlap
258
- //
259
- // then the blockStart-dpositions is an uncompressed file offset
260
- // from that bgzip block boundary, and since the cpositions are
261
- // multiplied by (1 << 8) these uncompressed offsets get a unique
262
- // space
263
- cpositions[pos]! * (1 << 8) +
264
- (blockStart - dpositions[pos]!) +
265
- c.minv.dataPosition +
266
- 1,
267
302
  )
268
- } else if (startCoordinate !== undefined && startCoordinate >= end) {
269
- // the lines were overlapping the region, but now have stopped, so we
270
- // must be at the end of the relevant data and we can stop processing
271
- // data now
272
- return
303
+
304
+ if (overlaps) {
305
+ callback(
306
+ line,
307
+ this.calculateFileOffset(
308
+ cpositions,
309
+ dpositions,
310
+ pos,
311
+ blockStart,
312
+ c.minv.dataPosition,
313
+ ),
314
+ )
315
+ } else if (startCoordinate !== undefined && startCoordinate >= end) {
316
+ // the lines were overlapping the region, but now have stopped, so we
317
+ // must be at the end of the relevant data and we can stop processing
318
+ // data now
319
+ return
320
+ }
321
+ blockStart = n + 1
273
322
  }
274
- blockStart = n + 1
275
323
  }
276
324
  }
277
325
  }
@@ -303,11 +351,13 @@ export default class TabixIndexedFile {
303
351
  let lastNewline = -1
304
352
  const newlineByte = '\n'.charCodeAt(0)
305
353
  const metaByte = metaChar.charCodeAt(0)
306
- for (let i = 0; i < bytes.length; i += 1) {
307
- if (i === lastNewline + 1 && bytes[i] !== metaByte) {
354
+
355
+ for (let i = 0, l = bytes.length; i < l; i++) {
356
+ const byte = bytes[i]
357
+ if (i === lastNewline + 1 && byte !== metaByte) {
308
358
  break
309
359
  }
310
- if (bytes[i] === newlineByte) {
360
+ if (byte === newlineByte) {
311
361
  lastNewline = i
312
362
  }
313
363
  }
@@ -402,7 +452,10 @@ export default class TabixIndexedFile {
402
452
  }
403
453
  }
404
454
  } else if (currentColumnNumber === start) {
405
- startCoordinate = parseInt(line.slice(currentColumnStart, i), 10)
455
+ startCoordinate = Number.parseInt(
456
+ line.slice(currentColumnStart, i),
457
+ 10,
458
+ )
406
459
  // we convert to 0-based-half-open
407
460
  if (coordinateType === '1-based-closed') {
408
461
  startCoordinate -= 1
@@ -413,13 +466,13 @@ export default class TabixIndexedFile {
413
466
  overlaps: false,
414
467
  }
415
468
  }
416
- if (end === 0 || end === start) {
417
- // if we have no end, we assume the feature is 1 bp long
418
- if (startCoordinate + 1 <= regionStart) {
419
- return {
420
- startCoordinate,
421
- overlaps: false,
422
- }
469
+ if (
470
+ (end === 0 || end === start) && // if we have no end, we assume the feature is 1 bp long
471
+ startCoordinate + 1 <= regionStart
472
+ ) {
473
+ return {
474
+ startCoordinate,
475
+ overlaps: false,
423
476
  }
424
477
  }
425
478
  } else if (format === 'VCF' && currentColumnNumber === 4) {
@@ -440,11 +493,11 @@ export default class TabixIndexedFile {
440
493
  }
441
494
  }
442
495
  }
443
- currentColumnStart = i + 1
444
- currentColumnNumber += 1
445
- if (currentColumnNumber > maxColumn) {
496
+ if (currentColumnNumber === maxColumn) {
446
497
  break
447
498
  }
499
+ currentColumnStart = i + 1
500
+ currentColumnNumber += 1
448
501
  }
449
502
  }
450
503
  return {
@@ -462,17 +515,10 @@ export default class TabixIndexedFile {
462
515
  // be another pairwise feature at the end of this one
463
516
  const isTRA = info.includes('SVTYPE=TRA')
464
517
  if (info[0] !== '.' && !isTRA) {
465
- let prevChar = ';'
466
- for (let j = 0; j < info.length; j += 1) {
467
- if (prevChar === ';' && info.slice(j, j + 4) === 'END=') {
468
- let valueEnd = info.indexOf(';', j)
469
- if (valueEnd === -1) {
470
- valueEnd = info.length
471
- }
472
- endCoordinate = parseInt(info.slice(j + 4, valueEnd), 10)
473
- break
474
- }
475
- prevChar = info[j]
518
+ const endRegex = /(?:^|;)END=([^;]+)/
519
+ const match = endRegex.exec(info)
520
+ if (match) {
521
+ endCoordinate = Number.parseInt(match[1]!, 10)
476
522
  }
477
523
  } else if (isTRA) {
478
524
  return startCoordinate + 1
@@ -502,6 +548,6 @@ export default class TabixIndexedFile {
502
548
  c.minv.blockPosition,
503
549
  opts,
504
550
  )
505
- return unzipChunkSlice(ret, c)
551
+ return unzipChunkSlice(ret, c, this.cache)
506
552
  }
507
553
  }
package/src/tbi.ts CHANGED
@@ -75,7 +75,7 @@ export default class TabixIndex extends IndexFile {
75
75
  const depth = 5
76
76
  const maxBinNumber = ((1 << ((depth + 1) * 3)) - 1) / 7
77
77
  const maxRefLength = 2 ** (14 + depth * 3)
78
- const metaChar = metaValue ? String.fromCharCode(metaValue) : null
78
+ const metaChar = metaValue ? String.fromCharCode(metaValue) : undefined
79
79
  const skipLines = dataView.getInt32(28, true)
80
80
 
81
81
  // read sequence dictionary
@@ -160,31 +160,6 @@ export default class TabixIndex extends IndexFile {
160
160
  }
161
161
  }
162
162
 
163
- _parseNameBytes(namesBytes: Uint8Array) {
164
- let currRefId = 0
165
- let currNameStart = 0
166
- const refIdToName: string[] = []
167
- const refNameToId: Record<string, number> = {}
168
- const decoder = new TextDecoder('utf8')
169
- for (let i = 0; i < namesBytes.length; i += 1) {
170
- if (!namesBytes[i]) {
171
- if (currNameStart < i) {
172
- const refName = this.renameRefSeq(
173
- decoder.decode(namesBytes.subarray(currNameStart, i)),
174
- )
175
- refIdToName[currRefId] = refName
176
- refNameToId[refName] = currRefId
177
- }
178
- currNameStart = i + 1
179
- currRefId += 1
180
- }
181
- }
182
- return {
183
- refNameToId,
184
- refIdToName,
185
- }
186
- }
187
-
188
163
  async blocksForRange(
189
164
  refName: string,
190
165
  min: number,
@@ -205,13 +180,14 @@ export default class TabixIndex extends IndexFile {
205
180
  return []
206
181
  }
207
182
 
208
- const minOffset = ba.linearIndex.length
209
- ? ba.linearIndex[
210
- min >> TAD_LIDX_SHIFT >= ba.linearIndex.length
211
- ? ba.linearIndex.length - 1
212
- : min >> TAD_LIDX_SHIFT
213
- ]
214
- : new VirtualOffset(0, 0)
183
+ const minOffset =
184
+ ba.linearIndex.length > 0
185
+ ? ba.linearIndex[
186
+ min >> TAD_LIDX_SHIFT >= ba.linearIndex.length
187
+ ? ba.linearIndex.length - 1
188
+ : min >> TAD_LIDX_SHIFT
189
+ ]
190
+ : new VirtualOffset(0, 0)
215
191
  if (!minOffset) {
216
192
  console.warn('querying outside of possible tabix range')
217
193
  }
@@ -235,15 +211,13 @@ export default class TabixIndex extends IndexFile {
235
211
  // Use the linear index to find minimum file position of chunks that could
236
212
  // contain alignments in the region
237
213
  const nintv = ba.linearIndex.length
238
- let lowest = null
214
+ let lowest: VirtualOffset | undefined
239
215
  const minLin = Math.min(min >> 14, nintv - 1)
240
216
  const maxLin = Math.min(max >> 14, nintv - 1)
241
217
  for (let i = minLin; i <= maxLin; ++i) {
242
218
  const vp = ba.linearIndex[i]
243
- if (vp) {
244
- if (!lowest || vp.compareTo(lowest) < 0) {
245
- lowest = vp
246
- }
219
+ if (vp && (!lowest || vp.compareTo(lowest) < 0)) {
220
+ lowest = vp
247
221
  }
248
222
  }
249
223
 
package/src/util.ts CHANGED
@@ -22,12 +22,12 @@ export function checkAbortSignal(signal?: AbortSignal) {
22
22
  }
23
23
 
24
24
  if (signal.aborted) {
25
- if (typeof DOMException !== 'undefined') {
26
- throw new DOMException('aborted', 'AbortError')
27
- } else {
25
+ if (typeof DOMException === 'undefined') {
28
26
  const e = new AbortError('aborted')
29
27
  e.code = 'ERR_ABORTED'
30
28
  throw e
29
+ } else {
30
+ throw new DOMException('aborted', 'AbortError')
31
31
  }
32
32
  }
33
33
  }
@@ -52,7 +52,7 @@ export function canMergeBlocks(chunk1: Chunk, chunk2: Chunk) {
52
52
 
53
53
  export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
54
54
  const mergedChunks: Chunk[] = []
55
- let lastChunk: Chunk | null = null
55
+ let lastChunk: Chunk | undefined
56
56
 
57
57
  if (chunks.length === 0) {
58
58
  return chunks
@@ -60,12 +60,12 @@ export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
60
60
 
61
61
  chunks.sort(function (c0, c1) {
62
62
  const dif = c0.minv.blockPosition - c1.minv.blockPosition
63
- return dif !== 0 ? dif : c0.minv.dataPosition - c1.minv.dataPosition
63
+ return dif === 0 ? c0.minv.dataPosition - c1.minv.dataPosition : dif
64
64
  })
65
65
 
66
- chunks.forEach(chunk => {
66
+ for (const chunk of chunks) {
67
67
  if (!lowest || chunk.maxv.compareTo(lowest) > 0) {
68
- if (lastChunk === null) {
68
+ if (lastChunk === undefined) {
69
69
  mergedChunks.push(chunk)
70
70
  lastChunk = chunk
71
71
  } else {
@@ -79,7 +79,7 @@ export function optimizeChunks(chunks: Chunk[], lowest?: VirtualOffset) {
79
79
  }
80
80
  }
81
81
  }
82
- })
82
+ }
83
83
 
84
84
  return mergedChunks
85
85
  }