jbrowse-plugin-msaview 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,31 @@
1
1
  export default function LaunchMsaViewExtensionPointF(pluginManager) {
2
- pluginManager.addToExtensionPoint('LaunchView-MsaView',
3
- // @ts-expect-error
4
- ({ session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, connectedViewId, connectedFeature, displayName, colorSchemeName, colWidth, rowHeight, treeAreaWidth, treeWidth, drawNodeBubbles, labelsAlignRight, showBranchLen, querySeqName, highlightColumns, }) => {
2
+ pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
3
+ const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
5
4
  if (!data && !msaFileLocation && !msaIndexedLocation) {
6
5
  throw new Error('No MSA data or file location provided when launching MSA view');
7
6
  }
7
+ // inline data and the tree URL are native react-msaview snapshot props, set
8
+ // directly. Only sources needing launch-time resolution go through `init`:
9
+ // msaUrl (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
8
10
  session.addView('MsaView', {
9
11
  type: 'MsaView',
10
- displayName,
11
- connectedViewId,
12
- connectedFeature,
13
- colorSchemeName,
14
- colWidth,
15
- rowHeight,
16
- treeAreaWidth,
17
- treeWidth,
18
- drawNodeBubbles,
19
- labelsAlignRight,
20
- showBranchLen,
21
- highlightColumns,
12
+ ...rest,
13
+ data,
14
+ ...(treeFileLocation
15
+ ? {
16
+ treeFilehandle: {
17
+ ...treeFileLocation,
18
+ locationType: 'UriLocation',
19
+ },
20
+ }
21
+ : {}),
22
22
  init: {
23
- msaData: data?.msa,
24
- treeData: data?.tree,
25
23
  msaUrl: msaFileLocation?.uri,
26
24
  msaIndexedLocation,
27
25
  msaName,
28
- treeUrl: treeFileLocation?.uri,
29
26
  querySeqName,
30
27
  },
31
28
  });
29
+ return args;
32
30
  });
33
31
  }
@@ -4,7 +4,7 @@ import { fetchIndexedMsa } from './fetchIndexedMsa';
4
4
  import { genomeToMSA } from './genomeToMSA';
5
5
  import { loadProteinDomains } from './loadProteinDomains';
6
6
  import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
7
- import { gappedToUngappedPosition, getProteinViews, } from './structureConnection';
7
+ import { gappedToUngappedPosition, getProteinViews, structureMatchesMsa, } from './structureConnection';
8
8
  import { getUniprotIdFromAlphaFoldUrl } from './util';
9
9
  export function loadStoredData(self) {
10
10
  const { dataStoreId, rows } = self;
@@ -116,13 +116,18 @@ export function autoLoadProteinDomains(self) {
116
116
  })();
117
117
  }
118
118
  }
