jbrowse-plugin-msaview 3.5.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.
Files changed (242) hide show
  1. package/README.md +8 -0
  2. package/dist/AddHighlightModel/index.js +3 -3
  3. package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +8 -2
  4. package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +3 -3
  5. package/dist/BgzipFastaMsaAdapter/configSchema.js +1 -1
  6. package/dist/LaunchMsaView/cleanProteinSequence.test.d.ts +1 -0
  7. package/dist/LaunchMsaView/cleanProteinSequence.test.js +77 -0
  8. package/dist/LaunchMsaView/codingFeature.d.ts +14 -0
  9. package/dist/LaunchMsaView/codingFeature.js +38 -0
  10. package/dist/LaunchMsaView/codingFeature.test.d.ts +1 -0
  11. package/dist/LaunchMsaView/codingFeature.test.js +75 -0
  12. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.d.ts +3 -1
  13. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +47 -55
  14. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +3 -1
  15. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +16 -17
  16. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.d.ts +7 -3
  17. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +8 -7
  18. package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.js +2 -2
  19. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +14 -13
  20. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +2 -0
  21. package/dist/LaunchMsaView/components/BlastQuery/consts.js +28 -4
  22. package/dist/LaunchMsaView/components/HelpButton.d.ts +2 -0
  23. package/dist/LaunchMsaView/components/HelpButton.js +17 -0
  24. package/dist/LaunchMsaView/components/HelpDialog.d.ts +4 -0
  25. package/dist/LaunchMsaView/components/HelpDialog.js +18 -0
  26. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.d.ts +3 -1
  27. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +21 -16
  28. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.d.ts +3 -1
  29. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +34 -26
  30. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +3 -1
  31. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +30 -26
  32. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +2 -1
  33. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +18 -2
  34. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.d.ts +1 -0
  35. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.js +13 -0
  36. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.d.ts +3 -1
  37. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +50 -27
  38. package/dist/LaunchMsaView/components/SequenceStatus.d.ts +14 -0
  39. package/dist/LaunchMsaView/components/SequenceStatus.js +15 -0
  40. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +6 -2
  41. package/dist/LaunchMsaView/components/SubmitCancelActions.js +28 -13
  42. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +52 -4
  43. package/dist/LaunchMsaView/components/TabPanel.js +15 -3
  44. package/dist/LaunchMsaView/components/TabPanel.test.d.ts +1 -0
  45. package/dist/LaunchMsaView/components/TabPanel.test.js +36 -0
  46. package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +3 -1
  47. package/dist/LaunchMsaView/components/TranscriptSelector.js +4 -2
  48. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +12 -2
  49. package/dist/LaunchMsaView/components/calculateProteinSequence.js +38 -23
  50. package/dist/LaunchMsaView/components/fetchSeq.d.ts +4 -1
  51. package/dist/LaunchMsaView/components/fetchSeq.js +14 -4
  52. package/dist/LaunchMsaView/components/launchConnectedView.d.ts +23 -0
  53. package/dist/LaunchMsaView/components/launchConnectedView.js +39 -0
  54. package/dist/LaunchMsaView/components/launchPlacement.d.ts +8 -0
  55. package/dist/LaunchMsaView/components/launchPlacement.js +25 -0
  56. package/dist/LaunchMsaView/components/useFeatureSequence.d.ts +2 -0
  57. package/dist/LaunchMsaView/components/useFeatureSequence.js +14 -11
  58. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +10 -1
  59. package/dist/LaunchMsaView/components/useTranscriptSelection.js +21 -3
  60. package/dist/LaunchMsaView/components/useTranscriptSelection.test.d.ts +1 -0
  61. package/dist/LaunchMsaView/components/useTranscriptSelection.test.js +57 -0
  62. package/dist/LaunchMsaView/detectQueryRow.js +4 -7
  63. package/dist/LaunchMsaView/index.js +28 -9
  64. package/dist/LaunchMsaView/launchTarget.d.ts +36 -8
  65. package/dist/LaunchMsaView/launchTarget.js +23 -13
  66. package/dist/LaunchMsaView/launchTarget.test.js +80 -15
  67. package/dist/LaunchMsaView/useQueryRowName.d.ts +12 -0
  68. package/dist/LaunchMsaView/useQueryRowName.js +11 -0
  69. package/dist/LaunchMsaView/useQueryRowName.test.js +18 -1
  70. package/dist/LaunchMsaView/util.d.ts +6 -0
  71. package/dist/LaunchMsaView/util.js +12 -3
  72. package/dist/LaunchMsaViewExtensionPoint/index.test.js +15 -0
  73. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +6 -4
  74. package/dist/MsaViewPanel/afterCreateAutoruns.js +38 -14
  75. package/dist/MsaViewPanel/applyRegion.d.ts +23 -0
  76. package/dist/MsaViewPanel/applyRegion.js +19 -0
  77. package/dist/MsaViewPanel/applyRegion.test.d.ts +1 -0
  78. package/dist/MsaViewPanel/applyRegion.test.js +45 -0
  79. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +4 -3
  80. package/dist/MsaViewPanel/components/LaunchProgress.js +10 -7
  81. package/dist/MsaViewPanel/components/MsaViewPanel.js +7 -3
  82. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +24 -0
  83. package/dist/MsaViewPanel/doLaunchBlast.js +2 -1
  84. package/dist/MsaViewPanel/doLaunchBlast.test.js +12 -0
  85. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
  86. package/dist/MsaViewPanel/genomeToMSA.js +24 -6
  87. package/dist/MsaViewPanel/genomeToMSA.test.js +218 -44
  88. package/dist/MsaViewPanel/launchAutoruns.test.d.ts +1 -0
  89. package/dist/MsaViewPanel/launchAutoruns.test.js +36 -0
  90. package/dist/MsaViewPanel/model.d.ts +156 -61
  91. package/dist/MsaViewPanel/model.js +69 -3
  92. package/dist/MsaViewPanel/model.test.js +66 -0
  93. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  94. package/dist/MsaViewPanel/msaDataStore.js +3 -12
  95. package/dist/MsaViewPanel/msaDataStore.test.js +8 -1
  96. package/dist/MsaViewPanel/observeProteinHighlights.test.js +4 -4
  97. package/dist/MsaViewPanel/regionSnapshot.test.d.ts +1 -0
  98. package/dist/MsaViewPanel/regionSnapshot.test.js +29 -0
  99. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +41 -4
  100. package/dist/MsaViewPanel/resolveConnectedTranscript.js +109 -68
  101. package/dist/MsaViewPanel/resolveConnectedTranscript.test.d.ts +1 -0
  102. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +149 -0
  103. package/dist/MsaViewPanel/runLaunch.d.ts +8 -4
  104. package/dist/MsaViewPanel/runLaunch.js +15 -4
  105. package/dist/MsaViewPanel/runLaunch.test.js +16 -0
  106. package/dist/MsaViewPanel/storedData.test.js +43 -0
  107. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +15 -7
  108. package/dist/MsaViewPanel/util.d.ts +23 -23
  109. package/dist/MsaViewPanel/util.js +23 -24
  110. package/dist/index.d.ts +11 -11
  111. package/dist/index.js +17 -10
  112. package/dist/jbrowse-plugin-msaview.umd.production.min.js +54 -67
  113. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  114. package/dist/utils/blastCache.d.ts +26 -2
  115. package/dist/utils/blastCache.js +51 -39
  116. package/dist/utils/blastCache.test.js +46 -19
  117. package/dist/utils/domainCache.d.ts +1 -1
  118. package/dist/utils/domainCache.js +18 -14
  119. package/dist/utils/ebiJobDispatcher.d.ts +11 -0
  120. package/dist/utils/ebiJobDispatcher.js +12 -13
  121. package/dist/utils/ebiJobDispatcher.test.js +17 -3
  122. package/dist/utils/eutils.d.ts +14 -0
  123. package/dist/utils/eutils.js +48 -14
  124. package/dist/utils/eutils.test.d.ts +1 -0
  125. package/dist/utils/eutils.test.js +31 -0
  126. package/dist/utils/idb.d.ts +16 -3
  127. package/dist/utils/idb.js +51 -5
  128. package/dist/utils/idb.test.d.ts +1 -0
  129. package/dist/utils/idb.test.js +78 -0
  130. package/dist/utils/ncbiDomains.js +4 -4
  131. package/dist/utils/ncbiOrthologs.d.ts +1 -1
  132. package/dist/utils/ncbiOrthologs.js +18 -17
  133. package/dist/utils/ncbiOrthologs.test.js +17 -1
  134. package/dist/utils/ncbiTaxonomy.js +14 -15
  135. package/dist/utils/taxonomyNames.js +23 -19
  136. package/dist/utils/taxonomyNames.test.d.ts +1 -0
  137. package/dist/utils/taxonomyNames.test.js +33 -0
  138. package/dist/utils/unirefHomologs.js +2 -2
  139. package/dist/utils/unirefHomologs.test.js +8 -1
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/package.json +23 -23
  143. package/src/AddHighlightModel/index.tsx +1 -1
  144. package/src/BgzipFastaMsaAdapter/configSchema.ts +1 -1
  145. package/src/LaunchMsaView/cleanProteinSequence.test.ts +91 -0
  146. package/src/LaunchMsaView/codingFeature.test.ts +96 -0
  147. package/src/LaunchMsaView/codingFeature.ts +49 -0
  148. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +60 -56
  149. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +18 -15
  150. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +24 -11
  151. package/src/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.tsx +4 -4
  152. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +16 -15
  153. package/src/LaunchMsaView/components/BlastQuery/consts.ts +31 -4
  154. package/src/LaunchMsaView/components/HelpButton.tsx +33 -0
  155. package/src/LaunchMsaView/components/HelpDialog.tsx +78 -0
  156. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +54 -34
  157. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +46 -23
  158. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +41 -28
  159. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.ts +16 -0
  160. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +24 -2
  161. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +81 -25
  162. package/src/LaunchMsaView/components/SequenceStatus.tsx +29 -0
  163. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +88 -4
  164. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +43 -16
  165. package/src/LaunchMsaView/components/TabPanel.test.tsx +51 -0
  166. package/src/LaunchMsaView/components/TabPanel.tsx +16 -4
  167. package/src/LaunchMsaView/components/TranscriptSelector.tsx +6 -1
  168. package/src/LaunchMsaView/components/calculateProteinSequence.ts +51 -22
  169. package/src/LaunchMsaView/components/fetchSeq.ts +25 -5
  170. package/src/LaunchMsaView/components/launchConnectedView.ts +56 -0
  171. package/src/LaunchMsaView/components/launchPlacement.tsx +41 -0
  172. package/src/LaunchMsaView/components/useFeatureSequence.ts +18 -11
  173. package/src/LaunchMsaView/components/useTranscriptSelection.test.tsx +66 -0
  174. package/src/LaunchMsaView/components/useTranscriptSelection.ts +41 -2
  175. package/src/LaunchMsaView/detectQueryRow.ts +5 -7
  176. package/src/LaunchMsaView/index.ts +32 -13
  177. package/src/LaunchMsaView/launchTarget.test.ts +88 -15
  178. package/src/LaunchMsaView/launchTarget.ts +49 -14
  179. package/src/LaunchMsaView/useQueryRowName.test.ts +28 -1
  180. package/src/LaunchMsaView/useQueryRowName.ts +17 -0
  181. package/src/LaunchMsaView/util.ts +15 -5
  182. package/src/LaunchMsaViewExtensionPoint/index.test.ts +16 -0
  183. package/src/LaunchMsaViewExtensionPoint/index.ts +28 -1
  184. package/src/MsaViewPanel/afterCreateAutoruns.ts +46 -14
  185. package/src/MsaViewPanel/applyRegion.test.ts +55 -0
  186. package/src/MsaViewPanel/applyRegion.ts +33 -0
  187. package/src/MsaViewPanel/components/LaunchProgress.tsx +12 -5
  188. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +26 -0
  189. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -5
  190. package/src/MsaViewPanel/doLaunchBlast.test.ts +20 -1
  191. package/src/MsaViewPanel/doLaunchBlast.ts +2 -1
  192. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +1 -1
  193. package/src/MsaViewPanel/genomeToMSA.test.ts +255 -59
  194. package/src/MsaViewPanel/genomeToMSA.ts +30 -5
  195. package/src/MsaViewPanel/launchAutoruns.test.ts +45 -0
  196. package/src/MsaViewPanel/model.test.ts +75 -0
  197. package/src/MsaViewPanel/model.ts +76 -3
  198. package/src/MsaViewPanel/msaDataStore.test.ts +14 -2
  199. package/src/MsaViewPanel/msaDataStore.ts +4 -12
  200. package/src/MsaViewPanel/observeProteinHighlights.test.ts +4 -4
  201. package/src/MsaViewPanel/regionSnapshot.test.ts +34 -0
  202. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +183 -0
  203. package/src/MsaViewPanel/resolveConnectedTranscript.ts +160 -67
  204. package/src/MsaViewPanel/runLaunch.test.ts +19 -0
  205. package/src/MsaViewPanel/runLaunch.ts +21 -4
  206. package/src/MsaViewPanel/storedData.test.ts +58 -0
  207. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +17 -7
  208. package/src/MsaViewPanel/util.ts +38 -29
  209. package/src/index.ts +19 -10
  210. package/src/utils/blastCache.test.ts +59 -20
  211. package/src/utils/blastCache.ts +62 -39
  212. package/src/utils/domainCache.ts +27 -15
  213. package/src/utils/ebiJobDispatcher.test.ts +18 -3
  214. package/src/utils/ebiJobDispatcher.ts +12 -16
  215. package/src/utils/eutils.test.ts +39 -0
  216. package/src/utils/eutils.ts +64 -14
  217. package/src/utils/idb.test.ts +106 -0
  218. package/src/utils/idb.ts +57 -5
  219. package/src/utils/ncbiDomains.ts +4 -4
  220. package/src/utils/ncbiOrthologs.test.ts +20 -0
  221. package/src/utils/ncbiOrthologs.ts +26 -28
  222. package/src/utils/ncbiTaxonomy.ts +17 -20
  223. package/src/utils/taxonomyNames.test.ts +41 -0
  224. package/src/utils/taxonomyNames.ts +33 -19
  225. package/src/utils/unirefHomologs.test.ts +11 -1
  226. package/src/utils/unirefHomologs.ts +2 -2
  227. package/src/version.ts +1 -1
  228. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.d.ts +0 -16
  229. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +0 -31
  230. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +0 -16
  231. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +0 -16
  232. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +0 -9
  233. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +0 -15
  234. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +0 -11
  235. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +0 -21
  236. package/dist/LaunchMsaView/components/geneticCodes.d.ts +0 -15
  237. package/dist/LaunchMsaView/components/geneticCodes.js +0 -227
  238. package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +0 -59
  239. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +0 -43
  240. package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +0 -31
  241. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +0 -38
  242. package/src/LaunchMsaView/components/geneticCodes.ts +0 -298
