jbrowse-plugin-msaview 3.4.0 → 3.5.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 (171) hide show
  1. package/README.md +7 -0
  2. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +23 -17
  3. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
  4. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
  5. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
  6. package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  9. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  10. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
  11. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
  12. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
  13. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +10 -4
  14. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
  15. package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
  16. package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
  17. package/dist/LaunchMsaView/detectQueryRow.js +11 -4
  18. package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
  19. package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
  20. package/dist/LaunchMsaView/useQueryRowName.js +5 -1
  21. package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
  22. package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
  23. package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
  24. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  25. package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
  26. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
  27. package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
  28. package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
  29. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
  30. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
  31. package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
  32. package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
  33. package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
  34. package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
  35. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  36. package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
  37. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
  38. package/dist/MsaViewPanel/genomeToMSA.js +4 -4
  39. package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
  40. package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
  41. package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
  42. package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
  43. package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
  44. package/dist/MsaViewPanel/model.d.ts +306 -85
  45. package/dist/MsaViewPanel/model.js +169 -42
  46. package/dist/MsaViewPanel/model.test.d.ts +1 -0
  47. package/dist/MsaViewPanel/model.test.js +100 -0
  48. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
  49. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
  50. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
  51. package/dist/MsaViewPanel/msaDataStore.d.ts +7 -3
  52. package/dist/MsaViewPanel/msaDataStore.js +26 -7
  53. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  54. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  55. package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
  56. package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/processInit.test.js +72 -0
  58. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
  59. package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
  60. package/dist/MsaViewPanel/runLaunch.d.ts +39 -0
  61. package/dist/MsaViewPanel/runLaunch.js +100 -0
  62. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  63. package/dist/MsaViewPanel/runLaunch.test.js +175 -0
  64. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  65. package/dist/MsaViewPanel/storedData.test.js +160 -0
  66. package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
  67. package/dist/MsaViewPanel/structureConnection.js +0 -19
  68. package/dist/MsaViewPanel/util.d.ts +39 -0
  69. package/dist/MsaViewPanel/util.js +44 -0
  70. package/dist/jbrowse-plugin-msaview.umd.production.min.js +46 -45
  71. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  72. package/dist/utils/blastCache.js +32 -0
  73. package/dist/utils/blastCache.test.d.ts +1 -0
  74. package/dist/utils/blastCache.test.js +72 -0
  75. package/dist/utils/browserAlign.d.ts +42 -0
  76. package/dist/utils/browserAlign.js +297 -0
  77. package/dist/utils/browserAlign.test.d.ts +1 -0
  78. package/dist/utils/browserAlign.test.js +85 -0
  79. package/dist/utils/ebiBlast.d.ts +13 -2
  80. package/dist/utils/ebiBlast.js +32 -4
  81. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  82. package/dist/utils/ebiJobDispatcher.js +13 -6
  83. package/dist/utils/fetch.d.ts +8 -1
  84. package/dist/utils/fetch.js +28 -2
  85. package/dist/utils/homologSearch.d.ts +31 -0
  86. package/dist/utils/homologSearch.js +6 -0
  87. package/dist/utils/msa.d.ts +5 -11
  88. package/dist/utils/msa.js +19 -30
  89. package/dist/utils/msaRows.d.ts +11 -8
  90. package/dist/utils/msaRows.js +14 -12
  91. package/dist/utils/ncbiDomains.d.ts +2 -2
  92. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  93. package/dist/utils/ncbiOrthologs.js +26 -7
  94. package/dist/utils/ncbiOrthologs.test.js +23 -1
  95. package/dist/utils/pantherOrthologs.js +2 -10
  96. package/dist/utils/phmmer.d.ts +12 -1
  97. package/dist/utils/phmmer.js +52 -13
  98. package/dist/utils/poll.d.ts +6 -1
  99. package/dist/utils/poll.js +7 -2
  100. package/dist/utils/unirefHomologs.d.ts +94 -0
  101. package/dist/utils/unirefHomologs.js +193 -0
  102. package/dist/utils/unirefHomologs.test.d.ts +1 -0
  103. package/dist/utils/unirefHomologs.test.js +118 -0
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +17 -16
  107. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
  108. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
  109. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
  110. package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
  111. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  112. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  113. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
  114. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
  115. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +12 -5
  116. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
  117. package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
  118. package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
  119. package/src/LaunchMsaView/detectQueryRow.ts +22 -4
  120. package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
  121. package/src/LaunchMsaView/useQueryRowName.ts +5 -1
  122. package/src/LaunchMsaViewExtensionPoint/index.test.ts +74 -0
  123. package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
  124. package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
  125. package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
  126. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
  127. package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
  128. package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
  129. package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
  130. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
  131. package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
  132. package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
  133. package/src/MsaViewPanel/genomeToMSA.ts +8 -4
  134. package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
  135. package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
  136. package/src/MsaViewPanel/model.test.ts +121 -0
  137. package/src/MsaViewPanel/model.ts +200 -49
  138. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
  139. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
  140. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  141. package/src/MsaViewPanel/msaDataStore.ts +32 -13
  142. package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
  143. package/src/MsaViewPanel/processInit.test.ts +84 -0
  144. package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
  145. package/src/MsaViewPanel/runLaunch.test.ts +208 -0
  146. package/src/MsaViewPanel/runLaunch.ts +141 -0
  147. package/src/MsaViewPanel/storedData.test.ts +196 -0
  148. package/src/MsaViewPanel/structureConnection.ts +0 -26
  149. package/src/MsaViewPanel/util.ts +75 -0
  150. package/src/utils/blastCache.test.ts +86 -0
  151. package/src/utils/blastCache.ts +34 -1
  152. package/src/utils/browserAlign.test.ts +102 -0
  153. package/src/utils/browserAlign.ts +352 -0
  154. package/src/utils/ebiBlast.ts +42 -2
  155. package/src/utils/ebiJobDispatcher.ts +18 -3
  156. package/src/utils/fetch.ts +29 -2
  157. package/src/utils/homologSearch.ts +51 -0
  158. package/src/utils/msa.ts +25 -39
  159. package/src/utils/msaRows.ts +20 -16
  160. package/src/utils/ncbiDomains.ts +4 -2
  161. package/src/utils/ncbiOrthologs.test.ts +25 -0
  162. package/src/utils/ncbiOrthologs.ts +27 -7
  163. package/src/utils/pantherOrthologs.ts +6 -11
  164. package/src/utils/phmmer.ts +62 -12
  165. package/src/utils/poll.ts +8 -1
  166. package/src/utils/unirefHomologs.test.ts +149 -0
  167. package/src/utils/unirefHomologs.ts +323 -0
  168. package/src/version.ts +1 -1
  169. package/dist/MsaViewPanel/structureConnection.test.js +0 -53
  170. package/src/MsaViewPanel/structureConnection.test.ts +0 -62
  171. /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts} +0 -0
