jbrowse-plugin-msaview 2.8.1 → 2.9.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 (91) hide show
  1. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastRIDPanel.d.ts → BlastQuery/BlastAutomaticPanel.d.ts} +2 -3
  2. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.js → BlastQuery/BlastAutomaticPanel.js} +6 -25
  3. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.d.ts → BlastQuery/BlastManualPanel.d.ts} +2 -3
  4. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.js → BlastQuery/BlastManualPanel.js} +7 -4
  5. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.d.ts → BlastQuery/BlastMethodSelector.d.ts} +2 -2
  6. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.js → BlastQuery/BlastMethodSelector.js} +1 -2
  7. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.d.ts → BlastQuery/BlastPanel.d.ts} +3 -12
  8. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.js → BlastQuery/BlastPanel.js} +14 -16
  9. package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.d.ts +8 -0
  10. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.js → BlastQuery/BlastSettingsDialog.js} +13 -9
  11. package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +12 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +17 -0
  14. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +3 -3
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +1 -1
  16. package/dist/LaunchMsaViewExtensionPoint/index.js +11 -4
  17. package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
  18. package/dist/MsaViewPanel/components/JobLink.js +13 -0
  19. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
  20. package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
  21. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  22. package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
  23. package/dist/MsaViewPanel/doLaunchOrthologs.js +44 -21
  24. package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
  25. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +127 -0
  26. package/dist/MsaViewPanel/model.d.ts +15 -7
  27. package/dist/jbrowse-plugin-msaview.umd.production.min.js +32 -32
  28. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  29. package/dist/utils/blastCache.d.ts +8 -4
  30. package/dist/utils/blastCache.js +4 -5
  31. package/dist/utils/ebiBlast.d.ts +52 -0
  32. package/dist/utils/ebiBlast.js +63 -0
  33. package/dist/utils/ebiJobDispatcher.d.ts +33 -0
  34. package/dist/utils/ebiJobDispatcher.js +55 -0
  35. package/dist/utils/fetch.js +24 -1
  36. package/dist/utils/msa.d.ts +1 -1
  37. package/dist/utils/msa.js +25 -32
  38. package/dist/utils/types.d.ts +9 -14
  39. package/dist/utils/useLocalStorage.d.ts +1 -0
  40. package/dist/utils/useLocalStorage.js +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +1 -1
  44. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
  45. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
  46. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
  47. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
  48. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
  49. package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
  50. package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
  51. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
  52. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +2 -2
  53. package/src/LaunchMsaViewExtensionPoint/index.ts +31 -4
  54. package/src/MsaViewPanel/components/JobLink.tsx +17 -0
  55. package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
  56. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  57. package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
  58. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +159 -0
  59. package/src/MsaViewPanel/doLaunchOrthologs.ts +49 -20
  60. package/src/MsaViewPanel/model.ts +15 -8
  61. package/src/utils/blastCache.ts +8 -9
  62. package/src/utils/ebiBlast.ts +114 -0
  63. package/src/utils/ebiJobDispatcher.ts +91 -0
  64. package/src/utils/fetch.ts +26 -1
  65. package/src/utils/msa.ts +26 -47
  66. package/src/utils/types.ts +8 -13
  67. package/src/utils/useLocalStorage.ts +1 -1
  68. package/src/version.ts +1 -1
  69. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
  70. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
  71. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
  72. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
  73. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
  74. package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
  75. package/dist/MsaViewPanel/components/RIDLink.js +0 -12
  76. package/dist/utils/ncbiBlast.d.ts +0 -30
  77. package/dist/utils/ncbiBlast.js +0 -84
  78. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
  79. package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
  80. package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
  81. package/src/utils/ncbiBlast.ts +0 -143
  82. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
  83. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
  84. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
  85. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
  86. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
  87. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
  88. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
  89. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
  90. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
  91. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util'
2
2
  import { launchMSA } from '../utils/msa'
