@gmod/bgzf-filehandle
Version:
read from a compressed bgzip file (with .gzi) as if it were uncompressed
84 lines • 3.57 kB
JavaScript
;
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