jbrowse-plugin-msaview 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/LaunchMsaView/cleanProteinSequence.test.d.ts +1 -0
- package/dist/LaunchMsaView/cleanProteinSequence.test.js +77 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -32
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +10 -15
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +14 -13
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +25 -22
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +14 -19
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +18 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.js +13 -0
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +19 -14
- package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
- package/dist/LaunchMsaView/components/SubmitCancelActions.js +10 -7
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +20 -2
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +8 -1
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +25 -20
- package/dist/LaunchMsaView/components/launchConnectedView.d.ts +23 -0
- package/dist/LaunchMsaView/components/launchConnectedView.js +39 -0
- package/dist/LaunchMsaView/detectQueryRow.js +4 -7
- package/dist/LaunchMsaView/util.d.ts +6 -0
- package/dist/LaunchMsaView/util.js +7 -1
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +12 -3
- package/dist/MsaViewPanel/applyRegion.d.ts +23 -0
- package/dist/MsaViewPanel/applyRegion.js +19 -0
- package/dist/MsaViewPanel/applyRegion.test.d.ts +1 -0
- package/dist/MsaViewPanel/applyRegion.test.js +45 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +2 -1
- package/dist/MsaViewPanel/doLaunchBlast.test.js +12 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
- package/dist/MsaViewPanel/genomeToMSA.js +3 -3
- package/dist/MsaViewPanel/genomeToMSA.test.js +69 -23
- package/dist/MsaViewPanel/launchAutoruns.test.d.ts +1 -0
- package/dist/MsaViewPanel/launchAutoruns.test.js +36 -0
- package/dist/MsaViewPanel/model.d.ts +18 -14
- package/dist/MsaViewPanel/model.js +17 -0
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +3 -12
- package/dist/MsaViewPanel/msaDataStore.test.js +8 -1
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +4 -4
- package/dist/MsaViewPanel/regionSnapshot.test.d.ts +1 -0
- package/dist/MsaViewPanel/regionSnapshot.test.js +29 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +41 -4
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +103 -68
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.d.ts +1 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +149 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +8 -4
- package/dist/MsaViewPanel/runLaunch.js +15 -4
- package/dist/MsaViewPanel/runLaunch.test.js +16 -0
- package/dist/MsaViewPanel/storedData.test.js +23 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +3 -2
- package/dist/MsaViewPanel/util.d.ts +23 -23
- package/dist/MsaViewPanel/util.js +23 -24
- package/dist/index.js +17 -10
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +36 -36
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +26 -2
- package/dist/utils/blastCache.js +51 -39
- package/dist/utils/blastCache.test.js +46 -19
- package/dist/utils/domainCache.d.ts +1 -1
- package/dist/utils/domainCache.js +18 -14
- package/dist/utils/ebiJobDispatcher.d.ts +11 -0
- package/dist/utils/ebiJobDispatcher.js +12 -13
- package/dist/utils/ebiJobDispatcher.test.js +17 -3
- package/dist/utils/eutils.d.ts +14 -0
- package/dist/utils/eutils.js +48 -14
- package/dist/utils/eutils.test.d.ts +1 -0
- package/dist/utils/eutils.test.js +31 -0
- package/dist/utils/idb.d.ts +16 -3
- package/dist/utils/idb.js +51 -5
- package/dist/utils/idb.test.d.ts +1 -0
- package/dist/utils/idb.test.js +78 -0
- package/dist/utils/ncbiDomains.js +4 -4
- package/dist/utils/ncbiOrthologs.d.ts +1 -1
- package/dist/utils/ncbiOrthologs.js +18 -17
- package/dist/utils/ncbiOrthologs.test.js +17 -1
- package/dist/utils/ncbiTaxonomy.js +14 -15
- package/dist/utils/taxonomyNames.js +23 -19
- package/dist/utils/taxonomyNames.test.d.ts +1 -0
- package/dist/utils/taxonomyNames.test.js +33 -0
- package/dist/utils/unirefHomologs.js +2 -2
- package/dist/utils/unirefHomologs.test.js +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -9
- package/src/LaunchMsaView/cleanProteinSequence.test.ts +91 -0
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +35 -32
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +10 -14
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +16 -15
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +34 -21
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +18 -21
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.ts +16 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +24 -2
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +19 -13
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +27 -2
- package/src/LaunchMsaView/components/SubmitCancelActions.tsx +12 -7
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +28 -18
- package/src/LaunchMsaView/components/launchConnectedView.ts +56 -0
- package/src/LaunchMsaView/detectQueryRow.ts +5 -7
- package/src/LaunchMsaView/util.ts +7 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +16 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +28 -1
- package/src/MsaViewPanel/afterCreateAutoruns.ts +12 -3
- package/src/MsaViewPanel/applyRegion.test.ts +55 -0
- package/src/MsaViewPanel/applyRegion.ts +33 -0
- package/src/MsaViewPanel/doLaunchBlast.test.ts +20 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +2 -1
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +1 -1
- package/src/MsaViewPanel/genomeToMSA.test.ts +75 -23
- package/src/MsaViewPanel/genomeToMSA.ts +3 -2
- package/src/MsaViewPanel/launchAutoruns.test.ts +45 -0
- package/src/MsaViewPanel/model.ts +22 -0
- package/src/MsaViewPanel/msaDataStore.test.ts +14 -2
- package/src/MsaViewPanel/msaDataStore.ts +4 -12
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +4 -4
- package/src/MsaViewPanel/regionSnapshot.test.ts +34 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +183 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +154 -71
- package/src/MsaViewPanel/runLaunch.test.ts +19 -0
- package/src/MsaViewPanel/runLaunch.ts +21 -4
- package/src/MsaViewPanel/storedData.test.ts +32 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +3 -2
- package/src/MsaViewPanel/util.ts +38 -29
- package/src/index.ts +19 -10
- package/src/utils/blastCache.test.ts +59 -20
- package/src/utils/blastCache.ts +62 -39
- package/src/utils/domainCache.ts +27 -15
- package/src/utils/ebiJobDispatcher.test.ts +18 -3
- package/src/utils/ebiJobDispatcher.ts +12 -16
- package/src/utils/eutils.test.ts +39 -0
- package/src/utils/eutils.ts +64 -14
- package/src/utils/idb.test.ts +106 -0
- package/src/utils/idb.ts +57 -5
- package/src/utils/ncbiDomains.ts +4 -4
- package/src/utils/ncbiOrthologs.test.ts +20 -0
- package/src/utils/ncbiOrthologs.ts +26 -28
- package/src/utils/ncbiTaxonomy.ts +17 -20
- package/src/utils/taxonomyNames.test.ts +41 -0
- package/src/utils/taxonomyNames.ts +33 -19
- package/src/utils/unirefHomologs.test.ts +11 -1
- package/src/utils/unirefHomologs.ts +2 -2
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.d.ts +0 -16
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +0 -31
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +0 -16
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +0 -16
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +0 -9
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +0 -15
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +0 -13
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +0 -22
- package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +0 -59
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +0 -43
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +0 -31
- package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +0 -42
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { bestEffort, createDbOpener } from './idb';
|
|
3
|
+
const { opens } = vi.hoisted(() => ({ opens: [] }));
|
|
4
|
+
vi.mock('idb', () => ({
|
|
5
|
+
openDB: (_name, _version, callbacks) => new Promise((resolve, reject) => {
|
|
6
|
+
opens.push({ callbacks, resolve, reject });
|
|
7
|
+
}),
|
|
8
|
+
}));
|
|
9
|
+
function fakeDb() {
|
|
10
|
+
return { close: vi.fn() };
|
|
11
|
+
}
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
opens.length = 0;
|
|
14
|
+
});
|
|
15
|
+
describe('createDbOpener', () => {
|
|
16
|
+
test('shares one connection between callers', async () => {
|
|
17
|
+
const getDB = createDbOpener('db', 1, () => { });
|
|
18
|
+
const first = getDB();
|
|
19
|
+
const second = getDB();
|
|
20
|
+
const db = fakeDb();
|
|
21
|
+
opens[0].resolve(db);
|
|
22
|
+
expect(await first).toBe(db);
|
|
23
|
+
expect(await second).toBe(db);
|
|
24
|
+
expect(opens.length).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
test('a failed open is retried by the next caller', async () => {
|
|
27
|
+
const getDB = createDbOpener('db', 1, () => { });
|
|
28
|
+
const first = getDB();
|
|
29
|
+
opens[0].reject(new Error('private mode'));
|
|
30
|
+
await expect(first).rejects.toThrow('private mode');
|
|
31
|
+
void getDB();
|
|
32
|
+
expect(opens.length).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
test('an open blocked by another tab rejects instead of waiting on it', async () => {
|
|
35
|
+
const getDB = createDbOpener('db', 2, () => { });
|
|
36
|
+
const first = getDB();
|
|
37
|
+
opens[0].callbacks.blocked(1, 2, {});
|
|
38
|
+
await expect(first).rejects.toThrow(/held open at version 1/);
|
|
39
|
+
// the upgrade goes through once the other tab lets go, and the connection
|
|
40
|
+
// nobody is waiting for any more is closed rather than leaked
|
|
41
|
+
const late = fakeDb();
|
|
42
|
+
opens[0].resolve(late);
|
|
43
|
+
await Promise.resolve();
|
|
44
|
+
expect(late.close).toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
test('a connection blocking another tab closes and is reopened on next use', async () => {
|
|
47
|
+
const getDB = createDbOpener('db', 1, () => { });
|
|
48
|
+
const pending = getDB();
|
|
49
|
+
const db = fakeDb();
|
|
50
|
+
opens[0].resolve(db);
|
|
51
|
+
await pending;
|
|
52
|
+
const target = { close: vi.fn() };
|
|
53
|
+
opens[0].callbacks.blocking(1, 2, {
|
|
54
|
+
target,
|
|
55
|
+
});
|
|
56
|
+
expect(target.close).toHaveBeenCalled();
|
|
57
|
+
void getDB();
|
|
58
|
+
expect(opens.length).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
test('a terminated connection is reopened on next use', async () => {
|
|
61
|
+
const getDB = createDbOpener('db', 1, () => { });
|
|
62
|
+
const pending = getDB();
|
|
63
|
+
opens[0].resolve(fakeDb());
|
|
64
|
+
await pending;
|
|
65
|
+
opens[0].callbacks.terminated();
|
|
66
|
+
void getDB();
|
|
67
|
+
expect(opens.length).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('bestEffort', () => {
|
|
71
|
+
test('answers the operation when it succeeds', async () => {
|
|
72
|
+
expect(await bestEffort('read', () => Promise.resolve(3), 0)).toBe(3);
|
|
73
|
+
});
|
|
74
|
+
test('answers the fallback when it throws', async () => {
|
|
75
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
76
|
+
expect(await bestEffort('read', () => Promise.reject(new Error('quota')), 0)).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getCachedDomains, saveDomains } from './domainCache';
|
|
2
|
-
import { efetchUrl } from './eutils';
|
|
3
|
-
import { textfetch } from './fetch';
|
|
2
|
+
import { decodeXmlEntities, efetchUrl, eutilsText } from './eutils';
|
|
4
3
|
function field(xml, tag) {
|
|
5
|
-
|
|
4
|
+
const text = new RegExp(`<${tag}>(.*?)</${tag}>`, 's').exec(xml)?.[1];
|
|
5
|
+
return text === undefined ? undefined : decodeXmlEntities(text);
|
|
6
6
|
}
|
|
7
7
|
function parseQualifiers(featureXml) {
|
|
8
8
|
const quals = {};
|
|
@@ -136,7 +136,7 @@ export async function fetchProteinDomains(accessions) {
|
|
|
136
136
|
const batchSize = 100;
|
|
137
137
|
for (let i = 0; i < uncached.length; i += batchSize) {
|
|
138
138
|
const batch = uncached.slice(i, i + batchSize);
|
|
139
|
-
const xml = await
|
|
139
|
+
const xml = await eutilsText(efetchUrl({
|
|
140
140
|
db: 'protein',
|
|
141
141
|
id: batch.join(','),
|
|
142
142
|
rettype: 'gp',
|
|
@@ -22,7 +22,7 @@ export declare function cleanGeneCandidate(raw: string): string;
|
|
|
22
22
|
* itself; anything else is searched as a gene name within the query taxon.
|
|
23
23
|
* Several candidate identifiers are tried in order, because a JBrowse feature
|
|
24
24
|
* carries whatever its GFF/BigBed had — `id()`, `name`, `gene_name` — and only
|
|
25
|
-
* some of those are real symbols.
|
|
25
|
+
* some of those are real symbols. `gene:TP53` and `TP53` are one search.
|
|
26
26
|
*/
|
|
27
27
|
export declare function resolveGeneId(candidates: string[], taxId: number): Promise<{
|
|
28
28
|
geneId: string;
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Mirrors jb2hubs' website/src/components/proteinMsa.ts assembler, trimmed to
|
|
16
16
|
// what the launch dialog needs and using this plugin's fetch/eutils helpers.
|
|
17
|
-
import {
|
|
18
|
-
import { jsonfetch
|
|
17
|
+
import { efetchPost, efetchUrl, eutilsJson, eutilsText, eutilsUrl, } from './eutils';
|
|
18
|
+
import { jsonfetch } from './fetch';
|
|
19
19
|
// v2, not v2alpha: the alpha path still answers /orthologs but 404s
|
|
20
20
|
// /product_report, so an assembler pointed at it silently resolves zero
|
|
21
21
|
// representative proteins and reports "no orthologs" for every gene.
|
|
22
22
|
const DATASETS = 'https://api.ncbi.nlm.nih.gov/datasets/v2';
|
|
23
|
-
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
24
23
|
// NCBI's ortholog report IS the species panel. There is no list here to keep in
|
|
25
24
|
// step with what NCBI knows, and the panel widens by itself as NCBI annotates
|
|
26
25
|
// more genomes.
|
|
@@ -49,10 +48,6 @@ const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
|
49
48
|
// a minute; EBI's own ceiling is 4000 sequences and 4MB, which even CFTR's full
|
|
50
49
|
// set (1.3MB) sits inside.
|
|
51
50
|
export const defaultMaxSpecies = 100;
|
|
52
|
-
function ncbiUrl(url) {
|
|
53
|
-
const sep = url.includes('?') ? '&' : '?';
|
|
54
|
-
return `${url}${sep}tool=${NCBI_TOOL}&email=${encodeURIComponent(NCBI_EMAIL)}`;
|
|
55
|
-
}
|
|
56
51
|
/**
|
|
57
52
|
* A candidate gene reference as the ortholog services know it: a GFF ID prefix
|
|
58
53
|
* (gene:TP53) and a version suffix (NM_000546.6) stripped off. Shared with
|
|
@@ -70,20 +65,26 @@ export function cleanGeneCandidate(raw) {
|
|
|
70
65
|
* itself; anything else is searched as a gene name within the query taxon.
|
|
71
66
|
* Several candidate identifiers are tried in order, because a JBrowse feature
|
|
72
67
|
* carries whatever its GFF/BigBed had — `id()`, `name`, `gene_name` — and only
|
|
73
|
-
* some of those are real symbols.
|
|
68
|
+
* some of those are real symbols. `gene:TP53` and `TP53` are one search.
|
|
74
69
|
*/
|
|
75
70
|
export async function resolveGeneId(candidates, taxId) {
|
|
71
|
+
const asked = new Set();
|
|
76
72
|
for (const raw of candidates) {
|
|
77
73
|
const query = raw.trim();
|
|
78
|
-
if (!query) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
74
|
if (/^\d+$/.test(query)) {
|
|
82
75
|
return { geneId: query, matched: query };
|
|
83
76
|
}
|
|
84
77
|
const cleaned = cleanGeneCandidate(query);
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
if (!cleaned || asked.has(cleaned)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
asked.add(cleaned);
|
|
82
|
+
const json = await eutilsJson(eutilsUrl('esearch', {
|
|
83
|
+
db: 'gene',
|
|
84
|
+
term: `${cleaned}[Gene Name] AND ${taxId}[taxid]`,
|
|
85
|
+
retmode: 'json',
|
|
86
|
+
retmax: '1',
|
|
87
|
+
}));
|
|
87
88
|
const geneId = json.esearchresult?.idlist?.[0];
|
|
88
89
|
if (geneId) {
|
|
89
90
|
return { geneId, matched: cleaned };
|
|
@@ -99,7 +100,7 @@ export async function resolveGeneId(candidates, taxId) {
|
|
|
99
100
|
* taxon, which already has its own `<species>_query` row.
|
|
100
101
|
*/
|
|
101
102
|
export async function fetchOrthologGenes(geneId, { taxa, exclude, limit = defaultMaxSpecies, } = {}) {
|
|
102
|
-
const json = await jsonfetch(
|
|
103
|
+
const json = await jsonfetch(`${DATASETS}/gene/id/${geneId}/orthologs?returned_content=COMPLETE`);
|
|
103
104
|
const byTaxon = new Map();
|
|
104
105
|
for (const { gene } of json.reports ?? []) {
|
|
105
106
|
const taxId = Number(gene?.tax_id);
|
|
@@ -143,7 +144,7 @@ export async function fetchRepresentativeProteins(geneIds) {
|
|
|
143
144
|
const byGene = new Map();
|
|
144
145
|
for (let i = 0; i < geneIds.length; i += PRODUCT_REPORT_CHUNK) {
|
|
145
146
|
const chunk = geneIds.slice(i, i + PRODUCT_REPORT_CHUNK);
|
|
146
|
-
const json = await jsonfetch(
|
|
147
|
+
const json = await jsonfetch(`${DATASETS}/gene/id/${chunk.join(',')}/product_report?page_size=${chunk.length}`);
|
|
147
148
|
for (const { product } of json.reports ?? []) {
|
|
148
149
|
const candidates = (product?.transcripts ?? [])
|
|
149
150
|
.map(t => ({
|
|
@@ -220,7 +221,7 @@ export async function fetchProteinForGene(geneId) {
|
|
|
220
221
|
if (!acc) {
|
|
221
222
|
return undefined;
|
|
222
223
|
}
|
|
223
|
-
const seq = parseFasta(await
|
|
224
|
+
const seq = parseFasta(await eutilsText(efetchUrl({ db: 'protein', id: acc, rettype: 'fasta', retmode: 'text' }))).get(acc);
|
|
224
225
|
return seq ? { accession: acc, sequence: seq } : undefined;
|
|
225
226
|
}
|
|
226
227
|
/**
|
|
@@ -242,7 +243,7 @@ export async function fetchOrthologRows({ geneId, taxa, exclude, limit, onProgre
|
|
|
242
243
|
}
|
|
243
244
|
onProgress(`Fetching ${withProtein.length} protein sequences...`);
|
|
244
245
|
const accessions = withProtein.map(g => proteinByGene.get(g.geneId));
|
|
245
|
-
const seqByAcc = parseFasta(await
|
|
246
|
+
const seqByAcc = parseFasta(await eutilsText(...efetchPost({
|
|
246
247
|
db: 'protein',
|
|
247
248
|
id: accessions.join(','),
|
|
248
249
|
rettype: 'fasta',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { cleanGeneCandidate, dedupeLabels, fetchOrthologGenes, fetchRepresentativeProteins, parseFasta, } from './ncbiOrthologs';
|
|
2
|
+
import { cleanGeneCandidate, dedupeLabels, fetchOrthologGenes, fetchRepresentativeProteins, parseFasta, resolveGeneId, } from './ncbiOrthologs';
|
|
3
3
|
describe('dedupeLabels', () => {
|
|
4
4
|
test('sanitizes to single tokens', () => {
|
|
5
5
|
// labels are used identically as FASTA headers, Newick leaf names and GFF
|
|
@@ -141,3 +141,19 @@ describe('the NCBI request ceilings', () => {
|
|
|
141
141
|
expect(seen.every(u => /page_size=\d+/.test(u))).toBe(true);
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
// eutils allows three requests a second, so asking the same symbol twice is a
|
|
145
|
+
// request spent on nothing
|
|
146
|
+
test('resolveGeneId searches each cleaned symbol once', async () => {
|
|
147
|
+
const seen = [];
|
|
148
|
+
vi.stubGlobal('fetch', (url) => {
|
|
149
|
+
seen.push(url);
|
|
150
|
+
return Promise.resolve({
|
|
151
|
+
ok: true,
|
|
152
|
+
status: 200,
|
|
153
|
+
json: () => Promise.resolve({ esearchresult: { idlist: [] } }),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
expect(await resolveGeneId(['gene:TP53', 'TP53', ' TP53 ', 'NM_000546.6'], 9606)).toBeUndefined();
|
|
157
|
+
expect(seen).toHaveLength(2);
|
|
158
|
+
vi.unstubAllGlobals();
|
|
159
|
+
});
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { jsonfetch } from './fetch';
|
|
3
|
-
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
4
|
-
function eutilsUrl(endpoint, params) {
|
|
5
|
-
const search = new URLSearchParams({
|
|
6
|
-
...params,
|
|
7
|
-
retmode: 'json',
|
|
8
|
-
tool: NCBI_TOOL,
|
|
9
|
-
email: NCBI_EMAIL,
|
|
10
|
-
});
|
|
11
|
-
return `${EUTILS}/${endpoint}?${search.toString()}`;
|
|
12
|
-
}
|
|
1
|
+
import { eutilsJson, eutilsUrl } from './eutils';
|
|
13
2
|
/**
|
|
14
3
|
* Free text -> NCBI taxon id. A bare number is taken as the id itself; anything
|
|
15
4
|
* else is searched against db=taxonomy, which resolves a scientific name
|
|
@@ -28,7 +17,12 @@ export async function resolveTaxId(query) {
|
|
|
28
17
|
if (/^\d+$/.test(term)) {
|
|
29
18
|
return Number(term);
|
|
30
19
|
}
|
|
31
|
-
const json = await
|
|
20
|
+
const json = await eutilsJson(eutilsUrl('esearch', {
|
|
21
|
+
db: 'taxonomy',
|
|
22
|
+
term,
|
|
23
|
+
retmax: '1',
|
|
24
|
+
retmode: 'json',
|
|
25
|
+
}));
|
|
32
26
|
const id = json.esearchresult?.idlist?.[0];
|
|
33
27
|
return id ? Number(id) : undefined;
|
|
34
28
|
}
|
|
@@ -52,12 +46,17 @@ export async function resolveAssemblySpecies(assemblyName) {
|
|
|
52
46
|
if (!term) {
|
|
53
47
|
return undefined;
|
|
54
48
|
}
|
|
55
|
-
const search = await
|
|
49
|
+
const search = await eutilsJson(eutilsUrl('esearch', {
|
|
50
|
+
db: 'assembly',
|
|
51
|
+
term,
|
|
52
|
+
retmax: '1',
|
|
53
|
+
retmode: 'json',
|
|
54
|
+
}));
|
|
56
55
|
const uid = search.esearchresult?.idlist?.[0];
|
|
57
56
|
if (!uid) {
|
|
58
57
|
return undefined;
|
|
59
58
|
}
|
|
60
|
-
const summary = await
|
|
59
|
+
const summary = await eutilsJson(eutilsUrl('esummary', { db: 'assembly', id: uid, retmode: 'json' }));
|
|
61
60
|
const record = summary.result?.[uid];
|
|
62
61
|
const taxId = Number(record?.speciestaxid);
|
|
63
62
|
return record?.speciesname && taxId
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { efetchUrl } from './eutils';
|
|
2
|
-
import {
|
|
3
|
-
import { createDbOpener } from './idb';
|
|
1
|
+
import { decodeXmlEntities, efetchUrl, eutilsText } from './eutils';
|
|
2
|
+
import { bestEffort, createDbOpener } from './idb';
|
|
4
3
|
const DB_NAME = 'jbrowse-msaview-taxonomy-cache';
|
|
5
4
|
const STORE_NAME = 'common-names';
|
|
6
5
|
const DB_VERSION = 2;
|
|
@@ -10,20 +9,24 @@ const getDB = createDbOpener(DB_NAME, DB_VERSION, db => {
|
|
|
10
9
|
}
|
|
11
10
|
db.createObjectStore(STORE_NAME, { keyPath: 'taxid' });
|
|
12
11
|
});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
function getCachedTaxonomies(taxids) {
|
|
13
|
+
return bestEffort('taxonomy cache read', async () => {
|
|
14
|
+
const db = await getDB();
|
|
15
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
16
|
+
const results = await Promise.all(taxids.map(taxid => tx.store.get(taxid)));
|
|
17
|
+
await tx.done;
|
|
18
|
+
return results;
|
|
19
|
+
}, []);
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
function saveTaxonomyCache(entries) {
|
|
22
|
+
return bestEffort('taxonomy cache write', async () => {
|
|
23
|
+
const db = await getDB();
|
|
24
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
await tx.store.put(entry);
|
|
27
|
+
}
|
|
28
|
+
await tx.done;
|
|
29
|
+
}, undefined);
|
|
27
30
|
}
|
|
28
31
|
export async function fetchTaxonomyInfo(taxidsWithRepeats) {
|
|
29
32
|
// callers pass one taxid per alignment row, and a BLAST hit list is several
|
|
@@ -60,7 +63,7 @@ export async function fetchTaxonomyInfo(taxidsWithRepeats) {
|
|
|
60
63
|
// error body that the regexes below silently find nothing in, so without
|
|
61
64
|
// the status check a throttled batch looks like "these taxa have no
|
|
62
65
|
// names" instead of reporting why
|
|
63
|
-
const text = await
|
|
66
|
+
const text = await eutilsText(efetchUrl({ db: 'taxonomy', id: idsParam, retmode: 'xml' }));
|
|
64
67
|
// Build a map of taxid -> taxon block by finding Taxon elements.
|
|
65
68
|
// Prefer entries with <LineageEx> (full top-level entries) over nested
|
|
66
69
|
// entries inside another taxon's LineageEx
|
|
@@ -102,8 +105,9 @@ export async function fetchTaxonomyInfo(taxidsWithRepeats) {
|
|
|
102
105
|
const genbankCommon = /<GenbankCommonName>(.*?)<\/GenbankCommonName>/.exec(taxonXml);
|
|
103
106
|
const commonName = /<CommonName>(.*?)<\/CommonName>/.exec(taxonXml);
|
|
104
107
|
const sciName = /<ScientificName>(.*?)<\/ScientificName>/.exec(taxonXml);
|
|
105
|
-
const
|
|
106
|
-
const
|
|
108
|
+
const rawName = genbankCommon?.[1] ?? commonName?.[1];
|
|
109
|
+
const name = rawName && decodeXmlEntities(rawName);
|
|
110
|
+
const sci = decodeXmlEntities(sciName?.[1] ?? '');
|
|
107
111
|
result.set(taxid, { sciname: sci, commonName: name });
|
|
108
112
|
toCache.push({ taxid, sciname: sci, commonName: name });
|
|
109
113
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from 'vitest';
|
|
2
|
+
import { fetchTaxonomyInfo } from './taxonomyNames';
|
|
3
|
+
vi.mock('./idb', async (importOriginal) => ({
|
|
4
|
+
...(await importOriginal()),
|
|
5
|
+
createDbOpener: () => () => Promise.reject(new DOMException('site data blocked', 'SecurityError')),
|
|
6
|
+
}));
|
|
7
|
+
const XML = `<TaxaSet>
|
|
8
|
+
<Taxon>
|
|
9
|
+
<TaxId>10090</TaxId>
|
|
10
|
+
<ScientificName>Mus musculus</ScientificName>
|
|
11
|
+
<OtherNames><GenbankCommonName>house mouse</GenbankCommonName></OtherNames>
|
|
12
|
+
<LineageEx></LineageEx>
|
|
13
|
+
</Taxon>
|
|
14
|
+
</TaxaSet>`;
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.unstubAllGlobals();
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
// runs between the search and the alignment, so a throw here discarded a BLAST
|
|
20
|
+
// job that had already finished
|
|
21
|
+
test('a browser without IndexedDB still gets the names from NCBI', async () => {
|
|
22
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
23
|
+
vi.stubGlobal('fetch', () => Promise.resolve({
|
|
24
|
+
ok: true,
|
|
25
|
+
status: 200,
|
|
26
|
+
text: () => Promise.resolve(XML),
|
|
27
|
+
}));
|
|
28
|
+
const info = await fetchTaxonomyInfo([10090]);
|
|
29
|
+
expect(info.get(10090)).toEqual({
|
|
30
|
+
sciname: 'Mus musculus',
|
|
31
|
+
commonName: 'house mouse',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -71,14 +71,14 @@ export async function resolveUniProtEntry(candidates, taxId, signal) {
|
|
|
71
71
|
continue;
|
|
72
72
|
}
|
|
73
73
|
if (UNIPROT_ACCESSION.test(candidate)) {
|
|
74
|
-
const json = await jsonfetch(`${UNIPROT}/uniprotkb/${candidate
|
|
74
|
+
const json = await jsonfetch(`${UNIPROT}/uniprotkb/${candidate}?fields=${ENTRY_FIELDS}`, { signal });
|
|
75
75
|
const entry = parseEntry(json);
|
|
76
76
|
if (entry) {
|
|
77
77
|
return entry;
|
|
78
78
|
}
|
|
79
79
|
continue;
|
|
80
80
|
}
|
|
81
|
-
const query = `gene_exact
|
|
81
|
+
const query = `gene_exact:"${candidate.replaceAll('"', '')}" AND organism_id:${taxId}`;
|
|
82
82
|
const json = await jsonfetch(`${UNIPROT}/uniprotkb/search?query=${encodeURIComponent(query)}&fields=${ENTRY_FIELDS}&size=25`, { signal });
|
|
83
83
|
const [best] = parseEntries(json).sort(rank);
|
|
84
84
|
if (best) {
|
|
@@ -114,5 +114,12 @@ test('resolveUniProtEntry fetches an accession directly and searches a symbol',
|
|
|
114
114
|
expect((await resolveUniProtEntry(['P04637'], 9606))?.accession).toBe('P04637');
|
|
115
115
|
expect(calls[0]).toContain('/uniprotkb/P04637?');
|
|
116
116
|
expect((await resolveUniProtEntry(['gene:TP53'], 9606))?.accession).toBe('Q2');
|
|
117
|
-
expect(decodeURIComponent(calls[1])).toContain('gene_exact:TP53 AND organism_id:9606');
|
|
117
|
+
expect(decodeURIComponent(calls[1])).toContain('gene_exact:"TP53" AND organism_id:9606');
|
|
118
|
+
});
|
|
119
|
+
// the isoform is the query: dropping the suffix launched the canonical
|
|
120
|
+
// sequence in its place, 393 residues for P04637-2's 341
|
|
121
|
+
test('resolveUniProtEntry keeps an isoform suffix', async () => {
|
|
122
|
+
const calls = stubFetch([{ body: entry('P04637-2', 9606) }]);
|
|
123
|
+
expect((await resolveUniProtEntry(['P04637-2'], 9606))?.accession).toBe('P04637-2');
|
|
124
|
+
expect(calls[0]).toContain('/uniprotkb/P04637-2?');
|
|
118
125
|
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "3.
|
|
1
|
+
export declare const version = "3.7.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.
|
|
1
|
+
export const version = '3.7.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.7.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -25,28 +25,28 @@
|
|
|
25
25
|
"@emotion/react": "^11.14.0",
|
|
26
26
|
"@eslint/js": "^10.0.1",
|
|
27
27
|
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
|
28
|
-
"@jbrowse/core": "
|
|
28
|
+
"@jbrowse/core": "5.0.0-beta.8",
|
|
29
29
|
"@jbrowse/mobx-state-tree": "^6.5.1",
|
|
30
|
-
"@jbrowse/plugin-linear-genome-view": "
|
|
30
|
+
"@jbrowse/plugin-linear-genome-view": "5.0.0-beta.8",
|
|
31
31
|
"@mui/icons-material": "^9.4.0",
|
|
32
32
|
"@mui/material": "^9.4.0",
|
|
33
33
|
"@mui/system": "^9.4.0",
|
|
34
|
-
"@mui/x-data-grid": "^9.
|
|
34
|
+
"@mui/x-data-grid": "^9.14.0",
|
|
35
35
|
"@testing-library/dom": "^10.4.2",
|
|
36
36
|
"@testing-library/react": "^16.3.3",
|
|
37
|
-
"@types/node": "^26.6.
|
|
37
|
+
"@types/node": "^26.6.1",
|
|
38
38
|
"@types/react": "^19.3.0",
|
|
39
39
|
"esbuild": "^0.28.2",
|
|
40
40
|
"eslint": "^10.10.0",
|
|
41
41
|
"eslint-plugin-import-x": "^4.17.1",
|
|
42
42
|
"eslint-plugin-react": "^7.37.5",
|
|
43
43
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
44
|
-
"eslint-plugin-unicorn": "^
|
|
44
|
+
"eslint-plugin-unicorn": "^75.0.0",
|
|
45
45
|
"git-cliff": "^2.13.1",
|
|
46
|
-
"jsdom": "^30.0
|
|
46
|
+
"jsdom": "^30.1.0",
|
|
47
47
|
"mobx": "^7.0.3",
|
|
48
48
|
"mobx-react": "^10.0.2",
|
|
49
|
-
"msa-parsers": "^8.
|
|
49
|
+
"msa-parsers": "^8.1.0",
|
|
50
50
|
"oxfmt": "^0.68.0",
|
|
51
51
|
"oxlint": "^1.83.0",
|
|
52
52
|
"oxlint-tsgolint": "^7.0.2001",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"puppeteer": "^25.11.0",
|
|
57
57
|
"react": "^19.3.0",
|
|
58
58
|
"react-dom": "^19.3.0",
|
|
59
|
-
"react-msaview": "^8.
|
|
59
|
+
"react-msaview": "^8.1.0",
|
|
60
60
|
"rimraf": "^6.1.3",
|
|
61
61
|
"rxjs": "^7.8.2",
|
|
62
62
|
"serve": "^14.2.6",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { SimpleFeature } from '@jbrowse/core/util'
|
|
2
|
+
import { genomeToTranscriptSeqMapping } from 'g2p_mapper'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { transcriptPosToVisibleCol } from '../MsaViewPanel/util'
|
|
6
|
+
import { getProteinSequenceFromFeature } from './components/calculateProteinSequence'
|
|
7
|
+
import { findQueryRow } from './detectQueryRow'
|
|
8
|
+
import { cleanProteinSequence } from './util'
|
|
9
|
+
|
|
10
|
+
describe('cleanProteinSequence', () => {
|
|
11
|
+
test('drops the stop and a trailing partial codon', () => {
|
|
12
|
+
expect(cleanProteinSequence('MAWK*')).toBe('MAWK')
|
|
13
|
+
expect(cleanProteinSequence('MAWK&')).toBe('MAWK')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('keeps a residue for every other codon', () => {
|
|
17
|
+
expect(cleanProteinSequence('&MAWK*')).toBe('XMAWK')
|
|
18
|
+
expect(cleanProteinSequence('MA*WK*')).toBe('MAXWK')
|
|
19
|
+
expect(cleanProteinSequence('MA&WK')).toBe('MAXWK')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// what the genome view hovers is a codon, and what lights is the query row's
|
|
24
|
+
// residue at g2p's number for that codon, so the two have to count alike
|
|
25
|
+
describe('a launched query row read through g2p', () => {
|
|
26
|
+
function rowResidueUnder(genomePos: number, json: Record<string, unknown>) {
|
|
27
|
+
const seq = json.seq as string
|
|
28
|
+
const feature = new SimpleFeature(json as never)
|
|
29
|
+
const row = cleanProteinSequence(
|
|
30
|
+
getProteinSequenceFromFeature({ seq, feature }),
|
|
31
|
+
)
|
|
32
|
+
const { g2p } = genomeToTranscriptSeqMapping(feature.toJSON() as never)
|
|
33
|
+
const col = transcriptPosToVisibleCol(
|
|
34
|
+
{
|
|
35
|
+
querySeqName: 'QUERY',
|
|
36
|
+
querySeqOffset: 0,
|
|
37
|
+
seqPosToVisibleCol: (_, p) => (p < row.length ? p : row.length),
|
|
38
|
+
visibleColToSeqPos: (_, c) => (c < row.length ? c : undefined),
|
|
39
|
+
},
|
|
40
|
+
g2p[genomePos]!,
|
|
41
|
+
)
|
|
42
|
+
return row[col!]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function transcript(seq: string, cds: Record<string, unknown>) {
|
|
46
|
+
return {
|
|
47
|
+
uniqueId: 't1',
|
|
48
|
+
refName: 'chr1',
|
|
49
|
+
start: 0,
|
|
50
|
+
end: seq.length,
|
|
51
|
+
strand: 1,
|
|
52
|
+
type: 'mRNA',
|
|
53
|
+
seq,
|
|
54
|
+
subfeatures: [
|
|
55
|
+
{ uniqueId: 'c1', refName: 'chr1', start: 0, end: seq.length, ...cds },
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('after a partial first codon', () => {
|
|
61
|
+
// G | ATG GCC TGG AAA TAA: base 4 opens GCC, alanine
|
|
62
|
+
const json = transcript('GATGGCCTGGAAATAA', { type: 'CDS', phase: 1 })
|
|
63
|
+
expect(rowResidueUnder(4, json)).toBe('A')
|
|
64
|
+
expect(rowResidueUnder(10, json)).toBe('K')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('after an internal stop', () => {
|
|
68
|
+
// ATG TGA GCC TGG TAA: a selenocysteine GENCODE does not annotate
|
|
69
|
+
const json = transcript('ATGTGAGCCTGGTAA', { type: 'CDS' })
|
|
70
|
+
expect(rowResidueUnder(6, json)).toBe('A')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('query row detection', () => {
|
|
75
|
+
test('matches a row a search launch built from a partial transcript', () => {
|
|
76
|
+
const msa = '>QUERY\nXMAWK\n>other\nMAWR-'
|
|
77
|
+
expect(findQueryRow(msa, '&MAWK*').match).toMatchObject({
|
|
78
|
+
name: 'QUERY',
|
|
79
|
+
quality: 'exact',
|
|
80
|
+
offset: 0,
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('still places a row that starts at the first whole codon', () => {
|
|
85
|
+
const msa = '>Query_1\nMAWK\n>other\nMAWR'
|
|
86
|
+
expect(findQueryRow(msa, '&MAWK*').match).toMatchObject({
|
|
87
|
+
name: 'Query_1',
|
|
88
|
+
offset: 1,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|