jbrowse-plugin-msaview 3.3.0 → 3.4.1

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 (149) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  15. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  16. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
  17. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  18. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  19. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
  20. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  21. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  22. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  23. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  24. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  25. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  26. package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
  27. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  28. package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
  29. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
  30. package/dist/MsaViewPanel/afterCreateAutoruns.js +95 -72
  31. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  32. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  33. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  34. package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
  35. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  36. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  37. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
  38. package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
  39. package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
  40. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  41. package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
  42. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
  43. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  44. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  45. package/dist/MsaViewPanel/model.d.ts +190 -19
  46. package/dist/MsaViewPanel/model.js +51 -1
  47. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  48. package/dist/MsaViewPanel/msaDataStore.js +16 -7
  49. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  50. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  51. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  52. package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
  53. package/dist/MsaViewPanel/runLaunch.js +65 -0
  54. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  55. package/dist/MsaViewPanel/runLaunch.test.js +129 -0
  56. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/storedData.test.js +160 -0
  58. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  59. package/dist/MsaViewPanel/util.d.ts +18 -0
  60. package/dist/MsaViewPanel/util.js +17 -0
  61. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  62. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  63. package/dist/utils/blastCache.d.ts +10 -6
  64. package/dist/utils/blastCache.js +47 -3
  65. package/dist/utils/blastCache.test.d.ts +1 -0
  66. package/dist/utils/blastCache.test.js +72 -0
  67. package/dist/utils/ebiBlast.d.ts +5 -3
  68. package/dist/utils/ebiBlast.js +6 -4
  69. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  70. package/dist/utils/ebiJobDispatcher.js +13 -6
  71. package/dist/utils/fetch.d.ts +8 -1
  72. package/dist/utils/fetch.js +28 -2
  73. package/dist/utils/msa.d.ts +15 -1
  74. package/dist/utils/msa.js +40 -13
  75. package/dist/utils/msaRows.d.ts +31 -0
  76. package/dist/utils/msaRows.js +67 -0
  77. package/dist/utils/ncbiDomains.d.ts +2 -2
  78. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  79. package/dist/utils/ncbiOrthologs.js +26 -7
  80. package/dist/utils/ncbiOrthologs.test.js +23 -1
  81. package/dist/utils/pantherOrthologs.js +2 -10
  82. package/dist/utils/phmmer.d.ts +54 -0
  83. package/dist/utils/phmmer.js +120 -0
  84. package/dist/utils/poll.d.ts +6 -1
  85. package/dist/utils/poll.js +7 -2
  86. package/dist/utils/taxonomyNames.d.ts +1 -1
  87. package/dist/utils/taxonomyNames.js +6 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +30 -24
  91. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  92. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  93. package/src/AddHighlightModel/index.tsx +1 -1
  94. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
  95. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  96. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  97. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  98. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  99. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  100. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  101. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  102. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  103. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  104. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
  105. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  106. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  107. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  108. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  109. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  110. package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
  111. package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
  112. package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
  113. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  114. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  115. package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
  116. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
  117. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  118. package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
  119. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
  120. package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
  121. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  122. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  123. package/src/MsaViewPanel/model.ts +82 -6
  124. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  125. package/src/MsaViewPanel/msaDataStore.ts +22 -13
  126. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  127. package/src/MsaViewPanel/runLaunch.test.ts +154 -0
  128. package/src/MsaViewPanel/runLaunch.ts +102 -0
  129. package/src/MsaViewPanel/storedData.test.ts +196 -0
  130. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  131. package/src/MsaViewPanel/util.ts +18 -0
  132. package/src/utils/blastCache.test.ts +86 -0
  133. package/src/utils/blastCache.ts +67 -13
  134. package/src/utils/ebiBlast.ts +9 -3
  135. package/src/utils/ebiJobDispatcher.ts +18 -3
  136. package/src/utils/fetch.ts +29 -2
  137. package/src/utils/msa.ts +51 -12
  138. package/src/utils/msaRows.ts +95 -0
  139. package/src/utils/ncbiDomains.ts +4 -2
  140. package/src/utils/ncbiOrthologs.test.ts +25 -0
  141. package/src/utils/ncbiOrthologs.ts +27 -7
  142. package/src/utils/pantherOrthologs.ts +6 -11
  143. package/src/utils/phmmer.ts +178 -0
  144. package/src/utils/poll.ts +8 -1
  145. package/src/utils/taxonomyNames.ts +6 -1
  146. package/src/version.ts +1 -1
  147. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  148. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  149. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { detectQueryRow, getMsaRowNames } from './detectQueryRow';
