@gmod/bam 7.1.19 → 7.1.21

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.
Files changed (66) hide show
  1. package/README.md +22 -13
  2. package/dist/bai.d.ts +2 -2
  3. package/dist/bai.js +1 -0
  4. package/dist/bai.js.map +1 -1
  5. package/dist/bamFile.d.ts +2 -1
  6. package/dist/bamFile.js +15 -6
  7. package/dist/bamFile.js.map +1 -1
  8. package/dist/chunk.d.ts +1 -1
  9. package/dist/chunk.js +5 -0
  10. package/dist/chunk.js.map +1 -1
  11. package/dist/csi.js +4 -6
  12. package/dist/csi.js.map +1 -1
  13. package/dist/htsget.d.ts +1 -1
  14. package/dist/htsget.js +15 -9
  15. package/dist/htsget.js.map +1 -1
  16. package/dist/indexFile.d.ts +2 -2
  17. package/dist/indexFile.js +2 -0
  18. package/dist/indexFile.js.map +1 -1
  19. package/dist/nullFilehandle.d.ts +4 -4
  20. package/dist/nullIndex.d.ts +4 -4
  21. package/dist/record.d.ts +11 -14
  22. package/dist/record.js +119 -117
  23. package/dist/record.js.map +1 -1
  24. package/dist/util.d.ts +2 -2
  25. package/dist/util.js +4 -7
  26. package/dist/util.js.map +1 -1
  27. package/dist/virtualOffset.js +2 -0
  28. package/dist/virtualOffset.js.map +1 -1
  29. package/esm/bai.d.ts +2 -2
  30. package/esm/bai.js +2 -1
  31. package/esm/bai.js.map +1 -1
  32. package/esm/bamFile.d.ts +2 -1
  33. package/esm/bamFile.js +15 -6
  34. package/esm/bamFile.js.map +1 -1
  35. package/esm/chunk.d.ts +1 -1
  36. package/esm/chunk.js +5 -0
  37. package/esm/chunk.js.map +1 -1
  38. package/esm/csi.js +4 -6
  39. package/esm/csi.js.map +1 -1
  40. package/esm/htsget.d.ts +1 -1
  41. package/esm/htsget.js +12 -9
  42. package/esm/htsget.js.map +1 -1
  43. package/esm/indexFile.d.ts +2 -2
  44. package/esm/indexFile.js +2 -0
  45. package/esm/indexFile.js.map +1 -1
  46. package/esm/nullFilehandle.d.ts +4 -4
  47. package/esm/nullIndex.d.ts +4 -4
  48. package/esm/record.d.ts +11 -14
  49. package/esm/record.js +119 -117
  50. package/esm/record.js.map +1 -1
  51. package/esm/util.d.ts +2 -2
  52. package/esm/util.js +4 -7
  53. package/esm/util.js.map +1 -1
  54. package/esm/virtualOffset.js +2 -0
  55. package/esm/virtualOffset.js.map +1 -1
  56. package/package.json +28 -32
  57. package/src/bai.ts +5 -7
  58. package/src/bamFile.ts +3 -1
  59. package/src/chunk.ts +1 -1
  60. package/src/htsget.ts +18 -14
  61. package/src/indexFile.ts +3 -2
  62. package/src/nullFilehandle.ts +4 -4
  63. package/src/nullIndex.ts +4 -4
  64. package/src/record.ts +137 -140
  65. package/src/util.ts +11 -11
  66. package/CHANGELOG.md +0 -530
package/src/record.ts CHANGED
@@ -17,6 +17,8 @@ const ASCII_CIGAR_CODES = [
17
17
  77, 73, 68, 78, 83, 72, 80, 61, 88, 63, 63, 63, 63, 63, 63, 63,
18
18
  ]
19
19
 
20
+ const textDecoder = new TextDecoder()
21
+
20
22
  // Bitmask for ops that consume ref: M=0, D=2, N=3, P=6, ==7, X=8
21
23
  // Binary: 0b111001101 = 0x1CD
22
24
  const CIGAR_CONSUMES_REF_MASK = 0x1cd
@@ -27,55 +29,42 @@ export interface Bytes {
27
29
  byteArray: Uint8Array
28
30
  }
29
31
 
