jbrowse-plugin-msaview 2.5.0 → 2.5.2
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.
- package/dist/LaunchMsaViewExtensionPoint/index.js +2 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +22 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.js +83 -26
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +16 -0
- package/dist/MsaViewPanel/loadProteinDomains.js +33 -0
- package/dist/MsaViewPanel/model.d.ts +189 -158
- package/dist/MsaViewPanel/model.js +17 -5
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +28 -28
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/domainCache.d.ts +8 -0
- package/dist/utils/domainCache.js +28 -0
- package/dist/utils/eutils.d.ts +3 -0
- package/dist/utils/eutils.js +14 -0
- package/dist/utils/msa.js +18 -15
- package/dist/utils/ncbiBlast.js +22 -24
- package/dist/utils/ncbiDomains.d.ts +39 -0
- package/dist/utils/ncbiDomains.js +156 -0
- package/dist/utils/poll.d.ts +11 -0
- package/dist/utils/poll.js +19 -0
- package/dist/utils/taxonomyNames.js +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +22 -19
- package/src/LaunchMsaViewExtensionPoint/index.ts +3 -0
- package/src/MsaViewPanel/afterCreateAutoruns.ts +86 -29
- package/src/MsaViewPanel/loadProteinDomains.ts +57 -0
- package/src/MsaViewPanel/model.ts +22 -4
- package/src/utils/domainCache.ts +43 -0
- package/src/utils/eutils.ts +16 -0
- package/src/utils/msa.ts +18 -16
- package/src/utils/ncbiBlast.ts +27 -31
- package/src/utils/ncbiDomains.ts +173 -0
- package/src/utils/poll.ts +28 -0
- package/src/utils/taxonomyNames.ts +3 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fetchProteinDomains } from '../utils/ncbiDomains'
|
|
2
|
+
|
|
3
|
+
import type { InterProScanResults } from 'react-msaview'
|
|
4
|
+
|
|
5
|
+
// structural subset of the MSA model: the full model type can't be used here
|
|
6
|
+
// because it references this very action, creating a self-referential cycle
|
|
7
|
+
interface DomainModel {
|
|
8
|
+
data: { treeMetadata?: string }
|
|
9
|
+
setProgress: (arg: string) => void
|
|
10
|
+
setDomains: (data: Record<string, InterProScanResults>) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Overlay protein domains on the alignment using NCBI's pre-computed CDD
|
|
15
|
+
* annotations. The BLAST workflow stores each hit's accession in treeMetadata,
|
|
16
|
+
* so we look those up via efetch and key the results by MSA row name (which is
|
|
17
|
+
* what react-msaview matches domains against).
|
|
18
|
+
*/
|
|
19
|
+
export async function loadProteinDomains(self: DomainModel) {
|
|
20
|
+
const metadataJson = self.data.treeMetadata
|
|
21
|
+
if (!metadataJson) {
|
|
22
|
+
throw new Error('No sequence metadata available to look up domains')
|
|
23
|
+
}
|
|
24
|
+
const metadata = JSON.parse(metadataJson) as Record<
|
|
25
|
+
string,
|
|
26
|
+
Record<string, string>
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
const rowAccessions = Object.entries(metadata)
|
|
30
|
+
.map(([rowName, meta]) => ({ rowName, accession: meta.Accession }))
|
|
31
|
+
.filter((r): r is { rowName: string; accession: string } => !!r.accession)
|
|
32
|
+
|
|
33
|
+
if (rowAccessions.length === 0) {
|
|
34
|
+
throw new Error('No NCBI accessions found in alignment rows')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
self.setProgress(
|
|
38
|
+
`Fetching protein domains from NCBI for ${rowAccessions.length} sequences...`,
|
|
39
|
+
)
|
|
40
|
+
const byAccession = await fetchProteinDomains(
|
|
41
|
+
rowAccessions.map(r => r.accession),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const annotations: Record<string, InterProScanResults> = {}
|
|
45
|
+
for (const { rowName, accession } of rowAccessions) {
|
|
46
|
+
const matches = byAccession.get(accession)
|
|
47
|
+
if (matches && matches.length > 0) {
|
|
48
|
+
annotations[rowName] = { matches, xref: [{ id: rowName }] }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Object.keys(annotations).length === 0) {
|
|
53
|
+
throw new Error('No CDD domain annotations found for these proteins')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
self.setDomains(annotations)
|
|
57
|
+
}
|
|
@@ -7,8 +7,13 @@ import { genomeToTranscriptSeqMapping } from 'g2p_mapper'
|
|
|
7
7
|
import { autorun } from 'mobx'
|
|
8
8
|
import { MSAModelF } from 'react-msaview'
|
|
9
9
|
|
|
10
|
+
// re-exported so the inferred (composed) state-model type can name MSAFormat
|
|
11
|
+
// from msa-parsers when emitting declarations (avoids TS2883 portability error)
|
|
12
|
+
export type { MSAFormat } from 'msa-parsers'
|
|
13
|
+
|
|
10
14
|
import {
|
|
11
15
|
autoConnectStructures,
|
|
16
|
+
autoLoadProteinDomains,
|
|
12
17
|
highlightConnectedStructures,
|
|
13
18
|
launchBlastIfNeeded,
|
|
14
19
|
loadStoredData,
|
|
@@ -77,9 +82,6 @@ export default function stateModelFactory() {
|
|
|
77
82
|
* #property
|
|
78
83
|
*/
|
|
79
84
|
connectedFeature: types.frozen(),
|
|
80
|
-
/**
|
|
81
|
-
* #property
|
|
82
|
-
*/
|
|
83
85
|
/**
|
|
84
86
|
* #property
|
|
85
87
|
*/
|
|
@@ -128,6 +130,7 @@ export default function stateModelFactory() {
|
|
|
128
130
|
error: unknown
|
|
129
131
|
loadingStoredData: boolean
|
|
130
132
|
isStoringData: boolean
|
|
133
|
+
domainsRequested: boolean
|
|
131
134
|
} => ({
|
|
132
135
|
/**
|
|
133
136
|
* #volatile
|
|
@@ -149,6 +152,12 @@ export default function stateModelFactory() {
|
|
|
149
152
|
* #volatile
|
|
150
153
|
*/
|
|
151
154
|
isStoringData: false,
|
|
155
|
+
/**
|
|
156
|
+
* #volatile
|
|
157
|
+
* guards the one-shot auto-fetch of protein domains so it doesn't refire
|
|
158
|
+
* when NCBI returns no domains (leaving interProAnnotations undefined)
|
|
159
|
+
*/
|
|
160
|
+
domainsRequested: false,
|
|
152
161
|
}),
|
|
153
162
|
)
|
|
154
163
|
|
|
@@ -295,6 +304,12 @@ export default function stateModelFactory() {
|
|
|
295
304
|
setIsStoringData(arg: boolean) {
|
|
296
305
|
self.isStoringData = arg
|
|
297
306
|
},
|
|
307
|
+
/**
|
|
308
|
+
* #action
|
|
309
|
+
*/
|
|
310
|
+
setDomainsRequested(arg: boolean) {
|
|
311
|
+
self.domainsRequested = arg
|
|
312
|
+
},
|
|
298
313
|
/**
|
|
299
314
|
* #action
|
|
300
315
|
*/
|
|
@@ -455,7 +470,7 @@ export default function stateModelFactory() {
|
|
|
455
470
|
processInit,
|
|
456
471
|
highlightConnectedStructures,
|
|
457
472
|
autoConnectStructures,
|
|
458
|
-
|
|
473
|
+
autoLoadProteinDomains,
|
|
459
474
|
]) {
|
|
460
475
|
addDisposer(
|
|
461
476
|
self,
|
|
@@ -464,7 +479,10 @@ export default function stateModelFactory() {
|
|
|
464
479
|
}),
|
|
465
480
|
)
|
|
466
481
|
}
|
|
482
|
+
// these two keep per-reaction state across runs (a "did I set it?" flag),
|
|
483
|
+
// so they're factories returning the autorun body rather than plain fns
|
|
467
484
|
addDisposer(self, autorun(syncGenomeHoverToMsaColumn(self)))
|
|
485
|
+
addDisposer(self, autorun(observeProteinHighlights(self)))
|
|
468
486
|
},
|
|
469
487
|
}))
|
|
470
488
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { openDB } from 'idb'
|
|
2
|
+
|
|
3
|
+
import type { DomainMatch } from './ncbiDomains'
|
|
4
|
+
|
|
5
|
+
const DB_NAME = 'jbrowse-msaview-domain-cache'
|
|
6
|
+
const STORE_NAME = 'domains'
|
|
7
|
+
const DB_VERSION = 1
|
|
8
|
+
|
|
9
|
+
interface CachedDomain {
|
|
10
|
+
accession: string
|
|
11
|
+
matches: DomainMatch[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function getDB() {
|
|
15
|
+
return openDB(DB_NAME, DB_VERSION, {
|
|
16
|
+
upgrade(db) {
|
|
17
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
18
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'accession' })
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getCachedDomains(accessions: string[]) {
|
|
25
|
+
const db = await getDB()
|
|
26
|
+
const tx = db.transaction(STORE_NAME, 'readonly')
|
|
27
|
+
const results = await Promise.all(
|
|
28
|
+
accessions.map(
|
|
29
|
+
accession => tx.store.get(accession) as Promise<CachedDomain | undefined>,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
await tx.done
|
|
33
|
+
return results
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function saveDomains(entries: CachedDomain[]) {
|
|
37
|
+
const db = await getDB()
|
|
38
|
+
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
await tx.store.put(entry)
|
|
41
|
+
}
|
|
42
|
+
await tx.done
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// NCBI asks that programmatic E-utilities requests identify themselves with a
|
|
2
|
+
// tool name and contact email so they can reach out before throttling, rather
|
|
3
|
+
// than silently rate-limiting. https://www.ncbi.nlm.nih.gov/books/NBK25497/
|
|
4
|
+
export const NCBI_TOOL = 'jbrowse-plugin-msaview'
|
|
5
|
+
export const NCBI_EMAIL = 'colin.diesh@gmail.com'
|
|
6
|
+
|
|
7
|
+
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils'
|
|
8
|
+
|
|
9
|
+
export function efetchUrl(params: Record<string, string>) {
|
|
10
|
+
const search = new URLSearchParams({
|
|
11
|
+
...params,
|
|
12
|
+
tool: NCBI_TOOL,
|
|
13
|
+
email: NCBI_EMAIL,
|
|
14
|
+
})
|
|
15
|
+
return `${EUTILS}/efetch.fcgi?${search.toString()}`
|
|
16
|
+
}
|
package/src/utils/msa.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { textfetch
|
|
1
|
+
import { textfetch } from './fetch'
|
|
2
|
+
import { pollLoop } from './poll'
|
|
2
3
|
|
|
3
4
|
import type { MsaAlgorithm } from '../LaunchMsaView/components/NCBIBlastQuery/consts'
|
|
4
5
|
|
|
@@ -44,21 +45,22 @@ async function wait({
|
|
|
44
45
|
algorithm: MsaAlgorithm
|
|
45
46
|
onProgress: (arg: string) => void
|
|
46
47
|
}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
})
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export async function launchMSA({
|
package/src/utils/ncbiBlast.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { jsonfetch, textfetch
|
|
1
|
+
import { jsonfetch, textfetch } from './fetch'
|
|
2
|
+
import { pollLoop } from './poll'
|
|
2
3
|
|
|
3
4
|
import type { BlastResults } from './types'
|
|
4
5
|
import type {
|
|
@@ -110,38 +111,33 @@ async function waitForRid({
|
|
|
110
111
|
onProgress: (arg: string) => void
|
|
111
112
|
baseUrl: string
|
|
112
113
|
}) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
await pollLoop({
|
|
115
|
+
intervalSeconds: 20,
|
|
116
|
+
onCountdown: s => {
|
|
117
|
+
onProgress(`Re-checking BLAST status in... ${s}`)
|
|
118
|
+
},
|
|
119
|
+
check: async () => {
|
|
120
|
+
const res = await textfetch(
|
|
121
|
+
`${baseUrl}?CMD=Get&FORMAT_OBJECT=SearchInfo&RID=${rid}`,
|
|
122
|
+
)
|
|
123
|
+
const status = /\s+Status=(\S+)/m.exec(res)?.[1]
|
|
124
|
+
const hasHits = /\s+ThereAreHits=yes/m.test(res)
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for (let i = 0; i < iter; i++) {
|
|
125
|
-
onProgress(`Re-checking BLAST status in... ${iter - i}`)
|
|
126
|
-
await timeout(1000)
|
|
126
|
+
if (status === 'WAITING') {
|
|
127
|
+
return false
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (status === 'READY') {
|
|
136
|
-
if (hasHits) {
|
|
137
|
-
return true
|
|
138
|
-
} else {
|
|
129
|
+
if (status === 'FAILED') {
|
|
130
|
+
throw new Error(`BLAST ${rid} failed`)
|
|
131
|
+
}
|
|
132
|
+
if (status === 'READY') {
|
|
133
|
+
if (hasHits) {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
139
136
|
throw new Error('No hits found')
|
|
140
137
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
138
|
+
throw new Error(
|
|
139
|
+
`BLAST ${rid} returned unexpected status: ${status ?? 'unknown'}`,
|
|
140
|
+
)
|
|
141
|
+
},
|
|
142
|
+
})
|
|
147
143
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { getCachedDomains, saveDomains } from './domainCache'
|
|
2
|
+
import { efetchUrl } from './eutils'
|
|
3
|
+
import { textfetch } from './fetch'
|
|
4
|
+
|
|
5
|
+
import type { InterProScanResults } from 'react-msaview'
|
|
6
|
+
|
|
7
|
+
export type DomainMatch = InterProScanResults['matches'][number]
|
|
8
|
+
|
|
9
|
+
function field(xml: string, tag: string) {
|
|
10
|
+
return new RegExp(`<${tag}>(.*?)</${tag}>`, 's').exec(xml)?.[1]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseQualifiers(featureXml: string) {
|
|
14
|
+
const quals: Record<string, string> = {}
|
|
15
|
+
const re = /<GBQualifier>([\s\S]*?)<\/GBQualifier>/g
|
|
16
|
+
let m
|
|
17
|
+
while ((m = re.exec(featureXml)) !== null) {
|
|
18
|
+
const name = field(m[1]!, 'GBQualifier_name')
|
|
19
|
+
const value = field(m[1]!, 'GBQualifier_value')
|
|
20
|
+
// keep the first occurrence: NCBI lists the canonical value first
|
|
21
|
+
if (name && value !== undefined && quals[name] === undefined) {
|
|
22
|
+
quals[name] = value
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return quals
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A feature can span several intervals: domains are usually one contiguous
|
|
29
|
+
// range, but CDD Sites (e.g. an active site) are a set of scattered residues
|
|
30
|
+
// expressed as multiple GBInterval ranges and single GBInterval_point residues.
|
|
31
|
+
// We collapse those to a single bounding span so a site renders as one box
|
|
32
|
+
// rather than a spray of 1px specks.
|
|
33
|
+
function parseBoundingSpan(featureXml: string) {
|
|
34
|
+
const starts: number[] = []
|
|
35
|
+
const ends: number[] = []
|
|
36
|
+
const re = /<GBInterval>([\s\S]*?)<\/GBInterval>/g
|
|
37
|
+
let m
|
|
38
|
+
while ((m = re.exec(featureXml)) !== null) {
|
|
39
|
+
const block = m[1]!
|
|
40
|
+
const from = field(block, 'GBInterval_from')
|
|
41
|
+
const to = field(block, 'GBInterval_to')
|
|
42
|
+
const point = field(block, 'GBInterval_point')
|
|
43
|
+
if (from && to) {
|
|
44
|
+
starts.push(Number(from))
|
|
45
|
+
ends.push(Number(to))
|
|
46
|
+
} else if (point) {
|
|
47
|
+
starts.push(Number(point))
|
|
48
|
+
ends.push(Number(point))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return starts.length > 0
|
|
52
|
+
? { start: Math.min(...starts), end: Math.max(...ends) }
|
|
53
|
+
: undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Drop single-residue specks (acetylation/phospho points) but keep every
|
|
57
|
+
// genuine domain and functional site; react-msaview draws longest-first, so
|
|
58
|
+
// smaller features (binding sites, loops) layer on top of the domain they sit
|
|
59
|
+
// inside.
|
|
60
|
+
const MIN_FEATURE_LENGTH = 2
|
|
61
|
+
|
|
62
|
+
function parseFeature(featureXml: string): DomainMatch | undefined {
|
|
63
|
+
const key = field(featureXml, 'GBFeature_key')
|
|
64
|
+
const quals = parseQualifiers(featureXml)
|
|
65
|
+
const xref = quals.db_xref
|
|
66
|
+
const span = parseBoundingSpan(featureXml)
|
|
67
|
+
// only CDD-backed Regions/Sites are conserved-domain annotations; Regions and
|
|
68
|
+
// Sites without a CDD xref are UniProt-propagated point motifs we don't want
|
|
69
|
+
if (
|
|
70
|
+
(key === 'Region' || key === 'Site') &&
|
|
71
|
+
xref?.startsWith('CDD:') &&
|
|
72
|
+
span &&
|
|
73
|
+
span.end - span.start + 1 >= MIN_FEATURE_LENGTH
|
|
74
|
+
) {
|
|
75
|
+
const cddId = xref.replace('CDD:', '')
|
|
76
|
+
const isDomain = key === 'Region'
|
|
77
|
+
// a site's note (e.g. "ATP binding site [chemical binding]") is more
|
|
78
|
+
// specific than its generic site_type ("other"), so prefer it for the name
|
|
79
|
+
// — that gives each functional site its own color/legend/filter entry
|
|
80
|
+
const noteName = quals.note?.split(/[[(]/)[0]?.trim()
|
|
81
|
+
const name = isDomain
|
|
82
|
+
? (quals.region_name ?? cddId)
|
|
83
|
+
: noteName || quals.site_type || 'site'
|
|
84
|
+
const accession = isDomain ? cddId : `${cddId}:${name}`
|
|
85
|
+
return {
|
|
86
|
+
signature: {
|
|
87
|
+
entry: { name, description: quals.note ?? name, accession },
|
|
88
|
+
},
|
|
89
|
+
locations: [span],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse a GenPept (efetch db=protein&rettype=gp&retmode=xml) document into CDD
|
|
97
|
+
* domain and site annotations, keyed by both the versioned and primary
|
|
98
|
+
* accession so callers can look up by whichever NCBI returned.
|
|
99
|
+
*/
|
|
100
|
+
export function parseCddDomains(xml: string) {
|
|
101
|
+
const byAccession = new Map<string, DomainMatch[]>()
|
|
102
|
+
const seqRe = /<GBSeq>([\s\S]*?)<\/GBSeq>/g
|
|
103
|
+
let seqMatch
|
|
104
|
+
while ((seqMatch = seqRe.exec(xml)) !== null) {
|
|
105
|
+
const seqXml = seqMatch[1]!
|
|
106
|
+
const matches: DomainMatch[] = []
|
|
107
|
+
|
|
108
|
+
const featRe = /<GBFeature>([\s\S]*?)<\/GBFeature>/g
|
|
109
|
+
let featMatch
|
|
110
|
+
while ((featMatch = featRe.exec(seqXml)) !== null) {
|
|
111
|
+
const match = parseFeature(featMatch[1]!)
|
|
112
|
+
if (match) {
|
|
113
|
+
matches.push(match)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const acc of [
|
|
118
|
+
field(seqXml, 'GBSeq_accession-version'),
|
|
119
|
+
field(seqXml, 'GBSeq_primary-accession'),
|
|
120
|
+
]) {
|
|
121
|
+
if (acc) {
|
|
122
|
+
byAccession.set(acc, matches)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return byAccession
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fetch pre-computed CDD domain and site annotations for NCBI protein
|
|
131
|
+
* accessions. These come baked into the GenPept records, so a single batched
|
|
132
|
+
* efetch returns them with no job submission or polling. Results are cached in
|
|
133
|
+
* IndexedDB so reopening a view doesn't refetch.
|
|
134
|
+
*/
|
|
135
|
+
export async function fetchProteinDomains(accessions: string[]) {
|
|
136
|
+
const unique = [...new Set(accessions)].filter(Boolean)
|
|
137
|
+
const byAccession = new Map<string, DomainMatch[]>()
|
|
138
|
+
|
|
139
|
+
const cached = await getCachedDomains(unique)
|
|
140
|
+
const uncached: string[] = []
|
|
141
|
+
unique.forEach((acc, i) => {
|
|
142
|
+
const hit = cached[i]
|
|
143
|
+
if (hit) {
|
|
144
|
+
byAccession.set(acc, hit.matches)
|
|
145
|
+
} else {
|
|
146
|
+
uncached.push(acc)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const toCache: { accession: string; matches: DomainMatch[] }[] = []
|
|
151
|
+
const batchSize = 100
|
|
152
|
+
for (let i = 0; i < uncached.length; i += batchSize) {
|
|
153
|
+
const batch = uncached.slice(i, i + batchSize)
|
|
154
|
+
const xml = await textfetch(
|
|
155
|
+
efetchUrl({
|
|
156
|
+
db: 'protein',
|
|
157
|
+
id: batch.join(','),
|
|
158
|
+
rettype: 'gp',
|
|
159
|
+
retmode: 'xml',
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
const parsed = parseCddDomains(xml)
|
|
163
|
+
for (const acc of batch) {
|
|
164
|
+
const matches = parsed.get(acc) ?? []
|
|
165
|
+
byAccession.set(acc, matches)
|
|
166
|
+
toCache.push({ accession: acc, matches })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (toCache.length > 0) {
|
|
170
|
+
await saveDomains(toCache)
|
|
171
|
+
}
|
|
172
|
+
return byAccession
|
|
173
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { timeout } from './fetch'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Poll a remote job until it reports done. `check` returns true when finished,
|
|
5
|
+
* false when still pending, and throws on failure. Between checks it counts down
|
|
6
|
+
* `intervalSeconds`, calling `onCountdown` each second so the UI can show
|
|
7
|
+
* progress.
|
|
8
|
+
*/
|
|
9
|
+
export async function pollLoop({
|
|
10
|
+
check,
|
|
11
|
+
intervalSeconds,
|
|
12
|
+
onCountdown,
|
|
13
|
+
}: {
|
|
14
|
+
check: () => Promise<boolean>
|
|
15
|
+
intervalSeconds: number
|
|
16
|
+
onCountdown: (secondsRemaining: number) => void
|
|
17
|
+
}) {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
19
|
+
while (true) {
|
|
20
|
+
if (await check()) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
for (let i = intervalSeconds; i > 0; i--) {
|
|
24
|
+
onCountdown(i)
|
|
25
|
+
await timeout(1000)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { openDB } from 'idb'
|
|
2
2
|
|
|
3
|
+
import { efetchUrl } from './eutils'
|
|
4
|
+
|
|
3
5
|
const DB_NAME = 'jbrowse-msaview-taxonomy-cache'
|
|
4
6
|
const STORE_NAME = 'common-names'
|
|
5
7
|
const DB_VERSION = 2
|
|
@@ -80,7 +82,7 @@ export async function fetchTaxonomyInfo(
|
|
|
80
82
|
|
|
81
83
|
try {
|
|
82
84
|
const response = await fetch(
|
|
83
|
-
|
|
85
|
+
efetchUrl({ db: 'taxonomy', id: idsParam, retmode: 'xml' }),
|
|
84
86
|
)
|
|
85
87
|
const text = await response.text()
|
|
86
88
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.5.
|
|
1
|
+
export const version = '2.5.2'
|