jbrowse-plugin-msaview 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/LaunchMsaView/cleanProteinSequence.test.d.ts +1 -0
- package/dist/LaunchMsaView/cleanProteinSequence.test.js +77 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -32
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +10 -15
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +14 -13
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +25 -22
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +14 -19
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +18 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.js +13 -0
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +19 -14
- package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
- package/dist/LaunchMsaView/components/SubmitCancelActions.js +10 -7
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +20 -2
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +8 -1
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +25 -20
- package/dist/LaunchMsaView/components/launchConnectedView.d.ts +23 -0
- package/dist/LaunchMsaView/components/launchConnectedView.js +39 -0
- package/dist/LaunchMsaView/detectQueryRow.js +4 -7
- package/dist/LaunchMsaView/util.d.ts +6 -0
- package/dist/LaunchMsaView/util.js +7 -1
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +12 -3
- package/dist/MsaViewPanel/applyRegion.d.ts +23 -0
- package/dist/MsaViewPanel/applyRegion.js +19 -0
- package/dist/MsaViewPanel/applyRegion.test.d.ts +1 -0
- package/dist/MsaViewPanel/applyRegion.test.js +45 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +2 -1
- package/dist/MsaViewPanel/doLaunchBlast.test.js +12 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
- package/dist/MsaViewPanel/genomeToMSA.js +3 -3
- package/dist/MsaViewPanel/genomeToMSA.test.js +69 -23
- package/dist/MsaViewPanel/launchAutoruns.test.d.ts +1 -0
- package/dist/MsaViewPanel/launchAutoruns.test.js +36 -0
- package/dist/MsaViewPanel/model.d.ts +18 -14
- package/dist/MsaViewPanel/model.js +17 -0
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +3 -12
- package/dist/MsaViewPanel/msaDataStore.test.js +8 -1
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +4 -4
- package/dist/MsaViewPanel/regionSnapshot.test.d.ts +1 -0
- package/dist/MsaViewPanel/regionSnapshot.test.js +29 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +41 -4
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +103 -68
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.d.ts +1 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +149 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +8 -4
- package/dist/MsaViewPanel/runLaunch.js +15 -4
- package/dist/MsaViewPanel/runLaunch.test.js +16 -0
- package/dist/MsaViewPanel/storedData.test.js +23 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +3 -2
- package/dist/MsaViewPanel/util.d.ts +23 -23
- package/dist/MsaViewPanel/util.js +23 -24
- package/dist/index.js +17 -10
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +36 -36
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +26 -2
- package/dist/utils/blastCache.js +51 -39
- package/dist/utils/blastCache.test.js +46 -19
- package/dist/utils/domainCache.d.ts +1 -1
- package/dist/utils/domainCache.js +18 -14
- package/dist/utils/ebiJobDispatcher.d.ts +11 -0
- package/dist/utils/ebiJobDispatcher.js +12 -13
- package/dist/utils/ebiJobDispatcher.test.js +17 -3
- package/dist/utils/eutils.d.ts +14 -0
- package/dist/utils/eutils.js +48 -14
- package/dist/utils/eutils.test.d.ts +1 -0
- package/dist/utils/eutils.test.js +31 -0
- package/dist/utils/idb.d.ts +16 -3
- package/dist/utils/idb.js +51 -5
- package/dist/utils/idb.test.d.ts +1 -0
- package/dist/utils/idb.test.js +78 -0
- package/dist/utils/ncbiDomains.js +4 -4
- package/dist/utils/ncbiOrthologs.d.ts +1 -1
- package/dist/utils/ncbiOrthologs.js +18 -17
- package/dist/utils/ncbiOrthologs.test.js +17 -1
- package/dist/utils/ncbiTaxonomy.js +14 -15
- package/dist/utils/taxonomyNames.js +23 -19
- package/dist/utils/taxonomyNames.test.d.ts +1 -0
- package/dist/utils/taxonomyNames.test.js +33 -0
- package/dist/utils/unirefHomologs.js +2 -2
- package/dist/utils/unirefHomologs.test.js +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -9
- package/src/LaunchMsaView/cleanProteinSequence.test.ts +91 -0
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +35 -32
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +10 -14
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +16 -15
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +34 -21
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +18 -21
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.ts +16 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +24 -2
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +19 -13
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +27 -2
- package/src/LaunchMsaView/components/SubmitCancelActions.tsx +12 -7
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +28 -18
- package/src/LaunchMsaView/components/launchConnectedView.ts +56 -0
- package/src/LaunchMsaView/detectQueryRow.ts +5 -7
- package/src/LaunchMsaView/util.ts +7 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +16 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +28 -1
- package/src/MsaViewPanel/afterCreateAutoruns.ts +12 -3
- package/src/MsaViewPanel/applyRegion.test.ts +55 -0
- package/src/MsaViewPanel/applyRegion.ts +33 -0
- package/src/MsaViewPanel/doLaunchBlast.test.ts +20 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +2 -1
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +1 -1
- package/src/MsaViewPanel/genomeToMSA.test.ts +75 -23
- package/src/MsaViewPanel/genomeToMSA.ts +3 -2
- package/src/MsaViewPanel/launchAutoruns.test.ts +45 -0
- package/src/MsaViewPanel/model.ts +22 -0
- package/src/MsaViewPanel/msaDataStore.test.ts +14 -2
- package/src/MsaViewPanel/msaDataStore.ts +4 -12
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +4 -4
- package/src/MsaViewPanel/regionSnapshot.test.ts +34 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +183 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +154 -71
- package/src/MsaViewPanel/runLaunch.test.ts +19 -0
- package/src/MsaViewPanel/runLaunch.ts +21 -4
- package/src/MsaViewPanel/storedData.test.ts +32 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +3 -2
- package/src/MsaViewPanel/util.ts +38 -29
- package/src/index.ts +19 -10
- package/src/utils/blastCache.test.ts +59 -20
- package/src/utils/blastCache.ts +62 -39
- package/src/utils/domainCache.ts +27 -15
- package/src/utils/ebiJobDispatcher.test.ts +18 -3
- package/src/utils/ebiJobDispatcher.ts +12 -16
- package/src/utils/eutils.test.ts +39 -0
- package/src/utils/eutils.ts +64 -14
- package/src/utils/idb.test.ts +106 -0
- package/src/utils/idb.ts +57 -5
- package/src/utils/ncbiDomains.ts +4 -4
- package/src/utils/ncbiOrthologs.test.ts +20 -0
- package/src/utils/ncbiOrthologs.ts +26 -28
- package/src/utils/ncbiTaxonomy.ts +17 -20
- package/src/utils/taxonomyNames.test.ts +41 -0
- package/src/utils/taxonomyNames.ts +33 -19
- package/src/utils/unirefHomologs.test.ts +11 -1
- package/src/utils/unirefHomologs.ts +2 -2
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.d.ts +0 -16
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +0 -31
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +0 -16
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +0 -16
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +0 -9
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +0 -15
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +0 -13
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +0 -22
- package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +0 -59
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +0 -43
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +0 -31
- package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +0 -42
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { getConf } from '@jbrowse/core/configuration';
|
|
2
2
|
import { getSession } from '@jbrowse/core/util';
|
|
3
|
+
import { isAlive } from '@jbrowse/mobx-state-tree';
|
|
4
|
+
import { transaction, untracked } from 'mobx';
|
|
3
5
|
import { getProteinSequenceFromFeature } from '../LaunchMsaView/components/calculateProteinSequence';
|
|
4
6
|
import { fetchSeq } from '../LaunchMsaView/components/fetchSeq';
|
|
5
7
|
import { getTranscriptFeatures } from '../LaunchMsaView/util';
|
|
6
|
-
// The short form of a connected launch. A spec that says
|
|
7
|
-
//
|
|
8
|
-
// {"type":"MsaView","connectedViewId":"lgv1","connectedTranscript":"NM_000546.6", ...}
|
|
9
|
-
//
|
|
10
|
-
// gets its `connectedFeature` -- the exon model every genome<->column mapping
|
|
11
|
-
// runs through -- looked up here, from the gene tracks the connected genome
|
|
12
|
-
// view has open, and its query row translated from that transcript. Before
|
|
13
|
-
// this the spec had to carry the transcript's CDS list itself, ~1.5kB of
|
|
14
|
-
// coordinates a person cannot type and a tutorial cannot show.
|
|
15
|
-
//
|
|
16
|
-
// jbrowse-plugin-protein3d does the same for its `transcriptId` shorthand,
|
|
17
|
-
// against a `connectedView` spec of its own; here the genome view already
|
|
18
|
-
// exists (the spec pinned its id), so its tracks and regions are read off the
|
|
19
|
-
// live model instead.
|
|
20
8
|
function stripVersion(id) {
|
|
21
9
|
return id.replace(/\.\d+$/, '');
|
|
22
10
|
}
|
|
@@ -29,28 +17,51 @@ function transcriptMatches(transcript, id) {
|
|
|
29
17
|
transcript.id(),
|
|
30
18
|
].some(c => typeof c === 'string' && (c === id || stripVersion(c) === target));
|
|
31
19
|
}
|
|
20
|
+
function plainRegions(regions) {
|
|
21
|
+
return regions.map(r => ({
|
|
22
|
+
assemblyName: r.assemblyName,
|
|
23
|
+
refName: r.refName,
|
|
24
|
+
start: Math.floor(r.start),
|
|
25
|
+
end: Math.ceil(r.end),
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The part of the genome on screen, then everything the view can scroll to.
|
|
30
|
+
* `displayedRegions` is usually whole chromosomes -- navigating to a locus
|
|
31
|
+
* displays its parent region -- so it is the fallback, not the first ask.
|
|
32
|
+
*/
|
|
33
|
+
export function searchWindows(view) {
|
|
34
|
+
const visible = plainRegions(view.dynamicBlocks?.contentBlocks ?? []);
|
|
35
|
+
const displayed = plainRegions(view.displayedRegions);
|
|
36
|
+
return visible.length > 0 ? [visible, displayed] : [displayed];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Gene models live on feature tracks. Asking an alignments, variant or
|
|
40
|
+
* quantitative track for a chromosome's worth of features to look for a
|
|
41
|
+
* transcript among them is the slowest possible way to find nothing.
|
|
42
|
+
*/
|
|
43
|
+
export function transcriptTracks(tracks) {
|
|
44
|
+
return tracks.filter(t => t.type === 'FeatureTrack');
|
|
45
|
+
}
|
|
32
46
|
async function findTranscript(self, transcriptId) {
|
|
33
47
|
const view = self.connectedView;
|
|
34
48
|
const session = getSession(self);
|
|
35
49
|
const sessionId = 'msaview-connectedTranscript';
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const hit = getTranscriptFeatures(feat).find(t => transcriptMatches(t, transcriptId));
|
|
52
|
-
if (hit) {
|
|
53
|
-
return hit;
|
|
50
|
+
const tracks = transcriptTracks(view.tracks);
|
|
51
|
+
for (const regions of searchWindows(view)) {
|
|
52
|
+
for (const track of tracks) {
|
|
53
|
+
// a named object keeps sessionId, which v4 hosts read from the args
|
|
54
|
+
const args = {
|
|
55
|
+
adapterConfig: getConf(track, 'adapter'),
|
|
56
|
+
sessionId,
|
|
57
|
+
regions,
|
|
58
|
+
};
|
|
59
|
+
const feats = await session.rpcManager.call(sessionId, 'CoreGetFeatures', args);
|
|
60
|
+
for (const feat of feats) {
|
|
61
|
+
const hit = getTranscriptFeatures(feat).find(t => transcriptMatches(t, transcriptId));
|
|
62
|
+
if (hit) {
|
|
63
|
+
return hit;
|
|
64
|
+
}
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
}
|
|
@@ -72,37 +83,30 @@ async function translate(self, transcript) {
|
|
|
72
83
|
assemblyGeneticCodeId,
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
!
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!proteinSequence) {
|
|
100
|
-
throw new Error(`Transcript "${connectedTranscript}" has no CDS to translate`);
|
|
101
|
-
}
|
|
102
|
-
// toJSON, not the Feature: the params are frozen snapshot properties, so
|
|
103
|
-
// an instance in one is gone the moment the session reloads
|
|
104
|
-
const transcriptJson = transcript.toJSON();
|
|
105
|
-
const { blastParams, orthologParams } = self;
|
|
86
|
+
/** the latest lookup per view; an older one finishing late writes nothing */
|
|
87
|
+
const latestLookup = new WeakMap();
|
|
88
|
+
async function lookUp({ self, transcriptId, blastParams, orthologParams, isCurrent, }) {
|
|
89
|
+
const trackCount = transcriptTracks(self.connectedView.tracks).length;
|
|
90
|
+
try {
|
|
91
|
+
self.setProgress(`Looking up ${transcriptId} in the genome view...`);
|
|
92
|
+
const transcript = await findTranscript(self, transcriptId);
|
|
93
|
+
if (!transcript) {
|
|
94
|
+
throw new Error(`Transcript "${transcriptId}" was not found in the ${trackCount} feature track(s) open in the connected genome view. Open the gene track that carries it, and make sure the view is on its locus.`);
|
|
95
|
+
}
|
|
96
|
+
const proteinSequence = await translate(self, transcript);
|
|
97
|
+
if (!proteinSequence) {
|
|
98
|
+
throw new Error(`Transcript "${transcriptId}" has no CDS to translate`);
|
|
99
|
+
}
|
|
100
|
+
if (!isCurrent()) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// toJSON, not the Feature: the params are frozen snapshot properties, so
|
|
104
|
+
// an instance in one is gone the moment the session reloads
|
|
105
|
+
const transcriptJson = transcript.toJSON();
|
|
106
|
+
// one transaction, so the launchers wake once, on a request that already
|
|
107
|
+
// carries its query
|
|
108
|
+
transaction(() => {
|
|
109
|
+
self.setProgress('');
|
|
106
110
|
if (blastParams && !blastParams.proteinSequence) {
|
|
107
111
|
self.setBlastParams({
|
|
108
112
|
...blastParams,
|
|
@@ -117,12 +121,43 @@ export function resolveConnectedTranscriptIfNeeded(self) {
|
|
|
117
121
|
proteinSequence,
|
|
118
122
|
});
|
|
119
123
|
}
|
|
120
|
-
// last, because the launchers wake on it
|
|
121
124
|
self.setConnectedFeature(transcriptJson);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
console.error(e);
|
|
129
|
+
if (isCurrent()) {
|
|
130
|
+
self.setProgress('');
|
|
125
131
|
self.setError(e);
|
|
126
132
|
}
|
|
127
|
-
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve `connectedTranscript` into `connectedFeature` once the connected
|
|
137
|
+
* genome view has its assembly and tracks, then hand the translation to
|
|
138
|
+
* whichever launch is waiting for a query.
|
|
139
|
+
*
|
|
140
|
+
* The autorun re-fires on the view's readiness (a track opened after a miss)
|
|
141
|
+
* and on the request itself, which is what `retryLaunch` re-states. The lookup
|
|
142
|
+
* runs untracked, so scrolling the genome view is not a reason to look again.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveConnectedTranscriptIfNeeded(self) {
|
|
145
|
+
const { connectedTranscript, connectedFeature, connectedView, blastParams, orthologParams, } = self;
|
|
146
|
+
if (!connectedTranscript ||
|
|
147
|
+
connectedFeature ||
|
|
148
|
+
!connectedView?.initialized ||
|
|
149
|
+
connectedView.tracks.length === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const lookup = (latestLookup.get(self) ?? 0) + 1;
|
|
153
|
+
latestLookup.set(self, lookup);
|
|
154
|
+
untracked(() => {
|
|
155
|
+
void lookUp({
|
|
156
|
+
self,
|
|
157
|
+
transcriptId: connectedTranscript,
|
|
158
|
+
blastParams,
|
|
159
|
+
orthologParams,
|
|
160
|
+
isCurrent: () => isAlive(self) && latestLookup.get(self) === lookup,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
128
163
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import stateModelFactory from './model';
|
|
3
|
+
const rpcCall = vi.fn();
|
|
4
|
+
const lgv = {
|
|
5
|
+
id: 'lgv1',
|
|
6
|
+
initialized: true,
|
|
7
|
+
assemblyNames: ['hg38'],
|
|
8
|
+
tracks: [],
|
|
9
|
+
displayedRegions: [
|
|
10
|
+
{ assemblyName: 'hg38', refName: 'chr17', start: 0, end: 83_257_441 },
|
|
11
|
+
],
|
|
12
|
+
dynamicBlocks: {
|
|
13
|
+
contentBlocks: [],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
vi.mock('@jbrowse/core/util', async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal()),
|
|
18
|
+
getSession: () => ({
|
|
19
|
+
views: [lgv],
|
|
20
|
+
hovered: undefined,
|
|
21
|
+
rpcManager: { call: rpcCall },
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
vi.mock('@jbrowse/core/configuration', async (importOriginal) => ({
|
|
25
|
+
...(await importOriginal()),
|
|
26
|
+
getConf: (track) => track.adapter,
|
|
27
|
+
}));
|
|
28
|
+
vi.mock('../LaunchMsaView/components/fetchSeq', () => ({
|
|
29
|
+
fetchSeq: vi.fn(async () => ({ seq: 'ATGAAAGTG' })),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock('../LaunchMsaView/components/calculateProteinSequence', () => ({
|
|
32
|
+
getProteinSequenceFromFeature: vi.fn(() => 'MKV'),
|
|
33
|
+
}));
|
|
34
|
+
vi.mock('./doLaunchBlast', () => ({
|
|
35
|
+
doLaunchBlast: vi.fn(() => new Promise(() => { })),
|
|
36
|
+
}));
|
|
37
|
+
vi.mock('./msaDataStore', () => ({
|
|
38
|
+
cleanupOldData: vi.fn(async () => { }),
|
|
39
|
+
deleteMsaData: vi.fn(async () => { }),
|
|
40
|
+
generateDataStoreId: vi.fn(),
|
|
41
|
+
retrieveMsaData: vi.fn(),
|
|
42
|
+
storeMsaData: vi.fn(),
|
|
43
|
+
}));
|
|
44
|
+
function transcript(id) {
|
|
45
|
+
const json = { uniqueId: id, id, refName: 'chr17', start: 0, end: 9 };
|
|
46
|
+
return {
|
|
47
|
+
id: () => id,
|
|
48
|
+
get: (key) => (key === 'id' || key === 'name' ? id : undefined),
|
|
49
|
+
toJSON: () => json,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const settle = () => new Promise(res => setTimeout(res, 0));
|
|
53
|
+
function connectedView() {
|
|
54
|
+
return stateModelFactory().create({
|
|
55
|
+
type: 'MsaView',
|
|
56
|
+
id: 'msa1',
|
|
57
|
+
connectedViewId: 'lgv1',
|
|
58
|
+
connectedTranscript: 'NM_000546.6',
|
|
59
|
+
blastParams: {
|
|
60
|
+
searchProgram: 'blastp',
|
|
61
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
62
|
+
msaAlgorithm: 'clustalo',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
rpcCall.mockReset();
|
|
68
|
+
lgv.tracks = [{ type: 'FeatureTrack', adapter: 'genes' }];
|
|
69
|
+
lgv.dynamicBlocks.contentBlocks = [];
|
|
70
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
71
|
+
});
|
|
72
|
+
describe('a lookup that misses', () => {
|
|
73
|
+
test('reports the miss without leaving its progress up', async () => {
|
|
74
|
+
rpcCall.mockResolvedValue([]);
|
|
75
|
+
const model = connectedView();
|
|
76
|
+
await settle();
|
|
77
|
+
expect(model.error).toBeInstanceOf(Error);
|
|
78
|
+
expect(model.progress).toBe('');
|
|
79
|
+
});
|
|
80
|
+
test('Retry looks again', async () => {
|
|
81
|
+
rpcCall.mockResolvedValue([]);
|
|
82
|
+
const model = connectedView();
|
|
83
|
+
await settle();
|
|
84
|
+
const calls = rpcCall.mock.calls.length;
|
|
85
|
+
rpcCall.mockResolvedValue([transcript('NM_000546.6')]);
|
|
86
|
+
model.retryLaunch();
|
|
87
|
+
await settle();
|
|
88
|
+
expect(rpcCall.mock.calls.length).toBeGreaterThan(calls);
|
|
89
|
+
expect(model.connectedFeature).toMatchObject({ id: 'NM_000546.6' });
|
|
90
|
+
expect(model.blastParams?.proteinSequence).toBe('MKV');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('where a lookup searches', () => {
|
|
94
|
+
test('feature tracks only, on screen before the whole chromosome', async () => {
|
|
95
|
+
rpcCall.mockResolvedValue([]);
|
|
96
|
+
lgv.tracks = [
|
|
97
|
+
{ type: 'AlignmentsTrack', adapter: 'bam' },
|
|
98
|
+
{ type: 'FeatureTrack', adapter: 'genes' },
|
|
99
|
+
];
|
|
100
|
+
lgv.dynamicBlocks.contentBlocks = [
|
|
101
|
+
{
|
|
102
|
+
assemblyName: 'hg38',
|
|
103
|
+
refName: 'chr17',
|
|
104
|
+
start: 7_661_778.6,
|
|
105
|
+
end: 7_687_538.2,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
connectedView();
|
|
109
|
+
await settle();
|
|
110
|
+
const asked = rpcCall.mock.calls.map(([, , args]) => [
|
|
111
|
+
args.adapterConfig,
|
|
112
|
+
args.regions,
|
|
113
|
+
]);
|
|
114
|
+
expect(asked).toEqual([
|
|
115
|
+
[
|
|
116
|
+
'genes',
|
|
117
|
+
[
|
|
118
|
+
{
|
|
119
|
+
assemblyName: 'hg38',
|
|
120
|
+
refName: 'chr17',
|
|
121
|
+
start: 7_661_778,
|
|
122
|
+
end: 7_687_539,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
],
|
|
126
|
+
['genes', lgv.displayedRegions],
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('two lookups in flight', () => {
|
|
131
|
+
test('only the later one writes', async () => {
|
|
132
|
+
const pending = [];
|
|
133
|
+
rpcCall.mockImplementation(() => new Promise(res => {
|
|
134
|
+
pending.push(res);
|
|
135
|
+
}));
|
|
136
|
+
const model = connectedView();
|
|
137
|
+
await settle();
|
|
138
|
+
model.retryLaunch();
|
|
139
|
+
await settle();
|
|
140
|
+
expect(pending).toHaveLength(2);
|
|
141
|
+
pending[1]([transcript('NM_000546.6')]);
|
|
142
|
+
await settle();
|
|
143
|
+
const written = model.connectedFeature;
|
|
144
|
+
pending[0]([transcript('NM_000546.5')]);
|
|
145
|
+
await settle();
|
|
146
|
+
expect(written).toMatchObject({ id: 'NM_000546.6' });
|
|
147
|
+
expect(model.connectedFeature).toBe(written);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -26,11 +26,15 @@ interface LaunchedData {
|
|
|
26
26
|
* Run one launch attempt, owning the AbortController that ties it to the view.
|
|
27
27
|
*
|
|
28
28
|
* The controller lives on the model because two things end a launch and neither
|
|
29
|
-
* is here: the Cancel button, and the disposer `afterCreate` registers.
|
|
30
|
-
* it
|
|
31
|
-
*
|
|
29
|
+
* is here: the Cancel button, and the disposer `afterCreate` registers. A launch
|
|
30
|
+
* already holding it is aborted first, so a re-fired autorun replaces the
|
|
31
|
+
* attempt rather than orphaning one that nothing can cancel.
|
|
32
|
+
*
|
|
33
|
+
* Runs untracked: it is called from the launch autoruns, and whatever the
|
|
34
|
+
* launch body reads before its first await would otherwise become a reason to
|
|
35
|
+
* launch again.
|
|
32
36
|
*/
|
|
33
|
-
export declare function runLaunch(
|
|
37
|
+
export declare function runLaunch(args: {
|
|
34
38
|
self: JBrowsePluginMsaViewModel;
|
|
35
39
|
message: string;
|
|
36
40
|
launch: (scope: LaunchScope) => Promise<LaunchedData>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { untracked } from 'mobx';
|
|
2
3
|
import { isAbortError } from '../utils/fetch';
|
|
3
4
|
/**
|
|
4
5
|
* The in-browser aligner and a search hand back rows and no tree;
|
|
@@ -33,11 +34,21 @@ function buildTree(self) {
|
|
|
33
34
|
* Run one launch attempt, owning the AbortController that ties it to the view.
|
|
34
35
|
*
|
|
35
36
|
* The controller lives on the model because two things end a launch and neither
|
|
36
|
-
* is here: the Cancel button, and the disposer `afterCreate` registers.
|
|
37
|
-
* it
|
|
38
|
-
*
|
|
37
|
+
* is here: the Cancel button, and the disposer `afterCreate` registers. A launch
|
|
38
|
+
* already holding it is aborted first, so a re-fired autorun replaces the
|
|
39
|
+
* attempt rather than orphaning one that nothing can cancel.
|
|
40
|
+
*
|
|
41
|
+
* Runs untracked: it is called from the launch autoruns, and whatever the
|
|
42
|
+
* launch body reads before its first await would otherwise become a reason to
|
|
43
|
+
* launch again.
|
|
39
44
|
*/
|
|
40
|
-
export function runLaunch(
|
|
45
|
+
export function runLaunch(args) {
|
|
46
|
+
untracked(() => {
|
|
47
|
+
startLaunch(args);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function startLaunch({ self, message, launch, onLaunched, }) {
|
|
51
|
+
self.launchController?.abort();
|
|
41
52
|
const controller = new AbortController();
|
|
42
53
|
const { signal } = controller;
|
|
43
54
|
self.setLaunchController(controller);
|
|
@@ -112,6 +112,22 @@ describe('a launch that is abandoned', () => {
|
|
|
112
112
|
expect(writes.some(w => w.startsWith('setError:Aborted'))).toBe(false);
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
|
+
describe('a launch started while another holds the controller', () => {
|
|
116
|
+
test('aborts the first, so nothing is left polling that Cancel cannot reach', async () => {
|
|
117
|
+
const { model } = makeModel();
|
|
118
|
+
const signals = [];
|
|
119
|
+
const launch = (scope) => {
|
|
120
|
+
signals.push(scope.signal);
|
|
121
|
+
return new Promise(() => { });
|
|
122
|
+
};
|
|
123
|
+
runLaunch({ self: model, message: 'a', onLaunched: () => { }, launch });
|
|
124
|
+
runLaunch({ self: model, message: 'b', onLaunched: () => { }, launch });
|
|
125
|
+
await settle();
|
|
126
|
+
expect(signals.map(s => s.aborted)).toEqual([true, false]);
|
|
127
|
+
model.launchController.abort();
|
|
128
|
+
expect(signals[1].aborted).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
115
131
|
describe('a launch that fails', () => {
|
|
116
132
|
test('records the error and stops the spinner', async () => {
|
|
117
133
|
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
@@ -49,6 +49,9 @@ function makeModel(over = {}) {
|
|
|
49
49
|
setTreeMetadata(arg) {
|
|
50
50
|
model.data.treeMetadata = arg;
|
|
51
51
|
},
|
|
52
|
+
setGFF(arg) {
|
|
53
|
+
model.data.gff = arg;
|
|
54
|
+
},
|
|
52
55
|
...over,
|
|
53
56
|
};
|
|
54
57
|
return model;
|
|
@@ -82,6 +85,14 @@ describe('restoring a view from IndexedDB', () => {
|
|
|
82
85
|
storeDataToIndexedDB(model);
|
|
83
86
|
expect(mockStore).not.toHaveBeenCalled();
|
|
84
87
|
});
|
|
88
|
+
// react-msaview drops a GFF over 50kB from the snapshot too
|
|
89
|
+
test('a stored GFF comes back with the alignment', async () => {
|
|
90
|
+
mockRetrieve.mockResolvedValue({ msa: MSA, gff: 'a\t.\tdomain\t1\t2' });
|
|
91
|
+
const model = makeModel({ dataStoreId: 'msa-1' });
|
|
92
|
+
loadStoredData(model);
|
|
93
|
+
await settle();
|
|
94
|
+
expect(model.data.gff).toBe('a\t.\tdomain\t1\t2');
|
|
95
|
+
});
|
|
85
96
|
// the row expires or the user clears site data, and the view used to reopen as
|
|
86
97
|
// a blank import form with no hint that anything had been lost
|
|
87
98
|
test('a row that is gone says so, in words that name the policy', async () => {
|
|
@@ -147,6 +158,18 @@ describe('keeping IndexedDB up to date', () => {
|
|
|
147
158
|
});
|
|
148
159
|
expect(model.dataStoreId).toBe('msa-1');
|
|
149
160
|
});
|
|
161
|
+
test('a GFF loaded after the first write updates the row', async () => {
|
|
162
|
+
const model = makeModel({
|
|
163
|
+
dataStoreId: 'msa-1',
|
|
164
|
+
rows: [['a', 'MK']],
|
|
165
|
+
data: { msa: MSA },
|
|
166
|
+
lastStoredData: { msa: MSA },
|
|
167
|
+
});
|
|
168
|
+
model.data.gff = 'a\t.\tdomain\t1\t2';
|
|
169
|
+
storeDataToIndexedDB(model);
|
|
170
|
+
await settle();
|
|
171
|
+
expect(mockStore).toHaveBeenCalledWith('msa-1', expect.objectContaining({ gff: 'a\t.\tdomain\t1\t2' }));
|
|
172
|
+
});
|
|
150
173
|
// a browser that refuses IndexedDB (private mode) answers every write the same
|
|
151
174
|
// way, and the autorun reruns on its own isStoringData flag -- so a write that
|
|
152
175
|
// is not recorded as attempted is a write that retries forever
|
|
@@ -15,17 +15,18 @@ const mafRegion = {
|
|
|
15
15
|
assemblyName: 'hg38',
|
|
16
16
|
};
|
|
17
17
|
// A model wired through the real genomeToMSA path: a connected genome view
|
|
18
|
-
// over a maf region, with
|
|
18
|
+
// over a maf region, with the column converters as identity so the asserted
|
|
19
19
|
// column equals the ungapped offset into the region.
|
|
20
20
|
function makeModel() {
|
|
21
21
|
const calls = [];
|
|
22
22
|
const model = {
|
|
23
23
|
querySeqName: 'hg38.chr1',
|
|
24
|
-
|
|
24
|
+
rowMap: new Map([['hg38.chr1', 'ACGTACGTAC']]),
|
|
25
25
|
transcriptToMsaMap: undefined,
|
|
26
26
|
mafRegion,
|
|
27
27
|
connectedView: { initialized: true, assemblyNames: ['hg38'] },
|
|
28
28
|
seqPosToVisibleCol: (_name, pos) => pos,
|
|
29
|
+
visibleColToSeqPos: (_name, col) => col,
|
|
29
30
|
setMousePos: (col) => {
|
|
30
31
|
calls.push(col);
|
|
31
32
|
},
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether `querySeqName` names a row this alignment actually has.
|
|
2
|
+
* Whether `querySeqName` names a row this alignment actually has. The name
|
|
3
|
+
* defaults to `QUERY`, which an uploaded alignment has no reason to carry, so
|
|
4
|
+
* the genome and structure hovers ask this before mapping anything.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* name is wrong more often than it looks: it defaults to `QUERY`, which an
|
|
8
|
-
* uploaded alignment has no reason to carry, and the manual panel leaves it
|
|
9
|
-
* empty when it cannot match the protein to a row.
|
|
10
|
-
*
|
|
11
|
-
* The other direction has no such hole: msaCoordToGenomeRegions needs the query
|
|
12
|
-
* row's sequence to map a column at all, so a missing row is already nothing
|
|
13
|
-
* there.
|
|
6
|
+
* `rowMap`, not `rows`: `rows` is what is on screen, and a query row folded
|
|
7
|
+
* into a collapsed clade is still in the alignment, still has its columns, and
|
|
8
|
+
* still maps.
|
|
14
9
|
*/
|
|
15
10
|
export declare function hasQueryRow(model: {
|
|
16
|
-
|
|
11
|
+
rowMap: ReadonlyMap<string, string>;
|
|
17
12
|
querySeqName: string;
|
|
18
13
|
}): boolean;
|
|
19
14
|
/**
|
|
@@ -35,23 +30,28 @@ interface LiveFeature {
|
|
|
35
30
|
export declare function transcriptFields(transcript?: TranscriptRef): Record<string, unknown>;
|
|
36
31
|
/** what a transcript is called, preferring its name over its id */
|
|
37
32
|
export declare function transcriptName(transcript?: TranscriptRef): string | undefined;
|
|
38
|
-
|
|
33
|
+
interface QueryRowColumns {
|
|
39
34
|
querySeqName: string;
|
|
40
|
-
querySeqOffset: number;
|
|
41
35
|
seqPosToVisibleCol: (rowName: string, seqPos: number) => number | undefined;
|
|
42
36
|
visibleColToSeqPos: (rowName: string, visibleCol: number) => number | undefined;
|
|
43
37
|
}
|
|
38
|
+
export interface QueryRowModel extends QueryRowColumns {
|
|
39
|
+
querySeqOffset: number;
|
|
40
|
+
}
|
|
44
41
|
/**
|
|
45
|
-
* The visible column showing residue `
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* The visible column showing residue `seqPos` (0-based) of the query row, or
|
|
43
|
+
* undefined when the row does not carry that residue or react-msaview is
|
|
44
|
+
* hiding its column.
|
|
48
45
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
* react-msaview answers one column past the end for a position the row does not
|
|
47
|
+
* have, which would light the last column for every residue beyond the row, so
|
|
48
|
+
* the round trip back through visibleColToSeqPos is what rejects those.
|
|
49
|
+
*/
|
|
50
|
+
export declare function querySeqPosToVisibleCol(model: QueryRowColumns, seqPos: number): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The visible column showing residue `proteinPos` (0-based) of the transcript.
|
|
53
|
+
* `querySeqOffset` is what separates the two: a pasted BLAST alignment carries
|
|
54
|
+
* the aligned region, not the whole protein.
|
|
55
55
|
*/
|
|
56
56
|
export declare function transcriptPosToVisibleCol(model: QueryRowModel, proteinPos: number): number | undefined;
|
|
57
57
|
export declare function hasHoverPosition(hovered: unknown): hovered is {
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether `querySeqName` names a row this alignment actually has.
|
|
2
|
+
* Whether `querySeqName` names a row this alignment actually has. The name
|
|
3
|
+
* defaults to `QUERY`, which an uploaded alignment has no reason to carry, so
|
|
4
|
+
* the genome and structure hovers ask this before mapping anything.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* name is wrong more often than it looks: it defaults to `QUERY`, which an
|
|
8
|
-
* uploaded alignment has no reason to carry, and the manual panel leaves it
|
|
9
|
-
* empty when it cannot match the protein to a row.
|
|
10
|
-
*
|
|
11
|
-
* The other direction has no such hole: msaCoordToGenomeRegions needs the query
|
|
12
|
-
* row's sequence to map a column at all, so a missing row is already nothing
|
|
13
|
-
* there.
|
|
6
|
+
* `rowMap`, not `rows`: `rows` is what is on screen, and a query row folded
|
|
7
|
+
* into a collapsed clade is still in the alignment, still has its columns, and
|
|
8
|
+
* still maps.
|
|
14
9
|
*/
|
|
15
10
|
export function hasQueryRow(model) {
|
|
16
|
-
return model.
|
|
11
|
+
return model.rowMap.has(model.querySeqName);
|
|
17
12
|
}
|
|
18
13
|
/**
|
|
19
14
|
* Read the transcript's fields whichever shape it arrived in: a caller inside
|
|
@@ -36,20 +31,16 @@ export function transcriptName(transcript) {
|
|
|
36
31
|
return str(fields.name) ?? str(fields.id);
|
|
37
32
|
}
|
|
38
33
|
/**
|
|
39
|
-
* The visible column showing residue `
|
|
40
|
-
*
|
|
41
|
-
*
|
|
34
|
+
* The visible column showing residue `seqPos` (0-based) of the query row, or
|
|
35
|
+
* undefined when the row does not carry that residue or react-msaview is
|
|
36
|
+
* hiding its column.
|
|
42
37
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* past the end for a position it does not have, which would light the last
|
|
47
|
-
* column for every residue beyond the row, so the round trip back through
|
|
48
|
-
* visibleColToSeqPos is what rejects those.
|
|
38
|
+
* react-msaview answers one column past the end for a position the row does not
|
|
39
|
+
* have, which would light the last column for every residue beyond the row, so
|
|
40
|
+
* the round trip back through visibleColToSeqPos is what rejects those.
|
|
49
41
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const { querySeqName
|
|
52
|
-
const seqPos = proteinPos - querySeqOffset;
|
|
42
|
+
export function querySeqPosToVisibleCol(model, seqPos) {
|
|
43
|
+
const { querySeqName } = model;
|
|
53
44
|
if (seqPos < 0) {
|
|
54
45
|
return undefined;
|
|
55
46
|
}
|
|
@@ -59,6 +50,14 @@ export function transcriptPosToVisibleCol(model, proteinPos) {
|
|
|
59
50
|
? col
|
|
60
51
|
: undefined;
|
|
61
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The visible column showing residue `proteinPos` (0-based) of the transcript.
|
|
55
|
+
* `querySeqOffset` is what separates the two: a pasted BLAST alignment carries
|
|
56
|
+
* the aligned region, not the whole protein.
|
|
57
|
+
*/
|
|
58
|
+
export function transcriptPosToVisibleCol(model, proteinPos) {
|
|
59
|
+
return querySeqPosToVisibleCol(model, proteinPos - model.querySeqOffset);
|
|
60
|
+
}
|
|
62
61
|
export function hasHoverPosition(hovered) {
|
|
63
62
|
return (!!hovered &&
|
|
64
63
|
typeof hovered === 'object' &&
|