jbrowse-plugin-msaview 2.6.2 → 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.
@@ -4,18 +4,25 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
4
4
  if (!data && !msaFileLocation && !msaIndexedLocation) {
5
5
  throw new Error('No MSA data or file location provided when launching MSA view');
6
6
  }
7
- // all data sources flow through `init` so processInit is the single place
8
- // that resolves them (AlphaFold detection, native filehandle loading, etc.)
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).
9
10
  session.addView('MsaView', {
10
11
  type: 'MsaView',
11
12
  ...rest,
13
+ data,
14
+ ...(treeFileLocation
15
+ ? {
16
+ treeFilehandle: {
17
+ ...treeFileLocation,
18
+ locationType: 'UriLocation',
19
+ },
20
+ }
21
+ : {}),
12
22
  init: {
13
- msaData: data?.msa,
14
- treeData: data?.tree,
15
23
  msaUrl: msaFileLocation?.uri,
16
24
  msaIndexedLocation,
17
25
  msaName,
18
- treeUrl: treeFileLocation?.uri,
19
26
  querySeqName,
20
27
  },
21
28
  });
@@ -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,14 +116,15 @@ export function autoLoadProteinDomains(self) {
116
116
  })();
117
117
  }
118
118
  }
119
- // Resolve the declarative `init` launch contract once. Inline strings go straight
120
- // to the data model; URLs are handed to react-msaview's native filehandle loaders
121
- // (openLocation + progress + abort + CORS-proxy) rather than a hand-rolled fetch;
122
- // the bgzip name-indexed block is the one source with no native loader.
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.
123
124
  export function processInit(self) {
124
125
  const { init } = self;
125
126
  if (init) {
126
- const { msaData, msaUrl, msaIndexedLocation, msaName, treeData, treeUrl, querySeqName, } = init;
127
+ const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init;
127
128
  void (async () => {
128
129
  try {
129
130
  self.setError(undefined);
@@ -137,10 +138,7 @@ export function processInit(self) {
137
138
  if (querySeqName) {
138
139
  self.setQuerySeqName(querySeqName);
139
140
  }
140
- if (msaData) {
141
- self.setMSA(msaData);
142
- }
143
- else if (msaUrl) {
141
+ if (msaUrl) {
144
142
  self.setMSAFilehandle({ uri: msaUrl, locationType: 'UriLocation' });
145
143
  }
146
144
  else if (msaIndexedLocation && msaName) {
@@ -155,12 +153,6 @@ export function processInit(self) {
155
153
  throw new Error(`No alignment named ${msaName} in ${msaIndexedLocation.uri}`);
156
154
  }
157
155
  }
158
- if (treeData) {
159
- self.setTree(treeData);
160
- }
161
- else if (treeUrl) {
162
- self.setTreeFilehandle({ uri: treeUrl, locationType: 'UriLocation' });
163
- }
164
156
  self.setInit(undefined);
165
157
  }
166
158
  catch (e) {
@@ -225,7 +217,7 @@ export function highlightConnectedStructures(self) {
225
217
  }
226
218
  export function autoConnectStructures(self) {
227
219
  const { connectedViewId, uniprotId, rows, connectedStructures } = self;
228
- if (!uniprotId || rows.length === 0) {
220
+ if (rows.length === 0) {
229
221
  return;
230
222
  }
231
223
  for (const view of getProteinViews(getSession(self).views)) {
@@ -234,10 +226,7 @@ export function autoConnectStructures(self) {
234
226
  if (!structure) {
235
227
  continue;
236
228
  }
237
- if (structure.connectedViewId !== connectedViewId) {
238
- continue;
239
- }
240
- if (structure.uniprotId !== uniprotId) {
229
+ if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
241
230
  continue;
242
231
  }
243
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;