119
+ // Resolve the declarative `init` launch contract once, then clear it. msaUrl is
120
+ // handed to react-msaview's native filehandle loader (openLocation + progress +
121
+ // abort + CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip
122
+ // name-indexed block is the one source with no native loader, so it's fetched
123
+ // here. Inline data and tree URLs arrive as native snapshot props, not via init.
119
124
  export function processInit(self) {
120
125
  const { init } = self;
121
126
  if (init) {
127
+ const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
122
128
  void (async () => {
123
129
  try {
124
130
  self.setError(undefined);
125
- const { msaData, msaUrl, msaIndexedLocation, msaName, treeData, treeUrl, querySeqName, } = init;
126
131
  if (msaUrl) {
127
132
  const id = getUniprotIdFromAlphaFoldUrl(msaUrl);
128
133
  if (id) {
@@ -133,16 +138,8 @@ export function processInit(self) {
133
138
  if (querySeqName) {
134
139
  self.setQuerySeqName(querySeqName);
135
140
  }
136
- if (msaData) {
137
- self.setMSA(msaData);
138
- }
139
- else if (msaUrl) {
140
- const response = await fetch(msaUrl);
141
- if (!response.ok) {
142
- throw new Error(`Failed to fetch MSA: ${response.status}`);
143
- }
144
- const data = await response.text();
145
- self.setMSA(data);
141
+ if (msaUrl) {
142
+ self.setMSAFilehandle({ uri: msaUrl, locationType: 'UriLocation' });
146
143
  }
147
144
  else if (msaIndexedLocation && msaName) {
148
145
  const fasta = await fetchIndexedMsa({
@@ -156,17 +153,6 @@ export function processInit(self) {
156
153
  throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
157
154
  }
158
155
  }
159
- if (treeData) {
160
- self.setTree(treeData);
161
- }
162
- else if (treeUrl) {
163
- const response = await fetch(treeUrl);
164
- if (!response.ok) {
165
- throw new Error(`Failed to fetch tree: ${response.status}`);
166
- }
167
- const data = await response.text();
168
- self.setTree(data);
169
- }
170
156
  self.setInit(undefined);
171
157
  }
172
158
  catch (e) {
@@ -231,7 +217,7 @@ export function highlightConnectedStructures(self) {
231
217
  }
232
218
  export function autoConnectStructures(self) {
233
219
  const { connectedViewId, uniprotId, rows, connectedStructures } = self;
234
- if (!uniprotId || rows.length === 0) {
220
+ if (rows.length === 0) {
235
221
  return;
236
222
  }
237
223
  for (const view of getProteinViews(getSession(self).views)) {
@@ -240,10 +226,7 @@ export function autoConnectStructures(self) {
240
226
  if (!structure) {
241
227
  continue;
242
228
  }
243
- if (structure.connectedViewId !== connectedViewId) {
244
- continue;
245
- }
246
- if (structure.uniprotId !== uniprotId) {
229
+ if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
247
230
  continue;
248
231
  }
249
232
  const alreadyConnected = connectedStructures.some(c => c.proteinViewId === view.id && c.structureIdx === structureIdx);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { getSession } from '@jbrowse/core/util';
2
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
3
+ import { autoConnectStructures } from './afterCreateAutoruns';
4
+ // Integration coverage for the autorun itself — the structure-matching matrix
5
+ // lives in structureConnection.test.ts (structureMatchesMsa). Here we check the
6
+ // autorun wires a match through to connectToStructure and respects its guards.
7
+ // Mock only getSession; keep the rest of the util module real so the
8
+ // afterCreateAutoruns import graph still loads.
9
+ vi.mock('@jbrowse/core/util', async (importOriginal) => ({
10
+ ...(await importOriginal()),
11
+ getSession: vi.fn(),
12
+ }));
13
+ const mockGetSession = vi.mocked(getSession);
14
+ function makeModel(opts) {
15
+ const connected = [];
16
+ const model = {
17
+ connectedViewId: opts.connectedViewId,
18
+ uniprotId: opts.uniprotId,
19
+ rows: [['hg38', 'MKATEST']],
20
+ connectedStructures: connected,
21
+ connectToStructure: (proteinViewId, structureIdx) => {
22
+ connected.push({ proteinViewId, structureIdx });
23
+ },
24
+ };
25
+ return { model, connected };
26
+ }
27
+ function withStructure(structure) {
28
+ const view = {
29
+ type: 'ProteinView',
30
+ id: 'pv1',
31
+ structures: [{ ...structure, structureSequences: ['MKATEST'] }],
32
+ };
33
+ mockGetSession.mockReturnValue({
34
+ views: [view],
35
+ });
36
+ }
37
+ describe('autoConnectStructures', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+ test('connects a matching structure (genome-view link, no UniProt id)', () => {
42
+ const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' });
43
+ withStructure({ connectedViewId: 'lgv-TP53' });
44
+ autoConnectStructures(model);
45
+ expect(connected).toEqual([{ proteinViewId: 'pv1', structureIdx: 0 }]);
46
+ });
47
+ test('does not connect a non-matching structure', () => {
48
+ const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' });
49
+ withStructure({ connectedViewId: 'lgv-OTHER' });
50
+ autoConnectStructures(model);
51
+ expect(connected).toEqual([]);
52
+ });
53
+ test('does not connect before the alignment has loaded (no rows)', () => {
54
+ const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' });
55
+ model.rows = [];
56
+ withStructure({ connectedViewId: 'lgv-TP53' });
57
+ autoConnectStructures(model);
58
+ expect(connected).toEqual([]);
59
+ });
60
+ });
@@ -26,6 +26,26 @@ export declare function isProteinView(view: unknown): view is ProteinView;
26
26
  export declare function getProteinViews(views: {
27
27
  type: string;
28
28
  }[]): ProteinView[];
29
+ /**
30
+ * Whether a 3D structure belongs to a given alignment — the single source of
31
+ * truth for pairing an MsaView with a ProteinView's structure. A structure
32
+ * matches when it either:
33
+ * - shares the alignment's genome view (both pinned to the same
34
+ * LinearGenomeView via `connectedViewId` — the genome-centric gene-explorer
35
+ * flow, the same key genome↔MSA and genome↔structure already bridge through),
36
+ * or
37
+ * - shares the alignment's UniProt accession (the BLAST/AlphaFold flow, which
38
+ * has no genome view to bridge through).
39
+ *
40
+ * The residue map itself is built by sequence (connectToStructure pairwise-
41
+ * aligns the query row against the structure), so neither key is mechanically
42
+ * required — they only scope WHICH structure pairs with the alignment.
43
+ */
44
+ export declare function structureMatchesMsa({ structure, connectedViewId, uniprotId, }: {
45
+ structure: Pick<ProteinViewStructure, 'connectedViewId' | 'uniprotId'>;
46
+ connectedViewId?: string;
47
+ uniprotId?: string;
48
+ }): boolean;
29
49
  /**
30
50
  * Represents a connection between the MSA view and a protein structure
31
51
  */
@@ -8,6 +8,26 @@ export function isProteinView(view) {
8
8
  export function getProteinViews(views) {
9
9
  return views.filter(isProteinView);
10
10
  }
11
+ /**
12
+ * Whether a 3D structure belongs to a given alignment — the single source of
13
+ * truth for pairing an MsaView with a ProteinView's structure. A structure
14
+ * matches when it either:
15
+ * - shares the alignment's genome view (both pinned to the same
16
+ * LinearGenomeView via `connectedViewId` — the genome-centric gene-explorer
17
+ * flow, the same key genome↔MSA and genome↔structure already bridge through),
18
+ * or
19
+ * - shares the alignment's UniProt accession (the BLAST/AlphaFold flow, which
20
+ * has no genome view to bridge through).
21
+ *
22
+ * The residue map itself is built by sequence (connectToStructure pairwise-
23
+ * aligns the query row against the structure), so neither key is mechanically
24
+ * required — they only scope WHICH structure pairs with the alignment.
25
+ */
26
+ export function structureMatchesMsa({ structure, connectedViewId, uniprotId, }) {
27
+ const sharesGenomeView = !!connectedViewId && structure.connectedViewId === connectedViewId;
28
+ const sharesUniprot = !!uniprotId && structure.uniprotId === uniprotId;
29
+ return sharesGenomeView || sharesUniprot;
30
+ }
11
31
  /**
12
32
  * Helper to convert gapped MSA column to ungapped position for a specific row
13
33
  */
@@ -1,5 +1,39 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { gappedToUngappedPosition } from './structureConnection';
2
+ import { gappedToUngappedPosition, structureMatchesMsa, } from './structureConnection';
3
+ describe('structureMatchesMsa', () => {
4
+ test('matches on a shared genome view alone (no UniProt id)', () => {
5
+ expect(structureMatchesMsa({
6
+ structure: { connectedViewId: 'lgv-TP53' },
7
+ connectedViewId: 'lgv-TP53',
8
+ })).toBe(true);
9
+ });
10
+ test('matches on a shared UniProt id alone (no genome view)', () => {
11
+ expect(structureMatchesMsa({
12
+ structure: { uniprotId: 'P04637' },
13
+ uniprotId: 'P04637',
14
+ })).toBe(true);
15
+ });
16
+ test('shared genome view wins even when UniProt ids differ', () => {
17
+ expect(structureMatchesMsa({
18
+ structure: { connectedViewId: 'lgv-TP53', uniprotId: 'OTHER' },
19
+ connectedViewId: 'lgv-TP53',
20
+ uniprotId: 'P04637',
21
+ })).toBe(true);
22
+ });
23
+ test('no match when neither key matches', () => {
24
+ expect(structureMatchesMsa({
25
+ structure: { connectedViewId: 'lgv-OTHER', uniprotId: 'OTHER' },
26
+ connectedViewId: 'lgv-TP53',
27
+ uniprotId: 'P04637',
28
+ })).toBe(false);
29
+ });
30
+ test('two undefined connectedViewIds do not count as a shared genome view', () => {
31
+ // both sides lacking a genome view must NOT auto-pair on `undefined ===
32
+ // undefined`; only an explicit shared id (or UniProt id) connects
33
+ expect(structureMatchesMsa({ structure: {} })).toBe(false);
34
+ expect(structureMatchesMsa({ structure: { uniprotId: 'P04637' } })).toBe(false);
35
+ });
36
+ });
3
37
  describe('gappedToUngappedPosition', () => {
4
38
  test('returns correct ungapped position for non-gap character', () => {
5
39
  const seq = 'M-KA-A';
@@ -1,13 +1,10 @@
1
1
  export interface MsaViewInitState {
2
- msaData?: string;
3
2
  msaUrl?: string;
3
+ querySeqName?: string;
4
4
  msaIndexedLocation?: {
5
5
  uri: string;
6
6
  };
7
7
  msaName?: string;
8
- treeData?: string;
9
- treeUrl?: string;
10
- querySeqName?: string;
11
8
  }
12
9
  export interface MafRegion {
13
10
  refName: string;