3
3
  import {
4
+ COMMON_SPECIES,
4
5
  fetchOrthologRows,
5
6
  fetchProteinForGene,
6
7
  resolveGeneId,
@@ -31,7 +32,6 @@ export async function doLaunchOrthologs({
31
32
  }) {
32
33
  const { taxId, taxa, geneCandidates, msaAlgorithm, proteinSequence } =
33
34
  self.orthologParams!
34
- const cleanedSeq = cleanProteinSequence(proteinSequence)
35
35
 
36
36
  const onProgress = (arg: string) => {
37
37
  self.setProgress(arg)
@@ -45,8 +45,29 @@ export async function doLaunchOrthologs({
45
45
  )
46
46
  }
47
47
 
48
- // the query species is represented by the user's own transcript below
49
- const wanted = new Set(taxa.filter(t => t !== taxId))
48
+ // The query row. The dialog always supplies it — it is the user's OWN
49
+ // selected transcript, which is what makes `connectedFeature` map genome
50
+ // coordinates through this row. A launch that has no transcript to translate
51
+ // (a session spec naming only a gene) falls back to NCBI's representative
52
+ // protein for the resolved gene, which is the same choice made for every
53
+ // other row, so the alignment is the one NCBI would build for that gene.
54
+ const representative = await fetchRepresentativeQueryProtein(resolved.geneId)
55
+ const cleanedSeq = proteinSequence
56
+ ? cleanProteinSequence(proteinSequence)
57
+ : representative?.sequence
58
+ if (!cleanedSeq) {
59
+ throw new Error(
60
+ `No query protein: none was supplied and NCBI returned no representative protein for gene ${resolved.geneId}.`,
61
+ )
62
+ }
63
+
64
+ // Every species the panel offers, when a launch names none. A spec that wants
65
+ // a narrower comparison says so; one that just wants "this gene across
66
+ // species" should not have to enumerate the list the dialog would have
67
+ // checked for it.
68
+ const wantedTaxa = taxa ?? COMMON_SPECIES.map(s => s.taxId as number)
69
+ // the query species is represented by the query row above
70
+ const wanted = new Set(wantedTaxa.filter(t => t !== taxId))
50
71
  const rows = await fetchOrthologRows({
51
72
  geneId: resolved.geneId,
52
73
  taxa: wanted,
@@ -54,7 +75,7 @@ export async function doLaunchOrthologs({
54
75
  })
55
76
 
56
77
  const treeMetadata: Record<string, Record<string, string>> = {
57
- QUERY: await buildQueryMetadata(self, resolved.geneId, cleanedSeq),
78
+ QUERY: buildQueryMetadata(self, resolved.geneId, cleanedSeq, representative),
58
79
  }
59
80
  for (const row of rows) {
60
81
  treeMetadata[row.label] = buildRowMetadata(row)
@@ -76,34 +97,42 @@ export async function doLaunchOrthologs({
76
97
  }
77
98
 
78
99
  /**
79
- * The query row is the user's own translated transcript, so it carries an
80
- * Accession — which is what drives the automatic CDD overlay
81
- * (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) — ONLY
82
- * when its sequence is byte-identical to the RefSeq protein that accession
100
+ * A failed lookup only costs the query row its domain overlay and, for a launch
101
+ * that supplied no sequence of its own, the alignment — so it is reported by
102
+ * returning nothing rather than by throwing here.
103
+ */
104
+ async function fetchRepresentativeQueryProtein(geneId: string) {
105
+ try {
106
+ return await fetchProteinForGene(geneId)
107
+ } catch (e) {
108
+ console.warn('[msaview-orthologs] query protein lookup failed:', e)
109
+ return undefined
110
+ }
111
+ }
112
+
113
+ /**
114
+ * The query row carries an Accession — which is what drives the automatic CDD
115
+ * overlay (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) —
116
+ * ONLY when its sequence is byte-identical to the RefSeq protein that accession
83
117
  * names. Attaching it unconditionally would put every domain box at an offset
84
118
  * whenever the user picked a non-representative isoform, which is a silently
85
- * wrong figure rather than a missing one.
119
+ * wrong figure rather than a missing one. A launch that took the representative
120
+ * protein as its query row passes that test by construction.
86
121
  */
87
- async function buildQueryMetadata(
122
+ function buildQueryMetadata(
88
123
  self: JBrowsePluginMsaViewModel,
89
124
  geneId: string,
90
125
  proteinSequence: string,
91
- ): Promise<Record<string, string>> {
126
+ representative: { accession: string; sequence: string } | undefined,
127
+ ): Record<string, string> {
92
128
  const transcript = self.orthologParams?.selectedTranscript
93
129
  const metadata: Record<string, string> = { 'Gene ID': geneId }
94
130
  const name = transcript?.get('name') ?? transcript?.get('id')
95
131
  if (name) {
96
132
  metadata.Transcript = name
97
133
  }
98
- try {
99
- const representative = await fetchProteinForGene(geneId)
100
- if (representative?.sequence === proteinSequence) {
101
- metadata.Accession = representative.accession
102
- }
103
- } catch (e) {
104
- // a failed lookup only costs the query row its domain overlay, so it must
105
- // not take down an alignment that is otherwise complete
106
- console.warn('[msaview-orthologs] query protein lookup failed:', e)
134
+ if (representative?.sequence === proteinSequence) {
135
+ metadata.Accession = representative.accession
107
136
  }
108
137
  return metadata
109
138
  }
@@ -28,9 +28,8 @@ import {
28
28
  import type { MafRegion, MsaViewInitState } from './types'
29
29
  import type {
30
30
  BlastDatabase,
31
- BlastProgram,
32
31
  MsaAlgorithm,
33
- } from '../LaunchMsaView/components/NCBIBlastQuery/consts'
32
+ } from '../LaunchMsaView/components/BlastQuery/consts'
34
33
  import type { Feature } from '@jbrowse/core/util'
35
34
  import type { Instance } from '@jbrowse/mobx-state-tree'
36
35
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
@@ -46,25 +45,33 @@ export interface IRegion {
46
45
  }
47
46
 
48
47
  export interface BlastParams {
49
- baseUrl: string
50
48
  blastDatabase: BlastDatabase
51
49
  msaAlgorithm: MsaAlgorithm
52
- blastProgram: BlastProgram
53
50
  selectedTranscript?: Feature
54
51
  proteinSequence: string
55
- rid?: string
56
52
  }
57
53
 
58
54
  export interface OrthologParams {
59
55
  /** NCBI taxon id of the assembly the query gene came from */
60
56
  taxId: number
61
- /** taxon ids to include as rows (the query taxon is represented by QUERY) */
62
- taxa: number[]
57
+ /**
58
+ * taxon ids to include as rows (the query taxon is represented by QUERY).
59
+ * Omitted means every species the launch dialog offers, which is what a
60
+ * launch that just wants "this gene across species" wants.
61
+ */
62
+ taxa?: number[]
63
63
  /** candidate gene identifiers off the feature, tried in order */
64
64
  geneCandidates: string[]
65
65
  msaAlgorithm: MsaAlgorithm
66
66
  selectedTranscript?: Feature
67
- proteinSequence: string
67
+ /**
68
+ * The QUERY row. The launch dialog always supplies it, translated from the
69
+ * transcript the user picked, which is what `connectedFeature` maps genome
70
+ * coordinates through. Omitted — a session spec naming a gene and nothing
71
+ * else — the query row becomes NCBI's representative protein for the resolved
72
+ * gene, the same choice every other row makes.
73
+ */
74
+ proteinSequence?: string
68
75
  }
69
76
 
70
77
  /**
@@ -2,9 +2,8 @@ import { createDbOpener } from './idb'
2
2
 
3
3
  import type {
4
4
  BlastDatabase,
5
- BlastProgram,
6
5
  MsaAlgorithm,
7
- } from '../LaunchMsaView/components/NCBIBlastQuery/consts'
6
+ } from '../LaunchMsaView/components/BlastQuery/consts'
8
7
  import type { DBSchema } from 'idb'
9
8
 
10
9
  const DB_NAME = 'jbrowse-msaview-blast-cache'
@@ -15,7 +14,12 @@ export interface CachedBlastResult {
15
14
  id: string
16
15
  proteinSequence: string
17
16
  blastDatabase: BlastDatabase
18
- blastProgram: BlastProgram
17
+ /**
18
+ * Only ever set on rows cached by a version that still queried NCBI, where
19
+ * the choice between blastp and quick-blastp was real. Kept so those rows
20
+ * still display; never written now.
21
+ */
22
+ blastProgram?: string
19
23
  msaAlgorithm: MsaAlgorithm
20
24
  msa: string
21
25
  tree: string
@@ -51,7 +55,6 @@ const getDB = createDbOpener<BlastCacheDB>(
51
55
  function createCacheKey(
52
56
  proteinSequence: string,
53
57
  blastDatabase: BlastDatabase,
54
- blastProgram: BlastProgram,
55
58
  msaAlgorithm: MsaAlgorithm,
56
59
  transcriptId?: string,
57
60
  ) {
@@ -59,13 +62,12 @@ function createCacheKey(
59
62
  // msaAlgorithm is part of the key because the stored msa/tree are produced by
60
63
  // it — without it, re-running the same query under a different algorithm
61
64
  // overwrites the earlier result and drops it from the history list
62
- return `${blastDatabase}:${blastProgram}:${msaAlgorithm}${idPart}:${proteinSequence}`
65
+ return `${blastDatabase}:${msaAlgorithm}${idPart}:${proteinSequence}`
63
66
  }
64
67
 
65
68
  export async function saveBlastResult({
66
69
  proteinSequence,
67
70
  blastDatabase,
68
- blastProgram,
69
71
  msaAlgorithm,
70
72
  msa,
71
73
  tree,
@@ -78,7 +80,6 @@ export async function saveBlastResult({
78
80
  }: {
79
81
  proteinSequence: string
80
82
  blastDatabase: BlastDatabase
81
- blastProgram: BlastProgram
82
83
  msaAlgorithm: MsaAlgorithm
83
84
  msa: string
84
85
  tree: string
@@ -93,7 +94,6 @@ export async function saveBlastResult({
93
94
  const id = createCacheKey(
94
95
  proteinSequence,
95
96
  blastDatabase,
96
- blastProgram,
97
97
  msaAlgorithm,
98
98
  transcriptId,
99
99
  )
@@ -101,7 +101,6 @@ export async function saveBlastResult({
101
101
  id,
102
102
  proteinSequence,
103
103
  blastDatabase,
104
- blastProgram,
105
104
  msaAlgorithm,
106
105
  msa,
107
106
  tree,
@@ -0,0 +1,114 @@
1
+ import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
2
+
3
+ import type { BlastHit } from './types'
4
+ import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
5
+
6
+ const TOOL = 'ncbiblast'
7
+
8
+ /**
9
+ * The subset of EBI's ncbiblast JSON result this plugin reads. The service
10
+ * returns a great deal more per hit (urls, bit scores, e-values, the match
11
+ * string); only what the MSA rows are built from is typed here.
12
+ */
13
+ interface EbiBlastJson {
14
+ hits?: {
15
+ hit_acc?: string
16
+ hit_id?: string
17
+ hit_desc?: string
18
+ /** the UniProt fields are absent on hits from non-UniProt databases */
19
+ hit_os?: string
20
+ hit_uni_de?: string
21
+ hit_uni_os?: string
22
+ /** NCBI taxon id, delivered as a string */
23
+ hit_uni_ox?: string
24
+ hit_hsps?: { hsp_hseq?: string }[]
25
+ }[]
26
+ }
27
+
28
+ /**
29
+ * Map EBI's hit shape onto the normalized one. Exported for testing against a
30
+ * captured response — the field names are the whole risk here, and nothing else
31
+ * in CI would notice if EBI renamed one.
32
+ */
33
+ export function normalizeEbiBlastHits(result: EbiBlastJson): BlastHit[] {
34
+ return (result.hits ?? []).map(hit => {
35
+ const taxid = Number.parseInt(hit.hit_uni_ox ?? '', 10)
36
+ return {
37
+ description: [
38
+ {
39
+ accession: hit.hit_acc ?? 'unknown',
40
+ id: hit.hit_id ?? hit.hit_acc ?? 'unknown',
41
+ sciname: hit.hit_uni_os ?? hit.hit_os ?? 'unknown',
42
+ taxid: Number.isNaN(taxid) ? undefined : taxid,
43
+ // hit_uni_de is the bare protein name; hit_desc repeats it with the
44
+ // OS=/OX=/GN= suffix that makeId already covers with real columns
45
+ title: hit.hit_uni_de ?? hit.hit_desc,
46
+ },
47
+ ],
48
+ hsps: (hit.hit_hsps ?? []).flatMap(hsp =>
49
+ hsp.hsp_hseq ? [{ hseq: hsp.hsp_hseq }] : [],
50
+ ),
51
+ }
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Human-facing link to a job, shown while it runs and on error — so it has to
57
+ * be EBI's own results UI, not the REST result endpoint, which does not exist
58
+ * yet at the moment the link is on screen.
59
+ */
60
+ export function ebiBlastResultUrl(jobId: string) {
61
+ return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`
62
+ }
63
+
64
+ export async function queryEbiBlastFromJobId({
65
+ jobId,
66
+ onProgress,
67
+ }: {
68
+ jobId: string
69
+ onProgress: (arg: string) => void
70
+ }) {
71
+ onProgress(`Checking BLAST status for job: ${jobId}...`)
72
+ await waitForEbiJob({
73
+ tool: TOOL,
74
+ jobId,
75
+ onCountdown: s => {
76
+ onProgress(`Re-checking BLAST status in... ${s}`)
77
+ },
78
+ })
79
+
80
+ const hits = normalizeEbiBlastHits(
81
+ JSON.parse(
82
+ await fetchEbiResult({ tool: TOOL, jobId, type: 'json' }),
83
+ ) as EbiBlastJson,
84
+ )
85
+ if (hits.length === 0) {
86
+ throw new Error('No hits found')
87
+ }
88
+ return { rid: jobId, hits }
89
+ }
90
+
91
+ export async function queryEbiBlast({
92
+ query,
93
+ blastDatabase,
94
+ onProgress,
95
+ onRid,
96
+ }: {
97
+ query: string
98
+ blastDatabase: BlastDatabase
99
+ onProgress: (arg: string) => void
100
+ onRid: (arg: string) => void
101
+ }) {
102
+ onProgress('Submitting to EBI BLAST...')
103
+ const jobId = await submitEbiJob({
104
+ tool: TOOL,
105
+ params: {
106
+ program: 'blastp',
107
+ stype: 'protein',
108
+ database: blastDatabase,
109
+ sequence: query,
110
+ },
111
+ })
112
+ onRid(jobId)
113
+ return queryEbiBlastFromJobId({ jobId, onProgress })
114
+ }
@@ -0,0 +1,91 @@
1
+ import { textfetch } from './fetch'
2
+ import { pollLoop } from './poll'
3
+ import { readLocalStorage } from './useLocalStorage'
4
+
5
+ /**
6
+ * EBI's Job Dispatcher REST services (clustalo, muscle, ncbiblast, ...) all
7
+ * speak the same run/status/result protocol, so the transport lives here and
8
+ * each tool only supplies its own parameters and result types.
9
+ *
10
+ * Unlike NCBI's Blast.cgi these endpoints send `Access-Control-Allow-Origin: *`,
11
+ * which is why the BLAST backend moved here — see docs/blast.md.
12
+ */
13
+ export const EBI_BASE = 'https://www.ebi.ac.uk/Tools/services/rest'
14
+
15
+ /**
16
+ * EBI asks for a contact address on every submission so they can reach whoever
17
+ * is generating the load. A deployment that sends real volume should point this
18
+ * at its own maintainer via the BLAST settings dialog — otherwise every
19
+ * msaview job in the world is attributed to one person.
20
+ */
21
+ export const EBI_EMAIL_STORAGE_KEY = 'msa-ebiContactEmail'
22
+ export const DEFAULT_EBI_EMAIL = 'colin.diesh@gmail.com'
23
+
24
+ export function getEbiEmail() {
25
+ const configured = readLocalStorage(
26
+ EBI_EMAIL_STORAGE_KEY,
27
+ DEFAULT_EBI_EMAIL,
28
+ ).trim()
29
+ return configured || DEFAULT_EBI_EMAIL
30
+ }
31
+
32
+ /** Statuses that mean the job is over and produced no result. */
33
+ const FAILED_STATUSES = new Set(['ERROR', 'FAILURE', 'NOT_FOUND'])
34
+
35
+ export async function submitEbiJob({
36
+ tool,
37
+ params,
38
+ }: {
39
+ tool: string
40
+ params: Record<string, string>
41
+ }) {
42
+ const jobId = await textfetch(`${EBI_BASE}/${tool}/run`, {
43
+ method: 'POST',
44
+ body: new URLSearchParams({ email: getEbiEmail(), ...params }),
45
+ })
46
+ return jobId.trim()
47
+ }
48
+
49
+ export async function waitForEbiJob({
50
+ tool,
51
+ jobId,
52
+ intervalSeconds = 10,
53
+ onCountdown,
54
+ }: {
55
+ tool: string
56
+ jobId: string
57
+ intervalSeconds?: number
58
+ onCountdown: (secondsRemaining: number) => void
59
+ }) {
60
+ await pollLoop({
61
+ intervalSeconds,
62
+ onCountdown,
63
+ check: async () => {
64
+ // exact match, not includes(): a job whose status is ERROR must not be
65
+ // able to poll forever waiting for a FINISHED that will never arrive
66
+ const status = (
67
+ await textfetch(`${EBI_BASE}/${tool}/status/${jobId}`)
68
+ ).trim()
69
+ if (status === 'FINISHED') {
70
+ return true
71
+ }
72
+ if (FAILED_STATUSES.has(status)) {
73
+ throw new Error(`EBI ${tool} job ${jobId} returned status ${status}`)
74
+ }
75
+ // RUNNING, QUEUED, PENDING and anything else EBI adds later
76
+ return false
77
+ },
78
+ })
79
+ }
80
+
81
+ export async function fetchEbiResult({
82
+ tool,
83
+ jobId,
84
+ type,
85
+ }: {
86
+ tool: string
87
+ jobId: string
88
+ type: string
89
+ }) {
90
+ return textfetch(`${EBI_BASE}/${tool}/result/${jobId}/${type}`)
91
+ }
@@ -1,5 +1,30 @@
1
+ function hostOf(url: string) {
2
+ try {
3
+ return new URL(url).host
4
+ } catch {
5
+ // a relative url, e.g. a same-origin proxy path
6
+ return url
7
+ }
8
+ }
9
+
1
10
  export async function handleFetch(url: string, args?: RequestInit) {
2
- const response = await fetch(url, args)
11
+ let response: Response
12
+ try {
13
+ response = await fetch(url, args)
14
+ } catch (e) {
15
+ // fetch rejects with a bare "TypeError: Failed to fetch" for every network
16
+ // level failure, including a cross-origin response the browser refused to
17
+ // hand over. That is not a hypothetical: NCBI's Blast.cgi stopped sending
18
+ // Access-Control-Allow-Origin to third-party origins, and the raw TypeError
19
+ // told users nothing at all about why. See docs/blast.md.
20
+ if (e instanceof TypeError) {
21
+ throw new Error(
22
+ `Could not reach ${hostOf(url)} — the request failed at the network level. This is usually a CORS restriction (the server refused to let this site read the response), an offline connection, or a blocked request.`,
23
+ { cause: e },
24
+ )
25
+ }
26
+ throw e
27
+ }
3
28
 
4
29
  if (!response.ok) {
5
30
  throw new Error(
package/src/utils/msa.ts CHANGED
@@ -1,10 +1,6 @@
1
- import { textfetch } from './fetch'
2
- import { pollLoop } from './poll'
1
+ import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
3
2
 
4
- import type { MsaAlgorithm } from '../LaunchMsaView/components/NCBIBlastQuery/consts'
5
-
6
- const base = `https://www.ebi.ac.uk/Tools/services/rest`
7
- const email = 'colin.diesh@gmail.com'
3
+ import type { MsaAlgorithm } from '../LaunchMsaView/components/BlastQuery/consts'
8
4
 
9
5
  const algorithms: Record<
10
6
  MsaAlgorithm,
@@ -15,54 +11,27 @@ const algorithms: Record<
15
11
  }
16
12
  > = {
17
13
  clustalo: {
18
- params: { email },
14
+ params: {},
19
15
  msaResult: 'aln-clustal_num',
20
16
  treeResult: 'phylotree',
21
17
  },
22
18
  muscle: {
23
- params: { email, format: 'clw', tree: 'tree1' },
19
+ params: { format: 'clw', tree: 'tree1' },
24
20
  msaResult: 'fa',
25
21
  treeResult: 'phylotree',
26
22
  },
27
23
  kalign: {
28
- params: { email, stype: 'protein' },
24
+ params: { stype: 'protein' },
29
25
  msaResult: 'fa',
30
26
  treeResult: 'phylotree',
31
27
  },
32
28
  mafft: {
33
- params: { email, stype: 'protein' },
29
+ params: { stype: 'protein' },
34
30
  msaResult: 'fa',
35
31
  treeResult: 'phylotree',
36
32
  },
37
33
  }
38
34
 
39
- async function wait({
40
- onProgress,
41
- jobId,
42
- algorithm,
43
- }: {
44
- jobId: string
45
- algorithm: MsaAlgorithm
46
- onProgress: (arg: string) => void
47
- }) {
48
- await pollLoop({
49
- intervalSeconds: 10,
50
- onCountdown: s => {
51
- onProgress(`Re-checking MSA status in... ${s}`)
52
- },
53
- check: async () => {
54
- const result = await textfetch(`${base}/${algorithm}/status/${jobId}`)
55
- if (result.includes('FINISHED')) {
56
- return true
57
- }
58
- if (result.includes('FAILURE')) {
59
- throw new Error(`Failed to run: jobId ${jobId}`)
60
- }
61
- return false
62
- },
63
- })
64
- }
65
-
66
35
  export async function launchMSA({
67
36
  algorithm,
68
37
  sequence,
@@ -76,17 +45,27 @@ export async function launchMSA({
76
45
 
77
46
  onProgress(`Launching ${algorithm} MSA...`)
78
47
 
79
- const jobId = await textfetch(`${base}/${algorithm}/run`, {
80
- method: 'POST',
81
- body: new URLSearchParams({ ...config.params, sequence }),
48
+ const jobId = await submitEbiJob({
49
+ tool: algorithm,
50
+ params: { ...config.params, sequence },
51
+ })
52
+ await waitForEbiJob({
53
+ tool: algorithm,
54
+ jobId,
55
+ onCountdown: s => {
56
+ onProgress(`Re-checking MSA status in... ${s}`)
57
+ },
82
58
  })
83
- await wait({ jobId, algorithm, onProgress })
84
59
  return {
85
- msa: await textfetch(
86
- `${base}/${algorithm}/result/${jobId}/${config.msaResult}`,
87
- ),
88
- tree: await textfetch(
89
- `${base}/${algorithm}/result/${jobId}/${config.treeResult}`,
90
- ),
60
+ msa: await fetchEbiResult({
61
+ tool: algorithm,
62
+ jobId,
63
+ type: config.msaResult,
64
+ }),
65
+ tree: await fetchEbiResult({
66
+ tool: algorithm,
67
+ jobId,
68
+ type: config.treeResult,
69
+ }),
91
70
  }
92
71
  }
@@ -6,17 +6,12 @@ export interface BlastHitDescription {
6
6
  title?: string
7
7
  }
8
8
 
9
- export interface BlastResults {
10
- BlastOutput2: {
11
- report: {
12
- results: {
13
- search: {
14
- hits: {
15
- description: BlastHitDescription[]
16
- hsps: { hseq: string }[]
17
- }[]
18
- }
19
- }
20
- }
21
- }[]
9
+ /**
10
+ * The shape utils/ebiBlast.ts normalizes EBI's hits into. It keeps the field
11
+ * names NCBI's JSON2 used, so everything downstream of the search — row naming,
12
+ * taxonomy lookup, the MSA — was left untouched when the backend moved.
13
+ */
14
+ export interface BlastHit {
15
+ description: BlastHitDescription[]
16
+ hsps: { hseq: string }[]
22
17
  }
@@ -5,7 +5,7 @@ import { useState } from 'react'
5
5
  // throw "(0, PR.useLocalStorage) is not a function" on hosts built during that
6
6
  // window. Same failure mode as `defaultCodonTable`; keeping our own copy takes
7
7
  // this plugin out of the whack-a-mole.
8
- function readLocalStorage<T>(key: string, initialValue: T): T {
8
+ export function readLocalStorage<T>(key: string, initialValue: T): T {
9
9
  try {
10
10
  const item = globalThis.localStorage.getItem(key)
11
11
  return item === null ? initialValue : (JSON.parse(item) as T)
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '2.8.1'
1
+ export const version = '2.9.0'
@@ -1,10 +0,0 @@
1
- import React from 'react';
2
- import type { AbstractTrackModel, Feature } from '@jbrowse/core/util';
3
- declare const NCBIBlastAutomaticPanel: ({ handleClose, feature, model, children, baseUrl, }: {
4
- model: AbstractTrackModel;
5
- feature: Feature;
6
- baseUrl: string;
7
- handleClose: () => void;
8
- children: React.ReactNode;
9
- }) => React.JSX.Element;
10
- export default NCBIBlastAutomaticPanel;