jbrowse-plugin-msaview 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  15. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  16. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
  17. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  18. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  19. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
  20. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  21. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  22. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  23. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  24. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  25. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  26. package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
  27. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  28. package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
  29. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
  30. package/dist/MsaViewPanel/afterCreateAutoruns.js +95 -72
  31. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  32. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  33. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  34. package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
  35. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  36. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  37. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
  38. package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
  39. package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
  40. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  41. package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
  42. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
  43. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  44. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  45. package/dist/MsaViewPanel/model.d.ts +190 -19
  46. package/dist/MsaViewPanel/model.js +51 -1
  47. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  48. package/dist/MsaViewPanel/msaDataStore.js +16 -7
  49. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  50. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  51. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  52. package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
  53. package/dist/MsaViewPanel/runLaunch.js +65 -0
  54. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  55. package/dist/MsaViewPanel/runLaunch.test.js +129 -0
  56. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/storedData.test.js +160 -0
  58. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  59. package/dist/MsaViewPanel/util.d.ts +18 -0
  60. package/dist/MsaViewPanel/util.js +17 -0
  61. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  62. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  63. package/dist/utils/blastCache.d.ts +10 -6
  64. package/dist/utils/blastCache.js +47 -3
  65. package/dist/utils/blastCache.test.d.ts +1 -0
  66. package/dist/utils/blastCache.test.js +72 -0
  67. package/dist/utils/ebiBlast.d.ts +5 -3
  68. package/dist/utils/ebiBlast.js +6 -4
  69. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  70. package/dist/utils/ebiJobDispatcher.js +13 -6
  71. package/dist/utils/fetch.d.ts +8 -1
  72. package/dist/utils/fetch.js +28 -2
  73. package/dist/utils/msa.d.ts +15 -1
  74. package/dist/utils/msa.js +40 -13
  75. package/dist/utils/msaRows.d.ts +31 -0
  76. package/dist/utils/msaRows.js +67 -0
  77. package/dist/utils/ncbiDomains.d.ts +2 -2
  78. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  79. package/dist/utils/ncbiOrthologs.js +26 -7
  80. package/dist/utils/ncbiOrthologs.test.js +23 -1
  81. package/dist/utils/pantherOrthologs.js +2 -10
  82. package/dist/utils/phmmer.d.ts +54 -0
  83. package/dist/utils/phmmer.js +120 -0
  84. package/dist/utils/poll.d.ts +6 -1
  85. package/dist/utils/poll.js +7 -2
  86. package/dist/utils/taxonomyNames.d.ts +1 -1
  87. package/dist/utils/taxonomyNames.js +6 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +30 -24
  91. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  92. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  93. package/src/AddHighlightModel/index.tsx +1 -1
  94. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
  95. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  96. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  97. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  98. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  99. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  100. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  101. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  102. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  103. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  104. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
  105. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  106. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  107. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  108. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  109. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  110. package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
  111. package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
  112. package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
  113. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  114. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  115. package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
  116. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
  117. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  118. package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
  119. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
  120. package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
  121. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  122. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  123. package/src/MsaViewPanel/model.ts +82 -6
  124. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  125. package/src/MsaViewPanel/msaDataStore.ts +22 -13
  126. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  127. package/src/MsaViewPanel/runLaunch.test.ts +154 -0
  128. package/src/MsaViewPanel/runLaunch.ts +102 -0
  129. package/src/MsaViewPanel/storedData.test.ts +196 -0
  130. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  131. package/src/MsaViewPanel/util.ts +18 -0
  132. package/src/utils/blastCache.test.ts +86 -0
  133. package/src/utils/blastCache.ts +67 -13
  134. package/src/utils/ebiBlast.ts +9 -3
  135. package/src/utils/ebiJobDispatcher.ts +18 -3
  136. package/src/utils/fetch.ts +29 -2
  137. package/src/utils/msa.ts +51 -12
  138. package/src/utils/msaRows.ts +95 -0
  139. package/src/utils/ncbiDomains.ts +4 -2
  140. package/src/utils/ncbiOrthologs.test.ts +25 -0
  141. package/src/utils/ncbiOrthologs.ts +27 -7
  142. package/src/utils/pantherOrthologs.ts +6 -11
  143. package/src/utils/phmmer.ts +178 -0
  144. package/src/utils/poll.ts +8 -1
  145. package/src/utils/taxonomyNames.ts +6 -1
  146. package/src/version.ts +1 -1
  147. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  148. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  149. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -0,0 +1,65 @@
