jbrowse-plugin-msaview 2.7.2 → 2.7.3

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 (44) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -5
  2. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
  3. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
  4. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
  5. package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
  6. package/dist/MsaViewPanel/genomeToMSA.js +13 -6
  7. package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
  8. package/dist/MsaViewPanel/model.d.ts +68 -66
  9. package/dist/MsaViewPanel/model.js +14 -12
  10. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
  11. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
  12. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
  13. package/dist/MsaViewPanel/msaDataStore.js +8 -17
  14. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
  15. package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -31
  16. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  17. package/dist/utils/blastCache.d.ts +1 -2
  18. package/dist/utils/blastCache.js +9 -22
  19. package/dist/utils/domainCache.js +6 -15
  20. package/dist/utils/idb.d.ts +12 -0
  21. package/dist/utils/idb.js +21 -0
  22. package/dist/utils/taxonomyNames.js +13 -18
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/package.json +2 -2
  26. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
  27. package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
  28. package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
  29. package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
  30. package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
  31. package/src/MsaViewPanel/genomeToMSA.ts +14 -6
  32. package/src/MsaViewPanel/model.ts +17 -12
  33. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
  34. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
  35. package/src/MsaViewPanel/msaDataStore.ts +17 -17
  36. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
  37. package/src/utils/blastCache.ts +20 -23
  38. package/src/utils/domainCache.ts +14 -18
  39. package/src/utils/idb.ts +28 -0
  40. package/src/utils/taxonomyNames.ts +22 -24
  41. package/src/version.ts +1 -1
  42. package/dist/MsaViewPanel/blosum62.d.ts +0 -2
  43. package/dist/MsaViewPanel/blosum62.js +0 -627
  44. package/src/MsaViewPanel/blosum62.ts +0 -628
@@ -1,6 +1,21 @@
1
+ import { genomeToTranscriptSeqMapping } from 'g2p_mapper'
1
2
  import { describe, expect, test } from 'vitest'
2
3
 
3
- import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
4
+ import {
5
+ msaCoordToGenomeCoord,
6
+ msaCoordToGenomeRegions,
7
+ } from './msaCoordToGenomeCoord'
8
+
9
+ // codon at protein position i covers three consecutive genome bases starting at
10
+ // 100 + i * 3, i.e. a single-exon forward-strand transcript
11
+ function forwardCodons(n: number) {
12
+ return Object.fromEntries(
13
+ Array.from({ length: n }, (_, i) => [
14
+ i,
15
+ [100 + i * 3, 101 + i * 3, 102 + i * 3],
16
+ ]),
17
+ )
18
+ }
4
19
 
