jbrowse-plugin-msaview 2.4.5 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/LaunchMsaView/util.js +1 -1
  2. package/dist/LaunchMsaViewExtensionPoint/index.js +2 -1
  3. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
  4. package/dist/MsaViewPanel/afterCreateAutoruns.js +53 -0
  5. package/dist/MsaViewPanel/loadProteinDomains.d.ts +16 -0
  6. package/dist/MsaViewPanel/loadProteinDomains.js +33 -0
  7. package/dist/MsaViewPanel/model.d.ts +69 -399
  8. package/dist/MsaViewPanel/model.js +17 -39
  9. package/dist/MsaViewPanel/pairwiseAlignment.js +2 -9
  10. package/dist/MsaViewPanel/structureConnection.d.ts +0 -6
  11. package/dist/MsaViewPanel/structureConnection.js +0 -16
  12. package/dist/MsaViewPanel/structureConnection.test.js +1 -51
  13. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.d.ts +1 -0
  14. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +92 -0
  15. package/dist/jbrowse-plugin-msaview.umd.production.min.js +30 -30
  16. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  17. package/dist/utils/domainCache.d.ts +8 -0
  18. package/dist/utils/domainCache.js +28 -0
  19. package/dist/utils/eutils.d.ts +3 -0
  20. package/dist/utils/eutils.js +14 -0
  21. package/dist/utils/msa.js +23 -19
  22. package/dist/utils/ncbiBlast.js +23 -25
  23. package/dist/utils/ncbiDomains.d.ts +39 -0
  24. package/dist/utils/ncbiDomains.js +154 -0
  25. package/dist/utils/poll.d.ts +11 -0
  26. package/dist/utils/poll.js +19 -0
  27. package/dist/utils/taxonomyNames.js +2 -1
  28. package/dist/version.d.ts +1 -1
  29. package/dist/version.js +1 -1
  30. package/package.json +17 -14
  31. package/src/LaunchMsaView/util.ts +1 -3
  32. package/src/LaunchMsaViewExtensionPoint/index.ts +3 -0
  33. package/src/MsaViewPanel/afterCreateAutoruns.ts +54 -0
  34. package/src/MsaViewPanel/loadProteinDomains.ts +57 -0
  35. package/src/MsaViewPanel/model.ts +23 -45
  36. package/src/MsaViewPanel/pairwiseAlignment.ts +2 -7
  37. package/src/MsaViewPanel/structureConnection.test.ts +1 -61
  38. package/src/MsaViewPanel/structureConnection.ts +0 -22
  39. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +112 -0
  40. package/src/utils/domainCache.ts +44 -0
  41. package/src/utils/eutils.ts +16 -0
  42. package/src/utils/msa.ts +23 -19
  43. package/src/utils/ncbiBlast.ts +28 -33
  44. package/src/utils/ncbiDomains.ts +171 -0
  45. package/src/utils/poll.ts +28 -0
  46. package/src/utils/taxonomyNames.ts +3 -1
  47. package/src/version.ts +1 -1
@@ -0,0 +1,171 @@
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: { entry: { name, description: quals.note ?? name, accession } },
87
+ locations: [span],
88
+ }
89
+ }
90
+ return undefined
91
+ }
92
+
93
+ /**
94
+ * Parse a GenPept (efetch db=protein&rettype=gp&retmode=xml) document into CDD
95
+ * domain and site annotations, keyed by both the versioned and primary
96
+ * accession so callers can look up by whichever NCBI returned.
97
+ */
98
+ export function parseCddDomains(xml: string) {
99
+ const byAccession = new Map<string, DomainMatch[]>()
100
+ const seqRe = /<GBSeq>([\s\S]*?)<\/GBSeq>/g
101
+ let seqMatch
102
+ while ((seqMatch = seqRe.exec(xml)) !== null) {
103
+ const seqXml = seqMatch[1]!
104
+ const matches: DomainMatch[] = []
105
+
106
+ const featRe = /<GBFeature>([\s\S]*?)<\/GBFeature>/g
107
+ let featMatch
108
+ while ((featMatch = featRe.exec(seqXml)) !== null) {
109
+ const match = parseFeature(featMatch[1]!)
110
+ if (match) {
111
+ matches.push(match)
112
+ }
113
+ }
114
+
115
+ for (const acc of [
116
+ field(seqXml, 'GBSeq_accession-version'),
117
+ field(seqXml, 'GBSeq_primary-accession'),
118
+ ]) {
119
+ if (acc) {
120
+ byAccession.set(acc, matches)
121
+ }
122
+ }
123
+ }
124
+ return byAccession
125
+ }
126
+
127
+ /**
128
+ * Fetch pre-computed CDD domain and site annotations for NCBI protein
129
+ * accessions. These come baked into the GenPept records, so a single batched
130
+ * efetch returns them with no job submission or polling. Results are cached in
131
+ * IndexedDB so reopening a view doesn't refetch.
132
+ */
133
+ export async function fetchProteinDomains(accessions: string[]) {
134
+ const unique = [...new Set(accessions)].filter(Boolean)
135
+ const byAccession = new Map<string, DomainMatch[]>()
136
+
137
+ const cached = await getCachedDomains(unique)
138
+ const uncached: string[] = []
139
+ unique.forEach((acc, i) => {
140
+ const hit = cached[i]
141
+ if (hit) {
142
+ byAccession.set(acc, hit.matches)
143
+ } else {
144
+ uncached.push(acc)
145
+ }
146
+ })
147
+
148
+ const toCache: { accession: string; matches: DomainMatch[] }[] = []
149
+ const batchSize = 100
150
+ for (let i = 0; i < uncached.length; i += batchSize) {
151
+ const batch = uncached.slice(i, i + batchSize)
152
+ const xml = await textfetch(
153
+ efetchUrl({
154
+ db: 'protein',
155
+ id: batch.join(','),
156
+ rettype: 'gp',
157
+ retmode: 'xml',
158
+ }),
159
+ )
160
+ const parsed = parseCddDomains(xml)
161
+ for (const acc of batch) {
162
+ const matches = parsed.get(acc) ?? []
163
+ byAccession.set(acc, matches)
164
+ toCache.push({ accession: acc, matches })
165
+ }
166
+ }
167
+ if (toCache.length > 0) {
168
+ await saveDomains(toCache)
169
+ }
170
+ return byAccession
171
+ }
@@ -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
- `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=taxonomy&id=${idsParam}&retmode=xml`,
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.4.5'
1
+ export const version = '2.5.1'