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