From 9ba9268817c1d4843610918241f48fe1bbfc2b82 Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Fri, 12 Sep 2025 13:38:20 -0700 Subject: [PATCH 1/2] Add subadapter + reflection pattern to ExtendedVariantAdapter in order to delete VCFTabixAdapter --- .../ExtendedVariantAdapter.ts | 64 ++++++- .../ExtendedVariantAdapter/VcfTabixAdapter.ts | 159 ------------------ 2 files changed, 61 insertions(+), 162 deletions(-) delete mode 100644 jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/VcfTabixAdapter.ts diff --git a/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts b/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts index d7973773b..2c1df2a9c 100644 --- a/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts +++ b/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts @@ -1,14 +1,57 @@ import QuickLRU from '@jbrowse/core/util/QuickLRU'; -import { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'; +import { BaseOptions, BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'; import { NoAssemblyRegion } from '@jbrowse/core/util/types'; import { ObservableCreate } from '@jbrowse/core/util/rxjs'; import { Feature } from '@jbrowse/core/util/simpleFeature'; import ExtendedVcfFeature from './ExtendedVcfFeature'; import { VcfFeature } from '@jbrowse/plugin-variants'; -import { default as VcfTabixAdapter } from './VcfTabixAdapter'; -export default class extends VcfTabixAdapter { +export default class extends BaseFeatureDataAdapter { protected featureCache = new QuickLRU({ maxSize: 20 }) + private subAdapterP?: Promise + + constructor(...args: any[]) { + super(...args) + + // Return a Proxy that forwards any unknown member access to the sub-adapter. + // This avoids re-implementing methods like getHeader/getRefNames/getMetadata/etc. + const self = this + return new Proxy(this, { + get(target, prop, receiver) { + // If we have it already (e.g., getFeatures, getFeaturesAsArray, BaseFeatureDataAdapter-derived properties), use it directly + if (prop in target || typeof prop === 'symbol') { + return Reflect.get(target, prop, receiver) + } + + // Otherwise, forward to the VcfTabixAdapter sub-adapter + return async (...callArgs: any[]) => { + const sub = await self.getVcfSubAdapter() + const value = (sub as any)[prop] + + // If it’s a method, call it; otherwise return the property value + if (typeof value === 'function') { + return value.apply(sub, callArgs) + } + return value + } + }, + }) + } + + private async getVcfSubAdapter(): Promise { + if (!this.subAdapterP) { + const vcfGzLocation = this.getConf('vcfGzLocation') + const index = this.getConf(['index']) + const vcfAdapterConf = { type: 'VcfTabixAdapter', vcfGzLocation, index } + this.subAdapterP = this.getSubAdapter!(vcfAdapterConf) + .then(({ dataAdapter }) => dataAdapter) + .catch(e => { + this.subAdapterP = undefined + throw e + }) + } + return this.subAdapterP + } public getFeatures(query: NoAssemblyRegion, opts: BaseOptions = {}) { return ObservableCreate(async observer => { @@ -50,4 +93,19 @@ export default class extends VcfTabixAdapter { return features } + + // Typescript errors at compile time without these stubs + async configure(opts?: BaseOptions) { + const sub = await this.getVcfSubAdapter() + return sub.configure(opts) + } + + async getRefNames(opts: BaseOptions = {}) { + const sub = await this.getVcfSubAdapter() + return sub.getRefNames(opts) + } + + freeResources(): void { + void this.getVcfSubAdapter().then(sub => sub.freeResources?.()) + } } \ No newline at end of file diff --git a/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/VcfTabixAdapter.ts b/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/VcfTabixAdapter.ts deleted file mode 100644 index fca6a3e64..000000000 --- a/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/VcfTabixAdapter.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { TabixIndexedFile } from '@gmod/tabix' -import VcfParser from '@gmod/vcf' -import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' -import { - fetchAndMaybeUnzipText, - updateStatus, -} from '@jbrowse/core/util' -import { openLocation } from '@jbrowse/core/util/io' -import { ObservableCreate } from '@jbrowse/core/util/rxjs' - -import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter' -import type { Feature } from '@jbrowse/core/util' -import type { NoAssemblyRegion } from '@jbrowse/core/util/types' -import { VcfFeature } from '@jbrowse/plugin-variants'; - -function shorten2(name: string, max = 70) { - return name.length > max ? `${name.slice(0, max)}...` : name -} - -export default class VcfTabixAdapter extends BaseFeatureDataAdapter { - private configured?: Promise<{ - vcf: TabixIndexedFile - parser: VcfParser - }> - - private async configurePre(_opts?: BaseOptions) { - const vcfGzLocation = this.getConf('vcfGzLocation') - const location = this.getConf(['index', 'location']) - const indexType = this.getConf(['index', 'indexType']) - - const filehandle = openLocation(vcfGzLocation, this.pluginManager) - const isCSI = indexType === 'CSI' - const vcf = new TabixIndexedFile({ - filehandle, - csiFilehandle: isCSI - ? openLocation(location, this.pluginManager) - : undefined, - tbiFilehandle: !isCSI - ? openLocation(location, this.pluginManager) - : undefined, - chunkCacheSize: 50 * 2 ** 20, - }) - - return { - vcf, - parser: new VcfParser({ - header: await vcf.getHeader(), - }), - } - } - - protected async configurePre2() { - if (!this.configured) { - this.configured = this.configurePre().catch((e: unknown) => { - this.configured = undefined - throw e - }) - } - return this.configured - } - - async configure(opts?: BaseOptions) { - const { statusCallback = () => {} } = opts || {} - return updateStatus('Downloading index', statusCallback, () => - this.configurePre2(), - ) - } - public async getRefNames(opts: BaseOptions = {}) { - const { vcf } = await this.configure(opts) - return vcf.getReferenceSequenceNames(opts) - } - - async getHeader(opts?: BaseOptions) { - const { vcf } = await this.configure(opts) - return vcf.getHeader() - } - - async getMetadata(opts?: BaseOptions) { - const { parser } = await this.configure(opts) - return parser.getMetadata() - } - - public getFeatures(query: NoAssemblyRegion, opts: BaseOptions = {}) { - return ObservableCreate(async observer => { - const { refName, start, end } = query - const { statusCallback = () => {} } = opts - const { vcf, parser } = await this.configure(opts) - - await updateStatus('Downloading variants', statusCallback, () => - vcf.getLines(refName, start, end, { - lineCallback: (line, fileOffset) => { - observer.next( - new VcfFeature({ - variant: parser.parseLine(line), - parser, - id: `${this.id}-vcf-${fileOffset}`, - }), - ) - }, - ...opts, - }), - ) - observer.complete() - }, opts.stopToken) - } - - async getSources() { - const conf = this.getConf('samplesTsvLocation') - if (conf.uri === '' || conf.uri === '/path/to/samples.tsv') { - const { parser } = await this.configure() - return parser.samples.map(name => ({ - name, - })) - } else { - const txt = await fetchAndMaybeUnzipText( - openLocation(conf, this.pluginManager), - ) - const lines = txt.split(/\n|\r\n|\r/) - const header = lines[0]!.split('\t') - const { parser } = await this.configure() - const metadataLines = lines - .slice(1) - .filter(f => !!f) - .map(line => { - const [name, ...rest] = line.split('\t') - return { - ...Object.fromEntries( - // force col 0 to be called name - rest.map((c, idx) => [header[idx + 1]!, c] as const), - ), - name: name!, - } - }) - const vcfSampleSet = new Set(parser.samples) - const metadataSet = new Set(metadataLines.map(r => r.name)) - const metadataNotInVcfSamples = [...metadataSet].filter( - f => !vcfSampleSet.has(f), - ) - const vcfSamplesNotInMetadata = [...vcfSampleSet].filter( - f => !metadataSet.has(f), - ) - if (metadataNotInVcfSamples.length) { - console.warn( - `There are ${metadataNotInVcfSamples.length} samples in metadata file (${metadataLines.length} lines) not in VCF (${parser.samples.length} samples):`, - shorten2(metadataNotInVcfSamples.join(',')), - ) - } - if (vcfSamplesNotInMetadata.length) { - console.warn( - `There are ${vcfSamplesNotInMetadata.length} samples in VCF file (${parser.samples.length} samples) not in metadata file (${metadataLines.length} lines):`, - shorten2(vcfSamplesNotInMetadata.map(m => m).join(',')), - ) - } - return metadataLines.filter(f => vcfSampleSet.has(f.name)) - } - } - - public freeResources(/* { region } */): void {} -} From ca085ad4b8a0af9c60441e30dc6f3c9227896c20 Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Tue, 16 Sep 2025 10:24:33 -0700 Subject: [PATCH 2/2] Add API wrapper for getMetadata and getHeader --- .../ExtendedVariantAdapter/ExtendedVariantAdapter.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts b/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts index 2c1df2a9c..035790c92 100644 --- a/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts +++ b/jbrowse/src/client/JBrowse/Browser/plugins/ExtendedVariantPlugin/ExtendedVariantAdapter/ExtendedVariantAdapter.ts @@ -105,6 +105,16 @@ export default class extends BaseFeatureDataAdapter { return sub.getRefNames(opts) } + async getHeader(opts?: BaseOptions) { + const sub = await this.getVcfSubAdapter() + return sub.getHeader(opts) + } + + async getMetadata(opts?: BaseOptions) { + const sub = await this.getVcfSubAdapter() + return sub.getMetadata(opts) + } + freeResources(): void { void this.getVcfSubAdapter().then(sub => sub.freeResources?.()) }