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
@@ -2,11 +2,14 @@ import { getContainingView, sum } from '@jbrowse/core/util';
2
2
  export function getLinearGenomeView(model) {
3
3
  return getContainingView(model);
4
4
  }
5
+ function isNonEmptyString(v) {
6
+ return typeof v === 'string' && v !== '';
7
+ }
5
8
  function uniqueDefined(vals) {
6
- return [...new Set(vals.filter((v) => !!v))];
9
+ return [...new Set(vals.filter(isNonEmptyString))];
7
10
  }
8
11
  function joinDefined(sep, parts) {
9
- return parts.filter((p) => !!p).join(sep);
12
+ return parts.filter(isNonEmptyString).join(sep);
10
13
  }
11
14
  export function getTranscriptFeatures(feature) {
12
15
  // check if we are looking at a 'two-level' or 'three-level' feature by
@@ -67,8 +70,14 @@ export function getSortedTranscriptFeatures(feature) {
67
70
  const transcripts = getTranscriptFeatures(feature);
68
71
  return transcripts.toSorted((a, b) => getTranscriptLength(b).len - getTranscriptLength(a).len);
69
72
  }
73
+ /**
74
+ * A translation as a query, still one residue per codon: g2p numbers codons,
75
+ * and the query row is read through it. So the partial first codon (`&`), an
76
+ * internal stop and an unreadable codon become X, and only what trails the
77
+ * last whole codon is dropped.
78
+ */
70
79
  export function cleanProteinSequence(seq) {
71
- return seq.replaceAll('*', '').replaceAll('&', '');
80
+ return seq.replace(/[*&]+$/, '').replaceAll(/[*&]/g, 'X');
72
81
  }
73
82
  export function getGeneIdentifiers(feature) {
74
83
  return uniqueDefined([
@@ -53,6 +53,21 @@ test('searchParams is a source, stored under the name the dialog uses', () => {
53
53
  test('a launch naming no source at all is refused', () => {
54
54
  expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
55
55
  });
56
+ test('region and the data-layer keys reach the view as snapshot properties', () => {
57
+ const region = { row: 'Human', start: 245, end: 249 };
58
+ const clades = [{ mrca: ['Human', 'Mouse'], tips: 2, mark: 'bracket' }];
59
+ const snapshot = launch({
60
+ data: { msa: '>Human\nMEEP' },
61
+ region,
62
+ clades,
63
+ relativeTo: 'Human',
64
+ gffFilehandle: { uri: 'http://example.com/a.gff' },
65
+ });
66
+ expect(snapshot.region).toEqual(region);
67
+ expect(snapshot.clades).toEqual(clades);
68
+ expect(snapshot.relativeTo).toBe('Human');
69
+ expect(snapshot.gffFilehandle).toEqual({ uri: 'http://example.com/a.gff' });
70
+ });
56
71
  test('one field set is enough to need init', () => {
57
72
  const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
58
73
  expect(snapshot.init).toEqual({
@@ -19,10 +19,12 @@ export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
19
19
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
20
20
  /**
21
21
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
22
- * request, and clearing them on success is what marks it done. They are left in
23
- * place on failure so the error stays attributable to a specific request; the
24
- * autorun's only tracked read is orthologParams itself, so nothing refires
25
- * until a new request replaces them.
22
+ * request, and `launchCompleted` is what marks it done. They are left in place
23
+ * either way -- on failure so the error stays attributable to a specific
24
+ * request, and on success because they are the only durable statement of what
25
+ * the view is, which is what a stored alignment that expired is rebuilt from.
26
+ * The autorun tracks those two reads alone, so nothing refires until a new
27
+ * request replaces them or a retry clears the mark.
26
28
  */
27
29
  export declare function launchOrthologsIfNeeded(self: JBrowsePluginMsaViewModel): void;
28
30
  export declare function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel): void;
@@ -9,7 +9,9 @@ import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } f
9
9
  import { runLaunch } from './runLaunch';
10
10
  import { getProteinViews } from './structureConnection';
11
11
  import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
12
- const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared. Relaunch the alignment to rebuild it.";
12
+ const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
13
+ const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
14
+ const START_OVER = ' Relaunch it from the gene to rebuild it.';
13
15
  export function loadStoredData(self) {
14
16
  const { dataStoreId, rows } = self;
15
17
  if (dataStoreId && rows.length === 0) {
@@ -30,15 +32,24 @@ export function loadStoredData(self) {
30
32
  if (storedData.treeMetadata) {
31
33
  self.setTreeMetadata(storedData.treeMetadata);
32
34
  }
35
+ if (storedData.gff) {
36
+ self.setGFF(storedData.gff);
37
+ }
33
38
  self.setLastStoredData(currentData(self));
34
39
  });
35
40
  }
36
41
  else {
37
42
  // the id names nothing, so clearing it is what lets react-msaview's
38
- // "Return to import form" actually return instead of landing back here
43
+ // "Return to import form" actually return instead of landing back
44
+ // here. The request that built the alignment is kept, though: it is
45
+ // the whole of a retry, and dropping it left the user reading an
46
+ // apology with nothing to press.
39
47
  transaction(() => {
40
48
  self.setDataStoreId(undefined);
41
- self.setError(new Error(EXPIRED_MESSAGE));
49
+ self.setError(new Error(EXPIRED_MESSAGE +
50
+ ((self.blastParams ?? self.orthologParams)
51
+ ? RELAUNCHABLE
52
+ : START_OVER)));
42
53
  });
43
54
  }
44
55
  }
@@ -51,15 +62,21 @@ export function loadStoredData(self) {
51
62
  })();
52
63
  }
53
64
  }
65
+ /**
66
+ * Everything react-msaview drops from the snapshot once it passes 50kB. The
67
+ * GFF is among them: a local one's filehandle is cleared once it loads, so
68
+ * this row is the only copy.
69
+ */
54
70
  function currentData(self) {
55
- const { msa, tree, treeMetadata } = self.data;
56
- return { msa, tree, treeMetadata };
71
+ const { msa, tree, treeMetadata, gff } = self.data;
72
+ return { msa, tree, treeMetadata, gff };
57
73
  }
58
74
  function sameData(a, b) {
59
75
  return (!!a &&
60
76
  a.msa === b.msa &&
61
77
  a.tree === b.tree &&
62
- a.treeMetadata === b.treeMetadata);
78
+ a.treeMetadata === b.treeMetadata &&
79
+ a.gff === b.gff);
63
80
  }
64
81
  /**
65
82
  * Keep IndexedDB holding what the view holds. The first run writes a new row and
@@ -115,19 +132,26 @@ export function storeDataToIndexedDB(self) {
115
132
  }
116
133
  /**
117
134
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
118
- * request, and clearing them on success is what marks it done. They are left in
119
- * place on failure so the error stays attributable to a specific request; the
120
- * autorun's only tracked read is orthologParams itself, so nothing refires
121
- * until a new request replaces them.
135
+ * request, and `launchCompleted` is what marks it done. They are left in place
136
+ * either way -- on failure so the error stays attributable to a specific
137
+ * request, and on success because they are the only durable statement of what
138
+ * the view is, which is what a stored alignment that expired is rebuilt from.
139
+ * The autorun tracks those two reads alone, so nothing refires until a new
140
+ * request replaces them or a retry clears the mark.
122
141
  */
123
142
  export function launchOrthologsIfNeeded(self) {
124
- if (self.orthologParams && !awaitingTranscript(self)) {
143
+ if (self.orthologParams &&
144
+ !self.launchCompleted &&
145
+ !awaitingTranscript(self)) {
125
146
  runLaunch({
126
147
  self,
127
148
  message: 'Resolving orthologs',
128
149
  launch: scope => doLaunchOrthologs({ self, scope }),
150
+ // marked rather than dropped: the request is the only durable statement
151
+ // of what this view is, and a view whose stored alignment expired runs it
152
+ // again
129
153
  onLaunched: () => {
130
- self.setOrthologParams(undefined);
154
+ self.setLaunchCompleted(true);
131
155
  },
132
156
  });
133
157
  }
@@ -142,13 +166,13 @@ function awaitingTranscript(self) {
142
166
  return !!self.connectedTranscript && !self.connectedFeature;
143
167
  }
144
168
  export function launchBlastIfNeeded(self) {
145
- if (self.blastParams && !awaitingTranscript(self)) {
169
+ if (self.blastParams && !self.launchCompleted && !awaitingTranscript(self)) {
146
170
  runLaunch({
147
171
  self,
148
172
  message: 'Submitting query',
149
173
  launch: scope => doLaunchBlast({ self, scope }),
150
174
  onLaunched: () => {
151
- self.setBlastParams(undefined);
175
+ self.setLaunchCompleted(true);
152
176
  },
153
177
  });
154
178
  }
@@ -0,0 +1,23 @@
1
+ import type { Region } from 'react-msaview';
2
+ interface RegionModel {
3
+ region?: Region;
4
+ viewInitialized: boolean;
5
+ numColumns: number;
6
+ treeFilehandle?: unknown;
7
+ data: {
8
+ tree?: string;
9
+ };
10
+ zoomToRegion: (region: Region) => void;
11
+ setRegion: (region?: Region) => void;
12
+ }
13
+ /**
14
+ * Zoom onto the launch's `region` once, then forget it, so a reloaded session
15
+ * opens where the reader left it rather than back on the region.
16
+ *
17
+ * zoomToRegion is a no-op until the view has a width, and it resolves residues
18
+ * through the visible columns, so it waits for the alignment and for a tree
19
+ * file too: a clade the tree collapses hides columns and would move the
20
+ * target after the zoom.
21
+ */
22
+ export declare function applyRegion(self: RegionModel): void;
23
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Zoom onto the launch's `region` once, then forget it, so a reloaded session
3
+ * opens where the reader left it rather than back on the region.
4
+ *
5
+ * zoomToRegion is a no-op until the view has a width, and it resolves residues
6
+ * through the visible columns, so it waits for the alignment and for a tree
7
+ * file too: a clade the tree collapses hides columns and would move the
8
+ * target after the zoom.
9
+ */
10
+ export function applyRegion(self) {
11
+ const { region } = self;
12
+ if (region &&
13
+ self.viewInitialized &&
14
+ self.numColumns > 0 &&
15
+ !(self.treeFilehandle && !self.data.tree)) {
16
+ self.zoomToRegion(region);
17
+ self.setRegion(undefined);
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { expect, test, vi } from 'vitest';
2
+ import { applyRegion } from './applyRegion';
3
+ const REGION = { row: 'Human', start: 245, end: 249 };
4
+ function makeModel(over = {}) {
5
+ const model = {
6
+ region: REGION,
7
+ viewInitialized: true,
8
+ numColumns: 400,
9
+ treeFilehandle: undefined,
10
+ data: {},
11
+ zoomToRegion: vi.fn(),
12
+ setRegion: vi.fn((arg) => {
13
+ model.region = arg;
14
+ }),
15
+ ...over,
16
+ };
17
+ return model;
18
+ }
19
+ test('zooms onto the region once, then forgets it', () => {
20
+ const model = makeModel();
21
+ applyRegion(model);
22
+ applyRegion(model);
23
+ expect(model.zoomToRegion).toHaveBeenCalledTimes(1);
24
+ expect(model.zoomToRegion).toHaveBeenCalledWith(REGION);
25
+ expect(model.region).toBeUndefined();
26
+ });
27
+ test('waits for the view to have a width', () => {
28
+ const model = makeModel({ viewInitialized: false });
29
+ applyRegion(model);
30
+ expect(model.zoomToRegion).not.toHaveBeenCalled();
31
+ expect(model.region).toEqual(REGION);
32
+ });
33
+ test('waits for the alignment', () => {
34
+ const model = makeModel({ numColumns: 0 });
35
+ applyRegion(model);
36
+ expect(model.zoomToRegion).not.toHaveBeenCalled();
37
+ });
38
+ test('waits for a tree file, whose collapsed clades move the columns', () => {
39
+ const model = makeModel({ treeFilehandle: { uri: 'x.nh' } });
40
+ applyRegion(model);
41
+ expect(model.zoomToRegion).not.toHaveBeenCalled();
42
+ model.data.tree = '(a,b);';
43
+ applyRegion(model);
44
+ expect(model.zoomToRegion).toHaveBeenCalledTimes(1);
45
+ });
@@ -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,16 +18,21 @@ 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
37
  ? `Running EBI ${blastParams.searchProgram === 'phmmer' ? 'phmmer' : 'BLAST'}`
33
38
  : orthologParams
@@ -36,9 +41,7 @@ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
36
41
  : 'Building ortholog alignment'
37
42
  : 'Loading alignment';
38
43
  return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
39
- React.createElement(Typography, { variant: "h5" },
40
- message,
41
- " failed"),
44
+ React.createElement(Typography, { variant: "h5" }, pending ? `${message} failed` : 'Alignment no longer available'),
42
45
  rid ? React.createElement(JobLink, { jobId: rid }) : null,
43
46
  React.createElement(ErrorMessage, { error: error }),
44
47
  React.createElement("div", { className: classes.progressRow },
@@ -17,9 +17,13 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
17
17
  // three gate the same panel -- see LaunchProgress. An indexed view keeps its
18
18
  // init for the life of the view (it is how the block is refetched), so that
19
19
  // one is only "launching" until the alignment arrives.
20
- const launching = !!(blastParams ??
21
- orthologParams ??
22
- (init && !model.dataInitialized));
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);
23
27
  return (React.createElement(ErrorBoundary, null,
24
28
  React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
25
29
  React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
@@ -97,6 +97,30 @@ test('a failed launch can be retried or dismissed', () => {
97
97
  screen.getByRole('button', { name: 'Dismiss' }).click();
98
98
  expect(cancelLaunch).toHaveBeenCalled();
99
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();
123
+ });
100
124
  test('a running job links out to it', () => {
101
125
  panel({
102
126
  blastParams: { proteinSequence: 'MKV' },
@@ -84,8 +84,9 @@ export async function doLaunchBlast({ self, scope, }) {
84
84
  await saveBlastResult({
85
85
  proteinSequence: query,
86
86
  blastDatabase: params.blastDatabase,
87
- msaAlgorithm: params.msaAlgorithm,
87
+ msaAlgorithm: queryRow ? undefined : (params.msaAlgorithm ?? 'browser'),
88
88
  searchProgram: params.searchProgram,
89
+ maxHits,
89
90
  msa,
90
91
  tree,
91
92
  treeMetadata: treeMetadataJson,
@@ -67,6 +67,18 @@ test('bare hits go to the chosen aligner, with the query first', async () => {
67
67
  expect(result.msa).toBe('aligned');
68
68
  expect(result.tree).toBe('tree');
69
69
  });
70
+ // the history names the aligner that ran, which for a spec naming none is the
71
+ // in-browser one -- not a key reading `db:undefined:...`
72
+ test('a search naming no aligner is saved as aligned in the browser', async () => {
73
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
74
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: '' });
75
+ await launch(makeModel({
76
+ blastDatabase: 'uniprotkb_swissprot',
77
+ maxHits: 20,
78
+ proteinSequence: 'MKWVTF',
79
+ }));
80
+ expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({ msaAlgorithm: 'browser', maxHits: 20 }));
81
+ });
70
82
  test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
71
83
  phmmer.mockResolvedValue({
72
84
  rid: 'job',
@@ -175,7 +175,7 @@ describe('the query row sequence', () => {
175
175
  await launch({
176
176
  self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
177
177
  });
178
- expect(queryRowSent()).toBe('MAGGAWGR');
178
+ expect(queryRowSent()).toBe('MAGGXAWGR');
179
179
  });
180
180
  test('throws when neither a sequence nor a representative is available', async () => {
181
181
  mockFetchProtein.mockResolvedValue(undefined);
@@ -1,8 +1,8 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
- import { hasHoverPosition, hasQueryRow, transcriptPosToVisibleCol, } from './util';
2
+ import { hasHoverPosition, hasQueryRow, querySeqPosToVisibleCol, transcriptPosToVisibleCol, } from './util';
3
3
  export function genomeToMSA({ model }) {
4
- const { hovered } = getSession(model);
5
- const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
4
+ const { assemblyManager, hovered } = getSession(model);
5
+ const { transcriptToMsaMap, connectedView, mafRegion } = model;
6
6
  if (!connectedView?.initialized ||
7
7
  !hasHoverPosition(hovered) ||
8
8
  !hasQueryRow(model)) {
@@ -12,20 +12,38 @@ export function genomeToMSA({ model }) {
12
12
  // hoverPosition.coord is a 1-based display coordinate (core's pxToBp adds the
13
13
  // +1), while g2p and mafRegion are keyed by 0-based genome position
14
14
  const genomePos = coord - 1;
15
+ // The two sides name the chromosome differently. A hover carries the
16
+ // assembly's canonical name -- `1` on jbrowse.org's hg38 -- while mafRegion
17
+ // and the transcript's g2p map carry the feature's, straight out of the
18
+ // annotation file, which is `chr1` in GENCODE. Compared raw they agree only
19
+ // where a config happens to pair files that agree, and everywhere else
20
+ // hovering a codon lit nothing at all, with no throw and no console line.
21
+ //
22
+ // `initialized` is the gate rather than a try/catch: getCanonicalRefName
23
+ // throws until the assembly's aliases load, and this runs from a hover
24
+ // autorun over an assembly nothing here waited for -- mafRegion names its own
25
+ // on a frozen spec, which need not be one the connected view loaded. Until
26
+ // the aliases arrive the raw name is the whole answer, and the hover lights
27
+ // nothing for those frames rather than throwing out of the autorun.
28
+ const assembly = assemblyManager.get(mafRegion?.assemblyName ?? connectedView.assemblyNames[0] ?? '');
29
+ const canonical = (name) => (assembly?.initialized ? assembly.getCanonicalRefName(name) : undefined) ??
30
+ name;
31
+ const hoveredRefName = canonical(refName);
15
32
  if (mafRegion) {
16
- if (refName !== mafRegion.refName ||
33
+ if (hoveredRefName !== canonical(mafRegion.refName) ||
17
34
  !connectedView.assemblyNames.includes(mafRegion.assemblyName) ||
18
35
  genomePos < mafRegion.start ||
19
36
  genomePos >= mafRegion.end) {
20
37
  return undefined;
21
38
  }
22
- return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start);
39
+ return querySeqPosToVisibleCol(model, genomePos - mafRegion.start);
23
40
  }
24
41
  // session.hovered is global -- set by whichever LinearGenomeView the cursor
25
42
  // was last over, on any assembly -- so the refName gate is load bearing:
26
43
  // without it the same numeric coordinate on an unrelated chromosome matches a
27
44
  // g2p key and lights up a column for a different locus
28
- if (refName === transcriptToMsaMap?.refName) {
45
+ if (transcriptToMsaMap &&
46
+ hoveredRefName === canonical(transcriptToMsaMap.refName)) {
29
47
  const proteinPos = transcriptToMsaMap.g2p[genomePos];
30
48
  if (proteinPos !== undefined) {
31
49
  return transcriptPosToVisibleCol(model, proteinPos);