30
- interface CIGAR_AND_LENGTH {
31
- length_on_ref: number
32
- NUMERIC_CIGAR: Uint32Array | number[]
33
- }
34
-
35
32
  export default class BamRecord {
36
33
  public fileOffset: number
37
- private bytes: Bytes
34
+ private _byteArray: Uint8Array
35
+ private _start: number
36
+ private _end: number
38
37
  private _dataView: DataView
39
38
 
40
- private _cachedFlags?: number
41
- private _cachedRefId?: number
42
- private _cachedStart?: number
43
39
  private _cachedEnd?: number
44
40
  private _cachedTags?: Record<string, unknown>
45
- private _cachedCigarAndLength?: CIGAR_AND_LENGTH
41
+ private _cachedLengthOnRef?: number
42
+ private _cachedNumericCigar?: Uint32Array | number[]
46
43
  private _cachedNUMERIC_MD?: Uint8Array | null
47
44
  private _cachedTagsStart?: number
48
45
 
49
- constructor(args: { bytes: Bytes; fileOffset: number }) {
50
- this.bytes = args.bytes
46
+ constructor(args: { bytes: Bytes; fileOffset: number; dataView: DataView }) {
47
+ this._byteArray = args.bytes.byteArray
48
+ this._start = args.bytes.start
49
+ this._end = args.bytes.end
51
50
  this.fileOffset = args.fileOffset
52
- this._dataView = new DataView(this.bytes.byteArray.buffer)
51
+ this._dataView = args.dataView
53
52
  }
54
53
 
55
54
  get byteArray() {
56
- return this.bytes.byteArray
55
+ return this._byteArray
57
56
  }
58
57
 
59
58
  get flags() {
60
- if (this._cachedFlags === undefined) {
61
- this._cachedFlags =
62
- (this._dataView.getInt32(this.bytes.start + 16, true) & 0xffff0000) >>
63
- 16
64
- }
65
- return this._cachedFlags
59
+ return (this._dataView.getInt32(this._start + 16, true) & 0xffff0000) >> 16
66
60
  }
61
+
67
62
  get ref_id() {
68
- if (this._cachedRefId === undefined) {
69
- this._cachedRefId = this._dataView.getInt32(this.bytes.start + 4, true)
70
- }
71
- return this._cachedRefId
63
+ return this._dataView.getInt32(this._start + 4, true)
72
64
  }
73
65
 
74
66
  get start() {
75
- if (this._cachedStart === undefined) {
76
- this._cachedStart = this._dataView.getInt32(this.bytes.start + 8, true)
77
- }
78
- return this._cachedStart
67
+ return this._dataView.getInt32(this._start + 8, true)
79
68
  }
80
69
 
81
70
  get end() {
@@ -98,12 +87,13 @@ export default class BamRecord {
98
87
  if (this.isSegmentUnmapped()) {
99
88
  return null
100
89
  } else {
90
+ const seqLen = this.seq_length
101
91
  const p =
102
92
  this.b0 +
103
93
  this.read_name_length +
104
94
  this.num_cigar_bytes +
105
- this.num_seq_bytes
106
- return this.byteArray.subarray(p, p + this.seq_length)
95
+ ((seqLen + 1) >> 1)
96
+ return this._byteArray.subarray(p, p + seqLen)
107
97
  }
108
98
  }
109
99
 
@@ -112,25 +102,27 @@ export default class BamRecord {
112
102
  }
113
103
 
114
104
  get b0() {
115
- return this.bytes.start + 36
105
+ return this._start + 36
116
106
  }
117
107
 
118
108
  get tagsStart() {
119
109
  if (this._cachedTagsStart === undefined) {
110
+ const seqLen = this.seq_length
120
111
  this._cachedTagsStart =
121
112
  this.b0 +
122
113
  this.read_name_length +
123
114
  this.num_cigar_bytes +
124
- this.num_seq_bytes +
125
- this.seq_length
115
+ ((seqLen + 1) >> 1) +
116
+ seqLen
126
117
  }
127
118
  return this._cachedTagsStart
128
119
  }
120
+
129
121
  // batch fromCharCode: fastest for typical name lengths (see benchmarks/string-building.bench.ts)
130
122
  get name() {
131
123
  const len = this.read_name_length - 1
132
124
  const start = this.b0
133
- const ba = this.byteArray
125
+ const ba = this._byteArray
134
126
  const codes = new Array(len)
135
127
  for (let i = 0; i < len; i++) {
136
128
  codes[i] = ba[start + i]!
@@ -170,12 +162,12 @@ export default class BamRecord {
170
162
 
171
163
  let p = this.tagsStart
172
164
 
173
- const blockEnd = this.bytes.end
174
- const ba = this.byteArray
165
+ const blockEnd = this._end
166
+ const ba = this._byteArray
175
167
  while (p < blockEnd) {
176
- const currentTag1 = ba[p]!
177
- const currentTag2 = ba[p + 1]!
178
- const type = ba[p + 2]!
168
+ const currentTag1 = ba[p]
169
+ const currentTag2 = ba[p + 1]
170
+ const type = ba[p + 2]
179
171
  p += 3
180
172
 
181
173
  const isMatch = currentTag1 === tag1 && currentTag2 === tag2
@@ -232,26 +224,21 @@ export default class BamRecord {
232
224
  case 0x5a: // 'Z'
233
225
  case 0x48: {
234
226
  // 'H'
227
+ const start = p
228
+ while (p < blockEnd && ba[p] !== 0) {
229
+ p++
230
+ }
235
231
  if (isMatch) {
236
- const start = p
237
- while (p < blockEnd && ba[p] !== 0) {
238
- p++
239
- }
240
- if (raw) {
241
- return ba.subarray(start, p)
242
- }
243
- const value = []
244
- for (let i = start; i < p; i++) {
245
- value.push(String.fromCharCode(ba[i]!))
246
- }
247
- return value.join('')
232
+ return raw
233
+ ? ba.subarray(start, p)
234
+ : textDecoder.decode(ba.subarray(start, p))
248
235
  }
249
- while (p <= blockEnd && ba[p++] !== 0) {}
236
+ p++ // advance past null terminator
250
237
  break
251
238
  }
252
239
  case 0x42: {
253
240
  // 'B'
254
- const Btype = ba[p++]!
241
+ const Btype = ba[p++]
255
242
  const limit = this._dataView.getInt32(p, true)
256
243
  p += 4
257
244
  const absOffset = ba.byteOffset + p
@@ -331,9 +318,9 @@ export default class BamRecord {
331
318
  private _computeTags() {
332
319
  let p = this.tagsStart
333
320
 
334
- const blockEnd = this.bytes.end
335
- const ba = this.byteArray
336
- const tags = {} as Record<string, unknown>
321
+ const blockEnd = this._end
322
+ const ba = this._byteArray
323
+ const tags: Record<string, unknown> = {}
337
324
  while (p < blockEnd) {
338
325
  const tag = String.fromCharCode(ba[p]!, ba[p + 1]!)
339
326
  const type = ba[p + 2]!
@@ -375,21 +362,17 @@ export default class BamRecord {
375
362
  case 0x5a: // 'Z'
376
363
  case 0x48: {
377
364
  // 'H'
378
- const value = []
379
- while (p <= blockEnd) {
380
- const cc = ba[p++]!
381
- if (cc !== 0) {
382
- value.push(String.fromCharCode(cc))
383
- } else {
384
- break
385
- }
365
+ const start = p
366
+ while (p < blockEnd && ba[p] !== 0) {
367
+ p++
386
368
  }
387
- tags[tag] = value.join('')
369
+ tags[tag] = textDecoder.decode(ba.subarray(start, p))
370
+ p++ // advance past null terminator
388
371
  break
389
372
  }
390
373
  case 0x42: {
391
374
  // 'B'
392
- const Btype = ba[p++]!
375
+ const Btype = ba[p++]
393
376
  const limit = this._dataView.getInt32(p, true)
394
377
  p += 4
395
378
  const absOffset = ba.byteOffset + p
@@ -520,13 +503,6 @@ export default class BamRecord {
520
503
  return !!(this.flags & Constants.BAM_FSUPPLEMENTARY)
521
504
  }
522
505
 
523
- get cigarAndLength() {
524
- if (this._cachedCigarAndLength === undefined) {
525
- this._cachedCigarAndLength = this._computeCigarAndLength()
526
- }
527
- return this._cachedCigarAndLength
528
- }
529
-
530
506
  // Benchmark results for CIGAR parsing strategies (see benchmarks/cigar-lifecycle.bench.ts):
531
507
  //
532
508
  // Aligned data:
@@ -545,78 +521,94 @@ export default class BamRecord {
545
521
  //
546
522
  // Strategy: use plain array with |0 for small aligned (≤50 ops) and all unaligned,
547
523
  // Uint32Array view only for large aligned CIGARs.
548
- private _computeCigarAndLength() {
549
- if (this.isSegmentUnmapped()) {
550
- return {
551
- length_on_ref: 0,
552
- NUMERIC_CIGAR: new Uint32Array(0),
553
- }
524
+
525
+ // CG tag pattern: first op is soft-clip consuming entire sequence, second op is N encoding length-on-ref
526
+ private _isCGTagPattern(p: number) {
527
+ const cigop = this._dataView.getInt32(p, true)
528
+ return (cigop & 0xf) === CIGAR_SOFT_CLIP && cigop >> 4 === this.seq_length
529
+ }
530
+
531
+ private _computeLengthOnRef(): number {
532
+ const flag_nc = this._dataView.getInt32(this._start + 16, true)
533
+ if (flag_nc & (Constants.BAM_FUNMAP << 16)) {
534
+ return 0
554
535
  }
555
536
 
556
- const numCigarOps = this.num_cigar_ops
557
- let p = this.b0 + this.read_name_length
537
+ const numCigarOps = flag_nc & 0xffff
538
+ const p = this.b0 + this.read_name_length
558
539
 
559
- // check for CG tag by inspecting whether the CIGAR field contains a clip
560
- // that consumes entire seqLen
561
- const cigop = this._dataView.getInt32(p, true)
562
- const lop = cigop >> 4
563
- const op = cigop & 0xf
564
- if (op === CIGAR_SOFT_CLIP && lop === this.seq_length) {
565
- // if there is a CG the second CIGAR field will be a N tag the represents
566
- // the length on ref
567
- p += 4
568
- const cigop = this._dataView.getInt32(p, true)
569
- const lop = cigop >> 4
570
- const op = cigop & 0xf
571
- if (op !== CIGAR_REF_SKIP) {
540
+ if (this._isCGTagPattern(p)) {
541
+ const cigop2 = this._dataView.getInt32(p + 4, true)
542
+ if ((cigop2 & 0xf) !== CIGAR_REF_SKIP) {
572
543
  console.warn('CG tag with no N tag')
573
544
  }
574
- const cgArray = this.tags.CG as Uint32Array
575
- return {
576
- NUMERIC_CIGAR: cgArray,
577
- length_on_ref: lop,
578
- }
545
+ return cigop2 >> 4
579
546
  }
580
547
 
581
- const absOffset = this.byteArray.byteOffset + p
582
- const isAligned = absOffset % 4 === 0
583
-
584
- if (isAligned && numCigarOps > 50) {
548
+ const absOffset = this._byteArray.byteOffset + p
549
+ if (absOffset % 4 === 0 && numCigarOps > 50) {
585
550
  const cigarView = new Uint32Array(
586
- this.byteArray.buffer,
551
+ this._byteArray.buffer,
587
552
  absOffset,
588
553
  numCigarOps,
589
554
  )
555
+ this._cachedNumericCigar = cigarView
590
556
  let lref = 0
591
557
  for (let c = 0; c < numCigarOps; ++c) {
592
- const cigop = cigarView[c]!
593
- lref += (cigop >> 4) * ((CIGAR_CONSUMES_REF_MASK >> (cigop & 0xf)) & 1)
594
- }
595
- return {
596
- NUMERIC_CIGAR: cigarView,
597
- length_on_ref: lref,
558
+ const co = cigarView[c]!
559
+ lref += (co >> 4) * ((CIGAR_CONSUMES_REF_MASK >> (co & 0xf)) & 1)
598
560
  }
561
+ return lref
599
562
  }
600
563
 
601
- const cigarArray: number[] = new Array(numCigarOps)
602
564
  let lref = 0
603
565
  for (let c = 0; c < numCigarOps; ++c) {
604
- const cigop = this._dataView.getInt32(p + c * 4, true) | 0
605
- cigarArray[c] = cigop
606
- lref += (cigop >> 4) * ((CIGAR_CONSUMES_REF_MASK >> (cigop & 0xf)) & 1)
566
+ const co = this._dataView.getInt32(p + c * 4, true)
567
+ lref += (co >> 4) * ((CIGAR_CONSUMES_REF_MASK >> (co & 0xf)) & 1)
607
568
  }
608
- return {
609
- NUMERIC_CIGAR: cigarArray,
610
- length_on_ref: lref,
569
+ return lref
570
+ }
571
+
572
+ private _computeNumericCigar(): Uint32Array | number[] {
573
+ const flag_nc = this._dataView.getInt32(this._start + 16, true)
574
+ if (flag_nc & (Constants.BAM_FUNMAP << 16)) {
575
+ return new Uint32Array(0)
611
576
  }
577
+
578
+ const numCigarOps = flag_nc & 0xffff
579
+ const p = this.b0 + this.read_name_length
580
+
581
+ if (this._isCGTagPattern(p)) {
582
+ return (
583
+ (this.tags.CG as Uint32Array | number[] | undefined) ??
584
+ new Uint32Array(0)
585
+ )
586
+ }
587
+
588
+ const absOffset = this._byteArray.byteOffset + p
589
+ if (absOffset % 4 === 0 && numCigarOps > 50) {
590
+ return new Uint32Array(this._byteArray.buffer, absOffset, numCigarOps)
591
+ }
592
+
593
+ const cigarArray: number[] = new Array(numCigarOps)
594
+ for (let c = 0; c < numCigarOps; ++c) {
595
+ cigarArray[c] = this._dataView.getInt32(p + c * 4, true) | 0
596
+ }
597
+ return cigarArray
612
598
  }
613
599
 
614
600
  get length_on_ref() {
615
- return this.cigarAndLength.length_on_ref
601
+ if (this._cachedLengthOnRef === undefined) {
602
+ this._cachedLengthOnRef = this._computeLengthOnRef()
603
+ }
604
+ return this._cachedLengthOnRef
616
605
  }
617
606
 
618
607
  get NUMERIC_CIGAR() {
619
- return this.cigarAndLength.NUMERIC_CIGAR
608
+ if (this._cachedNumericCigar === undefined) {
609
+ this._cachedNumericCigar = this._computeNumericCigar()
610
+ }
611
+ return this._cachedNumericCigar
620
612
  }
621
613
 
622
614
  get CIGAR() {
@@ -649,35 +641,40 @@ export default class BamRecord {
649
641
 
650
642
  get NUMERIC_SEQ() {
651
643
  const p = this.b0 + this.read_name_length + this.num_cigar_bytes
652
- return this.byteArray.subarray(p, p + this.num_seq_bytes)
644
+ return this._byteArray.subarray(p, p + this.num_seq_bytes)
653
645
  }
654
646
 
655
647
  get seq() {
656
- const numeric = this.NUMERIC_SEQ
657
648
  const len = this.seq_length
649
+ const seqStart = this.b0 + this.read_name_length + this.num_cigar_bytes
650
+ const numeric = this._byteArray
658
651
  const buf = new Array(len)
659
652
  let i = 0
660
653
  const fullBytes = len >> 1
661
654
 
662
655
  for (let j = 0; j < fullBytes; ++j) {
663
- const sb = numeric[j]!
664
- buf[i++] = SEQRET_DECODER[(sb & 0xf0) >> 4]
665
- buf[i++] = SEQRET_DECODER[sb & 0x0f]
656
+ const sb = numeric[seqStart + j]!
657
+ buf[i++] = SEQRET_DECODER[(sb & 0xf0) >> 4]!
658
+ buf[i++] = SEQRET_DECODER[sb & 0x0f]!
666
659
  }
667
660
 
668
661
  if (i < len) {
669
- const sb = numeric[fullBytes]!
670
- buf[i] = SEQRET_DECODER[(sb & 0xf0) >> 4]
662
+ const sb = numeric[seqStart + fullBytes]!
663
+ buf[i] = SEQRET_DECODER[(sb & 0xf0) >> 4]!
671
664
  }
672
665
 
673
666
  return buf.join('')
674
667
  }
675
668
 
676
669
  // adapted from igv.js
677
- // uses precomputed lookup table indexed by flag bits + isize sign
670
+ // uses precomputed lookup table indexed by flag bits + isize sign.
671
+ // the BAM spec defines tlen as positive for the leftmost segment and
672
+ // negative for the rightmost, so tlen > 0 reliably indicates which
673
+ // read comes first without needing position-based correction
674
+ // (see also: gmod/cram-js src/cramFile/record.ts getPairOrientation)
678
675
  get pair_orientation() {
679
676
  const f = this.flags
680
- // combined check: unmapped (0x4) clear, mate unmapped (0x8) clear
677
+ // unmapped (0x4) or mate unmapped (0x8) -> undefined
681
678
  if (f & 0xc || this.ref_id !== this.next_refid) {
682
679
  return undefined
683
680
  }
@@ -687,52 +684,52 @@ export default class BamRecord {
687
684
  }
688
685
 
689
686
  get bin_mq_nl() {
690
- return this._dataView.getInt32(this.bytes.start + 12, true)
687
+ return this._dataView.getInt32(this._start + 12, true)
691
688
  }
692
689
 
693
690
  get flag_nc() {
694
- return this._dataView.getInt32(this.bytes.start + 16, true)
691
+ return this._dataView.getInt32(this._start + 16, true)
695
692
  }
696
693
 
697
694
  get seq_length() {
698
- return this._dataView.getInt32(this.bytes.start + 20, true)
695
+ return this._dataView.getInt32(this._start + 20, true)
699
696
  }
700
697
 
701
698
  get next_refid() {
702
- return this._dataView.getInt32(this.bytes.start + 24, true)
699
+ return this._dataView.getInt32(this._start + 24, true)
703
700
  }
704
701
 
705
702
  get next_pos() {
706
- return this._dataView.getInt32(this.bytes.start + 28, true)
703
+ return this._dataView.getInt32(this._start + 28, true)
707
704
  }
708
705
 
709
706
  get template_length() {
710
- return this._dataView.getInt32(this.bytes.start + 32, true)
707
+ return this._dataView.getInt32(this._start + 32, true)
711
708
  }
712
709
 
713
710
  seqAt(idx: number): string | undefined {
714
711
  if (idx < this.seq_length) {
715
712
  const byteIndex = idx >> 1
716
713
  const sb =
717
- this.byteArray[
714
+ this._byteArray[
718
715
  this.b0 + this.read_name_length + this.num_cigar_bytes + byteIndex
719
716
  ]!
720
717
 
721
718
  return idx % 2 === 0
722
- ? SEQRET_DECODER[(sb & 0xf0) >> 4]
723
- : SEQRET_DECODER[sb & 0x0f]
719
+ ? SEQRET_DECODER[(sb & 0xf0) >> 4]!
720
+ : SEQRET_DECODER[sb & 0x0f]!
724
721
  } else {
725
722
  return undefined
726
723
  }
727
724
  }
728
725
 
729
726
  toJSON() {
730
- const data: Record<string, any> = {}
727
+ const data: Record<string, unknown> = {}
731
728
  for (const k of Object.keys(this)) {
732
- if (k.startsWith('_') || k === 'bytes') {
729
+ if (k.startsWith('_')) {
733
730
  continue
734
731
  }
735
- // @ts-ignore
732
+ // @ts-expect-error
736
733
  data[k] = this[k]
737
734
  }
738
735
 
package/src/util.ts CHANGED
@@ -1,6 +1,7 @@
1
- import Chunk from './chunk.ts'
2
1
  import { longFromBytesToUnsigned } from './long.ts'
3
- import { Offset, VirtualOffset } from './virtualOffset.ts'
2
+
3
+ import type Chunk from './chunk.ts'
4
+ import type { Offset, VirtualOffset } from './virtualOffset.ts'
4
5
 
5
6
  export function canMergeBlocks(chunk1: Chunk, chunk2: Chunk) {
6
7
  return (
@@ -33,7 +34,7 @@ export interface BaseOpts {
33
34
  }
34
35
 
35
36
  export function makeOpts(obj: AbortSignal | BaseOpts = {}): BaseOpts {
36
- return 'aborted' in obj ? ({ signal: obj } as BaseOpts) : obj
37
+ return 'aborted' in obj ? { signal: obj } : obj
37
38
  }
38
39
 
39
40
  export function optimizeChunks(chunks: Chunk[], lowest?: Offset) {
@@ -126,23 +127,22 @@ export function parseNameBytes(
126
127
  namesBytes: Uint8Array,
127
128
  renameRefSeq: (arg: string) => string = s => s,
128
129
  ) {
130
+ const decoder = new TextDecoder()
129
131
  let currRefId = 0
130
132
  let currNameStart = 0
131
- const refIdToName = []
133
+ const refIdToName: string[] = []
132
134
  const refNameToId: Record<string, number> = {}
133
- for (let i = 0; i < namesBytes.length; i += 1) {
135
+ for (let i = 0; i < namesBytes.length; i++) {
134
136
  if (!namesBytes[i]) {
135
137
  if (currNameStart < i) {
136
- let refName = ''
137
- for (let j = currNameStart; j < i; j++) {
138
- refName += String.fromCharCode(namesBytes[j]!)
139
- }
140
- refName = renameRefSeq(refName)
138
+ const refName = renameRefSeq(
139
+ decoder.decode(namesBytes.subarray(currNameStart, i)),
140
+ )
141
141
  refIdToName[currRefId] = refName
142
142
  refNameToId[refName] = currRefId
143
143
  }
144
144
  currNameStart = i + 1
145
- currRefId += 1
145
+ currRefId++
146
146
  }
147
147
  }
148
148
  return { refNameToId, refIdToName }