@@ -1,6 +1,41 @@
1
1
  import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
2
+ import { MSAModelF } from 'react-msaview';
2
3
  import { describe, expect, test } from 'vitest';
3
4
  import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
5
+ /**
6
+ * A stand-in for the view model, converting exactly as react-msaview does: a
7
+ * visible column is shifted right past every hidden column before it, and the
8
+ * residue is then counted along the row.
9
+ */
10
+ function coordModel({ rows, blanks = [], ...rest }) {
11
+ return {
12
+ ...rest,
13
+ transcriptToMsaMap: rest.transcriptToMsaMap,
14
+ visibleColToSeqPos(rowName, visibleCol) {
15
+ const seq = rows.find(r => r[0] === rowName)?.[1];
16
+ if (seq === undefined) {
17
+ return undefined;
18
+ }
19
+ let globalCol = visibleCol;
20
+ for (const blank of blanks) {
21
+ if (blank <= globalCol) {
22
+ globalCol++;
23
+ }
24
+ }
25
+ const isBlank = (c) => c === '-' || c === '.';
26
+ if (globalCol >= seq.length || isBlank(seq[globalCol])) {
27
+ return undefined;
28
+ }
29
+ let seqPos = 0;
30
+ for (let i = 0; i < globalCol; i++) {
31
+ if (!isBlank(seq[i])) {
32
+ seqPos++;
33
+ }
34
+ }
35
+ return seqPos;
36
+ },
37
+ };
38
+ }
4
39
  // codon at protein position i covers three consecutive genome bases starting at
