jbrowse-plugin-msaview 3.4.0 → 3.5.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 (171) hide show
  1. package/README.md +7 -0
  2. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +23 -17
  3. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
  4. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
  5. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
  6. package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  9. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  10. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
  11. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
  12. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
  13. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +10 -4
  14. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
  15. package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
  16. package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
  17. package/dist/LaunchMsaView/detectQueryRow.js +11 -4
  18. package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
  19. package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
  20. package/dist/LaunchMsaView/useQueryRowName.js +5 -1
  21. package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
  22. package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
  23. package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
  24. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  25. package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
  26. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
  27. package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
  28. package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
  29. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
  30. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
  31. package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
  32. package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
  33. package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
  34. package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
  35. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  36. package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
  37. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
  38. package/dist/MsaViewPanel/genomeToMSA.js +4 -4
  39. package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
  40. package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
  41. package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
  42. package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
  43. package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
  44. package/dist/MsaViewPanel/model.d.ts +306 -85
  45. package/dist/MsaViewPanel/model.js +169 -42
  46. package/dist/MsaViewPanel/model.test.d.ts +1 -0
  47. package/dist/MsaViewPanel/model.test.js +100 -0
  48. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
  49. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
  50. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
  51. package/dist/MsaViewPanel/msaDataStore.d.ts +7 -3
  52. package/dist/MsaViewPanel/msaDataStore.js +26 -7
  53. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  54. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  55. package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
  56. package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/processInit.test.js +72 -0
  58. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
  59. package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
  60. package/dist/MsaViewPanel/runLaunch.d.ts +39 -0
  61. package/dist/MsaViewPanel/runLaunch.js +100 -0
  62. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  63. package/dist/MsaViewPanel/runLaunch.test.js +175 -0
  64. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  65. package/dist/MsaViewPanel/storedData.test.js +160 -0
  66. package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
  67. package/dist/MsaViewPanel/structureConnection.js +0 -19
  68. package/dist/MsaViewPanel/util.d.ts +39 -0
  69. package/dist/MsaViewPanel/util.js +44 -0
  70. package/dist/jbrowse-plugin-msaview.umd.production.min.js +46 -45
  71. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  72. package/dist/utils/blastCache.js +32 -0
  73. package/dist/utils/blastCache.test.d.ts +1 -0
  74. package/dist/utils/blastCache.test.js +72 -0
  75. package/dist/utils/browserAlign.d.ts +42 -0
  76. package/dist/utils/browserAlign.js +297 -0
  77. package/dist/utils/browserAlign.test.d.ts +1 -0
  78. package/dist/utils/browserAlign.test.js +85 -0
  79. package/dist/utils/ebiBlast.d.ts +13 -2
  80. package/dist/utils/ebiBlast.js +32 -4
  81. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  82. package/dist/utils/ebiJobDispatcher.js +13 -6
  83. package/dist/utils/fetch.d.ts +8 -1
  84. package/dist/utils/fetch.js +28 -2
  85. package/dist/utils/homologSearch.d.ts +31 -0
  86. package/dist/utils/homologSearch.js +6 -0
  87. package/dist/utils/msa.d.ts +5 -11
  88. package/dist/utils/msa.js +19 -30
  89. package/dist/utils/msaRows.d.ts +11 -8
  90. package/dist/utils/msaRows.js +14 -12
  91. package/dist/utils/ncbiDomains.d.ts +2 -2
  92. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  93. package/dist/utils/ncbiOrthologs.js +26 -7
  94. package/dist/utils/ncbiOrthologs.test.js +23 -1
  95. package/dist/utils/pantherOrthologs.js +2 -10
  96. package/dist/utils/phmmer.d.ts +12 -1
  97. package/dist/utils/phmmer.js +52 -13
  98. package/dist/utils/poll.d.ts +6 -1
  99. package/dist/utils/poll.js +7 -2
  100. package/dist/utils/unirefHomologs.d.ts +94 -0
  101. package/dist/utils/unirefHomologs.js +193 -0
  102. package/dist/utils/unirefHomologs.test.d.ts +1 -0
  103. package/dist/utils/unirefHomologs.test.js +118 -0
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +17 -16
  107. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
  108. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
  109. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
  110. package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
  111. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  112. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  113. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
  114. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
  115. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +12 -5
  116. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
  117. package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
  118. package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
  119. package/src/LaunchMsaView/detectQueryRow.ts +22 -4
  120. package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
  121. package/src/LaunchMsaView/useQueryRowName.ts +5 -1
  122. package/src/LaunchMsaViewExtensionPoint/index.test.ts +74 -0
  123. package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
  124. package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
  125. package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
  126. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
  127. package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
  128. package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
  129. package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
  130. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
  131. package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
  132. package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
  133. package/src/MsaViewPanel/genomeToMSA.ts +8 -4
  134. package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
  135. package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
  136. package/src/MsaViewPanel/model.test.ts +121 -0
  137. package/src/MsaViewPanel/model.ts +200 -49
  138. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
  139. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
  140. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  141. package/src/MsaViewPanel/msaDataStore.ts +32 -13
  142. package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
  143. package/src/MsaViewPanel/processInit.test.ts +84 -0
  144. package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
  145. package/src/MsaViewPanel/runLaunch.test.ts +208 -0
  146. package/src/MsaViewPanel/runLaunch.ts +141 -0
  147. package/src/MsaViewPanel/storedData.test.ts +196 -0
  148. package/src/MsaViewPanel/structureConnection.ts +0 -26
  149. package/src/MsaViewPanel/util.ts +75 -0
  150. package/src/utils/blastCache.test.ts +86 -0
  151. package/src/utils/blastCache.ts +34 -1
  152. package/src/utils/browserAlign.test.ts +102 -0
  153. package/src/utils/browserAlign.ts +352 -0
  154. package/src/utils/ebiBlast.ts +42 -2
  155. package/src/utils/ebiJobDispatcher.ts +18 -3
  156. package/src/utils/fetch.ts +29 -2
  157. package/src/utils/homologSearch.ts +51 -0
  158. package/src/utils/msa.ts +25 -39
  159. package/src/utils/msaRows.ts +20 -16
  160. package/src/utils/ncbiDomains.ts +4 -2
  161. package/src/utils/ncbiOrthologs.test.ts +25 -0
  162. package/src/utils/ncbiOrthologs.ts +27 -7
  163. package/src/utils/pantherOrthologs.ts +6 -11
  164. package/src/utils/phmmer.ts +62 -12
  165. package/src/utils/poll.ts +8 -1
  166. package/src/utils/unirefHomologs.test.ts +149 -0
  167. package/src/utils/unirefHomologs.ts +323 -0
  168. package/src/version.ts +1 -1
  169. package/dist/MsaViewPanel/structureConnection.test.js +0 -53
  170. package/src/MsaViewPanel/structureConnection.test.ts +0 -62
  171. /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts} +0 -0
