jbrowse-plugin-msaview 3.8.0 → 3.10.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/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -22
- package/dist/AddHighlightModel/connectedHighlights.d.ts +9 -0
- package/dist/AddHighlightModel/connectedHighlights.js +15 -0
- package/dist/AddHighlightModel/connectedHighlights.test.js +36 -0
- package/dist/AddHighlightModel/index.js +0 -1
- package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +2 -8
- package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +10 -3
- package/dist/LaunchMsaView/cleanProteinSequence.test.js +36 -2
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +4 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +6 -4
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +0 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +2 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +0 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +5 -5
- package/dist/LaunchMsaView/components/useFeatureSequence.js +3 -6
- package/dist/LaunchMsaView/launchTarget.test.js +1 -1
- package/dist/LaunchMsaView/util.d.ts +0 -1
- package/dist/LaunchMsaView/util.js +1 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +5 -14
- package/dist/MsaViewPanel/afterCreateAutoruns.js +77 -86
- package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +42 -16
- package/dist/MsaViewPanel/doLaunchBlast.test.js +54 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +18 -11
- package/dist/MsaViewPanel/launchAutoruns.test.js +0 -1
- package/dist/MsaViewPanel/model.d.ts +651 -216
- package/dist/MsaViewPanel/model.js +39 -10
- package/dist/MsaViewPanel/model.test.js +3 -4
- package/dist/MsaViewPanel/msaDataStore.d.ts +1 -2
- package/dist/MsaViewPanel/msaDataStore.js +11 -26
- package/dist/MsaViewPanel/msaDataStore.test.js +10 -2
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +1 -0
- package/dist/MsaViewPanel/processInit.test.js +1 -7
- package/dist/MsaViewPanel/regionSnapshot.test.js +0 -1
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +7 -13
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +2 -3
- package/dist/MsaViewPanel/storedData.test.js +238 -145
- package/dist/MsaViewPanel/transcriptMap.d.ts +7 -0
- package/dist/MsaViewPanel/transcriptMap.js +108 -0
- package/dist/MsaViewPanel/transcriptMap.test.d.ts +1 -0
- package/dist/MsaViewPanel/transcriptMap.test.js +127 -0
- package/dist/index.d.ts +11 -8
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +56 -62
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/browserAlign.d.ts +5 -3
- package/dist/utils/browserAlign.js +29 -23
- package/dist/utils/browserAlign.test.js +14 -1
- package/dist/utils/ebiBlast.d.ts +0 -8
- package/dist/utils/ebiBlast.js +10 -21
- package/dist/utils/ebiJobDispatcher.d.ts +9 -9
- package/dist/utils/ebiJobDispatcher.js +17 -3
- package/dist/utils/eutils.d.ts +0 -2
- package/dist/utils/eutils.js +11 -3
- package/dist/utils/eutils.test.js +22 -0
- package/dist/utils/msa.js +6 -14
- package/dist/utils/msaRows.d.ts +0 -8
- package/dist/utils/msaRows.js +1 -1
- package/dist/utils/ncbiOrthologs.d.ts +7 -5
- package/dist/utils/ncbiOrthologs.js +20 -14
- package/dist/utils/ncbiOrthologs.test.js +13 -0
- package/dist/utils/ncbiTaxonomy.d.ts +2 -2
- package/dist/utils/ncbiTaxonomy.js +5 -5
- package/dist/utils/pantherOrthologs.d.ts +2 -3
- package/dist/utils/pantherOrthologs.js +18 -12
- package/dist/utils/phmmer.js +7 -14
- package/dist/utils/searchCache.d.ts +18 -0
- package/dist/utils/searchCache.js +33 -0
- package/dist/utils/searchCache.test.d.ts +1 -0
- package/dist/utils/searchCache.test.js +80 -0
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +7 -2
- package/dist/utils/taxonomyNamesAbort.test.d.ts +1 -0
- package/dist/utils/taxonomyNamesAbort.test.js +36 -0
- package/dist/utils/unirefHomologs.d.ts +1 -6
- package/dist/utils/unirefHomologs.js +2 -2
- package/dist/utils/useFetch.d.ts +1 -1
- package/dist/utils/useFetch.js +5 -5
- package/dist/utils/useFetchAbort.test.d.ts +1 -0
- package/dist/utils/useFetchAbort.test.js +29 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +18 -18
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +10 -23
- package/src/AddHighlightModel/connectedHighlights.test.ts +46 -0
- package/src/AddHighlightModel/connectedHighlights.ts +35 -0
- package/src/AddHighlightModel/index.tsx +0 -1
- package/src/LaunchMsaView/cleanProteinSequence.test.ts +37 -2
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +6 -4
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +2 -2
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +5 -5
- package/src/LaunchMsaView/components/useFeatureSequence.ts +3 -6
- package/src/LaunchMsaView/launchTarget.test.ts +1 -1
- package/src/LaunchMsaView/launchTarget.ts +2 -3
- package/src/LaunchMsaView/util.ts +1 -1
- package/src/MsaViewPanel/afterCreateAutoruns.ts +90 -87
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
- package/src/MsaViewPanel/doLaunchBlast.test.ts +71 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +71 -20
- package/src/MsaViewPanel/doLaunchOrthologs.ts +28 -11
- package/src/MsaViewPanel/launchAutoruns.test.ts +0 -1
- package/src/MsaViewPanel/model.test.ts +3 -4
- package/src/MsaViewPanel/model.ts +40 -10
- package/src/MsaViewPanel/msaDataStore.test.ts +13 -2
- package/src/MsaViewPanel/msaDataStore.ts +11 -25
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +1 -0
- package/src/MsaViewPanel/processInit.test.ts +1 -8
- package/src/MsaViewPanel/regionSnapshot.test.ts +0 -1
- package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +2 -3
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +7 -17
- package/src/MsaViewPanel/storedData.test.ts +273 -158
- package/src/MsaViewPanel/transcriptMap.test.ts +160 -0
- package/src/MsaViewPanel/transcriptMap.ts +144 -0
- package/src/utils/browserAlign.test.ts +17 -0
- package/src/utils/browserAlign.ts +39 -25
- package/src/utils/ebiBlast.ts +12 -35
- package/src/utils/ebiJobDispatcher.ts +26 -7
- package/src/utils/eutils.test.ts +25 -0
- package/src/utils/eutils.ts +10 -3
- package/src/utils/msa.ts +6 -16
- package/src/utils/msaRows.ts +1 -1
- package/src/utils/ncbiOrthologs.test.ts +16 -0
- package/src/utils/ncbiOrthologs.ts +37 -15
- package/src/utils/ncbiTaxonomy.ts +8 -2
- package/src/utils/pantherOrthologs.ts +28 -12
- package/src/utils/phmmer.ts +7 -16
- package/src/utils/searchCache.test.ts +98 -0
- package/src/utils/searchCache.ts +75 -0
- package/src/utils/taxonomyNames.ts +7 -0
- package/src/utils/taxonomyNamesAbort.test.ts +46 -0
- package/src/utils/unirefHomologs.ts +2 -2
- package/src/utils/useFetch.ts +6 -6
- package/src/utils/useFetchAbort.test.tsx +33 -0
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +0 -27
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +0 -71
- package/dist/LaunchMsaView/components/types.d.ts +0 -9
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +0 -113
- package/src/LaunchMsaView/components/types.ts +0 -12
- /package/dist/{LaunchMsaView/components/types.js → AddHighlightModel/connectedHighlights.test.d.ts} +0 -0
|
@@ -8,68 +8,70 @@ import { loadProteinDomains } from './loadProteinDomains';
|
|
|
8
8
|
import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
|
|
9
9
|
import { runLaunch } from './runLaunch';
|
|
10
10
|
import { getProteinViews } from './structureConnection';
|
|
11
|
+
import { proteinPositionsInRange } from './transcriptMap';
|
|
11
12
|
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
|
|
12
13
|
const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
|
|
13
14
|
const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
|
|
14
15
|
const START_OVER = ' Relaunch it from the gene to rebuild it.';
|
|
16
|
+
const EXPIRED_EXTRAS_WARNING = 'Part of this view is no longer in browser storage: annotations, a tree or row metadata too large for the session. Stored documents are kept for 7 days after they were last used, and are lost when site data is cleared.';
|
|
15
17
|
export function loadStoredData(self) {
|
|
16
|
-
const { dataStoreId
|
|
17
|
-
if (dataStoreId
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
const { dataStoreId } = self;
|
|
19
|
+
if (!dataStoreId) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
void (async () => {
|
|
23
|
+
try {
|
|
24
|
+
self.setLoadingStoredData(true);
|
|
25
|
+
const stored = await retrieveMsaData(dataStoreId);
|
|
26
|
+
if (self.dataStoreId !== dataStoreId) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
transaction(() => {
|
|
30
|
+
if (stored) {
|
|
31
|
+
const { data } = self;
|
|
32
|
+
if (stored.msa && !data.msa) {
|
|
33
|
+
self.setMSA(stored.msa);
|
|
34
|
+
}
|
|
35
|
+
if (stored.tree && !data.tree) {
|
|
36
|
+
self.setTree(stored.tree);
|
|
37
|
+
}
|
|
38
|
+
if (stored.treeMetadata && !data.treeMetadata) {
|
|
39
|
+
self.setTreeMetadata(stored.treeMetadata);
|
|
40
|
+
}
|
|
41
|
+
if (stored.gff && !data.gff) {
|
|
42
|
+
self.setGFF(stored.gff);
|
|
43
|
+
}
|
|
44
|
+
self.setLastStoredData(stored);
|
|
40
45
|
}
|
|
41
46
|
else {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
transaction(() => {
|
|
48
|
-
self.setDataStoreId(undefined);
|
|
47
|
+
self.setDataStoreId(undefined);
|
|
48
|
+
if (hasAlignmentSource(self)) {
|
|
49
|
+
self.addWarning(EXPIRED_EXTRAS_WARNING);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
49
52
|
self.setError(new Error(EXPIRED_MESSAGE +
|
|
50
53
|
((self.blastParams ?? self.orthologParams)
|
|
51
54
|
? RELAUNCHABLE
|
|
52
55
|
: START_OVER)));
|
|
53
|
-
}
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.error('Failed to load MSA data from IndexedDB:', e);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
self.setLoadingStoredData(false);
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
return { msa, tree, treeMetadata, gff };
|
|
68
|
+
function hasAlignmentSource(self) {
|
|
69
|
+
return !!(self.data.msa ||
|
|
70
|
+
self.msaFilehandle ||
|
|
71
|
+
self.init?.msaIndexedLocation);
|
|
72
|
+
}
|
|
73
|
+
function isEmpty(data) {
|
|
74
|
+
return !(data.msa || data.tree || data.treeMetadata || data.gff);
|
|
73
75
|
}
|
|
74
76
|
function sameData(a, b) {
|
|
75
77
|
return (!!a &&
|
|
@@ -79,44 +81,37 @@ function sameData(a, b) {
|
|
|
79
81
|
a.gff === b.gff);
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
82
|
-
* Keep IndexedDB holding
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
89
|
-
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
90
|
-
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
91
|
-
* IndexedDB (private mode) fails once rather than in a loop.
|
|
92
|
-
*
|
|
93
|
-
* A view whose data comes from a filehandle -- or from the indexed block its
|
|
94
|
-
* kept `init` names -- stores nothing at all: the file is the source of truth
|
|
95
|
-
* and it is refetched at startup.
|
|
84
|
+
* Keep IndexedDB holding `unsavedDocuments`. A view writes in place only to a
|
|
85
|
+
* row it created (`ownsDataStoreRow`), and never deletes one, since other
|
|
86
|
+
* views may name it: an emptied set drops the id and cleanupOldData ages the
|
|
87
|
+
* row out. `lastStoredData` is recorded even when a write fails, so a browser
|
|
88
|
+
* refusing IndexedDB fails once rather than in a loop.
|
|
96
89
|
*/
|
|
97
90
|
export function storeDataToIndexedDB(self) {
|
|
98
|
-
const {
|
|
99
|
-
const data =
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
const { dataStoreId, ownsDataStoreRow, isStoringData, loadingStoredData, lastStoredData, } = self;
|
|
92
|
+
const data = self.unsavedDocuments;
|
|
93
|
+
if (isStoringData || loadingStoredData || sameData(lastStoredData, data)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (isEmpty(data)) {
|
|
97
|
+
if (lastStoredData) {
|
|
98
|
+
transaction(() => {
|
|
99
|
+
self.setDataStoreId(undefined);
|
|
100
|
+
self.setOwnsDataStoreRow(false);
|
|
101
|
+
self.setLastStoredData(data);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
109
104
|
return;
|
|
110
105
|
}
|
|
111
|
-
// mark as storing synchronously so re-runs of this autorun (e.g. when data
|
|
112
|
-
// observables change while the write is pending) don't kick off a duplicate
|
|
113
|
-
// write and leave an orphan IndexedDB entry
|
|
114
106
|
self.setIsStoringData(true);
|
|
115
107
|
void (async () => {
|
|
116
108
|
try {
|
|
117
|
-
const id = dataStoreId
|
|
109
|
+
const id = dataStoreId && ownsDataStoreRow ? dataStoreId : generateDataStoreId();
|
|
118
110
|
if (await storeMsaData(id, data)) {
|
|
119
|
-
|
|
111
|
+
transaction(() => {
|
|
112
|
+
self.setDataStoreId(id);
|
|
113
|
+
self.setOwnsDataStoreRow(true);
|
|
114
|
+
});
|
|
120
115
|
}
|
|
121
116
|
}
|
|
122
117
|
catch (e) {
|
|
@@ -297,19 +292,15 @@ function genomeHighlightsToVisibleColumns(self, field) {
|
|
|
297
292
|
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
298
293
|
return [];
|
|
299
294
|
}
|
|
300
|
-
const { g2p } = transcriptToMsaMap;
|
|
301
295
|
const columns = new Set();
|
|
302
296
|
for (const view of getProteinViews(getSession(self).views)) {
|
|
303
297
|
for (const structure of view.structures) {
|
|
304
298
|
if (structure.connectedViewId !== connectedViewId) {
|
|
305
299
|
continue;
|
|
306
300
|
}
|
|
307
|
-
for (const
|
|
308
|
-
for (
|
|
309
|
-
const
|
|
310
|
-
const col = proteinPos === undefined
|
|
311
|
-
? undefined
|
|
312
|
-
: transcriptPosToVisibleCol(self, proteinPos);
|
|
301
|
+
for (const { start, end } of structure[field] ?? []) {
|
|
302
|
+
for (const proteinPos of proteinPositionsInRange(transcriptToMsaMap, start, end)) {
|
|
303
|
+
const col = transcriptPosToVisibleCol(self, proteinPos);
|
|
313
304
|
if (col !== undefined) {
|
|
314
305
|
columns.add(col);
|
|
315
306
|
}
|
|
@@ -25,7 +25,7 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
|
|
|
25
25
|
const expired = !!request && !!model.error && !model.dataInitialized;
|
|
26
26
|
const launching = pending || expired || (!!init && !model.dataInitialized);
|
|
27
27
|
return (React.createElement(ErrorBoundary, null,
|
|
28
|
-
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
|
|
28
|
+
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData && !model.dataInitialized ? (React.createElement("div", { className: classes.loadingContainer },
|
|
29
29
|
React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
|
|
30
30
|
});
|
|
31
31
|
export default MsaViewPanel;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
2
2
|
import { saveBlastResult } from '../utils/blastCache';
|
|
3
|
+
import { checkPairSize } from '../utils/browserAlign';
|
|
3
4
|
import { searchBackends } from '../utils/homologSearch';
|
|
4
5
|
import { launchMSA } from '../utils/msa';
|
|
5
6
|
import { buildSearchMsa } from '../utils/msaRows';
|
|
7
|
+
import { getCachedSearch, saveSearch, searchKey } from '../utils/searchCache';
|
|
6
8
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
7
9
|
import { resolveUniProtEntry } from '../utils/unirefHomologs';
|
|
8
10
|
import { transcriptFields, transcriptName } from './util';
|
|
@@ -38,23 +40,13 @@ async function resolveQuery(self, scope) {
|
|
|
38
40
|
}
|
|
39
41
|
throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
* A similarity search, then an alignment of what it found. The program is a
|
|
43
|
-
* backend behind one interface (utils/homologSearch.ts); what differs between
|
|
44
|
-
* them is settled by whether the result came back aligned. A program that
|
|
45
|
-
* aligns as it searches (phmmer) hands over the alignment and the tree is
|
|
46
|
-
* built from it in the browser; one that does not (blastp) hands over bare
|
|
47
|
-
* hits and the chosen aligner runs on them.
|
|
48
|
-
*/
|
|
49
|
-
export async function doLaunchBlast({ self, scope, }) {
|
|
43
|
+
async function search({ self, scope, query, querySeqName, key, }) {
|
|
50
44
|
const params = self.blastParams;
|
|
51
|
-
const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
|
|
52
|
-
const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
|
|
53
45
|
const { onProgress, onRid, signal } = scope;
|
|
54
|
-
const { hits, queryRow, rid } = await searchBackends[searchProgram]({
|
|
46
|
+
const { hits, queryRow, rid } = await searchBackends[params.searchProgram ?? 'blastp']({
|
|
55
47
|
query,
|
|
56
48
|
database: params.blastDatabase,
|
|
57
|
-
maxHits,
|
|
49
|
+
maxHits: params.maxHits,
|
|
58
50
|
onProgress,
|
|
59
51
|
onRid,
|
|
60
52
|
signal,
|
|
@@ -63,7 +55,7 @@ export async function doLaunchBlast({ self, scope, }) {
|
|
|
63
55
|
throw new Error('No hits found');
|
|
64
56
|
}
|
|
65
57
|
onProgress('Fetching species taxonomy info...');
|
|
66
|
-
const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
|
|
58
|
+
const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined), signal);
|
|
67
59
|
const { msa: fasta, treeMetadata } = buildSearchMsa({
|
|
68
60
|
hits,
|
|
69
61
|
query,
|
|
@@ -71,10 +63,44 @@ export async function doLaunchBlast({ self, scope, }) {
|
|
|
71
63
|
taxonomyInfo,
|
|
72
64
|
querySeqName,
|
|
73
65
|
});
|
|
66
|
+
if (!queryRow) {
|
|
67
|
+
await saveSearch({ id: key, fasta, treeMetadata, rid });
|
|
68
|
+
}
|
|
69
|
+
return { fasta, treeMetadata, rid, queryRow };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A similarity search, then an alignment of what it found. The program is a
|
|
73
|
+
* backend behind one interface (utils/homologSearch.ts); what differs between
|
|
74
|
+
* them is settled by whether the result came back aligned. A program that
|
|
75
|
+
* aligns as it searches (phmmer) hands over the alignment and the tree is
|
|
76
|
+
* built from it in the browser; one that does not (blastp) hands over bare
|
|
77
|
+
* hits and the chosen aligner runs on them.
|
|
78
|
+
*/
|
|
79
|
+
export async function doLaunchBlast({ self, scope, }) {
|
|
80
|
+
const params = self.blastParams;
|
|
81
|
+
const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
|
|
82
|
+
const msaAlgorithm = params.msaAlgorithm ?? 'browser';
|
|
83
|
+
const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
|
|
84
|
+
if (searchProgram === 'blastp' && msaAlgorithm === 'browser') {
|
|
85
|
+
checkPairSize(querySeqName, query.length, query.length);
|
|
86
|
+
}
|
|
87
|
+
const { onProgress, signal } = scope;
|
|
88
|
+
const key = searchKey({
|
|
89
|
+
searchProgram,
|
|
90
|
+
database: params.blastDatabase,
|
|
91
|
+
maxHits,
|
|
92
|
+
querySeqName,
|
|
93
|
+
query,
|
|
94
|
+
});
|
|
95
|
+
const found = await getCachedSearch(key);
|
|
96
|
+
if (found) {
|
|
97
|
+
onProgress(`Reusing the ${searchProgram} search from ${new Date(found.timestamp).toLocaleString()}...`);
|
|
98
|
+
}
|
|
99
|
+
const { fasta, treeMetadata, rid, queryRow } = found ?? (await search({ self, scope, query, querySeqName, key }));
|
|
74
100
|
const { msa, tree } = queryRow
|
|
75
101
|
? { msa: fasta, tree: '' }
|
|
76
102
|
: await launchMSA({
|
|
77
|
-
algorithm:
|
|
103
|
+
algorithm: msaAlgorithm,
|
|
78
104
|
sequence: fasta,
|
|
79
105
|
onProgress,
|
|
80
106
|
signal,
|
|
@@ -84,7 +110,7 @@ export async function doLaunchBlast({ self, scope, }) {
|
|
|
84
110
|
await saveBlastResult({
|
|
85
111
|
proteinSequence: query,
|
|
86
112
|
blastDatabase: params.blastDatabase,
|
|
87
|
-
msaAlgorithm: queryRow ? undefined :
|
|
113
|
+
msaAlgorithm: queryRow ? undefined : msaAlgorithm,
|
|
88
114
|
searchProgram: params.searchProgram,
|
|
89
115
|
maxHits,
|
|
90
116
|
msa,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { beforeEach, expect, test, vi } from 'vitest';
|
|
2
2
|
import { saveBlastResult } from '../utils/blastCache';
|
|
3
|
+
import { MAX_PAIR_CELLS } from '../utils/browserAlign';
|
|
3
4
|
import { searchBackends } from '../utils/homologSearch';
|
|
4
5
|
import { launchMSA } from '../utils/msa';
|
|
6
|
+
import { getCachedSearch, saveSearch } from '../utils/searchCache';
|
|
5
7
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
8
|
import { resolveUniProtEntry } from '../utils/unirefHomologs';
|
|
7
9
|
import { doLaunchBlast } from './doLaunchBlast';
|
|
@@ -15,6 +17,11 @@ vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
|
|
|
15
17
|
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
|
|
16
18
|
vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
|
|
17
19
|
vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
|
|
20
|
+
vi.mock('../utils/searchCache', async (importOriginal) => ({
|
|
21
|
+
...(await importOriginal()),
|
|
22
|
+
getCachedSearch: vi.fn(),
|
|
23
|
+
saveSearch: vi.fn(),
|
|
24
|
+
}));
|
|
18
25
|
const blastp = vi.mocked(searchBackends.blastp);
|
|
19
26
|
const phmmer = vi.mocked(searchBackends.phmmer);
|
|
20
27
|
const mockLaunchMSA = vi.mocked(launchMSA);
|
|
@@ -44,6 +51,8 @@ beforeEach(() => {
|
|
|
44
51
|
vi.clearAllMocks();
|
|
45
52
|
vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
|
|
46
53
|
vi.mocked(saveBlastResult).mockResolvedValue(undefined);
|
|
54
|
+
vi.mocked(getCachedSearch).mockResolvedValue(undefined);
|
|
55
|
+
vi.mocked(saveSearch).mockResolvedValue(undefined);
|
|
47
56
|
});
|
|
48
57
|
test('bare hits go to the chosen aligner, with the query first', async () => {
|
|
49
58
|
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
@@ -168,3 +177,48 @@ test('a live Feature handed over in the same session labels it the same way', as
|
|
|
168
177
|
geneName: 'TP53',
|
|
169
178
|
}));
|
|
170
179
|
});
|
|
180
|
+
const BLASTP_BROWSER = {
|
|
181
|
+
searchProgram: 'blastp',
|
|
182
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
183
|
+
msaAlgorithm: 'browser',
|
|
184
|
+
proteinSequence: 'MKWVTF',
|
|
185
|
+
};
|
|
186
|
+
test('the hits are saved before the aligner runs, so a failed alignment keeps them', async () => {
|
|
187
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
188
|
+
mockLaunchMSA.mockRejectedValue(new Error('aligner fell over'));
|
|
189
|
+
await expect(launch(makeModel(BLASTP_BROWSER))).rejects.toThrow('aligner fell over');
|
|
190
|
+
expect(saveSearch).toHaveBeenCalledWith(expect.objectContaining({
|
|
191
|
+
fasta: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
|
|
192
|
+
rid: 'job',
|
|
193
|
+
}));
|
|
194
|
+
});
|
|
195
|
+
test('a relaunch with saved hits aligns them without searching again', async () => {
|
|
196
|
+
vi.mocked(getCachedSearch).mockResolvedValue({
|
|
197
|
+
id: 'k',
|
|
198
|
+
fasta: '>QUERY\nMKWVTF\n>P1\nMKWV',
|
|
199
|
+
treeMetadata: {},
|
|
200
|
+
rid: 'job',
|
|
201
|
+
timestamp: 0,
|
|
202
|
+
});
|
|
203
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: '' });
|
|
204
|
+
const result = await launch(makeModel(BLASTP_BROWSER));
|
|
205
|
+
expect(blastp).not.toHaveBeenCalled();
|
|
206
|
+
expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({ sequence: '>QUERY\nMKWVTF\n>P1\nMKWV' }));
|
|
207
|
+
expect(result.msa).toBe('aligned');
|
|
208
|
+
});
|
|
209
|
+
test('a query too long for the in-browser aligner is refused before the search', async () => {
|
|
210
|
+
const side = Math.ceil(Math.sqrt(MAX_PAIR_CELLS)) + 1;
|
|
211
|
+
await expect(launch(makeModel({ ...BLASTP_BROWSER, proteinSequence: 'M'.repeat(side) }))).rejects.toThrow(/too large to align in the browser/);
|
|
212
|
+
expect(blastp).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
test('the same long query goes to an EBI aligner without complaint', async () => {
|
|
215
|
+
const side = Math.ceil(Math.sqrt(MAX_PAIR_CELLS)) + 1;
|
|
216
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
217
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
218
|
+
await launch(makeModel({
|
|
219
|
+
...BLASTP_BROWSER,
|
|
220
|
+
msaAlgorithm: 'clustalo',
|
|
221
|
+
proteinSequence: 'M'.repeat(side),
|
|
222
|
+
}));
|
|
223
|
+
expect(blastp).toHaveBeenCalled();
|
|
224
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
2
|
+
import { isAbortError } from '../utils/fetch';
|
|
2
3
|
import { launchMSA } from '../utils/msa';
|
|
3
4
|
import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
4
5
|
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
@@ -33,6 +34,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
|
|
|
33
34
|
exclude: taxId,
|
|
34
35
|
limit: maxSpecies,
|
|
35
36
|
onProgress,
|
|
37
|
+
signal,
|
|
36
38
|
};
|
|
37
39
|
const { geneId, representative, rows } = source === 'panther'
|
|
38
40
|
? await findPantherOrthologs(request)
|
|
@@ -41,7 +43,6 @@ export async function doLaunchOrthologs({ self, scope, }) {
|
|
|
41
43
|
...request,
|
|
42
44
|
identity,
|
|
43
45
|
referenceProteomesOnly,
|
|
44
|
-
signal,
|
|
45
46
|
})
|
|
46
47
|
: await findNcbiOrthologs(request);
|
|
47
48
|
// The query row. The dialog always supplies it — it is the user's OWN
|
|
@@ -68,7 +69,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
|
|
|
68
69
|
// taxon is excluded from that set, but a subspecies can sanitize to the same
|
|
69
70
|
// token, and a collision would silently point the coordinate mapping at
|
|
70
71
|
// another animal's row.
|
|
71
|
-
const queryLabel = await queryRowLabel(taxId, rows);
|
|
72
|
+
const queryLabel = await queryRowLabel(taxId, rows, signal);
|
|
72
73
|
act(() => {
|
|
73
74
|
self.setQuerySeqName(queryLabel);
|
|
74
75
|
});
|
|
@@ -98,16 +99,17 @@ export async function doLaunchOrthologs({ self, scope, }) {
|
|
|
98
99
|
* wants "this gene across species" gets NCBI's own order, which leads with the
|
|
99
100
|
* reference organisms.
|
|
100
101
|
*/
|
|
101
|
-
async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest }) {
|
|
102
|
+
async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, signal, ...rest }) {
|
|
102
103
|
onProgress('Resolving gene at NCBI...');
|
|
103
|
-
const resolved = await resolveGeneId(geneCandidates, taxId);
|
|
104
|
+
const resolved = await resolveGeneId(geneCandidates, taxId, signal);
|
|
104
105
|
if (!resolved) {
|
|
105
106
|
throw new Error(`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`);
|
|
106
107
|
}
|
|
107
|
-
const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
|
|
108
|
+
const representative = await fetchRepresentativeQueryProtein(resolved.geneId, signal);
|
|
108
109
|
const rows = await fetchOrthologRows({
|
|
109
110
|
geneId: resolved.geneId,
|
|
110
111
|
onProgress,
|
|
112
|
+
signal,
|
|
111
113
|
...rest,
|
|
112
114
|
});
|
|
113
115
|
return { geneId: resolved.geneId, representative, rows };
|
|
@@ -122,12 +124,11 @@ const sourceNames = {
|
|
|
122
124
|
* entry the cluster was looked up from, whose sequence a spec-launched row
|
|
123
125
|
* takes and whose accession earns the CDD overlay when the two match.
|
|
124
126
|
*/
|
|
125
|
-
async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly,
|
|
127
|
+
async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, ...rest }) {
|
|
126
128
|
const found = await fetchUnirefHomologs({
|
|
127
129
|
candidates: geneCandidates,
|
|
128
130
|
identity,
|
|
129
131
|
referenceProteomesOnly,
|
|
130
|
-
signal,
|
|
131
132
|
...rest,
|
|
132
133
|
});
|
|
133
134
|
return {
|
|
@@ -160,13 +161,16 @@ async function findPantherOrthologs({ geneCandidates, ...rest }) {
|
|
|
160
161
|
* marker when NCBI cannot name the taxon, which is a naming failure and must not
|
|
161
162
|
* take down the launch.
|
|
162
163
|
*/
|
|
163
|
-
async function queryRowLabel(taxId, rows) {
|
|
164
|
+
async function queryRowLabel(taxId, rows, signal) {
|
|
164
165
|
let name;
|
|
165
166
|
try {
|
|
166
|
-
const info = (await fetchTaxonomyInfo([taxId])).get(taxId);
|
|
167
|
+
const info = (await fetchTaxonomyInfo([taxId], signal)).get(taxId);
|
|
167
168
|
name = info?.commonName ?? info?.sciname;
|
|
168
169
|
}
|
|
169
170
|
catch (e) {
|
|
171
|
+
if (isAbortError(e)) {
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
170
174
|
console.warn('[msaview-orthologs] taxonomy name lookup failed:', e);
|
|
171
175
|
}
|
|
172
176
|
return dedupeLabels([
|
|
@@ -179,11 +183,14 @@ async function queryRowLabel(taxId, rows) {
|
|
|
179
183
|
* that supplied no sequence of its own, the alignment — so it is reported by
|
|
180
184
|
* returning nothing rather than by throwing here.
|
|
181
185
|
*/
|
|
182
|
-
async function fetchRepresentativeQueryProtein(geneId) {
|
|
186
|
+
async function fetchRepresentativeQueryProtein(geneId, signal) {
|
|
183
187
|
try {
|
|
184
|
-
return await fetchProteinForGene(geneId);
|
|
188
|
+
return await fetchProteinForGene(geneId, signal);
|
|
185
189
|
}
|
|
186
190
|
catch (e) {
|
|
191
|
+
if (isAbortError(e)) {
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
187
194
|
console.warn('[msaview-orthologs] query protein lookup failed:', e);
|
|
188
195
|
return undefined;
|
|
189
196
|
}
|
|
@@ -8,7 +8,6 @@ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
|
|
|
8
8
|
vi.mock('./doLaunchBlast', () => ({ doLaunchBlast: vi.fn() }));
|
|
9
9
|
vi.mock('./msaDataStore', () => ({
|
|
10
10
|
cleanupOldData: vi.fn(async () => { }),
|
|
11
|
-
deleteMsaData: vi.fn(async () => { }),
|
|
12
11
|
generateDataStoreId: vi.fn(),
|
|
13
12
|
retrieveMsaData: vi.fn(),
|
|
14
13
|
storeMsaData: vi.fn(),
|