jbrowse-plugin-msaview 3.4.0 → 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 +23 -17
- 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 +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- 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 +10 -4
- 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.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
- package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
- package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
- 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 +306 -85
- package/dist/MsaViewPanel/model.js +169 -42
- 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 +7 -3
- package/dist/MsaViewPanel/msaDataStore.js +26 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -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 +39 -0
- package/dist/MsaViewPanel/runLaunch.js +100 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +175 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -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 +46 -45
- 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/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 +13 -2
- package/dist/utils/ebiBlast.js +32 -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/homologSearch.d.ts +31 -0
- package/dist/utils/homologSearch.js +6 -0
- package/dist/utils/msa.d.ts +5 -11
- package/dist/utils/msa.js +19 -30
- package/dist/utils/msaRows.d.ts +11 -8
- package/dist/utils/msaRows.js +14 -12
- 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 +12 -1
- package/dist/utils/phmmer.js +52 -13
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- 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 +17 -16
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
- 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/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- 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 +12 -5
- 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 +74 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
- package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
- package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
- package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
- 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 +200 -49
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +32 -13
- 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 +208 -0
- package/src/MsaViewPanel/runLaunch.ts +141 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/MsaViewPanel/structureConnection.ts +0 -26
- package/src/MsaViewPanel/util.ts +75 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/browserAlign.test.ts +102 -0
- package/src/utils/browserAlign.ts +352 -0
- package/src/utils/ebiBlast.ts +42 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/homologSearch.ts +51 -0
- package/src/utils/msa.ts +25 -39
- package/src/utils/msaRows.ts +20 -16
- 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 +62 -12
- package/src/utils/poll.ts +8 -1
- 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/components/BlastQuery/searchChoiceStorage.test.d.ts} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, renderHook } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, test } from 'vitest';
|
|
4
|
+
import { useQueryRowName } from './useQueryRowName';
|
|
5
|
+
const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
|
|
6
|
+
const trimmed = `>aligned_query\n${protein.slice(5, 45)}\n>other\nWRONGWRONGWRONGWRONGWRONGWRONGWRONGWRONG\n`;
|
|
7
|
+
describe('useQueryRowName', () => {
|
|
8
|
+
test('carries the detected row and where it starts', () => {
|
|
9
|
+
const { result } = renderHook(() => useQueryRowName(trimmed, protein));
|
|
10
|
+
expect(result.current.querySeqName).toBe('aligned_query');
|
|
11
|
+
expect(result.current.querySeqOffset).toBe(5);
|
|
12
|
+
});
|
|
13
|
+
// the offset was measured against the detected row; another row is a row the
|
|
14
|
+
// user vouched for, and nothing here knows where in the protein it sits
|
|
15
|
+
test('a row the user picks instead is taken at face value', () => {
|
|
16
|
+
const { result } = renderHook(() => useQueryRowName(trimmed, protein));
|
|
17
|
+
act(() => {
|
|
18
|
+
result.current.setQuerySeqName('other');
|
|
19
|
+
});
|
|
20
|
+
expect(result.current.querySeqName).toBe('other');
|
|
21
|
+
expect(result.current.querySeqOffset).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { launchMsaView } from '../utils/launchMsaView';
|
|
2
2
|
export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
3
3
|
pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
|
|
4
|
-
const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
|
|
5
|
-
// `orthologParams`
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, searchParams, ...rest } = args;
|
|
5
|
+
// `orthologParams` and `searchParams` name no alignment at all — the
|
|
6
|
+
// view builds one at launch, which is the dialog's Orthologs and BLAST
|
|
7
|
+
// tabs reached declaratively.
|
|
8
8
|
if (!data &&
|
|
9
9
|
!msaFileLocation &&
|
|
10
10
|
!msaIndexedLocation &&
|
|
11
|
-
!rest.orthologParams
|
|
12
|
-
|
|
11
|
+
!rest.orthologParams &&
|
|
12
|
+
!searchParams) {
|
|
13
|
+
throw new Error('No MSA data, file location, orthologParams or searchParams provided when launching MSA view');
|
|
13
14
|
}
|
|
14
15
|
// inline data and the tree URL are native react-msaview snapshot props, set
|
|
15
16
|
// directly, and so is orthologParams (the model's own autorun picks it up).
|
|
16
17
|
// Only sources needing launch-time resolution go through `init`: msaUrl
|
|
17
18
|
// (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
19
|
+
//
|
|
20
|
+
// An init whose every field is undefined is still a truthy object, and
|
|
21
|
+
// MsaViewPanel reads any init as a launch in flight, so an inline-data
|
|
22
|
+
// launch flashed "Loading alignment" until processInit cleared it.
|
|
23
|
+
const init = {
|
|
24
|
+
msaUrl: msaFileLocation?.uri,
|
|
25
|
+
msaIndexedLocation,
|
|
26
|
+
msaName,
|
|
27
|
+
querySeqName,
|
|
28
|
+
};
|
|
18
29
|
launchMsaView(session, {
|
|
19
30
|
...rest,
|
|
31
|
+
...(searchParams ? { blastParams: searchParams } : {}),
|
|
20
32
|
data,
|
|
21
33
|
...(treeFileLocation
|
|
22
34
|
? {
|
|
@@ -26,12 +38,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
26
38
|
},
|
|
27
39
|
}
|
|
28
40
|
: {}),
|
|
29
|
-
init: {
|
|
30
|
-
msaUrl: msaFileLocation?.uri,
|
|
31
|
-
msaIndexedLocation,
|
|
32
|
-
msaName,
|
|
33
|
-
querySeqName,
|
|
34
|
-
},
|
|
41
|
+
...(Object.values(init).some(v => v !== undefined) ? { init } : {}),
|
|
35
42
|
});
|
|
36
43
|
return args;
|
|
37
44
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
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('searchParams is a source, stored under the name the dialog uses', () => {
|
|
36
|
+
const snapshot = launch({
|
|
37
|
+
searchParams: {
|
|
38
|
+
searchProgram: 'phmmer',
|
|
39
|
+
blastDatabase: 'rp15',
|
|
40
|
+
accession: 'P04637',
|
|
41
|
+
},
|
|
42
|
+
connectedViewId: 'lgv1',
|
|
43
|
+
connectedTranscript: 'NM_000546.6',
|
|
44
|
+
});
|
|
45
|
+
expect(snapshot.blastParams).toEqual({
|
|
46
|
+
searchProgram: 'phmmer',
|
|
47
|
+
blastDatabase: 'rp15',
|
|
48
|
+
accession: 'P04637',
|
|
49
|
+
});
|
|
50
|
+
expect('searchParams' in snapshot).toBe(false);
|
|
51
|
+
expect(snapshot.connectedTranscript).toBe('NM_000546.6');
|
|
52
|
+
});
|
|
53
|
+
test('a launch naming no source at all is refused', () => {
|
|
54
|
+
expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
|
|
55
|
+
});
|
|
56
|
+
test('one field set is enough to need init', () => {
|
|
57
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
|
|
58
|
+
expect(snapshot.init).toEqual({
|
|
59
|
+
msaUrl: undefined,
|
|
60
|
+
msaIndexedLocation: undefined,
|
|
61
|
+
msaName: undefined,
|
|
62
|
+
querySeqName: 'QUERY',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,5 +1,21 @@
|
|
|
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 -- or from the indexed block its
|
|
16
|
+
* kept `init` names -- stores nothing at all: the file is the source of truth
|
|
17
|
+
* and it is refetched at startup.
|
|
18
|
+
*/
|
|
3
19
|
export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
|
|
4
20
|
/**
|
|
5
21
|
* 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
|
-
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
|
|
11
|
+
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } 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,67 @@ 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 -- 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.
|
|
79
|
+
*/
|
|
38
80
|
export function storeDataToIndexedDB(self) {
|
|
39
|
-
const { rows, dataStoreId, isStoringData } = self;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
const { rows, dataStoreId, isStoringData, lastStoredData, init } = self;
|
|
82
|
+
const data = currentData(self);
|
|
83
|
+
if (rows.length === 0 ||
|
|
84
|
+
isStoringData ||
|
|
85
|
+
self.msaFilehandle ||
|
|
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 ||
|
|
90
|
+
!(data.msa || data.tree) ||
|
|
91
|
+
sameData(lastStoredData, data)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// mark as storing synchronously so re-runs of this autorun (e.g. when data
|
|
95
|
+
// observables change while the write is pending) don't kick off a duplicate
|
|
96
|
+
// write and leave an orphan IndexedDB entry
|
|
97
|
+
self.setIsStoringData(true);
|
|
98
|
+
void (async () => {
|
|
99
|
+
try {
|
|
100
|
+
const id = dataStoreId ?? generateDataStoreId();
|
|
101
|
+
if (await storeMsaData(id, data)) {
|
|
102
|
+
self.setDataStoreId(id);
|
|
103
|
+
}
|
|
43
104
|
}
|
|
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
|
-
})();
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error('Failed to store MSA data to IndexedDB:', e);
|
|
70
107
|
}
|
|
71
|
-
|
|
108
|
+
finally {
|
|
109
|
+
transaction(() => {
|
|
110
|
+
self.setLastStoredData(data);
|
|
111
|
+
self.setIsStoringData(false);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
72
115
|
}
|
|
73
116
|
/**
|
|
74
117
|
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
@@ -78,43 +121,36 @@ export function storeDataToIndexedDB(self) {
|
|
|
78
121
|
* until a new request replaces them.
|
|
79
122
|
*/
|
|
80
123
|
export function launchOrthologsIfNeeded(self) {
|
|
81
|
-
if (self.orthologParams) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.setData(data);
|
|
124
|
+
if (self.orthologParams && !awaitingTranscript(self)) {
|
|
125
|
+
runLaunch({
|
|
126
|
+
self,
|
|
127
|
+
message: 'Resolving orthologs',
|
|
128
|
+
launch: scope => doLaunchOrthologs({ self, scope }),
|
|
129
|
+
onLaunched: () => {
|
|
88
130
|
self.setOrthologParams(undefined);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
self.setError(e);
|
|
92
|
-
console.error(e);
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
self.setProgress('');
|
|
96
|
-
}
|
|
97
|
-
})();
|
|
131
|
+
},
|
|
132
|
+
});
|
|
98
133
|
}
|
|
99
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
|
+
}
|
|
100
144
|
export function launchBlastIfNeeded(self) {
|
|
101
|
-
if (self.blastParams) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self.setData(data);
|
|
145
|
+
if (self.blastParams && !awaitingTranscript(self)) {
|
|
146
|
+
runLaunch({
|
|
147
|
+
self,
|
|
148
|
+
message: 'Submitting query',
|
|
149
|
+
launch: scope => doLaunchBlast({ self, scope }),
|
|
150
|
+
onLaunched: () => {
|
|
108
151
|
self.setBlastParams(undefined);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
self.setError(e);
|
|
112
|
-
console.error(e);
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
self.setProgress('');
|
|
116
|
-
}
|
|
117
|
-
})();
|
|
152
|
+
},
|
|
153
|
+
});
|
|
118
154
|
}
|
|
119
155
|
}
|
|
120
156
|
/**
|
|
@@ -144,15 +180,28 @@ export function autoLoadProteinDomains(self) {
|
|
|
144
180
|
})();
|
|
145
181
|
}
|
|
146
182
|
}
|
|
147
|
-
// Resolve the declarative `init` launch contract
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
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.
|
|
152
195
|
export function processInit(self) {
|
|
153
196
|
const { init } = self;
|
|
154
197
|
if (init) {
|
|
155
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
|
+
}
|
|
156
205
|
void (async () => {
|
|
157
206
|
try {
|
|
158
207
|
self.setError(undefined);
|
|
@@ -181,7 +230,9 @@ export function processInit(self) {
|
|
|
181
230
|
throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
|
|
182
231
|
}
|
|
183
232
|
}
|
|
184
|
-
|
|
233
|
+
if (!indexed) {
|
|
234
|
+
self.setInit(undefined);
|
|
235
|
+
}
|
|
185
236
|
}
|
|
186
237
|
catch (e) {
|
|
187
238
|
self.setError(e);
|
|
@@ -215,10 +266,10 @@ export function syncGenomeHoverToMsaColumn(self) {
|
|
|
215
266
|
* Translate genome regions published by a 3D protein view into this MSA's
|
|
216
267
|
* visible columns. The genome is the only coordinate space the two plugins
|
|
217
268
|
* share, so the hops are genome coord -> protein position (the transcript's g2p
|
|
218
|
-
* map) ->
|
|
269
|
+
* map) -> visible column.
|
|
219
270
|
*/
|
|
220
271
|
function genomeHighlightsToVisibleColumns(self, field) {
|
|
221
|
-
const { connectedViewId, transcriptToMsaMap
|
|
272
|
+
const { connectedViewId, transcriptToMsaMap } = self;
|
|
222
273
|
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
223
274
|
return [];
|
|
224
275
|
}
|
|
@@ -232,16 +283,17 @@ function genomeHighlightsToVisibleColumns(self, field) {
|
|
|
232
283
|
for (const highlight of structure[field] ?? []) {
|
|
233
284
|
for (let coord = highlight.start; coord < highlight.end; coord++) {
|
|
234
285
|
const proteinPos = g2p[coord];
|
|
235
|
-
|
|
236
|
-
|
|
286
|
+
const col = proteinPos === undefined
|
|
287
|
+
? undefined
|
|
288
|
+
: transcriptPosToVisibleCol(self, proteinPos);
|
|
289
|
+
if (col !== undefined) {
|
|
290
|
+
columns.add(col);
|
|
237
291
|
}
|
|
238
292
|
}
|
|
239
293
|
}
|
|
240
294
|
}
|
|
241
295
|
}
|
|
242
|
-
return [...columns]
|
|
243
|
-
.map(col => self.globalColToVisibleCol(col))
|
|
244
|
-
.filter((col) => col !== undefined);
|
|
296
|
+
return [...columns];
|
|
245
297
|
}
|
|
246
298
|
function sameColumns(a, b) {
|
|
247
299
|
if (!a || !b) {
|
|
@@ -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
|
|
@@ -24,18 +29,31 @@ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
|
|
|
24
29
|
const { blastParams, orthologParams, progress, rid, error } = model;
|
|
25
30
|
const { classes } = useStyles();
|
|
26
31
|
const message = blastParams
|
|
27
|
-
?
|
|
32
|
+
? `Running EBI ${blastParams.searchProgram === 'phmmer' ? 'phmmer' : 'BLAST'}`
|
|
28
33
|
: orthologParams
|
|
29
|
-
?
|
|
34
|
+
? orthologParams.source === 'uniref'
|
|
35
|
+
? 'Building UniRef homolog alignment'
|
|
36
|
+
: 'Building ortholog alignment'
|
|
30
37
|
: 'Loading alignment';
|
|
31
38
|
return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
|
|
32
39
|
React.createElement(Typography, { variant: "h5" },
|
|
33
40
|
message,
|
|
34
41
|
" failed"),
|
|
35
42
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
36
|
-
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,
|
|
37
51
|
React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
|
|
38
52
|
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
39
|
-
React.createElement(
|
|
53
|
+
React.createElement("div", { className: classes.progressRow },
|
|
54
|
+
React.createElement(Typography, null, progress || 'Initializing'),
|
|
55
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
56
|
+
model.cancelLaunch();
|
|
57
|
+
} }, "Cancel"))))));
|
|
40
58
|
});
|
|
41
59
|
export default LaunchProgress;
|
|
@@ -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' },
|
|
@@ -57,6 +67,36 @@ test('a failed init shows why', () => {
|
|
|
57
67
|
expect(screen.queryByText('the alignment')).toBeNull();
|
|
58
68
|
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
|
|
59
69
|
});
|
|
70
|
+
// a launch runs for 10+ minutes, so leaving with no way out means watching it
|
|
71
|
+
test('a running launch offers a way out', () => {
|
|
72
|
+
const cancelLaunch = vi.fn();
|
|
73
|
+
panel({
|
|
74
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
75
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
76
|
+
cancelLaunch,
|
|
77
|
+
});
|
|
78
|
+
screen.getByRole('button', { name: 'Cancel' }).click();
|
|
79
|
+
expect(cancelLaunch).toHaveBeenCalled();
|
|
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();
|
|
87
|
+
panel({
|
|
88
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
89
|
+
progress: '',
|
|
90
|
+
error: new Error('No hits found'),
|
|
91
|
+
cancelLaunch,
|
|
92
|
+
retryLaunch,
|
|
93
|
+
});
|
|
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();
|
|
99
|
+
});
|
|
60
100
|
test('a running job links out to it', () => {
|
|
61
101
|
panel({
|
|
62
102
|
blastParams: { proteinSequence: 'MKV' },
|
|
@@ -1,6 +1,16 @@
|
|
|
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
|
+
*/
|
|
11
|
+
export declare function doLaunchBlast({ self, scope, }: {
|
|
3
12
|
self: JBrowsePluginMsaViewModel;
|
|
13
|
+
scope: LaunchScope;
|
|
4
14
|
}): Promise<{
|
|
5
15
|
msa: string;
|
|
6
16
|
tree: string;
|