@@ -0,0 +1,23 @@
1
+ // @vitest-environment jsdom
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { useQueryRowName } from './useQueryRowName';
5
+ const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
6
+ const trimmed = `>aligned_query\n${protein.slice(5, 45)}\n>other\nWRONGWRONGWRONGWRONGWRONGWRONGWRONGWRONG\n`;
7
+ describe('useQueryRowName', () => {
8
+ test('carries the detected row and where it starts', () => {
9
+ const { result } = renderHook(() => useQueryRowName(trimmed, protein));
10
+ expect(result.current.querySeqName).toBe('aligned_query');
11
+ expect(result.current.querySeqOffset).toBe(5);
12
+ });
13
+ // the offset was measured against the detected row; another row is a row the
14
+ // user vouched for, and nothing here knows where in the protein it sits
15
+ test('a row the user picks instead is taken at face value', () => {
16
+ const { result } = renderHook(() => useQueryRowName(trimmed, protein));
17
+ act(() => {
18
+ result.current.setQuerySeqName('other');
19
+ });
20
+ expect(result.current.querySeqName).toBe('other');
21
+ expect(result.current.querySeqOffset).toBe(0);
22
+ });
23
+ });
@@ -1,22 +1,34 @@
1
1
  import { launchMsaView } from '../utils/launchMsaView';
