jbrowse-plugin-msaview 3.4.1 → 3.6.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 (222) hide show
  1. package/README.md +7 -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/codingFeature.d.ts +14 -0
  7. package/dist/LaunchMsaView/codingFeature.js +38 -0
  8. package/dist/LaunchMsaView/codingFeature.test.js +75 -0
  9. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.d.ts +3 -1
  10. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -26
  11. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +3 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +7 -2
  13. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.d.ts +7 -3
  14. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +8 -7
  15. package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.js +2 -2
  16. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
  17. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +26 -4
  18. package/dist/LaunchMsaView/components/BlastQuery/consts.js +68 -2
  19. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +1 -1
  20. package/dist/LaunchMsaView/components/HelpButton.d.ts +2 -0
  21. package/dist/LaunchMsaView/components/HelpButton.js +17 -0
  22. package/dist/LaunchMsaView/components/HelpDialog.d.ts +4 -0
  23. package/dist/LaunchMsaView/components/HelpDialog.js +18 -0
  24. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.d.ts +3 -1
  25. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +21 -16
  26. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.d.ts +3 -1
  27. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +10 -4
  28. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
  29. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
  30. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +3 -1
  31. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +21 -7
  32. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
  33. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.d.ts +3 -1
  34. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +33 -15
  35. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +3 -1
  36. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +2 -1
  37. package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
  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 +3 -1
  41. package/dist/LaunchMsaView/components/SubmitCancelActions.js +25 -13
  42. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +33 -3
  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 +4 -1
  49. package/dist/LaunchMsaView/components/calculateProteinSequence.js +20 -10
  50. package/dist/LaunchMsaView/components/fetchSeq.d.ts +4 -1
  51. package/dist/LaunchMsaView/components/fetchSeq.js +14 -4
  52. package/dist/LaunchMsaView/components/launchPlacement.d.ts +8 -0
  53. package/dist/LaunchMsaView/components/launchPlacement.js +25 -0
  54. package/dist/LaunchMsaView/components/useFeatureSequence.d.ts +2 -0
  55. package/dist/LaunchMsaView/components/useFeatureSequence.js +14 -11
  56. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +10 -1
  57. package/dist/LaunchMsaView/components/useTranscriptSelection.js +21 -3
  58. package/dist/LaunchMsaView/components/useTranscriptSelection.test.d.ts +1 -0
  59. package/dist/LaunchMsaView/components/useTranscriptSelection.test.js +57 -0
  60. package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
  61. package/dist/LaunchMsaView/detectQueryRow.js +11 -4
  62. package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
  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 +13 -0
  68. package/dist/LaunchMsaView/useQueryRowName.js +16 -1
  69. package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
  70. package/dist/LaunchMsaView/useQueryRowName.test.js +40 -0
  71. package/dist/LaunchMsaView/util.js +5 -2
  72. package/dist/LaunchMsaViewExtensionPoint/index.js +8 -6
  73. package/dist/LaunchMsaViewExtensionPoint/index.test.js +21 -0
  74. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +9 -6
  75. package/dist/MsaViewPanel/afterCreateAutoruns.js +72 -28
  76. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +4 -3
  77. package/dist/MsaViewPanel/components/LaunchProgress.js +22 -10
  78. package/dist/MsaViewPanel/components/MsaViewPanel.js +10 -2
  79. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +47 -2
  80. package/dist/MsaViewPanel/doLaunchBlast.d.ts +8 -0
  81. package/dist/MsaViewPanel/doLaunchBlast.js +78 -93
  82. package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
  83. package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
  84. package/dist/MsaViewPanel/doLaunchOrthologs.js +40 -5
  85. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +10 -0
  86. package/dist/MsaViewPanel/genomeToMSA.js +25 -7
  87. package/dist/MsaViewPanel/genomeToMSA.test.js +214 -19
  88. package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
  89. package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
  90. package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
  91. package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
  92. package/dist/MsaViewPanel/model.d.ts +265 -103
  93. package/dist/MsaViewPanel/model.js +174 -48
  94. package/dist/MsaViewPanel/model.test.d.ts +1 -0
  95. package/dist/MsaViewPanel/model.test.js +166 -0
  96. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
  97. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
  98. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
  99. package/dist/MsaViewPanel/msaDataStore.d.ts +2 -0
  100. package/dist/MsaViewPanel/msaDataStore.js +10 -0
  101. package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
  102. package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
  103. package/dist/MsaViewPanel/processInit.test.js +72 -0
  104. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
  105. package/dist/MsaViewPanel/resolveConnectedTranscript.js +128 -0
  106. package/dist/MsaViewPanel/runLaunch.d.ts +1 -0
  107. package/dist/MsaViewPanel/runLaunch.js +35 -0
  108. package/dist/MsaViewPanel/runLaunch.test.js +46 -0
  109. package/dist/MsaViewPanel/storedData.test.js +20 -0
  110. package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
  111. package/dist/MsaViewPanel/structureConnection.js +0 -19
  112. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +12 -5
  113. package/dist/MsaViewPanel/util.d.ts +39 -0
  114. package/dist/MsaViewPanel/util.js +44 -0
  115. package/dist/index.d.ts +11 -11
  116. package/dist/jbrowse-plugin-msaview.umd.production.min.js +56 -68
  117. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  118. package/dist/utils/browserAlign.d.ts +42 -0
  119. package/dist/utils/browserAlign.js +297 -0
  120. package/dist/utils/browserAlign.test.d.ts +1 -0
  121. package/dist/utils/browserAlign.test.js +85 -0
  122. package/dist/utils/ebiBlast.d.ts +10 -1
  123. package/dist/utils/ebiBlast.js +27 -1
  124. package/dist/utils/homologSearch.d.ts +31 -0
  125. package/dist/utils/homologSearch.js +6 -0
  126. package/dist/utils/msa.d.ts +3 -11
  127. package/dist/utils/msa.js +14 -29
  128. package/dist/utils/msaRows.d.ts +11 -8
  129. package/dist/utils/msaRows.js +14 -12
  130. package/dist/utils/phmmer.d.ts +11 -1
  131. package/dist/utils/phmmer.js +49 -12
  132. package/dist/utils/unirefHomologs.d.ts +94 -0
  133. package/dist/utils/unirefHomologs.js +193 -0
  134. package/dist/utils/unirefHomologs.test.d.ts +1 -0
  135. package/dist/utils/unirefHomologs.test.js +118 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/package.json +23 -22
  139. package/src/AddHighlightModel/index.tsx +1 -1
  140. package/src/BgzipFastaMsaAdapter/configSchema.ts +1 -1
  141. package/src/LaunchMsaView/codingFeature.test.ts +96 -0
  142. package/src/LaunchMsaView/codingFeature.ts +49 -0
  143. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +52 -26
  144. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +9 -1
  145. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +24 -11
  146. package/src/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.tsx +4 -4
  147. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
  148. package/src/LaunchMsaView/components/BlastQuery/consts.ts +77 -2
  149. package/src/LaunchMsaView/components/HelpButton.tsx +33 -0
  150. package/src/LaunchMsaView/components/HelpDialog.tsx +78 -0
  151. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +54 -34
  152. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +13 -2
  153. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
  154. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +28 -5
  155. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
  156. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +63 -13
  157. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +4 -0
  158. package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
  159. package/src/LaunchMsaView/components/SequenceStatus.tsx +29 -0
  160. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +62 -3
  161. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +37 -15
  162. package/src/LaunchMsaView/components/TabPanel.test.tsx +51 -0
  163. package/src/LaunchMsaView/components/TabPanel.tsx +16 -4
  164. package/src/LaunchMsaView/components/TranscriptSelector.tsx +6 -1
  165. package/src/LaunchMsaView/components/calculateProteinSequence.ts +29 -10
  166. package/src/LaunchMsaView/components/fetchSeq.ts +25 -5
  167. package/src/LaunchMsaView/components/launchPlacement.tsx +41 -0
  168. package/src/LaunchMsaView/components/useFeatureSequence.ts +18 -11
  169. package/src/LaunchMsaView/components/useTranscriptSelection.test.tsx +66 -0
  170. package/src/LaunchMsaView/components/useTranscriptSelection.ts +41 -2
  171. package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
  172. package/src/LaunchMsaView/detectQueryRow.ts +22 -4
  173. package/src/LaunchMsaView/index.ts +32 -13
  174. package/src/LaunchMsaView/launchTarget.test.ts +88 -15
  175. package/src/LaunchMsaView/launchTarget.ts +49 -14
  176. package/src/LaunchMsaView/useQueryRowName.test.ts +54 -0
  177. package/src/LaunchMsaView/useQueryRowName.ts +22 -1
  178. package/src/LaunchMsaView/util.ts +8 -4
  179. package/src/LaunchMsaViewExtensionPoint/index.test.ts +23 -0
  180. package/src/LaunchMsaViewExtensionPoint/index.ts +32 -6
  181. package/src/MsaViewPanel/afterCreateAutoruns.ts +85 -27
  182. package/src/MsaViewPanel/components/LaunchProgress.tsx +38 -7
  183. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +50 -2
  184. package/src/MsaViewPanel/components/MsaViewPanel.tsx +10 -2
  185. package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
  186. package/src/MsaViewPanel/doLaunchBlast.ts +92 -142
  187. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +13 -0
  188. package/src/MsaViewPanel/doLaunchOrthologs.ts +55 -5
  189. package/src/MsaViewPanel/genomeToMSA.test.ts +251 -31
  190. package/src/MsaViewPanel/genomeToMSA.ts +35 -7
  191. package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
  192. package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
  193. package/src/MsaViewPanel/model.test.ts +196 -0
  194. package/src/MsaViewPanel/model.ts +204 -55
  195. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
  196. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
  197. package/src/MsaViewPanel/msaDataStore.ts +10 -0
  198. package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
  199. package/src/MsaViewPanel/processInit.test.ts +84 -0
  200. package/src/MsaViewPanel/resolveConnectedTranscript.ts +160 -0
  201. package/src/MsaViewPanel/runLaunch.test.ts +54 -0
  202. package/src/MsaViewPanel/runLaunch.ts +39 -0
  203. package/src/MsaViewPanel/storedData.test.ts +26 -0
  204. package/src/MsaViewPanel/structureConnection.ts +0 -26
  205. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +14 -5
  206. package/src/MsaViewPanel/util.ts +75 -0
  207. package/src/utils/browserAlign.test.ts +102 -0
  208. package/src/utils/browserAlign.ts +352 -0
  209. package/src/utils/ebiBlast.ts +34 -0
  210. package/src/utils/homologSearch.ts +51 -0
  211. package/src/utils/msa.ts +19 -41
  212. package/src/utils/msaRows.ts +20 -16
  213. package/src/utils/phmmer.ts +57 -11
  214. package/src/utils/unirefHomologs.test.ts +149 -0
  215. package/src/utils/unirefHomologs.ts +323 -0
  216. package/src/version.ts +1 -1
  217. package/dist/LaunchMsaView/components/geneticCodes.d.ts +0 -15
  218. package/dist/LaunchMsaView/components/geneticCodes.js +0 -227
  219. package/dist/MsaViewPanel/structureConnection.test.js +0 -53
  220. package/src/LaunchMsaView/components/geneticCodes.ts +0 -298
  221. package/src/MsaViewPanel/structureConnection.test.ts +0 -62
  222. /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/codingFeature.test.d.ts} +0 -0
