jbrowse-plugin-msaview 3.8.0 → 3.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 (129) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -22
  2. package/dist/AddHighlightModel/connectedHighlights.d.ts +9 -0
  3. package/dist/AddHighlightModel/connectedHighlights.js +15 -0
  4. package/dist/AddHighlightModel/connectedHighlights.test.d.ts +1 -0
  5. package/dist/AddHighlightModel/connectedHighlights.test.js +36 -0
  6. package/dist/AddHighlightModel/index.js +0 -1
  7. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +4 -4
  8. package/dist/LaunchMsaView/components/BlastQuery/consts.js +6 -4
  9. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +0 -2
  10. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +2 -2
  11. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +0 -1
  12. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +1 -1
  13. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +5 -5
  14. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +0 -6
  15. package/dist/LaunchMsaView/components/calculateProteinSequence.js +1 -1
  16. package/dist/LaunchMsaView/components/types.d.ts +0 -3
  17. package/dist/LaunchMsaView/util.d.ts +0 -1
  18. package/dist/LaunchMsaView/util.js +1 -1
  19. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +5 -14
  20. package/dist/MsaViewPanel/afterCreateAutoruns.js +77 -86
  21. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  22. package/dist/MsaViewPanel/doLaunchBlast.js +42 -16
  23. package/dist/MsaViewPanel/doLaunchBlast.test.js +54 -0
  24. package/dist/MsaViewPanel/doLaunchOrthologs.js +18 -11
  25. package/dist/MsaViewPanel/launchAutoruns.test.js +0 -1
  26. package/dist/MsaViewPanel/model.d.ts +13 -7
  27. package/dist/MsaViewPanel/model.js +39 -10
  28. package/dist/MsaViewPanel/model.test.js +3 -4
  29. package/dist/MsaViewPanel/msaDataStore.d.ts +1 -2
  30. package/dist/MsaViewPanel/msaDataStore.js +11 -26
  31. package/dist/MsaViewPanel/msaDataStore.test.js +10 -2
  32. package/dist/MsaViewPanel/observeProteinHighlights.test.js +1 -0
  33. package/dist/MsaViewPanel/processInit.test.js +1 -7
  34. package/dist/MsaViewPanel/regionSnapshot.test.js +0 -1
  35. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +0 -1
  36. package/dist/MsaViewPanel/storedData.test.js +238 -145
  37. package/dist/MsaViewPanel/transcriptMap.d.ts +7 -0
  38. package/dist/MsaViewPanel/transcriptMap.js +89 -0
  39. package/dist/MsaViewPanel/transcriptMap.test.d.ts +1 -0
  40. package/dist/MsaViewPanel/transcriptMap.test.js +108 -0
  41. package/dist/jbrowse-plugin-msaview.umd.production.min.js +38 -38
  42. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  43. package/dist/utils/browserAlign.d.ts +5 -3
  44. package/dist/utils/browserAlign.js +29 -23
  45. package/dist/utils/browserAlign.test.js +14 -1
  46. package/dist/utils/ebiBlast.d.ts +0 -8
  47. package/dist/utils/ebiBlast.js +10 -21
  48. package/dist/utils/ebiJobDispatcher.d.ts +9 -9
  49. package/dist/utils/ebiJobDispatcher.js +17 -3
  50. package/dist/utils/eutils.d.ts +0 -2
  51. package/dist/utils/eutils.js +11 -3
  52. package/dist/utils/eutils.test.js +22 -0
  53. package/dist/utils/msa.js +6 -14
  54. package/dist/utils/msaRows.d.ts +0 -8
  55. package/dist/utils/msaRows.js +1 -1
  56. package/dist/utils/ncbiOrthologs.d.ts +7 -5
  57. package/dist/utils/ncbiOrthologs.js +20 -14
  58. package/dist/utils/ncbiOrthologs.test.js +13 -0
  59. package/dist/utils/ncbiTaxonomy.d.ts +2 -2
  60. package/dist/utils/ncbiTaxonomy.js +5 -5
  61. package/dist/utils/pantherOrthologs.d.ts +2 -3
  62. package/dist/utils/pantherOrthologs.js +18 -12
  63. package/dist/utils/phmmer.js +7 -14
  64. package/dist/utils/searchCache.d.ts +18 -0
  65. package/dist/utils/searchCache.js +33 -0
  66. package/dist/utils/searchCache.test.d.ts +1 -0
  67. package/dist/utils/searchCache.test.js +80 -0
  68. package/dist/utils/taxonomyNames.d.ts +1 -1
  69. package/dist/utils/taxonomyNames.js +7 -2
  70. package/dist/utils/taxonomyNamesAbort.test.d.ts +1 -0
  71. package/dist/utils/taxonomyNamesAbort.test.js +36 -0
  72. package/dist/utils/unirefHomologs.d.ts +1 -6
  73. package/dist/utils/unirefHomologs.js +2 -2
  74. package/dist/utils/useFetch.d.ts +1 -1
  75. package/dist/utils/useFetch.js +5 -5
  76. package/dist/utils/useFetchAbort.test.d.ts +1 -0
  77. package/dist/utils/useFetchAbort.test.js +29 -0
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/package.json +1 -1
  81. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +10 -23
  82. package/src/AddHighlightModel/connectedHighlights.test.ts +46 -0
  83. package/src/AddHighlightModel/connectedHighlights.ts +35 -0
  84. package/src/AddHighlightModel/index.tsx +0 -1
  85. package/src/LaunchMsaView/components/BlastQuery/consts.ts +6 -4
  86. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +2 -2
  87. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +1 -1
  88. package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +5 -5
  89. package/src/LaunchMsaView/components/calculateProteinSequence.ts +1 -1
  90. package/src/LaunchMsaView/components/types.ts +0 -4
  91. package/src/LaunchMsaView/util.ts +1 -1
  92. package/src/MsaViewPanel/afterCreateAutoruns.ts +90 -87
  93. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  94. package/src/MsaViewPanel/doLaunchBlast.test.ts +71 -0
  95. package/src/MsaViewPanel/doLaunchBlast.ts +71 -20
  96. package/src/MsaViewPanel/doLaunchOrthologs.ts +28 -11
  97. package/src/MsaViewPanel/launchAutoruns.test.ts +0 -1
  98. package/src/MsaViewPanel/model.test.ts +3 -4
  99. package/src/MsaViewPanel/model.ts +40 -10
  100. package/src/MsaViewPanel/msaDataStore.test.ts +13 -2
  101. package/src/MsaViewPanel/msaDataStore.ts +11 -25
  102. package/src/MsaViewPanel/observeProteinHighlights.test.ts +1 -0
  103. package/src/MsaViewPanel/processInit.test.ts +1 -8
  104. package/src/MsaViewPanel/regionSnapshot.test.ts +0 -1
  105. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +0 -1
  106. package/src/MsaViewPanel/storedData.test.ts +273 -158
  107. package/src/MsaViewPanel/transcriptMap.test.ts +137 -0
  108. package/src/MsaViewPanel/transcriptMap.ts +117 -0
  109. package/src/utils/browserAlign.test.ts +17 -0
  110. package/src/utils/browserAlign.ts +39 -25
  111. package/src/utils/ebiBlast.ts +12 -35
  112. package/src/utils/ebiJobDispatcher.ts +26 -7
  113. package/src/utils/eutils.test.ts +25 -0
  114. package/src/utils/eutils.ts +10 -3
  115. package/src/utils/msa.ts +6 -16
  116. package/src/utils/msaRows.ts +1 -1
  117. package/src/utils/ncbiOrthologs.test.ts +16 -0
  118. package/src/utils/ncbiOrthologs.ts +37 -15
  119. package/src/utils/ncbiTaxonomy.ts +8 -2
  120. package/src/utils/pantherOrthologs.ts +28 -12
  121. package/src/utils/phmmer.ts +7 -16
  122. package/src/utils/searchCache.test.ts +98 -0
  123. package/src/utils/searchCache.ts +75 -0
  124. package/src/utils/taxonomyNames.ts +7 -0
  125. package/src/utils/taxonomyNamesAbort.test.ts +46 -0
  126. package/src/utils/unirefHomologs.ts +2 -2
  127. package/src/utils/useFetch.ts +6 -6
  128. package/src/utils/useFetchAbort.test.tsx +33 -0
  129. package/src/version.ts +1 -1