2
2
  export default function LaunchMsaViewExtensionPointF(pluginManager) {
3
3
  pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
4
- const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
5
- // `orthologParams` is a fourth source, and unlike the other three it names
6
- // no alignment at all — the view builds one from NCBI at launch, which is
7
- // the dialog's Orthologs tab reached declaratively.
4
+ const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, searchParams, ...rest } = args;
5
+ // `orthologParams` and `searchParams` name no alignment at all — the
6
+ // view builds one at launch, which is the dialog's Orthologs and BLAST
7
+ // tabs reached declaratively.
8
8
  if (!data &&
9
9
  !msaFileLocation &&
10
10
  !msaIndexedLocation &&
11
- !rest.orthologParams) {
12
- throw new Error('No MSA data, file location or orthologParams provided when launching MSA view');
11
+ !rest.orthologParams &&
12
+ !searchParams) {
13
+ throw new Error('No MSA data, file location, orthologParams or searchParams provided when launching MSA view');
13
14
  }
14
15
  // inline data and the tree URL are native react-msaview snapshot props, set
15
16
  // directly, and so is orthologParams (the model's own autorun picks it up).
16
17
  // Only sources needing launch-time resolution go through `init`: msaUrl
17
18
  // (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
19
+ //
20
+ // An init whose every field is undefined is still a truthy object, and
21
+ // MsaViewPanel reads any init as a launch in flight, so an inline-data
22
+ // launch flashed "Loading alignment" until processInit cleared it.
23
+ const init = {
24
+ msaUrl: msaFileLocation?.uri,
25
+ msaIndexedLocation,
26
+ msaName,
27
+ querySeqName,
28
+ };
18
29
  launchMsaView(session, {
19
30
  ...rest,
31
+ ...(searchParams ? { blastParams: searchParams } : {}),
20
32
  data,
21
33
  ...(treeFileLocation
22
34
  ? {
@@ -26,12 +38,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
26
38
  },
27
39
  }
28
40
  : {}),
29
- init: {
30
- msaUrl: msaFileLocation?.uri,
31
- msaIndexedLocation,
32
- msaName,
33
- querySeqName,
34
- },
41
+ ...(Object.values(init).some(v => v !== undefined) ? { init } : {}),
35
42
  });
36
43
  return args;
37
44
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { expect, test } from 'vitest';
2
+ import LaunchMsaViewExtensionPointF from './index';
3
+ function launch(args) {
4
+ let run;
5
+ LaunchMsaViewExtensionPointF({
6
+ addToExtensionPoint(_name, cb) {
7
+ run = cb;
8
+ },
9
+ });
10
+ const added = [];
11
+ const session = {
12
+ addView(_type, snapshot) {
13
+ added.push(snapshot);
14
+ return { id: 'view-1' };
15
+ },
16
+ };
17
+ run({ session, ...args });
18
+ return added[0];
19
+ }
20
+ test('inline data carries no init, which the panel would read as a launch in flight', () => {
21
+ const snapshot = launch({ data: { msa: '>a\nMEEP' } });
22
+ expect('init' in snapshot).toBe(false);
23
+ });
24
+ test('a file location travels through init', () => {
25
+ const snapshot = launch({
26
+ msaFileLocation: { uri: 'http://example.com/a.fa' },
27
+ });
28
+ expect(snapshot.init).toEqual({
29
+ msaUrl: 'http://example.com/a.fa',
30
+ msaIndexedLocation: undefined,
31
+ msaName: undefined,
32
+ querySeqName: undefined,
33
+ });
34
+ });
35
+ test('searchParams is a source, stored under the name the dialog uses', () => {
36
+ const snapshot = launch({
37
+ searchParams: {
38
+ searchProgram: 'phmmer',
39
+ blastDatabase: 'rp15',
40
+ accession: 'P04637',
41
+ },
42
+ connectedViewId: 'lgv1',
43
+ connectedTranscript: 'NM_000546.6',
44
+ });
45
+ expect(snapshot.blastParams).toEqual({
46
+ searchProgram: 'phmmer',
47
+ blastDatabase: 'rp15',
48
+ accession: 'P04637',
49
+ });
50
+ expect('searchParams' in snapshot).toBe(false);
51
+ expect(snapshot.connectedTranscript).toBe('NM_000546.6');
52
+ });
53
+ test('a launch naming no source at all is refused', () => {
54
+ expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
55
+ });
56
+ test('one field set is enough to need init', () => {
57
+ const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
58
+ expect(snapshot.init).toEqual({
59
+ msaUrl: undefined,
60
+ msaIndexedLocation: undefined,
61
+ msaName: undefined,
62
+ querySeqName: 'QUERY',
63
+ });
64
+ });
@@ -1,5 +1,21 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
2
  export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
