@gmod/cram
Version:
read CRAM files with pure Javascript
136 lines • 5.21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const errors_ts_1 = require("./errors.js");
const io_ts_1 = require("./io.js");
const unzip_ts_1 = require("./unzip.js");
const BAI_MAGIC = 21_578_050; // BAI\1
function addRecordToIndex(index, record) {
const [seqId, start, span, containerStart, sliceStart, sliceBytes] = record;
const s = seqId;
if (!index[s]) {
index[s] = [];
}
index[s].push({
start: start,
span: span,
containerStart: containerStart,
sliceStart: sliceStart,
sliceBytes: sliceBytes,
});
}
async function maybeUnzip(data) {
return data[0] === 31 && data[1] === 139 ? (0, unzip_ts_1.unzip)(data) : data;
}
class CraiIndex {
// A CRAM index (.crai) is a gzipped tab delimited file containing the
// following columns:
//
// 1. Sequence id
// 2. Alignment start
// 3. Alignment span
// 4. Container start byte position in the file
// 5. Slice start byte position in the container data (‘blocks’)
// 6. Slice size in bytes
//
// Each line represents a slice in the CRAM file. Please note that all slices
// must be listed in index file.
parseIndexP;
filehandle;
/**
*
* @param {object} args
* @param {string} [args.path]
* @param {string} [args.url]
* @param {FileHandle} [args.filehandle]
*/
constructor(args) {
this.filehandle = (0, io_ts_1.open)(args.url, args.path, args.filehandle);
}
async parseIndex(opts = {}) {
const index = {};
const uncompressedBuffer = await maybeUnzip(await this.filehandle.readFile({
signal: opts.signal,
onProgress: opts.onProgress,
}));
const dataView = new DataView(uncompressedBuffer.buffer, uncompressedBuffer.byteOffset, uncompressedBuffer.byteLength);
if (uncompressedBuffer.length > 4 &&
dataView.getUint32(0, true) === BAI_MAGIC) {
throw new errors_ts_1.CramMalformedError('invalid .crai index file. note: file appears to be a .bai index. this is technically legal but please open a github issue if you need support');
}
// interpret the text as regular ascii, since it is supposed to be only
// digits and whitespace characters this is written in a deliberately
// low-level fashion for performance, because some .crai files can be
// pretty large.
let currentRecord = [];
let currentString = '';
for (const charCode of uncompressedBuffer) {
if ((charCode >= 48 && charCode <= 57) /* 0-9 */ ||
(!currentString && charCode === 45) /* leading - */) {
currentString += String.fromCharCode(charCode);
}
else if (charCode === 9 /* \t */) {
currentRecord.push(Number.parseInt(currentString, 10));
currentString = '';
}
else if (charCode === 10 /* \n */) {
currentRecord.push(Number.parseInt(currentString, 10));
currentString = '';
addRecordToIndex(index, currentRecord);
currentRecord = [];
}
else if (charCode !== 13 /* \r */ && charCode !== 32 /* space */) {
// if there are other characters in the file besides
// space and \r, something is wrong.
throw new errors_ts_1.CramMalformedError('invalid .crai index file');
}
}
// if the file ends without a \n, we need to flush our buffers
if (currentString) {
currentRecord.push(Number.parseInt(currentString, 10));
}
if (currentRecord.length === 6) {
addRecordToIndex(index, currentRecord);
}
// sort each of them by start
for (const ent of Object.values(index)) {
ent?.sort((a, b) => a.start - b.start || a.span - b.span);
}
return index;
}
getIndex(opts = {}) {
if (!this.parseIndexP) {
this.parseIndexP = this.parseIndex(opts).catch((e) => {
this.parseIndexP = undefined;
throw e;
});
}
return this.parseIndexP;
}
/**
* @param {number} seqId
* @returns {Promise} true if the index contains entries for
* the given reference sequence ID, false otherwise
*/
async hasDataForReferenceSequence(seqId) {
return !!(await this.getIndex())[seqId];
}
/**
* fetch index entries for the given range
*
* @param {number} seqId
* @param {number} queryStart
* @param {number} queryEnd
*
* @returns {Promise} promise for
* an array of objects of the form
* `{start, span, containerStart, sliceStart, sliceBytes }`
*/
async getEntriesForRange(seqId, queryStart, queryEnd, opts = {}) {
const seqEntries = (await this.getIndex(opts))[seqId];
return seqEntries
? seqEntries.filter(entry => entry.start <= queryEnd && entry.start + entry.span > queryStart)
: [];
}
}
exports.default = CraiIndex;
//# sourceMappingURL=craiIndex.js.map