@@ -1,8 +1,10 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
2
2
  import { saveBlastResult } from '../utils/blastCache';
3
+ import { checkPairSize } from '../utils/browserAlign';
3
4
  import { searchBackends } from '../utils/homologSearch';
4
5
  import { launchMSA } from '../utils/msa';
5
6
  import { buildSearchMsa } from '../utils/msaRows';
7
+ import { getCachedSearch, saveSearch, searchKey } from '../utils/searchCache';
6
8
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
7
9
  import { resolveUniProtEntry } from '../utils/unirefHomologs';
8
10
  import { transcriptFields, transcriptName } from './util';
@@ -38,23 +40,13 @@ async function resolveQuery(self, scope) {
38
40
  }
39
41
  throw new Error('No query: a search needs a proteinSequence, a UniProt accession, or a connectedTranscript to translate');
40
42
  }
41
- /**
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.
48
- */
49
- export async function doLaunchBlast({ self, scope, }) {
43
+ async function search({ self, scope, query, querySeqName, key, }) {
50
44
  const params = self.blastParams;
51
- const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
52
- const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
53
45
  const { onProgress, onRid, signal } = scope;
54
- const { hits, queryRow, rid } = await searchBackends[searchProgram]({
46
+ const { hits, queryRow, rid } = await searchBackends[params.searchProgram ?? 'blastp']({
55
47
  query,
56
48
  database: params.blastDatabase,
57
- maxHits,
49
+ maxHits: params.maxHits,
58
50
  onProgress,
59
51
  onRid,
60
52
  signal,
@@ -63,7 +55,7 @@ export async function doLaunchBlast({ self, scope, }) {
63
55
  throw new Error('No hits found');
64
56
  }
65
57
  onProgress('Fetching species taxonomy info...');
66
- const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined));
58
+ const taxonomyInfo = await fetchTaxonomyInfo(hits.map(h => h.taxid).filter((t) => t !== undefined), signal);
67
59
  const { msa: fasta, treeMetadata } = buildSearchMsa({
68
60
  hits,
69
61
  query,
@@ -71,10 +63,44 @@ export async function doLaunchBlast({ self, scope, }) {
71
63
  taxonomyInfo,
72
64
  querySeqName,
73
65
  });
66
+ if (!queryRow) {
67
+ await saveSearch({ id: key, fasta, treeMetadata, rid });
68
+ }
69
+ return { fasta, treeMetadata, rid, queryRow };
70
+ }
71
+ /**
72
+ * A similarity search, then an alignment of what it found. The program is a
73
+ * backend behind one interface (utils/homologSearch.ts); what differs between
74
+ * them is settled by whether the result came back aligned. A program that
75
+ * aligns as it searches (phmmer) hands over the alignment and the tree is
76
+ * built from it in the browser; one that does not (blastp) hands over bare
77
+ * hits and the chosen aligner runs on them.
78
+ */
79
+ export async function doLaunchBlast({ self, scope, }) {
80
+ const params = self.blastParams;
81
+ const { selectedTranscript, maxHits, searchProgram = 'blastp' } = params;
82
+ const msaAlgorithm = params.msaAlgorithm ?? 'browser';
83
+ const { sequence: query, name: querySeqName } = await resolveQuery(self, scope);
84
+ if (searchProgram === 'blastp' && msaAlgorithm === 'browser') {
85
+ checkPairSize(querySeqName, query.length, query.length);
86
+ }
87
+ const { onProgress, signal } = scope;
88
+ const key = searchKey({
89
+ searchProgram,
90
+ database: params.blastDatabase,
91
+ maxHits,
92
+ querySeqName,
93
+ query,
94
+ });
95
+ const found = await getCachedSearch(key);
96
+ if (found) {
97
+ onProgress(`Reusing the ${searchProgram} search from ${new Date(found.timestamp).toLocaleString()}...`);
98
+ }
99
+ const { fasta, treeMetadata, rid, queryRow } = found ?? (await search({ self, scope, query, querySeqName, key }));
74
100
  const { msa, tree } = queryRow
75
101
  ? { msa: fasta, tree: '' }
76
102
  : await launchMSA({
77
- algorithm: params.msaAlgorithm ?? 'browser',
103
+ algorithm: msaAlgorithm,
78
104
  sequence: fasta,
79
105
  onProgress,
80
106
  signal,
@@ -84,7 +110,7 @@ export async function doLaunchBlast({ self, scope, }) {
84
110
  await saveBlastResult({
85
111
  proteinSequence: query,
86
112
  blastDatabase: params.blastDatabase,
87
- msaAlgorithm: queryRow ? undefined : (params.msaAlgorithm ?? 'browser'),
113
+ msaAlgorithm: queryRow ? undefined : msaAlgorithm,
88
114
  searchProgram: params.searchProgram,
89
115
  maxHits,
90
116
  msa,
@@ -1,7 +1,9 @@
1
1
  import { beforeEach, expect, test, vi } from 'vitest';
2
2
  import { saveBlastResult } from '../utils/blastCache';
3
+ import { MAX_PAIR_CELLS } from '../utils/browserAlign';
3
4
  import { searchBackends } from '../utils/homologSearch';
4
5
  import { launchMSA } from '../utils/msa';
6
+ import { getCachedSearch, saveSearch } from '../utils/searchCache';
5
7
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
8
  import { resolveUniProtEntry } from '../utils/unirefHomologs';
7
9
  import { doLaunchBlast } from './doLaunchBlast';
@@ -15,6 +17,11 @@ vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
15
17
  vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
16
18
  vi.mock('../utils/blastCache', () => ({ saveBlastResult: vi.fn() }));
17
19
  vi.mock('../utils/unirefHomologs', () => ({ resolveUniProtEntry: vi.fn() }));
20
+ vi.mock('../utils/searchCache', async (importOriginal) => ({
21
+ ...(await importOriginal()),
22
+ getCachedSearch: vi.fn(),
23
+ saveSearch: vi.fn(),
24
+ }));
18
25
  const blastp = vi.mocked(searchBackends.blastp);
19
26
  const phmmer = vi.mocked(searchBackends.phmmer);
20
27
  const mockLaunchMSA = vi.mocked(launchMSA);
@@ -44,6 +51,8 @@ beforeEach(() => {
44
51
  vi.clearAllMocks();
45
52
  vi.mocked(fetchTaxonomyInfo).mockResolvedValue(new Map());
46
53
  vi.mocked(saveBlastResult).mockResolvedValue(undefined);
54
+ vi.mocked(getCachedSearch).mockResolvedValue(undefined);
55
+ vi.mocked(saveSearch).mockResolvedValue(undefined);
47
56
  });
48
57
  test('bare hits go to the chosen aligner, with the query first', async () => {
49
58
  blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
@@ -168,3 +177,48 @@ test('a live Feature handed over in the same session labels it the same way', as
168
177
  geneName: 'TP53',
169
178
  }));
170
179
  });
180
+ const BLASTP_BROWSER = {
181
+ searchProgram: 'blastp',
182
+ blastDatabase: 'uniprotkb_swissprot',
183
+ msaAlgorithm: 'browser',
184
+ proteinSequence: 'MKWVTF',
185
+ };
186
+ test('the hits are saved before the aligner runs, so a failed alignment keeps them', async () => {
187
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
188
+ mockLaunchMSA.mockRejectedValue(new Error('aligner fell over'));
189
+ await expect(launch(makeModel(BLASTP_BROWSER))).rejects.toThrow('aligner fell over');
190
+ expect(saveSearch).toHaveBeenCalledWith(expect.objectContaining({
191
+ fasta: '>QUERY\nMKWVTF\n>P1-Mus_musculus\nMKWV',
192
+ rid: 'job',
193
+ }));
194
+ });
195
+ test('a relaunch with saved hits aligns them without searching again', async () => {
196
+ vi.mocked(getCachedSearch).mockResolvedValue({
197
+ id: 'k',
198
+ fasta: '>QUERY\nMKWVTF\n>P1\nMKWV',
199
+ treeMetadata: {},
200
+ rid: 'job',
201
+ timestamp: 0,
202
+ });
203
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: '' });
204
+ const result = await launch(makeModel(BLASTP_BROWSER));
205
+ expect(blastp).not.toHaveBeenCalled();
206
+ expect(mockLaunchMSA).toHaveBeenCalledWith(expect.objectContaining({ sequence: '>QUERY\nMKWVTF\n>P1\nMKWV' }));
207
+ expect(result.msa).toBe('aligned');
208
+ });
209
+ test('a query too long for the in-browser aligner is refused before the search', async () => {
210
+ const side = Math.ceil(Math.sqrt(MAX_PAIR_CELLS)) + 1;
211
+ await expect(launch(makeModel({ ...BLASTP_BROWSER, proteinSequence: 'M'.repeat(side) }))).rejects.toThrow(/too large to align in the browser/);
212
+ expect(blastp).not.toHaveBeenCalled();
213
+ });
214
+ test('the same long query goes to an EBI aligner without complaint', async () => {
215
+ const side = Math.ceil(Math.sqrt(MAX_PAIR_CELLS)) + 1;
216
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
217
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: 'tree' });
218
+ await launch(makeModel({
219
+ ...BLASTP_BROWSER,
220
+ msaAlgorithm: 'clustalo',
221
+ proteinSequence: 'M'.repeat(side),
222
+ }));
223
+ expect(blastp).toHaveBeenCalled();
224
+ });
@@ -1,4 +1,5 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
2
+ import { isAbortError } from '../utils/fetch';
2
3
  import { launchMSA } from '../utils/msa';
3
4
  import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
4
5
  import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
@@ -33,6 +34,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
33
34
  exclude: taxId,
34
35
  limit: maxSpecies,
35
36
  onProgress,
37
+ signal,
36
38
  };
37
39
  const { geneId, representative, rows } = source === 'panther'
38
40
  ? await findPantherOrthologs(request)
@@ -41,7 +43,6 @@ export async function doLaunchOrthologs({ self, scope, }) {
41
43
  ...request,
42
44
  identity,
43
45
  referenceProteomesOnly,
44
- signal,
45
46
  })
46
47
  : await findNcbiOrthologs(request);
47
48
  // The query row. The dialog always supplies it — it is the user's OWN
@@ -68,7 +69,7 @@ export async function doLaunchOrthologs({ self, scope, }) {
68
69
  // taxon is excluded from that set, but a subspecies can sanitize to the same
69
70
  // token, and a collision would silently point the coordinate mapping at
70
71
  // another animal's row.
71
- const queryLabel = await queryRowLabel(taxId, rows);
72
+ const queryLabel = await queryRowLabel(taxId, rows, signal);
72
73
  act(() => {
73
74
  self.setQuerySeqName(queryLabel);
74
75
  });
@@ -98,16 +99,17 @@ export async function doLaunchOrthologs({ self, scope, }) {
98
99
  * wants "this gene across species" gets NCBI's own order, which leads with the
99
100
  * reference organisms.
100
101
  */
101
- async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest }) {
102
+ async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, signal, ...rest }) {
102
103
  onProgress('Resolving gene at NCBI...');
103
- const resolved = await resolveGeneId(geneCandidates, taxId);
104
+ const resolved = await resolveGeneId(geneCandidates, taxId, signal);
104
105
  if (!resolved) {
105
106
  throw new Error(`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`);
106
107
  }
107
- const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
108
+ const representative = await fetchRepresentativeQueryProtein(resolved.geneId, signal);
108
109
  const rows = await fetchOrthologRows({
109
110
  geneId: resolved.geneId,
110
111
  onProgress,
112
+ signal,
111
113
  ...rest,
112
114
  });
113
115
  return { geneId: resolved.geneId, representative, rows };
@@ -122,12 +124,11 @@ const sourceNames = {
122
124
  * entry the cluster was looked up from, whose sequence a spec-launched row
123
125
  * takes and whose accession earns the CDD overlay when the two match.
124
126
  */
125
- async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, signal, ...rest }) {
127
+ async function findUnirefHomologs({ geneCandidates, identity, referenceProteomesOnly, ...rest }) {
126
128
  const found = await fetchUnirefHomologs({
127
129
  candidates: geneCandidates,
128
130
  identity,
129
131
  referenceProteomesOnly,
130
- signal,
131
132
  ...rest,
132
133
  });
133
134
  return {
@@ -160,13 +161,16 @@ async function findPantherOrthologs({ geneCandidates, ...rest }) {
160
161
  * marker when NCBI cannot name the taxon, which is a naming failure and must not
161
162
  * take down the launch.
162
163
  */
163
- async function queryRowLabel(taxId, rows) {
164
+ async function queryRowLabel(taxId, rows, signal) {
164
165
  let name;
165
166
  try {
166
- const info = (await fetchTaxonomyInfo([taxId])).get(taxId);
167
+ const info = (await fetchTaxonomyInfo([taxId], signal)).get(taxId);
167
168
  name = info?.commonName ?? info?.sciname;
168
169
  }
169
170
  catch (e) {
171
+ if (isAbortError(e)) {
172
+ throw e;
173
+ }
170
174
  console.warn('[msaview-orthologs] taxonomy name lookup failed:', e);
171
175
  }
172
176
  return dedupeLabels([
@@ -179,11 +183,14 @@ async function queryRowLabel(taxId, rows) {
179
183
  * that supplied no sequence of its own, the alignment — so it is reported by
180
184
  * returning nothing rather than by throwing here.
181
185
  */
182
- async function fetchRepresentativeQueryProtein(geneId) {
186
+ async function fetchRepresentativeQueryProtein(geneId, signal) {
183
187
  try {
184
- return await fetchProteinForGene(geneId);
188
+ return await fetchProteinForGene(geneId, signal);
185
189
  }
186
190
  catch (e) {
191
+ if (isAbortError(e)) {
192
+ throw e;
193
+ }
187
194
  console.warn('[msaview-orthologs] query protein lookup failed:', e);
188
195
  return undefined;
189
196
  }
@@ -8,7 +8,6 @@ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
8
8
  vi.mock('./doLaunchBlast', () => ({ doLaunchBlast: vi.fn() }));
9
9
  vi.mock('./msaDataStore', () => ({
10
10
  cleanupOldData: vi.fn(async () => { }),
11
- deleteMsaData: vi.fn(async () => { }),
12
11
  generateDataStoreId: vi.fn(),
13
12
  retrieveMsaData: vi.fn(),
14
13
  storeMsaData: vi.fn(),
@@ -933,19 +933,14 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
933
933
  loadingStoredData: boolean;
934
934
  isStoringData: boolean;
935
935
  lastStoredData: MsaDataPayload | undefined;
936
+ ownsDataStoreRow: boolean;
936
937
  launchController: AbortController | undefined;
937
938
  domainsRequested: boolean;
938
939
  } & {
939
940
  /**
940
941
  * #getter
941
942
  */
942
- readonly transcriptToMsaMap: {
943
- g2p: Record<number, number>;
944
- p2g: Record<number, number>;
945
- p2gCodon: Record<number, number[]>;
946
- refName: string;
947
- strand: number;
948
- } | undefined;
943
+ readonly transcriptToMsaMap: import("./transcriptMap").TranscriptMap | undefined;
949
944
  /**
950
945
  * #getter
951
946
  */
@@ -983,6 +978,13 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
983
978
  * alignment the link does carry.
984
979
  */
985
980
  readonly hostRestoresData: boolean;
981
+ /**
982
+ * #getter
983
+ * the documents a reload would lose: too big for react-msaview to keep
984
+ * in the snapshot, and with no filehandle or kept init to refetch them
985
+ * from. IndexedDB holds exactly these.
986
+ */
987
+ readonly unsavedDocuments: MsaDataPayload;
986
988
  } & {
987
989
  /**
988
990
  * #action
@@ -1040,6 +1042,10 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
1040
1042
  * #action
1041
1043
  */
1042
1044
  setLastStoredData(arg?: MsaDataPayload): void;
1045
+ /**
1046
+ * #action
1047
+ */
1048
+ setOwnsDataStoreRow(arg: boolean): void;
1043
1049
  /**
1044
1050
  * #action
1045
1051
  */
@@ -1,13 +1,12 @@
1
1
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes';
2
2
  import { getSession } from '@jbrowse/core/util';
3
- import { addDisposer, types } from '@jbrowse/mobx-state-tree';
4
- import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
3
+ import { addDisposer, getSnapshot, types } from '@jbrowse/mobx-state-tree';
5
4
  import { autorun } from 'mobx';
6
5
  import { MSAModelF } from 'react-msaview';
7
6
  import { autoLoadProteinDomains, launchBlastIfNeeded, launchOrthologsIfNeeded, loadStoredData, observeProteinHighlights, processInit, runCleanup, storeDataToIndexedDB, syncGenomeHoverToMsaColumn, } from './afterCreateAutoruns';
8
7
  import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
9
- import { deleteMsaData } from './msaDataStore';
10
8
  import { resolveConnectedTranscriptIfNeeded } from './resolveConnectedTranscript';
9
+ import { transcriptMap } from './transcriptMap';
11
10
  /**
12
11
  * Where the homolog set comes from. NCBI's ortholog sets cover vertebrates and
13
12
  * insects; PANTHER's span its 144 reference proteomes, human to yeast to
@@ -132,6 +131,13 @@ export default function stateModelFactory() {
132
131
  * neighbor-joining tree, say) is recognized as needing a new one
133
132
  */
134
133
  lastStoredData: undefined,
134
+ /**
135
+ * #volatile
136
+ * whether this view wrote the row `dataStoreId` names, this session;
137
+ * a restored row may be shared with a copied view or a duplicated
138
+ * session, so a write to one the view did not create takes a fresh id
139
+ */
140
+ ownsDataStoreRow: false,
135
141
  /**
136
142
  * #volatile
137
143
  * aborts the launch currently in flight -- from the Cancel button, and
@@ -153,7 +159,7 @@ export default function stateModelFactory() {
153
159
  */
154
160
  get transcriptToMsaMap() {
155
161
  return self.connectedFeature
156
- ? genomeToTranscriptSeqMapping(self.connectedFeature)
162
+ ? transcriptMap(self.connectedFeature)
157
163
  : undefined;
158
164
  },
159
165
  /**
@@ -219,6 +225,26 @@ export default function stateModelFactory() {
219
225
  // away for every view this one does not speak for
220
226
  return self.hostCarriesData || !!(msaIndexedLocation && msaName);
221
227
  },
228
+ /**
229
+ * #getter
230
+ * the documents a reload would lose: too big for react-msaview to keep
231
+ * in the snapshot, and with no filehandle or kept init to refetch them
232
+ * from. IndexedDB holds exactly these.
233
+ */
234
+ get unsavedDocuments() {
235
+ const { data, init } = self;
236
+ const inSnapshot = getSnapshot(data);
237
+ const unsaved = (key, source) => {
238
+ const text = data[key];
239
+ return text && !source && !inSnapshot[key] ? text : undefined;
240
+ };
241
+ return {
242
+ msa: unsaved('msa', self.msaFilehandle ?? init?.msaIndexedLocation),
243
+ tree: unsaved('tree', self.treeFilehandle),
244
+ treeMetadata: unsaved('treeMetadata', self.treeMetadataFilehandle),
245
+ gff: unsaved('gff', self.gffFilehandle),
246
+ };
247
+ },
222
248
  }))
223
249
  .actions(self => ({
224
250
  /**
@@ -305,6 +331,12 @@ export default function stateModelFactory() {
305
331
  setLastStoredData(arg) {
306
332
  self.lastStoredData = arg;
307
333
  },
334
+ /**
335
+ * #action
336
+ */
337
+ setOwnsDataStoreRow(arg) {
338
+ self.ownsDataStoreRow = arg;
339
+ },
308
340
  /**
309
341
  * #action
310
342
  */
@@ -414,7 +446,7 @@ export default function stateModelFactory() {
414
446
  * once per view, never fired again.
415
447
  */
416
448
  reset() {
417
- const { displayName, minimized, zoomToBaseLevel, dataStoreId } = self;
449
+ const { displayName, minimized, zoomToBaseLevel } = self;
418
450
  superReset();
419
451
  if (displayName !== undefined) {
420
452
  self.setDisplayName(displayName);
@@ -423,10 +455,7 @@ export default function stateModelFactory() {
423
455
  self.setZoomToBaseLevel(zoomToBaseLevel);
424
456
  self.setDomainsRequested(false);
425
457
  self.setLastStoredData(undefined);
426
- if (dataStoreId) {
427
- // nothing points at that row now
428
- void deleteMsaData(dataStoreId);
429
- }
458
+ self.setOwnsDataStoreRow(false);
430
459
  },
431
460
  };
432
461
  })
@@ -464,8 +493,8 @@ export default function stateModelFactory() {
464
493
  addDisposer(self, () => {
465
494
  self.launchController?.abort();
466
495
  });
496
+ loadStoredData(self);
467
497
  for (const fn of [
468
- loadStoredData,
469
498
  storeDataToIndexedDB,
470
499
  resolveConnectedTranscriptIfNeeded,
471
500
  launchBlastIfNeeded,
@@ -1,7 +1,6 @@
1
1
  import { beforeEach, describe, expect, test, vi } from 'vitest';
2
2
  import { doLaunchBlast } from './doLaunchBlast';
3
3
  import stateModelFactory from './model';
4
- import { deleteMsaData } from './msaDataStore';
5
4
  // the launch autoruns are live on a real model, and a blastParams write is what
6
5
  // wakes them -- so the search is mocked rather than sent to EBI. The session is
7
6
  // mocked for the same reason: the hover and highlight autoruns reach for one on
@@ -13,7 +12,6 @@ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
13
12
  vi.mock('./doLaunchBlast', () => ({ doLaunchBlast: vi.fn() }));
14
13
  vi.mock('./msaDataStore', () => ({
15
14
  cleanupOldData: vi.fn(async () => { }),
16
- deleteMsaData: vi.fn(async () => { }),
17
15
  generateDataStoreId: vi.fn(),
18
16
  retrieveMsaData: vi.fn(),
19
17
  storeMsaData: vi.fn(),
@@ -152,15 +150,16 @@ describe('returning to the import form', () => {
152
150
  expect(model.zoomToBaseLevel).toBe(true);
153
151
  expect(model.dataInitialized).toBe(false);
154
152
  });
155
- test('clears the volatiles applySnapshot cannot reach, and the stored row', () => {
153
+ test('clears the volatiles applySnapshot cannot reach, and the stored id', () => {
156
154
  const model = view();
157
155
  model.setDomainsRequested(true);
158
156
  model.setLastStoredData({ msa: '>a\nMK' });
159
157
  model.setDataStoreId('msa-1');
158
+ model.setOwnsDataStoreRow(true);
160
159
  model.reset();
161
160
  expect(model.domainsRequested).toBe(false);
162
161
  expect(model.lastStoredData).toBeUndefined();
162
+ expect(model.ownsDataStoreRow).toBe(false);
163
163
  expect(model.dataStoreId).toBeUndefined();
164
- expect(deleteMsaData).toHaveBeenCalledWith('msa-1');
165
164
  });
166
165
  });
@@ -7,12 +7,11 @@ export interface MsaDataPayload {
7
7
  }
8
8
  export declare function generateDataStoreId(): string;
9
9
  export declare function storeMsaData(id: string, data: MsaDataPayload): Promise<boolean>;
10
+ /** undefined only when no row has the id; a failed read throws */
10
11
  export declare function retrieveMsaData(id: string): Promise<{
11
12
  msa?: string;
12
13
  tree?: string;
13
14
  treeMetadata?: string;
14
15
  gff?: string;
15
16
  } | undefined>;
16
- /** drop one view's row, for a reset that just orphaned it */
17
- export declare function deleteMsaData(id: string): Promise<void>;
18
17
  export declare function cleanupOldData(maxAgeMs?: number): Promise<number>;
@@ -23,39 +23,24 @@ export async function storeMsaData(id, data) {
23
23
  return false;
24
24
  }
25
25
  }
26
+ /** undefined only when no row has the id; a failed read throws */
26
27
  export async function retrieveMsaData(id) {
27
- try {
28
- const db = await getDB();
29
- const result = await db.get(STORE_NAME, id);
30
- if (!result) {
31
- return undefined;
32
- }
33
- // reading counts as use, which is what makes cleanupOldData's policy
34
- // "unused for 7 days" rather than "written 7 days ago" -- a session opened
35
- // every day used to lose its alignment on the eighth
36
- try {
37
- await db.put(STORE_NAME, { ...result, timestamp: Date.now() });
38
- }
39
- catch (e) {
40
- console.warn('Failed to refresh MSA data timestamp:', e);
41
- }
42
- const { id: _id, timestamp: _timestamp, ...payload } = result;
43
- return payload;
44
- }
45
- catch (e) {
46
- console.warn('Failed to retrieve MSA data:', e);
28
+ const db = await getDB();
29
+ const result = await db.get(STORE_NAME, id);
30
+ if (!result) {
47
31
  return undefined;
48
32
  }
49
- }
50
- /** drop one view's row, for a reset that just orphaned it */
51
- export async function deleteMsaData(id) {
33
+ // reading counts as use, which is what makes cleanupOldData's policy
34
+ // "unused for 7 days" rather than "written 7 days ago" -- a session opened
35
+ // every day used to lose its alignment on the eighth
52
36
  try {
53
- const db = await getDB();
54
- await db.delete(STORE_NAME, id);
37
+ await db.put(STORE_NAME, { ...result, timestamp: Date.now() });
55
38
  }
56
39
  catch (e) {
57
- console.warn('Failed to delete MSA data:', e);
40
+ console.warn('Failed to refresh MSA data timestamp:', e);
58
41
  }
42
+ const { id: _id, timestamp: _timestamp, ...payload } = result;
43
+ return payload;
59
44
  }
60
45
  export async function cleanupOldData(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
61
46
  try {
@@ -4,11 +4,13 @@ import { retrieveMsaData, storeMsaData } from './msaDataStore';
4
4
  // switch for the write half so a browser that refuses writes can be played back.
5
5
  const { rows, state } = vi.hoisted(() => ({
6
6
  rows: new Map(),
7
- state: { putFails: false },
7
+ state: { putFails: false, getFails: false },
8
8
  }));
9
9
  vi.mock('../utils/idb', () => ({
10
10
  createDbOpener: () => () => Promise.resolve({
11
- get: (_store, id) => Promise.resolve(rows.get(id)),
11
+ get: (_store, id) => state.getFails
12
+ ? Promise.reject(new Error('InvalidStateError'))
13
+ : Promise.resolve(rows.get(id)),
12
14
  put: (_store, value) => {
13
15
  if (state.putFails) {
14
16
  return Promise.reject(new Error('QuotaExceededError'));
@@ -21,6 +23,7 @@ vi.mock('../utils/idb', () => ({
21
23
  beforeEach(() => {
22
24
  rows.clear();
23
25
  state.putFails = false;
26
+ state.getFails = false;
24
27
  });
25
28
  // cleanupOldData deletes by timestamp, so without this a session opened every
26
29
  // single day still lost its alignment on the eighth
@@ -34,6 +37,11 @@ test('a row that is no longer there reads as undefined and writes nothing', asyn
34
37
  expect(await retrieveMsaData('msa-gone')).toBeUndefined();
35
38
  expect(rows.size).toBe(0);
36
39
  });
40
+ test('a read that fails throws rather than reading as a missing row', async () => {
41
+ rows.set('msa-1', { id: 'msa-1', msa: '>a\nMK', timestamp: 1000 });
42
+ state.getFails = true;
43
+ await expect(retrieveMsaData('msa-1')).rejects.toThrow('InvalidStateError');
44
+ });
37
45
  // the refresh is housekeeping; failing it must not turn a readable alignment
38
46
  // into a missing one, which is what the caller reports as expired
39
47
  test('a refresh that fails still hands back the data it read', async () => {
@@ -24,6 +24,7 @@ function makeModel({ highlightColumns, querySeqOffset = 0, } = {}) {
24
24
  // g2p is indexed by genome coord; identity keeps the arithmetic out of the way
25
25
  transcriptToMsaMap: {
26
26
  g2p: Object.fromEntries([...Array(200).keys()].map(i => [i, i])),
27
+ codingPositions: [...Array(200).keys()],
27
28
  },
28
29
  highlightColumns,
29
30
  highlightedColumns: undefined,
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, expect, test, vi } from 'vitest';
2
- import { processInit, storeDataToIndexedDB } from './afterCreateAutoruns';
2
+ import { processInit } from './afterCreateAutoruns';
3
3
  import { fetchIndexedMsa } from './fetchIndexedMsa';
4
4
  vi.mock('./fetchIndexedMsa', () => ({ fetchIndexedMsa: vi.fn() }));
5
5
  vi.mock('./msaDataStore', () => ({
@@ -64,9 +64,3 @@ test('a url init is still resolved once and cleared', async () => {
64
64
  expect(model.setMSAFilehandle).toHaveBeenCalled();
65
65
  expect(model.setInit).toHaveBeenCalledWith(undefined);
66
66
  });
67
- test('an indexed view writes no IndexedDB row', async () => {
68
- const model = makeModel({ rows: [['a', 'MK']], data: { msa: '>a\nMK' } });
69
- storeDataToIndexedDB(model);
70
- await settle();
71
- expect(model.setDataStoreId).not.toHaveBeenCalled();
72
- });
@@ -6,7 +6,6 @@ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
6
6
  }));
7
7
  vi.mock('./msaDataStore', () => ({
8
8
  cleanupOldData: vi.fn(async () => { }),
9
- deleteMsaData: vi.fn(async () => { }),
10
9
  generateDataStoreId: vi.fn(),
11
10
  retrieveMsaData: vi.fn(),
12
11
  storeMsaData: vi.fn(async () => true),
@@ -36,7 +36,6 @@ vi.mock('./doLaunchBlast', () => ({
36
36
  }));
37
37
  vi.mock('./msaDataStore', () => ({
38
38
  cleanupOldData: vi.fn(async () => { }),
39
- deleteMsaData: vi.fn(async () => { }),
40
39
  generateDataStoreId: vi.fn(),
41
40
  retrieveMsaData: vi.fn(),
42
41
  storeMsaData: vi.fn(),