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.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "2.6.2";
1
+ export declare const version = "2.6.3";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.6.2';
1
+ export const version = '2.6.3';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.6.2",
2
+ "version": "2.6.3",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -46,18 +46,25 @@ export default function LaunchMsaViewExtensionPointF(
46
46
  )
47
47
  }
48
48
 
49
- // all data sources flow through `init` so processInit is the single place
50
- // that resolves them (AlphaFold detection, native filehandle loading, etc.)
49
+ // inline data and the tree URL are native react-msaview snapshot props, set
50
+ // directly. Only sources needing launch-time resolution go through `init`:
51
+ // msaUrl (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
51
52
  session.addView('MsaView', {
52
53
  type: 'MsaView',
53
54
  ...rest,
55
+ data,
56
+ ...(treeFileLocation
57
+ ? {
58
+ treeFilehandle: {
59
+ ...treeFileLocation,
60
+ locationType: 'UriLocation',
61
+ },
62
+ }
63
+ : {}),
54
64
  init: {
55
- msaData: data?.msa,
56
- treeData: data?.tree,
57
65
  msaUrl: msaFileLocation?.uri,
58
66
  msaIndexedLocation,
59
67
  msaName,
60
- treeUrl: treeFileLocation?.uri,
61
68
  querySeqName,
62
69
  },
63
70
  })
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  gappedToUngappedPosition,
15
15
  getProteinViews,
16
+ structureMatchesMsa,
16
17
  } from './structureConnection'
17
18
  import { getUniprotIdFromAlphaFoldUrl } from './util'
18
19
 
@@ -128,22 +129,15 @@ export function autoLoadProteinDomains(self: JBrowsePluginMsaViewModel) {
128
129
  }
129
130
  }
130
131
 