2
+ import { findQueryRow } from './detectQueryRow';
3
3
  const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
4
4
  // COBALT renames the query `Query_1`, so only the residues identify it
5
5
  const clustal = `CLUSTAL W (1.81) multiple sequence alignment
@@ -13,22 +13,22 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
13
13
  >sp|P02769|ALBU_BOVIN
14
14
  MKWVTFISLLLLFSSAYSRG--RRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
15
15
  `;
16
- describe('detectQueryRow', () => {
16
+ describe('findQueryRow', () => {
17
17
  test('finds the query by sequence when the aligner renamed it', () => {
18
- expect(detectQueryRow(clustal, protein)).toMatchObject({
18
+ expect(findQueryRow(clustal, protein).match).toMatchObject({
19
19
  name: 'Query_1',
20
20
  quality: 'exact',
21
21
  });
22
22
  });
23
23
  test('ignores gaps in the aligned row', () => {
24
- expect(detectQueryRow(fasta, protein)?.name).toBe('Query_1');
24
+ expect(findQueryRow(fasta, protein).match?.name).toBe('Query_1');
25
25
  });
26
26
  test('tolerates the trailing stop codon the translation carries', () => {
27
- expect(detectQueryRow(clustal, `${protein}*`)?.name).toBe('Query_1');
27
+ expect(findQueryRow(clustal, `${protein}*`).match?.name).toBe('Query_1');
28
28
  });
29
29
  test('matches a row that is the query trimmed to the aligned region', () => {
30
30
  const trimmed = `>hit_one\nWRONGWRONGWRONGWRONG\n>aligned_query\n${protein.slice(5, 40)}\n`;
31
- expect(detectQueryRow(trimmed, protein)).toMatchObject({
31
+ expect(findQueryRow(trimmed, protein).match).toMatchObject({
32
32
  name: 'aligned_query',
33
33
  quality: 'partial',
34
34
  });
@@ -41,25 +41,25 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
41
41
  >hit_two
42
42
  MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
43
43
  `;
44
- expect(detectQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW')).toBeUndefined();
44
+ expect(findQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW').match).toBeUndefined();
45
45
  });
46
46
  test('returns nothing rather than throwing on a half-pasted alignment', () => {
47
- expect(detectQueryRow('>partial\nMKWV', protein)).toBeUndefined();
48
- expect(detectQueryRow('not an alignment at all', protein)).toBeUndefined();
49
- expect(detectQueryRow('', protein)).toBeUndefined();
50
- expect(detectQueryRow(clustal, '')).toBeUndefined();
47
+ expect(findQueryRow('>partial\nMKWV', protein).match).toBeUndefined();
48
+ expect(findQueryRow('not an alignment at all', protein).match).toBeUndefined();
49
+ expect(findQueryRow('', protein).match).toBeUndefined();
50
+ expect(findQueryRow(clustal, '').match).toBeUndefined();
51
51
  });
52
52
  });
53
- describe('getMsaRowNames', () => {
53
+ describe('findQueryRow row names', () => {
54
54
  test('lists the rows for the override dropdown', () => {
55
- expect(getMsaRowNames(clustal)).toEqual([
55
+ expect(findQueryRow(clustal, protein).names).toEqual([
56
56
  'Query_1',
57
57
  'sp|P02769|ALBU',
58
58
  'sp|Q5XLE4|OTHE',
59
59
  ]);
60
60
  });
61
61
  test('is empty rather than throwing while the user is still pasting', () => {
62
- expect(getMsaRowNames('CLUSTAL W')).toEqual([]);
63
- expect(getMsaRowNames('')).toEqual([]);
62
+ expect(findQueryRow('CLUSTAL W', protein).names).toEqual([]);
63
+ expect(findQueryRow('', protein).names).toEqual([]);
64
64
  });
65
65
  });
@@ -1,5 +1,5 @@
1
1
  import { useMemo, useState } from 'react';
2
- import { detectQueryRow, getMsaRowNames } from './detectQueryRow';
2
+ import { findQueryRow } from './detectQueryRow';
3
3
  /**
4
4
  * The MSA row name to launch with, found by sequence rather than typed.
5
5
  *
@@ -12,15 +12,12 @@ export function useQueryRowName(msaText, proteinSequence) {
12
12
  const [override, setOverride] = useState();
13
13
  // parsing runs on every keystroke in the paste box otherwise, and an
14
14
  // alignment of a few hundred rows is not free
15
- const { detected, names } = useMemo(() => ({
16
- detected: detectQueryRow(msaText, proteinSequence),
17
- names: getMsaRowNames(msaText),
18
- }), [msaText, proteinSequence]);
15
+ const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
19
16
  return {
20
- detected,
17
+ detected: match,
21
18
  names,
22
- querySeqName: override ?? detected?.name ?? '',
19
+ querySeqName: override ?? match?.name ?? '',
23
20
  setQuerySeqName: setOverride,
24
- isAutoDetected: override === undefined && !!detected,
21
+ isAutoDetected: override === undefined && !!match,
25
22
  };
26
23
  }
@@ -15,6 +15,16 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
15
15
  // directly, and so is orthologParams (the model's own autorun picks it up).
16
16
  // Only sources needing launch-time resolution go through `init`: msaUrl
17
17
  // (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
18
+ //
19
+ // An init whose every field is undefined is still a truthy object, and
20
+ // MsaViewPanel reads any init as a launch in flight, so an inline-data
21
+ // launch flashed "Loading alignment" until processInit cleared it.
22
+ const init = {
23
+ msaUrl: msaFileLocation?.uri,
24
+ msaIndexedLocation,
25
+ msaName,
26
+ querySeqName,
27
+ };
18
28
  launchMsaView(session, {
19
29
  ...rest,
20
30
  data,
@@ -26,12 +36,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
26
36
  },
27
37
  }
28
38
  : {}),
29
- init: {
30
- msaUrl: msaFileLocation?.uri,
31
- msaIndexedLocation,
32
- msaName,
33
- querySeqName,
34
- },
39
+ ...(Object.values(init).some(v => v !== undefined) ? { init } : {}),
35
40
  });
36
41
  return args;
37
42
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
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('one field set is enough to need init', () => {
36
+ const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
37
+ expect(snapshot.init).toEqual({
38
+ msaUrl: undefined,
39
+ msaIndexedLocation: undefined,
40
+ msaName: undefined,
41
+ querySeqName: 'QUERY',
42
+ });
43
+ });
@@ -1,5 +1,20 @@
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 stores nothing at all: the file is
16
+ * the source of truth and react-msaview refetches it at startup.
17
+ */
3
18
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
4
19
  /**
5
20
  * 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 } from './util';
11
+ import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } 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,63 @@ 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 stores nothing at all: the file is
77
+ * the source of truth and react-msaview refetches it at startup.
78
+ */
38
79
  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;
80
+ const { rows, dataStoreId, isStoringData, lastStoredData } = self;
81
+ const data = currentData(self);
82
+ if (rows.length === 0 ||
83
+ isStoringData ||
84
+ self.msaFilehandle ||
85
+ self.treeFilehandle ||
86
+ !(data.msa || data.tree) ||
87
+ sameData(lastStoredData, data)) {
88
+ return;
89
+ }
90
+ // mark as storing synchronously so re-runs of this autorun (e.g. when data
91
+ // observables change while the write is pending) don't kick off a duplicate
92
+ // write and leave an orphan IndexedDB entry
93
+ self.setIsStoringData(true);
94
+ void (async () => {
95
+ try {
96
+ const id = dataStoreId ?? generateDataStoreId();
97
+ if (await storeMsaData(id, data)) {
98
+ self.setDataStoreId(id);
99
+ }
43
100
  }
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
- })();
101
+ catch (e) {
102
+ console.error('Failed to store MSA data to IndexedDB:', e);
70
103
  }
71
- }
104
+ finally {
105
+ transaction(() => {
106
+ self.setLastStoredData(data);
107
+ self.setIsStoringData(false);
108
+ });
109
+ }
110
+ })();
72
111
  }
73
112
  /**
74
113
  * Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
@@ -79,42 +118,26 @@ export function storeDataToIndexedDB(self) {
79
118
  */
80
119
  export function launchOrthologsIfNeeded(self) {
81
120
  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);
121
+ runLaunch({
122
+ self,
123
+ message: 'Resolving orthologs',
124
+ launch: scope => doLaunchOrthologs({ self, scope }),
125
+ onLaunched: () => {
88
126
  self.setOrthologParams(undefined);
89
- }
90
- catch (e) {
91
- self.setError(e);
92
- console.error(e);
93
- }
94
- finally {
95
- self.setProgress('');
96
- }
97
- })();
127
+ },
128
+ });
98
129
  }
99
130
  }
100
131
  export function launchBlastIfNeeded(self) {
101
132
  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);
133
+ runLaunch({
134
+ self,
135
+ message: 'Submitting query',
136
+ launch: scope => doLaunchBlast({ self, scope }),
137
+ onLaunched: () => {
108
138
  self.setBlastParams(undefined);
109
- }
110
- catch (e) {
111
- self.setError(e);
112
- console.error(e);
113
- }
114
- finally {
115
- self.setProgress('');
116
- }
117
- })();
139
+ },
140
+ });
118
141
  }
119
142
  }
120
143
  /**
@@ -219,7 +242,7 @@ export function syncGenomeHoverToMsaColumn(self) {
219
242
  */
220
243
  function genomeHighlightsToVisibleColumns(self, field) {
221
244
  const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
222
- if (!transcriptToMsaMap) {
245
+ if (!transcriptToMsaMap || !hasQueryRow(self)) {
223
246
  return [];
224
247
  }
225
248
  const { g2p } = transcriptToMsaMap;
@@ -1,5 +1,5 @@
1
- import type { ReactNode } from 'react';
2
1
  import React, { Component } from 'react';
2
+ import type { ReactNode } from 'react';
3
3
  interface Props {
4
4
  children: ReactNode;
5
5
  }
@@ -14,6 +14,6 @@ export declare class ErrorBoundary extends Component<Props, State> {
14
14
  error: unknown;
15
15
  };
16
16
  componentDidCatch(error: unknown, info: React.ErrorInfo): void;
17
- render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
17
+ render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
18
18
  }
19
19
  export {};
@@ -2,12 +2,18 @@ import React from 'react';
2
2
  import { Typography } from '@mui/material';
3
3
  import ExternalLink from '../../components/ExternalLink';
4
4
  import { ebiBlastResultUrl } from '../../utils/ebiBlast';
5
+ import { isPhmmerJobId, phmmerResultUrl } from '../../utils/phmmer';
5
6
  function JobLink({ jobId }) {
7
+ // read off the job id rather than the launch params, so a link rebuilt for an
8
+ // old cached job still points at the tool that actually ran it
9
+ const url = isPhmmerJobId(jobId)
10
+ ? phmmerResultUrl(jobId)
11
+ : ebiBlastResultUrl(jobId);
6
12
  return (React.createElement(Typography, null,
7
13
  "Job ",
8
14
  jobId,
9
15
  " (",
10
- React.createElement(ExternalLink, { href: ebiBlastResultUrl(jobId) }, "see status"),
16
+ React.createElement(ExternalLink, { href: url }, "see status"),
11
17
  ")"));
12
18
  }
13
19
  export default JobLink;
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import type { JBrowsePluginMsaViewModel } from '../model';
3
+ /**
4
+ * What a view shows while it is still building its alignment, and what it shows
5
+ * when that fails.
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
10
+ * readable here. This used to key on `blastParams` alone, which left an ortholog
11
+ * launch rendering an empty MSAView for the minutes its alignment takes and, on
12
+ * failure, forever: the error was set and nothing drew it.
13
+ */
14
+ declare const LaunchProgress: ({ model, }: {
15
+ model: JBrowsePluginMsaViewModel;
16
+ }) => React.JSX.Element;
17
+ export default LaunchProgress;
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
3
+ import { Button, Typography } from '@mui/material';
4
+ import { observer } from 'mobx-react';
5
+ import { makeStyles } from 'tss-react/mui';
6
+ import JobLink from './JobLink';
7
+ const useStyles = makeStyles()({
8
+ margin: {
9
+ padding: 20,
10
+ },
11
+ progressRow: {
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ gap: 12,
15
+ },
16
+ });
17
+ /**
18
+ * What a view shows while it is still building its alignment, and what it shows
19
+ * when that fails.
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
24
+ * readable here. This used to key on `blastParams` alone, which left an ortholog
25
+ * launch rendering an empty MSAView for the minutes its alignment takes and, on
26
+ * failure, forever: the error was set and nothing drew it.
27
+ */
28
+ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
29
+ const { blastParams, orthologParams, progress, rid, error } = model;
30
+ const { classes } = useStyles();
31
+ const message = blastParams
32
+ ? 'Running EBI BLAST'
33
+ : orthologParams
34
+ ? 'Building ortholog alignment'
35
+ : 'Loading alignment';
36
+ return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
37
+ React.createElement(Typography, { variant: "h5" },
38
+ message,
39
+ " failed"),
40
+ rid ? React.createElement(JobLink, { jobId: rid }) : null,
41
+ React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
42
+ React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
43
+ rid ? React.createElement(JobLink, { jobId: rid }) : null,
44
+ React.createElement("div", { className: classes.progressRow },
45
+ React.createElement(Typography, null, progress || 'Initializing'),
46
+ React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
47
+ model.cancelLaunch();
48
+ } }, "Cancel"))))));
49
+ });
50
+ export default LaunchProgress;
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
4
4
  import { MSAView } from 'react-msaview';
5
5
  import { makeStyles } from 'tss-react/mui';
6
6
  import { ErrorBoundary } from './ErrorBoundary';
7
- import LoadingBLAST from './LoadingBLAST';
7
+ import LaunchProgress from './LaunchProgress';
8
8
  const useStyles = makeStyles()({
9
9
  loadingContainer: {
10
10
  padding: 20,
@@ -12,9 +12,12 @@ const useStyles = makeStyles()({
12
12
  });
13
13
  const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
14
14
  const { classes } = useStyles();
15
- const { blastParams, loadingStoredData } = model;
15
+ const { blastParams, orthologParams, init, loadingStoredData } = model;
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);
16
19
  return (React.createElement(ErrorBoundary, null,
17
- React.createElement("div", null, blastParams ? (React.createElement(LoadingBLAST, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
20
+ React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
18
21
  React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
19
22
  });
20
23
  export default MsaViewPanel;
@@ -0,0 +1,87 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { afterEach, expect, test, vi } from 'vitest';
5
+ import MsaViewPanel from './MsaViewPanel';
6
+ // react-msaview's MSAView is the "nothing is launching" branch and wants a real
7
+ // MST model; a marker is enough to say the panel reached it
8
+ vi.mock('react-msaview', () => ({
9
+ MSAView: () => React.createElement("div", null, "the alignment"),
10
+ }));
11
+ function panel(model) {
12
+ return render(React.createElement(MsaViewPanel, { model: model }));
13
+ }
14
+ afterEach(() => {
15
+ cleanup();
16
+ });
17
+ test('an alignment with no pending launch draws itself', () => {
18
+ panel({ progress: '' });
19
+ expect(screen.getByText('the alignment')).toBeTruthy();
20
+ });
21
+ test('a running BLAST shows its progress, not an empty alignment', () => {
22
+ panel({
23
+ blastParams: { proteinSequence: 'MKV' },
24
+ progress: 'Submitting query',
25
+ });
26
+ expect(screen.queryByText('the alignment')).toBeNull();
27
+ expect(screen.getByText(/Running EBI BLAST/)).toBeTruthy();
28
+ expect(screen.getByText('Submitting query')).toBeTruthy();
29
+ });
30
+ // the bug this file was written for: an ortholog launch sets orthologParams
31
+ // rather than blastParams, and the panel keyed on blastParams alone -- so it
32
+ // rendered an empty MSAView for the minutes the alignment takes, and drew
33
+ // nothing at all when the launch failed
34
+ test('a running ortholog launch shows its progress', () => {
35
+ panel({
36
+ orthologParams: { taxId: 9606 },
37
+ progress: 'Resolving orthologs',
38
+ });
39
+ expect(screen.queryByText('the alignment')).toBeNull();
40
+ expect(screen.getByText(/Building ortholog alignment/)).toBeTruthy();
41
+ expect(screen.getByText('Resolving orthologs')).toBeTruthy();
42
+ });
43
+ test('a failed ortholog launch shows why', () => {
44
+ panel({
45
+ orthologParams: { taxId: 9606 },
46
+ progress: '',
47
+ error: new Error('Only 1 ortholog(s) found for this gene'),
48
+ });
49
+ expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
50
+ });
51
+ test('a failed init shows why', () => {
52
+ panel({
53
+ init: { msaName: 'ENST00000288602' },
54
+ progress: '',
55
+ error: new Error('No alignment named ENST00000288602 in msa.fa.gz'),
56
+ });
57
+ expect(screen.queryByText('the alignment')).toBeNull();
58
+ expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
59
+ });
60
+ // a launch runs for 10+ minutes, so leaving with no way out means watching it
61
+ test('a running launch offers a way out, and a failed one does not', () => {
62
+ const cancelLaunch = vi.fn();
63
+ panel({
64
+ blastParams: { proteinSequence: 'MKV' },
65
+ progress: 'Re-checking BLAST status in... 7',
66
+ cancelLaunch,
67
+ });
68
+ screen.getByRole('button', { name: 'Cancel' }).click();
69
+ expect(cancelLaunch).toHaveBeenCalled();
70
+ cleanup();
71
+ panel({
72
+ blastParams: { proteinSequence: 'MKV' },
73
+ progress: '',
74
+ error: new Error('No hits found'),
75
+ cancelLaunch,
76
+ });
77
+ expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
78
+ });
79
+ test('a running job links out to it', () => {
80
+ panel({
81
+ blastParams: { proteinSequence: 'MKV' },
82
+ progress: 'Re-checking BLAST status in... 7',
83
+ rid: 'ncbiblast-R20260826-123456-0001-abc',
84
+ });
85
+ const link = screen.getByRole('link');
86
+ expect(link.getAttribute('href')).toContain('jobId=ncbiblast-R20260826-123456-0001-abc');
87
+ });
@@ -1,8 +1,10 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
- export declare function doLaunchBlast({ self, }: {
2
+ import type { LaunchScope } from './runLaunch';
3
+ export declare function doLaunchBlast({ self, scope, }: {
3
4
  self: JBrowsePluginMsaViewModel;
5
+ scope: LaunchScope;
4
6
  }): Promise<{
5
- treeMetadata: string;
6
7
  msa: string;
7
8
  tree: string;
9
+ treeMetadata: string;
8
10
  }>;