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
@@ -1,116 +1,99 @@
1
- import { makeId, strip } from '../LaunchMsaView/components/util';
2
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
3
2
  import { saveBlastResult } from '../utils/blastCache';
4
- import { queryEbiBlast } from '../utils/ebiBlast';
5
- import { launchMSA, launchTree } from '../utils/msa';
6
- import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
7
- import { queryPhmmer } from '../utils/phmmer';
3
+ import { searchBackends } from '../utils/homologSearch';
4
+ import { launchMSA } from '../utils/msa';
5
+ import { buildSearchMsa } from '../utils/msaRows';
8
6
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
9
- export async function doLaunchBlast({ self, }) {
10
- // kept whole rather than destructured: the database's type depends on
11
- // searchProgram, and pulling the two apart loses the link between them
12
- const params = self.blastParams;
13
- const { selectedTranscript } = params;
14
- const cleanedSeq = cleanProteinSequence(params.proteinSequence);
15
- const onProgress = (arg) => {
16
- self.setProgress(arg);
17
- };
18
- // publish the job id before the first poll so the view can link out while the
19
- // job is still running
20
- const onRid = (r) => {
21
- self.setRid(r);
22
- };
23
- const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
24
- ? await runPhmmer({
25
- query: cleanedSeq,
26
- database: params.blastDatabase,
27
- onProgress,
28
- onRid,
29
- })
30
- : await runBlast({
31
- query: cleanedSeq,
32
- blastDatabase: params.blastDatabase,
33
- msaAlgorithm: params.msaAlgorithm,
34
- onProgress,
35
- onRid,
36
- });
37
- const treeMetadataJson = JSON.stringify(treeMetadata);
38
- await saveBlastResult({
39
- proteinSequence: cleanedSeq,
40
- blastDatabase: params.blastDatabase,
41
- msaAlgorithm: params.msaAlgorithm,
42
- searchProgram: params.searchProgram,
43
- msa,
44
- tree,
45
- treeMetadata: treeMetadataJson,
46
- rid,
47
- geneId: selectedTranscript?.get('parentId'),
48
- transcriptId: selectedTranscript?.id(),
49
- transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
50
- geneName: selectedTranscript?.get('gene_name') ??
51
- selectedTranscript?.get('parentId'),
52
- });
53
- return { msa, tree, treeMetadata: treeMetadataJson };
7
+ import { resolveUniProtEntry } from '../utils/unirefHomologs';
8
+ import { transcriptFields, transcriptName } from './util';
9
+ function asString(val) {
10
+ return typeof val === 'string' ? val : undefined;
54
11
  }
55
12
  /**
56
- * BLAST returns each hit already aligned to the query, but pairwise and one hit
57
- * at a time, so the alignments are stripped back off and every hit is realigned
58
- * together by a dedicated aligner.
13
+ * The query sequence, and what its row is called. The dialog hands over the
14
+ * translated transcript and the row stays `QUERY`; a spec naming a UniProt
15
+ * accession has the sequence fetched and the row named after the entry
16
+ * (`P53_HUMAN_query`), since a search of swissprot returns the entry itself
17
+ * as a hit and two rows called the same thing collapse into one.
59
18
  */
60
- async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, }) {
61
- const { hits, rid } = await queryEbiBlast({
62
- query,
63
- blastDatabase,
64
- onProgress,
65
- onRid,
66
- });
67
- onProgress('Fetching species taxonomy info...');
68
- const taxonomyInfo = await fetchTaxonomyInfo(hits
69
- .map(h => h.description[0]?.taxid)
70
- .filter((t) => t !== undefined));
71
- const treeMetadata = {};
72
- const sequences = hits.map(h => {
73
- const desc = h.description[0] ?? {
74
- accession: 'unknown',
75
- id: 'unknown',
76
- sciname: 'unknown',
19
+ async function resolveQuery(self, scope) {
20
+ const params = self.blastParams;
21
+ if (params.proteinSequence) {
22
+ return {
23
+ sequence: cleanProteinSequence(params.proteinSequence),
24
+ name: self.querySeqName,
77
25
  };
78
- const rowName = makeId(desc, taxonomyInfo);
79
- treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo);
80
- return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`;
81
- });
82
- const result = await launchMSA({
83
- algorithm: msaAlgorithm,
84
- sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
85
- onProgress,
86
- });
87
- return { ...result, treeMetadata, rid };
26
+ }
27
+ if (params.accession) {
28
+ scope.onProgress(`Fetching ${params.accession} from UniProt...`);
29
+ const entry = await resolveUniProtEntry([params.accession], 0, scope.signal);
30
+ if (!entry) {
31
+ throw new Error(`UniProt has no entry ${params.accession}`);
32
+ }
33
+ const name = `${entry.id}_query`;
34
+ scope.act(() => {
35
+ self.setQuerySeqName(name);
36
+ });
37
+ return { sequence: entry.sequence, name };
38
+ }
39
+ throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
88
40
  }
89
41
  /**
90
- * phmmer aligns every hit to a profile of the query as it searches, so its own
91
- * output is the MSA and there is no realignment step — the hits keep the
92
- * placement HMMER gave them, and the query row is derived from the alignment's
93
- * match columns rather than being aligned back in afterwards. That leaves no
94
- * aligner run to take a tree from, so the tree is built from this alignment.
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.
95
48
  */
96
- async function runPhmmer({ query, database, onProgress, onRid, }) {
97
- const { rows, queryRow, rid } = await queryPhmmer({
49
+ export async function doLaunchBlast({ self, scope, }) {
50
+ const params = self.blastParams;
51
+ const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
52
+ const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
53
+ const { onProgress, onRid, signal } = scope;
54
+ const { hits, queryRow, rid } = await searchBackends[searchProgram]({
98
55
  query,
99
- database,
56
+ database: params.blastDatabase,
57
+ maxHits,
100
58
  onProgress,
101
59
  onRid,
60
+ signal,
102
61
  });
62
+ if (hits.length === 0) {
63
+ throw new Error('No hits found');
64
+ }
103
65
  onProgress('Fetching species taxonomy info...');
104
- const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
105
- const { msa, treeMetadata } = buildPhmmerMsa({
106
- rows,
66
+ const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
67
+ const { msa: fasta, treeMetadata } = buildSearchMsa({
68
+ hits,
69
+ query,
107
70
  queryRow,
108
71
  taxonomyInfo,
72
+ querySeqName,
109
73
  });
110
- return {
74
+ const { msa, tree } = queryRow
75
+ ? { msa: fasta, tree: '' }
76
+ : await launchMSA({
77
+ algorithm: params.msaAlgorithm ?? 'browser',
78
+ sequence: fasta,
79
+ onProgress,
80
+ signal,
81
+ });
82
+ const transcript = transcriptFields(selectedTranscript);
83
+ const treeMetadataJson = JSON.stringify(treeMetadata);
84
+ await saveBlastResult({
85
+ proteinSequence: query,
86
+ blastDatabase: params.blastDatabase,
87
+ msaAlgorithm: params.msaAlgorithm,
88
+ searchProgram: params.searchProgram,
111
89
  msa,
112
- tree: await launchTree({ alignment: msa, onProgress }),
113
- treeMetadata,
114
- rid,
115
- };
90
+ tree,
91
+ treeMetadata: treeMetadataJson,
92
+ rid: rid ?? '',
93
+ geneId: asString(transcript.parentId),
94
+ transcriptId: asString(transcript.uniqueId),
95
+ transcriptName: transcriptName(selectedTranscript),
96
+ geneName: asString(transcript.gene_name) ?? asString(transcript.parentId),
97
+ });
98
+ return { msa, tree, treeMetadata: treeMetadataJson };
116
99
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ import { beforeEach, expect, test, vi } from 'vitest';
2
+ import { saveBlastResult } from '../utils/blastCache';
3
+ import { searchBackends } from '../utils/homologSearch';
4
+ import { launchMSA } from '../utils/msa';
5
+ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
+ import { resolveUniProtEntry } from '../utils/unirefHomologs';
7
+ import { doLaunchBlast } from './doLaunchBlast';
8
+ // Every network call is mocked: what is under test is how the launch turns a
9
+ // request into a search and the search's answer into rows -- which backend it
10
+ // asks, where the query comes from, and whether an aligner runs.
11
+ vi.mock('../utils/homologSearch', () => ({
12
+ searchBackends: { blastp: vi.fn(), phmmer: vi.fn() },
13
+ }));
14
+ vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
15
+ vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
16
+ vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
17
+ vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
18
+ const blastp = vi.mocked(searchBackends.blastp);
19
+ const phmmer = vi.mocked(searchBackends.phmmer);
20
+ const mockLaunchMSA = vi.mocked(launchMSA);
21
+ const setQuerySeqName = vi.fn();
22
+ const HIT = { accession: 'P1', id: 'P1_MOUSE', sciname: 'Mus musculus' };
23
+ function makeModel(blastParams) {
24
+ return {
25
+ blastParams,
26
+ querySeqName: 'QUERY',
27
+ setQuerySeqName,
28
+ };
29
+ }
30
+ function launch(self) {
31
+ return doLaunchBlast({
32
+ self,
33
+ scope: {
34
+ signal: new AbortController().signal,
35
+ act: fn => {
36
+ fn();
37
+ },
38
+ onProgress: () => { },
39
+ onRid: () => { },
40
+ },
41
+ });
42
+ }
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
46
+ vi.mocked(saveBlastResult).mockResolvedValue(undefined);
47
+ });
48
+ test('bare hits go to the chosen aligner, with the query first', async () => {
49
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
50
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
51
+ const result = await launch(makeModel({
52
+ searchProgram: 'blastp',
53
+ blastDatabase: 'uniprotkb_swissprot',
54
+ msaAlgorithm: 'clustalo',
55
+ maxHits: 20,
56
+ proteinSequence: 'MKWVTF*',
57
+ }));
58
+ expect(blastp).toHaveBeenCalledWith(expect.objectContaining({
59
+ query: 'MKWVTF',
60
+ database: 'uniprotkb_swissprot',
61
+ maxHits: 20,
62
+ }));
63
+ expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({
64
+ algorithm: 'clustalo',
65
+ sequence: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
66
+ }));
67
+ expect(result.msa).toBe('aligned');
68
+ expect(result.tree).toBe('tree');
69
+ });
70
+ test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
71
+ phmmer.mockResolvedValue({
72
+ rid: 'job',
73
+ queryRow: 'MKWV-TF',
74
+ hits: [{ ...HIT, sequence: 'MKWVSTF' }],
75
+ });
76
+ const result = await launch(makeModel({
77
+ searchProgram: 'phmmer',
78
+ blastDatabase: 'rp15',
79
+ proteinSequence: 'MKWVTF',
80
+ }));
81
+ expect(mockLaunchMSA).not.toHaveBeenCalled();
82
+ expect(result.msa).toBe('>QUERY\nMKWV-TF\n>P1-Mus_musculus\nMKWVSTF');
83
+ expect(result.tree).toBe('');
84
+ });
85
+ test('a UniProt accession supplies the query and names its row', async () => {
86
+ vi.mocked(resolveUniProtEntry).mockResolvedValue({
87
+ accession: 'P04637',
88
+ id: 'P53_HUMAN',
89
+ reviewed: true,
90
+ taxId: 9606,
91
+ scientificName: 'Homo sapiens',
92
+ sequence: 'MEEPQ',
93
+ });
94
+ phmmer.mockResolvedValue({
95
+ queryRow: 'MEEPQ',
96
+ hits: [{ ...HIT, sequence: 'MEEPQ' }],
97
+ });
98
+ const result = await launch(makeModel({
99
+ searchProgram: 'phmmer',
100
+ blastDatabase: 'swissprot',
101
+ accession: 'P04637',
102
+ }));
103
+ expect(resolveUniProtEntry).toHaveBeenCalledWith(['P04637'], 0, expect.any(AbortSignal));
104
+ expect(setQuerySeqName).toHaveBeenCalledWith('P53_HUMAN_query');
105
+ expect(result.msa.split('\n')[0]).toBe('>P53_HUMAN_query');
106
+ });
107
+ test('a request with no query at all is refused before any search runs', async () => {
108
+ await expect(launch(makeModel({ searchProgram: 'phmmer', blastDatabase: 'swissprot' }))).rejects.toThrow(/connectedTranscript/);
109
+ expect(phmmer).not.toHaveBeenCalled();
110
+ });
111
+ // blastParams is a frozen snapshot property, so the Feature the dialog put in
112
+ // it comes back from a session reload as the plain JSON it serialized to. The
113
+ // launch used to call .get() on that, minutes after the EBI job the reload
114
+ // resubmitted had come back, and threw where nothing was catching.
115
+ test('a transcript restored from a session snapshot still labels the cache row', async () => {
116
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
117
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
118
+ await launch(makeModel({
119
+ searchProgram: 'blastp',
120
+ blastDatabase: 'uniprotkb_swissprot',
121
+ msaAlgorithm: 'clustalo',
122
+ proteinSequence: 'MKWV',
123
+ selectedTranscript: {
124
+ uniqueId: 'NM_000546.6',
125
+ name: 'TP53-201',
126
+ parentId: 'TP53',
127
+ gene_name: 'TP53',
128
+ },
129
+ }));
130
+ expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
131
+ geneId: 'TP53',
132
+ transcriptId: 'NM_000546.6',
133
+ transcriptName: 'TP53-201',
134
+ geneName: 'TP53',
135
+ }));
136
+ });
137
+ test('a live Feature handed over in the same session labels it the same way', async () => {
138
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
139
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
140
+ const json = {
141
+ uniqueId: 'NM_000546.6',
142
+ name: 'TP53-201',
143
+ parentId: 'TP53',
144
+ };
145
+ await launch(makeModel({
146
+ searchProgram: 'blastp',
147
+ blastDatabase: 'uniprotkb_swissprot',
148
+ msaAlgorithm: 'clustalo',
149
+ proteinSequence: 'MKWV',
150
+ selectedTranscript: { toJSON: () => json },
151
+ }));
152
+ expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({
153
+ geneId: 'TP53',
154
+ transcriptId: 'NM_000546.6',
155
+ transcriptName: 'TP53-201',
156
+ geneName: 'TP53',
157
+ }));
158
+ });
@@ -1,4 +1,5 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
+ import type { LaunchScope } from './runLaunch';
2
3
  /**
3
4
  * The no-search-job alternative to doLaunchBlast.
4
5
  *
@@ -16,8 +17,9 @@ import type { JBrowsePluginMsaViewModel } from './model';
16
17
  * would silently break the genome<->MSA linkage. The query species is therefore
17
18
  * excluded from the ortholog set rather than appearing twice.
18
19
  */
19
- export declare function doLaunchOrthologs({ self, }: {
20
+ export declare function doLaunchOrthologs({ self, scope, }: {
20
21
  self: JBrowsePluginMsaViewModel;
22
+ scope: LaunchScope;
21
23
  }): Promise<{
22
24
  treeMetadata: string;
23
25
  msa: string;
@@ -3,6 +3,8 @@ import { launchMSA } from '../utils/msa';
3
3
  import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
4
4
  import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
5
5
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
+ import { fetchUnirefHomologs } from '../utils/unirefHomologs';
7
+ import { transcriptName } from './util';
6
8
  /**
7
9
  * The no-search-job alternative to doLaunchBlast.
8
10
  *
@@ -20,11 +22,9 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
20
22
  * would silently break the genome<->MSA linkage. The query species is therefore
21
23
  * excluded from the ortholog set rather than appearing twice.
22
24
  */
23
- export async function doLaunchOrthologs({ self, }) {
24
- const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
25
- const onProgress = (arg) => {
26
- self.setProgress(arg);
27
- };
25
+ export async function doLaunchOrthologs({ self, scope, }) {
26
+ const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', identity, referenceProteomesOnly, } = self.orthologParams;
27
+ const { onProgress, act, signal } = scope;
28
28
  const request = {
29
29
  taxId,
30
30
  geneCandidates,
@@ -36,7 +36,14 @@ export async function doLaunchOrthologs({ self, }) {
36
36
  };
37
37
  const { geneId, representative, rows } = source === 'panther'
38
38
  ? await findPantherOrthologs(request)
39
- : await findNcbiOrthologs(request);
39
+ : source === 'uniref'
40
+ ? await findUnirefHomologs({
41
+ ...request,
42
+ identity,
43
+ referenceProteomesOnly,
44
+ signal,
45
+ })
46
+ : await findNcbiOrthologs(request);
40
47
  // The query row. The dialog always supplies it — it is the user's OWN
41
48
  // selected transcript, which is what makes `connectedFeature` map genome
42
49
  // coordinates through this row. A launch that has no transcript to translate
@@ -48,7 +55,7 @@ export async function doLaunchOrthologs({ self, }) {
48
55
  ? cleanProteinSequence(proteinSequence)
49
56
  : representative?.sequence;
50
57
  if (!cleanedSeq) {
51
- throw new Error(`No query protein: none was supplied and ${source === 'panther' ? 'PANTHER' : 'NCBI'} returned no representative protein for gene ${geneId}.`);
58
+ throw new Error(`No query protein: none was supplied and ${sourceNames[source]} returned no representative protein for gene ${geneId}.`);
52
59
  }
53
60
  // The query row is named for its species like every other row, with a suffix
54
61
  // marking it as the one the genome view is linked to. A bare `QUERY` among
@@ -62,7 +69,9 @@ export async function doLaunchOrthologs({ self, }) {
62
69
  // token, and a collision would silently point the coordinate mapping at
63
70
  // another animal's row.
64
71
  const queryLabel = await queryRowLabel(taxId, rows);
65
- self.setQuerySeqName(queryLabel);
72
+ act(() => {
73
+ self.setQuerySeqName(queryLabel);
74
+ });
66
75
  const treeMetadata = {
67
76
  [queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
68
77
  };
@@ -76,6 +85,7 @@ export async function doLaunchOrthologs({ self, }) {
76
85
  ...rows.map(r => `>${r.label}\n${r.sequence}`),
77
86
  ].join('\n'),
78
87
  onProgress,
88
+ signal,
79
89
  });
80
90
  return {
81
91
  ...result,
@@ -102,6 +112,33 @@ async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest })
102
112
  });
103
113
  return { geneId: resolved.geneId, representative, rows };
104
114
  }
115
+ const sourceNames = {
116
+ ncbi: 'NCBI',
117
+ panther: 'PANTHER',
118
+ uniref: 'UniProt',
119
+ };
120
+ /**
121
+ * The query's own UniProt entry stands in for a representative protein: the
122
+ * entry the cluster was looked up from, whose sequence a spec-launched row
123
+ * takes and whose accession earns the CDD overlay when the two match.
124
+ */
125
+ async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, signal, ...rest }) {
126
+ const found = await fetchUnirefHomologs({
127
+ candidates: geneCandidates,
128
+ identity,
129
+ referenceProteomesOnly,
130
+ signal,
131
+ ...rest,
132
+ });
133
+ return {
134
+ geneId: found.query.id,
135
+ representative: {
136
+ accession: found.query.accession,
137
+ sequence: found.query.sequence,
138
+ },
139
+ rows: found.rows,
140
+ };
141
+ }
105
142
  /**
106
143
  * One `matchortho` call resolves the gene, names its own UniProt entry and
107
144
  * lists an ortholog per genome, so the representative protein needs no second
@@ -161,9 +198,8 @@ async function fetchRepresentativeQueryProtein(geneId) {
161
198
  * protein as its query row passes that test by construction.
162
199
  */
163
200
  function buildQueryMetadata(self, geneId, proteinSequence, representative) {
164
- const transcript = self.orthologParams?.selectedTranscript;
165
201
  const metadata = { 'Gene ID': geneId };
166
- const name = transcript?.get('name') ?? transcript?.get('id');
202
+ const name = transcriptName(self.orthologParams?.selectedTranscript);
167
203
  if (name) {
168
204
  metadata.Transcript = name;
169
205
  }