npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gmod/tabix

v3.8.2

Published

Read Tabix-indexed files, supports both .tbi and .csi indexes

Readme

@gmod/tabix

NPM version Build Status

Read Tabix-indexed files using either .tbi or .csi indexes.

Install

npm install @gmod/tabix

Usage

import { TabixIndexedFile } from '@gmod/tabix'

// Local file — TBI index assumed at path + '.tbi'
const file = new TabixIndexedFile({ path: 'file.vcf.gz' })

// CSI index
const csi = new TabixIndexedFile({
  path: 'file.vcf.gz',
  csiPath: 'file.vcf.gz.csi',
})

// Remote files
const remote = new TabixIndexedFile({
  url: 'https://example.com/file.vcf.gz',
  tbiUrl: 'https://example.com/file.vcf.gz.tbi',
})

// Or with a filehandle from generic-filehandle2
import { RemoteFile } from 'generic-filehandle2'

const custom = new TabixIndexedFile({
  filehandle: new RemoteFile('https://example.com/file.vcf.gz'),
  tbiFilehandle: new RemoteFile('https://example.com/file.vcf.gz.tbi'),
})

Over HTTP, swapping in @gmod/range-cache-filehandle is usually worth it: a query reads the index and then a scattered set of BGZF blocks, and a byte-range cache coalesces those into one request per contiguous run and serves an overlapping query from memory.

import { RemoteFileWithRangeCache } from '@gmod/range-cache-filehandle'

const cached = new TabixIndexedFile({
  filehandle: new RemoteFileWithRangeCache('https://example.com/file.vcf.gz'),
  tbiFilehandle: new RemoteFileWithRangeCache(
    'https://example.com/file.vcf.gz.tbi',
  ),
})

getLines

Fetches lines overlapping a region. start/end are 0-based half-open coordinates (unlike the tabix CLI which uses 1-based closed).

const lines: string[] = []
await file.getLines('chr1', 200, 300, line => lines.push(line))

The callback also receives the virtual file offset and parsed coordinates for the line:

await file.getLines('chr1', 200, 300, (line, fileOffset, start, end) => {
  lines.push(line)
})

Pass an options object instead of a bare callback to abort the query or track download progress:

const aborter = new AbortController()
await file.getLines('chr1', 200, 300, {
  lineCallback: (line, fileOffset, start, end) => lines.push(line),
  signal: aborter.signal,
  onProgress: (bytesDownloaded, totalBytes) => {
    console.log(`${bytesDownloaded}/${totalBytes}`)
  },
})

onProgress ticks once per chunk — the run of BGZF blocks the index resolves a query to — including instant ticks for chunks already cached, and the index supplies totalBytes up front, which is enough for a determinate progress bar.

Notes:

  • The scan skips meta/comment lines
  • Line strings have no trailing whitespace
  • Pass undefined for end to read to the end of the contig
  • A refName that is not in the index yields no lines and no error, so a chr1/1 naming mismatch looks like an empty region. Check against getReferenceSequenceNames if a query comes back unexpectedly empty
  • start > end throws a TypeError; start === end returns without reading

Without NPM (CDN)

<script src="https://unpkg.com/@gmod/tabix/dist/tabix-bundle.js"></script>

See example/index.html for a working demo. It fetches the VCF over HTTP, so serve the directory (e.g. npx serve example) rather than opening the file directly.

How a query flows

getLines turns a region into BGZF chunks through the index and decompresses each one in wasm — index reads included, since .tbi and .csi are bgzipped too. The rest is ordinary JS: it matches lines as bytes and decodes only the ones you asked for. docs/dataflow.md has the diagram and walks it through.

The file then holds on to those decompressed chunks, so overlapping and adjacent queries reuse them instead of inflating again — up to 1GB per file, dropped after three idle minutes. A consumer holding one file per track should bound them together with a shared chunkCacheBudget rather than shrinking each file's own ceiling: docs/caching.md.

Decompressing on a worker pool

BGZF blocks inflate independently, so that decompression can spread across threads.

import { getSharedWorkerPool } from '@gmod/bgzf-filehandle'

const file = new TabixIndexedFile({
  url: 'https://example.com/yourfile.vcf.gz',
  // the pending promise is fine — it is awaited at the point of use
  bgzfWorkerPool: getSharedWorkerPool(),
})

Safe to pass unconditionally: getSharedWorkerPool() returns undefined under node, or anywhere the host forbids Workers, which keeps the in-process path. No cross-origin isolation needed. tabix-js never creates a pool on its own — the thread budget belongs to the consumer.

Worth about 1.4x here, against the 1.95x a BAM reader reports. Measured in jbrowse-components on test/data/1kg.chr1.subset.vcf.gz — 213MB of 1000 Genomes, headless Chrome, real HTTP, four workers, arms interleaved, both returning the same record count: 1.34-1.46x across five window sizes and a twelve-step pan.

The decompression itself moves 1.83x. What holds the end-to-end figure below that is a 28% floor of per-line byte scanning and string decoding, which no worker count reaches — and that floor is at its worst on multi-sample VCF, whose records carry a genotype field per sample and run to ~60KB a line. A format with narrower lines sits closer to BAM. If you want more than ~1.5x on a multi-sample VCF, the scan is what is left to attack, not the decompression.

Worker counts, lifecycle and benchmarks: bgzf-filehandle's worker pool docs; the end-to-end numbers above, and how to confirm a pool is really engaging in production rather than quietly falling back, are in jbrowse-components' BGZF_WORKER_POOL.md.

Docs

Academic Use

Written with NHGRI funding as part of JBrowse. If you use this in a publication, please cite the most recent JBrowse paper at jbrowse.org.

License

MIT © Robert Buels