jbrowse-plugin-msaview 3.4.0 → 3.4.1
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/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +4 -6
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +2 -1
- package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +93 -70
- package/dist/MsaViewPanel/components/LaunchProgress.js +11 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +19 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +10 -12
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +38 -24
- package/dist/MsaViewPanel/model.d.ts +160 -10
- package/dist/MsaViewPanel/model.js +51 -1
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +16 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
- package/dist/MsaViewPanel/runLaunch.js +65 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +129 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +35 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.js +32 -0
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/ebiBlast.d.ts +4 -2
- package/dist/utils/ebiBlast.js +6 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/msa.d.ts +4 -2
- package/dist/utils/msa.js +9 -5
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +2 -1
- package/dist/utils/phmmer.js +4 -2
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -14
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +7 -11
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +2 -2
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +100 -66
- package/src/MsaViewPanel/components/LaunchProgress.tsx +20 -2
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +21 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +14 -10
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +41 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +8 -4
- package/src/MsaViewPanel/model.ts +54 -1
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +22 -13
- package/src/MsaViewPanel/runLaunch.test.ts +154 -0
- package/src/MsaViewPanel/runLaunch.ts +102 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/ebiBlast.ts +8 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/msa.ts +11 -3
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +5 -1
- package/src/utils/poll.ts +8 -1
- package/src/version.ts +1 -1
|
@@ -12,7 +12,8 @@ import { useTranscriptSelection } from '../useTranscriptSelection';
|
|
|
12
12
|
import CachedBlastResults from './CachedBlastResults';
|
|
13
13
|
import MsaAlgorithmSelect from './MsaAlgorithmSelect';
|
|
14
14
|
import { blastLaunchView } from './blastLaunchView';
|
|
15
|
-
import { databaseOptionsFor,
|
|
15
|
+
import { databaseOptionsFor, defaultSearchFor, searchPrograms } from './consts';
|
|
16
|
+
import { useStoredMsaAlgorithm, useStoredSearchChoice, } from './searchChoiceStorage';
|
|
16
17
|
import { useCachedBlastResults } from './useCachedBlastResults';
|
|
17
18
|
const useStyles = makeStyles()({
|
|
18
19
|
selectField: {
|
|
@@ -38,11 +39,8 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
|
|
|
38
39
|
// one piece of state, not two: a program and a database that program does not
|
|
39
40
|
// have is a 400 from EBI minutes after Submit, and holding them apart is what
|
|
40
41
|
// would let them drift into that
|
|
41
|
-
const [search, setSearch] =
|
|
42
|
-
|
|
43
|
-
database: defaultBlastDatabase,
|
|
44
|
-
});
|
|
45
|
-
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] = useState('clustalo');
|
|
42
|
+
const [search, setSearch] = useStoredSearchChoice();
|
|
43
|
+
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] = useStoredMsaAlgorithm();
|
|
46
44
|
const isPhmmer = search.program === 'phmmer';
|
|
47
45
|
const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
48
46
|
const { results: cachedResults, error: cachedResultsError } = useCachedBlastResults(geneIds);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MsaAlgorithm, SearchChoice } from './consts';
|
|
2
|
+
export declare const SEARCH_CHOICE_STORAGE_KEY = "msaView-blastSearch";
|
|
3
|
+
export declare const MSA_ALGORITHM_STORAGE_KEY = "msaView-msaAlgorithm";
|
|
4
|
+
export declare function validSearchChoice(stored: unknown): SearchChoice;
|
|
5
|
+
export declare function validMsaAlgorithm(stored: unknown): MsaAlgorithm;
|
|
6
|
+
export declare function useStoredSearchChoice(): readonly [SearchChoice, (choice: SearchChoice) => void];
|
|
7
|
+
export declare function useStoredMsaAlgorithm(): readonly ["clustalo" | "muscle" | "kalign" | "mafft", (algorithm: MsaAlgorithm) => void];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
2
|
+
import { databaseOptionsFor, defaultSearchFor, msaAlgorithms, searchPrograms, } from './consts';
|
|
3
|
+
// The panel reopens on the last search run rather than on the defaults, the way
|
|
4
|
+
// the Orthologs tab remembers its source.
|
|
5
|
+
//
|
|
6
|
+
// The program and its database stay ONE stored value for the reason consts.ts
|
|
7
|
+
// keeps them one state: neither service knows the other's database names. What
|
|
8
|
+
// comes back is checked against the current options before it is used — an
|
|
9
|
+
// option this plugin has since dropped would otherwise reach EBI and come back
|
|
10
|
+
// a 400, minutes after the user pressed Submit.
|
|
11
|
+
export const SEARCH_CHOICE_STORAGE_KEY = 'msaView-blastSearch';
|
|
12
|
+
export const MSA_ALGORITHM_STORAGE_KEY = 'msaView-msaAlgorithm';
|
|
13
|
+
const defaultSearch = defaultSearchFor('blastp');
|
|
14
|
+
const defaultMsaAlgorithm = 'clustalo';
|
|
15
|
+
export function validSearchChoice(stored) {
|
|
16
|
+
const { program, database } = (stored ?? {});
|
|
17
|
+
const known = searchPrograms.find(p => p === program);
|
|
18
|
+
return known && databaseOptionsFor(known).some(d => d === database)
|
|
19
|
+
? { program: known, database }
|
|
20
|
+
: defaultSearch;
|
|
21
|
+
}
|
|
22
|
+
export function validMsaAlgorithm(stored) {
|
|
23
|
+
return msaAlgorithms.find(a => a === stored) ?? defaultMsaAlgorithm;
|
|
24
|
+
}
|
|
25
|
+
export function useStoredSearchChoice() {
|
|
26
|
+
const [stored, setStored] = useLocalStorage(SEARCH_CHOICE_STORAGE_KEY, defaultSearch);
|
|
27
|
+
return [
|
|
28
|
+
validSearchChoice(stored),
|
|
29
|
+
(choice) => {
|
|
30
|
+
setStored(choice);
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
export function useStoredMsaAlgorithm() {
|
|
35
|
+
const [stored, setStored] = useLocalStorage(MSA_ALGORITHM_STORAGE_KEY, defaultMsaAlgorithm);
|
|
36
|
+
return [
|
|
37
|
+
validMsaAlgorithm(stored),
|
|
38
|
+
(algorithm) => {
|
|
39
|
+
setStored(algorithm);
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { validMsaAlgorithm, validSearchChoice } from './searchChoiceStorage';
|
|
3
|
+
test('a stored pair both services recognise comes back as it went in', () => {
|
|
4
|
+
expect(validSearchChoice({ program: 'phmmer', database: 'swissprot' })).toEqual({
|
|
5
|
+
program: 'phmmer',
|
|
6
|
+
database: 'swissprot',
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
test('a database the stored program does not have falls back to the defaults', () => {
|
|
10
|
+
// uniprotkb_swissprot is blastp's name for it; sending it to phmmer is a 400
|
|
11
|
+
// from EBI minutes after Submit
|
|
12
|
+
expect(validSearchChoice({ program: 'phmmer', database: 'uniprotkb_swissprot' })).toEqual({ program: 'blastp', database: 'uniprotkb_swissprot' });
|
|
13
|
+
// uniprotkb_reference_proteomes shipped as a menu entry EBI rejects, and 3.0.0
|
|
14
|
+
// is old enough that someone's storage still holds it
|
|
15
|
+
expect(validSearchChoice({
|
|
16
|
+
program: 'blastp',
|
|
17
|
+
database: 'uniprotkb_reference_proteomes',
|
|
18
|
+
})).toEqual({ program: 'blastp', database: 'uniprotkb_swissprot' });
|
|
19
|
+
});
|
|
20
|
+
test('storage holding something else entirely falls back rather than throwing', () => {
|
|
21
|
+
for (const stored of [null, undefined, 'blastp', 42, {}, []]) {
|
|
22
|
+
expect(validSearchChoice(stored)).toEqual({
|
|
23
|
+
program: 'blastp',
|
|
24
|
+
database: 'uniprotkb_swissprot',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
test('an unknown algorithm falls back to clustalo', () => {
|
|
29
|
+
expect(validMsaAlgorithm('mafft')).toBe('mafft');
|
|
30
|
+
expect(validMsaAlgorithm('t_coffee')).toBe('clustalo');
|
|
31
|
+
expect(validMsaAlgorithm(null)).toBe('clustalo');
|
|
32
|
+
});
|
|
@@ -7,6 +7,7 @@ import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs';
|
|
|
7
7
|
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
8
8
|
import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
|
|
9
9
|
import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
|
|
10
|
+
import { useStoredMsaAlgorithm } from '../BlastQuery/searchChoiceStorage';
|
|
10
11
|
import LaunchPanelContent from '../LaunchPanelContent';
|
|
11
12
|
import SubmitCancelActions from '../SubmitCancelActions';
|
|
12
13
|
import TranscriptSelector from '../TranscriptSelector';
|
|
@@ -25,7 +26,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
25
26
|
const [launchViewError, setLaunchViewError] = useState();
|
|
26
27
|
const [taxId, setTaxId] = useState(9606);
|
|
27
28
|
const [source, setSource] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
|
|
28
|
-
const [msaAlgorithm, setMsaAlgorithm] =
|
|
29
|
+
const [msaAlgorithm, setMsaAlgorithm] = useStoredMsaAlgorithm();
|
|
29
30
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
|
|
30
31
|
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
31
32
|
const transcriptSelection = useTranscriptSelection({ feature, view });
|
|
@@ -15,6 +15,16 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
15
15
|
// directly, and so is orthologParams (the model's own autorun picks it up).
|
|
16
16
|
// Only sources needing launch-time resolution go through `init`: msaUrl
|
|
17
17
|
// (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
18
|
+
//
|
|
19
|
+
// An init whose every field is undefined is still a truthy object, and
|
|
20
|
+
// MsaViewPanel reads any init as a launch in flight, so an inline-data
|
|
21
|
+
// launch flashed "Loading alignment" until processInit cleared it.
|
|
22
|
+
const init = {
|
|
23
|
+
msaUrl: msaFileLocation?.uri,
|
|
24
|
+
msaIndexedLocation,
|
|
25
|
+
msaName,
|
|
26
|
+
querySeqName,
|
|
27
|
+
};
|
|
18
28
|
launchMsaView(session, {
|
|
19
29
|
...rest,
|
|
20
30
|
data,
|
|
@@ -26,12 +36,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
26
36
|
},
|
|
27
37
|
}
|
|
28
38
|
: {}),
|
|
29
|
-
init: {
|
|
30
|
-
msaUrl: msaFileLocation?.uri,
|
|
31
|
-
msaIndexedLocation,
|
|
32
|
-
msaName,
|
|
33
|
-
querySeqName,
|
|
34
|
-
},
|
|
39
|
+
...(Object.values(init).some(v => v !== undefined) ? { init } : {}),
|
|
35
40
|
});
|
|
36
41
|
return args;
|
|
37
42
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import LaunchMsaViewExtensionPointF from './index';
|
|
3
|
+
function launch(args) {
|
|
4
|
+
let run;
|
|
5
|
+
LaunchMsaViewExtensionPointF({
|
|
6
|
+
addToExtensionPoint(_name, cb) {
|
|
7
|
+
run = cb;
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
const added = [];
|
|
11
|
+
const session = {
|
|
12
|
+
addView(_type, snapshot) {
|
|
13
|
+
added.push(snapshot);
|
|
14
|
+
return { id: 'view-1' };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
run({ session, ...args });
|
|
18
|
+
return added[0];
|
|
19
|
+
}
|
|
20
|
+
test('inline data carries no init, which the panel would read as a launch in flight', () => {
|
|
21
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' } });
|
|
22
|
+
expect('init' in snapshot).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
test('a file location travels through init', () => {
|
|
25
|
+
const snapshot = launch({
|
|
26
|
+
msaFileLocation: { uri: 'http://example.com/a.fa' },
|
|
27
|
+
});
|
|
28
|
+
expect(snapshot.init).toEqual({
|
|
29
|
+
msaUrl: 'http://example.com/a.fa',
|
|
30
|
+
msaIndexedLocation: undefined,
|
|
31
|
+
msaName: undefined,
|
|
32
|
+
querySeqName: undefined,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
test('one field set is enough to need init', () => {
|
|
36
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
|
|
37
|
+
expect(snapshot.init).toEqual({
|
|
38
|
+
msaUrl: undefined,
|
|
39
|
+
msaIndexedLocation: undefined,
|
|
40
|
+
msaName: undefined,
|
|
41
|
+
querySeqName: 'QUERY',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
2
|
export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
|
|
3
|
+
/**
|
|
4
|
+
* Keep IndexedDB holding what the view holds. The first run writes a new row and
|
|
5
|
+
* records its id in the session snapshot; later runs update that row, because
|
|
6
|
+
* the alignment keeps changing after it arrives — react-msaview's
|
|
7
|
+
* "calculate neighbor-joining tree" replaces `data.tree` — and a session
|
|
8
|
+
* reopened against a stale row restores the wrong picture.
|
|
9
|
+
*
|
|
10
|
+
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
11
|
+
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
12
|
+
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
13
|
+
* IndexedDB (private mode) fails once rather than in a loop.
|
|
14
|
+
*
|
|
15
|
+
* A view whose data comes from a filehandle stores nothing at all: the file is
|
|
16
|
+
* the source of truth and react-msaview refetches it at startup.
|
|
17
|
+
*/
|
|
3
18
|
export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
|
|
4
19
|
/**
|
|
5
20
|
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { transaction } from 'mobx';
|
|
2
3
|
import { doLaunchBlast } from './doLaunchBlast';
|
|
3
4
|
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
4
5
|
import { fetchIndexedMsa } from './fetchIndexedMsa';
|
|
5
6
|
import { genomeToMSA } from './genomeToMSA';
|
|
6
7
|
import { loadProteinDomains } from './loadProteinDomains';
|
|
7
8
|
import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
|
|
9
|
+
import { runLaunch } from './runLaunch';
|
|
8
10
|
import { getProteinViews } from './structureConnection';
|
|
9
11
|
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
|
|
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.";
|
|
10
13
|
export function loadStoredData(self) {
|
|
11
14
|
const { dataStoreId, rows } = self;
|
|
12
15
|
if (dataStoreId && rows.length === 0) {
|
|
@@ -15,15 +18,28 @@ export function loadStoredData(self) {
|
|
|
15
18
|
self.setLoadingStoredData(true);
|
|
16
19
|
const storedData = await retrieveMsaData(dataStoreId);
|
|
17
20
|
if (storedData) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
// one transaction, so storeDataToIndexedDB sees the restored data and
|
|
22
|
+
// the record of it together and does not write it straight back
|
|
23
|
+
transaction(() => {
|
|
24
|
+
if (storedData.msa) {
|
|
25
|
+
self.setMSA(storedData.msa);
|
|
26
|
+
}
|
|
27
|
+
if (storedData.tree) {
|
|
28
|
+
self.setTree(storedData.tree);
|
|
29
|
+
}
|
|
30
|
+
if (storedData.treeMetadata) {
|
|
31
|
+
self.setTreeMetadata(storedData.treeMetadata);
|
|
32
|
+
}
|
|
33
|
+
self.setLastStoredData(currentData(self));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// the id names nothing, so clearing it is what lets react-msaview's
|
|
38
|
+
// "Return to import form" actually return instead of landing back here
|
|
39
|
+
transaction(() => {
|
|
40
|
+
self.setDataStoreId(undefined);
|
|
41
|
+
self.setError(new Error(EXPIRED_MESSAGE));
|
|
42
|
+
});
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
catch (e) {
|
|
@@ -35,40 +51,63 @@ export function loadStoredData(self) {
|
|
|
35
51
|
})();
|
|
36
52
|
}
|
|
37
53
|
}
|
|
54
|
+
function currentData(self) {
|
|
55
|
+
const { msa, tree, treeMetadata } = self.data;
|
|
56
|
+
return { msa, tree, treeMetadata };
|
|
57
|
+
}
|
|
58
|
+
function sameData(a, b) {
|
|
59
|
+
return (!!a &&
|
|
60
|
+
a.msa === b.msa &&
|
|
61
|
+
a.tree === b.tree &&
|
|
62
|
+
a.treeMetadata === b.treeMetadata);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Keep IndexedDB holding what the view holds. The first run writes a new row and
|
|
66
|
+
* records its id in the session snapshot; later runs update that row, because
|
|
67
|
+
* the alignment keeps changing after it arrives — react-msaview's
|
|
68
|
+
* "calculate neighbor-joining tree" replaces `data.tree` — and a session
|
|
69
|
+
* reopened against a stale row restores the wrong picture.
|
|
70
|
+
*
|
|
71
|
+
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
72
|
+
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
73
|
+
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
74
|
+
* IndexedDB (private mode) fails once rather than in a loop.
|
|
75
|
+
*
|
|
76
|
+
* A view whose data comes from a filehandle stores nothing at all: the file is
|
|
77
|
+
* the source of truth and react-msaview refetches it at startup.
|
|
78
|
+
*/
|
|
38
79
|
export function storeDataToIndexedDB(self) {
|
|
39
|
-
const { rows, dataStoreId, isStoringData } = self;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
const { rows, dataStoreId, isStoringData, lastStoredData } = self;
|
|
81
|
+
const data = currentData(self);
|
|
82
|
+
if (rows.length === 0 ||
|
|
83
|
+
isStoringData ||
|
|
84
|
+
self.msaFilehandle ||
|
|
85
|
+
self.treeFilehandle ||
|
|
86
|
+
!(data.msa || data.tree) ||
|
|
87
|
+
sameData(lastStoredData, data)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// mark as storing synchronously so re-runs of this autorun (e.g. when data
|
|
91
|
+
// observables change while the write is pending) don't kick off a duplicate
|
|
92
|
+
// write and leave an orphan IndexedDB entry
|
|
93
|
+
self.setIsStoringData(true);
|
|
94
|
+
void (async () => {
|
|
95
|
+
try {
|
|
96
|
+
const id = dataStoreId ?? generateDataStoreId();
|
|
97
|
+
if (await storeMsaData(id, data)) {
|
|
98
|
+
self.setDataStoreId(id);
|
|
99
|
+
}
|
|
43
100
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (msaData || treeData) {
|
|
47
|
-
// mark as storing synchronously so re-runs of this autorun (e.g. when
|
|
48
|
-
// data observables change while the write is pending) don't kick off a
|
|
49
|
-
// duplicate write and leave an orphan IndexedDB entry
|
|
50
|
-
self.setIsStoringData(true);
|
|
51
|
-
void (async () => {
|
|
52
|
-
try {
|
|
53
|
-
const newId = generateDataStoreId();
|
|
54
|
-
const success = await storeMsaData(newId, {
|
|
55
|
-
msa: msaData,
|
|
56
|
-
tree: treeData,
|
|
57
|
-
treeMetadata: self.data.treeMetadata,
|
|
58
|
-
});
|
|
59
|
-
if (success) {
|
|
60
|
-
self.setDataStoreId(newId);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
console.error('Failed to store MSA data to IndexedDB:', e);
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
self.setIsStoringData(false);
|
|
68
|
-
}
|
|
69
|
-
})();
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error('Failed to store MSA data to IndexedDB:', e);
|
|
70
103
|
}
|
|
71
|
-
|
|
104
|
+
finally {
|
|
105
|
+
transaction(() => {
|
|
106
|
+
self.setLastStoredData(data);
|
|
107
|
+
self.setIsStoringData(false);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
72
111
|
}
|
|
73
112
|
/**
|
|
74
113
|
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
@@ -79,42 +118,26 @@ export function storeDataToIndexedDB(self) {
|
|
|
79
118
|
*/
|
|
80
119
|
export function launchOrthologsIfNeeded(self) {
|
|
81
120
|
if (self.orthologParams) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.setData(data);
|
|
121
|
+
runLaunch({
|
|
122
|
+
self,
|
|
123
|
+
message: 'Resolving orthologs',
|
|
124
|
+
launch: scope => doLaunchOrthologs({ self, scope }),
|
|
125
|
+
onLaunched: () => {
|
|
88
126
|
self.setOrthologParams(undefined);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
self.setError(e);
|
|
92
|
-
console.error(e);
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
self.setProgress('');
|
|
96
|
-
}
|
|
97
|
-
})();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
131
|
export function launchBlastIfNeeded(self) {
|
|
101
132
|
if (self.blastParams) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self.setData(data);
|
|
133
|
+
runLaunch({
|
|
134
|
+
self,
|
|
135
|
+
message: 'Submitting query',
|
|
136
|
+
launch: scope => doLaunchBlast({ self, scope }),
|
|
137
|
+
onLaunched: () => {
|
|
108
138
|
self.setBlastParams(undefined);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
self.setError(e);
|
|
112
|
-
console.error(e);
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
self.setProgress('');
|
|
116
|
-
}
|
|
117
|
-
})();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
143
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
|
|
3
|
-
import { Typography } from '@mui/material';
|
|
3
|
+
import { Button, Typography } from '@mui/material';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
5
|
import { makeStyles } from 'tss-react/mui';
|
|
6
6
|
import JobLink from './JobLink';
|
|
@@ -8,6 +8,11 @@ const useStyles = makeStyles()({
|
|
|
8
8
|
margin: {
|
|
9
9
|
padding: 20,
|
|
10
10
|
},
|
|
11
|
+
progressRow: {
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
gap: 12,
|
|
15
|
+
},
|
|
11
16
|
});
|
|
12
17
|
/**
|
|
13
18
|
* What a view shows while it is still building its alignment, and what it shows
|
|
@@ -36,6 +41,10 @@ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
|
|
|
36
41
|
React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
|
|
37
42
|
React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
|
|
38
43
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
39
|
-
React.createElement(
|
|
44
|
+
React.createElement("div", { className: classes.progressRow },
|
|
45
|
+
React.createElement(Typography, null, progress || 'Initializing'),
|
|
46
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
47
|
+
model.cancelLaunch();
|
|
48
|
+
} }, "Cancel"))))));
|
|
40
49
|
});
|
|
41
50
|
export default LaunchProgress;
|
|
@@ -57,6 +57,25 @@ test('a failed init shows why', () => {
|
|
|
57
57
|
expect(screen.queryByText('the alignment')).toBeNull();
|
|
58
58
|
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
|
|
59
59
|
});
|
|
60
|
+
// a launch runs for 10+ minutes, so leaving with no way out means watching it
|
|
61
|
+
test('a running launch offers a way out, and a failed one does not', () => {
|
|
62
|
+
const cancelLaunch = vi.fn();
|
|
63
|
+
panel({
|
|
64
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
65
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
66
|
+
cancelLaunch,
|
|
67
|
+
});
|
|
68
|
+
screen.getByRole('button', { name: 'Cancel' }).click();
|
|
69
|
+
expect(cancelLaunch).toHaveBeenCalled();
|
|
70
|
+
cleanup();
|
|
71
|
+
panel({
|
|
72
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
73
|
+
progress: '',
|
|
74
|
+
error: new Error('No hits found'),
|
|
75
|
+
cancelLaunch,
|
|
76
|
+
});
|
|
77
|
+
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
|
|
78
|
+
});
|
|
60
79
|
test('a running job links out to it', () => {
|
|
61
80
|
panel({
|
|
62
81
|
blastParams: { proteinSequence: 'MKV' },
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
-
|
|
2
|
+
import type { LaunchScope } from './runLaunch';
|
|
3
|
+
export declare function doLaunchBlast({ self, scope, }: {
|
|
3
4
|
self: JBrowsePluginMsaViewModel;
|
|
5
|
+
scope: LaunchScope;
|
|
4
6
|
}): Promise<{
|
|
5
7
|
msa: string;
|
|
6
8
|
tree: string;
|
|
@@ -6,26 +6,20 @@ import { launchMSA, launchTree } from '../utils/msa';
|
|
|
6
6
|
import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
|
|
7
7
|
import { queryPhmmer } from '../utils/phmmer';
|
|
8
8
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
9
|
-
export async function doLaunchBlast({ self, }) {
|
|
9
|
+
export async function doLaunchBlast({ self, scope, }) {
|
|
10
10
|
// kept whole rather than destructured: the database's type depends on
|
|
11
11
|
// searchProgram, and pulling the two apart loses the link between them
|
|
12
12
|
const params = self.blastParams;
|
|
13
13
|
const { selectedTranscript } = params;
|
|
14
14
|
const cleanedSeq = cleanProteinSequence(params.proteinSequence);
|
|
15
|
-
const onProgress
|
|
16
|
-
self.setProgress(arg);
|
|
17
|
-
};
|
|
18
|
-
// publish the job id before the first poll so the view can link out while the
|
|
19
|
-
// job is still running
|
|
20
|
-
const onRid = (r) => {
|
|
21
|
-
self.setRid(r);
|
|
22
|
-
};
|
|
15
|
+
const { onProgress, onRid, signal } = scope;
|
|
23
16
|
const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
|
|
24
17
|
? await runPhmmer({
|
|
25
18
|
query: cleanedSeq,
|
|
26
19
|
database: params.blastDatabase,
|
|
27
20
|
onProgress,
|
|
28
21
|
onRid,
|
|
22
|
+
signal,
|
|
29
23
|
})
|
|
30
24
|
: await runBlast({
|
|
31
25
|
query: cleanedSeq,
|
|
@@ -33,6 +27,7 @@ export async function doLaunchBlast({ self, }) {
|
|
|
33
27
|
msaAlgorithm: params.msaAlgorithm,
|
|
34
28
|
onProgress,
|
|
35
29
|
onRid,
|
|
30
|
+
signal,
|
|
36
31
|
});
|
|
37
32
|
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
38
33
|
await saveBlastResult({
|
|
@@ -57,12 +52,13 @@ export async function doLaunchBlast({ self, }) {
|
|
|
57
52
|
* at a time, so the alignments are stripped back off and every hit is realigned
|
|
58
53
|
* together by a dedicated aligner.
|
|
59
54
|
*/
|
|
60
|
-
async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, }) {
|
|
55
|
+
async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, signal, }) {
|
|
61
56
|
const { hits, rid } = await queryEbiBlast({
|
|
62
57
|
query,
|
|
63
58
|
blastDatabase,
|
|
64
59
|
onProgress,
|
|
65
60
|
onRid,
|
|
61
|
+
signal,
|
|
66
62
|
});
|
|
67
63
|
onProgress('Fetching species taxonomy info...');
|
|
68
64
|
const taxonomyInfo = await fetchTaxonomyInfo(hits
|
|
@@ -83,6 +79,7 @@ async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid,
|
|
|
83
79
|
algorithm: msaAlgorithm,
|
|
84
80
|
sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
|
|
85
81
|
onProgress,
|
|
82
|
+
signal,
|
|
86
83
|
});
|
|
87
84
|
return { ...result, treeMetadata, rid };
|
|
88
85
|
}
|
|
@@ -93,12 +90,13 @@ async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid,
|
|
|
93
90
|
* match columns rather than being aligned back in afterwards. That leaves no
|
|
94
91
|
* aligner run to take a tree from, so the tree is built from this alignment.
|
|
95
92
|
*/
|
|
96
|
-
async function runPhmmer({ query, database, onProgress, onRid, }) {
|
|
93
|
+
async function runPhmmer({ query, database, onProgress, onRid, signal, }) {
|
|
97
94
|
const { rows, queryRow, rid } = await queryPhmmer({
|
|
98
95
|
query,
|
|
99
96
|
database,
|
|
100
97
|
onProgress,
|
|
101
98
|
onRid,
|
|
99
|
+
signal,
|
|
102
100
|
});
|
|
103
101
|
onProgress('Fetching species taxonomy info...');
|
|
104
102
|
const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
|
|
@@ -109,7 +107,7 @@ async function runPhmmer({ query, database, onProgress, onRid, }) {
|
|
|
109
107
|
});
|
|
110
108
|
return {
|
|
111
109
|
msa,
|
|
112
|
-
tree: await launchTree({ alignment: msa, onProgress }),
|
|
110
|
+
tree: await launchTree({ alignment: msa, onProgress, signal }),
|
|
113
111
|
treeMetadata,
|
|
114
112
|
rid,
|
|
115
113
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
+
import type { LaunchScope } from './runLaunch';
|
|
2
3
|
/**
|
|
3
4
|
* The no-search-job alternative to doLaunchBlast.
|
|
4
5
|
*
|
|
@@ -16,8 +17,9 @@ import type { JBrowsePluginMsaViewModel } from './model';
|
|
|
16
17
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
17
18
|
* excluded from the ortholog set rather than appearing twice.
|
|
18
19
|
*/
|
|
19
|
-
export declare function doLaunchOrthologs({ self, }: {
|
|
20
|
+
export declare function doLaunchOrthologs({ self, scope, }: {
|
|
20
21
|
self: JBrowsePluginMsaViewModel;
|
|
22
|
+
scope: LaunchScope;
|
|
21
23
|
}): Promise<{
|
|
22
24
|
treeMetadata: string;
|
|
23
25
|
msa: string;
|
|
@@ -20,11 +20,9 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
|
20
20
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
21
21
|
* excluded from the ortholog set rather than appearing twice.
|
|
22
22
|
*/
|
|
23
|
-
export async function doLaunchOrthologs({ self, }) {
|
|
23
|
+
export async function doLaunchOrthologs({ self, scope, }) {
|
|
24
24
|
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
|
|
25
|
-
const onProgress
|
|
26
|
-
self.setProgress(arg);
|
|
27
|
-
};
|
|
25
|
+
const { onProgress, act, signal } = scope;
|
|
28
26
|
const request = {
|
|
29
27
|
taxId,
|
|
30
28
|
geneCandidates,
|
|
@@ -62,7 +60,9 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
62
60
|
// token, and a collision would silently point the coordinate mapping at
|
|
63
61
|
// another animal's row.
|
|
64
62
|
const queryLabel = await queryRowLabel(taxId, rows);
|
|
65
|
-
|
|
63
|
+
act(() => {
|
|
64
|
+
self.setQuerySeqName(queryLabel);
|
|
65
|
+
});
|
|
66
66
|
const treeMetadata = {
|
|
67
67
|
[queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
|
|
68
68
|
};
|
|
@@ -76,6 +76,7 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
76
76
|
...rows.map(r => `>${r.label}\n${r.sequence}`),
|
|
77
77
|
].join('\n'),
|
|
78
78
|
onProgress,
|
|
79
|
+
signal,
|
|
79
80
|
});
|
|
80
81
|
return {
|
|
81
82
|
...result,
|