5
20
  describe('msaCoordToGenomeCoord', () => {
6
21
  test('returns undefined when neither transcriptToMsaMap nor mafRegion is defined', () => {
@@ -19,7 +34,7 @@ describe('msaCoordToGenomeCoord', () => {
19
34
  querySeqName: 'QUERY',
20
35
  transcriptToMsaMap: {
21
36
  refName: 'chr1',
22
- p2g: { 0: 100, 1: 103 },
37
+ p2gCodon: forwardCodons(2),
23
38
  },
24
39
  rows: [['OTHER', 'MKAA']],
25
40
  }
@@ -32,7 +47,7 @@ describe('msaCoordToGenomeCoord', () => {
32
47
  querySeqName: 'QUERY',
33
48
  transcriptToMsaMap: {
34
49
  refName: 'chr1',
35
- p2g: { 0: 100, 1: 103 },
50
+ p2gCodon: forwardCodons(2),
36
51
  },
37
52
  rows: [['QUERY', 'M-KA']],
38
53
  }
@@ -46,7 +61,7 @@ describe('msaCoordToGenomeCoord', () => {
46
61
  querySeqName: 'QUERY',
47
62
  transcriptToMsaMap: {
48
63
  refName: 'chr1',
49
- p2g: { 0: 100, 1: 103, 2: 106, 3: 109 },
64
+ p2gCodon: forwardCodons(4),
50
65
  },
51
66
  rows: [['QUERY', 'MKAA']],
52
67
  }
@@ -64,7 +79,7 @@ describe('msaCoordToGenomeCoord', () => {
64
79
  querySeqName: 'QUERY',
65
80
  transcriptToMsaMap: {
66
81
  refName: 'chr1',
67
- p2g: { 0: 100, 1: 103, 2: 106, 3: 109 },
82
+ p2gCodon: forwardCodons(4),
68
83
  },
69
84
  rows: [['QUERY', 'M-K-AA']],
70
85
  // 012345 gapped positions
@@ -87,35 +102,34 @@ describe('msaCoordToGenomeCoord', () => {
87
102
  })
88
103
  })
89
104
 
90
- test('returns undefined when p2g mapping is incomplete', () => {
105
+ test('returns undefined when the position has no codon mapping', () => {
91
106
  const model = {
92
107
  querySeqName: 'QUERY',
93
108
  transcriptToMsaMap: {
94
109
  refName: 'chr1',
95
- p2g: { 0: 100 }, // Missing entry for position 1
110
+ p2gCodon: forwardCodons(1),
96
111
  },
97
112
  rows: [['QUERY', 'MKAA']],
98
113
  }
99
- // Position 0 needs p2g[0] and p2g[1], but p2g[1] is missing
100
- const result = msaCoordToGenomeCoord({ model, coord: 0 })
114
+ // ungapped position 1 has no entry in p2gCodon
115
+ const result = msaCoordToGenomeCoord({ model, coord: 1 })
101
116
  expect(result).toBeUndefined()
102
117
  })
103
118
 
104
- test('handles reverse strand (start > end in p2g)', () => {
119
+ test('maps the final residue, whose codon has no successor', () => {
105
120
  const model = {
106
121
  querySeqName: 'QUERY',
107
122
  transcriptToMsaMap: {
108
123
  refName: 'chr1',
109
- p2g: { 0: 109, 1: 106, 2: 103, 3: 100 }, // Reverse strand
124
+ p2gCodon: forwardCodons(4),
110
125
  },
111
126
  rows: [['QUERY', 'MKAA']],
112
127
  }
113
- // Should return min/max correctly
114
- const result = msaCoordToGenomeCoord({ model, coord: 0 })
128
+ const result = msaCoordToGenomeCoord({ model, coord: 3 })
115
129
  expect(result).toEqual({
116
130
  refName: 'chr1',
117
- start: 106, // min(109, 106)
118
- end: 109, // max(109, 106)
131
+ start: 109,
132
+ end: 112,
119
133
  })
120
134
  })
121
135
 
@@ -124,7 +138,7 @@ describe('msaCoordToGenomeCoord', () => {
124
138
  querySeqName: 'QUERY',
125
139
  transcriptToMsaMap: {
126
140
  refName: 'chr1',
127
- p2g: { 0: 100, 1: 103 },
141
+ p2gCodon: forwardCodons(2),
128
142
  },
129
143
  rows: [['QUERY', 'MK']],
130
144
  }
@@ -138,7 +152,7 @@ describe('msaCoordToGenomeCoord', () => {
138
152
  querySeqName: 'SEQ2',
139
153
  transcriptToMsaMap: {
140
154
  refName: 'chr1',
141
- p2g: { 0: 200, 1: 203 },
155
+ p2gCodon: { 0: [200, 201, 202], 1: [203, 204, 205] },
142
156
  },
143
157
  rows: [
144
158
  ['SEQ1', 'AAAA'],
@@ -154,6 +168,91 @@ describe('msaCoordToGenomeCoord', () => {
154
168
  })
155
169
  })
156
170
 
171
+ // The mapping comes from the real g2p_mapper rather than hand-written
172
+ // fixtures: on the reverse strand p2g stores the codon's *highest*
173
+ // coordinate, which is what the old p2g[pos]..p2g[pos+1] arithmetic got wrong
174
+ describe('real g2p_mapper mappings', () => {
175
+ test('forward strand, single exon', () => {
176
+ const { p2gCodon, refName } = genomeToTranscriptSeqMapping({
177
+ refName: 'chr1',
178
+ start: 100,
179
+ end: 112,
180
+ strand: 1,
181
+ subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
182
+ })
183
+ const model = {
184
+ querySeqName: 'QUERY',
185
+ transcriptToMsaMap: { refName, p2gCodon },
186
+ rows: [['QUERY', 'MKAA']],
187
+ }
188
+ expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
189
+ refName: 'chr1',
190
+ start: 100,
191
+ end: 103,
192
+ })
193
+ expect(msaCoordToGenomeCoord({ model, coord: 3 })).toEqual({
194
+ refName: 'chr1',
195
+ start: 109,
196
+ end: 112,
197
+ })
198
+ })
199
+
200
+ test('reverse strand codon covers the last three bases of the CDS', () => {
201
+ const { p2gCodon, refName } = genomeToTranscriptSeqMapping({
202
+ refName: 'chr1',
203
+ start: 100,
204
+ end: 112,
205
+ strand: -1,
206
+ subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
207
+ })
208
+ const model = {
209
+ querySeqName: 'QUERY',
210
+ transcriptToMsaMap: { refName, p2gCodon },
211
+ rows: [['QUERY', 'MKAA']],
212
+ }
213
+ // the first residue is translated from the 3' end of the genome region
214
+ expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
215
+ refName: 'chr1',
216
+ start: 109,
217
+ end: 112,
218
+ })
219
+ expect(msaCoordToGenomeCoord({ model, coord: 3 })).toEqual({
220
+ refName: 'chr1',
221
+ start: 100,
222
+ end: 103,
223
+ })
224
+ })
225
+
226
+ test('codon split across an exon boundary yields one region per piece', () => {
227
+ // exon 1 contributes 4 bases, so residue 1 straddles the intron
228
+ const { p2gCodon, refName } = genomeToTranscriptSeqMapping({
229
+ refName: 'chr1',
230
+ start: 100,
231
+ end: 210,
232
+ strand: 1,
233
+ subfeatures: [
234
+ { refName: 'chr1', type: 'CDS', start: 100, end: 104 },
235
+ { refName: 'chr1', type: 'CDS', start: 200, end: 202 },
236
+ ],
237
+ })
238
+ const model = {
239
+ querySeqName: 'QUERY',
240
+ transcriptToMsaMap: { refName, p2gCodon },
241
+ rows: [['QUERY', 'MK']],
242
+ }
243
+ expect(msaCoordToGenomeRegions({ model, coord: 1 })).toEqual([
244
+ { refName: 'chr1', start: 103, end: 104 },
245
+ { refName: 'chr1', start: 200, end: 202 },
246
+ ])
247
+ // the single-region form bounds the pieces, for navigation
248
+ expect(msaCoordToGenomeCoord({ model, coord: 1 })).toEqual({
249
+ refName: 'chr1',
250
+ start: 103,
251
+ end: 202,
252
+ })
253
+ })
254
+ })
255
+
157
256
  // MAF region tests
158
257
  describe('mafRegion', () => {
159
258
  test('returns genome position for mafRegion mapping', () => {
@@ -234,7 +333,7 @@ describe('msaCoordToGenomeCoord', () => {
234
333
  querySeqName: 'hg38.chr1',
235
334
  transcriptToMsaMap: {
236
335
  refName: 'chr2',
237
- p2g: { 0: 5000, 1: 5003 },
336
+ p2gCodon: { 0: [5000, 5001, 5002], 1: [5003, 5004, 5005] },
238
337
  },
239
338
  mafRegion: {
240
339
  refName: 'chr1',
@@ -1,51 +1,95 @@
1
+ import { getCodonRanges } from 'g2p_mapper'
2
+
1
3
  import { gappedToUngappedPosition } from './structureConnection'
2
4
 
3
5
  import type { MafRegion } from './types'
4
6
 
5
- export function msaCoordToGenomeCoord({
7
+ interface GenomeRegion {
8
+ refName: string
9
+ start: number
10
+ end: number
11
+ }
12
+
13
+ interface CoordModel {
14
+ querySeqName: string
15
+ transcriptToMsaMap:
16
+ | {
17
+ refName: string
18
+ p2gCodon: Record<number, number[]>
19
+ }
20
+ | undefined
21
+ mafRegion?: MafRegion
22
+ rows: string[][]
23
+ }
24
+
25
+ /**
26
+ * The genome regions covered by MSA column `coord` of the query row, in 0-based
27
+ * half-open coordinates (what bpToPx and navTo take).
28
+ *
29
+ * Usually one region -- one codon, or one base in a MAF alignment -- but a
30
+ * codon split across an exon boundary yields one region per contiguous piece,
31
+ * which is why this returns a list.
32
+ */
33
+ export function msaCoordToGenomeRegions({
6
34
  model,
7
35
  coord: mouseCol,
8
36
  }: {
9
- model: {
10
- querySeqName: string
11
- transcriptToMsaMap:
12
- | {
13
- refName: string
14
- p2g: Record<number, number>
15
- }
16
- | undefined
17
- mafRegion?: MafRegion
18
- rows: string[][]
19
- }
37
+ model: CoordModel
20
38
  coord: number
21
- }) {
22
- const { querySeqName, transcriptToMsaMap, mafRegion } = model
39
+ }): GenomeRegion[] {
40
+ const { querySeqName, transcriptToMsaMap, mafRegion, rows } = model
23
41
 
24
- const querySeq = model.rows.find(f => f[0] === querySeqName)?.[1]
42
+ const querySeq = rows.find(f => f[0] === querySeqName)?.[1]
25
43
  if (!querySeq) {
26
- return undefined
44
+ return []
27
45
  }
28
46
 
29
47
  const ungappedPos = gappedToUngappedPosition(querySeq, mouseCol)
30
48
  if (ungappedPos === undefined) {
31
- return undefined
49
+ return []
32
50
  }
33
51
 
34
52
  if (mafRegion) {
35
53
  const genomePos = mafRegion.start + ungappedPos
36
54
  return genomePos < mafRegion.end
37
- ? { refName: mafRegion.refName, start: genomePos, end: genomePos + 1 }
38
- : undefined
55
+ ? [{ refName: mafRegion.refName, start: genomePos, end: genomePos + 1 }]
56
+ : []
39
57
  }
40
58
 
41
59
  if (transcriptToMsaMap) {
42
- const { refName, p2g } = transcriptToMsaMap
43
- const s = p2g[ungappedPos]
44
- const e = p2g[ungappedPos + 1]
45
- return s !== undefined && e !== undefined
46
- ? { refName, start: Math.min(s, e), end: Math.max(s, e) }
47
- : undefined
60
+ const { refName, p2gCodon } = transcriptToMsaMap
61
+ // p2gCodon holds every genomic base of the codon, so the range is exact on
62
+ // either strand. Deriving it from consecutive p2g entries instead
63
+ // (p2g[pos]..p2g[pos+1]) was off by one base on the reverse strand -- where
64
+ // p2g stores the codon's *highest* coordinate -- dropped the final residue,
65
+ // whose successor has no p2g entry, and spanned the whole intron for a
66
+ // codon split across an exon boundary.
67
+ return (
68
+ getCodonRanges(p2gCodon, ungappedPos)?.map(([start, end]) => ({
69
+ refName,
70
+ start,
71
+ end,
72
+ })) ?? []
73
+ )
48
74
  }
49
75
 
50
- return undefined
76
+ return []
77
+ }
78
+
79
+ /**
80
+ * A single region spanning the codon at MSA column `coord`, for navigation. For
81
+ * a codon split across an exon boundary this spans the intervening intron.
82
+ */
83
+ export function msaCoordToGenomeCoord(args: {
84
+ model: CoordModel
85
+ coord: number
86
+ }): GenomeRegion | undefined {
87
+ const regions = msaCoordToGenomeRegions(args)
88
+ const first = regions[0]
89
+ const last = regions.at(-1)
90
+ // getCodonRanges returns ranges sorted ascending, so first.start..last.end
91
+ // bounds the codon
92
+ return first && last
93
+ ? { refName: first.refName, start: first.start, end: last.end }
94
+ : undefined
51
95
  }
@@ -1,4 +1,6 @@
1
- import { openDB } from 'idb'
1
+ import { createDbOpener } from '../utils/idb'
2
+
3
+ import type { DBSchema } from 'idb'
2
4
 
3
5
  const DB_NAME = 'jbrowse-msaview-data'
4
6
  const DB_VERSION = 1
@@ -12,23 +14,21 @@ interface StoredMsaData {
12
14
  timestamp: number
13
15
  }
14
16
 
15
- let dbPromise: ReturnType<typeof openDB> | undefined
16
-
17
- function getDB() {
18
- dbPromise ??= openDB(DB_NAME, DB_VERSION, {
19
- upgrade(db) {
20
- if (!db.objectStoreNames.contains(STORE_NAME)) {
21
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
22
- store.createIndex('timestamp', 'timestamp', { unique: false })
23
- }
24
- },
25
- }).catch((e: unknown) => {
26
- dbPromise = undefined
27
- throw e
28
- })
29
- return dbPromise
17
+ interface MsaDataDB extends DBSchema {
18
+ [STORE_NAME]: {
19
+ key: string
20
+ value: StoredMsaData
21
+ indexes: { timestamp: number }
22
+ }
30
23
  }
31
24
 
25
+ const getDB = createDbOpener<MsaDataDB>(DB_NAME, DB_VERSION, db => {
26
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
27
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
28
+ store.createIndex('timestamp', 'timestamp', { unique: false })
29
+ }
30
+ })
31
+
32
32
  export function generateDataStoreId() {
33
33
  return `msa-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
34
34
  }
@@ -57,7 +57,7 @@ export async function storeMsaData(
57
57
  export async function retrieveMsaData(id: string) {
58
58
  try {
59
59
  const db = await getDB()
60
- const result = (await db.get(STORE_NAME, id)) as StoredMsaData | undefined
60
+ const result = await db.get(STORE_NAME, id)
61
61
  if (result) {
62
62
  return {
63
63
  msa: result.msa,
@@ -56,13 +56,15 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
56
56
  vi.clearAllMocks()
57
57
  })
58
58
 
59
- test('genome hover at coord 1005 highlights MSA column 5', () => {
59
+ test('genome hover at coord 1005 highlights MSA column 4', () => {
60
60
  const { model, calls } = makeModel()
61
61
  const run = syncGenomeHoverToMsaColumn(model)
62
62
 
63
- hoverGenome(1005) // 1005 - mafRegion.start(1000) = ungapped 5
63
+ // the hover coord is 1-based, so 1005 is the 0-based base 1004, i.e.
64
+ // ungapped offset 4 into a region starting at 1000
65
+ hoverGenome(1005)
64
66
  run()
65
- expect(calls).toEqual([5])
67
+ expect(calls).toEqual([4])
66
68
  })
67
69
 
68
70
  test('moving the genome hover moves the highlighted column', () => {
@@ -73,7 +75,7 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
73
75
  run()
74
76
  hoverGenome(1007)
75
77
  run()
76
- expect(calls).toEqual([2, 7])
78
+ expect(calls).toEqual([1, 6])
77
79
  })
78
80
 
79
81
  test('leaving the genome clears the column it set', () => {
@@ -84,7 +86,7 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
84
86
  run()
85
87
  clearGenomeHover()
86
88
  run()
87
- expect(calls).toEqual([4, undefined])
89
+ expect(calls).toEqual([3, undefined])
88
90
  })
89
91
 
90
92
  test('a hover outside the maf region clears a previously-set column once', () => {
@@ -96,7 +98,7 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
96
98
  hoverGenome(5000) // outside [1000,1010) -> genomeToMSA returns undefined
97
99
  run()
98
100
  run()
99
- expect(calls).toEqual([4, undefined])
101
+ expect(calls).toEqual([3, undefined])
100
102
  })
101
103
 
102
104
  test('never touches mouseCol when the genome never provides a column, so a direct MSA hover survives unrelated session hovers', () => {
@@ -1,10 +1,11 @@
1
- import { openDB } from 'idb'
1
+ import { createDbOpener } from './idb'
2
2
 
3
3
  import type {
4
4
  BlastDatabase,
5
5
  BlastProgram,
6
6
  MsaAlgorithm,
7
7
  } from '../LaunchMsaView/components/NCBIBlastQuery/consts'
8
+ import type { DBSchema } from 'idb'
8
9
 
9
10
  const DB_NAME = 'jbrowse-msaview-blast-cache'
10
11
  const STORE_NAME = 'blast-results'
@@ -27,25 +28,26 @@ export interface CachedBlastResult {
27
28
  geneName?: string
28
29
  }
29
30
 
30
- let dbPromise: ReturnType<typeof openDB> | undefined
31
-
32
- function getDB() {
33
- dbPromise ??= openDB(DB_NAME, DB_VERSION, {
34
- upgrade(db, oldVersion) {
35
- if (oldVersion < 2 && db.objectStoreNames.contains(STORE_NAME)) {
36
- db.deleteObjectStore(STORE_NAME)
37
- }
38
- if (!db.objectStoreNames.contains(STORE_NAME)) {
39
- db.createObjectStore(STORE_NAME, { keyPath: 'id' })
40
- }
41
- },
42
- }).catch((e: unknown) => {
43
- dbPromise = undefined
44
- throw e
45
- })
46
- return dbPromise
31
+ interface BlastCacheDB extends DBSchema {
32
+ [STORE_NAME]: {
33
+ key: string
34
+ value: CachedBlastResult
35
+ }
47
36
  }
48
37
 
38
+ const getDB = createDbOpener<BlastCacheDB>(
39
+ DB_NAME,
40
+ DB_VERSION,
41
+ (db, oldVersion) => {
42
+ if (oldVersion < 2 && db.objectStoreNames.contains(STORE_NAME)) {
43
+ db.deleteObjectStore(STORE_NAME)
44
+ }
45
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
46
+ db.createObjectStore(STORE_NAME, { keyPath: 'id' })
47
+ }
48
+ },
49
+ )
50
+
49
51
  function createCacheKey(
50
52
  proteinSequence: string,
51
53
  blastDatabase: BlastDatabase,
@@ -125,8 +127,3 @@ export async function deleteCachedResult(id: string) {
125
127
  const db = await getDB()
126
128
  await db.delete(STORE_NAME, id)
127
129
  }
128
-
129
- export async function clearAllCachedResults() {
130
- const db = await getDB()
131
- await db.clear(STORE_NAME)
132
- }
@@ -1,6 +1,7 @@
1
- import { openDB } from 'idb'
1
+ import { createDbOpener } from './idb'
2
2
 
3
3
  import type { DomainMatch } from './ncbiDomains'
4
+ import type { DBSchema } from 'idb'
4
5
 
5
6
  const DB_NAME = 'jbrowse-msaview-domain-cache'
6
7
  const STORE_NAME = 'domains'
@@ -11,29 +12,24 @@ interface CachedDomain {
11
12
  matches: DomainMatch[]
12
13
  }
13
14
 
14
- let dbPromise: ReturnType<typeof openDB> | undefined
15
-
16
- function getDB() {
17
- dbPromise ??= openDB(DB_NAME, DB_VERSION, {
18
- upgrade(db) {
19
- if (!db.objectStoreNames.contains(STORE_NAME)) {
20
- db.createObjectStore(STORE_NAME, { keyPath: 'accession' })
21
- }
22
- },
23
- }).catch((e: unknown) => {
24
- dbPromise = undefined
25
- throw e
26
- })
27
- return dbPromise
15
+ interface DomainCacheDB extends DBSchema {
16
+ [STORE_NAME]: {
17
+ key: string
18
+ value: CachedDomain
19
+ }
28
20
  }
29
21
 
22
+ const getDB = createDbOpener<DomainCacheDB>(DB_NAME, DB_VERSION, db => {
23
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
24
+ db.createObjectStore(STORE_NAME, { keyPath: 'accession' })
25
+ }
26
+ })
27
+
30
28
  export async function getCachedDomains(accessions: string[]) {
31
29
  const db = await getDB()
32
30
  const tx = db.transaction(STORE_NAME, 'readonly')
33
31
  const results = await Promise.all(
34
- accessions.map(
35
- accession => tx.store.get(accession) as Promise<CachedDomain | undefined>,
36
- ),
32
+ accessions.map(accession => tx.store.get(accession)),
37
33
  )
38
34
  await tx.done
39
35
  return results
@@ -0,0 +1,28 @@
1
+ import { openDB } from 'idb'
2
+
3
+ import type { DBSchema, IDBPDatabase, OpenDBCallbacks } from 'idb'
4
+
5
+ /**
6
+ * Memoized `openDB` for a typed schema, shared by this plugin's caches.
7
+ *
8
+ * The connection promise is cached so callers share one connection, and dropped
9
+ * again if the open fails, so a later call retries instead of replaying the same
10
+ * rejection forever (IndexedDB is unavailable in some private-browsing modes).
11
+ *
12
+ * Passing a DBSchema is what keeps `get`/`getAll` from returning `any`: with an
13
+ * untyped database every cached record reaches the UI unchecked.
14
+ */
15
+ export function createDbOpener<T extends DBSchema>(
16
+ name: string,
17
+ version: number,
18
+ upgrade: OpenDBCallbacks<T>['upgrade'],
19
+ ) {
20
+ let dbPromise: Promise<IDBPDatabase<T>> | undefined
21
+ return () => {
22
+ dbPromise ??= openDB<T>(name, version, { upgrade }).catch((e: unknown) => {
23
+ dbPromise = undefined
24
+ throw e
25
+ })
26
+ return dbPromise
27
+ }
28
+ }
@@ -1,6 +1,8 @@
1
- import { openDB } from 'idb'
2
-
3
1
  import { efetchUrl } from './eutils'
2
+ import { textfetch } from './fetch'
3
+ import { createDbOpener } from './idb'
4
+
5
+ import type { DBSchema } from 'idb'
4
6
 
5
7
  const DB_NAME = 'jbrowse-msaview-taxonomy-cache'
6
8
  const STORE_NAME = 'common-names'
@@ -12,31 +14,24 @@ interface CachedTaxonomy {
12
14
  commonName?: string
13
15
  }
14
16
 
15
- let dbPromise: ReturnType<typeof openDB> | undefined
16
-
17
- function getDB() {
18
- dbPromise ??= openDB(DB_NAME, DB_VERSION, {
19
- upgrade(db) {
20
- if (db.objectStoreNames.contains(STORE_NAME)) {
21
- db.deleteObjectStore(STORE_NAME)
22
- }
23
- db.createObjectStore(STORE_NAME, { keyPath: 'taxid' })
24
- },
25
- }).catch((e: unknown) => {
26
- dbPromise = undefined
27
- throw e
28
- })
29
- return dbPromise
17
+ interface TaxonomyCacheDB extends DBSchema {
18
+ [STORE_NAME]: {
19
+ key: number
20
+ value: CachedTaxonomy
21
+ }
30
22
  }
31
23
 
24
+ const getDB = createDbOpener<TaxonomyCacheDB>(DB_NAME, DB_VERSION, db => {
25
+ if (db.objectStoreNames.contains(STORE_NAME)) {
26
+ db.deleteObjectStore(STORE_NAME)
27
+ }
28
+ db.createObjectStore(STORE_NAME, { keyPath: 'taxid' })
29
+ })
30
+
32
31
  async function getCachedTaxonomies(taxids: number[]) {
33
32
  const db = await getDB()
34
33
  const tx = db.transaction(STORE_NAME, 'readonly')
35
- const results = await Promise.all(
36
- taxids.map(
37
- taxid => tx.store.get(taxid) as Promise<CachedTaxonomy | undefined>,
38
- ),
39
- )
34
+ const results = await Promise.all(taxids.map(taxid => tx.store.get(taxid)))
40
35
  await tx.done
41
36
  return results
42
37
  }
@@ -87,10 +82,13 @@ export async function fetchTaxonomyInfo(
87
82
  const idsParam = batch.join(',')
88
83
 
89
84
  try {
90
- const response = await fetch(
85
+ // textfetch rather than a bare fetch: an NCBI 429/5xx returns an HTML
86
+ // error body that the regexes below silently find nothing in, so without
87
+ // the status check a throttled batch looks like "these taxa have no
88
+ // names" instead of reporting why
89
+ const text = await textfetch(
91
90
  efetchUrl({ db: 'taxonomy', id: idsParam, retmode: 'xml' }),
92
91
  )
93
- const text = await response.text()
94
92
 
95
93
  // Build a map of taxid -> taxon block by finding Taxon elements.
96
94
  // Prefer entries with <LineageEx> (full top-level entries) over nested
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '2.7.2'
1
+ export const version = '2.7.3'
@@ -1,2 +0,0 @@
1
- declare const BLOSUM62: Record<string, Record<string, number>>;
2
- export default BLOSUM62;