1
+ import { isAbortError } from '../utils/fetch';
2
+ /**
3
+ * Run one launch attempt, owning the AbortController that ties it to the view.
4
+ *
5
+ * The controller lives on the model because two things end a launch and neither
6
+ * is here: the Cancel button, and the disposer `afterCreate` registers. Before
7
+ * it existed, closing a view mid-BLAST left the poller checking EBI for the
8
+ * job's lifetime and then writing to a node that was gone.
9
+ */
10
+ export function runLaunch({ self, message, launch, onLaunched, }) {
11
+ const controller = new AbortController();
12
+ const { signal } = controller;
13
+ self.setLaunchController(controller);
14
+ const act = (fn) => {
15
+ if (!signal.aborted) {
16
+ fn();
17
+ }
18
+ };
19
+ const scope = {
20
+ signal,
21
+ act,
22
+ onProgress: arg => {
23
+ act(() => {
24
+ self.setProgress(arg);
25
+ });
26
+ },
27
+ onRid: arg => {
28
+ act(() => {
29
+ self.setRid(arg);
30
+ });
31
+ },
32
+ };
33
+ void (async () => {
34
+ try {
35
+ act(() => {
36
+ self.setProgress(message);
37
+ self.setError(undefined);
38
+ });
39
+ const data = await launch(scope);
40
+ act(() => {
41
+ self.setData(data);
42
+ onLaunched();
43
+ });
44
+ }
45
+ catch (e) {
46
+ // a cancel is not a failure: drawing the error panel for one would tell
47
+ // the user their launch broke when they are the one who stopped it
48
+ if (!isAbortError(e)) {
49
+ act(() => {
50
+ self.setError(e);
51
+ });
52
+ console.error(e);
53
+ }
54
+ }
55
+ finally {
56
+ act(() => {
57
+ self.setProgress('');
58
+ // only if a later launch has not already claimed the slot
59
+ if (self.launchController === controller) {
60
+ self.setLaunchController(undefined);
61
+ }
62
+ });
63
+ }
64
+ })();
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import { runLaunch } from './runLaunch';
3
+ /**
4
+ * A model that records every write, so "no model actions after abort" is
5
+ * assertable rather than inferred. The real thing throws on a destroyed node;
6
+ * here a write that should not have happened simply shows up in the log.
7
+ */
8
+ function makeModel() {
9
+ const writes = [];
10
+ const model = {
11
+ launchController: undefined,
12
+ setLaunchController(arg) {
13
+ model.launchController = arg;
14
+ writes.push(`setLaunchController:${arg ? 'set' : 'cleared'}`);
15
+ },
16
+ setProgress: (arg) => writes.push(`setProgress:${arg}`),
17
+ setError: (e) => writes.push(`setError:${e instanceof Error ? e.message : String(e)}`),
18
+ setRid: (arg) => writes.push(`setRid:${arg}`),
19
+ setData: () => writes.push('setData'),
20
+ };
21
+ return { model, writes };
22
+ }
23
+ const DATA = { msa: '>a\nMK', tree: '(a);', treeMetadata: '{}' };
24
+ /** run the microtask queue out so the launch's async body has finished */
25
+ const settle = () => new Promise(res => setTimeout(res, 0));
26
+ describe('a launch that finishes', () => {
27
+ test('applies its data, clears its params and releases the controller', async () => {
28
+ const { model, writes } = makeModel();
29
+ const onLaunched = vi.fn();
30
+ runLaunch({
31
+ self: model,
32
+ message: 'Submitting query',
33
+ onLaunched,
34
+ launch: async (scope) => {
35
+ scope.onRid('ncbiblast-1');
36
+ scope.onProgress('Re-checking BLAST status in... 7');
37
+ return DATA;
38
+ },
39
+ });
40
+ await settle();
41
+ expect(writes).toEqual([
42
+ 'setLaunchController:set',
43
+ 'setProgress:Submitting query',
44
+ 'setError:undefined',
45
+ 'setRid:ncbiblast-1',
46
+ 'setProgress:Re-checking BLAST status in... 7',
47
+ 'setData',
48
+ 'setProgress:',
49
+ 'setLaunchController:cleared',
50
+ ]);
51
+ expect(onLaunched).toHaveBeenCalled();
52
+ });
53
+ });
54
+ // The failure this file exists for: closing a view mid-BLAST left the poller
55
+ // running for the job's lifetime and then writing to a destroyed
56
+ // mobx-state-tree node, whose throw surfaced as an unhandled rejection.
57
+ describe('a launch that is abandoned', () => {
58
+ async function abandonMidFlight(reject) {
59
+ const { model, writes } = makeModel();
60
+ let released;
61
+ const held = new Promise(res => {
62
+ released = res;
63
+ });
64
+ let captured;
65
+ runLaunch({
66
+ self: model,
67
+ message: 'Submitting query',
68
+ onLaunched: () => {
69
+ throw new Error('a cancelled launch must not report success');
70
+ },
71
+ launch: async (scope) => {
72
+ captured = scope;
73
+ await held;
74
+ // what the network hop does once its signal is aborted: either it
75
+ // rejects with an AbortError, or it had already resolved and returns
76
+ if (reject) {
77
+ throw new DOMException('Aborted', 'AbortError');
78
+ }
79
+ return DATA;
80
+ },
81
+ });
82
+ await settle();
83
+ const before = writes.length;
84
+ model.launchController.abort();
85
+ // everything the launch would still try to do after the abort
86
+ captured.onProgress('Re-checking BLAST status in... 3');
87
+ captured.onRid('ncbiblast-2');
88
+ captured.act(() => {
89
+ throw new Error('act must not run its callback after an abort');
90
+ });
91
+ released();
92
+ await settle();
93
+ return writes.slice(before);
94
+ }
95
+ test('nothing is written to the model once the abort lands', async () => {
96
+ expect(await abandonMidFlight(true)).toEqual([]);
97
+ });
98
+ test('a launch that resolved anyway still applies nothing', async () => {
99
+ expect(await abandonMidFlight(false)).toEqual([]);
100
+ });
101
+ test('an abort renders no error, so a cancel does not read as a failure', async () => {
102
+ const { model, writes } = makeModel();
103
+ runLaunch({
104
+ self: model,
105
+ message: 'Submitting query',
106
+ onLaunched: () => { },
107
+ launch: () => Promise.reject(new DOMException('Aborted', 'AbortError')),
108
+ });
109
+ await settle();
110
+ expect(writes.some(w => w.startsWith('setError:Aborted'))).toBe(false);
111
+ });
112
+ });
113
+ describe('a launch that fails', () => {
114
+ test('records the error and stops the spinner', async () => {
115
+ vi.spyOn(console, 'error').mockImplementation(() => { });
116
+ const { model, writes } = makeModel();
117
+ runLaunch({
118
+ self: model,
119
+ message: 'Resolving orthologs',
120
+ onLaunched: () => {
121
+ throw new Error('a failed launch must not report success');
122
+ },
123
+ launch: () => Promise.reject(new Error('Only 1 ortholog(s) found')),
124
+ });
125
+ await settle();
126
+ expect(writes).toContain('setError:Only 1 ortholog(s) found');
127
+ expect(writes.at(-2)).toBe('setProgress:');
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { loadStoredData, storeDataToIndexedDB } from './afterCreateAutoruns';
3
+ import { generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
4
+ // IndexedDB itself is msaDataStore.test.ts's subject; what is under test here is
5
+ // which writes these two autoruns decide to make.
6
+ vi.mock('./msaDataStore', () => ({
7
+ cleanupOldData: vi.fn(),
8
+ generateDataStoreId: vi.fn(),
9
+ retrieveMsaData: vi.fn(),
10
+ storeMsaData: vi.fn(),
11
+ }));
12
+ const mockRetrieve = vi.mocked(retrieveMsaData);
13
+ const mockStore = vi.mocked(storeMsaData);
14
+ const mockGenerateId = vi.mocked(generateDataStoreId);
15
+ const MSA = '>a\nMK';
16
+ const NEW_ID = 'msa-generated';
17
+ function makeModel(over = {}) {
18
+ const model = {
19
+ dataStoreId: undefined,
20
+ rows: [],
21
+ data: {},
22
+ msaFilehandle: undefined,
23
+ treeFilehandle: undefined,
24
+ isStoringData: false,
25
+ lastStoredData: undefined,
26
+ loadingStoredData: false,
27
+ error: undefined,
28
+ setDataStoreId(arg) {
29
+ model.dataStoreId = arg;
30
+ },
31
+ setIsStoringData(arg) {
32
+ model.isStoringData = arg;
33
+ },
34
+ setLastStoredData(arg) {
35
+ model.lastStoredData = arg;
36
+ },
37
+ setLoadingStoredData(arg) {
38
+ model.loadingStoredData = arg;
39
+ },
40
+ setError(arg) {
41
+ model.error = arg;
42
+ },
43
+ setMSA(arg) {
44
+ model.data.msa = arg;
45
+ },
46
+ setTree(arg) {
47
+ model.data.tree = arg;
48
+ },
49
+ setTreeMetadata(arg) {
50
+ model.data.treeMetadata = arg;
51
+ },
52
+ ...over,
53
+ };
54
+ return model;
55
+ }
56
+ const settle = () => new Promise(res => setTimeout(res, 0));
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ mockGenerateId.mockReturnValue(NEW_ID);
60
+ mockStore.mockResolvedValue(true);
61
+ });
62
+ describe('restoring a view from IndexedDB', () => {
63
+ test('a row that is still there is applied and recorded as stored', async () => {
64
+ mockRetrieve.mockResolvedValue({
65
+ msa: MSA,
66
+ tree: '(a);',
67
+ treeMetadata: '{}',
68
+ });
69
+ const model = makeModel({ dataStoreId: 'msa-1' });
70
+ loadStoredData(model);
71
+ await settle();
72
+ expect(model.data.msa).toBe(MSA);
73
+ expect(model.error).toBeUndefined();
74
+ // recorded, so the store autorun does not write the restored data straight
75
+ // back out again
76
+ expect(model.lastStoredData).toEqual({
77
+ msa: MSA,
78
+ tree: '(a);',
79
+ treeMetadata: '{}',
80
+ });
81
+ model.rows = [['a', 'MK']];
82
+ storeDataToIndexedDB(model);
83
+ expect(mockStore).not.toHaveBeenCalled();
84
+ });
85
+ // the row expires or the user clears site data, and the view used to reopen as
86
+ // a blank import form with no hint that anything had been lost
87
+ test('a row that is gone says so, in words that name the policy', async () => {
88
+ mockRetrieve.mockResolvedValue(undefined);
89
+ const model = makeModel({ dataStoreId: 'msa-1' });
90
+ loadStoredData(model);
91
+ await settle();
92
+ expect(model.error).toBeInstanceOf(Error);
93
+ expect(model.error.message).toMatch(/7 days/);
94
+ // the id names nothing now, so it goes -- otherwise react-msaview's "Return
95
+ // to import form" lands straight back on this error
96
+ expect(model.dataStoreId).toBeUndefined();
97
+ });
98
+ });
99
+ describe('keeping IndexedDB up to date', () => {
100
+ test('the first alignment is written under a fresh id', async () => {
101
+ const model = makeModel({ rows: [['a', 'MK']], data: { msa: MSA } });
102
+ storeDataToIndexedDB(model);
103
+ await settle();
104
+ expect(mockStore).toHaveBeenCalledWith(NEW_ID, { msa: MSA });
105
+ expect(model.dataStoreId).toBe(NEW_ID);
106
+ expect(model.isStoringData).toBe(false);
107
+ });
108
+ // react-msaview's "calculate neighbor-joining tree" replaces data.tree long
109
+ // after the first write; the row used to keep the alignment's original tree
110
+ // forever, so a reopened session restored a picture the user had replaced
111
+ test('an edit after the first write updates the existing row', async () => {
112
+ const model = makeModel({
113
+ dataStoreId: 'msa-1',
114
+ rows: [['a', 'MK']],
115
+ data: { msa: MSA, tree: '(a);' },
116
+ lastStoredData: { msa: MSA, tree: '(a);' },
117
+ });
118
+ storeDataToIndexedDB(model);
119
+ expect(mockStore).not.toHaveBeenCalled();
120
+ model.data.tree = '(a:0.1);';
121
+ storeDataToIndexedDB(model);
122
+ await settle();
123
+ expect(mockStore).toHaveBeenCalledWith('msa-1', {
124
+ msa: MSA,
125
+ tree: '(a:0.1);',
126
+ treeMetadata: undefined,
127
+ });
128
+ expect(model.dataStoreId).toBe('msa-1');
129
+ });
130
+ // a browser that refuses IndexedDB (private mode) answers every write the same
131
+ // way, and the autorun reruns on its own isStoringData flag -- so a write that
132
+ // is not recorded as attempted is a write that retries forever
133
+ test('a write that fails is not retried on the same data', async () => {
134
+ vi.spyOn(console, 'error').mockImplementation(() => { });
135
+ mockStore.mockResolvedValue(false);
136
+ const model = makeModel({ rows: [['a', 'MK']], data: { msa: MSA } });
137
+ storeDataToIndexedDB(model);
138
+ await settle();
139
+ expect(model.dataStoreId).toBeUndefined();
140
+ storeDataToIndexedDB(model);
141
+ await settle();
142
+ expect(mockStore).toHaveBeenCalledTimes(1);
143
+ });
144
+ test('a view reading from a filehandle stores nothing, since the file is the source', () => {
145
+ storeDataToIndexedDB(makeModel({
146
+ rows: [['a', 'MK']],
147
+ data: { msa: MSA },
148
+ msaFilehandle: { uri: 'msa.fa', locationType: 'UriLocation' },
149
+ }));
150
+ expect(mockStore).not.toHaveBeenCalled();
151
+ });
152
+ test('a write already in flight is not duplicated', () => {
153
+ storeDataToIndexedDB(makeModel({
154
+ rows: [['a', 'MK']],
155
+ data: { msa: MSA },
156
+ isStoringData: true,
157
+ }));
158
+ expect(mockStore).not.toHaveBeenCalled();
159
+ });
160
+ });
@@ -21,6 +21,7 @@ function makeModel() {
21
21
  const calls = [];
22
22
  const model = {
23
23
  querySeqName: 'hg38.chr1',
24
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
24
25
  transcriptToMsaMap: undefined,
25
26
  mafRegion,
26
27
  connectedView: { initialized: true, assemblyNames: ['hg38'] },
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Whether `querySeqName` names a row this alignment actually has.
3
+ *
4
+ * react-msaview's `seqPosToGlobalCol` answers 0 for a name it does not know, so
5
+ * without this every genome position maps to the first column and hovering the
6
+ * genome — or a connected structure — lights column 0 of an unrelated row. The
7
+ * name is wrong more often than it looks: it defaults to `QUERY`, which an
8
+ * uploaded alignment has no reason to carry, and the manual panel leaves it
9
+ * empty when it cannot match the protein to a row.
10
+ *
11
+ * The other direction has no such hole: msaCoordToGenomeRegions needs the query
12
+ * row's sequence to map a column at all, so a missing row is already nothing
13
+ * there.
14
+ */
15
+ export declare function hasQueryRow(model: {
16
+ rows: string[][];
17
+ querySeqName: string;
18
+ }): boolean;
1
19
  export declare function hasHoverPosition(hovered: unknown): hovered is {
2
20
  hoverPosition: {
3
21
  coord: number;
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Whether `querySeqName` names a row this alignment actually has.
3
+ *
4
+ * react-msaview's `seqPosToGlobalCol` answers 0 for a name it does not know, so
5
+ * without this every genome position maps to the first column and hovering the
6
+ * genome — or a connected structure — lights column 0 of an unrelated row. The
7
+ * name is wrong more often than it looks: it defaults to `QUERY`, which an
8
+ * uploaded alignment has no reason to carry, and the manual panel leaves it
9
+ * empty when it cannot match the protein to a row.
10
+ *
11
+ * The other direction has no such hole: msaCoordToGenomeRegions needs the query
12
+ * row's sequence to map a column at all, so a missing row is already nothing
13
+ * there.
14
+ */
15
+ export function hasQueryRow(model) {
16
+ return model.rows.some(r => r[0] === model.querySeqName);
17
+ }
1
18
  export function hasHoverPosition(hovered) {
2
19
  return (!!hovered &&
3
20
  typeof hovered === 'object' &&