3
+ /**
4
+ * Keep IndexedDB holding what the view holds. The first run writes a new row and
5
+ * records its id in the session snapshot; later runs update that row, because
6
+ * the alignment keeps changing after it arrives — react-msaview's
7
+ * "calculate neighbor-joining tree" replaces `data.tree` — and a session
8
+ * reopened against a stale row restores the wrong picture.
9
+ *
10
+ * `lastStoredData` is what makes that safe to run on every data change: it is
11
+ * the only thing separating "this is new" from "this is what we just wrote", and
12
+ * it is recorded whether or not the write succeeded, so a browser refusing
13
+ * IndexedDB (private mode) fails once rather than in a loop.
14
+ *
15
+ * A view whose data comes from a filehandle -- or from the indexed block its
16
+ * kept `init` names -- stores nothing at all: the file is the source of truth
17
+ * and it is refetched at startup.
18
+ */
3
19
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
4
20
  /**
5
21
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
@@ -1,12 +1,15 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
+ import { transaction } from 'mobx';
2
3
  import { doLaunchBlast } from './doLaunchBlast';
3
4
  import { doLaunchOrthologs } from './doLaunchOrthologs';
4
5
  import { fetchIndexedMsa } from './fetchIndexedMsa';
5
6
  import { genomeToMSA } from './genomeToMSA';
6
7
  import { loadProteinDomains } from './loadProteinDomains';
7
8
  import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
9
+ import { runLaunch } from './runLaunch';
8
10
  import { getProteinViews } from './structureConnection';
9
- import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
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.";
10
13
  export function loadStoredData(self) {
11
14
  const { dataStoreId, rows } = self;
12
15
  if (dataStoreId && rows.length === 0) {
@@ -15,15 +18,28 @@ export function loadStoredData(self) {
15
18
  self.setLoadingStoredData(true);
16
19
  const storedData = await retrieveMsaData(dataStoreId);
17
20
  if (storedData) {
18
- if (storedData.msa) {
19
- self.setMSA(storedData.msa);
20
- }
21
- if (storedData.tree) {
22
- self.setTree(storedData.tree);
23
- }
24
- if (storedData.treeMetadata) {
25
- self.setTreeMetadata(storedData.treeMetadata);
26
- }
21
+ // one transaction, so storeDataToIndexedDB sees the restored data and
22
+ // the record of it together and does not write it straight back
23
+ transaction(() => {
24
+ if (storedData.msa) {
25
+ self.setMSA(storedData.msa);
26
+ }
27
+ if (storedData.tree) {
28
+ self.setTree(storedData.tree);
29
+ }
30
+ if (storedData.treeMetadata) {
31
+ self.setTreeMetadata(storedData.treeMetadata);
32
+ }
33
+ self.setLastStoredData(currentData(self));
34
+ });
35
+ }
36
+ else {
37
+ // 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
39
+ transaction(() => {
40
+ self.setDataStoreId(undefined);
41
+ self.setError(new Error(EXPIRED_MESSAGE));
42
+ });
27
43
  }
28
44
  }
29
45
  catch (e) {
@@ -35,40 +51,67 @@ export function loadStoredData(self) {
35
51
  })();
36
52
  }
37
53
  }
54
+ function currentData(self) {
55
+ const { msa, tree, treeMetadata } = self.data;
56
+ return { msa, tree, treeMetadata };
57
+ }
58
+ function sameData(a, b) {
59
+ return (!!a &&
60
+ a.msa === b.msa &&
61
+ a.tree === b.tree &&
62
+ a.treeMetadata === b.treeMetadata);
63
+ }
64
+ /**
65
+ * Keep IndexedDB holding what the view holds. The first run writes a new row and
66
+ * records its id in the session snapshot; later runs update that row, because
67
+ * the alignment keeps changing after it arrives — react-msaview's
68
+ * "calculate neighbor-joining tree" replaces `data.tree` — and a session
69
+ * reopened against a stale row restores the wrong picture.
70
+ *
71
+ * `lastStoredData` is what makes that safe to run on every data change: it is
72
+ * the only thing separating "this is new" from "this is what we just wrote", and
73
+ * it is recorded whether or not the write succeeded, so a browser refusing
74
+ * IndexedDB (private mode) fails once rather than in a loop.
75
+ *
76
+ * A view whose data comes from a filehandle -- or from the indexed block its
77
+ * kept `init` names -- stores nothing at all: the file is the source of truth
78
+ * and it is refetched at startup.
79
+ */
38
80
  export function storeDataToIndexedDB(self) {
39
- const { rows, dataStoreId, isStoringData } = self;
40
- if (rows.length > 0 && !dataStoreId && !isStoringData) {
41
- if (self.msaFilehandle || self.treeFilehandle) {
42
- return;
81
+ const { rows, dataStoreId, isStoringData, lastStoredData, init } = self;
82
+ const data = currentData(self);
83
+ if (rows.length === 0 ||
84
+ isStoringData ||
85
+ self.msaFilehandle ||
86
+ self.treeFilehandle ||
87
+ // an indexed view keeps its init and refetches the block, so a row here
88
+ // would be one nothing ever reads
89
+ !!init?.msaIndexedLocation ||
90
+ !(data.msa || data.tree) ||
91
+ sameData(lastStoredData, data)) {
92
+ return;
93
+ }
94
+ // mark as storing synchronously so re-runs of this autorun (e.g. when data
95
+ // observables change while the write is pending) don't kick off a duplicate
96
+ // write and leave an orphan IndexedDB entry
97
+ self.setIsStoringData(true);
98
+ void (async () => {
99
+ try {
100
+ const id = dataStoreId ?? generateDataStoreId();
101
+ if (await storeMsaData(id, data)) {
102
+ self.setDataStoreId(id);
103
+ }
43
104
  }
44
- const msaData = self.data.msa;
45
- const treeData = self.data.tree;
46
- if (msaData || treeData) {
47
- // mark as storing synchronously so re-runs of this autorun (e.g. when
48
- // data observables change while the write is pending) don't kick off a
49
- // duplicate write and leave an orphan IndexedDB entry
50
- self.setIsStoringData(true);
51
- void (async () => {
52
- try {
53
- const newId = generateDataStoreId();
54
- const success = await storeMsaData(newId, {
55
- msa: msaData,
56
- tree: treeData,
57
- treeMetadata: self.data.treeMetadata,
58
- });
59
- if (success) {
60
- self.setDataStoreId(newId);
61
- }
62
- }
63
- catch (e) {
64
- console.error('Failed to store MSA data to IndexedDB:', e);
65
- }
66
- finally {
67
- self.setIsStoringData(false);
68
- }
69
- })();
105
+ catch (e) {
106
+ console.error('Failed to store MSA data to IndexedDB:', e);
70
107
  }
71
- }
108
+ finally {
109
+ transaction(() => {
110
+ self.setLastStoredData(data);
111
+ self.setIsStoringData(false);
112
+ });
113
+ }
114
+ })();
72
115
  }
73
116
  /**
74
117
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
@@ -78,43 +121,36 @@ export function storeDataToIndexedDB(self) {
78
121
  * until a new request replaces them.
79
122
  */
80
123
  export function launchOrthologsIfNeeded(self) {
81
- if (self.orthologParams) {
82
- void (async () => {
83
- try {
84
- self.setProgress('Resolving orthologs');
85
- self.setError(undefined);
86
- const data = await doLaunchOrthologs({ self });
87
- self.setData(data);
124
+ if (self.orthologParams && !awaitingTranscript(self)) {
125
+ runLaunch({
126
+ self,
127
+ message: 'Resolving orthologs',
128
+ launch: scope => doLaunchOrthologs({ self, scope }),
129
+ onLaunched: () => {
88
130
  self.setOrthologParams(undefined);
89
- }
90
- catch (e) {
91
- self.setError(e);
92
- console.error(e);
93
- }
94
- finally {
95
- self.setProgress('');
96
- }
97
- })();
131
+ },
132
+ });
98
133
  }
99
134
  }
135
+ /**
136
+ * A launch that names its transcript rather than its feature has to wait for
137
+ * the lookup: the query row is that transcript's translation, and the launch
138
+ * cannot start without a query. Both launchers read the same two fields, so
139
+ * the resolver setting `connectedFeature` is what refires them.
140
+ */
141
+ function awaitingTranscript(self) {
142
+ return !!self.connectedTranscript && !self.connectedFeature;
143
+ }
100
144
  export function launchBlastIfNeeded(self) {
101
- if (self.blastParams) {
102
- void (async () => {
103
- try {
104
- self.setProgress('Submitting query');
105
- self.setError(undefined);
106
- const data = await doLaunchBlast({ self });
107
- self.setData(data);
145
+ if (self.blastParams && !awaitingTranscript(self)) {
146
+ runLaunch({
147
+ self,
148
+ message: 'Submitting query',
149
+ launch: scope => doLaunchBlast({ self, scope }),
150
+ onLaunched: () => {
108
151
  self.setBlastParams(undefined);
109
- }
110
- catch (e) {
111
- self.setError(e);
112
- console.error(e);
113
- }
114
- finally {
115
- self.setProgress('');
116
- }
117
- })();
152
+ },
153
+ });
118
154
  }
119
155
  }
120
156
  /**
@@ -144,15 +180,28 @@ export function autoLoadProteinDomains(self) {
144
180
  })();
145
181
  }
146
182
  }
147
- // Resolve the declarative `init` launch contract once, then clear it. msaUrl is
148
- // handed to react-msaview's native filehandle loader (openLocation + progress +
149
- // abort + CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip
150
- // name-indexed block is the one source with no native loader, so it's fetched
151
- // here. Inline data and tree URLs arrive as native snapshot props, not via init.
183
+ // Resolve the declarative `init` launch contract. msaUrl is handed to
184
+ // react-msaview's native filehandle loader (openLocation + progress + abort +
185
+ // CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip name-indexed
186
+ // block is the one source with no native loader, so it's fetched here. Inline
187
+ // data and tree URLs arrive as native snapshot props, not via init.
188
+ //
189
+ // An init that named the indexed block is KEPT rather than cleared, as
190
+ // jbrowse-plugin-tview keeps its own: what it resolves to is one alignment
191
+ // string, react-msaview drops a document over 50kb from the snapshot, and there
192
+ // is no filehandle to reload it from -- so a shared session came back saying the
193
+ // alignment had expired. The init is both smaller than what it fetches and the
194
+ // only durable statement of what the view is.
152
195
  export function processInit(self) {
153
196
  const { init } = self;
154
197
  if (init) {
155
198
  const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
199
+ const indexed = !!(msaIndexedLocation && msaName);
200
+ // a kept init re-runs this on every session restore; the alignment already
201
+ // in hand is the one it would fetch
202
+ if (indexed && self.data.msa) {
203
+ return;
204
+ }
156
205
  void (async () => {
157
206
  try {
158
207
  self.setError(undefined);
@@ -181,7 +230,9 @@ export function processInit(self) {
181
230
  throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
182
231
  }
183
232
  }
184
- self.setInit(undefined);
233
+ if (!indexed) {
234
+ self.setInit(undefined);
235
+ }
185
236
  }
186
237
  catch (e) {
187
238
  self.setError(e);
@@ -215,10 +266,10 @@ export function syncGenomeHoverToMsaColumn(self) {
215
266
  * Translate genome regions published by a 3D protein view into this MSA's
216
267
  * visible columns. The genome is the only coordinate space the two plugins
217
268
  * share, so the hops are genome coord -> protein position (the transcript's g2p
218
- * map) -> global alignment column -> visible column.
269
+ * map) -> visible column.
219
270
  */
220
271
  function genomeHighlightsToVisibleColumns(self, field) {
221
- const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
272
+ const { connectedViewId, transcriptToMsaMap } = self;
222
273
  if (!transcriptToMsaMap || !hasQueryRow(self)) {
223
274
  return [];
224
275
  }
@@ -232,16 +283,17 @@ function genomeHighlightsToVisibleColumns(self, field) {
232
283
  for (const highlight of structure[field] ?? []) {
233
284
  for (let coord = highlight.start; coord < highlight.end; coord++) {
234
285
  const proteinPos = g2p[coord];
235
- if (proteinPos !== undefined) {
236
- columns.add(self.seqPosToGlobalCol(querySeqName, proteinPos));
286
+ const col = proteinPos === undefined
287
+ ? undefined
288
+ : transcriptPosToVisibleCol(self, proteinPos);
289
+ if (col !== undefined) {
290
+ columns.add(col);
237
291
  }
238
292
  }
239
293
  }
240
294
  }
241
295
  }
242
- return [...columns]
243
- .map(col => self.globalColToVisibleCol(col))
244
- .filter((col) => col !== undefined);
296
+ return [...columns];
245
297
  }
246
298
  function sameColumns(a, b) {
247
299
  if (!a || !b) {
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
3
- import { Typography } from '@mui/material';
3
+ import { Button, Typography } from '@mui/material';
4
4
  import { observer } from 'mobx-react';
5
5
  import { makeStyles } from 'tss-react/mui';
6
6
  import JobLink from './JobLink';
@@ -8,6 +8,11 @@ const useStyles = makeStyles()({
8
8
  margin: {
9
9
  padding: 20,
10
10
  },
11
+ progressRow: {
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ gap: 12,
15
+ },
11
16
  });
12
17
  /**
13
18
  * What a view shows while it is still building its alignment, and what it shows
@@ -24,18 +29,31 @@ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
24
29
  const { blastParams, orthologParams, progress, rid, error } = model;
25
30
  const { classes } = useStyles();
26
31
  const message = blastParams
27
- ? 'Running EBI BLAST'
32
+ ? `Running EBI ${blastParams.searchProgram === 'phmmer' ? 'phmmer' : 'BLAST'}`
28
33
  : orthologParams
29
- ? 'Building ortholog alignment'
34
+ ? orthologParams.source === 'uniref'
35
+ ? 'Building UniRef homolog alignment'
36
+ : 'Building ortholog alignment'
30
37
  : 'Loading alignment';
31
38
  return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
32
39
  React.createElement(Typography, { variant: "h5" },
33
40
  message,
34
41
  " failed"),
35
42
  rid ? React.createElement(JobLink, { jobId: rid }) : null,
36
- React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
43
+ React.createElement(ErrorMessage, { error: error }),
44
+ React.createElement("div", { className: classes.progressRow },
45
+ React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
46
+ model.retryLaunch();
47
+ } }, "Retry"),
48
+ React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
49
+ model.cancelLaunch();
50
+ } }, "Dismiss")))) : (React.createElement(React.Fragment, null,
37
51
  React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
38
52
  rid ? React.createElement(JobLink, { jobId: rid }) : null,
39
- React.createElement(Typography, null, progress || 'Initializing')))));
53
+ React.createElement("div", { className: classes.progressRow },
54
+ React.createElement(Typography, null, progress || 'Initializing'),
55
+ React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
56
+ model.cancelLaunch();
57
+ } }, "Cancel"))))));
40
58
  });
41
59
  export default LaunchProgress;
@@ -14,8 +14,12 @@ 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 launching = !!(blastParams ??
21
+ orthologParams ??
22
+ (init && !model.dataInitialized));
19
23
  return (React.createElement(ErrorBoundary, null,
20
24
  React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
21
25
  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' },
@@ -57,6 +67,36 @@ test('a failed init shows why', () => {
57
67
  expect(screen.queryByText('the alignment')).toBeNull();
58
68
  expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
59
69
  });
70
+ // a launch runs for 10+ minutes, so leaving with no way out means watching it
71
+ test('a running launch offers a way out', () => {
72
+ const cancelLaunch = vi.fn();
73
+ panel({
74
+ blastParams: { proteinSequence: 'MKV' },
75
+ progress: 'Re-checking BLAST status in... 7',
76
+ cancelLaunch,
77
+ });
78
+ screen.getByRole('button', { name: 'Cancel' }).click();
79
+ expect(cancelLaunch).toHaveBeenCalled();
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();
87
+ panel({
88
+ blastParams: { proteinSequence: 'MKV' },
89
+ progress: '',
90
+ error: new Error('No hits found'),
91
+ cancelLaunch,
92
+ retryLaunch,
93
+ });
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
+ });
60
100
  test('a running job links out to it', () => {
61
101
  panel({
62
102
  blastParams: { proteinSequence: 'MKV' },
@@ -1,6 +1,16 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
- export declare function doLaunchBlast({ self, }: {
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
+ */
11
+ export declare function doLaunchBlast({ self, scope, }: {
3
12
  self: JBrowsePluginMsaViewModel;
13
+ scope: LaunchScope;
4
14
  }): Promise<{
5
15
  msa: string;
6
16
  tree: string;