@gmod/bbi 7.1.0 → 8.0.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +59 -0
  3. package/dist/bbi.d.ts +13 -3
  4. package/dist/bbi.js +77 -17
  5. package/dist/bbi.js.map +1 -1
  6. package/dist/bigbed.d.ts +14 -2
  7. package/dist/bigbed.js +115 -76
  8. package/dist/bigbed.js.map +1 -1
  9. package/dist/bigwig.js +1 -2
  10. package/dist/bigwig.js.map +1 -1
  11. package/dist/block-view.d.ts +13 -4
  12. package/dist/block-view.js +324 -148
  13. package/dist/block-view.js.map +1 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/parse-bigwig.d.ts +3 -0
  18. package/dist/parse-bigwig.js +15 -0
  19. package/dist/parse-bigwig.js.map +1 -0
  20. package/dist/range.js +11 -24
  21. package/dist/range.js.map +1 -1
  22. package/dist/types.d.ts +14 -2
  23. package/dist/unzip.d.ts +18 -1
  24. package/dist/unzip.js +33 -4
  25. package/dist/unzip.js.map +1 -1
  26. package/dist/util.d.ts +2 -4
  27. package/dist/util.js +6 -5
  28. package/dist/util.js.map +1 -1
  29. package/dist/wasm/inflate-wasm-inlined.d.ts +19 -0
  30. package/dist/wasm/inflate-wasm-inlined.js +117 -0
  31. package/dist/wasm/inflate-wasm-inlined.js.map +1 -0
  32. package/dist/wasm/inflate_wasm.d.ts +1 -0
  33. package/dist/wasm/inflate_wasm.js +43 -0
  34. package/dist/wasm/inflate_wasm.js.map +1 -0
  35. package/dist/wasm/inflate_wasm_bg.d.ts +68 -0
  36. package/dist/wasm/inflate_wasm_bg.js +307 -0
  37. package/dist/wasm/inflate_wasm_bg.js.map +1 -0
  38. package/esm/bbi.d.ts +13 -3
  39. package/esm/bbi.js +77 -17
  40. package/esm/bbi.js.map +1 -1
  41. package/esm/bigbed.d.ts +14 -2
  42. package/esm/bigbed.js +115 -76
  43. package/esm/bigbed.js.map +1 -1
  44. package/esm/bigwig.js +1 -2
  45. package/esm/bigwig.js.map +1 -1
  46. package/esm/block-view.d.ts +13 -4
  47. package/esm/block-view.js +326 -150
  48. package/esm/block-view.js.map +1 -1
  49. package/esm/index.d.ts +2 -1
  50. package/esm/index.js +1 -0
  51. package/esm/index.js.map +1 -1
  52. package/esm/parse-bigwig.d.ts +3 -0
  53. package/esm/parse-bigwig.js +12 -0
  54. package/esm/parse-bigwig.js.map +1 -0
  55. package/esm/range.js +11 -24
  56. package/esm/range.js.map +1 -1
  57. package/esm/types.d.ts +14 -2
  58. package/esm/unzip.d.ts +18 -1
  59. package/esm/unzip.js +30 -3
  60. package/esm/unzip.js.map +1 -1
  61. package/esm/util.d.ts +2 -4
  62. package/esm/util.js +6 -5
  63. package/esm/util.js.map +1 -1
  64. package/esm/wasm/inflate-wasm-inlined.d.ts +19 -0
  65. package/esm/wasm/inflate-wasm-inlined.js +111 -0
  66. package/esm/wasm/inflate-wasm-inlined.js.map +1 -0
  67. package/esm/wasm/inflate_wasm.d.ts +1 -0
  68. package/esm/wasm/inflate_wasm.js +5 -0
  69. package/esm/wasm/inflate_wasm.js.map +1 -0
  70. package/esm/wasm/inflate_wasm_bg.d.ts +68 -0
  71. package/esm/wasm/inflate_wasm_bg.js +296 -0
  72. package/esm/wasm/inflate_wasm_bg.js.map +1 -0
  73. package/package.json +15 -10
  74. package/src/bbi.ts +100 -20
  75. package/src/bigbed.ts +157 -80
  76. package/src/bigwig.ts +1 -2
  77. package/src/block-view.ts +415 -156
  78. package/src/index.ts +8 -1
  79. package/src/parse-bigwig.ts +19 -0
  80. package/src/range.ts +13 -21
  81. package/src/types.ts +19 -2
  82. package/src/unzip.ts +88 -3
  83. package/src/util.ts +9 -10
  84. package/src/wasm/inflate-wasm-inlined.d.ts +49 -0
  85. package/src/wasm/inflate-wasm-inlined.js +1 -0
  86. package/src/wasm/inflate_wasm.d.ts +35 -0
  87. package/src/wasm/inflate_wasm.js +4 -0
  88. package/src/wasm/inflate_wasm_bg.js +309 -0
  89. package/src/wasm/inflate_wasm_bg.wasm +0 -0
  90. package/src/wasm/inflate_wasm_bg.wasm.d.ts +13 -0
