UNPKG

@gmod/bgzf-filehandle

Version:

read from a compressed bgzip file (with .gzi) as if it were uncompressed

84 lines 3.57 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const gziIndex_ts_1 = __importDefault(require("./gziIndex.js")); const unzip_ts_1 = require("./unzip.js"); const util_ts_1 = require("./util.js"); const DEFAULT_BLOCK_CONCURRENCY = 10; // BGZF blocks are bounded by a 16-bit BSIZE field in the gzip extra subfield — // the compressed size of a single block can never exceed 65 536 bytes. Used as // an upper bound for over-reading the trailing block whose end offset isn't // recorded in the gzi index, so we can drop the stat() dependency. const MAX_BGZF_BLOCK_SIZE = 1 << 16; // Small fixed-concurrency limiter to replace p-limit (which is pure ESM in // v7+ and breaks downstream Jest/CJS consumers). Tasks beyond the limit // queue until a slot frees. function createLimit(concurrency) { let active = 0; const queue = []; const next = () => { active--; queue.shift()?.(); }; return async (task) => { if (active >= concurrency) { await new Promise(resolve => { queue.push(resolve); }); } active++; try { return await task(); } finally { next(); } }; } function sliceBlock(uncompressedBuffer, blockStart, readStart, readEnd) { const sourceOffset = Math.max(0, readStart - blockStart); const sourceEnd = Math.min(readEnd, blockStart + uncompressedBuffer.length) - blockStart; return sourceOffset < uncompressedBuffer.length ? uncompressedBuffer.subarray(sourceOffset, sourceEnd) : new Uint8Array(0); } class BgzFilehandle { filehandle; gzi; limit; constructor({ filehandle, gziFilehandle, blockConcurrency = DEFAULT_BLOCK_CONCURRENCY, }) { this.filehandle = filehandle; this.gzi = new gziIndex_ts_1.default({ filehandle: gziFilehandle, }); this.limit = createLimit(blockConcurrency); } async _readAndUncompressBlock(compressedPosition, length) { const blockBuffer = await this.filehandle.read(length, compressedPosition); return (0, unzip_ts_1.unzip)(blockBuffer); } async read(length, position) { const { blocks, nextCompressedPosition } = await this.gzi.getRelevantBlocksForRead(length, position); if (blocks.length === 0) { return new Uint8Array(0); } const readEnd = position + length; const decompressed = await Promise.all(blocks.map(([compressedPos, uncompressedPos], i) => this.limit(async () => { // For the trailing block whose end isn't pinned by the next gzi entry // or the next-read-position hint, over-read by the max BGZF block // size. `compressedPos` is always a valid gzi offset (start within // file); the server clips the request to actual file size, and // unzip handles whatever bytes come back. const nextCompressed = blocks[i + 1]?.[0] ?? nextCompressedPosition ?? compressedPos + MAX_BGZF_BLOCK_SIZE; const buffer = await this._readAndUncompressBlock(compressedPos, nextCompressed - compressedPos); return sliceBlock(buffer, uncompressedPos, position, readEnd); }))); return (0, util_ts_1.concatUint8Array)(decompressed); } } exports.default = BgzFilehandle; //# sourceMappingURL=bgzFilehandle.js.map