jbrowse-plugin-msaview 3.8.0 → 3.10.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 (141) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -22
  2. package/dist/AddHighlightModel/connectedHighlights.d.ts +9 -0
  3. package/dist/AddHighlightModel/connectedHighlights.js +15 -0
  4. package/dist/AddHighlightModel/connectedHighlights.test.js +36 -0
  5. package/dist/AddHighlightModel/index.js +0 -1
  6. package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +2 -8
  7. package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +10 -3
  8. package/dist/LaunchMsaView/cleanProteinSequence.test.js +36 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +4 -4
  10. package/dist/LaunchMsaView/components/BlastQuery/consts.js +6 -4
  11. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +0 -2
  12. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +2 -2
  13. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +0 -1
  14. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +1 -1
  15. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +5 -5
  16. package/dist/LaunchMsaView/components/useFeatureSequence.js +3 -6
  17. package/dist/LaunchMsaView/launchTarget.test.js +1 -1
  18. package/dist/LaunchMsaView/util.d.ts +0 -1
  19. package/dist/LaunchMsaView/util.js +1 -1
  20. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +5 -14
  21. package/dist/MsaViewPanel/afterCreateAutoruns.js +77 -86
  22. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  23. package/dist/MsaViewPanel/doLaunchBlast.js +42 -16
  24. package/dist/MsaViewPanel/doLaunchBlast.test.js +54 -0
  25. package/dist/MsaViewPanel/doLaunchOrthologs.js +18 -11
  26. package/dist/MsaViewPanel/launchAutoruns.test.js +0 -1
  27. package/dist/MsaViewPanel/model.d.ts +651 -216
  28. package/dist/MsaViewPanel/model.js +39 -10
  29. package/dist/MsaViewPanel/model.test.js +3 -4
  30. package/dist/MsaViewPanel/msaDataStore.d.ts +1 -2
  31. package/dist/MsaViewPanel/msaDataStore.js +11 -26
  32. package/dist/MsaViewPanel/msaDataStore.test.js +10 -2
  33. package/dist/MsaViewPanel/observeProteinHighlights.test.js +1 -0
  34. package/dist/MsaViewPanel/processInit.test.js +1 -7
  35. package/dist/MsaViewPanel/regionSnapshot.test.js +0 -1
  36. package/dist/MsaViewPanel/resolveConnectedTranscript.js +7 -13
  37. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +2 -3
  38. package/dist/MsaViewPanel/storedData.test.js +238 -145
  39. package/dist/MsaViewPanel/transcriptMap.d.ts +7 -0
  40. package/dist/MsaViewPanel/transcriptMap.js +108 -0
  41. package/dist/MsaViewPanel/transcriptMap.test.d.ts +1 -0
  42. package/dist/MsaViewPanel/transcriptMap.test.js +127 -0
  43. package/dist/index.d.ts +11 -8
  44. package/dist/jbrowse-plugin-msaview.umd.production.min.js +56 -62
  45. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  46. package/dist/utils/browserAlign.d.ts +5 -3
  47. package/dist/utils/browserAlign.js +29 -23
  48. package/dist/utils/browserAlign.test.js +14 -1
  49. package/dist/utils/ebiBlast.d.ts +0 -8
  50. package/dist/utils/ebiBlast.js +10 -21
  51. package/dist/utils/ebiJobDispatcher.d.ts +9 -9
  52. package/dist/utils/ebiJobDispatcher.js +17 -3
  53. package/dist/utils/eutils.d.ts +0 -2
  54. package/dist/utils/eutils.js +11 -3
  55. package/dist/utils/eutils.test.js +22 -0
  56. package/dist/utils/msa.js +6 -14
  57. package/dist/utils/msaRows.d.ts +0 -8
  58. package/dist/utils/msaRows.js +1 -1
  59. package/dist/utils/ncbiOrthologs.d.ts +7 -5
  60. package/dist/utils/ncbiOrthologs.js +20 -14
  61. package/dist/utils/ncbiOrthologs.test.js +13 -0
  62. package/dist/utils/ncbiTaxonomy.d.ts +2 -2
  63. package/dist/utils/ncbiTaxonomy.js +5 -5
  64. package/dist/utils/pantherOrthologs.d.ts +2 -3
  65. package/dist/utils/pantherOrthologs.js +18 -12
  66. package/dist/utils/phmmer.js +7 -14
  67. package/dist/utils/searchCache.d.ts +18 -0
  68. package/dist/utils/searchCache.js +33 -0
  69. package/dist/utils/searchCache.test.d.ts +1 -0
  70. package/dist/utils/searchCache.test.js +80 -0
  71. package/dist/utils/taxonomyNames.d.ts +1 -1
  72. package/dist/utils/taxonomyNames.js +7 -2
  73. package/dist/utils/taxonomyNamesAbort.test.d.ts +1 -0
  74. package/dist/utils/taxonomyNamesAbort.test.js +36 -0
  75. package/dist/utils/unirefHomologs.d.ts +1 -6
  76. package/dist/utils/unirefHomologs.js +2 -2
  77. package/dist/utils/useFetch.d.ts +1 -1
  78. package/dist/utils/useFetch.js +5 -5
  79. package/dist/utils/useFetchAbort.test.d.ts +1 -0
  80. package/dist/utils/useFetchAbort.test.js +29 -0
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/package.json +18 -18
  84. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +10 -23
  85. package/src/AddHighlightModel/connectedHighlights.test.ts +46 -0
  86. package/src/AddHighlightModel/connectedHighlights.ts +35 -0
  87. package/src/AddHighlightModel/index.tsx +0 -1
  88. package/src/LaunchMsaView/cleanProteinSequence.test.ts +37 -2
  89. package/src/LaunchMsaView/components/BlastQuery/consts.ts +6 -4
  90. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +2 -2
  91. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +1 -1
  92. package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +5 -5
  93. package/src/LaunchMsaView/components/useFeatureSequence.ts +3 -6
  94. package/src/LaunchMsaView/launchTarget.test.ts +1 -1
  95. package/src/LaunchMsaView/launchTarget.ts +2 -3
  96. package/src/LaunchMsaView/util.ts +1 -1
  97. package/src/MsaViewPanel/afterCreateAutoruns.ts +90 -87
  98. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  99. package/src/MsaViewPanel/doLaunchBlast.test.ts +71 -0
  100. package/src/MsaViewPanel/doLaunchBlast.ts +71 -20
  101. package/src/MsaViewPanel/doLaunchOrthologs.ts +28 -11
  102. package/src/MsaViewPanel/launchAutoruns.test.ts +0 -1
  103. package/src/MsaViewPanel/model.test.ts +3 -4
  104. package/src/MsaViewPanel/model.ts +40 -10
  105. package/src/MsaViewPanel/msaDataStore.test.ts +13 -2
  106. package/src/MsaViewPanel/msaDataStore.ts +11 -25
  107. package/src/MsaViewPanel/observeProteinHighlights.test.ts +1 -0
  108. package/src/MsaViewPanel/processInit.test.ts +1 -8
  109. package/src/MsaViewPanel/regionSnapshot.test.ts +0 -1
  110. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +2 -3
  111. package/src/MsaViewPanel/resolveConnectedTranscript.ts +7 -17
  112. package/src/MsaViewPanel/storedData.test.ts +273 -158
  113. package/src/MsaViewPanel/transcriptMap.test.ts +160 -0
  114. package/src/MsaViewPanel/transcriptMap.ts +144 -0
  115. package/src/utils/browserAlign.test.ts +17 -0
  116. package/src/utils/browserAlign.ts +39 -25
  117. package/src/utils/ebiBlast.ts +12 -35
  118. package/src/utils/ebiJobDispatcher.ts +26 -7
  119. package/src/utils/eutils.test.ts +25 -0
  120. package/src/utils/eutils.ts +10 -3
  121. package/src/utils/msa.ts +6 -16
  122. package/src/utils/msaRows.ts +1 -1
  123. package/src/utils/ncbiOrthologs.test.ts +16 -0
  124. package/src/utils/ncbiOrthologs.ts +37 -15
  125. package/src/utils/ncbiTaxonomy.ts +8 -2
  126. package/src/utils/pantherOrthologs.ts +28 -12
  127. package/src/utils/phmmer.ts +7 -16
  128. package/src/utils/searchCache.test.ts +98 -0
  129. package/src/utils/searchCache.ts +75 -0
  130. package/src/utils/taxonomyNames.ts +7 -0
  131. package/src/utils/taxonomyNamesAbort.test.ts +46 -0
  132. package/src/utils/unirefHomologs.ts +2 -2
  133. package/src/utils/useFetch.ts +6 -6
  134. package/src/utils/useFetchAbort.test.tsx +33 -0
  135. package/src/version.ts +1 -1
  136. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +0 -27
  137. package/dist/LaunchMsaView/components/calculateProteinSequence.js +0 -71
  138. package/dist/LaunchMsaView/components/types.d.ts +0 -9
  139. package/src/LaunchMsaView/components/calculateProteinSequence.ts +0 -113
  140. package/src/LaunchMsaView/components/types.ts +0 -12
  141. /package/dist/{LaunchMsaView/components/types.js → AddHighlightModel/connectedHighlights.test.d.ts} +0 -0
