UNPKG

@gmod/cram

Version:

read CRAM files with pure Javascript

899 lines (830 loc) 30.6 kB
import { CramArgumentError, CramBufferOverrunError, CramMalformedError, } from '../../errors.ts' import ByteArrayStopCodec from '../codecs/byteArrayStop.ts' import ExternalCodec, { batchDecodeItf8, parseItf8, } from '../codecs/external.ts' import Constants from '../constants.ts' import decodeRecord, { buildRFSchema } from './decodeRecord.ts' import { dataSeriesTypes } from '../container/compressionScheme.ts' import { type CramFileBlock } from '../file.ts' import CramRecord, { defaultDecodeOptions } from '../record.ts' import { getSectionParsers, isMappedSliceHeader } from '../sectionParsers.ts' import { decodeUtf8, parseItem, sequenceMD5 } from '../util.ts' import type { DecodeOptions } from '../record.ts' import type { MappedSliceHeader, UnmappedSliceHeader, } from '../sectionParsers.ts' import type { BoundDecoders, BulkByteRawDecoder } from './decodeRecord.ts' import type { Cursor, Cursors, PreDecodedIntBlock } from '../codecs/_base.ts' import type { DataSeriesEncodingKey } from '../codecs/dataSeriesTypes.ts' import type CramContainer from '../container/index.ts' import type { CramEncoding } from '../encoding.ts' import type CramFile from '../file.ts' // shared zero-length sentinel returned by bound tag decoders when length=0 const EMPTY_BYTES = new Uint8Array(0) export type SliceHeader = CramFileBlock & { parsedContent: MappedSliceHeader | UnmappedSliceHeader } interface RefRegion { id: number start: number end: number seq: string | null } /** * Try to estimate the template length from a bunch of interrelated * multi-segment reads. */ function calculateMultiSegmentMatedTemplateLength( allRecords: CramRecord[], _currentRecordNumber: number, thisRecord: CramRecord, ) { const matedRecords: CramRecord[] = [thisRecord] let cur = thisRecord while (cur.mateRecordNumber !== undefined && cur.mateRecordNumber >= 0) { const mateRecord = allRecords[cur.mateRecordNumber] if (!mateRecord) { throw new CramMalformedError( 'intra-slice mate record not found, this file seems malformed', ) } matedRecords.push(mateRecord) cur = mateRecord } let minStart = matedRecords[0]!.alignmentStart let maxEnd = minStart + matedRecords[0]!.readLength - 1 for (let i = 1; i < matedRecords.length; i++) { const r = matedRecords[i]! if (r.alignmentStart < minStart) { minStart = r.alignmentStart } const end = r.alignmentStart + r.readLength - 1 if (end > maxEnd) { maxEnd = end } } const estimatedTemplateLength = maxEnd - minStart + 1 if (estimatedTemplateLength >= 0) { matedRecords.forEach(r => { if (r.templateLength !== undefined) { throw new CramMalformedError( 'mate pair group has some members that have template lengths already, this file seems malformed', ) } // sign per SAM spec: positive for leftmost, negative for rightmost r.templateLength = r.alignmentStart === minStart ? estimatedTemplateLength : -estimatedTemplateLength }) } } /** * Attempt to calculate the `templateLength` for a pair of intra-slice paired * reads. Ported from htslib. Algorithm is imperfect. */ function calculateIntraSliceMatePairTemplateLength( thisRecord: CramRecord, mateRecord: CramRecord, ) { // this just estimates the template length by using the simple (non-gapped) // end coordinate of each read, because gapping in the alignment doesn't mean // the template is longer or shorter const start = Math.min(thisRecord.alignmentStart, mateRecord.alignmentStart) const end = Math.max( thisRecord.alignmentStart + thisRecord.readLength - 1, mateRecord.alignmentStart + mateRecord.readLength - 1, ) const lengthEstimate = end - start + 1 // sign per SAM spec: positive for leftmost, negative for rightmost thisRecord.templateLength = thisRecord.alignmentStart <= mateRecord.alignmentStart ? lengthEstimate : -lengthEstimate mateRecord.templateLength = mateRecord.alignmentStart <= thisRecord.alignmentStart ? lengthEstimate : -lengthEstimate } /** * establishes a mate-pair relationship between two records in the same slice. * CRAM compresses mate-pair relationships between records in the same slice * down into just one record having the index in the slice of its mate */ function associateIntraSliceMate( allRecords: CramRecord[], currentRecordNumber: number, thisRecord: CramRecord, mateRecord: CramRecord, ) { const complicatedMultiSegment = !!( mateRecord.mate || (mateRecord.mateRecordNumber !== undefined && mateRecord.mateRecordNumber !== currentRecordNumber) ) // Deal with lossy read names — assign a synthetic name from uniqueId // so that paired records share the same name if (!thisRecord.readName) { const syntheticName = String(thisRecord.uniqueId) thisRecord._syntheticReadName = syntheticName mateRecord._syntheticReadName = syntheticName } thisRecord.mate = { sequenceId: mateRecord.sequenceId, alignmentStart: mateRecord.alignmentStart, uniqueId: mateRecord.uniqueId, } if (mateRecord.readName) { thisRecord.mate.readName = mateRecord.readName } // the mate record might have its own mate pointer, if this is some kind of // multi-segment (more than paired) scheme, so only relate that one back to this one // if it does not have any other relationship if (!mateRecord.mate && mateRecord.mateRecordNumber === undefined) { mateRecord.mate = { sequenceId: thisRecord.sequenceId, alignmentStart: thisRecord.alignmentStart, uniqueId: thisRecord.uniqueId, } if (thisRecord.readName) { mateRecord.mate.readName = thisRecord.readName } } // make sure the proper flags and cramFlags are set on both records // paired thisRecord.flags |= Constants.BAM_FPAIRED // set mate unmapped if needed if (mateRecord.flags & Constants.BAM_FUNMAP) { thisRecord.flags |= Constants.BAM_FMUNMAP // thisRecord.templateLength = 0 } if (thisRecord.flags & Constants.BAM_FUNMAP) { // thisRecord.templateLength = 0 mateRecord.flags |= Constants.BAM_FMUNMAP } // set mate reversed if needed if (mateRecord.flags & Constants.BAM_FREVERSE) { thisRecord.flags |= Constants.BAM_FMREVERSE } if (thisRecord.flags & Constants.BAM_FREVERSE) { mateRecord.flags |= Constants.BAM_FMREVERSE } if (thisRecord.templateLength === undefined) { if (complicatedMultiSegment) { calculateMultiSegmentMatedTemplateLength( allRecords, currentRecordNumber, thisRecord, ) } else { calculateIntraSliceMatePairTemplateLength(thisRecord, mateRecord) } } // delete this last because it's used by the // complicated template length estimation thisRecord.mateRecordNumber = undefined } export default class CramSlice { private file: CramFile container: CramContainer containerPosition: number sliceSize: number private _headerResult?: ReturnType<CramSlice['_fetchHeader']> private _blocksResult?: ReturnType<CramSlice['_fetchBlocks']> private _blocksContentIdIndexResult?: ReturnType< CramSlice['_fetchBlocksContentIdIndex'] > constructor( container: CramContainer, containerPosition: number, sliceSize: number, ) { this.file = container.file this.container = container this.containerPosition = containerPosition this.sliceSize = sliceSize } getHeader() { if (this._headerResult === undefined) { this._headerResult = this._fetchHeader() this._headerResult.catch(() => { this._headerResult = undefined }) } return this._headerResult } private async _fetchHeader() { // fetch and parse the slice header const { majorVersion } = await this.file.getDefinition() const sectionParsers = getSectionParsers(majorVersion) const containerHeader = await this.container.getHeader() const header = await this.file.readBlock( containerHeader._endPosition + this.containerPosition, ) const parser = header.contentType === 'MAPPED_SLICE_HEADER' ? sectionParsers.cramMappedSliceHeader.parser : header.contentType === 'UNMAPPED_SLICE_HEADER' ? sectionParsers.cramUnmappedSliceHeader.parser : undefined if (parser) { const content = parseItem( header.content, parser, 0, containerHeader._endPosition, ) return { ...header, parsedContent: content } } else { throw new CramMalformedError( `error reading slice header block, invalid content type ${header.contentType}`, ) } } getBlocks() { if (this._blocksResult === undefined) { this._blocksResult = this._fetchBlocks() this._blocksResult.catch(() => { this._blocksResult = undefined }) } return this._blocksResult } private async _fetchBlocks() { const header = await this.getHeader() if (this.sliceSize) { // if we know the slice size (from the index), do one big read for all // blocks and parse from the in-memory buffer const containerHeader = await this.container.getHeader() const sliceFilePosition = containerHeader._endPosition + this.containerPosition const blocksFilePosition = header._endPosition const headerSize = blocksFilePosition - sliceFilePosition const remainingBytes = this.sliceSize - headerSize const allBlocksBuffer = await this.file.read( remainingBytes, blocksFilePosition, ) const blocks: CramFileBlock[] = new Array(header.parsedContent.numBlocks) let bufferOffset = 0 for (let i = 0; i < blocks.length; i++) { const block = await this.file.readBlockFromBuffer( allBlocksBuffer, bufferOffset, blocksFilePosition + bufferOffset, ) blocks[i] = block bufferOffset = block._endPosition - blocksFilePosition } return blocks } // fallback: read blocks one at a time (non-indexed access) let blockPosition = header._endPosition const blocks: CramFileBlock[] = new Array(header.parsedContent.numBlocks) for (let i = 0; i < blocks.length; i++) { const block = await this.file.readBlock(blockPosition) blocks[i] = block blockPosition = block._endPosition } return blocks } // no memoize async getCoreDataBlock() { const blocks = await this.getBlocks() return blocks[0] } _getBlocksContentIdIndex() { if (this._blocksContentIdIndexResult === undefined) { this._blocksContentIdIndexResult = this._fetchBlocksContentIdIndex() this._blocksContentIdIndexResult.catch(() => { this._blocksContentIdIndexResult = undefined }) } return this._blocksContentIdIndexResult } private async _fetchBlocksContentIdIndex(): Promise< Record<number, CramFileBlock> > { const blocks = await this.getBlocks() const blocksByContentId: Record<number, CramFileBlock> = {} blocks.forEach(block => { if (block.contentType === 'EXTERNAL_DATA') { blocksByContentId[block.contentId] = block } }) return blocksByContentId } async getBlockByContentId(id: number) { const blocksByContentId = await this._getBlocksContentIdIndex() return blocksByContentId[id] } async getReferenceRegion() { // read the slice header const sliceHeader = (await this.getHeader()).parsedContent if (!isMappedSliceHeader(sliceHeader)) { throw new Error('slice header not mapped') } if (sliceHeader.refSeqId < 0) { return undefined } const compressionScheme = await this.container.getCompressionScheme() if (compressionScheme === undefined) { throw new Error('compression scheme undefined') } if (sliceHeader.refBaseBlockId >= 0) { const refBlock = await this.getBlockByContentId( sliceHeader.refBaseBlockId, ) if (!refBlock) { throw new CramMalformedError( 'embedded reference specified, but reference block does not exist', ) } // TODO: we do not read anything named 'span' // if (sliceHeader.span > refBlock.uncompressedSize) { // throw new CramMalformedError('Embedded reference is too small') // } // TODO verify return { seq: decodeUtf8(refBlock.content), start: sliceHeader.refSeqStart, end: sliceHeader.refSeqStart + sliceHeader.refSeqSpan - 1, span: sliceHeader.refSeqSpan, } } if ( compressionScheme.referenceRequired || this.file.fetchReferenceSequenceCallback ) { if (!this.file.fetchReferenceSequenceCallback) { throw new Error( 'reference sequence not embedded, and seqFetch callback not provided, cannot fetch reference sequence', ) } const seq = await this.file.fetchReferenceSequenceCallback( sliceHeader.refSeqId, sliceHeader.refSeqStart, sliceHeader.refSeqStart + sliceHeader.refSeqSpan - 1, ) if (seq.length !== sliceHeader.refSeqSpan) { throw new CramArgumentError( 'seqFetch callback returned a reference sequence of the wrong length', ) } return { seq, start: sliceHeader.refSeqStart, end: sliceHeader.refSeqStart + sliceHeader.refSeqSpan - 1, span: sliceHeader.refSeqSpan, } } return undefined } getAllRecords() { return this.getRecords(() => true) } async _fetchRecords(decodeOptions: Required<DecodeOptions>) { const { majorVersion } = await this.file.getDefinition() const compressionScheme = await this.container.getCompressionScheme() if (compressionScheme === undefined) { throw new Error('compression scheme undefined') } const sliceHeader = await this.getHeader() const blocksByContentId = await this._getBlocksContentIdIndex() // check MD5 of reference if available if ( majorVersion > 1 && this.file.options.checkSequenceMD5 && isMappedSliceHeader(sliceHeader.parsedContent) && sliceHeader.parsedContent.refSeqId >= 0 && sliceHeader.parsedContent.md5?.join('') !== '0000000000000000' ) { const refRegion = await this.getReferenceRegion() if (refRegion) { const { seq, start, end } = refRegion const seqMd5 = sequenceMD5(seq) const storedMd5 = sliceHeader.parsedContent.md5 ?.map(byte => (byte < 16 ? '0' : '') + byte.toString(16)) .join('') if (seqMd5 !== storedMd5) { throw new CramMalformedError( `MD5 checksum reference mismatch for ref ${sliceHeader.parsedContent.refSeqId} pos ${start}..${end}. recorded MD5: ${storedMd5}, calculated MD5: ${seqMd5}`, ) } } } // tracks the read position within the block. codec.decode() methods // advance the byte and bit positions in the cursor as they decode // data note that we are only decoding a single block here, the core // data block const coreDataBlock = await this.getCoreDataBlock() const externalCursorMap = new Map<number, Cursor>() const cursors: Cursors = { lastAlignmentStart: isMappedSliceHeader(sliceHeader.parsedContent) ? sliceHeader.parsedContent.refSeqStart : 0, coreBlock: { bitPosition: 7, bytePosition: 0 }, externalBlocks: { getCursor(contentId: number) { let r = externalCursorMap.get(contentId) if (r === undefined) { r = { bitPosition: 7, bytePosition: 0 } externalCursorMap.set(contentId, r) } return r }, }, } // Pre-decode external int blocks: batch ITF8 decode via WASM so that // ExternalCodec.decode() becomes a simple array index read. // A block can only be pre-decoded if ALL accessors use int type. // If any byte-type accessor shares the same block, skip it. const externalIntBlockIds = new Set<number>() const externalByteBlockIds = new Set<number>() // Recurse through codec encodings to find which external block IDs are // used as int vs byte. codecId 1 = EXTERNAL, 4 = BYTE_ARRAY_LENGTH // (whose lengths sub-codec is int, values sub-codec is byte), // 5 = BYTE_ARRAY_STOP (always byte). function collectExternalBlockIds( enc: CramEncoding | undefined, isInt: boolean, ) { if (!enc) { return } if (enc.codecId === 1) { if (isInt) { externalIntBlockIds.add(enc.parameters.blockContentId) } else { externalByteBlockIds.add(enc.parameters.blockContentId) } } else if (enc.codecId === 4) { collectExternalBlockIds(enc.parameters.lengthsEncoding, true) collectExternalBlockIds(enc.parameters.valuesEncoding, false) } else if (enc.codecId === 5) { externalByteBlockIds.add(enc.parameters.blockContentId) } } for (const [ds, enc] of Object.entries( compressionScheme.dataSeriesEncoding, )) { const dsType = dataSeriesTypes[ds as keyof typeof dataSeriesTypes] collectExternalBlockIds(enc, dsType === 'int') } for (const tagEnc of Object.values(compressionScheme.tagEncoding)) { collectExternalBlockIds(tagEnc, false) } // Remove any int block that is also used as byte for (const id of externalByteBlockIds) { externalIntBlockIds.delete(id) } const preDecodedIntBlocks = new Map<number, PreDecodedIntBlock>() for (const contentId of externalIntBlockIds) { const block = blocksByContentId[contentId] if (block?.content.length) { const values = batchDecodeItf8(block.content) preDecodedIntBlocks.set(contentId, { values, index: 0 }) } } cursors.preDecodedIntBlocks = preDecodedIntBlocks // Build bound decode functions per data series. For ExternalCodec this // captures the content buffer and cursor directly, eliminating per-call // Record/Map lookup overhead. The bound decoders are assembled into a // single object literal with all data series present so V8 sees a stable // hidden class — call sites in decodeRecord then become direct property // accesses with monomorphic inline caches. const bind = (dataSeriesName: string) => { const codec = compressionScheme.getCodecForDataSeries( dataSeriesName as DataSeriesEncodingKey, ) if (!codec) { return () => { throw new CramMalformedError( `no codec defined for ${dataSeriesName} data series`, ) } } if (codec instanceof ExternalCodec) { const bid = codec.parameters.blockContentId const preDecoded = preDecodedIntBlocks.get(bid) if (preDecoded) { const { values } = preDecoded return () => values[preDecoded.index++] } const contentBlock = blocksByContentId[bid] if (!contentBlock) { return () => { throw new CramMalformedError( `no block found with content ID ${bid}`, ) } } const cursor = cursors.externalBlocks.getCursor(bid) const content = contentBlock.content if (codec.dataType === 'int') { return () => parseItf8(content, cursor) } // Mirror the bounds check in ExternalCodec.decode — without it, // a truncated/corrupt block silently yields `undefined` for byte // reads, which downstream propagates as NaN/0 (silent data // corruption) rather than a clear error. return () => { if (cursor.bytePosition >= content.length) { throw new CramBufferOverrunError( 'attempted to read beyond end of block. this file seems truncated.', ) } return content[cursor.bytePosition++] } } if (codec instanceof ByteArrayStopCodec) { const { blockContentId, stopByte } = codec.parameters const contentBlock = blocksByContentId[blockContentId] if (!contentBlock) { return () => { throw new CramMalformedError( `no block found with content ID ${blockContentId}`, ) } } const content = contentBlock.content const cursor = cursors.externalBlocks.getCursor(blockContentId) return () => { const start = cursor.bytePosition const len = content.length let pos = start while (pos < len && content[pos] !== stopByte) { pos++ } if (pos >= len) { throw new CramBufferOverrunError( 'byteArrayStop reading beyond length of data buffer?', ) } cursor.bytePosition = pos + 1 return content.subarray(start, pos) } } return () => codec.decode(this, coreDataBlock!, blocksByContentId, cursors) } const bd: BoundDecoders = { BF: bind('BF'), CF: bind('CF'), RI: bind('RI'), RL: bind('RL'), AP: bind('AP'), RG: bind('RG'), RN: bind('RN'), MF: bind('MF'), NS: bind('NS'), NP: bind('NP'), TS: bind('TS'), NF: bind('NF'), TL: bind('TL'), FN: bind('FN'), FC: bind('FC'), FP: bind('FP'), DL: bind('DL'), BB: bind('BB'), QQ: bind('QQ'), BS: bind('BS'), IN: bind('IN'), RS: bind('RS'), PD: bind('PD'), HC: bind('HC'), SC: bind('SC'), MQ: bind('MQ'), BA: bind('BA'), QS: bind('QS'), TC: bind('TC'), TN: bind('TN'), } as BoundDecoders // Bulk byte decoder for QS and BA — getBytesSubarray returns a subarray // view when the codec supports it (e.g. ExternalCodec), or undefined otherwise const qsCodec = compressionScheme.getCodecForDataSeries('QS') const baCodec = compressionScheme.getCodecForDataSeries('BA') const decodeBulkBytesRaw: BulkByteRawDecoder | undefined = qsCodec || baCodec ? (dataSeriesName, length) => { const codec = dataSeriesName === 'QS' ? qsCodec : baCodec return codec?.getBytesSubarray(blocksByContentId, cursors, length) } : undefined // Bound tag decoders — tags are typically encoded as byteArrayLength // (codecId=4) wrapping External-int lengths + External-byte values. We // build a fast closure per tagId that inlines the length read and value // subarray, eliminating per-call dispatch through ByteArrayLengthCodec // and the inner codecs. Other encodings fall back to the generic dispatch. const boundTagDecoders: Record< string, () => Uint8Array | number | undefined > = {} const bindTagFallback = (tagId: string) => { const codec = compressionScheme.getCodecForTag(tagId) return () => codec.decode(this, coreDataBlock!, blocksByContentId, cursors) } for (const tagId of Object.keys(compressionScheme.tagEncoding)) { const enc = compressionScheme.tagEncoding[tagId]! if ( enc.codecId === 4 && enc.parameters.lengthsEncoding.codecId === 1 && enc.parameters.valuesEncoding.codecId === 1 ) { const lenBid = enc.parameters.lengthsEncoding.parameters.blockContentId const valBid = enc.parameters.valuesEncoding.parameters.blockContentId const lenContentBlock = blocksByContentId[lenBid] const valContentBlock = blocksByContentId[valBid] if (!lenContentBlock || !valContentBlock) { boundTagDecoders[tagId] = bindTagFallback(tagId) continue } const valContent = valContentBlock.content const valCursor = cursors.externalBlocks.getCursor(valBid) const lenPreDecoded = preDecodedIntBlocks.get(lenBid) const lenContent = lenContentBlock.content const lenCursor = cursors.externalBlocks.getCursor(lenBid) const readTagLen = lenPreDecoded ? () => lenPreDecoded.values[lenPreDecoded.index++]! : () => parseItf8(lenContent, lenCursor) boundTagDecoders[tagId] = () => { const length = readTagLen() if (length === 0) { return EMPTY_BYTES } const start = valCursor.bytePosition const end = start + length if (end > valContent.length) { throw new CramBufferOverrunError( 'attempted to read beyond end of block. this file seems truncated.', ) } valCursor.bytePosition = end return valContent.subarray(start, end) } } else { boundTagDecoders[tagId] = bindTagFallback(tagId) } } const records: CramRecord[] = new Array( sliceHeader.parsedContent.numRecords, ) const rfSchema = buildRFSchema(bd, majorVersion) for (let i = 0; i < records.length; i += 1) { try { records[i] = new CramRecord( decodeRecord( this, bd, rfSchema, boundTagDecoders, compressionScheme, sliceHeader, coreDataBlock!, blocksByContentId, cursors, majorVersion, i, sliceHeader.contentPosition + sliceHeader.parsedContent.recordCounter + i + 1, decodeOptions, decodeBulkBytesRaw, ), ) } catch (e) { const err = e as { code?: string; message?: string } if (err.code === 'CRAM_BUFFER_OVERRUN') { const recordsDecoded = i const recordsExpected = sliceHeader.parsedContent.numRecords throw new CramMalformedError( `Failed to decode all records in slice. Decoded ${recordsDecoded} of ${recordsExpected} expected records. ` + `Buffer overrun suggests either: (1) file is truncated/corrupted, (2) compression scheme is incorrect, ` + `or (3) there's a bug in the decoder. Original error: ${err.message}`, ) } else { throw e } } } // interpret `recordsToNextFragment` attributes to make standard `mate` // objects. The records loop above fills every slot or throws — by the // time we get here, records[i] is always defined. The records[mate] // guard protects against malformed mateRecordNumber pointing past the // slice. for (let i = 0; i < records.length; i += 1) { const r = records[i]! const { mateRecordNumber } = r if ( mateRecordNumber !== undefined && mateRecordNumber >= 0 && records[mateRecordNumber] ) { associateIntraSliceMate(records, i, r, records[mateRecordNumber]) } } return records } async getRecords( filterFunction: (r: CramRecord) => boolean, decodeOptions?: DecodeOptions, ) { // Merge with defaults const opts = { ...defaultDecodeOptions, ...decodeOptions } // fetch the features if necessary, using the file-level feature cache // Include decode options in cache key so different decode configs are cached separately const optionsKey = `${opts.decodeTags ? 1 : 0}` const cacheKey = `${this.container.filePosition}:${this.containerPosition}:${optionsKey}` let recordsPromise = this.file.featureCache.get(cacheKey) if (!recordsPromise) { recordsPromise = this._fetchRecords(opts) this.file.featureCache.set(cacheKey, recordsPromise) } const unfiltered = await recordsPromise const records = unfiltered.filter(filterFunction) // if we can fetch reference sequence, add the reference sequence to the records if (records.length && this.file.fetchReferenceSequenceCallback) { const sliceHeader = await this.getHeader() if ( isMappedSliceHeader(sliceHeader.parsedContent) && (sliceHeader.parsedContent.refSeqId >= 0 || // single-ref slice sliceHeader.parsedContent.refSeqId === -2) // multi-ref slice ) { const singleRefId = sliceHeader.parsedContent.refSeqId >= 0 ? sliceHeader.parsedContent.refSeqId : undefined const compressionScheme = await this.container.getCompressionScheme() if (compressionScheme === undefined) { throw new Error('compression scheme undefined') } const refRegions: Record<string, RefRegion> = {} // iterate over the records to find the spans of the reference // sequences we need to fetch for (const record of records) { const seqId = singleRefId !== undefined ? singleRefId : record.sequenceId let refRegion = refRegions[seqId] if (!refRegion) { refRegion = { id: seqId, start: record.alignmentStart, end: Number.NEGATIVE_INFINITY, seq: null, } refRegions[seqId] = refRegion } const end = record.alignmentStart + (record.lengthOnRef || record.readLength) - 1 if (end > refRegion.end) { refRegion.end = end } if (record.alignmentStart < refRegion.start) { refRegion.start = record.alignmentStart } } // fetch the `seq` for all of the ref regions await Promise.all( Object.values(refRegions).map(async refRegion => { if ( refRegion.id !== -1 && refRegion.start <= refRegion.end && this.file.fetchReferenceSequenceCallback ) { refRegion.seq = await this.file.fetchReferenceSequenceCallback( refRegion.id, refRegion.start, refRegion.end, ) } }), ) // now decorate all the records with them for (const record of records) { const seqId = singleRefId !== undefined ? singleRefId : record.sequenceId const refRegion = refRegions[seqId] if (refRegion?.seq) { const seq = refRegion.seq record.addReferenceSequence( { ...refRegion, seq }, compressionScheme, ) } } } } return records } }