@@ -4,9 +4,10 @@ import type { JBrowsePluginMsaViewModel } from '../model';
4
4
  * What a view shows while it is still building its alignment, and what it shows
5
5
  * when that fails.
6
6
  *
7
- * Every launch that resolves something leaves its request on the model until it
8
- * succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
9
- * there IS "no alignment yet", and the error a failed launch records is only
7
+ * Every launch that resolves something states its request on the model --
8
+ * `blastParams`, `orthologParams`, `init` -- and one not yet marked
9
+ * `launchCompleted` IS "no alignment yet", as is a completed one whose stored
10
+ * alignment has since expired. The error a failed launch records is only
10
11
  * readable here. This used to key on `blastParams` alone, which left an ortholog
11
12
  * launch rendering an empty MSAView for the minutes its alignment takes and, on
12
13
  * failure, forever: the error was set and nothing drew it.
@@ -18,27 +18,39 @@ const useStyles = makeStyles()({
18
18
  * What a view shows while it is still building its alignment, and what it shows
19
19
  * when that fails.
20
20
  *
21
- * Every launch that resolves something leaves its request on the model until it
22
- * succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
23
- * there IS "no alignment yet", and the error a failed launch records is only
21
+ * Every launch that resolves something states its request on the model --
22
+ * `blastParams`, `orthologParams`, `init` -- and one not yet marked
23
+ * `launchCompleted` IS "no alignment yet", as is a completed one whose stored
24
+ * alignment has since expired. The error a failed launch records is only
24
25
  * readable here. This used to key on `blastParams` alone, which left an ortholog
25
26
  * launch rendering an empty MSAView for the minutes its alignment takes and, on
26
27
  * failure, forever: the error was set and nothing drew it.
27
28
  */
28
29
  const LaunchProgress = observer(function LaunchProgress2({ model, }) {
29
- const { blastParams, orthologParams, progress, rid, error } = model;
30
+ const { progress, rid, error } = model;
30
31
  const { classes } = useStyles();
32
+ // the request stays on the model after it succeeds, so an expired
33
+ // alignment's panel is still named after the search that built it
34
+ const pending = !model.launchCompleted;
35
+ const { blastParams, orthologParams } = model;
31
36
  const message = blastParams
32
- ? 'Running EBI BLAST'
37
+ ? `Running EBI ${blastParams.searchProgram === 'phmmer' ? 'phmmer' : 'BLAST'}`
33
38
  : orthologParams
34
- ? 'Building ortholog alignment'
39
+ ? orthologParams.source === 'uniref'
40
+ ? 'Building UniRef homolog alignment'
41
+ : 'Building ortholog alignment'
35
42
  : 'Loading alignment';
36
43
  return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
37
- React.createElement(Typography, { variant: "h5" },
38
- message,
39
- " failed"),
44
+ React.createElement(Typography, { variant: "h5" }, pending ? `${message} failed` : 'Alignment no longer available'),
40
45
  rid ? React.createElement(JobLink, { jobId: rid }) : null,
41
- React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
46
+ React.createElement(ErrorMessage, { error: error }),
47
+ React.createElement("div", { className: classes.progressRow },
48
+ React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
49
+ model.retryLaunch();
50
+ } }, "Retry"),
51
+ React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
52
+ model.cancelLaunch();
53
+ } }, "Dismiss")))) : (React.createElement(React.Fragment, null,
42
54
  React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
43
55
  rid ? React.createElement(JobLink, { jobId: rid }) : null,
44
56
  React.createElement("div", { className: classes.progressRow },
@@ -14,8 +14,16 @@ 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
- const launching = !!(blastParams ?? orthologParams ?? init);
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 request = blastParams ?? orthologParams;
21
+ const pending = !!request && !model.launchCompleted;
22
+ // a request that finished and then lost its alignment -- browser storage
23
+ // expires one after 7 days -- is a request again, and LaunchProgress draws it
24
+ // with the Retry that runs it
25
+ const expired = !!request && !!model.error && !model.dataInitialized;
26
+ const launching = pending || expired || (!!init && !model.dataInitialized);
19
27
  return (React.createElement(ErrorBoundary, null,
20
28
  React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
21
29
  React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
@@ -48,6 +48,16 @@ test('a failed ortholog launch shows why', () => {
48
48
  });
49
49
  expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
50
50
  });
51
+ // an indexed view keeps its init -- it is how the block is refetched on the
52
+ // next session -- so the panel cannot read init alone as "still launching"
53
+ test('an indexed view with its alignment draws it, init and all', () => {
54
+ panel({
55
+ init: { msaIndexedLocation: { uri: 'msa.fa.gz' }, msaName: 'ENST1' },
56
+ dataInitialized: true,
57
+ progress: '',
58
+ });
59
+ expect(screen.getByText('the alignment')).toBeTruthy();
60
+ });
51
61
  test('a failed init shows why', () => {
52
62
  panel({
53
63
  init: { msaName: 'ENST00000288602' },
@@ -58,7 +68,7 @@ test('a failed init shows why', () => {
58
68
  expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
59
69
  });
60
70
  // a launch runs for 10+ minutes, so leaving with no way out means watching it
61
- test('a running launch offers a way out, and a failed one does not', () => {
71
+ test('a running launch offers a way out', () => {
62
72
  const cancelLaunch = vi.fn();
63
73
  panel({
64
74
  blastParams: { proteinSequence: 'MKV' },
@@ -67,14 +77,49 @@ test('a running launch offers a way out, and a failed one does not', () => {
67
77
  });
68
78
  screen.getByRole('button', { name: 'Cancel' }).click();
69
79
  expect(cancelLaunch).toHaveBeenCalled();
70
- cleanup();
80
+ });
81
+ // the request outlives the failure -- it is what a reload resubmits -- so a
82
+ // failed launch that only drew the error was a dead end that re-ran the EBI job
83
+ // on every reload
84
+ test('a failed launch can be retried or dismissed', () => {
85
+ const cancelLaunch = vi.fn();
86
+ const retryLaunch = vi.fn();
71
87
  panel({
72
88
  blastParams: { proteinSequence: 'MKV' },
73
89
  progress: '',
74
90
  error: new Error('No hits found'),
75
91
  cancelLaunch,
92
+ retryLaunch,
76
93
  });
77
94
  expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
95
+ screen.getByRole('button', { name: 'Retry' }).click();
96
+ expect(retryLaunch).toHaveBeenCalled();
97
+ screen.getByRole('button', { name: 'Dismiss' }).click();
98
+ expect(cancelLaunch).toHaveBeenCalled();
99
+ });
100
+ // a stored alignment expires after 7 days, and the view used to say so and
101
+ // stop there -- the request that built it had been dropped on success, so the
102
+ // only way back was to find the gene again
103
+ test('an expired alignment offers to run its original search again', () => {
104
+ const retryLaunch = vi.fn();
105
+ panel({
106
+ progress: '',
107
+ dataInitialized: false,
108
+ error: new Error("This view's alignment is no longer in browser storage."),
109
+ blastParams: { blastDatabase: 'uniprotkb_swissprot' },
110
+ launchCompleted: true,
111
+ retryLaunch,
112
+ });
113
+ expect(screen.queryByText('the alignment')).toBeNull();
114
+ expect(screen.getByText('Alignment no longer available')).toBeTruthy();
115
+ screen.getByRole('button', { name: 'Retry' }).click();
116
+ expect(retryLaunch).toHaveBeenCalled();
117
+ });
118
+ // with nothing to retry from -- a pasted alignment, say -- the panel stays out
119
+ // of the way and react-msaview's own import form is what the user gets
120
+ test('an expired alignment with no request behind it draws the view', () => {
121
+ panel({ progress: '', dataInitialized: false, error: new Error('gone') });
122
+ expect(screen.getByText('the alignment')).toBeTruthy();
78
123
  });
79
124
  test('a running job links out to it', () => {
80
125
  panel({
@@ -1,5 +1,13 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
2
  import type { LaunchScope } from './runLaunch';
3
+ /**
4
+ * A similarity search, then an alignment of what it found. The program is a
5
+ * backend behind one interface (utils/homologSearch.ts); what differs between
6
+ * them is settled by whether the result came back aligned. A program that
7
+ * aligns as it searches (phmmer) hands over the alignment and the tree is
8
+ * built from it in the browser; one that does not (blastp) hands over bare
9
+ * hits and the chosen aligner runs on them.
10
+ */
3
11
  export declare function doLaunchBlast({ self, scope, }: {
4
12
  self: JBrowsePluginMsaViewModel;
5
13
  scope: LaunchScope;
@@ -1,114 +1,99 @@
1
- import { makeId, strip } from '../LaunchMsaView/components/util';
2
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
3
2
  import { saveBlastResult } from '../utils/blastCache';
4
- import { queryEbiBlast } from '../utils/ebiBlast';
5
- import { launchMSA, launchTree } from '../utils/msa';
6
- import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
7
- import { queryPhmmer } from '../utils/phmmer';
3
+ import { searchBackends } from '../utils/homologSearch';
4
+ import { launchMSA } from '../utils/msa';
5
+ import { buildSearchMsa } from '../utils/msaRows';
8
6
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
9
- export async function doLaunchBlast({ self, scope, }) {
10
- // kept whole rather than destructured: the database's type depends on
11
- // searchProgram, and pulling the two apart loses the link between them
12
- const params = self.blastParams;
13
- const { selectedTranscript } = params;
14
- const cleanedSeq = cleanProteinSequence(params.proteinSequence);
15
- const { onProgress, onRid, signal } = scope;
16
- const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
17
- ? await runPhmmer({
18
- query: cleanedSeq,
19
- database: params.blastDatabase,
20
- onProgress,
21
- onRid,
22
- signal,
23
- })
24
- : await runBlast({
25
- query: cleanedSeq,
26
- blastDatabase: params.blastDatabase,
27
- msaAlgorithm: params.msaAlgorithm,
28
- onProgress,
29
- onRid,
30
- signal,
31
- });
32
- const treeMetadataJson = JSON.stringify(treeMetadata);
33
- await saveBlastResult({
34
- proteinSequence: cleanedSeq,
35
- blastDatabase: params.blastDatabase,
36
- msaAlgorithm: params.msaAlgorithm,
37
- searchProgram: params.searchProgram,
38
- msa,
39
- tree,
40
- treeMetadata: treeMetadataJson,
41
- rid,
42
- geneId: selectedTranscript?.get('parentId'),
43
- transcriptId: selectedTranscript?.id(),
44
- transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
45
- geneName: selectedTranscript?.get('gene_name') ??
46
- selectedTranscript?.get('parentId'),
47
- });
48
- return { msa, tree, treeMetadata: treeMetadataJson };
7
+ import { resolveUniProtEntry } from '../utils/unirefHomologs';
8
+ import { transcriptFields, transcriptName } from './util';
9
+ function asString(val) {
10
+ return typeof val === 'string' ? val : undefined;
49
11
  }
50
12
  /**
51
- * BLAST returns each hit already aligned to the query, but pairwise and one hit
52
- * at a time, so the alignments are stripped back off and every hit is realigned
53
- * together by a dedicated aligner.
13
+ * The query sequence, and what its row is called. The dialog hands over the
14
+ * translated transcript and the row stays `QUERY`; a spec naming a UniProt
15
+ * accession has the sequence fetched and the row named after the entry
16
+ * (`P53_HUMAN_query`), since a search of swissprot returns the entry itself
17
+ * as a hit and two rows called the same thing collapse into one.
54
18
  */
55
- async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, signal, }) {
56
- const { hits, rid } = await queryEbiBlast({
57
- query,
58
- blastDatabase,
59
- onProgress,
60
- onRid,
61
- signal,
62
- });
63
- onProgress('Fetching species taxonomy info...');
64
- const taxonomyInfo = await fetchTaxonomyInfo(hits
65
- .map(h => h.description[0]?.taxid)
66
- .filter((t) => t !== undefined));
67
- const treeMetadata = {};
68
- const sequences = hits.map(h => {
69
- const desc = h.description[0] ?? {
70
- accession: 'unknown',
71
- id: 'unknown',
72
- sciname: 'unknown',
19
+ async function resolveQuery(self, scope) {
20
+ const params = self.blastParams;
21
+ if (params.proteinSequence) {
22
+ return {
23
+ sequence: cleanProteinSequence(params.proteinSequence),
24
+ name: self.querySeqName,
73
25
  };
74
- const rowName = makeId(desc, taxonomyInfo);
75
- treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo);
76
- return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`;
77
- });
78
- const result = await launchMSA({
79
- algorithm: msaAlgorithm,
80
- sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
81
- onProgress,
82
- signal,
83
- });
84
- return { ...result, treeMetadata, rid };
26
+ }
27
+ if (params.accession) {
28
+ scope.onProgress(`Fetching ${params.accession} from UniProt...`);
29
+ const entry = await resolveUniProtEntry([params.accession], 0, scope.signal);
30
+ if (!entry) {
31
+ throw new Error(`UniProt has no entry ${params.accession}`);
32
+ }
33
+ const name = `${entry.id}_query`;
34
+ scope.act(() => {
35
+ self.setQuerySeqName(name);
36
+ });
37
+ return { sequence: entry.sequence, name };
38
+ }
39
+ throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
85
40
  }
86
41
  /**
87
- * phmmer aligns every hit to a profile of the query as it searches, so its own
88
- * output is the MSA and there is no realignment step — the hits keep the
89
- * placement HMMER gave them, and the query row is derived from the alignment's
90
- * match columns rather than being aligned back in afterwards. That leaves no
91
- * aligner run to take a tree from, so the tree is built from this alignment.
42
+ * A similarity search, then an alignment of what it found. The program is a
43
+ * backend behind one interface (utils/homologSearch.ts); what differs between
44
+ * them is settled by whether the result came back aligned. A program that
45
+ * aligns as it searches (phmmer) hands over the alignment and the tree is
46
+ * built from it in the browser; one that does not (blastp) hands over bare
47
+ * hits and the chosen aligner runs on them.
92
48
  */
93
- async function runPhmmer({ query, database, onProgress, onRid, signal, }) {
94
- const { rows, queryRow, rid } = await queryPhmmer({
49
+ export async function doLaunchBlast({ self, scope, }) {
50
+ const params = self.blastParams;
51
+ const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
52
+ const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
53
+ const { onProgress, onRid, signal } = scope;
54
+ const { hits, queryRow, rid } = await searchBackends[searchProgram]({
95
55
  query,
96
- database,
56
+ database: params.blastDatabase,
57
+ maxHits,
97
58
  onProgress,
98
59
  onRid,
99
60
  signal,
100
61
  });
62
+ if (hits.length === 0) {
63
+ throw new Error('No hits found');
64
+ }
101
65
  onProgress('Fetching species taxonomy info...');
102
- const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
103
- const { msa, treeMetadata } = buildPhmmerMsa({
104
- rows,
66
+ const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
67
+ const { msa: fasta, treeMetadata } = buildSearchMsa({
68
+ hits,
69
+ query,
105
70
  queryRow,
106
71
  taxonomyInfo,
72
+ querySeqName,
107
73
  });
108
- return {
74
+ const { msa, tree } = queryRow
75
+ ? { msa: fasta, tree: '' }
76
+ : await launchMSA({
77
+ algorithm: params.msaAlgorithm ?? 'browser',
78
+ sequence: fasta,
79
+ onProgress,
80
+ signal,
81
+ });
82
+ const transcript = transcriptFields(selectedTranscript);
83
+ const treeMetadataJson = JSON.stringify(treeMetadata);
84
+ await saveBlastResult({
85
+ proteinSequence: query,
86
+ blastDatabase: params.blastDatabase,
87
+ msaAlgorithm: params.msaAlgorithm,
88
+ searchProgram: params.searchProgram,
109
89
  msa,
110
- tree: await launchTree({ alignment: msa, onProgress, signal }),
111
- treeMetadata,
112
- rid,
113
- };
90
+ tree,
91
+ treeMetadata: treeMetadataJson,
92
+ rid: rid ?? '',
93
+ geneId: asString(transcript.parentId),
94
+ transcriptId: asString(transcript.uniqueId),
95
+ transcriptName: transcriptName(selectedTranscript),
96
+ geneName: asString(transcript.gene_name) ?? asString(transcript.parentId),
97
+ });
98
+ return { msa, tree, treeMetadata: treeMetadataJson };
114
99
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ import { beforeEach, expect, test, vi } from 'vitest';
2
+ import { saveBlastResult } from '../utils/blastCache';
3
+ import { searchBackends } from '../utils/homologSearch';
4
+ import { launchMSA } from '../utils/msa';
5
+ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
+ import { resolveUniProtEntry } from '../utils/unirefHomologs';
7
+ import { doLaunchBlast } from './doLaunchBlast';
8
+ // Every network call is mocked: what is under test is how the launch turns a
9
+ // request into a search and the search's answer into rows -- which backend it
10
+ // asks, where the query comes from, and whether an aligner runs.
11
+ vi.mock('../utils/homologSearch', () => ({
12
+ searchBackends: { blastp: vi.fn(), phmmer: vi.fn() },
13
+ }));
14
+ vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
15
+ vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
16
+ vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
17
+ vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
18
+ const blastp = vi.mocked(searchBackends.blastp);
19
+ const phmmer = vi.mocked(searchBackends.phmmer);
20
+ const mockLaunchMSA = vi.mocked(launchMSA);
21
+ const setQuerySeqName = vi.fn();
22
+ const HIT = { accession: 'P1', id: 'P1_MOUSE', sciname: 'Mus musculus' };
23
+ function makeModel(blastParams) {
24
+ return {
25
+ blastParams,
26
+ querySeqName: 'QUERY',
27
+ setQuerySeqName,
28
+ };
29
+ }
30
+ function launch(self) {
31
+ return doLaunchBlast({
32
+ self,
33
+ scope: {
34
+ signal: new AbortController().signal,
35
+ act: fn => {
36
+ fn();
37
+ },
38
+ onProgress: () => { },
39
+ onRid: () => { },
40
+ },
41
+ });
42
+ }
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
46
+ vi.mocked(saveBlastResult).mockResolvedValue(undefined);
47
+ });
48
+ test('bare hits go to the chosen aligner, with the query first', async () => {
49
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
50
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
51
+ const result = await launch(makeModel({
52
+ searchProgram: 'blastp',
53
+ blastDatabase: 'uniprotkb_swissprot',
54
+ msaAlgorithm: 'clustalo',
55
+ maxHits: 20,
56
+ proteinSequence: 'MKWVTF*',
57
+ }));
58
+ expect(blastp).toHaveBeenCalledWith(expect.objectContaining({
59
+ query: 'MKWVTF',
60
+ database: 'uniprotkb_swissprot',
61
+ maxHits: 20,
62
+ }));
63
+ expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({
64
+ algorithm: 'clustalo',
65
+ sequence: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
66
+ }));
67
+ expect(result.msa).toBe('aligned');
68
+ expect(result.tree).toBe('tree');
69
+ });
70
+ test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
71
+ phmmer.mockResolvedValue({
72
+ rid: 'job',
73
+ queryRow: 'MKWV-TF',
74
+ hits: [{ ...HIT, sequence: 'MKWVSTF' }],
75
+ });
76
+ const result = await launch(makeModel({
77
+ searchProgram: 'phmmer',
78
+ blastDatabase: 'rp15',
79
+ proteinSequence: 'MKWVTF',
80
+ }));
81
+ expect(mockLaunchMSA).not.toHaveBeenCalled();
82
+ expect(result.msa).toBe('>QUERY\nMKWV-TF\n>P1-Mus_musculus\nMKWVSTF');
83
+ expect(result.tree).toBe('');
84
+ });
85
+ test('a UniProt accession supplies the query and names its row', async () => {
86
+ vi.mocked(resolveUniProtEntry).mockResolvedValue({
87
+ accession: 'P04637',
88
+ id: 'P53_HUMAN',
89
+ reviewed: true,
90
+ taxId: 9606,
91
+ scientificName: 'Homo sapiens',
92
+ sequence: 'MEEPQ',
93
+ });
94
+ phmmer.mockResolvedValue({
95
+ queryRow: 'MEEPQ',
96
+ hits: [{ ...HIT, sequence: 'MEEPQ' }],
97
+ });
98
+ const result = await launch(makeModel({
99
+ searchProgram: 'phmmer',
100
+ blastDatabase: 'swissprot',
101
+ accession: 'P04637',
102
+ }));
103
+ expect(resolveUniProtEntry).toHaveBeenCalledWith(['P04637'], 0, expect.any(AbortSignal));
104
+ expect(setQuerySeqName).toHaveBeenCalledWith('P53_HUMAN_query');
105
+ expect(result.msa.split('\n')[0]).toBe('>P53_HUMAN_query');
106
+ });
107
+ test('a request with no query at all is refused before any search runs', async () => {
108
+ await expect(launch(makeModel({ searchProgram: 'phmmer', blastDatabase: 'swissprot' }))).rejects.toThrow(/connectedTranscript/);
109
+ expect(phmmer).not.toHaveBeenCalled();
110
+ });
111
+ // blastParams is a frozen snapshot property, so the Feature the dialog put in
112
+ // it comes back from a session reload as the plain JSON it serialized to. The
113
+ // launch used to call .get() on that, minutes after the EBI job the reload
114
+ // resubmitted had come back, and threw where nothing was catching.
115
+ test('a transcript restored from a session snapshot still labels the cache row', async () => {
116
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
117
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
118
+ await launch(makeModel({
119
+ searchProgram: 'blastp',
120
+ blastDatabase: 'uniprotkb_swissprot',
121
+ msaAlgorithm: 'clustalo',
122
+ proteinSequence: 'MKWV',
123
+ selectedTranscript: {
124
+ uniqueId: 'NM_000546.6',
125
+ name: 'TP53-201',
126
+ parentId: 'TP53',
127
+ gene_name: 'TP53',
128
+ },
129
+ }));
130
+ expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
131
+ geneId: 'TP53',
132
+ transcriptId: 'NM_000546.6',
133
+ transcriptName: 'TP53-201',
134
+ geneName: 'TP53',
135
+ }));
136
+ });
137
+ test('a live Feature handed over in the same session labels it the same way', async () => {
138
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
139
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
140
+ const json = {
141
+ uniqueId: 'NM_000546.6',
142
+ name: 'TP53-201',
143
+ parentId: 'TP53',
144
+ };
145
+ await launch(makeModel({
146
+ searchProgram: 'blastp',
147
+ blastDatabase: 'uniprotkb_swissprot',
148
+ msaAlgorithm: 'clustalo',
149
+ proteinSequence: 'MKWV',
150
+ selectedTranscript: { toJSON: () => json },
151
+ }));
152
+ expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
153
+ geneId: 'TP53',
154
+ transcriptId: 'NM_000546.6',
155
+ transcriptName: 'TP53-201',
156
+ geneName: 'TP53',
157
+ }));
158
+ });
@@ -3,6 +3,8 @@ import { launchMSA } from '../utils/msa';
3
3
  import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
4
4
  import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
5
5
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
+ import { fetchUnirefHomologs } from '../utils/unirefHomologs';
7
+ import { transcriptName } from './util';
6
8
  /**
7
9
  * The no-search-job alternative to doLaunchBlast.
8
10
  *
@@ -21,7 +23,7 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
21
23
  * excluded from the ortholog set rather than appearing twice.
22
24
  */
23
25
  export async function doLaunchOrthologs({ self, scope, }) {
24
- const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
26
+ const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', identity, referenceProteomesOnly, } = self.orthologParams;
25
27
  const { onProgress, act, signal } = scope;
26
28
  const request = {
27
29
  taxId,
@@ -34,7 +36,14 @@ export async function doLaunchOrthologs({ self, scope, }) {
34
36
  };
35
37
  const { geneId, representative, rows } = source === 'panther'
36
38
  ? await findPantherOrthologs(request)
37
- : await findNcbiOrthologs(request);
39
+ : source === 'uniref'
40
+ ? await findUnirefHomologs({
41
+ ...request,
42
+ identity,
43
+ referenceProteomesOnly,
44
+ signal,
45
+ })
46
+ : await findNcbiOrthologs(request);
38
47
  // The query row. The dialog always supplies it — it is the user's OWN
39
48
  // selected transcript, which is what makes `connectedFeature` map genome
40
49
  // coordinates through this row. A launch that has no transcript to translate
@@ -46,7 +55,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
46
55
  ? cleanProteinSequence(proteinSequence)
47
56
  : representative?.sequence;
48
57
  if (!cleanedSeq) {
49
- throw new Error(`No query protein: none was supplied and ${source === 'panther' ? 'PANTHER' : 'NCBI'} returned no representative protein for gene ${geneId}.`);
58
+ throw new Error(`No query protein: none was supplied and ${sourceNames[source]} returned no representative protein for gene ${geneId}.`);
50
59
  }
51
60
  // The query row is named for its species like every other row, with a suffix
52
61
  // marking it as the one the genome view is linked to. A bare `QUERY` among
@@ -103,6 +112,33 @@ async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest })
103
112
  });
104
113
  return { geneId: resolved.geneId, representative, rows };
105
114
  }
115
+ const sourceNames = {
116
+ ncbi: 'NCBI',
117
+ panther: 'PANTHER',
118
+ uniref: 'UniProt',
119
+ };
120
+ /**
121
+ * The query's own UniProt entry stands in for a representative protein: the
122
+ * entry the cluster was looked up from, whose sequence a spec-launched row
123
+ * takes and whose accession earns the CDD overlay when the two match.
124
+ */
125
+ async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, signal, ...rest }) {
126
+ const found = await fetchUnirefHomologs({
127
+ candidates: geneCandidates,
128
+ identity,
129
+ referenceProteomesOnly,
130
+ signal,
131
+ ...rest,
132
+ });
133
+ return {
134
+ geneId: found.query.id,
135
+ representative: {
136
+ accession: found.query.accession,
137
+ sequence: found.query.sequence,
138
+ },
139
+ rows: found.rows,
140
+ };
141
+ }
106
142
  /**
107
143
  * One `matchortho` call resolves the gene, names its own UniProt entry and
108
144
  * lists an ortholog per genome, so the representative protein needs no second
@@ -162,9 +198,8 @@ async function fetchRepresentativeQueryProtein(geneId) {
162
198
  * protein as its query row passes that test by construction.
163
199
  */
164
200
  function buildQueryMetadata(self, geneId, proteinSequence, representative) {
165
- const transcript = self.orthologParams?.selectedTranscript;
166
201
  const metadata = { 'Gene ID': geneId };
167
- const name = transcript?.get('name') ?? transcript?.get('id');
202
+ const name = transcriptName(self.orthologParams?.selectedTranscript);
168
203
  if (name) {
169
204
  metadata.Transcript = name;
170
205
  }
@@ -203,6 +203,16 @@ describe('the Accession that drives the domain overlay', () => {
203
203
  Accession: REPRESENTATIVE.accession,
204
204
  });
205
205
  });
206
+ // orthologParams is frozen, so a session reload hands the transcript back as
207
+ // the plain JSON it serialized to; .get() on that threw
208
+ test('names the transcript from a snapshot-restored transcript', async () => {
209
+ const result = await launch({
210
+ self: makeModel(params({
211
+ selectedTranscript: { uniqueId: 'NM_1', name: 'TP53-201' },
212
+ })),
213
+ });
214
+ expect(queryMetadata(result).Transcript).toBe('TP53-201');
215
+ });
206
216
  test('is withheld from a non-representative isoform', async () => {
207
217
  const result = await launch({
208
218
  self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),