5
40
  // 100 + i * 3, i.e. a single-exon forward-strand transcript
6
41
  function forwardCodons(n) {
@@ -11,49 +46,49 @@ function forwardCodons(n) {
11
46
  }
12
47
  describe('msaCoordToGenomeCoord', () => {
13
48
  test('returns undefined when neither transcriptToMsaMap nor mafRegion is defined', () => {
14
- const model = {
49
+ const model = coordModel({
15
50
  querySeqName: 'QUERY',
16
51
  transcriptToMsaMap: undefined,
17
52
  mafRegion: undefined,
18
53
  rows: [['QUERY', 'MKAA']],
19
- };
54
+ });
20
55
  const result = msaCoordToGenomeCoord({ model, coord: 0 });
21
56
  expect(result).toBeUndefined();
22
57
  });
23
58
  test('returns undefined when query row is not found', () => {
24
- const model = {
59
+ const model = coordModel({
25
60
  querySeqName: 'QUERY',
26
61
  transcriptToMsaMap: {
27
62
  refName: 'chr1',
28
63
  p2gCodon: forwardCodons(2),
29
64
  },
30
65
  rows: [['OTHER', 'MKAA']],
31
- };
66
+ });
32
67
  const result = msaCoordToGenomeCoord({ model, coord: 0 });
33
68
  expect(result).toBeUndefined();
34
69
  });
35
70
  test('returns undefined when coord is a gap', () => {
36
- const model = {
71
+ const model = coordModel({
37
72
  querySeqName: 'QUERY',
38
73
  transcriptToMsaMap: {
39
74
  refName: 'chr1',
40
75
  p2gCodon: forwardCodons(2),
41
76
  },
42
77
  rows: [['QUERY', 'M-KA']],
43
- };
78
+ });
44
79
  // Position 1 is a gap
45
80
  const result = msaCoordToGenomeCoord({ model, coord: 1 });
46
81
  expect(result).toBeUndefined();
47
82
  });
48
83
  test('returns genome region for valid non-gap position', () => {
49
- const model = {
84
+ const model = coordModel({
50
85
  querySeqName: 'QUERY',
51
86
  transcriptToMsaMap: {
52
87
  refName: 'chr1',
53
88
  p2gCodon: forwardCodons(4),
54
89
  },
55
90
  rows: [['QUERY', 'MKAA']],
56
- };
91
+ });
57
92
  // Position 0 (M) should map to ungapped 0, genome 100-103
58
93
  const result = msaCoordToGenomeCoord({ model, coord: 0 });
59
94
  expect(result).toEqual({
@@ -63,7 +98,7 @@ describe('msaCoordToGenomeCoord', () => {
63
98
  });
64
99
  });
65
100
  test('handles gapped sequence correctly', () => {
66
- const model = {
101
+ const model = coordModel({
67
102
  querySeqName: 'QUERY',
68
103
  transcriptToMsaMap: {
69
104
  refName: 'chr1',
@@ -72,7 +107,7 @@ describe('msaCoordToGenomeCoord', () => {
72
107
  rows: [['QUERY', 'M-K-AA']],
73
108
  // 012345 gapped positions
74
109
  // 0 1 23 ungapped positions
75
- };
110
+ });
76
111
  // Gapped position 2 (K) = ungapped 1
77
112
  const result = msaCoordToGenomeCoord({ model, coord: 2 });
78
113
  expect(result).toEqual({
@@ -89,27 +124,27 @@ describe('msaCoordToGenomeCoord', () => {
89
124
  });
90
125
  });
91
126
  test('returns undefined when the position has no codon mapping', () => {
92
- const model = {
127
+ const model = coordModel({
93
128
  querySeqName: 'QUERY',
94
129
  transcriptToMsaMap: {
95
130
  refName: 'chr1',
96
131
  p2gCodon: forwardCodons(1),
97
132
  },
98
133
  rows: [['QUERY', 'MKAA']],
99
- };
134
+ });
100
135
  // ungapped position 1 has no entry in p2gCodon
101
136
  const result = msaCoordToGenomeCoord({ model, coord: 1 });
102
137
  expect(result).toBeUndefined();
103
138
  });
104
139
  test('maps the final residue, whose codon has no successor', () => {
105
- const model = {
140
+ const model = coordModel({
106
141
  querySeqName: 'QUERY',
107
142
  transcriptToMsaMap: {
108
143
  refName: 'chr1',
109
144
  p2gCodon: forwardCodons(4),
110
145
  },
111
146
  rows: [['QUERY', 'MKAA']],
112
- };
147
+ });
113
148
  const result = msaCoordToGenomeCoord({ model, coord: 3 });
114
149
  expect(result).toEqual({
115
150
  refName: 'chr1',
@@ -118,20 +153,20 @@ describe('msaCoordToGenomeCoord', () => {
118
153
  });
119
154
  });
120
155
  test('returns undefined for out of bounds coord', () => {
121
- const model = {
156
+ const model = coordModel({
122
157
  querySeqName: 'QUERY',
123
158
  transcriptToMsaMap: {
124
159
  refName: 'chr1',
125
160
  p2gCodon: forwardCodons(2),
126
161
  },
127
162
  rows: [['QUERY', 'MK']],
128
- };
163
+ });
129
164
  // Position 10 is out of bounds
130
165
  const result = msaCoordToGenomeCoord({ model, coord: 10 });
131
166
  expect(result).toBeUndefined();
132
167
  });
133
168
  test('works with multiple rows, uses querySeqName', () => {
134
- const model = {
169
+ const model = coordModel({
135
170
  querySeqName: 'SEQ2',
136
171
  transcriptToMsaMap: {
137
172
  refName: 'chr1',
@@ -142,7 +177,7 @@ describe('msaCoordToGenomeCoord', () => {
142
177
  ['SEQ2', 'MKAA'],
143
178
  ['SEQ3', 'LLLL'],
144
179
  ],
145
- };
180
+ });
146
181
  const result = msaCoordToGenomeCoord({ model, coord: 0 });
147
182
  expect(result).toEqual({
148
183
  refName: 'chr1',
@@ -162,11 +197,11 @@ describe('msaCoordToGenomeCoord', () => {
162
197
  strand: 1,
163
198
  subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
164
199
  });
165
- const model = {
200
+ const model = coordModel({
166
201
  querySeqName: 'QUERY',
167
202
  transcriptToMsaMap: { refName, p2gCodon },
168
203
  rows: [['QUERY', 'MKAA']],
169
- };
204
+ });
170
205
  expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
171
206
  refName: 'chr1',
172
207
  start: 100,
@@ -186,11 +221,11 @@ describe('msaCoordToGenomeCoord', () => {
186
221
  strand: -1,
187
222
  subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
188
223
  });
189
- const model = {
224
+ const model = coordModel({
190
225
  querySeqName: 'QUERY',
191
226
  transcriptToMsaMap: { refName, p2gCodon },
192
227
  rows: [['QUERY', 'MKAA']],
193
- };
228
+ });
194
229
  // the first residue is translated from the 3' end of the genome region
195
230
  expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
196
231
  refName: 'chr1',
@@ -215,11 +250,11 @@ describe('msaCoordToGenomeCoord', () => {
215
250
  { refName: 'chr1', type: 'CDS', start: 200, end: 202 },
216
251
  ],
217
252
  });
218
- const model = {
253
+ const model = coordModel({
219
254
  querySeqName: 'QUERY',
220
255
  transcriptToMsaMap: { refName, p2gCodon },
221
256
  rows: [['QUERY', 'MK']],
222
- };
257
+ });
223
258
  expect(msaCoordToGenomeRegions({ model, coord: 1 })).toEqual([
224
259
  { refName: 'chr1', start: 103, end: 104 },
225
260
  { refName: 'chr1', start: 200, end: 202 },
@@ -235,7 +270,7 @@ describe('msaCoordToGenomeCoord', () => {
235
270
  // MAF region tests
236
271
  describe('mafRegion', () => {
237
272
  test('returns genome position for mafRegion mapping', () => {
238
- const model = {
273
+ const model = coordModel({
239
274
  querySeqName: 'hg38.chr1',
240
275
  transcriptToMsaMap: undefined,
241
276
  mafRegion: {
@@ -245,7 +280,7 @@ describe('msaCoordToGenomeCoord', () => {
245
280
  assemblyName: 'hg38',
246
281
  },
247
282
  rows: [['hg38.chr1', 'ACGTACGTAC']],
248
- };
283
+ });
249
284
  // Position 0 should map to genome 1000
250
285
  const result = msaCoordToGenomeCoord({ model, coord: 0 });
251
286
  expect(result).toEqual({
@@ -262,7 +297,7 @@ describe('msaCoordToGenomeCoord', () => {
262
297
  });
263
298
  });
264
299
  test('handles gaps in mafRegion sequence', () => {
265
- const model = {
300
+ const model = coordModel({
266
301
  querySeqName: 'hg38.chr1',
267
302
  transcriptToMsaMap: undefined,
268
303
  mafRegion: {
@@ -274,7 +309,7 @@ describe('msaCoordToGenomeCoord', () => {
274
309
  rows: [['hg38.chr1', 'AC--GTAC']],
275
310
  // Gapped positions: 0 1 2 3 4 5 6 7
276
311
  // Ungapped: 0 1 2 3 4 5
277
- };
312
+ });
278
313
  // Position 2 is a gap, should return undefined
279
314
  const result = msaCoordToGenomeCoord({ model, coord: 2 });
280
315
  expect(result).toBeUndefined();
@@ -287,7 +322,7 @@ describe('msaCoordToGenomeCoord', () => {
287
322
  });
288
323
  });
289
324
  test('returns undefined when position exceeds mafRegion end', () => {
290
- const model = {
325
+ const model = coordModel({
291
326
  querySeqName: 'hg38.chr1',
292
327
  transcriptToMsaMap: undefined,
293
328
  mafRegion: {
@@ -297,13 +332,13 @@ describe('msaCoordToGenomeCoord', () => {
297
332
  assemblyName: 'hg38',
298
333
  },
299
334
  rows: [['hg38.chr1', 'ACGTACGTAC']], // 10 chars but region is only 5bp
300
- };
335
+ });
301
336
  // Position 8 would be ungapped 8 = genome 1008, but region ends at 1005
302
337
  const result = msaCoordToGenomeCoord({ model, coord: 8 });
303
338
  expect(result).toBeUndefined();
304
339
  });
305
340
  test('mafRegion takes precedence over transcriptToMsaMap', () => {
306
- const model = {
341
+ const model = coordModel({
307
342
  querySeqName: 'hg38.chr1',
308
343
  transcriptToMsaMap: {
309
344
  refName: 'chr2',
@@ -316,7 +351,7 @@ describe('msaCoordToGenomeCoord', () => {
316
351
  assemblyName: 'hg38',
317
352
  },
318
353
  rows: [['hg38.chr1', 'ACGTACGTAC']],
319
- };
354
+ });
320
355
  // Should use mafRegion, not transcriptToMsaMap
321
356
  const result = msaCoordToGenomeCoord({ model, coord: 0 });
322
357
  expect(result).toEqual({
@@ -327,3 +362,69 @@ describe('msaCoordToGenomeCoord', () => {
327
362
  });
328
363
  });
329
364
  });
365
+ // mouseCol and mouseClickCol are VISIBLE columns, and react-msaview hides
366
+ // columns as soon as a clade collapses or allowedGappyness drops below 100 --
367
+ // which `hideGaps` leaves on by default. Counting residues along the raw gapped
368
+ // row instead landed every hover and click some codons off.
369
+ describe('columns react-msaview is hiding', () => {
370
+ test('a visible column past a hidden one maps to the residue on screen', () => {
371
+ const model = coordModel({
372
+ querySeqName: 'QUERY',
373
+ transcriptToMsaMap: {
374
+ refName: 'chr1',
375
+ p2gCodon: forwardCodons(4),
376
+ },
377
+ rows: [['QUERY', 'M-KA']],
378
+ blanks: [1],
379
+ });
380
+ expect(msaCoordToGenomeCoord({ model, coord: 1 })).toEqual({
381
+ refName: 'chr1',
382
+ start: 103,
383
+ end: 106,
384
+ });
385
+ expect(msaCoordToGenomeCoord({ model, coord: 2 })).toEqual({
386
+ refName: 'chr1',
387
+ start: 106,
388
+ end: 109,
389
+ });
390
+ });
391
+ test('a MAF row shifts by the hidden columns too', () => {
392
+ const model = coordModel({
393
+ querySeqName: 'hg38.chr1',
394
+ mafRegion: {
395
+ refName: 'chr1',
396
+ start: 1000,
397
+ end: 1008,
398
+ assemblyName: 'hg38',
399
+ },
400
+ rows: [['hg38.chr1', 'AC--GTAC']],
401
+ blanks: [2, 3],
402
+ });
403
+ expect(msaCoordToGenomeCoord({ model, coord: 2 })).toEqual({
404
+ refName: 'chr1',
405
+ start: 1002,
406
+ end: 1003,
407
+ });
408
+ });
409
+ // the fixture above says what blanks mean; this says react-msaview agrees,
410
+ // so a change to how it hides columns fails here rather than in the browser
411
+ test('against the real react-msaview model', () => {
412
+ const model = MSAModelF().create({
413
+ type: 'MsaView',
414
+ data: { msa: '>QUERY\nM-KA\n>B\nMWKA\n>C\nM-KA\n' },
415
+ allowedGappyness: 50,
416
+ });
417
+ expect(model.blanks).toEqual([1]);
418
+ expect(model.numColumns).toBe(3);
419
+ const fromView = {
420
+ querySeqName: 'QUERY',
421
+ transcriptToMsaMap: { refName: 'chr1', p2gCodon: forwardCodons(4) },
422
+ visibleColToSeqPos: (rowName, visibleCol) => model.visibleColToSeqPos(rowName, visibleCol),
423
+ };
424
+ expect(msaCoordToGenomeCoord({ model: fromView, coord: 1 })).toEqual({
425
+ refName: 'chr1',
426
+ start: 103,
427
+ end: 106,
428
+ });
429
+ });
430
+ });
@@ -1,12 +1,16 @@
1
- export declare function generateDataStoreId(): string;
2
- export declare function storeMsaData(id: string, data: {
1
+ /** the documents a view keeps in IndexedDB rather than in its session snapshot */
2
+ export interface MsaDataPayload {
3
3
  msa?: string;
4
4
  tree?: string;
5
5
  treeMetadata?: string;
6
- }): Promise<boolean>;
6
+ }
7
+ export declare function generateDataStoreId(): string;
8
+ export declare function storeMsaData(id: string, data: MsaDataPayload): Promise<boolean>;
7
9
  export declare function retrieveMsaData(id: string): Promise<{
8
10
  msa: string | undefined;
9
11
  tree: string | undefined;
10
12
  treeMetadata: string | undefined;
11
13
  } | undefined>;
14
+ /** drop one view's row, for a reset that just orphaned it */
15
+ export declare function deleteMsaData(id: string): Promise<void>;
12
16
  export declare function cleanupOldData(maxAgeMs?: number): Promise<number>;
@@ -33,20 +33,39 @@ export async function retrieveMsaData(id) {
33
33
  try {
34
34
  const db = await getDB();
35
35
  const result = await db.get(STORE_NAME, id);
36
- if (result) {
37
- return {
38
- msa: result.msa,
39
- tree: result.tree,
40
- treeMetadata: result.treeMetadata,
41
- };
36
+ if (!result) {
37
+ return undefined;
42
38
  }
43
- return undefined;
39
+ // reading counts as use, which is what makes cleanupOldData's policy
40
+ // "unused for 7 days" rather than "written 7 days ago" -- a session opened
41
+ // every day used to lose its alignment on the eighth
42
+ try {
43
+ await db.put(STORE_NAME, { ...result, timestamp: Date.now() });
44
+ }
45
+ catch (e) {
46
+ console.warn('Failed to refresh MSA data timestamp:', e);
47
+ }
48
+ return {
49
+ msa: result.msa,
50
+ tree: result.tree,
51
+ treeMetadata: result.treeMetadata,
52
+ };
44
53
  }
45
54
  catch (e) {
46
55
  console.warn('Failed to retrieve MSA data:', e);
47
56
  return undefined;
48
57
  }
49
58
  }
59
+ /** drop one view's row, for a reset that just orphaned it */
60
+ export async function deleteMsaData(id) {
61
+ try {
62
+ const db = await getDB();
63
+ await db.delete(STORE_NAME, id);
64
+ }
65
+ catch (e) {
66
+ console.warn('Failed to delete MSA data:', e);
67
+ }
68
+ }
50
69
  export async function cleanupOldData(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
51
70
  try {
52
71
  const db = await getDB();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { beforeEach, expect, test, vi } from 'vitest';
2
+ import { retrieveMsaData } from './msaDataStore';
3
+ // An in-memory stand-in for the one object store this module opens, with a
4
+ // switch for the write half so a browser that refuses writes can be played back.
5
+ const { rows, state } = vi.hoisted(() => ({
6
+ rows: new Map(),
7
+ state: { putFails: false },
8
+ }));
9
+ vi.mock('../utils/idb', () => ({
10
+ createDbOpener: () => () => Promise.resolve({
11
+ get: (_store, id) => Promise.resolve(rows.get(id)),
12
+ put: (_store, value) => {
13
+ if (state.putFails) {
14
+ return Promise.reject(new Error('QuotaExceededError'));
15
+ }
16
+ rows.set(value.id, value);
17
+ return Promise.resolve(value.id);
18
+ },
19
+ }),
20
+ }));
21
+ beforeEach(() => {
22
+ rows.clear();
23
+ state.putFails = false;
24
+ });
25
+ // cleanupOldData deletes by timestamp, so without this a session opened every
26
+ // single day still lost its alignment on the eighth
27
+ test('reading a row refreshes its timestamp, making the expiry "unused for 7 days"', async () => {
28
+ rows.set('msa-1', { id: 'msa-1', msa: '>a\nMK', timestamp: 1000 });
29
+ const before = Date.now();
30
+ expect((await retrieveMsaData('msa-1'))?.msa).toBe('>a\nMK');
31
+ expect(rows.get('msa-1').timestamp).toBeGreaterThanOrEqual(before);
32
+ });
33
+ test('a row that is no longer there reads as undefined and writes nothing', async () => {
34
+ expect(await retrieveMsaData('msa-gone')).toBeUndefined();
35
+ expect(rows.size).toBe(0);
36
+ });
37
+ // the refresh is housekeeping; failing it must not turn a readable alignment
38
+ // into a missing one, which is what the caller reports as expired
39
+ test('a refresh that fails still hands back the data it read', async () => {
40
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
41
+ rows.set('msa-1', { id: 'msa-1', msa: '>a\nMK', timestamp: 1000 });
42
+ state.putFails = true;
43
+ expect((await retrieveMsaData('msa-1'))?.msa).toBe('>a\nMK');
44
+ });
@@ -14,10 +14,11 @@ const CONNECTED = 'lgv-1';
14
14
  * column equals the genome coordinate that produced it and the test reads as
15
15
  * "these genome coords lit these columns".
16
16
  */
17
- function makeModel({ highlightColumns } = {}) {
17
+ function makeModel({ highlightColumns, querySeqOffset = 0, } = {}) {
18
18
  const calls = [];
19
19
  const model = {
20
20
  querySeqName: 'query',
21
+ querySeqOffset,
21
22
  rows: [['query', 'MKVLTAEEK']],
22
23
  connectedViewId: CONNECTED,
23
24
  // g2p is indexed by genome coord; identity keeps the arithmetic out of the way
@@ -26,8 +27,8 @@ function makeModel({ highlightColumns } = {}) {
26
27
  },
27
28
  highlightColumns,
28
29
  highlightedColumns: undefined,
29
- seqPosToGlobalCol: (_name, pos) => pos,
30
- globalColToVisibleCol: (col) => col,
30
+ seqPosToVisibleCol: (_name, pos) => pos,
31
+ visibleColToSeqPos: (_name, col) => col,
31
32
  setHighlightedColumns: (cols) => {
32
33
  calls.push(cols);
33
34
  model.highlightedColumns = cols;
@@ -69,6 +70,22 @@ describe('the hover channel', () => {
69
70
  run();
70
71
  expect(calls).toEqual([[10, 11, 12]]);
71
72
  });
73
+ // a query row trimmed to what BLAST aligned starts partway into the protein,
74
+ // so the structure's residues sit that many columns to the left
75
+ test('a trimmed query row shifts the lit columns by its offset', () => {
76
+ const { model, calls } = makeModel({ querySeqOffset: 4 });
77
+ const run = observeProteinHighlights(model);
78
+ session({ hover: [{ start: 10, end: 13 }] });
79
+ run();
80
+ expect(calls).toEqual([[6, 7, 8]]);
81
+ });
82
+ test('a residue before a trimmed row lights nothing', () => {
83
+ const { model, calls } = makeModel({ querySeqOffset: 20 });
84
+ const run = observeProteinHighlights(model);
85
+ session({ hover: [{ start: 10, end: 13 }] });
86
+ run();
87
+ expect(calls).toEqual([]);
88
+ });
72
89
  test('releasing the hover clears the highlight', () => {
73
90
  const { model, calls } = makeModel();
74
91
  const run = observeProteinHighlights(model);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { beforeEach, expect, test, vi } from 'vitest';
2
+ import { processInit, storeDataToIndexedDB } from './afterCreateAutoruns';
3
+ import { fetchIndexedMsa } from './fetchIndexedMsa';
4
+ vi.mock('./fetchIndexedMsa', () => ({ fetchIndexedMsa: vi.fn() }));
5
+ vi.mock('./msaDataStore', () => ({
6
+ cleanupOldData: vi.fn(),
7
+ generateDataStoreId: vi.fn(() => 'generated'),
8
+ retrieveMsaData: vi.fn(),
9
+ storeMsaData: vi.fn(async () => true),
10
+ }));
11
+ const INDEXED = {
12
+ msaIndexedLocation: { uri: 'http://example.com/msa.fa.gz' },
13
+ msaName: 'ENST00000288602',
14
+ };
15
+ function makeModel(over = {}) {
16
+ const model = {
17
+ init: INDEXED,
18
+ data: {},
19
+ rows: [],
20
+ msaFilehandle: undefined,
21
+ treeFilehandle: undefined,
22
+ isStoringData: false,
23
+ lastStoredData: undefined,
24
+ setError: vi.fn(),
25
+ setUniprotId: vi.fn(),
26
+ setQuerySeqName: vi.fn(),
27
+ setMSAFilehandle: vi.fn(),
28
+ setDataStoreId: vi.fn(),
29
+ setIsStoringData: vi.fn(),
30
+ setLastStoredData: vi.fn(),
31
+ setInit: vi.fn(),
32
+ setMSA(arg) {
33
+ model.data.msa = arg;
34
+ },
35
+ ...over,
36
+ };
37
+ return model;
38
+ }
39
+ const settle = () => new Promise(res => setTimeout(res, 0));
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+ // react-msaview drops an inline alignment over 50kb from the snapshot and an
44
+ // indexed block has no filehandle to reload from, so clearing init left a
45
+ // shared session saying the alignment had expired
46
+ test('an indexed init is kept, so the block can be refetched', async () => {
47
+ vi.mocked(fetchIndexedMsa).mockResolvedValue('>a\nMK');
48
+ const model = makeModel();
49
+ processInit(model);
50
+ await settle();
51
+ expect(model.data.msa).toBe('>a\nMK');
52
+ expect(model.setInit).not.toHaveBeenCalled();
53
+ });
54
+ test('an alignment already in hand is not refetched', async () => {
55
+ const model = makeModel({ data: { msa: '>a\nMK' } });
56
+ processInit(model);
57
+ await settle();
58
+ expect(fetchIndexedMsa).not.toHaveBeenCalled();
59
+ });
60
+ test('a url init is still resolved once and cleared', async () => {
61
+ const model = makeModel({ init: { msaUrl: 'http://example.com/msa.fa' } });
62
+ processInit(model);
63
+ await settle();
64
+ expect(model.setMSAFilehandle).toHaveBeenCalled();
65
+ expect(model.setInit).toHaveBeenCalledWith(undefined);
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
+ });
@@ -0,0 +1,9 @@
1
+ import type { JBrowsePluginMsaViewModel } from './model';
2
+ /**
3
+ * Resolve `connectedTranscript` into `connectedFeature` once the connected
4
+ * genome view has its assembly, regions and tracks, then hand the translation
5
+ * to whichever launch is waiting for a query. Left in place on failure so the
6
+ * error stays attributable; the autorun's tracked reads are the view's
7
+ * readiness, so nothing refires until that changes.
8
+ */
9
+ export declare function resolveConnectedTranscriptIfNeeded(self: JBrowsePluginMsaViewModel): void;