@@ -8,68 +8,70 @@ import { loadProteinDomains } from './loadProteinDomains';
8
8
  import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
9
9
  import { runLaunch } from './runLaunch';
10
10
  import { getProteinViews } from './structureConnection';
11
+ import { proteinPositionsInRange } from './transcriptMap';
11
12
  import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
12
13
  const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
13
14
  const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
14
15
  const START_OVER = ' Relaunch it from the gene to rebuild it.';
16
+ const EXPIRED_EXTRAS_WARNING = 'Part of this view is no longer in browser storage: annotations, a tree or row metadata too large for the session. Stored documents are kept for 7 days after they were last used, and are lost when site data is cleared.';
15
17
  export function loadStoredData(self) {
16
- const { dataStoreId, rows } = self;
17
- if (dataStoreId && rows.length === 0) {
18
- void (async () => {
19
- try {
20
- self.setLoadingStoredData(true);
21
- const storedData = await retrieveMsaData(dataStoreId);
22
- if (storedData) {
23
- // one transaction, so storeDataToIndexedDB sees the restored data and
24
- // the record of it together and does not write it straight back
25
- transaction(() => {
26
- if (storedData.msa) {
27
- self.setMSA(storedData.msa);
28
- }
29
- if (storedData.tree) {
30
- self.setTree(storedData.tree);
31
- }
32
- if (storedData.treeMetadata) {
33
- self.setTreeMetadata(storedData.treeMetadata);
34
- }
35
- if (storedData.gff) {
36
- self.setGFF(storedData.gff);
37
- }
38
- self.setLastStoredData(currentData(self));
39
- });
18
+ const { dataStoreId } = self;
19
+ if (!dataStoreId) {
20
+ return;
21
+ }
22
+ void (async () => {
23
+ try {
24
+ self.setLoadingStoredData(true);
25
+ const stored = await retrieveMsaData(dataStoreId);
26
+ if (self.dataStoreId !== dataStoreId) {
27
+ return;
28
+ }
29
+ transaction(() => {
30
+ if (stored) {
31
+ const { data } = self;
32
+ if (stored.msa && !data.msa) {
33
+ self.setMSA(stored.msa);
34
+ }
35
+ if (stored.tree && !data.tree) {
36
+ self.setTree(stored.tree);
37
+ }
38
+ if (stored.treeMetadata && !data.treeMetadata) {
39
+ self.setTreeMetadata(stored.treeMetadata);
40
+ }
41
+ if (stored.gff && !data.gff) {
42
+ self.setGFF(stored.gff);
43
+ }
44
+ self.setLastStoredData(stored);
40
45
  }
41
46
  else {
42
- // the id names nothing, so clearing it is what lets react-msaview's
43
- // "Return to import form" actually return instead of landing back
44
- // here. The request that built the alignment is kept, though: it is
45
- // the whole of a retry, and dropping it left the user reading an
46
- // apology with nothing to press.
47
- transaction(() => {
48
- self.setDataStoreId(undefined);
47
+ self.setDataStoreId(undefined);
48
+ if (hasAlignmentSource(self)) {
49
+ self.addWarning(EXPIRED_EXTRAS_WARNING);
50
+ }
51
+ else {
49
52
  self.setError(new Error(EXPIRED_MESSAGE +
50
53
  ((self.blastParams ?? self.orthologParams)
51
54
  ? RELAUNCHABLE
52
55
  : START_OVER)));
53
- });
56
+ }
54
57
  }
55
- }
56
- catch (e) {
57
- console.error('Failed to load MSA data from IndexedDB:', e);
58
- }
59
- finally {
60
- self.setLoadingStoredData(false);
61
- }
62
- })();
63
- }
58
+ });
59
+ }
60
+ catch (e) {
61
+ console.error('Failed to load MSA data from IndexedDB:', e);
62
+ }
63
+ finally {
64
+ self.setLoadingStoredData(false);
65
+ }
66
+ })();
64
67
  }
65
- /**
66
- * Everything react-msaview drops from the snapshot once it passes 50kB. The
67
- * GFF is among them: a local one's filehandle is cleared once it loads, so
68
- * this row is the only copy.
69
- */
70
- function currentData(self) {
71
- const { msa, tree, treeMetadata, gff } = self.data;
72
- return { msa, tree, treeMetadata, gff };
68
+ function hasAlignmentSource(self) {
69
+ return !!(self.data.msa ||
70
+ self.msaFilehandle ||
71
+ self.init?.msaIndexedLocation);
72
+ }
73
+ function isEmpty(data) {
74
+ return !(data.msa || data.tree || data.treeMetadata || data.gff);
73
75
  }
74
76
  function sameData(a, b) {
75
77
  return (!!a &&
@@ -79,44 +81,37 @@ function sameData(a, b) {
79
81
  a.gff === b.gff);
80
82
  }
81
83
  /**
82
- * Keep IndexedDB holding what the view holds. The first run writes a new row and
83
- * records its id in the session snapshot; later runs update that row, because
84
- * the alignment keeps changing after it arrives — react-msaview's
85
- * "calculate neighbor-joining tree" replaces `data.tree` — and a session
86
- * reopened against a stale row restores the wrong picture.
87
- *
88
- * `lastStoredData` is what makes that safe to run on every data change: it is
89
- * the only thing separating "this is new" from "this is what we just wrote", and
90
- * it is recorded whether or not the write succeeded, so a browser refusing
91
- * IndexedDB (private mode) fails once rather than in a loop.
92
- *
93
- * A view whose data comes from a filehandle -- or from the indexed block its
94
- * kept `init` names -- stores nothing at all: the file is the source of truth
95
- * and it is refetched at startup.
84
+ * Keep IndexedDB holding `unsavedDocuments`. A view writes in place only to a
85
+ * row it created (`ownsDataStoreRow`), and never deletes one, since other
86
+ * views may name it: an emptied set drops the id and cleanupOldData ages the
87
+ * row out. `lastStoredData` is recorded even when a write fails, so a browser
88
+ * refusing IndexedDB fails once rather than in a loop.
96
89
  */
97
90
  export function storeDataToIndexedDB(self) {
98
- const { rows, dataStoreId, isStoringData, lastStoredData, init } = self;
99
- const data = currentData(self);
100
- if (rows.length === 0 ||
101
- isStoringData ||
102
- self.msaFilehandle ||
103
- self.treeFilehandle ||
104
- // an indexed view keeps its init and refetches the block, so a row here
105
- // would be one nothing ever reads
106
- !!init?.msaIndexedLocation ||
107
- !(data.msa || data.tree) ||
108
- sameData(lastStoredData, data)) {
91
+ const { dataStoreId, ownsDataStoreRow, isStoringData, loadingStoredData, lastStoredData, } = self;
92
+ const data = self.unsavedDocuments;
93
+ if (isStoringData || loadingStoredData || sameData(lastStoredData, data)) {
94
+ return;
95
+ }
96
+ if (isEmpty(data)) {
97
+ if (lastStoredData) {
98
+ transaction(() => {
99
+ self.setDataStoreId(undefined);
100
+ self.setOwnsDataStoreRow(false);
101
+ self.setLastStoredData(data);
102
+ });
103
+ }
109
104
  return;
110
105
  }
111
- // mark as storing synchronously so re-runs of this autorun (e.g. when data
112
- // observables change while the write is pending) don't kick off a duplicate
113
- // write and leave an orphan IndexedDB entry
114
106
  self.setIsStoringData(true);
115
107
  void (async () => {
116
108
  try {
117
- const id = dataStoreId ?? generateDataStoreId();
109
+ const id = dataStoreId && ownsDataStoreRow ? dataStoreId : generateDataStoreId();
118
110
  if (await storeMsaData(id, data)) {
119
- self.setDataStoreId(id);
111
+ transaction(() => {
112
+ self.setDataStoreId(id);
113
+ self.setOwnsDataStoreRow(true);
114
+ });
120
115
  }
121
116
  }
122
117
  catch (e) {
@@ -297,19 +292,15 @@ function genomeHighlightsToVisibleColumns(self, field) {
297
292
  if (!transcriptToMsaMap || !hasQueryRow(self)) {
298
293
  return [];
299
294
  }
300
- const { g2p } = transcriptToMsaMap;
301
295
  const columns = new Set();
302
296
  for (const view of getProteinViews(getSession(self).views)) {
303
297
  for (const structure of view.structures) {
304
298
  if (structure.connectedViewId !== connectedViewId) {
305
299
  continue;
306
300
  }
307
- for (const highlight of structure[field] ?? []) {
308
- for (let coord = highlight.start; coord < highlight.end; coord++) {
309
- const proteinPos = g2p[coord];
310
- const col = proteinPos === undefined
311
- ? undefined
312
- : transcriptPosToVisibleCol(self, proteinPos);
301
+ for (const { start, end } of structure[field] ?? []) {
302
+ for (const proteinPos of proteinPositionsInRange(transcriptToMsaMap, start, end)) {
303
+ const col = transcriptPosToVisibleCol(self, proteinPos);
313
304
  if (col !== undefined) {
314
305
  columns.add(col);
315
306
  }
@@ -25,7 +25,7 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
25
25
  const expired = !!request && !!model.error && !model.dataInitialized;
26
26
  const launching = pending || expired || (!!init && !model.dataInitialized);
27
27
  return (React.createElement(ErrorBoundary, null,
28
- React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
28
+ React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData && !model.dataInitialized ? (React.createElement("div", { className: classes.loadingContainer },
29
29
  React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
30
30
  });
31
31
  export default MsaViewPanel;
@@ -1,8 +1,10 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
2
2
  import { saveBlastResult } from '../utils/blastCache';
3
+ import { checkPairSize } from '../utils/browserAlign';
3
4
  import { searchBackends } from '../utils/homologSearch';
4
5
  import { launchMSA } from '../utils/msa';
5
6
  import { buildSearchMsa } from '../utils/msaRows';
7
+ import { getCachedSearch, saveSearch, searchKey } from '../utils/searchCache';
6
8
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
7
9
  import { resolveUniProtEntry } from '../utils/unirefHomologs';
8
10
  import { transcriptFields, transcriptName } from './util';
@@ -38,23 +40,13 @@ async function resolveQuery(self, scope) {
38
40
  }
39
41
  throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
40
42
  }
41
- /**
42
- * A similarity search, then an alignment of what it found. The program is a
43
- * backend behind one interface (utils/homologSearch.ts); what differs between
44
- * them is settled by whether the result came back aligned. A program that
45
- * aligns as it searches (phmmer) hands over the alignment and the tree is
46
- * built from it in the browser; one that does not (blastp) hands over bare
47
- * hits and the chosen aligner runs on them.
48
- */
49
- export async function doLaunchBlast({ self, scope, }) {
43
+ async function search({ self, scope, query, querySeqName, key, }) {
50
44
  const params = self.blastParams;
51
- const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
52
- const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
53
45
  const { onProgress, onRid, signal } = scope;
54
- const { hits, queryRow, rid } = await searchBackends[searchProgram]({
46
+ const { hits, queryRow, rid } = await searchBackends[params.searchProgram ?? 'blastp']({
55
47
  query,
56
48
  database: params.blastDatabase,
57
- maxHits,
49
+ maxHits: params.maxHits,
58
50
  onProgress,
59
51
  onRid,
60
52
  signal,
@@ -63,7 +55,7 @@ export async function doLaunchBlast({ self, scope, }) {
63
55
  throw new Error('No hits found');
64
56
  }
65
57
  onProgress('Fetching species taxonomy info...');
66
- const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
58
+ const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined), signal);
67
59
  const { msa: fasta, treeMetadata } = buildSearchMsa({
68
60
  hits,
69
61
  query,
@@ -71,10 +63,44 @@ export async function doLaunchBlast({ self, scope, }) {
71
63
  taxonomyInfo,
72
64
  querySeqName,
73
65
  });
66
+ if (!queryRow) {
67
+ await saveSearch({ id: key, fasta, treeMetadata, rid });
68
+ }
69
+ return { fasta, treeMetadata, rid, queryRow };
70
+ }
71
+ /**
72
+ * A similarity search, then an alignment of what it found. The program is a
73
+ * backend behind one interface (utils/homologSearch.ts); what differs between
74
+ * them is settled by whether the result came back aligned. A program that
75
+ * aligns as it searches (phmmer) hands over the alignment and the tree is
76
+ * built from it in the browser; one that does not (blastp) hands over bare
77
+ * hits and the chosen aligner runs on them.
78
+ */
79
+ export async function doLaunchBlast({ self, scope, }) {
80
+ const params = self.blastParams;
81
+ const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
82
+ const msaAlgorithm = params.msaAlgorithm ?? 'browser';
83
+ const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
84
+ if (searchProgram === 'blastp' && msaAlgorithm === 'browser') {
85
+ checkPairSize(querySeqName, query.length, query.length);
86
+ }
87
+ const { onProgress, signal } = scope;
88
+ const key = searchKey({
89
+ searchProgram,
90
+ database: params.blastDatabase,
91
+ maxHits,
92
+ querySeqName,
93
+ query,
94
+ });
95
+ const found = await getCachedSearch(key);
96
+ if (found) {
97
+ onProgress(`Reusing the ${searchProgram} search from ${new Date(found.timestamp).toLocaleString()}...`);
98
+ }
99
+ const { fasta, treeMetadata, rid, queryRow } = found ?? (await search({ self, scope, query, querySeqName, key }));
74
100
  const { msa, tree } = queryRow
75
101
  ? { msa: fasta, tree: '' }
76
102
  : await launchMSA({
77
- algorithm: params.msaAlgorithm ?? 'browser',
103
+ algorithm: msaAlgorithm,
78
104
  sequence: fasta,
79
105
  onProgress,
80
106
  signal,
@@ -84,7 +110,7 @@ export async function doLaunchBlast({ self, scope, }) {
84
110
  await saveBlastResult({
85
111
  proteinSequence: query,
86
112
  blastDatabase: params.blastDatabase,
87
- msaAlgorithm: queryRow ? undefined : (params.msaAlgorithm ?? 'browser'),
113
+ msaAlgorithm: queryRow ? undefined : msaAlgorithm,
88
114
  searchProgram: params.searchProgram,
89
115
  maxHits,
90
116
  msa,
@@ -1,7 +1,9 @@
1
1
  import { beforeEach, expect, test, vi } from 'vitest';
2
2
  import { saveBlastResult } from '../utils/blastCache';
3
+ import { MAX_PAIR_CELLS } from '../utils/browserAlign';
3
4
  import { searchBackends } from '../utils/homologSearch';
4
5
  import { launchMSA } from '../utils/msa';
6
+ import { getCachedSearch, saveSearch } from '../utils/searchCache';
5
7
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
8
  import { resolveUniProtEntry } from '../utils/unirefHomologs';
7
9
  import { doLaunchBlast } from './doLaunchBlast';
@@ -15,6 +17,11 @@ vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
15
17
  vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
16
18
  vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
17
19
  vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
20
+ vi.mock('../utils/searchCache', async (importOriginal) => ({
21
+ ...(await importOriginal()),
22
+ getCachedSearch: vi.fn(),
23
+ saveSearch: vi.fn(),
24
+ }));
18
25
  const blastp = vi.mocked(searchBackends.blastp);
19
26
  const phmmer = vi.mocked(searchBackends.phmmer);
20
27
  const mockLaunchMSA = vi.mocked(launchMSA);
@@ -44,6 +51,8 @@ beforeEach(() => {
44
51
  vi.clearAllMocks();
45
52
  vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
46
53
  vi.mocked(saveBlastResult).mockResolvedValue(undefined);
54
+ vi.mocked(getCachedSearch).mockResolvedValue(undefined);
55
+ vi.mocked(saveSearch).mockResolvedValue(undefined);
47
56
  });
48
57
  test('bare hits go to the chosen aligner, with the query first', async () => {
49
58
  blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
@@ -168,3 +177,48 @@ test('a live Feature handed over in the same session labels it the same way', as
168
177
  geneName: 'TP53',
169
178
  }));
170
179
  });
180
+ const BLASTP_BROWSER = {
181
+ searchProgram: 'blastp',
182
+ blastDatabase: 'uniprotkb_swissprot',
183
+ msaAlgorithm: 'browser',
184
+ proteinSequence: 'MKWVTF',
185
+ };
186
+ test('the hits are saved before the aligner runs, so a failed alignment keeps them', async () => {
187
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
188
+ mockLaunchMSA.mockRejectedValue(new Error('aligner fell over'));
189
+ await expect(launch(makeModel(BLASTP_BROWSER))).rejects.toThrow('aligner fell over');
190
+ expect(saveSearch).toHaveBeenCalledWith(expect.objectContaining({
191
+ fasta: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
192
+ rid: 'job',
193
+ }));
194
+ });
195
+ test('a relaunch with saved hits aligns them without searching again', async () => {
196
+ vi.mocked(getCachedSearch).mockResolvedValue({
197
+ id: 'k',
198
+ fasta: '>QUERY\nMKWVTF\n>P1\nMKWV',
199
+ treeMetadata: {},
200
+ rid: 'job',
201
+ timestamp: 0,
202
+ });
203
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: '' });
204
+ const result = await launch(makeModel(BLASTP_BROWSER));
205
+ expect(blastp).not.toHaveBeenCalled();
206
+ expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({ sequence: '>QUERY\nMKWVTF\n>P1\nMKWV' }));
207
+ expect(result.msa).toBe('aligned');
208
+ });
209
+ test('a query too long for the in-browser aligner is refused before the search', async () => {
210
+ const side = Math.ceil(Math.sqrt(MAX_PAIR_CELLS)) + 1;
211
+ await expect(launch(makeModel({ ...BLASTP_BROWSER, proteinSequence: 'M'.repeat(side) }))).rejects.toThrow(/too large to align in the browser/);
212
+ expect(blastp).not.toHaveBeenCalled();
213
+ });
214
+ test('the same long query goes to an EBI aligner without complaint', async () => {
215
+ const side = Math.ceil(Math.sqrt(MAX_PAIR_CELLS)) + 1;
216
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
217
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
218
+ await launch(makeModel({
219
+ ...BLASTP_BROWSER,
220
+ msaAlgorithm: 'clustalo',
221
+ proteinSequence: 'M'.repeat(side),
222
+ }));
223
+ expect(blastp).toHaveBeenCalled();
224
+ });
@@ -1,4 +1,5 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
2
+ import { isAbortError } from '../utils/fetch';
2
3
  import { launchMSA } from '../utils/msa';
3
4
  import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
4
5
  import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
@@ -33,6 +34,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
33
34
  exclude: taxId,
34
35
  limit: maxSpecies,
35
36
  onProgress,
37
+ signal,
36
38
  };
37
39
  const { geneId, representative, rows } = source === 'panther'
38
40
  ? await findPantherOrthologs(request)
@@ -41,7 +43,6 @@ export async function doLaunchOrthologs({ self, scope, }) {
41
43
  ...request,
42
44
  identity,
43
45
  referenceProteomesOnly,
44
- signal,
45
46
  })
46
47
  : await findNcbiOrthologs(request);
47
48
  // The query row. The dialog always supplies it — it is the user's OWN
@@ -68,7 +69,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
68
69
  // taxon is excluded from that set, but a subspecies can sanitize to the same
69
70
  // token, and a collision would silently point the coordinate mapping at
70
71
  // another animal's row.
71
- const queryLabel = await queryRowLabel(taxId, rows);
72
+ const queryLabel = await queryRowLabel(taxId, rows, signal);
72
73
  act(() => {
73
74
  self.setQuerySeqName(queryLabel);
74
75
  });
@@ -98,16 +99,17 @@ export async function doLaunchOrthologs({ self, scope, }) {
98
99
  * wants "this gene across species" gets NCBI's own order, which leads with the
99
100
  * reference organisms.
100
101
  */
101
- async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest }) {
102
+ async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, signal, ...rest }) {
102
103
  onProgress('Resolving gene at NCBI...');
103
- const resolved = await resolveGeneId(geneCandidates, taxId);
104
+ const resolved = await resolveGeneId(geneCandidates, taxId, signal);
104
105
  if (!resolved) {
105
106
  throw new Error(`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`);
106
107
  }
107
- const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
108
+ const representative = await fetchRepresentativeQueryProtein(resolved.geneId, signal);
108
109
  const rows = await fetchOrthologRows({
109
110
  geneId: resolved.geneId,
110
111
  onProgress,
112
+ signal,
111
113
  ...rest,
112
114
  });
113
115
  return { geneId: resolved.geneId, representative, rows };
@@ -122,12 +124,11 @@ const sourceNames = {
122
124
  * entry the cluster was looked up from, whose sequence a spec-launched row
123
125
  * takes and whose accession earns the CDD overlay when the two match.
124
126
  */
125
- async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, signal, ...rest }) {
127
+ async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, ...rest }) {
126
128
  const found = await fetchUnirefHomologs({
127
129
  candidates: geneCandidates,
128
130
  identity,
129
131
  referenceProteomesOnly,
130
- signal,
131
132
  ...rest,
132
133
  });
133
134
  return {
@@ -160,13 +161,16 @@ async function findPantherOrthologs({ geneCandidates, ...rest }) {
160
161
  * marker when NCBI cannot name the taxon, which is a naming failure and must not
161
162
  * take down the launch.
162
163
  */
163
- async function queryRowLabel(taxId, rows) {
164
+ async function queryRowLabel(taxId, rows, signal) {
164
165
  let name;
165
166
  try {
166
- const info = (await fetchTaxonomyInfo([taxId])).get(taxId);
167
+ const info = (await fetchTaxonomyInfo([taxId], signal)).get(taxId);
167
168
  name = info?.commonName ?? info?.sciname;
168
169
  }
169
170
  catch (e) {
171
+ if (isAbortError(e)) {
172
+ throw e;
173
+ }
170
174
  console.warn('[msaview-orthologs] taxonomy name lookup failed:', e);
171
175
  }
172
176
  return dedupeLabels([
@@ -179,11 +183,14 @@ async function queryRowLabel(taxId, rows) {
179
183
  * that supplied no sequence of its own, the alignment — so it is reported by
180
184
  * returning nothing rather than by throwing here.
181
185
  */
182
- async function fetchRepresentativeQueryProtein(geneId) {
186
+ async function fetchRepresentativeQueryProtein(geneId, signal) {
183
187
  try {
184
- return await fetchProteinForGene(geneId);
188
+ return await fetchProteinForGene(geneId, signal);
185
189
  }
186
190
  catch (e) {
191
+ if (isAbortError(e)) {
192
+ throw e;
193
+ }
187
194
  console.warn('[msaview-orthologs] query protein lookup failed:', e);
188
195
  return undefined;
189
196
  }
@@ -8,7 +8,6 @@ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
8
8
  vi.mock('./doLaunchBlast', () => ({ doLaunchBlast: vi.fn() }));
9
9
  vi.mock('./msaDataStore', () => ({
10
10
  cleanupOldData: vi.fn(async () => { }),
11
- deleteMsaData: vi.fn(async () => { }),
12
11
  generateDataStoreId: vi.fn(),
13
12
  retrieveMsaData: vi.fn(),
14
13
  storeMsaData: vi.fn(),