@@ -1,11 +1,17 @@
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
 
4
6
  import { getProteinSequenceFromFeature } from '../LaunchMsaView/components/calculateProteinSequence'
5
7
  import { fetchSeq } from '../LaunchMsaView/components/fetchSeq'
6
8
  import { getTranscriptFeatures } from '../LaunchMsaView/util'
7
9
 
8
- import type { JBrowsePluginMsaViewModel } from './model'
10
+ import type {
11
+ BlastParams,
12
+ JBrowsePluginMsaViewModel,
13
+ OrthologParams,
14
+ } from './model'
9
15
  import type { Feature } from '@jbrowse/core/util'
10
16
 
11
17
  // The short form of a connected launch. A spec that says
@@ -14,15 +20,26 @@ import type { Feature } from '@jbrowse/core/util'
14
20
  //
15
21
  // gets its `connectedFeature` -- the exon model every genome<->column mapping
16
22
  // runs through -- looked up here, from the gene tracks the connected genome
17
- // view has open, and its query row translated from that transcript. Before
18
- // this the spec had to carry the transcript's CDS list itself, ~1.5kB of
19
- // coordinates a person cannot type and a tutorial cannot show.
23
+ // view has open, and its query row translated from that transcript.
20
24
  //
21
25
  // jbrowse-plugin-protein3d does the same for its `transcriptId` shorthand,
