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
@@ -61,6 +61,39 @@ describe('a failed launch', () => {
61
61
  expect(model.blastParams).toEqual(BLAST_PARAMS);
62
62
  expect(doLaunchBlast).toHaveBeenCalledTimes(2);
63
63
  });
64
+ // the params used to be cleared on success to mark the request done, which
65
+ // threw away the only durable statement of what the view is: a stored
66
+ // alignment expires after 7 days and there was nothing left to run again
67
+ test('a successful launch keeps its request and is not run twice', async () => {
68
+ vi.mocked(doLaunchBlast).mockResolvedValue({
69
+ msa: '>a\nMK\n>b\nMK',
70
+ tree: '(a,b);',
71
+ treeMetadata: '{}',
72
+ });
73
+ const model = view();
74
+ model.setBlastParams({ ...BLAST_PARAMS });
75
+ await new Promise(res => setTimeout(res, 0));
76
+ expect(model.blastParams).toEqual(BLAST_PARAMS);
77
+ expect(model.launchCompleted).toBe(true);
78
+ expect(doLaunchBlast).toHaveBeenCalledTimes(1);
79
+ vi.mocked(doLaunchBlast).mockReturnValue(new Promise(() => { }));
80
+ model.setError(new Error('alignment is no longer in browser storage'));
81
+ model.retryLaunch();
82
+ expect(model.error).toBeUndefined();
83
+ expect(model.launchCompleted).toBe(false);
84
+ expect(doLaunchBlast).toHaveBeenCalledTimes(2);
85
+ });
86
+ // a session reopens with the request still on it, so the mark is what stops
87
+ // the whole search running again unasked
88
+ test('a restored session does not rerun a request it already ran', () => {
89
+ stateModelFactory().create({
90
+ type: 'MsaView',
91
+ id: 'msaview2',
92
+ blastParams: { ...BLAST_PARAMS },
93
+ launchCompleted: true,
94
+ });
95
+ expect(doLaunchBlast).not.toHaveBeenCalled();
96
+ });
64
97
  test('dismisses by dropping it', () => {
65
98
  const model = view();
66
99
  model.setBlastParams({ ...BLAST_PARAMS });
@@ -70,6 +103,39 @@ describe('a failed launch', () => {
70
103
  expect(model.error).toBeUndefined();
71
104
  });
72
105
  });
106
+ // react-msaview drops a snapshot document past 50kb and reports it through
107
+ // `unshareableData`, which the header renders as "Not in the link". An indexed
108
+ // view's alignment IS in the link -- its init names the block and processInit
109
+ // refetches it -- so that view answers the question itself.
110
+ describe('what the snapshot cannot carry', () => {
111
+ const big = `>a\n${'M'.repeat(60_000)}`;
112
+ test('an indexed view says the host brings its alignment back', () => {
113
+ const model = view();
114
+ model.setInit({
115
+ msaIndexedLocation: { uri: 'msa.fa.gz' },
116
+ msaName: 'ENST1',
117
+ });
118
+ model.setMSA(big);
119
+ expect(model.hostRestoresData).toBe(true);
120
+ expect(model.unshareableData).toEqual([]);
121
+ });
122
+ // the base getter reads a volatile a host flips to say it brings the data
123
+ // back by its own means; overriding without it threw that switch away
124
+ test('a host that says it carries the data is still believed', () => {
125
+ const model = view();
126
+ model.setMSA(big);
127
+ expect(model.unshareableData).not.toEqual([]);
128
+ model.setHostCarriesData(true);
129
+ expect(model.hostRestoresData).toBe(true);
130
+ expect(model.unshareableData).toEqual([]);
131
+ });
132
+ test('a pasted alignment is reported, because a link really does lose it', () => {
133
+ const model = view();
134
+ model.setMSA(big);
135
+ expect(model.hostRestoresData).toBe(false);
136
+ expect(model.unshareableData.map(d => d.what)).toEqual(['alignment']);
137
+ });
138
+ });
73
139
  // react-msaview's reset keeps only what is on its own preservedOnReset list,
74
140
  // and a downstream property is never on it; its volatiles survive instead,
75
141
  // which is the direction that carries the last file's state into the next one