package/src/block-view.ts CHANGED
@@ -2,15 +2,22 @@ import AbortablePromiseCache from '@gmod/abortable-promise-cache'
2
2
  import QuickLRU from 'quick-lru'
3
3
 
4
4
  import Range from './range.ts'
5
- import { unzip } from './unzip.ts'
6
- import { checkAbortSignal, groupBlocks } from './util.ts'
5
+ import {
6
+ decompressAndParseBigWigBlocks,
7
+ decompressAndParseSummaryBlocks,
8
+ unzipBatch,
9
+ } from './unzip.ts'
10
+ import { groupBlocks } from './util.ts'
7
11
 
8
12
  import type { Feature } from './types.ts'
13
+ import type {
14
+ BigWigFeatureArrays,
15
+ SummaryFeatureArrays,
16
+ } from './unzip.ts'
9
17
  import type { GenericFilehandle } from 'generic-filehandle2'
10
18
  import type { Observer } from 'rxjs'
11
19
 
12
- const decoder =
13
- typeof TextDecoder !== 'undefined' ? new TextDecoder('utf8') : undefined
20
+ const decoder = new TextDecoder('utf8')
14
21
 
15
22
  interface CoordRequest {
16
23
  chrId: number
@@ -40,7 +47,9 @@ function coordFilter(s1: number, e1: number, s2: number, e2: number): boolean {
40
47
  */
41
48
 
42
49
  export class BlockView {
43
- private cirTreePromise?: Promise<Uint8Array>
50
+ // R-tree index header cache - R-trees are spatial data structures used to
51
+ // efficiently query genomic intervals by chromosome and position
52
+ private rTreePromise?: Promise<Uint8Array>
44
53
 
45
54
  private featureCache = new AbortablePromiseCache<ReadData, Uint8Array>({
46
55
  cache: new QuickLRU({ maxSize: 1000 }),
@@ -51,13 +60,16 @@ export class BlockView {
51
60
 
52
61
  public constructor(
53
62
  private bbi: GenericFilehandle,
54
- private refsByName: any,
55
- private cirTreeOffset: number,
56
- private isCompressed: boolean,
63
+ private refsByName: Record<string, number>,
64
+ // Offset to the R-tree index in the file - this is part of the "cirTree"
65
+ // (combined ID R-tree), which combines a B+ tree for chromosome names
66
+ // with an R-tree for efficient spatial queries
67
+ private rTreeOffset: number,
68
+ private uncompressBufSize: number,
57
69
  private blockType: string,
58
70
  ) {
59
- if (!(cirTreeOffset >= 0)) {
60
- throw new Error('invalid cirTreeOffset!')
71
+ if (!(rTreeOffset >= 0)) {
72
+ throw new Error('invalid rTreeOffset!')
61
73
  }
62
74
  }
63
75
 
@@ -72,101 +84,115 @@ export class BlockView {
72
84
  const chrId = this.refsByName[chrName]
73
85
  if (chrId === undefined) {
74
86
  observer.complete()
87
+ return
75
88
  }
76
89
  const request = { chrId, start, end }
77
- if (!this.cirTreePromise) {
78
- this.cirTreePromise = this.bbi.read(48, this.cirTreeOffset, opts)
90
+ if (!this.rTreePromise) {
91
+ this.rTreePromise = this.bbi.read(48, this.rTreeOffset, opts)
79
92
  }
80
- const buffer = await this.cirTreePromise
81
- const dataView = new DataView(buffer.buffer)
82
- const cirBlockSize = dataView.getUint32(4, true)
83
- let blocksToFetch: any[] = []
93
+ const buffer = await this.rTreePromise
94
+ const dataView = new DataView(
95
+ buffer.buffer,
96
+ buffer.byteOffset,
97
+ buffer.length,
98
+ )
99
+ // Maximum number of children per R-tree node - used to calculate memory bounds
100
+ const rTreeBlockSize = dataView.getUint32(4, true)
101
+ const blocksToFetch: ReadData[] = []
84
102
  let outstanding = 0
85
103
 
86
- const cirFobRecur2 = (
87
- cirBlockData: Uint8Array,
104
+ // R-tree leaf nodes contain the actual data blocks to fetch
105
+ const processLeafNode = (
106
+ dataView: DataView,
107
+ startOffset: number,
108
+ count: number,
109
+ ) => {
110
+ let offset = startOffset
111
+ for (let i = 0; i < count; i++) {
112
+ const startChrom = dataView.getUint32(offset, true)
113
+ offset += 4
114
+ const startBase = dataView.getUint32(offset, true)
115
+ offset += 4
116
+ const endChrom = dataView.getUint32(offset, true)
117
+ offset += 4
118
+ const endBase = dataView.getUint32(offset, true)
119
+ offset += 4
120
+ const blockOffset = Number(dataView.getBigUint64(offset, true))
121
+ offset += 8
122
+ const blockSize = Number(dataView.getBigUint64(offset, true))
123
+ offset += 8
124
+ if (
125
+ blockIntersectsQuery({ startChrom, startBase, endBase, endChrom })
126
+ ) {
127
+ blocksToFetch.push({
128
+ offset: blockOffset,
129
+ length: blockSize,
130
+ })
131
+ }
132
+ }
133
+ }
134
+
135
+ // R-tree non-leaf nodes contain pointers to child nodes
136
+ const processNonLeafNode = (
137
+ dataView: DataView,
138
+ startOffset: number,
139
+ count: number,
140
+ level: number,
141
+ ) => {
142
+ const recurOffsets = []
143
+ let offset = startOffset
144
+ for (let i = 0; i < count; i++) {
145
+ const startChrom = dataView.getUint32(offset, true)
146
+ offset += 4
147
+ const startBase = dataView.getUint32(offset, true)
148
+ offset += 4
149
+ const endChrom = dataView.getUint32(offset, true)
150
+ offset += 4
151
+ const endBase = dataView.getUint32(offset, true)
152
+ offset += 4
153
+ const blockOffset = Number(dataView.getBigUint64(offset, true))
154
+ offset += 8
155
+ if (
156
+ blockIntersectsQuery({ startChrom, startBase, endChrom, endBase })
157
+ ) {
158
+ recurOffsets.push(blockOffset)
159
+ }
160
+ }
161
+ if (recurOffsets.length > 0) {
162
+ traverseRTree(recurOffsets, level + 1)
163
+ }
164
+ }
165
+
166
+ const processRTreeNode = (
167
+ rTreeBlockData: Uint8Array,
88
168
  offset2: number,
89
169
  level: number,
90
170
  ) => {
91
171
  try {
92
- const data = cirBlockData.subarray(offset2)
93
-
94
- const b = data
95
- const dataView = new DataView(b.buffer, b.byteOffset, b.length)
172
+ const data = rTreeBlockData.subarray(offset2)
173
+ const dataView = new DataView(
174
+ data.buffer,
175
+ data.byteOffset,
176
+ data.length,
177
+ )
96
178
  let offset = 0
97
179
 
98
180
  const isLeaf = dataView.getUint8(offset)
99
- offset += 2 // 1 skip
100
- const cnt = dataView.getUint16(offset, true)
181
+ offset += 2 // 1 skip for reserved byte
182
+ const count = dataView.getUint16(offset, true)
101
183
  offset += 2
184
+
102
185
  if (isLeaf === 1) {
103
- const blocksToFetch2 = []
104
- for (let i = 0; i < cnt; i++) {
105
- const startChrom = dataView.getUint32(offset, true)
106
- offset += 4
107
- const startBase = dataView.getUint32(offset, true)
108
- offset += 4
109
- const endChrom = dataView.getUint32(offset, true)
110
- offset += 4
111
- const endBase = dataView.getUint32(offset, true)
112
- offset += 4
113
- const blockOffset = Number(dataView.getBigUint64(offset, true))
114
- offset += 8
115
- const blockSize = Number(dataView.getBigUint64(offset, true))
116
- offset += 8
117
- blocksToFetch2.push({
118
- startChrom,
119
- startBase,
120
- endBase,
121
- endChrom,
122
- blockOffset,
123
- blockSize,
124
- offset,
125
- })
126
- }
127
- blocksToFetch = blocksToFetch.concat(
128
- blocksToFetch2
129
- .filter(f => filterFeats(f))
130
- .map(l => ({
131
- offset: l.blockOffset,
132
- length: l.blockSize,
133
- })),
134
- )
186
+ processLeafNode(dataView, offset, count)
135
187
  } else if (isLeaf === 0) {
136
- const recurOffsets = []
137
- for (let i = 0; i < cnt; i++) {
138
- const startChrom = dataView.getUint32(offset, true)
139
- offset += 4
140
- const startBase = dataView.getUint32(offset, true)
141
- offset += 4
142
- const endChrom = dataView.getUint32(offset, true)
143
- offset += 4
144
- const endBase = dataView.getUint32(offset, true)
145
- offset += 4
146
- const blockOffset = Number(dataView.getBigUint64(offset, true))
147
- offset += 8
148
- recurOffsets.push({
149
- startChrom,
150
- startBase,
151
- endChrom,
152
- endBase,
153
- blockOffset,
154
- offset,
155
- })
156
- }
157
- const recurOffsets2 = recurOffsets
158
- .filter(f => filterFeats(f))
159
- .map(l => l.blockOffset)
160
- if (recurOffsets2.length > 0) {
161
- cirFobRecur(recurOffsets2, level + 1)
162
- }
188
+ processNonLeafNode(dataView, offset, count, level)
163
189
  }
164
190
  } catch (e) {
165
191
  observer.error(e)
166
192
  }
167
193
  }
168
194
 
169
- const filterFeats = (b: {
195
+ const blockIntersectsQuery = (b: {
170
196
  startChrom: number
171
197
  startBase: number
172
198
  endChrom: number
@@ -179,22 +205,22 @@ export class BlockView {
179
205
  )
180
206
  }
181
207
 
182
- const cirFobStartFetch = async (
183
- off: number[],
184
- fr: Range,
208
+ const fetchAndProcessRTreeBlocks = async (
209
+ offsets: number[],
210
+ range: Range,
185
211
  level: number,
186
212
  ) => {
187
213
  try {
188
- const length = fr.max - fr.min
189
- const offset = fr.min
214
+ const length = range.max - range.min
215
+ const offset = range.min
190
216
  const resultBuffer = await this.featureCache.get(
191
217
  `${length}_${offset}`,
192
218
  { length, offset },
193
219
  opts?.signal,
194
220
  )
195
- for (const element of off) {
196
- if (fr.contains(element)) {
197
- cirFobRecur2(resultBuffer, element - offset, level)
221
+ for (const element of offsets) {
222
+ if (range.contains(element)) {
223
+ processRTreeNode(resultBuffer, element - offset, level)
198
224
  outstanding -= 1
199
225
  if (outstanding === 0) {
200
226
  this.readFeatures(observer, blocksToFetch, {
@@ -210,35 +236,40 @@ export class BlockView {
210
236
  observer.error(e)
211
237
  }
212
238
  }
213
- const cirFobRecur = (offset: number[], level: number) => {
239
+ const traverseRTree = (offsets: number[], level: number) => {
214
240
  try {
215
- outstanding += offset.length
241
+ outstanding += offsets.length
216
242
 
217
243
  // Upper bound on size, based on a completely full leaf node.
218
- const maxCirBlockSpan = 4 + cirBlockSize * 32
244
+ const maxRTreeBlockSpan = 4 + rTreeBlockSize * 32
219
245
  let spans = new Range([
220
246
  {
221
- min: offset[0],
222
- max: offset[0] + maxCirBlockSpan,
247
+ min: offsets[0]!,
248
+ max: offsets[0]! + maxRTreeBlockSpan,
223
249
  },
224
250
  ])
225
- for (let i = 1; i < offset.length; i += 1) {
251
+ for (let i = 1; i < offsets.length; i += 1) {
226
252
  const blockSpan = new Range([
227
253
  {
228
- min: offset[i],
229
- max: offset[i] + maxCirBlockSpan,
254
+ min: offsets[i]!,
255
+ max: offsets[i]! + maxRTreeBlockSpan,
230
256
  },
231
257
  ])
232
258
  spans = spans.union(blockSpan)
233
259
  }
234
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
235
- spans.getRanges().map(fr => cirFobStartFetch(offset, fr, level))
260
+ spans.getRanges().forEach(range => {
261
+ fetchAndProcessRTreeBlocks(offsets, range, level).catch(
262
+ (e: unknown) => {
263
+ observer.error(e)
264
+ },
265
+ )
266
+ })
236
267
  } catch (e) {
237
268
  observer.error(e)
238
269
  }
239
270
  }
240
271
 
241
- cirFobRecur([this.cirTreeOffset + 48], 1)
272
+ traverseRTree([this.rTreeOffset + 48], 1)
242
273
  return
243
274
  } catch (e) {
244
275
  observer.error(e)
@@ -250,13 +281,11 @@ export class BlockView {
250
281
  startOffset: number,
251
282
  request?: CoordRequest,
252
283
  ) {
253
- const features = [] as any[]
284
+ const features: Feature[] = []
254
285
  let offset = startOffset
255
286
 
256
287
  const dataView = new DataView(b.buffer, b.byteOffset, b.length)
257
288
  while (offset < b.byteLength) {
258
- // this was extracted from looking at the runtime code generated by
259
- // binary-parser
260
289
  const chromId = dataView.getUint32(offset, true)
261
290
  offset += 4
262
291
  const start = dataView.getUint32(offset, true)
@@ -270,16 +299,12 @@ export class BlockView {
270
299
  const maxScore = dataView.getFloat32(offset, true)
271
300
  offset += 4
272
301
  const sumData = dataView.getFloat32(offset, true)
273
- offset += 4
274
- // unused
275
- // const sumSqData = dataView.getFloat32(offset, true)
276
- offset += 4
302
+ offset += 8
277
303
 
278
304
  if (
279
- request
280
- ? chromId === request.chrId &&
281
- coordFilter(start, end, request.start, request.end)
282
- : true
305
+ !request ||
306
+ (chromId === request.chrId &&
307
+ coordFilter(start, end, request.start, request.end))
283
308
  ) {
284
309
  features.push({
285
310
  start,
@@ -301,10 +326,9 @@ export class BlockView {
301
326
  offset: number,
302
327
  request?: CoordRequest,
303
328
  ) {
304
- const items = [] as Feature[]
329
+ const items: Feature[] = []
305
330
  let currOffset = startOffset
306
- const b = data
307
- const dataView = new DataView(b.buffer, b.byteOffset, b.length)
331
+ const dataView = new DataView(data.buffer, data.byteOffset, data.length)
308
332
  while (currOffset < data.byteLength) {
309
333
  const c2 = currOffset
310
334
  const chromId = dataView.getUint32(currOffset, true)
@@ -320,22 +344,23 @@ export class BlockView {
320
344
  }
321
345
  }
322
346
  const b = data.subarray(currOffset, i)
323
- const rest = decoder?.decode(b) ?? b.toString()
347
+ const rest = decoder.decode(b)
324
348
  currOffset = i + 1
325
- items.push({
326
- chromId,
327
- start,
328
- end,
329
- rest,
330
- uniqueId: `bb-${offset + c2}`,
331
- })
349
+ if (
350
+ !request ||
351
+ (chromId === request.chrId &&
352
+ coordFilter(start, end, request.start, request.end))
353
+ ) {
354
+ items.push({
355
+ start,
356
+ end,
357
+ rest,
358
+ uniqueId: `bb-${offset + c2}`,
359
+ })
360
+ }
332
361
  }
333
362
 
334
- return request
335
- ? items.filter((f: any) =>
336
- coordFilter(f.start, f.end, request.start, request.end),
337
- )
338
- : items
363
+ return items
339
364
  }
340
365
 
341
366
  private parseBigWigBlock(
@@ -358,7 +383,7 @@ export class BlockView {
358
383
  offset += 2
359
384
  const itemCount = dataView.getUint16(offset, true)
360
385
  offset += 2
361
- const items = new Array(itemCount)
386
+ const items = []
362
387
  switch (blockType) {
363
388
  case 1: {
364
389
  for (let i = 0; i < itemCount; i++) {
@@ -368,10 +393,12 @@ export class BlockView {
368
393
  offset += 4
369
394
  const score = dataView.getFloat32(offset, true)
370
395
  offset += 4
371
- items[i] = {
372
- start,
373
- end,
374
- score,
396
+ if (!req || coordFilter(start, end, req.start, req.end)) {
397
+ items.push({
398
+ start,
399
+ end,
400
+ score,
401
+ })
375
402
  }
376
403
  }
377
404
  break
@@ -382,10 +409,13 @@ export class BlockView {
382
409
  offset += 4
383
410
  const score = dataView.getFloat32(offset, true)
384
411
  offset += 4
385
- items[i] = {
386
- score,
387
- start,
388
- end: start + itemSpan,
412
+ const end = start + itemSpan
413
+ if (!req || coordFilter(start, end, req.start, req.end)) {
414
+ items.push({
415
+ score,
416
+ start,
417
+ end,
418
+ })
389
419
  }
390
420
  }
391
421
  break
@@ -395,19 +425,20 @@ export class BlockView {
395
425
  const score = dataView.getFloat32(offset, true)
396
426
  offset += 4
397
427
  const start = blockStart + i * itemStep
398
- items[i] = {
399
- score,
400
- start,
401
- end: start + itemSpan,
428
+ const end = start + itemSpan
429
+ if (!req || coordFilter(start, end, req.start, req.end)) {
430
+ items.push({
431
+ score,
432
+ start,
433
+ end,
434
+ })
402
435
  }
403
436
  }
404
437
  break
405
438
  }
406
439
  }
407
440
 
408
- return req
409
- ? items.filter(f => coordFilter(f.start, f.end, req.start, req.end))
410
- : items
441
+ return items
411
442
  }
412
443
 
413
444
  public async readFeatures(
@@ -416,26 +447,45 @@ export class BlockView {
416
447
  opts: Options = {},
417
448
  ) {
418
449
  try {
419
- const { blockType, isCompressed } = this
450
+ const { blockType, uncompressBufSize } = this
420
451
  const { signal, request } = opts
421
452
  const blockGroupsToFetch = groupBlocks(blocks)
422
- checkAbortSignal(signal)
423
453
  await Promise.all(
424
454
  blockGroupsToFetch.map(async blockGroup => {
425
- checkAbortSignal(signal)
426
455
  const { length, offset } = blockGroup
427
456
  const data = await this.featureCache.get(
428
457
  `${length}_${offset}`,
429
458
  blockGroup,
430
459
  signal,
431
460
  )
432
- for (const block of blockGroup.blocks) {
433
- checkAbortSignal(signal)
434
- let resultData = data.subarray(block.offset - blockGroup.offset)
435
- if (isCompressed) {
436
- resultData = unzip(resultData)
437
- }
438
- checkAbortSignal(signal)
461
+
462
+ const localBlocks = blockGroup.blocks.map(block => ({
463
+ offset: block.offset - blockGroup.offset,
464
+ length: block.length,
465
+ }))
466
+
467
+ let decompressedData: Uint8Array
468
+ let decompressedOffsets: number[]
469
+
470
+ if (uncompressBufSize > 0) {
471
+ const result = await unzipBatch(
472
+ data,
473
+ localBlocks,
474
+ uncompressBufSize,
475
+ )
476
+ decompressedData = result.data
477
+ decompressedOffsets = result.offsets
478
+ } else {
479
+ decompressedData = data
480
+ decompressedOffsets = localBlocks.map(b => b.offset)
481
+ decompressedOffsets.push(data.length)
482
+ }
483
+
484
+ for (let i = 0; i < blockGroup.blocks.length; i++) {
485
+ const block = blockGroup.blocks[i]!
486
+ const start = decompressedOffsets[i]!
487
+ const end = decompressedOffsets[i + 1]!
488
+ const resultData = decompressedData.subarray(start, end)
439
489
 
440
490
  switch (blockType) {
441
491
  case 'summary': {
@@ -469,4 +519,213 @@ export class BlockView {
469
519
  observer.error(e)
470
520
  }
471
521
  }
522
+
523
+ public async readBigWigFeaturesAsArrays(
524
+ blocks: { offset: number; length: number }[],
525
+ opts: Options = {},
526
+ ): Promise<BigWigFeatureArrays> {
527
+ const { uncompressBufSize } = this
528
+ const { signal, request } = opts
529
+ const blockGroupsToFetch = groupBlocks(blocks)
530
+
531
+ const allStarts: Int32Array[] = []
532
+ const allEnds: Int32Array[] = []
533
+ const allScores: Float32Array[] = []
534
+ let totalCount = 0
535
+
536
+ await Promise.all(
537
+ blockGroupsToFetch.map(async blockGroup => {
538
+ const { length, offset } = blockGroup
539
+ const data = await this.featureCache.get(
540
+ `${length}_${offset}`,
541
+ blockGroup,
542
+ signal,
543
+ )
544
+
545
+ const localBlocks = blockGroup.blocks.map(block => ({
546
+ offset: block.offset - blockGroup.offset,
547
+ length: block.length,
548
+ }))
549
+
550
+ if (uncompressBufSize > 0) {
551
+ const result = await decompressAndParseBigWigBlocks(
552
+ data,
553
+ localBlocks,
554
+ uncompressBufSize,
555
+ request?.start ?? 0,
556
+ request?.end ?? 0,
557
+ )
558
+ if (result.starts.length > 0) {
559
+ allStarts.push(result.starts)
560
+ allEnds.push(result.ends)
561
+ allScores.push(result.scores)
562
+ totalCount += result.starts.length
563
+ }
564
+ } else {
565
+ for (const block of localBlocks) {
566
+ const blockData = data.subarray(block.offset, block.offset + block.length)
567
+ const features = this.parseBigWigBlock(blockData, 0, request)
568
+ if (features.length > 0) {
569
+ const starts = new Int32Array(features.length)
570
+ const ends = new Int32Array(features.length)
571
+ const scores = new Float32Array(features.length)
572
+ for (let i = 0; i < features.length; i++) {
573
+ const f = features[i]!
574
+ starts[i] = f.start
575
+ ends[i] = f.end
576
+ scores[i] = f.score
577
+ }
578
+ allStarts.push(starts)
579
+ allEnds.push(ends)
580
+ allScores.push(scores)
581
+ totalCount += features.length
582
+ }
583
+ }
584
+ }
585
+ }),
586
+ )
587
+
588
+ if (allStarts.length === 0) {
589
+ return {
590
+ starts: new Int32Array(0),
591
+ ends: new Int32Array(0),
592
+ scores: new Float32Array(0),
593
+ }
594
+ }
595
+
596
+ if (allStarts.length === 1) {
597
+ return {
598
+ starts: allStarts[0]!,
599
+ ends: allEnds[0]!,
600
+ scores: allScores[0]!,
601
+ }
602
+ }
603
+
604
+ const starts = new Int32Array(totalCount)
605
+ const ends = new Int32Array(totalCount)
606
+ const scores = new Float32Array(totalCount)
607
+ let offset = 0
608
+ for (let i = 0; i < allStarts.length; i++) {
609
+ starts.set(allStarts[i]!, offset)
610
+ ends.set(allEnds[i]!, offset)
611
+ scores.set(allScores[i]!, offset)
612
+ offset += allStarts[i]!.length
613
+ }
614
+
615
+ return { starts, ends, scores }
616
+ }
617
+
618
+ public async readSummaryFeaturesAsArrays(
619
+ blocks: { offset: number; length: number }[],
620
+ opts: Options = {},
621
+ ): Promise<SummaryFeatureArrays> {
622
+ const { uncompressBufSize } = this
623
+ const { signal, request } = opts
624
+ const blockGroupsToFetch = groupBlocks(blocks)
625
+
626
+ const allStarts: Int32Array[] = []
627
+ const allEnds: Int32Array[] = []
628
+ const allScores: Float32Array[] = []
629
+ const allMinScores: Float32Array[] = []
630
+ const allMaxScores: Float32Array[] = []
631
+ let totalCount = 0
632
+
633
+ await Promise.all(
634
+ blockGroupsToFetch.map(async blockGroup => {
635
+ const { length, offset } = blockGroup
636
+ const data = await this.featureCache.get(
637
+ `${length}_${offset}`,
638
+ blockGroup,
639
+ signal,
640
+ )
641
+
642
+ const localBlocks = blockGroup.blocks.map(block => ({
643
+ offset: block.offset - blockGroup.offset,
644
+ length: block.length,
645
+ }))
646
+
647
+ if (uncompressBufSize > 0) {
648
+ const result = await decompressAndParseSummaryBlocks(
649
+ data,
650
+ localBlocks,
651
+ uncompressBufSize,
652
+ request?.chrId ?? 0,
653
+ request?.start ?? 0,
654
+ request?.end ?? 0,
655
+ )
656
+ if (result.starts.length > 0) {
657
+ allStarts.push(result.starts)
658
+ allEnds.push(result.ends)
659
+ allScores.push(result.scores)
660
+ allMinScores.push(result.minScores)
661
+ allMaxScores.push(result.maxScores)
662
+ totalCount += result.starts.length
663
+ }
664
+ } else {
665
+ for (const block of localBlocks) {
666
+ const blockData = data.subarray(block.offset, block.offset + block.length)
667
+ const features = this.parseSummaryBlock(blockData, 0, request)
668
+ if (features.length > 0) {
669
+ const starts = new Int32Array(features.length)
670
+ const ends = new Int32Array(features.length)
671
+ const scores = new Float32Array(features.length)
672
+ const minScores = new Float32Array(features.length)
673
+ const maxScores = new Float32Array(features.length)
674
+ for (let i = 0; i < features.length; i++) {
675
+ const f = features[i]!
676
+ starts[i] = f.start
677
+ ends[i] = f.end
678
+ scores[i] = f.score ?? 0
679
+ minScores[i] = f.minScore ?? 0
680
+ maxScores[i] = f.maxScore ?? 0
681
+ }
682
+ allStarts.push(starts)
683
+ allEnds.push(ends)
684
+ allScores.push(scores)
685
+ allMinScores.push(minScores)
686
+ allMaxScores.push(maxScores)
687
+ totalCount += features.length
688
+ }
689
+ }
690
+ }
691
+ }),
692
+ )
693
+
694
+ if (allStarts.length === 0) {
695
+ return {
696
+ starts: new Int32Array(0),
697
+ ends: new Int32Array(0),
698
+ scores: new Float32Array(0),
699
+ minScores: new Float32Array(0),
700
+ maxScores: new Float32Array(0),
701
+ }
702
+ }
703
+
704
+ if (allStarts.length === 1) {
705
+ return {
706
+ starts: allStarts[0]!,
707
+ ends: allEnds[0]!,
708
+ scores: allScores[0]!,
709
+ minScores: allMinScores[0]!,
710
+ maxScores: allMaxScores[0]!,
711
+ }
712
+ }
713
+
714
+ const starts = new Int32Array(totalCount)
715
+ const ends = new Int32Array(totalCount)
716
+ const scores = new Float32Array(totalCount)
717
+ const minScores = new Float32Array(totalCount)
718
+ const maxScores = new Float32Array(totalCount)
719
+ let offset = 0
720
+ for (let i = 0; i < allStarts.length; i++) {
721
+ starts.set(allStarts[i]!, offset)
722
+ ends.set(allEnds[i]!, offset)
723
+ scores.set(allScores[i]!, offset)
724
+ minScores.set(allMinScores[i]!, offset)
725
+ maxScores.set(allMaxScores[i]!, offset)
726
+ offset += allStarts[i]!.length
727
+ }
728
+
729
+ return { starts, ends, scores, minScores, maxScores }
730
+ }
472
731
  }