22
26
  // against a `connectedView` spec of its own; here the genome view already
23
27
  // exists (the spec pinned its id), so its tracks and regions are read off the
24
28
  // live model instead.
25
29
 
30
+ interface Region {
31
+ assemblyName: string
32
+ refName: string
33
+ start: number
34
+ end: number
35
+ }
36
+
37
+ interface GenomeView {
38
+ tracks: { type: string }[]
39
+ displayedRegions: Region[]
40
+ dynamicBlocks?: { contentBlocks?: Region[] }
41
+ }
42
+
26
43
  function stripVersion(id: string) {
27
44
  return id.replace(/\.\d+$/, '')
28
45
  }
@@ -37,6 +54,35 @@ function transcriptMatches(transcript: Feature, id: string) {
37
54
  ].some(c => typeof c === 'string' && (c === id || stripVersion(c) === target))
38
55
  }
39
56
 
57
+ function plainRegions(regions: Region[]) {
58
+ return regions.map(r => ({
59
+ assemblyName: r.assemblyName,
60
+ refName: r.refName,
61
+ start: Math.floor(r.start),
62
+ end: Math.ceil(r.end),
63
+ }))
64
+ }
65
+
66
+ /**
67
+ * The part of the genome on screen, then everything the view can scroll to.
68
+ * `displayedRegions` is usually whole chromosomes -- navigating to a locus
69
+ * displays its parent region -- so it is the fallback, not the first ask.
70
+ */
71
+ export function searchWindows(view: GenomeView) {
72
+ const visible = plainRegions(view.dynamicBlocks?.contentBlocks ?? [])
73
+ const displayed = plainRegions(view.displayedRegions)
74
+ return visible.length > 0 ? [visible, displayed] : [displayed]
75
+ }
76
+
77
+ /**
78
+ * Gene models live on feature tracks. Asking an alignments, variant or
79
+ * quantitative track for a chromosome's worth of features to look for a
80
+ * transcript among them is the slowest possible way to find nothing.
81
+ */
82
+ export function transcriptTracks<T extends { type: string }>(tracks: T[]) {
83
+ return tracks.filter(t => t.type === 'FeatureTrack')
84
+ }
85
+
40
86
  async function findTranscript(
41
87
  self: JBrowsePluginMsaViewModel,
42
88
  transcriptId: string,
@@ -44,24 +90,27 @@ async function findTranscript(
44
90
  const view = self.connectedView!
45
91
  const session = getSession(self)
46
92
  const sessionId = 'msaview-connectedTranscript'
47
- const regions = view.displayedRegions.map(r => ({
48
- assemblyName: r.assemblyName,
49
- refName: r.refName,
50
- start: r.start,
51
- end: r.end,
52
- }))
53
- for (const track of view.tracks) {
54
- const feats = (await session.rpcManager.call(sessionId, 'CoreGetFeatures', {
55
- adapterConfig: getConf(track, 'adapter'),
56
- sessionId,
57
- regions,
58
- })) as Feature[]
59
- for (const feat of feats) {
60
- const hit = getTranscriptFeatures(feat).find(t =>
61
- transcriptMatches(t, transcriptId),
93
+ const tracks = transcriptTracks(view.tracks)
94
+ for (const regions of searchWindows(view)) {
95
+ for (const track of tracks) {
96
+ // a named object keeps sessionId, which v4 hosts read from the args
97
+ const args = {
98
+ adapterConfig: getConf(track, 'adapter'),
99
+ sessionId,
100
+ regions,
101
+ }
102
+ const feats = await session.rpcManager.call(
103
+ sessionId,
104
+ 'CoreGetFeatures',
105
+ args,
62
106
  )
63
- if (hit) {
64
- return hit
107
+ for (const feat of feats) {
108
+ const hit = getTranscriptFeatures(feat).find(t =>
109
+ transcriptMatches(t, transcriptId),
110
+ )
111
+ if (hit) {
112
+ return hit
113
+ }
65
114
  }
66
115
  }
67
116
  }
@@ -75,57 +124,59 @@ async function translate(self: JBrowsePluginMsaViewModel, transcript: Feature) {
75
124
  refName: string
76
125
  }
77
126
  const assemblyName = self.connectedView!.assemblyNames[0]!
78
- const seq = await fetchSeq({
127
+ const { seq, assemblyGeneticCodeId } = await fetchSeq({
79
128
  start,
80
129
  end,
81
130
  refName,
82
131
  assemblyName,
83
132
  session: getSession(self),
84
133
  })
85
- return getProteinSequenceFromFeature({ seq, feature: transcript })
134
+ return getProteinSequenceFromFeature({
135
+ seq,
136
+ feature: transcript,
137
+ assemblyGeneticCodeId,
138
+ })
86
139
  }
87
140
 
88
- /**
89
- * Resolve `connectedTranscript` into `connectedFeature` once the connected
90
- * genome view has its assembly, regions and tracks, then hand the translation
91
- * to whichever launch is waiting for a query. Left in place on failure so the
92
- * error stays attributable; the autorun's tracked reads are the view's
93
- * readiness, so nothing refires until that changes.
94
- */
95
- export function resolveConnectedTranscriptIfNeeded(
96
- self: JBrowsePluginMsaViewModel,
97
- ) {
98
- const { connectedTranscript, connectedFeature, connectedView } = self
99
- if (
100
- !connectedTranscript ||
101
- connectedFeature ||
102
- !connectedView?.initialized ||
103
- connectedView.tracks.length === 0
104
- ) {
105
- return
106
- }
107
- const trackCount = connectedView.tracks.length
108
- void (async () => {
109
- try {
110
- self.setProgress(
111
- `Looking up ${connectedTranscript} in the genome view...`,
141
+ /** the latest lookup per view; an older one finishing late writes nothing */
142
+ const latestLookup = new WeakMap<object, number>()
143
+
144
+ async function lookUp({
145
+ self,
146
+ transcriptId,
147
+ blastParams,
148
+ orthologParams,
149
+ isCurrent,
150
+ }: {
151
+ self: JBrowsePluginMsaViewModel
152
+ transcriptId: string
153
+ blastParams: BlastParams | undefined
154
+ orthologParams: OrthologParams | undefined
155
+ isCurrent: () => boolean
156
+ }) {
157
+ const trackCount = transcriptTracks(self.connectedView!.tracks).length
158
+ try {
159
+ self.setProgress(`Looking up ${transcriptId} in the genome view...`)
160
+ const transcript = await findTranscript(self, transcriptId)
161
+ if (!transcript) {
162
+ throw new Error(
163
+ `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.`,
112
164
  )
113
- const transcript = await findTranscript(self, connectedTranscript)
114
- if (!transcript) {
115
- throw new Error(
116
- `Transcript "${connectedTranscript}" was not found in the ${trackCount} track(s) open in the connected genome view. Open the gene track that carries it, and make sure the view is on its locus.`,
117
- )
118
- }
119
- const proteinSequence = await translate(self, transcript)
120
- if (!proteinSequence) {
121
- throw new Error(
122
- `Transcript "${connectedTranscript}" has no CDS to translate`,
123
- )
124
- }
125
- // toJSON, not the Feature: the params are frozen snapshot properties, so
126
- // an instance in one is gone the moment the session reloads
127
- const transcriptJson = transcript.toJSON()
128
- const { blastParams, orthologParams } = self
165
+ }
166
+ const proteinSequence = await translate(self, transcript)
167
+ if (!proteinSequence) {
168
+ throw new Error(`Transcript "${transcriptId}" has no CDS to translate`)
169
+ }
170
+ if (!isCurrent()) {
171
+ return
172
+ }
173
+ // toJSON, not the Feature: the params are frozen snapshot properties, so
174
+ // an instance in one is gone the moment the session reloads
175
+ const transcriptJson = transcript.toJSON()
176
+ // one transaction, so the launchers wake once, on a request that already
177
+ // carries its query
178
+ transaction(() => {
179
+ self.setProgress('')
129
180
  if (blastParams && !blastParams.proteinSequence) {
130
181
  self.setBlastParams({
131
182
  ...blastParams,
@@ -140,11 +191,53 @@ export function resolveConnectedTranscriptIfNeeded(
140
191
  proteinSequence,
141
192
  })
142
193
  }
143
- // last, because the launchers wake on it
144
194
  self.setConnectedFeature(transcriptJson)
145
- } catch (e) {
146
- console.error(e)
195
+ })
196
+ } catch (e) {
197
+ console.error(e)
198
+ if (isCurrent()) {
199
+ self.setProgress('')
147
200
  self.setError(e)
148
201
  }
149
- })()
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Resolve `connectedTranscript` into `connectedFeature` once the connected
207
+ * genome view has its assembly and tracks, then hand the translation to
208
+ * whichever launch is waiting for a query.
209
+ *
210
+ * The autorun re-fires on the view's readiness (a track opened after a miss)
211
+ * and on the request itself, which is what `retryLaunch` re-states. The lookup
212
+ * runs untracked, so scrolling the genome view is not a reason to look again.
213
+ */
214
+ export function resolveConnectedTranscriptIfNeeded(
215
+ self: JBrowsePluginMsaViewModel,
216
+ ) {
217
+ const {
218
+ connectedTranscript,
219
+ connectedFeature,
220
+ connectedView,
221
+ blastParams,
222
+ orthologParams,
223
+ } = self
224
+ if (
225
+ !connectedTranscript ||
226
+ connectedFeature ||
227
+ !connectedView?.initialized ||
228
+ connectedView.tracks.length === 0
229
+ ) {
230
+ return
231
+ }
232
+ const lookup = (latestLookup.get(self) ?? 0) + 1
233
+ latestLookup.set(self, lookup)
234
+ untracked(() => {
235
+ void lookUp({
236
+ self,
237
+ transcriptId: connectedTranscript,
238
+ blastParams,
239
+ orthologParams,
240
+ isCurrent: () => isAlive(self) && latestLookup.get(self) === lookup,
241
+ })
242
+ })
150
243
  }
@@ -137,6 +137,25 @@ describe('a launch that is abandoned', () => {
137
137
  })
138
138
  })
139
139
 
140
+ describe('a launch started while another holds the controller', () => {
141
+ test('aborts the first, so nothing is left polling that Cancel cannot reach', async () => {
142
+ const { model } = makeModel()
143
+ const signals: AbortSignal[] = []
144
+ const launch = (scope: LaunchScope) => {
145
+ signals.push(scope.signal)
146
+ return new Promise<typeof DATA>(() => {})
147
+ }
148
+
149
+ runLaunch({ self: model, message: 'a', onLaunched: () => {}, launch })
150
+ runLaunch({ self: model, message: 'b', onLaunched: () => {}, launch })
151
+ await settle()
152
+
153
+ expect(signals.map(s => s.aborted)).toEqual([true, false])
154
+ model.launchController!.abort()
155
+ expect(signals[1]!.aborted).toBe(true)
156
+ })
157
+ })
158
+
140
159
  describe('a launch that fails', () => {
141
160
  test('records the error and stops the spinner', async () => {
142
161
  vi.spyOn(console, 'error').mockImplementation(() => {})
@@ -1,4 +1,5 @@
1
1
  import { getSession } from '@jbrowse/core/util'
2
+ import { untracked } from 'mobx'
2
3
 
3
4
  import { isAbortError } from '../utils/fetch'
4
5
 
@@ -64,11 +65,26 @@ interface LaunchedData {
64
65
  * Run one launch attempt, owning the AbortController that ties it to the view.
65
66
  *
66
67
  * The controller lives on the model because two things end a launch and neither
67
- * is here: the Cancel button, and the disposer `afterCreate` registers. Before
68
- * it existed, closing a view mid-BLAST left the poller checking EBI for the
69
- * job's lifetime and then writing to a node that was gone.
68
+ * is here: the Cancel button, and the disposer `afterCreate` registers. A launch
69
+ * already holding it is aborted first, so a re-fired autorun replaces the
70
+ * attempt rather than orphaning one that nothing can cancel.
71
+ *
72
+ * Runs untracked: it is called from the launch autoruns, and whatever the
73
+ * launch body reads before its first await would otherwise become a reason to
74
+ * launch again.
70
75
  */
71
- export function runLaunch({
76
+ export function runLaunch(args: {
77
+ self: JBrowsePluginMsaViewModel
78
+ message: string
79
+ launch: (scope: LaunchScope) => Promise<LaunchedData>
80
+ onLaunched: () => void
81
+ }) {
82
+ untracked(() => {
83
+ startLaunch(args)
84
+ })
85
+ }
86
+
87
+ function startLaunch({
72
88
  self,
73
89
  message,
74
90
  launch,
@@ -79,6 +95,7 @@ export function runLaunch({
79
95
  launch: (scope: LaunchScope) => Promise<LaunchedData>
80
96
  onLaunched: () => void
81
97
  }) {
98
+ self.launchController?.abort()
82
99
  const controller = new AbortController()
83
100
  const { signal } = controller
84
101
  self.setLaunchController(controller)
@@ -61,6 +61,9 @@ function makeModel(over: Record<string, unknown> = {}) {
61
61
  setTreeMetadata(arg: string) {
62
62
  model.data.treeMetadata = arg
63
63
  },
64
+ setGFF(arg: string) {
65
+ model.data.gff = arg
66
+ },
64
67
  ...over,
65
68
  }
66
69
  return model as typeof model & JBrowsePluginMsaViewModel
@@ -100,6 +103,17 @@ describe('restoring a view from IndexedDB', () => {
100
103
  expect(mockStore).not.toHaveBeenCalled()
101
104
  })
102
105
 
106
+ // react-msaview drops a GFF over 50kB from the snapshot too
107
+ test('a stored GFF comes back with the alignment', async () => {
108
+ mockRetrieve.mockResolvedValue({ msa: MSA, gff: 'a\t.\tdomain\t1\t2' })
109
+ const model = makeModel({ dataStoreId: 'msa-1' })
110
+
111
+ loadStoredData(model)
112
+ await settle()
113
+
114
+ expect(model.data.gff).toBe('a\t.\tdomain\t1\t2')
115
+ })
116
+
103
117
  // the row expires or the user clears site data, and the view used to reopen as
104
118
  // a blank import form with no hint that anything had been lost
105
119
  test('a row that is gone says so, in words that name the policy', async () => {
@@ -115,6 +129,32 @@ describe('restoring a view from IndexedDB', () => {
115
129
  // to import form" lands straight back on this error
116
130
  expect(model.dataStoreId).toBeUndefined()
117
131
  })
132
+
133
+ // the request that built the alignment is kept past the launch, so the view
134
+ // can offer to run it again instead of sending the user back to the gene
135
+ test('a row that is gone points at Retry when the search is still known', async () => {
136
+ mockRetrieve.mockResolvedValue(undefined)
137
+ const model = makeModel({
138
+ dataStoreId: 'msa-1',
139
+ blastParams: { blastDatabase: 'uniprotkb_swissprot' },
140
+ })
141
+
142
+ loadStoredData(model)
143
+ await settle()
144
+
145
+ expect((model.error as Error).message).toMatch(/Retry/)
146
+ expect(model.blastParams).toBeDefined()
147
+ })
148
+
149
+ test('a row that is gone with no search behind it says to relaunch', async () => {
150
+ mockRetrieve.mockResolvedValue(undefined)
151
+ const model = makeModel({ dataStoreId: 'msa-1' })
152
+
153
+ loadStoredData(model)
154
+ await settle()
155
+
156
+ expect((model.error as Error).message).toMatch(/Relaunch it from the gene/)
157
+ })
118
158
  })
119
159
 
120
160
  describe('keeping IndexedDB up to date', () => {
@@ -155,6 +195,24 @@ describe('keeping IndexedDB up to date', () => {
155
195
  expect(model.dataStoreId).toBe('msa-1')
156
196
  })
157
197
 
198
+ test('a GFF loaded after the first write updates the row', async () => {
199
+ const model = makeModel({
200
+ dataStoreId: 'msa-1',
201
+ rows: [['a', 'MK']],
202
+ data: { msa: MSA },
203
+ lastStoredData: { msa: MSA },
204
+ })
205
+
206
+ model.data.gff = 'a\t.\tdomain\t1\t2'
207
+ storeDataToIndexedDB(model)
208
+ await settle()
209
+
210
+ expect(mockStore).toHaveBeenCalledWith(
211
+ 'msa-1',
212
+ expect.objectContaining({ gff: 'a\t.\tdomain\t1\t2' }),
213
+ )
214
+ })
215
+
158
216
  // a browser that refuses IndexedDB (private mode) answers every write the same
159
217
  // way, and the autorun reruns on its own isStoringData flag -- so a write that
160
218
  // is not recorded as attempted is a write that retries forever
@@ -22,17 +22,18 @@ const mafRegion = {
22
22
  }
23
23
 
24
24
  // A model wired through the real genomeToMSA path: a connected genome view
25
- // over a maf region, with seqPosToVisibleCol as identity so the asserted
25
+ // over a maf region, with the column converters as identity so the asserted
26
26
  // column equals the ungapped offset into the region.
27
27
  function makeModel() {
28
28
  const calls: (number | undefined)[] = []
29
29
  const model = {
30
30
  querySeqName: 'hg38.chr1',
31
- rows: [['hg38.chr1', 'ACGTACGTAC']],
31
+ rowMap: new Map([['hg38.chr1', 'ACGTACGTAC']]),
32
32
  transcriptToMsaMap: undefined,
33
33
  mafRegion,
34
34
  connectedView: { initialized: true, assemblyNames: ['hg38'] },
35
35
  seqPosToVisibleCol: (_name: string, pos: number) => pos,
36
+ visibleColToSeqPos: (_name: string, col: number) => col,
36
37
  setMousePos: (col?: number) => {
37
38
  calls.push(col)
38
39
  },
@@ -40,16 +41,25 @@ function makeModel() {
40
41
  return { model, calls }
41
42
  }
42
43
 
43
- function hoverGenome(coord: number) {
44
+ // this config spells the refName the same way on both sides, so the assembly
45
+ // hands every name straight back
46
+ const assemblyManager = {
47
+ get: () => ({ getCanonicalRefName: (refName: string) => refName }),
48
+ }
49
+
50
+ function mockSession(hovered: unknown) {
44
51
  mockGetSession.mockReturnValue({
45
- hovered: { hoverFeature: {}, hoverPosition: { coord, refName: 'chr1' } },
52
+ assemblyManager,
53
+ hovered,
46
54
  } as unknown as ReturnType<typeof getSession>)
47
55
  }
48
56
 
57
+ function hoverGenome(coord: number) {
58
+ mockSession({ hoverFeature: {}, hoverPosition: { coord, refName: 'chr1' } })
59
+ }
60
+
49
61
  function clearGenomeHover() {
50
- mockGetSession.mockReturnValue({
51
- hovered: null,
52
- } as unknown as ReturnType<typeof getSession>)
62
+ mockSession(null)
53
63
  }
54
64
 
55
65
  describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
@@ -1,19 +1,17 @@
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
- * react-msaview's `seqPosToGlobalCol` answers 0 for a name it does not know, so
5
- * without this every genome position maps to the first column and hovering the
6
- * genome — or a connected structure — lights column 0 of an unrelated row. The
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
- export function hasQueryRow(model: { rows: string[][]; querySeqName: string }) {
16
- return model.rows.some(r => r[0] === model.querySeqName)
10
+ export function hasQueryRow(model: {
11
+ rowMap: ReadonlyMap<string, string>
12
+ querySeqName: string
13
+ }) {
14
+ return model.rowMap.has(model.querySeqName)
17
15
  }
18
16
 
19
17
  /**
@@ -53,9 +51,8 @@ export function transcriptName(transcript?: TranscriptRef) {
53
51
  return str(fields.name) ?? str(fields.id)
54
52
  }
55
53
 
56
- export interface QueryRowModel {
54
+ interface QueryRowColumns {
57
55
  querySeqName: string
58
- querySeqOffset: number
59
56
  seqPosToVisibleCol: (rowName: string, seqPos: number) => number | undefined
60
57
  visibleColToSeqPos: (
61
58
  rowName: string,
@@ -63,24 +60,24 @@ export interface QueryRowModel {
63
60
  ) => number | undefined
64
61
  }
65
62
 
63
+ export interface QueryRowModel extends QueryRowColumns {
64
+ querySeqOffset: number
65
+ }
66
+
66
67
  /**
67
- * The visible column showing residue `proteinPos` (0-based) of the transcript,
68
- * or undefined when the query row does not carry that residue or react-msaview
69
- * is hiding its column.
68
+ * The visible column showing residue `seqPos` (0-based) of the query row, or
69
+ * undefined when the row does not carry that residue or react-msaview is
70
+ * hiding its column.
70
71
  *
71
- * Two things separate the transcript from the row. `querySeqOffset` is the
72
- * trimming — a pasted BLAST alignment carries the aligned region, not the whole
73
- * protein. And the row can simply stop short: react-msaview answers one column
74
- * past the end for a position it does not have, which would light the last
75
- * column for every residue beyond the row, so the round trip back through
76
- * visibleColToSeqPos is what rejects those.
72
+ * react-msaview answers one column past the end for a position the row does not
73
+ * have, which would light the last column for every residue beyond the row, so
74
+ * the round trip back through visibleColToSeqPos is what rejects those.
77
75
  */
78
- export function transcriptPosToVisibleCol(
79
- model: QueryRowModel,
80
- proteinPos: number,
76
+ export function querySeqPosToVisibleCol(
77
+ model: QueryRowColumns,
78
+ seqPos: number,
81
79
  ) {
82
- const { querySeqName, querySeqOffset } = model
83
- const seqPos = proteinPos - querySeqOffset
80
+ const { querySeqName } = model
84
81
  if (seqPos < 0) {
85
82
  return undefined
86
83
  }
@@ -91,6 +88,18 @@ export function transcriptPosToVisibleCol(
91
88
  : undefined
92
89
  }
93
90
 
91
+ /**
92
+ * The visible column showing residue `proteinPos` (0-based) of the transcript.
93
+ * `querySeqOffset` is what separates the two: a pasted BLAST alignment carries
94
+ * the aligned region, not the whole protein.
95
+ */
96
+ export function transcriptPosToVisibleCol(
97
+ model: QueryRowModel,
98
+ proteinPos: number,
99
+ ) {
100
+ return querySeqPosToVisibleCol(model, proteinPos - model.querySeqOffset)
101
+ }
102
+
94
103
  export function hasHoverPosition(
95
104
  hovered: unknown,
96
105
  ): hovered is { hoverPosition: { coord: number; refName: string } } {
package/src/index.ts CHANGED
@@ -28,16 +28,25 @@ export default class MsaViewPlugin extends Plugin {
28
28
  }
29
29
 
30
30
  configure(pluginManager: PluginManager) {
31
- if (isAbstractMenuManager(pluginManager.rootModel)) {
32
- pluginManager.rootModel.appendToSubMenu(['Add'], {
33
- label: 'Multiple sequence alignment view',
34
- icon: GridOn,
35
- onClick: (session: AbstractSessionModel) => {
36
- // stacked, by default: nothing was launched from, so there is no
37
- // connected view for it to sit beside
38
- launchMsaView(session, {})
39
- },
40
- })
31
+ // a throw here error-pages the whole app, and a menu entry is not worth a
32
+ // session: every released host's File menu already throws on append
33
+ try {
34
+ if (isAbstractMenuManager(pluginManager.rootModel)) {
35
+ pluginManager.rootModel.appendToSubMenu(['Add'], {
36
+ label: 'Multiple sequence alignment view',
37
+ icon: GridOn,
38
+ onClick: (session: AbstractSessionModel) => {
39
+ // stacked, by default: nothing was launched from, so there is no
40
+ // connected view for it to sit beside
41
+ launchMsaView(session, {})
42
+ },
43
+ })
44
+ }
45
+ } catch (e) {
46
+ console.error(
47
+ 'jbrowse-plugin-msaview: could not add the Add menu item',
48
+ e,
49
+ )
41
50
  }
42
51
  }
43
52