UNPKG

@gmod/cram

Version:

read CRAM files with pure Javascript

138 lines 5.07 kB
"use strict"; 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, }); } function maybeUnzip(data) { return data[0] === 31 && data[1] === 139 ? (0, unzip_ts_1.unzip)(data) : data; } class CraiIndex { /** * * @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() { const index = {}; const uncompressedBuffer = maybeUnzip(await this.filehandle.readFile()); const dataView = new DataView(uncompressedBuffer.buffer); 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 Object.entries(index).forEach(([seqId, ent]) => { const e2 = ent; index[seqId] = e2.sort((a, b) => a.start - b.start || a.span - b.span); }); return index; } getIndex() { if (!this.parseIndexP) { this.parseIndexP = this.parseIndex().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) { const seqEntries = (await this.getIndex())[seqId]; if (!seqEntries) { return []; } const compare = (entry) => { const entryStart = entry.start; const entryEnd = entry.start + entry.span; if (entryStart > queryEnd) { return -1; } // entry is ahead of query if (entryEnd <= queryStart) { return 1; } // entry is behind query return 0; // entry overlaps query }; const bins = []; for (const entry of seqEntries) { if (compare(entry) === 0) { bins.push(entry); } } return bins; } } exports.default = CraiIndex; //# sourceMappingURL=craiIndex.js.map