@gmod/tabix 3.6.0 → 3.6.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/dist/indexFile.d.ts +19 -23
- package/dist/indexFile.js +23 -60
- package/dist/indexFile.js.map +1 -1
- package/dist/tabix-bundle.js +1 -1
- package/dist/tabixIndexedFile.d.ts +13 -11
- package/dist/tabixIndexedFile.js +15 -47
- package/dist/tabixIndexedFile.js.map +1 -1
- package/esm/indexFile.d.ts +19 -23
- package/esm/indexFile.js +24 -61
- package/esm/indexFile.js.map +1 -1
- package/esm/tabixIndexedFile.d.ts +13 -11
- package/esm/tabixIndexedFile.js +16 -48
- package/esm/tabixIndexedFile.js.map +1 -1
- package/package.json +2 -2
- package/src/indexFile.ts +25 -64
- package/src/tabixIndexedFile.ts +20 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmod/tabix",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"packageManager": "pnpm@11.15.1",
|
|
5
5
|
"description": "Read Tabix-indexed files, supports both .tbi and .csi indexes",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@gmod/bgzf-filehandle": "^6.3.2",
|
|
55
|
-
"@gmod/shared-read-cache": "^1.4.
|
|
55
|
+
"@gmod/shared-read-cache": "^1.4.4",
|
|
56
56
|
"@jbrowse/quick-lru": "^7.3.5",
|
|
57
57
|
"generic-filehandle2": "^2.2.1"
|
|
58
58
|
},
|
package/src/indexFile.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unzip } from '@gmod/bgzf-filehandle'
|
|
2
|
+
import { SharedReadCache } from '@gmod/shared-read-cache'
|
|
2
3
|
|
|
3
|
-
import { optimizeChunks
|
|
4
|
+
import { optimizeChunks } from './util.ts'
|
|
4
5
|
|
|
5
6
|
import type Chunk from './chunk.ts'
|
|
6
7
|
import type VirtualOffset from './virtualOffset.ts'
|
|
@@ -51,14 +52,11 @@ export interface IndexData {
|
|
|
51
52
|
|
|
52
53
|
export default abstract class IndexFile {
|
|
53
54
|
public filehandle: GenericFilehandle
|
|
54
|
-
private parseP?: Promise<IndexData>
|
|
55
55
|
/**
|
|
56
|
-
* The
|
|
57
|
-
*
|
|
58
|
-
* this the first query to arrive would own a read all the others depend on —
|
|
59
|
-
* see {@link parse}.
|
|
56
|
+
* The parsed index, as a shared read — see {@link parse}. One entry, never
|
|
57
|
+
* evicted, which is what a memo is.
|
|
60
58
|
*/
|
|
61
|
-
private
|
|
59
|
+
private parseCache = new SharedReadCache<string, IndexData>({})
|
|
62
60
|
|
|
63
61
|
constructor({ filehandle }: { filehandle: GenericFilehandle }) {
|
|
64
62
|
this.filehandle = filehandle
|
|
@@ -158,75 +156,38 @@ export default abstract class IndexFile {
|
|
|
158
156
|
return optimizeChunks(chunks, this.lowestOffset(ba, min, indexData))
|
|
159
157
|
}
|
|
160
158
|
|
|
161
|
-
// SYNC: ~/src/gmod/bam-js/src/indexFile.ts parse — same
|
|
162
|
-
//
|
|
159
|
+
// SYNC: ~/src/gmod/bam-js/src/indexFile.ts parse — same shape and the same
|
|
160
|
+
// reasoning below.
|
|
163
161
|
/**
|
|
164
162
|
* Parse the index, or join the parse already running.
|
|
165
163
|
*
|
|
166
164
|
* The index is downloaded and parsed once for the life of this object, so it
|
|
167
165
|
* is the one read here that is shared between queries — and therefore the one
|
|
168
166
|
* place a cancellation can leak from the query that asked for it to a query
|
|
169
|
-
* that did not. `_parse` hands `opts` straight to `readIndexBytes`, so
|
|
170
|
-
*
|
|
171
|
-
* depends on: when it pans away, every concurrent query fails
|
|
167
|
+
* that did not. `_parse` hands `opts` straight to `readIndexBytes`, so a bare
|
|
168
|
+
* memoized promise makes the first query to arrive the owner of a read every
|
|
169
|
+
* other query depends on: when it pans away, every concurrent query fails
|
|
170
|
+
* with its abort.
|
|
172
171
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
172
|
+
* The same cache `ChunkCache` uses, for the same reason and with the same
|
|
173
|
+
* rule: the parse runs under a signal of its own and is cancelled only once
|
|
174
|
+
* every caller waiting on it has given up, so one query's abort is reported
|
|
175
|
+
* to that query alone and a bystander gets the parse already in flight rather
|
|
176
|
+
* than having to re-read the index. A rejection is dropped rather than
|
|
177
|
+
* cached, so a transient failure does not poison the index for the life of
|
|
178
|
+
* the file.
|
|
180
179
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* split for the same reason.
|
|
180
|
+
* The fill is per call rather than on the cache so that the caller who starts
|
|
181
|
+
* the parse has its `onProgress` reach `readIndexBytes` — the index is a
|
|
182
|
+
* whole-file read, and a determinate "downloading index" bar is what that
|
|
183
|
+
* callback exists for.
|
|
186
184
|
*
|
|
187
185
|
* @internal
|
|
188
186
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!pending) {
|
|
193
|
-
return this.startParse(opts)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// read before awaiting: the owner is forgotten as soon as the parse settles
|
|
197
|
-
const ownerSignal = this.parseSignal
|
|
198
|
-
try {
|
|
199
|
-
return await pending
|
|
200
|
-
} catch (e) {
|
|
201
|
-
if (retried || !ownerSignal?.aborted || opts.signal?.aborted) {
|
|
202
|
-
throw e
|
|
203
|
-
}
|
|
204
|
-
return this.parse(opts, true)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private startParse(opts: Options) {
|
|
209
|
-
const pending = this._parse(opts)
|
|
210
|
-
this.parseP = pending
|
|
211
|
-
this.parseSignal = opts.signal
|
|
212
|
-
// Drop a rejection rather than keeping it, so one transient failure does not
|
|
213
|
-
// poison the index for the lifetime of the file. Both branches are
|
|
214
|
-
// identity-checked so a retry started after this settles is not cleared by
|
|
215
|
-
// the attempt it already replaced.
|
|
216
|
-
pending.then(
|
|
217
|
-
() => {
|
|
218
|
-
if (this.parseP === pending) {
|
|
219
|
-
this.parseSignal = undefined
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
() => {
|
|
223
|
-
if (this.parseP === pending) {
|
|
224
|
-
this.parseP = undefined
|
|
225
|
-
this.parseSignal = undefined
|
|
226
|
-
}
|
|
227
|
-
},
|
|
187
|
+
parse(opts: Options = {}): Promise<IndexData> {
|
|
188
|
+
return this.parseCache.get('index', opts.signal, signal =>
|
|
189
|
+
this._parse({ ...opts, signal }),
|
|
228
190
|
)
|
|
229
|
-
return pending
|
|
230
191
|
}
|
|
231
192
|
|
|
232
193
|
/** @internal */
|
package/src/tabixIndexedFile.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { LocalFile, RemoteFile } from 'generic-filehandle2'
|
|
|
4
4
|
|
|
5
5
|
import CSI from './csi.ts'
|
|
6
6
|
import TBI from './tbi.ts'
|
|
7
|
-
import { optimizeChunks
|
|
7
|
+
import { optimizeChunks } from './util.ts'
|
|
8
8
|
|
|
9
9
|
import type Chunk from './chunk.ts'
|
|
10
10
|
import type IndexFile from './indexFile.ts'
|
|
@@ -277,14 +277,14 @@ export default class TabixIndexedFile {
|
|
|
277
277
|
private filehandle: GenericFilehandle
|
|
278
278
|
private index: IndexFile
|
|
279
279
|
public chunkCache: SharedReadCache<Chunk, ReadChunk>
|
|
280
|
-
private headerP?: Promise<{ header: string; skippedLines: string[] }>
|
|
281
280
|
/**
|
|
282
|
-
* The
|
|
283
|
-
*
|
|
284
|
-
* one to arrive would own a read all the others depend on — see
|
|
285
|
-
* {@link getParsedHeader}.
|
|
281
|
+
* The parsed header, as a shared read — see {@link getParsedHeader}. One
|
|
282
|
+
* entry, never evicted, which is what a memo is.
|
|
286
283
|
*/
|
|
287
|
-
private
|
|
284
|
+
private headerCache = new SharedReadCache<
|
|
285
|
+
string,
|
|
286
|
+
{ header: string; skippedLines: string[] }
|
|
287
|
+
>({})
|
|
288
288
|
|
|
289
289
|
constructor({
|
|
290
290
|
path,
|
|
@@ -641,58 +641,24 @@ export default class TabixIndexedFile {
|
|
|
641
641
|
* charge of a read every later caller joins: when it aborted, they failed
|
|
642
642
|
* with its cancellation, their own signals untouched.
|
|
643
643
|
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
644
|
+
* The same cache the chunk reads use, and the same rule: the parse runs under
|
|
645
|
+
* a signal of its own and is cancelled only once every caller waiting on it
|
|
646
|
+
* has given up, so one caller's abort is reported to that caller alone and a
|
|
647
|
+
* bystander gets the parse already in flight. There is no retry here because
|
|
648
|
+
* there is nothing to retry — the parse a bystander joined is not cancelled by
|
|
649
|
+
* someone else's abort. A rejection is dropped rather than cached, so a
|
|
650
|
+
* transient failure does not poison the header for the life of the file.
|
|
651
|
+
*
|
|
652
|
+
* The fill is per call rather than on the cache, so the caller that starts the
|
|
653
|
+
* parse has its `onProgress` reach the index download inside `getMetadata`.
|
|
648
654
|
*
|
|
649
655
|
* SYNC: ~/src/gmod/bam-js/src/bamFile.ts getHeader — same shape for the same
|
|
650
656
|
* reason, on the header rather than the index.
|
|
651
657
|
*/
|
|
652
|
-
private
|
|
653
|
-
opts
|
|
654
|
-
|
|
655
|
-
): Promise<{ header: string; skippedLines: string[] }> {
|
|
656
|
-
throwIfAborted(opts.signal)
|
|
657
|
-
const pending = this.headerP
|
|
658
|
-
if (!pending) {
|
|
659
|
-
return this.startHeaderParse(opts)
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// read before awaiting: the owner is forgotten as soon as the parse settles
|
|
663
|
-
const ownerSignal = this.headerSignal
|
|
664
|
-
try {
|
|
665
|
-
return await pending
|
|
666
|
-
} catch (e) {
|
|
667
|
-
if (retried || !ownerSignal?.aborted || opts.signal?.aborted) {
|
|
668
|
-
throw e
|
|
669
|
-
}
|
|
670
|
-
return this.getParsedHeader(opts, true)
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
private startHeaderParse(opts: Options) {
|
|
675
|
-
const pending = this.parseHeader(opts)
|
|
676
|
-
this.headerP = pending
|
|
677
|
-
this.headerSignal = opts.signal
|
|
678
|
-
// Drop a rejection rather than keeping it, so one transient failure does not
|
|
679
|
-
// poison the header for the lifetime of the file. Both branches are
|
|
680
|
-
// identity-checked so a retry started after this settles is not cleared by
|
|
681
|
-
// the attempt it already replaced.
|
|
682
|
-
pending.then(
|
|
683
|
-
() => {
|
|
684
|
-
if (this.headerP === pending) {
|
|
685
|
-
this.headerSignal = undefined
|
|
686
|
-
}
|
|
687
|
-
},
|
|
688
|
-
() => {
|
|
689
|
-
if (this.headerP === pending) {
|
|
690
|
-
this.headerP = undefined
|
|
691
|
-
this.headerSignal = undefined
|
|
692
|
-
}
|
|
693
|
-
},
|
|
658
|
+
private getParsedHeader(opts: Options = {}) {
|
|
659
|
+
return this.headerCache.get('header', opts.signal, signal =>
|
|
660
|
+
this.parseHeader({ ...opts, signal }),
|
|
694
661
|
)
|
|
695
|
-
return pending
|
|
696
662
|
}
|
|
697
663
|
|
|
698
664
|
/**
|