@@ -3,13 +3,15 @@ export interface MsaDataPayload {
3
3
  msa?: string;
4
4
  tree?: string;
5
5
  treeMetadata?: string;
6
+ gff?: string;
6
7
  }
7
8
  export declare function generateDataStoreId(): string;
8
9
  export declare function storeMsaData(id: string, data: MsaDataPayload): Promise<boolean>;
9
10
  export declare function retrieveMsaData(id: string): Promise<{
10
- msa: string | undefined;
11
- tree: string | undefined;
12
- treeMetadata: string | undefined;
11
+ msa?: string;
12
+ tree?: string;
13
+ treeMetadata?: string;
14
+ gff?: string;
13
15
  } | undefined>;
14
16
  /** drop one view's row, for a reset that just orphaned it */
15
17
  export declare function deleteMsaData(id: string): Promise<void>;
@@ -14,13 +14,7 @@ export function generateDataStoreId() {
14
14
  export async function storeMsaData(id, data) {
15
15
  try {
16
16
  const db = await getDB();
17
- const storedData = {
18
- id,
19
- msa: data.msa,
20
- tree: data.tree,
21
- treeMetadata: data.treeMetadata,
22
- timestamp: Date.now(),
23
- };
17
+ const storedData = { ...data, id, timestamp: Date.now() };
24
18
  await db.put(STORE_NAME, storedData);
25
19
  return true;
26
20
  }
@@ -45,11 +39,8 @@ export async function retrieveMsaData(id) {
45
39
  catch (e) {
46
40
  console.warn('Failed to refresh MSA data timestamp:', e);
47
41
  }
48
- return {
49
- msa: result.msa,
50
- tree: result.tree,
51
- treeMetadata: result.treeMetadata,
52
- };
42
+ const { id: _id, timestamp: _timestamp, ...payload } = result;
43
+ return payload;
53
44
  }
54
45
  catch (e) {
55
46
  console.warn('Failed to retrieve MSA data:', e);
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, expect, test, vi } from 'vitest';
2
- import { retrieveMsaData } from './msaDataStore';
2
+ import { retrieveMsaData, storeMsaData } from './msaDataStore';
3
3
  // An in-memory stand-in for the one object store this module opens, with a
4
4
  // switch for the write half so a browser that refuses writes can be played back.
5
5
  const { rows, state } = vi.hoisted(() => ({
@@ -42,3 +42,10 @@ test('a refresh that fails still hands back the data it read', async () => {
42
42
  state.putFails = true;
43
43
  expect((await retrieveMsaData('msa-1'))?.msa).toBe('>a\nMK');
44
44
  });
45
+ test('every document a view keeps comes back, and nothing else', async () => {
46
+ await storeMsaData('msa-1', { msa: '>a\nMK', gff: 'a\t.\tdomain\t1\t2' });
47
+ expect(await retrieveMsaData('msa-1')).toEqual({
48
+ msa: '>a\nMK',
49
+ gff: 'a\t.\tdomain\t1\t2',
50
+ });
51
+ });
@@ -19,7 +19,7 @@ function makeModel({ highlightColumns, querySeqOffset = 0, } = {}) {
19
19
  const model = {
20
20
  querySeqName: 'query',
21
21
  querySeqOffset,
22
- rows: [['query', 'MKVLTAEEK']],
22
+ rowMap: new Map([['query', 'MKVLTAEEK']]),
23
23
  connectedViewId: CONNECTED,
24
24
  // g2p is indexed by genome coord; identity keeps the arithmetic out of the way
25
25
  transcriptToMsaMap: {
@@ -213,11 +213,11 @@ describe('scope and redundant writes', () => {
213
213
  run();
214
214
  expect(calls).toEqual([]);
215
215
  });
216
- // seqPosToGlobalCol answers 0 for a row name it does not know, so without a
217
- // guard a structure hover would light column 0 of whatever row is first
218
216
  test('a query row this alignment does not have contributes no column', () => {
219
217
  const { model, calls } = makeModel();
220
- Object.assign(model, { rows: [['some_other_row', 'MKVLTAEEK']] });
218
+ Object.assign(model, {
219
+ rowMap: new Map([['some_other_row', 'MKVLTAEEK']]),
220
+ });
221
221
  const run = observeProteinHighlights(model);
222
222
  session({ hover: [{ start: 10, end: 12 }] });
223
223
  run();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { expect, test, vi } from 'vitest';
2
+ import stateModelFactory from './model';
3
+ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
4
+ ...(await importOriginal()),
5
+ getSession: () => ({ views: [], hovered: undefined }),
6
+ }));
7
+ vi.mock('./msaDataStore', () => ({
8
+ cleanupOldData: vi.fn(async () => { }),
9
+ deleteMsaData: vi.fn(async () => { }),
10
+ generateDataStoreId: vi.fn(),
11
+ retrieveMsaData: vi.fn(),
12
+ storeMsaData: vi.fn(async () => true),
13
+ }));
14
+ const MSA = `>Human\n${'MEEPQSDPSV'.repeat(20)}\n>Mouse\n${'MEE-QSDPSV'.repeat(20)}`;
15
+ test('a spec region waits for the view to have a width, zooms, and is dropped', () => {
16
+ const region = { row: 'Human', start: 150, end: 160 };
17
+ const model = stateModelFactory().create({
18
+ type: 'MsaView',
19
+ id: 'msaview1',
20
+ data: { msa: MSA },
21
+ region,
22
+ gffFilehandle: { uri: 'http://example.com/a.gff' },
23
+ });
24
+ expect(model.region).toEqual(region);
25
+ expect(model.gffFilehandle).toMatchObject({ locationType: 'UriLocation' });
26
+ model.setWidth(800);
27
+ expect(model.region).toBeUndefined();
28
+ expect(model.scrollX).toBeLessThan(0);
29
+ });
@@ -1,9 +1,46 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
+ interface Region {
3
+ assemblyName: string;
4
+ refName: string;
5
+ start: number;
6
+ end: number;
7
+ }
8
+ interface GenomeView {
9
+ tracks: {
10
+ type: string;
11
+ }[];
12
+ displayedRegions: Region[];
13
+ dynamicBlocks?: {
14
+ contentBlocks?: Region[];
15
+ };
16
+ }
17
+ /**
18
+ * The part of the genome on screen, then everything the view can scroll to.
19
+ * `displayedRegions` is usually whole chromosomes -- navigating to a locus
20
+ * displays its parent region -- so it is the fallback, not the first ask.
21
+ */
22
+ export declare function searchWindows(view: GenomeView): {
23
+ assemblyName: string;
24
+ refName: string;
25
+ start: number;
26
+ end: number;
27
+ }[][];
28
+ /**
29
+ * Gene models live on feature tracks. Asking an alignments, variant or
30
+ * quantitative track for a chromosome's worth of features to look for a
31
+ * transcript among them is the slowest possible way to find nothing.
32
+ */
33
+ export declare function transcriptTracks<T extends {
34
+ type: string;
35
+ }>(tracks: T[]): T[];
2
36
  /**
3
37
  * Resolve `connectedTranscript` into `connectedFeature` once the connected
4
- * genome view has its assembly, regions and tracks, then hand the translation
5
- * to whichever launch is waiting for a query. Left in place on failure so the
6
- * error stays attributable; the autorun's tracked reads are the view's
7
- * readiness, so nothing refires until that changes.
38
+ * genome view has its assembly and tracks, then hand the translation to
39
+ * whichever launch is waiting for a query.
40
+ *
41
+ * The autorun re-fires on the view's readiness (a track opened after a miss)
42
+ * and on the request itself, which is what `retryLaunch` re-states. The lookup
43
+ * runs untracked, so scrolling the genome view is not a reason to look again.
8
44
  */
9
45
  export declare function resolveConnectedTranscriptIfNeeded(self: JBrowsePluginMsaViewModel): void;
46
+ export {};
@@ -1,22 +1,10 @@
1
1
  import { getConf } from '@jbrowse/core/configuration';
2
2
  import { getSession } from '@jbrowse/core/util';
3
+ import { isAlive } from '@jbrowse/mobx-state-tree';
4
+ import { transaction, untracked } from 'mobx';
3
5
  import { getProteinSequenceFromFeature } from '../LaunchMsaView/components/calculateProteinSequence';
4
6
  import { fetchSeq } from '../LaunchMsaView/components/fetchSeq';
5
7
  import { getTranscriptFeatures } from '../LaunchMsaView/util';
6
- // The short form of a connected launch. A spec that says
7
- //
8
- // {"type":"MsaView","connectedViewId":"lgv1","connectedTranscript":"NM_000546.6", ...}
9
- //
10
- // gets its `connectedFeature` -- the exon model every genome<->column mapping
11
- // runs through -- looked up here, from the gene tracks the connected genome
12
- // view has open, and its query row translated from that transcript. Before
13
- // this the spec had to carry the transcript's CDS list itself, ~1.5kB of
14
- // coordinates a person cannot type and a tutorial cannot show.
15
- //
16
- // jbrowse-plugin-protein3d does the same for its `transcriptId` shorthand,
17
- // against a `connectedView` spec of its own; here the genome view already
18
- // exists (the spec pinned its id), so its tracks and regions are read off the
19
- // live model instead.
20
8
  function stripVersion(id) {
21
9
  return id.replace(/\.\d+$/, '');
22
10
  }
@@ -29,26 +17,51 @@ function transcriptMatches(transcript, id) {
29
17
  transcript.id(),
30
18
  ].some(c => typeof c === 'string' && (c === id || stripVersion(c) === target));
31
19
  }
20
+ function plainRegions(regions) {
21
+ return regions.map(r => ({
22
+ assemblyName: r.assemblyName,
23
+ refName: r.refName,
24
+ start: Math.floor(r.start),
25
+ end: Math.ceil(r.end),
26
+ }));
27
+ }
28
+ /**
29
+ * The part of the genome on screen, then everything the view can scroll to.
30
+ * `displayedRegions` is usually whole chromosomes -- navigating to a locus
31
+ * displays its parent region -- so it is the fallback, not the first ask.
32
+ */
33
+ export function searchWindows(view) {
34
+ const visible = plainRegions(view.dynamicBlocks?.contentBlocks ?? []);
35
+ const displayed = plainRegions(view.displayedRegions);
36
+ return visible.length > 0 ? [visible, displayed] : [displayed];
37
+ }
38
+ /**
39
+ * Gene models live on feature tracks. Asking an alignments, variant or
40
+ * quantitative track for a chromosome's worth of features to look for a
41
+ * transcript among them is the slowest possible way to find nothing.
42
+ */
43
+ export function transcriptTracks(tracks) {
44
+ return tracks.filter(t => t.type === 'FeatureTrack');
45
+ }
32
46
  async function findTranscript(self, transcriptId) {
33
47
  const view = self.connectedView;
34
48
  const session = getSession(self);
35
49
  const sessionId = 'msaview-connectedTranscript';
36
- const regions = view.displayedRegions.map(r => ({
37
- assemblyName: r.assemblyName,
38
- refName: r.refName,
39
- start: r.start,
40
- end: r.end,
41
- }));
42
- for (const track of view.tracks) {
43
- const feats = (await session.rpcManager.call(sessionId, 'CoreGetFeatures', {
44
- adapterConfig: getConf(track, 'adapter'),
45
- sessionId,
46
- regions,
47
- }));
48
- for (const feat of feats) {
49
- const hit = getTranscriptFeatures(feat).find(t => transcriptMatches(t, transcriptId));
50
- if (hit) {
51
- return hit;
50
+ const tracks = transcriptTracks(view.tracks);
51
+ for (const regions of searchWindows(view)) {
52
+ for (const track of tracks) {
53
+ // a named object keeps sessionId, which v4 hosts read from the args
54
+ const args = {
55
+ adapterConfig: getConf(track, 'adapter'),
56
+ sessionId,
57
+ regions,
58
+ };
59
+ const feats = await session.rpcManager.call(sessionId, 'CoreGetFeatures', args);
60
+ for (const feat of feats) {
61
+ const hit = getTranscriptFeatures(feat).find(t => transcriptMatches(t, transcriptId));
62
+ if (hit) {
63
+ return hit;
64
+ }
52
65
  }
53
66
  }
54
67
  }
@@ -57,46 +70,43 @@ async function findTranscript(self, transcriptId) {
57
70
  async function translate(self, transcript) {
58
71
  const { start, end, refName } = transcript.toJSON();
59
72
  const assemblyName = self.connectedView.assemblyNames[0];
60
- const seq = await fetchSeq({
73
+ const { seq, assemblyGeneticCodeId } = await fetchSeq({
61
74
  start,
62
75
  end,
63
76
  refName,
64
77
  assemblyName,
65
78
  session: getSession(self),
66
79
  });
67
- return getProteinSequenceFromFeature({ seq, feature: transcript });
80
+ return getProteinSequenceFromFeature({
81
+ seq,
82
+ feature: transcript,
83
+ assemblyGeneticCodeId,
84
+ });
68
85
  }
69
- /**
70
- * Resolve `connectedTranscript` into `connectedFeature` once the connected
71
- * genome view has its assembly, regions and tracks, then hand the translation
72
- * to whichever launch is waiting for a query. Left in place on failure so the
73
- * error stays attributable; the autorun's tracked reads are the view's
74
- * readiness, so nothing refires until that changes.
75
- */
76
- export function resolveConnectedTranscriptIfNeeded(self) {
77
- const { connectedTranscript, connectedFeature, connectedView } = self;
78
- if (!connectedTranscript ||
79
- connectedFeature ||
80
- !connectedView?.initialized ||
81
- connectedView.tracks.length === 0) {
82
- return;
83
- }
84
- const trackCount = connectedView.tracks.length;
85
- void (async () => {
86
- try {
87
- self.setProgress(`Looking up ${connectedTranscript} in the genome view...`);
88
- const transcript = await findTranscript(self, connectedTranscript);
89
- if (!transcript) {
90
- throw new Error(`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.`);
91
- }
92
- const proteinSequence = await translate(self, transcript);
93
- if (!proteinSequence) {
94
- throw new Error(`Transcript "${connectedTranscript}" has no CDS to translate`);
95
- }
96
- // toJSON, not the Feature: the params are frozen snapshot properties, so
97
- // an instance in one is gone the moment the session reloads
98
- const transcriptJson = transcript.toJSON();
99
- const { blastParams, orthologParams } = self;
86
+ /** the latest lookup per view; an older one finishing late writes nothing */
87
+ const latestLookup = new WeakMap();
88
+ async function lookUp({ self, transcriptId, blastParams, orthologParams, isCurrent, }) {
89
+ const trackCount = transcriptTracks(self.connectedView.tracks).length;
90
+ try {
91
+ self.setProgress(`Looking up ${transcriptId} in the genome view...`);
92
+ const transcript = await findTranscript(self, transcriptId);
93
+ if (!transcript) {
94
+ throw new Error(`Transcript "${transcriptId}" was not found in the ${trackCount} feature track(s) open in the connected genome view. Open the gene track that carries it, and make sure the view is on its locus.`);
95
+ }
96
+ const proteinSequence = await translate(self, transcript);
97
+ if (!proteinSequence) {
98
+ throw new Error(`Transcript "${transcriptId}" has no CDS to translate`);
99
+ }
100
+ if (!isCurrent()) {
101
+ return;
102
+ }
103
+ // toJSON, not the Feature: the params are frozen snapshot properties, so
104
+ // an instance in one is gone the moment the session reloads
105
+ const transcriptJson = transcript.toJSON();
106
+ // one transaction, so the launchers wake once, on a request that already
107
+ // carries its query
108
+ transaction(() => {
109
+ self.setProgress('');
100
110
  if (blastParams && !blastParams.proteinSequence) {
101
111
  self.setBlastParams({
102
112
  ...blastParams,
@@ -111,12 +121,43 @@ export function resolveConnectedTranscriptIfNeeded(self) {
111
121
  proteinSequence,
112
122
  });
113
123
  }
114
- // last, because the launchers wake on it
115
124
  self.setConnectedFeature(transcriptJson);
116
- }
117
- catch (e) {
118
- console.error(e);
125
+ });
126
+ }
127
+ catch (e) {
128
+ console.error(e);
129
+ if (isCurrent()) {
130
+ self.setProgress('');
119
131
  self.setError(e);
120
132
  }
121
- })();
133
+ }
134
+ }
135
+ /**
136
+ * Resolve `connectedTranscript` into `connectedFeature` once the connected
137
+ * genome view has its assembly and tracks, then hand the translation to
138
+ * whichever launch is waiting for a query.
139
+ *
140
+ * The autorun re-fires on the view's readiness (a track opened after a miss)
141
+ * and on the request itself, which is what `retryLaunch` re-states. The lookup
142
+ * runs untracked, so scrolling the genome view is not a reason to look again.
143
+ */
144
+ export function resolveConnectedTranscriptIfNeeded(self) {
145
+ const { connectedTranscript, connectedFeature, connectedView, blastParams, orthologParams, } = self;
146
+ if (!connectedTranscript ||
147
+ connectedFeature ||
148
+ !connectedView?.initialized ||
149
+ connectedView.tracks.length === 0) {
150
+ return;
151
+ }
152
+ const lookup = (latestLookup.get(self) ?? 0) + 1;
153
+ latestLookup.set(self, lookup);
154
+ untracked(() => {
155
+ void lookUp({
156
+ self,
157
+ transcriptId: connectedTranscript,
158
+ blastParams,
159
+ orthologParams,
160
+ isCurrent: () => isAlive(self) && latestLookup.get(self) === lookup,
161
+ });
162
+ });
122
163
  }
@@ -0,0 +1,149 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import stateModelFactory from './model';
3
+ const rpcCall = vi.fn();
4
+ const lgv = {
5
+ id: 'lgv1',
6
+ initialized: true,
7
+ assemblyNames: ['hg38'],
8
+ tracks: [],
9
+ displayedRegions: [
10
+ { assemblyName: 'hg38', refName: 'chr17', start: 0, end: 83_257_441 },
11
+ ],
12
+ dynamicBlocks: {
13
+ contentBlocks: [],
14
+ },
15
+ };
16
+ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
17
+ ...(await importOriginal()),
18
+ getSession: () => ({
19
+ views: [lgv],
20
+ hovered: undefined,
21
+ rpcManager: { call: rpcCall },
22
+ }),
23
+ }));
24
+ vi.mock('@jbrowse/core/configuration', async (importOriginal) => ({
25
+ ...(await importOriginal()),
26
+ getConf: (track) => track.adapter,
27
+ }));
28
+ vi.mock('../LaunchMsaView/components/fetchSeq', () => ({
29
+ fetchSeq: vi.fn(async () => ({ seq: 'ATGAAAGTG' })),
30
+ }));
31
+ vi.mock('../LaunchMsaView/components/calculateProteinSequence', () => ({
32
+ getProteinSequenceFromFeature: vi.fn(() => 'MKV'),
33
+ }));
34
+ vi.mock('./doLaunchBlast', () => ({
35
+ doLaunchBlast: vi.fn(() => new Promise(() => { })),
36
+ }));
37
+ vi.mock('./msaDataStore', () => ({
38
+ cleanupOldData: vi.fn(async () => { }),
39
+ deleteMsaData: vi.fn(async () => { }),
40
+ generateDataStoreId: vi.fn(),
41
+ retrieveMsaData: vi.fn(),
42
+ storeMsaData: vi.fn(),
43
+ }));
44
+ function transcript(id) {
45
+ const json = { uniqueId: id, id, refName: 'chr17', start: 0, end: 9 };
46
+ return {
47
+ id: () => id,
48
+ get: (key) => (key === 'id' || key === 'name' ? id : undefined),
49
+ toJSON: () => json,
50
+ };
51
+ }
52
+ const settle = () => new Promise(res => setTimeout(res, 0));
53
+ function connectedView() {
54
+ return stateModelFactory().create({
55
+ type: 'MsaView',
56
+ id: 'msa1',
57
+ connectedViewId: 'lgv1',
58
+ connectedTranscript: 'NM_000546.6',
59
+ blastParams: {
60
+ searchProgram: 'blastp',
61
+ blastDatabase: 'uniprotkb_swissprot',
62
+ msaAlgorithm: 'clustalo',
63
+ },
64
+ });
65
+ }
66
+ beforeEach(() => {
67
+ rpcCall.mockReset();
68
+ lgv.tracks = [{ type: 'FeatureTrack', adapter: 'genes' }];
69
+ lgv.dynamicBlocks.contentBlocks = [];
70
+ vi.spyOn(console, 'error').mockImplementation(() => { });
71
+ });
72
+ describe('a lookup that misses', () => {
73
+ test('reports the miss without leaving its progress up', async () => {
74
+ rpcCall.mockResolvedValue([]);
75
+ const model = connectedView();
76
+ await settle();
77
+ expect(model.error).toBeInstanceOf(Error);
78
+ expect(model.progress).toBe('');
79
+ });
80
+ test('Retry looks again', async () => {
81
+ rpcCall.mockResolvedValue([]);
82
+ const model = connectedView();
83
+ await settle();
84
+ const calls = rpcCall.mock.calls.length;
85
+ rpcCall.mockResolvedValue([transcript('NM_000546.6')]);
86
+ model.retryLaunch();
87
+ await settle();
88
+ expect(rpcCall.mock.calls.length).toBeGreaterThan(calls);
89
+ expect(model.connectedFeature).toMatchObject({ id: 'NM_000546.6' });
90
+ expect(model.blastParams?.proteinSequence).toBe('MKV');
91
+ });
92
+ });
93
+ describe('where a lookup searches', () => {
94
+ test('feature tracks only, on screen before the whole chromosome', async () => {
95
+ rpcCall.mockResolvedValue([]);
96
+ lgv.tracks = [
97
+ { type: 'AlignmentsTrack', adapter: 'bam' },
98
+ { type: 'FeatureTrack', adapter: 'genes' },
99
+ ];
100
+ lgv.dynamicBlocks.contentBlocks = [
101
+ {
102
+ assemblyName: 'hg38',
103
+ refName: 'chr17',
104
+ start: 7_661_778.6,
105
+ end: 7_687_538.2,
106
+ },
107
+ ];
108
+ connectedView();
109
+ await settle();
110
+ const asked = rpcCall.mock.calls.map(([, , args]) => [
111
+ args.adapterConfig,
112
+ args.regions,
113
+ ]);
114
+ expect(asked).toEqual([
115
+ [
116
+ 'genes',
117
+ [
118
+ {
119
+ assemblyName: 'hg38',
120
+ refName: 'chr17',
121
+ start: 7_661_778,
122
+ end: 7_687_539,
123
+ },
124
+ ],
125
+ ],
126
+ ['genes', lgv.displayedRegions],
127
+ ]);
128
+ });
129
+ });
130
+ describe('two lookups in flight', () => {
131
+ test('only the later one writes', async () => {
132
+ const pending = [];
133
+ rpcCall.mockImplementation(() => new Promise(res => {
134
+ pending.push(res);
135
+ }));
136
+ const model = connectedView();
137
+ await settle();
138
+ model.retryLaunch();
139
+ await settle();
140
+ expect(pending).toHaveLength(2);
141
+ pending[1]([transcript('NM_000546.6')]);
142
+ await settle();
143
+ const written = model.connectedFeature;
144
+ pending[0]([transcript('NM_000546.5')]);
145
+ await settle();
146
+ expect(written).toMatchObject({ id: 'NM_000546.6' });
147
+ expect(model.connectedFeature).toBe(written);
148
+ });
149
+ });
@@ -26,11 +26,15 @@ interface LaunchedData {
26
26
  * Run one launch attempt, owning the AbortController that ties it to the view.
27
27
  *
28
28
  * The controller lives on the model because two things end a launch and neither
29
- * is here: the Cancel button, and the disposer `afterCreate` registers. Before
30
- * it existed, closing a view mid-BLAST left the poller checking EBI for the
31
- * job's lifetime and then writing to a node that was gone.
29
+ * is here: the Cancel button, and the disposer `afterCreate` registers. A launch
30
+ * already holding it is aborted first, so a re-fired autorun replaces the
31
+ * attempt rather than orphaning one that nothing can cancel.
32
+ *
33
+ * Runs untracked: it is called from the launch autoruns, and whatever the
34
+ * launch body reads before its first await would otherwise become a reason to
35
+ * launch again.
32
36
  */
33
- export declare function runLaunch({ self, message, launch, onLaunched, }: {
37
+ export declare function runLaunch(args: {
34
38
  self: JBrowsePluginMsaViewModel;
35
39
  message: string;
36
40
  launch: (scope: LaunchScope) => Promise<LaunchedData>;