jbrowse-plugin-msaview 3.4.1 → 3.5.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 +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +20 -12
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +1 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +8 -3
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
- package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
- package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
- package/dist/LaunchMsaView/detectQueryRow.js +11 -4
- package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
- package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.js +5 -1
- package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +8 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +21 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +3 -2
- package/dist/MsaViewPanel/afterCreateAutoruns.js +48 -19
- package/dist/MsaViewPanel/components/LaunchProgress.js +12 -3
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +23 -2
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +8 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +78 -93
- package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +40 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +10 -0
- package/dist/MsaViewPanel/genomeToMSA.js +4 -4
- package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
- package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
- package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
- package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
- package/dist/MsaViewPanel/model.d.ts +160 -89
- package/dist/MsaViewPanel/model.js +125 -48
- package/dist/MsaViewPanel/model.test.d.ts +1 -0
- package/dist/MsaViewPanel/model.test.js +100 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
- package/dist/MsaViewPanel/msaDataStore.d.ts +2 -0
- package/dist/MsaViewPanel/msaDataStore.js +10 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
- package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
- package/dist/MsaViewPanel/processInit.test.js +72 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.js +35 -0
- package/dist/MsaViewPanel/runLaunch.test.js +46 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
- package/dist/MsaViewPanel/structureConnection.js +0 -19
- package/dist/MsaViewPanel/util.d.ts +39 -0
- package/dist/MsaViewPanel/util.js +44 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +44 -43
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/browserAlign.d.ts +42 -0
- package/dist/utils/browserAlign.js +297 -0
- package/dist/utils/browserAlign.test.d.ts +1 -0
- package/dist/utils/browserAlign.test.js +85 -0
- package/dist/utils/ebiBlast.d.ts +10 -1
- package/dist/utils/ebiBlast.js +27 -1
- package/dist/utils/homologSearch.d.ts +31 -0
- package/dist/utils/homologSearch.js +6 -0
- package/dist/utils/msa.d.ts +3 -11
- package/dist/utils/msa.js +14 -29
- package/dist/utils/msaRows.d.ts +11 -8
- package/dist/utils/msaRows.js +14 -12
- package/dist/utils/phmmer.d.ts +11 -1
- package/dist/utils/phmmer.js +49 -12
- package/dist/utils/unirefHomologs.d.ts +94 -0
- package/dist/utils/unirefHomologs.js +193 -0
- package/dist/utils/unirefHomologs.test.d.ts +1 -0
- package/dist/utils/unirefHomologs.test.js +118 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +38 -13
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +10 -3
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
- package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
- package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
- package/src/LaunchMsaView/detectQueryRow.ts +22 -4
- package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
- package/src/LaunchMsaView/useQueryRowName.ts +5 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +23 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +32 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +53 -18
- package/src/MsaViewPanel/components/LaunchProgress.tsx +26 -2
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +24 -2
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
- package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +92 -142
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +13 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +55 -5
- package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
- package/src/MsaViewPanel/genomeToMSA.ts +8 -4
- package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
- package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
- package/src/MsaViewPanel/model.test.ts +121 -0
- package/src/MsaViewPanel/model.ts +153 -55
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
- package/src/MsaViewPanel/msaDataStore.ts +10 -0
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
- package/src/MsaViewPanel/processInit.test.ts +84 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
- package/src/MsaViewPanel/runLaunch.test.ts +54 -0
- package/src/MsaViewPanel/runLaunch.ts +39 -0
- package/src/MsaViewPanel/structureConnection.ts +0 -26
- package/src/MsaViewPanel/util.ts +75 -0
- package/src/utils/browserAlign.test.ts +102 -0
- package/src/utils/browserAlign.ts +352 -0
- package/src/utils/ebiBlast.ts +34 -0
- package/src/utils/homologSearch.ts +51 -0
- package/src/utils/msa.ts +19 -41
- package/src/utils/msaRows.ts +20 -16
- package/src/utils/phmmer.ts +57 -11
- package/src/utils/unirefHomologs.test.ts +149 -0
- package/src/utils/unirefHomologs.ts +323 -0
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/structureConnection.test.js +0 -53
- package/src/MsaViewPanel/structureConnection.test.ts +0 -62
- /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/useQueryRowName.test.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ 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 { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
|
|
11
|
+
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
|
|
12
12
|
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. Relaunch the alignment to rebuild it.";
|
|
13
13
|
export function loadStoredData(self) {
|
|
14
14
|
const { dataStoreId, rows } = self;
|
|
@@ -73,16 +73,20 @@ function sameData(a, b) {
|
|
|
73
73
|
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
74
74
|
* IndexedDB (private mode) fails once rather than in a loop.
|
|
75
75
|
*
|
|
76
|
-
* A view whose data comes from a filehandle
|
|
77
|
-
*
|
|
76
|
+
* A view whose data comes from a filehandle -- or from the indexed block its
|
|
77
|
+
* kept `init` names -- stores nothing at all: the file is the source of truth
|
|
78
|
+
* and it is refetched at startup.
|
|
78
79
|
*/
|
|
79
80
|
export function storeDataToIndexedDB(self) {
|
|
80
|
-
const { rows, dataStoreId, isStoringData, lastStoredData } = self;
|
|
81
|
+
const { rows, dataStoreId, isStoringData, lastStoredData, init } = self;
|
|
81
82
|
const data = currentData(self);
|
|
82
83
|
if (rows.length === 0 ||
|
|
83
84
|
isStoringData ||
|
|
84
85
|
self.msaFilehandle ||
|
|
85
86
|
self.treeFilehandle ||
|
|
87
|
+
// an indexed view keeps its init and refetches the block, so a row here
|
|
88
|
+
// would be one nothing ever reads
|
|
89
|
+
!!init?.msaIndexedLocation ||
|
|
86
90
|
!(data.msa || data.tree) ||
|
|
87
91
|
sameData(lastStoredData, data)) {
|
|
88
92
|
return;
|
|
@@ -117,7 +121,7 @@ export function storeDataToIndexedDB(self) {
|
|
|
117
121
|
* until a new request replaces them.
|
|
118
122
|
*/
|
|
119
123
|
export function launchOrthologsIfNeeded(self) {
|
|
120
|
-
if (self.orthologParams) {
|
|
124
|
+
if (self.orthologParams && !awaitingTranscript(self)) {
|
|
121
125
|
runLaunch({
|
|
122
126
|
self,
|
|
123
127
|
message: 'Resolving orthologs',
|
|
@@ -128,8 +132,17 @@ export function launchOrthologsIfNeeded(self) {
|
|
|
128
132
|
});
|
|
129
133
|
}
|
|
130
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* A launch that names its transcript rather than its feature has to wait for
|
|
137
|
+
* the lookup: the query row is that transcript's translation, and the launch
|
|
138
|
+
* cannot start without a query. Both launchers read the same two fields, so
|
|
139
|
+
* the resolver setting `connectedFeature` is what refires them.
|
|
140
|
+
*/
|
|
141
|
+
function awaitingTranscript(self) {
|
|
142
|
+
return !!self.connectedTranscript && !self.connectedFeature;
|
|
143
|
+
}
|
|
131
144
|
export function launchBlastIfNeeded(self) {
|
|
132
|
-
if (self.blastParams) {
|
|
145
|
+
if (self.blastParams && !awaitingTranscript(self)) {
|
|
133
146
|
runLaunch({
|
|
134
147
|
self,
|
|
135
148
|
message: 'Submitting query',
|
|
@@ -167,15 +180,28 @@ export function autoLoadProteinDomains(self) {
|
|
|
167
180
|
})();
|
|
168
181
|
}
|
|
169
182
|
}
|
|
170
|
-
// Resolve the declarative `init` launch contract
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
183
|
+
// Resolve the declarative `init` launch contract. msaUrl is handed to
|
|
184
|
+
// react-msaview's native filehandle loader (openLocation + progress + abort +
|
|
185
|
+
// CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip name-indexed
|
|
186
|
+
// block is the one source with no native loader, so it's fetched here. Inline
|
|
187
|
+
// data and tree URLs arrive as native snapshot props, not via init.
|
|
188
|
+
//
|
|
189
|
+
// An init that named the indexed block is KEPT rather than cleared, as
|
|
190
|
+
// jbrowse-plugin-tview keeps its own: what it resolves to is one alignment
|
|
191
|
+
// string, react-msaview drops a document over 50kb from the snapshot, and there
|
|
192
|
+
// is no filehandle to reload it from -- so a shared session came back saying the
|
|
193
|
+
// alignment had expired. The init is both smaller than what it fetches and the
|
|
194
|
+
// only durable statement of what the view is.
|
|
175
195
|
export function processInit(self) {
|
|
176
196
|
const { init } = self;
|
|
177
197
|
if (init) {
|
|
178
198
|
const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
|
|
199
|
+
const indexed = !!(msaIndexedLocation && msaName);
|
|
200
|
+
// a kept init re-runs this on every session restore; the alignment already
|
|
201
|
+
// in hand is the one it would fetch
|
|
202
|
+
if (indexed && self.data.msa) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
179
205
|
void (async () => {
|
|
180
206
|
try {
|
|
181
207
|
self.setError(undefined);
|
|
@@ -204,7 +230,9 @@ export function processInit(self) {
|
|
|
204
230
|
throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
|
|
205
231
|
}
|
|
206
232
|
}
|
|
207
|
-
|
|
233
|
+
if (!indexed) {
|
|
234
|
+
self.setInit(undefined);
|
|
235
|
+
}
|
|
208
236
|
}
|
|
209
237
|
catch (e) {
|
|
210
238
|
self.setError(e);
|
|
@@ -238,10 +266,10 @@ export function syncGenomeHoverToMsaColumn(self) {
|
|
|
238
266
|
* Translate genome regions published by a 3D protein view into this MSA's
|
|
239
267
|
* visible columns. The genome is the only coordinate space the two plugins
|
|
240
268
|
* share, so the hops are genome coord -> protein position (the transcript's g2p
|
|
241
|
-
* map) ->
|
|
269
|
+
* map) -> visible column.
|
|
242
270
|
*/
|
|
243
271
|
function genomeHighlightsToVisibleColumns(self, field) {
|
|
244
|
-
const { connectedViewId, transcriptToMsaMap
|
|
272
|
+
const { connectedViewId, transcriptToMsaMap } = self;
|
|
245
273
|
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
246
274
|
return [];
|
|
247
275
|
}
|
|
@@ -255,16 +283,17 @@ function genomeHighlightsToVisibleColumns(self, field) {
|
|
|
255
283
|
for (const highlight of structure[field] ?? []) {
|
|
256
284
|
for (let coord = highlight.start; coord < highlight.end; coord++) {
|
|
257
285
|
const proteinPos = g2p[coord];
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
const col = proteinPos === undefined
|
|
287
|
+
? undefined
|
|
288
|
+
: transcriptPosToVisibleCol(self, proteinPos);
|
|
289
|
+
if (col !== undefined) {
|
|
290
|
+
columns.add(col);
|
|
260
291
|
}
|
|
261
292
|
}
|
|
262
293
|
}
|
|
263
294
|
}
|
|
264
295
|
}
|
|
265
|
-
return [...columns]
|
|
266
|
-
.map(col => self.globalColToVisibleCol(col))
|
|
267
|
-
.filter((col) => col !== undefined);
|
|
296
|
+
return [...columns];
|
|
268
297
|
}
|
|
269
298
|
function sameColumns(a, b) {
|
|
270
299
|
if (!a || !b) {
|
|
@@ -29,16 +29,25 @@ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
|
|
|
29
29
|
const { blastParams, orthologParams, progress, rid, error } = model;
|
|
30
30
|
const { classes } = useStyles();
|
|
31
31
|
const message = blastParams
|
|
32
|
-
?
|
|
32
|
+
? `Running EBI ${blastParams.searchProgram === 'phmmer' ? 'phmmer' : 'BLAST'}`
|
|
33
33
|
: orthologParams
|
|
34
|
-
?
|
|
34
|
+
? orthologParams.source === 'uniref'
|
|
35
|
+
? 'Building UniRef homolog alignment'
|
|
36
|
+
: 'Building ortholog alignment'
|
|
35
37
|
: 'Loading alignment';
|
|
36
38
|
return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
|
|
37
39
|
React.createElement(Typography, { variant: "h5" },
|
|
38
40
|
message,
|
|
39
41
|
" failed"),
|
|
40
42
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
41
|
-
React.createElement(ErrorMessage, { error: error })
|
|
43
|
+
React.createElement(ErrorMessage, { error: error }),
|
|
44
|
+
React.createElement("div", { className: classes.progressRow },
|
|
45
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
46
|
+
model.retryLaunch();
|
|
47
|
+
} }, "Retry"),
|
|
48
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
49
|
+
model.cancelLaunch();
|
|
50
|
+
} }, "Dismiss")))) : (React.createElement(React.Fragment, null,
|
|
42
51
|
React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
|
|
43
52
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
44
53
|
React.createElement("div", { className: classes.progressRow },
|
|
@@ -14,8 +14,12 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
|
|
|
14
14
|
const { classes } = useStyles();
|
|
15
15
|
const { blastParams, orthologParams, init, loadingStoredData } = model;
|
|
16
16
|
// an unresolved launch request means there is no alignment to draw yet, so all
|
|
17
|
-
// three gate the same panel -- see LaunchProgress
|
|
18
|
-
|
|
17
|
+
// three gate the same panel -- see LaunchProgress. An indexed view keeps its
|
|
18
|
+
// init for the life of the view (it is how the block is refetched), so that
|
|
19
|
+
// one is only "launching" until the alignment arrives.
|
|
20
|
+
const launching = !!(blastParams ??
|
|
21
|
+
orthologParams ??
|
|
22
|
+
(init && !model.dataInitialized));
|
|
19
23
|
return (React.createElement(ErrorBoundary, null,
|
|
20
24
|
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
|
|
21
25
|
React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
|
|
@@ -48,6 +48,16 @@ test('a failed ortholog launch shows why', () => {
|
|
|
48
48
|
});
|
|
49
49
|
expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
|
|
50
50
|
});
|
|
51
|
+
// an indexed view keeps its init -- it is how the block is refetched on the
|
|
52
|
+
// next session -- so the panel cannot read init alone as "still launching"
|
|
53
|
+
test('an indexed view with its alignment draws it, init and all', () => {
|
|
54
|
+
panel({
|
|
55
|
+
init: { msaIndexedLocation: { uri: 'msa.fa.gz' }, msaName: 'ENST1' },
|
|
56
|
+
dataInitialized: true,
|
|
57
|
+
progress: '',
|
|
58
|
+
});
|
|
59
|
+
expect(screen.getByText('the alignment')).toBeTruthy();
|
|
60
|
+
});
|
|
51
61
|
test('a failed init shows why', () => {
|
|
52
62
|
panel({
|
|
53
63
|
init: { msaName: 'ENST00000288602' },
|
|
@@ -58,7 +68,7 @@ test('a failed init shows why', () => {
|
|
|
58
68
|
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
|
|
59
69
|
});
|
|
60
70
|
// a launch runs for 10+ minutes, so leaving with no way out means watching it
|
|
61
|
-
test('a running launch offers a way out
|
|
71
|
+
test('a running launch offers a way out', () => {
|
|
62
72
|
const cancelLaunch = vi.fn();
|
|
63
73
|
panel({
|
|
64
74
|
blastParams: { proteinSequence: 'MKV' },
|
|
@@ -67,14 +77,25 @@ test('a running launch offers a way out, and a failed one does not', () => {
|
|
|
67
77
|
});
|
|
68
78
|
screen.getByRole('button', { name: 'Cancel' }).click();
|
|
69
79
|
expect(cancelLaunch).toHaveBeenCalled();
|
|
70
|
-
|
|
80
|
+
});
|
|
81
|
+
// the request outlives the failure -- it is what a reload resubmits -- so a
|
|
82
|
+
// failed launch that only drew the error was a dead end that re-ran the EBI job
|
|
83
|
+
// on every reload
|
|
84
|
+
test('a failed launch can be retried or dismissed', () => {
|
|
85
|
+
const cancelLaunch = vi.fn();
|
|
86
|
+
const retryLaunch = vi.fn();
|
|
71
87
|
panel({
|
|
72
88
|
blastParams: { proteinSequence: 'MKV' },
|
|
73
89
|
progress: '',
|
|
74
90
|
error: new Error('No hits found'),
|
|
75
91
|
cancelLaunch,
|
|
92
|
+
retryLaunch,
|
|
76
93
|
});
|
|
77
94
|
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
|
|
95
|
+
screen.getByRole('button', { name: 'Retry' }).click();
|
|
96
|
+
expect(retryLaunch).toHaveBeenCalled();
|
|
97
|
+
screen.getByRole('button', { name: 'Dismiss' }).click();
|
|
98
|
+
expect(cancelLaunch).toHaveBeenCalled();
|
|
78
99
|
});
|
|
79
100
|
test('a running job links out to it', () => {
|
|
80
101
|
panel({
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
2
|
import type { LaunchScope } from './runLaunch';
|
|
3
|
+
/**
|
|
4
|
+
* A similarity search, then an alignment of what it found. The program is a
|
|
5
|
+
* backend behind one interface (utils/homologSearch.ts); what differs between
|
|
6
|
+
* them is settled by whether the result came back aligned. A program that
|
|
7
|
+
* aligns as it searches (phmmer) hands over the alignment and the tree is
|
|
8
|
+
* built from it in the browser; one that does not (blastp) hands over bare
|
|
9
|
+
* hits and the chosen aligner runs on them.
|
|
10
|
+
*/
|
|
3
11
|
export declare function doLaunchBlast({ self, scope, }: {
|
|
4
12
|
self: JBrowsePluginMsaViewModel;
|
|
5
13
|
scope: LaunchScope;
|
|
@@ -1,114 +1,99 @@
|
|
|
1
|
-
import { makeId, strip } from '../LaunchMsaView/components/util';
|
|
2
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
3
2
|
import { saveBlastResult } from '../utils/blastCache';
|
|
4
|
-
import {
|
|
5
|
-
import { launchMSA
|
|
6
|
-
import {
|
|
7
|
-
import { queryPhmmer } from '../utils/phmmer';
|
|
3
|
+
import { searchBackends } from '../utils/homologSearch';
|
|
4
|
+
import { launchMSA } from '../utils/msa';
|
|
5
|
+
import { buildSearchMsa } from '../utils/msaRows';
|
|
8
6
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { selectedTranscript } = params;
|
|
14
|
-
const cleanedSeq = cleanProteinSequence(params.proteinSequence);
|
|
15
|
-
const { onProgress, onRid, signal } = scope;
|
|
16
|
-
const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
|
|
17
|
-
? await runPhmmer({
|
|
18
|
-
query: cleanedSeq,
|
|
19
|
-
database: params.blastDatabase,
|
|
20
|
-
onProgress,
|
|
21
|
-
onRid,
|
|
22
|
-
signal,
|
|
23
|
-
})
|
|
24
|
-
: await runBlast({
|
|
25
|
-
query: cleanedSeq,
|
|
26
|
-
blastDatabase: params.blastDatabase,
|
|
27
|
-
msaAlgorithm: params.msaAlgorithm,
|
|
28
|
-
onProgress,
|
|
29
|
-
onRid,
|
|
30
|
-
signal,
|
|
31
|
-
});
|
|
32
|
-
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
33
|
-
await saveBlastResult({
|
|
34
|
-
proteinSequence: cleanedSeq,
|
|
35
|
-
blastDatabase: params.blastDatabase,
|
|
36
|
-
msaAlgorithm: params.msaAlgorithm,
|
|
37
|
-
searchProgram: params.searchProgram,
|
|
38
|
-
msa,
|
|
39
|
-
tree,
|
|
40
|
-
treeMetadata: treeMetadataJson,
|
|
41
|
-
rid,
|
|
42
|
-
geneId: selectedTranscript?.get('parentId'),
|
|
43
|
-
transcriptId: selectedTranscript?.id(),
|
|
44
|
-
transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
|
|
45
|
-
geneName: selectedTranscript?.get('gene_name') ??
|
|
46
|
-
selectedTranscript?.get('parentId'),
|
|
47
|
-
});
|
|
48
|
-
return { msa, tree, treeMetadata: treeMetadataJson };
|
|
7
|
+
import { resolveUniProtEntry } from '../utils/unirefHomologs';
|
|
8
|
+
import { transcriptFields, transcriptName } from './util';
|
|
9
|
+
function asString(val) {
|
|
10
|
+
return typeof val === 'string' ? val : undefined;
|
|
49
11
|
}
|
|
50
12
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
13
|
+
* The query sequence, and what its row is called. The dialog hands over the
|
|
14
|
+
* translated transcript and the row stays `QUERY`; a spec naming a UniProt
|
|
15
|
+
* accession has the sequence fetched and the row named after the entry
|
|
16
|
+
* (`P53_HUMAN_query`), since a search of swissprot returns the entry itself
|
|
17
|
+
* as a hit and two rows called the same thing collapse into one.
|
|
54
18
|
*/
|
|
55
|
-
async function
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
signal,
|
|
62
|
-
});
|
|
63
|
-
onProgress('Fetching species taxonomy info...');
|
|
64
|
-
const taxonomyInfo = await fetchTaxonomyInfo(hits
|
|
65
|
-
.map(h => h.description[0]?.taxid)
|
|
66
|
-
.filter((t) => t !== undefined));
|
|
67
|
-
const treeMetadata = {};
|
|
68
|
-
const sequences = hits.map(h => {
|
|
69
|
-
const desc = h.description[0] ?? {
|
|
70
|
-
accession: 'unknown',
|
|
71
|
-
id: 'unknown',
|
|
72
|
-
sciname: 'unknown',
|
|
19
|
+
async function resolveQuery(self, scope) {
|
|
20
|
+
const params = self.blastParams;
|
|
21
|
+
if (params.proteinSequence) {
|
|
22
|
+
return {
|
|
23
|
+
sequence: cleanProteinSequence(params.proteinSequence),
|
|
24
|
+
name: self.querySeqName,
|
|
73
25
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
26
|
+
}
|
|
27
|
+
if (params.accession) {
|
|
28
|
+
scope.onProgress(`Fetching ${params.accession} from UniProt...`);
|
|
29
|
+
const entry = await resolveUniProtEntry([params.accession], 0, scope.signal);
|
|
30
|
+
if (!entry) {
|
|
31
|
+
throw new Error(`UniProt has no entry ${params.accession}`);
|
|
32
|
+
}
|
|
33
|
+
const name = `${entry.id}_query`;
|
|
34
|
+
scope.act(() => {
|
|
35
|
+
self.setQuerySeqName(name);
|
|
36
|
+
});
|
|
37
|
+
return { sequence: entry.sequence, name };
|
|
38
|
+
}
|
|
39
|
+
throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
|
|
85
40
|
}
|
|
86
41
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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.
|
|
92
48
|
*/
|
|
93
|
-
async function
|
|
94
|
-
const
|
|
49
|
+
export async function doLaunchBlast({ self, scope, }) {
|
|
50
|
+
const params = self.blastParams;
|
|
51
|
+
const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
|
|
52
|
+
const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
|
|
53
|
+
const { onProgress, onRid, signal } = scope;
|
|
54
|
+
const { hits, queryRow, rid } = await searchBackends[searchProgram]({
|
|
95
55
|
query,
|
|
96
|
-
database,
|
|
56
|
+
database: params.blastDatabase,
|
|
57
|
+
maxHits,
|
|
97
58
|
onProgress,
|
|
98
59
|
onRid,
|
|
99
60
|
signal,
|
|
100
61
|
});
|
|
62
|
+
if (hits.length === 0) {
|
|
63
|
+
throw new Error('No hits found');
|
|
64
|
+
}
|
|
101
65
|
onProgress('Fetching species taxonomy info...');
|
|
102
|
-
const taxonomyInfo = await fetchTaxonomyInfo(
|
|
103
|
-
const { msa, treeMetadata } =
|
|
104
|
-
|
|
66
|
+
const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
|
|
67
|
+
const { msa: fasta, treeMetadata } = buildSearchMsa({
|
|
68
|
+
hits,
|
|
69
|
+
query,
|
|
105
70
|
queryRow,
|
|
106
71
|
taxonomyInfo,
|
|
72
|
+
querySeqName,
|
|
107
73
|
});
|
|
108
|
-
|
|
74
|
+
const { msa, tree } = queryRow
|
|
75
|
+
? { msa: fasta, tree: '' }
|
|
76
|
+
: await launchMSA({
|
|
77
|
+
algorithm: params.msaAlgorithm ?? 'browser',
|
|
78
|
+
sequence: fasta,
|
|
79
|
+
onProgress,
|
|
80
|
+
signal,
|
|
81
|
+
});
|
|
82
|
+
const transcript = transcriptFields(selectedTranscript);
|
|
83
|
+
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
84
|
+
await saveBlastResult({
|
|
85
|
+
proteinSequence: query,
|
|
86
|
+
blastDatabase: params.blastDatabase,
|
|
87
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
88
|
+
searchProgram: params.searchProgram,
|
|
109
89
|
msa,
|
|
110
|
-
tree
|
|
111
|
-
treeMetadata,
|
|
112
|
-
rid,
|
|
113
|
-
|
|
90
|
+
tree,
|
|
91
|
+
treeMetadata: treeMetadataJson,
|
|
92
|
+
rid: rid ?? '',
|
|
93
|
+
geneId: asString(transcript.parentId),
|
|
94
|
+
transcriptId: asString(transcript.uniqueId),
|
|
95
|
+
transcriptName: transcriptName(selectedTranscript),
|
|
96
|
+
geneName: asString(transcript.gene_name) ?? asString(transcript.parentId),
|
|
97
|
+
});
|
|
98
|
+
return { msa, tree, treeMetadata: treeMetadataJson };
|
|
114
99
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { beforeEach, expect, test, vi } from 'vitest';
|
|
2
|
+
import { saveBlastResult } from '../utils/blastCache';
|
|
3
|
+
import { searchBackends } from '../utils/homologSearch';
|
|
4
|
+
import { launchMSA } from '../utils/msa';
|
|
5
|
+
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
|
+
import { resolveUniProtEntry } from '../utils/unirefHomologs';
|
|
7
|
+
import { doLaunchBlast } from './doLaunchBlast';
|
|
8
|
+
// Every network call is mocked: what is under test is how the launch turns a
|
|
9
|
+
// request into a search and the search's answer into rows -- which backend it
|
|
10
|
+
// asks, where the query comes from, and whether an aligner runs.
|
|
11
|
+
vi.mock('../utils/homologSearch', () => ({
|
|
12
|
+
searchBackends: { blastp: vi.fn(), phmmer: vi.fn() },
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
|
|
15
|
+
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
|
|
16
|
+
vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
|
|
17
|
+
vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
|
|
18
|
+
const blastp = vi.mocked(searchBackends.blastp);
|
|
19
|
+
const phmmer = vi.mocked(searchBackends.phmmer);
|
|
20
|
+
const mockLaunchMSA = vi.mocked(launchMSA);
|
|
21
|
+
const setQuerySeqName = vi.fn();
|
|
22
|
+
const HIT = { accession: 'P1', id: 'P1_MOUSE', sciname: 'Mus musculus' };
|
|
23
|
+
function makeModel(blastParams) {
|
|
24
|
+
return {
|
|
25
|
+
blastParams,
|
|
26
|
+
querySeqName: 'QUERY',
|
|
27
|
+
setQuerySeqName,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function launch(self) {
|
|
31
|
+
return doLaunchBlast({
|
|
32
|
+
self,
|
|
33
|
+
scope: {
|
|
34
|
+
signal: new AbortController().signal,
|
|
35
|
+
act: fn => {
|
|
36
|
+
fn();
|
|
37
|
+
},
|
|
38
|
+
onProgress: () => { },
|
|
39
|
+
onRid: () => { },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
|
|
46
|
+
vi.mocked(saveBlastResult).mockResolvedValue(undefined);
|
|
47
|
+
});
|
|
48
|
+
test('bare hits go to the chosen aligner, with the query first', async () => {
|
|
49
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
50
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
51
|
+
const result = await launch(makeModel({
|
|
52
|
+
searchProgram: 'blastp',
|
|
53
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
54
|
+
msaAlgorithm: 'clustalo',
|
|
55
|
+
maxHits: 20,
|
|
56
|
+
proteinSequence: 'MKWVTF*',
|
|
57
|
+
}));
|
|
58
|
+
expect(blastp).toHaveBeenCalledWith(expect.objectContaining({
|
|
59
|
+
query: 'MKWVTF',
|
|
60
|
+
database: 'uniprotkb_swissprot',
|
|
61
|
+
maxHits: 20,
|
|
62
|
+
}));
|
|
63
|
+
expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({
|
|
64
|
+
algorithm: 'clustalo',
|
|
65
|
+
sequence: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
|
|
66
|
+
}));
|
|
67
|
+
expect(result.msa).toBe('aligned');
|
|
68
|
+
expect(result.tree).toBe('tree');
|
|
69
|
+
});
|
|
70
|
+
test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
|
|
71
|
+
phmmer.mockResolvedValue({
|
|
72
|
+
rid: 'job',
|
|
73
|
+
queryRow: 'MKWV-TF',
|
|
74
|
+
hits: [{ ...HIT, sequence: 'MKWVSTF' }],
|
|
75
|
+
});
|
|
76
|
+
const result = await launch(makeModel({
|
|
77
|
+
searchProgram: 'phmmer',
|
|
78
|
+
blastDatabase: 'rp15',
|
|
79
|
+
proteinSequence: 'MKWVTF',
|
|
80
|
+
}));
|
|
81
|
+
expect(mockLaunchMSA).not.toHaveBeenCalled();
|
|
82
|
+
expect(result.msa).toBe('>QUERY\nMKWV-TF\n>P1-Mus_musculus\nMKWVSTF');
|
|
83
|
+
expect(result.tree).toBe('');
|
|
84
|
+
});
|
|
85
|
+
test('a UniProt accession supplies the query and names its row', async () => {
|
|
86
|
+
vi.mocked(resolveUniProtEntry).mockResolvedValue({
|
|
87
|
+
accession: 'P04637',
|
|
88
|
+
id: 'P53_HUMAN',
|
|
89
|
+
reviewed: true,
|
|
90
|
+
taxId: 9606,
|
|
91
|
+
scientificName: 'Homo sapiens',
|
|
92
|
+
sequence: 'MEEPQ',
|
|
93
|
+
});
|
|
94
|
+
phmmer.mockResolvedValue({
|
|
95
|
+
queryRow: 'MEEPQ',
|
|
96
|
+
hits: [{ ...HIT, sequence: 'MEEPQ' }],
|
|
97
|
+
});
|
|
98
|
+
const result = await launch(makeModel({
|
|
99
|
+
searchProgram: 'phmmer',
|
|
100
|
+
blastDatabase: 'swissprot',
|
|
101
|
+
accession: 'P04637',
|
|
102
|
+
}));
|
|
103
|
+
expect(resolveUniProtEntry).toHaveBeenCalledWith(['P04637'], 0, expect.any(AbortSignal));
|
|
104
|
+
expect(setQuerySeqName).toHaveBeenCalledWith('P53_HUMAN_query');
|
|
105
|
+
expect(result.msa.split('\n')[0]).toBe('>P53_HUMAN_query');
|
|
106
|
+
});
|
|
107
|
+
test('a request with no query at all is refused before any search runs', async () => {
|
|
108
|
+
await expect(launch(makeModel({ searchProgram: 'phmmer', blastDatabase: 'swissprot' }))).rejects.toThrow(/connectedTranscript/);
|
|
109
|
+
expect(phmmer).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
// blastParams is a frozen snapshot property, so the Feature the dialog put in
|
|
112
|
+
// it comes back from a session reload as the plain JSON it serialized to. The
|
|
113
|
+
// launch used to call .get() on that, minutes after the EBI job the reload
|
|
114
|
+
// resubmitted had come back, and threw where nothing was catching.
|
|
115
|
+
test('a transcript restored from a session snapshot still labels the cache row', async () => {
|
|
116
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
117
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
118
|
+
await launch(makeModel({
|
|
119
|
+
searchProgram: 'blastp',
|
|
120
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
121
|
+
msaAlgorithm: 'clustalo',
|
|
122
|
+
proteinSequence: 'MKWV',
|
|
123
|
+
selectedTranscript: {
|
|
124
|
+
uniqueId: 'NM_000546.6',
|
|
125
|
+
name: 'TP53-201',
|
|
126
|
+
parentId: 'TP53',
|
|
127
|
+
gene_name: 'TP53',
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
|
|
131
|
+
geneId: 'TP53',
|
|
132
|
+
transcriptId: 'NM_000546.6',
|
|
133
|
+
transcriptName: 'TP53-201',
|
|
134
|
+
geneName: 'TP53',
|
|
135
|
+
}));
|
|
136
|
+
});
|
|
137
|
+
test('a live Feature handed over in the same session labels it the same way', async () => {
|
|
138
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
139
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
|
|
140
|
+
const json = {
|
|
141
|
+
uniqueId: 'NM_000546.6',
|
|
142
|
+
name: 'TP53-201',
|
|
143
|
+
parentId: 'TP53',
|
|
144
|
+
};
|
|
145
|
+
await launch(makeModel({
|
|
146
|
+
searchProgram: 'blastp',
|
|
147
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
148
|
+
msaAlgorithm: 'clustalo',
|
|
149
|
+
proteinSequence: 'MKWV',
|
|
150
|
+
selectedTranscript: { toJSON: () => json },
|
|
151
|
+
}));
|
|
152
|
+
expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
|
|
153
|
+
geneId: 'TP53',
|
|
154
|
+
transcriptId: 'NM_000546.6',
|
|
155
|
+
transcriptName: 'TP53-201',
|
|
156
|
+
geneName: 'TP53',
|
|
157
|
+
}));
|
|
158
|
+
});
|