131
- // Resolve the declarative `init` launch contract once. Inline strings go straight
132
- // to the data model; URLs are handed to react-msaview's native filehandle loaders
133
- // (openLocation + progress + abort + CORS-proxy) rather than a hand-rolled fetch;
134
- // the bgzip name-indexed block is the one source with no native loader.
132
+ // Resolve the declarative `init` launch contract once, then clear it. msaUrl is
133
+ // handed to react-msaview's native filehandle loader (openLocation + progress +
134
+ // abort + CORS-proxy) and sniffed for an AlphaFold uniprotId; the bgzip
135
+ // name-indexed block is the one source with no native loader, so it's fetched
136
+ // here. Inline data and tree URLs arrive as native snapshot props, not via init.
135
137
  export function processInit(self: JBrowsePluginMsaViewModel) {
136
138
  const { init } = self
137
139
  if (init) {
138
- const {
139
- msaData,
140
- msaUrl,
141
- msaIndexedLocation,
142
- msaName,
143
- treeData,
144
- treeUrl,
145
- querySeqName,
146
- } = init
140
+ const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init
147
141
  void (async () => {
148
142
  try {
149
143
  self.setError(undefined)
@@ -159,9 +153,7 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
159
153
  self.setQuerySeqName(querySeqName)
160
154
  }
161
155
 
162
- if (msaData) {
163
- self.setMSA(msaData)
164
- } else if (msaUrl) {
156
+ if (msaUrl) {
165
157
  self.setMSAFilehandle({ uri: msaUrl, locationType: 'UriLocation' })
166
158
  } else if (msaIndexedLocation && msaName) {
167
159
  const fasta = await fetchIndexedMsa({
@@ -177,12 +169,6 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
177
169
  }
178
170
  }
179
171
 
180
- if (treeData) {
181
- self.setTree(treeData)
182
- } else if (treeUrl) {
183
- self.setTreeFilehandle({ uri: treeUrl, locationType: 'UriLocation' })
184
- }
185
-
186
172
  self.setInit(undefined)
187
173
  } catch (e) {
188
174
  self.setError(e)
@@ -253,7 +239,7 @@ export function highlightConnectedStructures(self: JBrowsePluginMsaViewModel) {
253
239
  export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
254
240
  const { connectedViewId, uniprotId, rows, connectedStructures } = self
255
241
 
256
- if (!uniprotId || rows.length === 0) {
242
+ if (rows.length === 0) {
257
243
  return
258
244
  }
259
245
 
@@ -268,11 +254,7 @@ export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
268
254
  continue
269
255
  }
270
256
 
271
- if (structure.connectedViewId !== connectedViewId) {
272
- continue
273
- }
274
-
275
- if (structure.uniprotId !== uniprotId) {
257
+ if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
276
258
  continue
277
259
  }
278
260
 
@@ -0,0 +1,76 @@
1
+ import { getSession } from '@jbrowse/core/util'
2
+ import { beforeEach, describe, expect, test, vi } from 'vitest'
3
+
4
+ import { autoConnectStructures } from './afterCreateAutoruns'
5
+
6
+ import type { JBrowsePluginMsaViewModel } from './model'
7
+ import type { ProteinView } from './structureConnection'
8
+
9
+ // Integration coverage for the autorun itself — the structure-matching matrix
10
+ // lives in structureConnection.test.ts (structureMatchesMsa). Here we check the
11
+ // autorun wires a match through to connectToStructure and respects its guards.
12
+
13
+ // Mock only getSession; keep the rest of the util module real so the
14
+ // afterCreateAutoruns import graph still loads.
15
+ vi.mock('@jbrowse/core/util', async importOriginal => ({
16
+ ...(await importOriginal<Record<string, unknown>>()),
17
+ getSession: vi.fn(),
18
+ }))
19
+
20
+ const mockGetSession = vi.mocked(getSession)
21
+
22
+ function makeModel(opts: { connectedViewId?: string; uniprotId?: string }) {
23
+ const connected: { proteinViewId: string; structureIdx: number }[] = []
24
+ const model = {
25
+ connectedViewId: opts.connectedViewId,
26
+ uniprotId: opts.uniprotId,
27
+ rows: [['hg38', 'MKATEST']],
28
+ connectedStructures: connected,
29
+ connectToStructure: (proteinViewId: string, structureIdx: number) => {
30
+ connected.push({ proteinViewId, structureIdx })
31
+ },
32
+ } as unknown as JBrowsePluginMsaViewModel
33
+ return { model, connected }
34
+ }
35
+
36
+ function withStructure(structure: {
37
+ connectedViewId?: string
38
+ uniprotId?: string
39
+ }) {
40
+ const view: ProteinView = {
41
+ type: 'ProteinView',
42
+ id: 'pv1',
43
+ structures: [{ ...structure, structureSequences: ['MKATEST'] }],
44
+ }
45
+ mockGetSession.mockReturnValue({
46
+ views: [view],
47
+ } as unknown as ReturnType<typeof getSession>)
48
+ }
49
+
50
+ describe('autoConnectStructures', () => {
51
+ beforeEach(() => {
52
+ vi.clearAllMocks()
53
+ })
54
+
55
+ test('connects a matching structure (genome-view link, no UniProt id)', () => {
56
+ const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' })
57
+ withStructure({ connectedViewId: 'lgv-TP53' })
58
+ autoConnectStructures(model)
59
+ expect(connected).toEqual([{ proteinViewId: 'pv1', structureIdx: 0 }])
60
+ })
61
+
62
+ test('does not connect a non-matching structure', () => {
63
+ const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' })
64
+ withStructure({ connectedViewId: 'lgv-OTHER' })
65
+ autoConnectStructures(model)
66
+ expect(connected).toEqual([])
67
+ })
68
+
69
+ test('does not connect before the alignment has loaded (no rows)', () => {
70
+ const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' })
71
+ ;(model as unknown as { rows: unknown[] }).rows = []
72
+ withStructure({ connectedViewId: 'lgv-TP53' })
73
+ autoConnectStructures(model)
74
+ expect(connected).toEqual([])
75
+ })
76
+ })
@@ -1,6 +1,58 @@
1
1
  import { describe, expect, test } from 'vitest'
2
2
 
3
- import { gappedToUngappedPosition } from './structureConnection'
3
+ import {
4
+ gappedToUngappedPosition,
5
+ structureMatchesMsa,
6
+ } from './structureConnection'
7
+
8
+ describe('structureMatchesMsa', () => {
9
+ test('matches on a shared genome view alone (no UniProt id)', () => {
10
+ expect(
11
+ structureMatchesMsa({
12
+ structure: { connectedViewId: 'lgv-TP53' },
13
+ connectedViewId: 'lgv-TP53',
14
+ }),
15
+ ).toBe(true)
16
+ })
17
+
18
+ test('matches on a shared UniProt id alone (no genome view)', () => {
19
+ expect(
20
+ structureMatchesMsa({
21
+ structure: { uniprotId: 'P04637' },
22
+ uniprotId: 'P04637',
23
+ }),
24
+ ).toBe(true)
25
+ })
26
+
27
+ test('shared genome view wins even when UniProt ids differ', () => {
28
+ expect(
29
+ structureMatchesMsa({
30
+ structure: { connectedViewId: 'lgv-TP53', uniprotId: 'OTHER' },
31
+ connectedViewId: 'lgv-TP53',
32
+ uniprotId: 'P04637',
33
+ }),
34
+ ).toBe(true)
35
+ })
36
+
37
+ test('no match when neither key matches', () => {
38
+ expect(
39
+ structureMatchesMsa({
40
+ structure: { connectedViewId: 'lgv-OTHER', uniprotId: 'OTHER' },
41
+ connectedViewId: 'lgv-TP53',
42
+ uniprotId: 'P04637',
43
+ }),
44
+ ).toBe(false)
45
+ })
46
+
47
+ test('two undefined connectedViewIds do not count as a shared genome view', () => {
48
+ // both sides lacking a genome view must NOT auto-pair on `undefined ===
49
+ // undefined`; only an explicit shared id (or UniProt id) connects
50
+ expect(structureMatchesMsa({ structure: {} })).toBe(false)
51
+ expect(
52
+ structureMatchesMsa({ structure: { uniprotId: 'P04637' } }),
53
+ ).toBe(false)
54
+ })
55
+ })
4
56
 
5
57
  describe('gappedToUngappedPosition', () => {
6
58
  test('returns correct ungapped position for non-gap character', () => {
@@ -28,6 +28,36 @@ export function getProteinViews(views: { type: string }[]): ProteinView[] {
28
28
  return (views as unknown[]).filter(isProteinView)
29
29
  }
30
30
 
31
+ /**
32
+ * Whether a 3D structure belongs to a given alignment — the single source of
33
+ * truth for pairing an MsaView with a ProteinView's structure. A structure
34
+ * matches when it either:
35
+ * - shares the alignment's genome view (both pinned to the same
36
+ * LinearGenomeView via `connectedViewId` — the genome-centric gene-explorer
37
+ * flow, the same key genome↔MSA and genome↔structure already bridge through),
38
+ * or
39
+ * - shares the alignment's UniProt accession (the BLAST/AlphaFold flow, which
40
+ * has no genome view to bridge through).
41
+ *
42
+ * The residue map itself is built by sequence (connectToStructure pairwise-
43
+ * aligns the query row against the structure), so neither key is mechanically
44
+ * required — they only scope WHICH structure pairs with the alignment.
45
+ */
46
+ export function structureMatchesMsa({
47
+ structure,
48
+ connectedViewId,
49
+ uniprotId,
50
+ }: {
51
+ structure: Pick<ProteinViewStructure, 'connectedViewId' | 'uniprotId'>
52
+ connectedViewId?: string
53
+ uniprotId?: string
54
+ }) {
55
+ const sharesGenomeView =
56
+ !!connectedViewId && structure.connectedViewId === connectedViewId
57
+ const sharesUniprot = !!uniprotId && structure.uniprotId === uniprotId
58
+ return sharesGenomeView || sharesUniprot
59
+ }
60
+
31
61
  /**
32
62
  * Represents a connection between the MSA view and a protein structure
33
63
  */
@@ -1,19 +1,24 @@
1
- // Declarative launch contract, resolved once by processInit. This is also a
2
- // cross-repo contract: jbrowse-plugin-protein3d builds an MsaView snapshot
3
- // directly with `init: { msaUrl }`, so these keys must stay stable.
1
+ // Declarative launch contract, resolved once by processInit, then cleared. Only
2
+ // sources that need launch-time resolution belong here. Inline data and tree URLs
3
+ // do NOT: they are native react-msaview snapshot props (`data`, `treeFilehandle`)
4
+ // applied directly from the addView snapshot, no resolution required.
5
+ //
6
+ // Cross-repo contract: jbrowse-plugin-protein3d builds an MsaView snapshot directly
7
+ // with `init: { msaUrl }`, so these keys must stay stable.
4
8
  export interface MsaViewInitState {
5
- msaData?: string
9
+ // resolved here (not as a native msaFilehandle) so the AlphaFold-URL → uniprotId
10
+ // sniff runs once at launch; querySeqName is coupled to it (AlphaFold files name
11
+ // the query row 'query'), which is why it rides along in init rather than being a
12
+ // plain top-level prop.
6
13
  msaUrl?: string
14
+ querySeqName?: string
7
15
  // a single bgzip `.fa.gz` of per-transcript FASTA blocks; its `.gzi` and name
8
16
  // index `.idx` (name<TAB>offset<TAB>length) are found by suffix. `msaName`
9
17
  // selects one transcript's block by name (a random read), so one genome-scale
10
- // alignment serves any gene without per-gene files or coordinates. See
11
- // react-msaview's gene-explorer.
18
+ // alignment serves any gene without per-gene files or coordinates. This is the
19
+ // one alignment source with no native loader. See react-msaview's gene-explorer.
12
20
  msaIndexedLocation?: { uri: string }
13
21
  msaName?: string
14
- treeData?: string
15
- treeUrl?: string
16
- querySeqName?: string
17
22
  }
18
23
 
19
24
  export interface MafRegion {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '2.6.2'
1
+ export const version = '2.6.3'