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.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "2.6.1";
1
+ export declare const version = "2.6.3";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.6.1';
1
+ export const version = '2.6.3';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.6.1",
2
+ "version": "2.6.3",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -1,83 +1,75 @@
1
1
  import type PluginManager from '@jbrowse/core/PluginManager'
2
2
  import type { AbstractSessionModel } from '@jbrowse/core/util'
3
3
 
4
+ interface LaunchMsaViewArgs {
5
+ session: AbstractSessionModel
6
+ data?: { msa: string; tree?: string }
7
+ msaFileLocation?: { uri: string }
8
+ msaIndexedLocation?: { uri: string }
9
+ msaName?: string
10
+ treeFileLocation?: { uri: string }
11
+ connectedViewId?: string
12
+ connectedFeature?: Record<string, unknown>
13
+ displayName?: string
14
+ colorSchemeName?: string
15
+ colWidth?: number
16
+ rowHeight?: number
17
+ treeAreaWidth?: number
18
+ treeWidth?: number
19
+ drawNodeBubbles?: boolean
20
+ labelsAlignRight?: boolean
21
+ showBranchLen?: boolean
22
+ querySeqName?: string
23
+ highlightColumns?: number[]
24
+ }
25
+
4
26
  export default function LaunchMsaViewExtensionPointF(
5
27
  pluginManager: PluginManager,
6
28
  ) {
7
29
  pluginManager.addToExtensionPoint(
8
30
  'LaunchView-MsaView',
9
- // @ts-expect-error
10
- ({
11
- session,
12
- data,
13
- msaFileLocation,
14
- msaIndexedLocation,
15
- msaName,
16
- treeFileLocation,
17
- connectedViewId,
18
- connectedFeature,
19
- displayName,
20
- colorSchemeName,
21
- colWidth,
22
- rowHeight,
23
- treeAreaWidth,
24
- treeWidth,
25
- drawNodeBubbles,
26
- labelsAlignRight,
27
- showBranchLen,
28
- querySeqName,
29
- highlightColumns,
30
- }: {
31
- session: AbstractSessionModel
32
- data?: { msa: string; tree?: string }
33
- msaFileLocation?: { uri: string }
34
- msaIndexedLocation?: { uri: string }
35
- msaName?: string
36
- treeFileLocation?: { uri: string }
37
- connectedViewId?: string
38
- connectedFeature?: Record<string, unknown>
39
- displayName?: string
40
- colorSchemeName?: string
41
- colWidth?: number
42
- rowHeight?: number
43
- treeAreaWidth?: number
44
- treeWidth?: number
45
- drawNodeBubbles?: boolean
46
- labelsAlignRight?: boolean
47
- showBranchLen?: boolean
48
- querySeqName?: string
49
- highlightColumns?: number[]
50
- }) => {
31
+ (args: LaunchMsaViewArgs) => {
32
+ const {
33
+ session,
34
+ data,
35
+ msaFileLocation,
36
+ msaIndexedLocation,
37
+ msaName,
38
+ treeFileLocation,
39
+ querySeqName,
40
+ ...rest
41
+ } = args
42
+
51
43
  if (!data && !msaFileLocation && !msaIndexedLocation) {
52
44
  throw new Error(
53
45
  'No MSA data or file location provided when launching MSA view',
54
46
  )
55
47
  }
56
48
 
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).
57
52
  session.addView('MsaView', {
58
53
  type: 'MsaView',
59
- displayName,
60
- connectedViewId,
61
- connectedFeature,
62
- colorSchemeName,
63
- colWidth,
64
- rowHeight,
65
- treeAreaWidth,
66
- treeWidth,
67
- drawNodeBubbles,
68
- labelsAlignRight,
69
- showBranchLen,
70
- highlightColumns,
54
+ ...rest,
55
+ data,
56
+ ...(treeFileLocation
57
+ ? {
58
+ treeFilehandle: {
59
+ ...treeFileLocation,
60
+ locationType: 'UriLocation',
61
+ },
62
+ }
63
+ : {}),
71
64
  init: {
72
- msaData: data?.msa,
73
- treeData: data?.tree,
74
65
  msaUrl: msaFileLocation?.uri,
75
66
  msaIndexedLocation,
76
67
  msaName,
77
- treeUrl: treeFileLocation?.uri,
78
68
  querySeqName,
79
69
  },
80
70
  })
71
+
72
+ return args
81
73
  },
82
74
  )
83
75
  }
@@ -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,21 +129,18 @@ export function autoLoadProteinDomains(self: JBrowsePluginMsaViewModel) {
128
129
  }
129
130
  }
130
131
 
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.
131
137
  export function processInit(self: JBrowsePluginMsaViewModel) {
132
138
  const { init } = self
133
139
  if (init) {
140
+ const { msaUrl, msaIndexedLocation, msaName, querySeqName } = init
134
141
  void (async () => {
135
142
  try {
136
143
  self.setError(undefined)
137
- const {
138
- msaData,
139
- msaUrl,
140
- msaIndexedLocation,
141
- msaName,
142
- treeData,
143
- treeUrl,
144
- querySeqName,
145
- } = init
146
144
 
147
145
  if (msaUrl) {
148
146
  const id = getUniprotIdFromAlphaFoldUrl(msaUrl)
@@ -151,20 +149,12 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
151
149
  self.setQuerySeqName('query')
152
150
  }
153
151
  }
154
-
155
152
  if (querySeqName) {
156
153
  self.setQuerySeqName(querySeqName)
157
154
  }
158
155
 
159
- if (msaData) {
160
- self.setMSA(msaData)
161
- } else if (msaUrl) {
162
- const response = await fetch(msaUrl)
163
- if (!response.ok) {
164
- throw new Error(`Failed to fetch MSA: ${response.status}`)
165
- }
166
- const data = await response.text()
167
- self.setMSA(data)
156
+ if (msaUrl) {
157
+ self.setMSAFilehandle({ uri: msaUrl, locationType: 'UriLocation' })
168
158
  } else if (msaIndexedLocation && msaName) {
169
159
  const fasta = await fetchIndexedMsa({
170
160
  location: msaIndexedLocation,
@@ -179,17 +169,6 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
179
169
  }
180
170
  }
181
171
 
182
- if (treeData) {
183
- self.setTree(treeData)
184
- } else if (treeUrl) {
185
- const response = await fetch(treeUrl)
186
- if (!response.ok) {
187
- throw new Error(`Failed to fetch tree: ${response.status}`)
188
- }
189
- const data = await response.text()
190
- self.setTree(data)
191
- }
192
-
193
172
  self.setInit(undefined)
194
173
  } catch (e) {
195
174
  self.setError(e)
@@ -260,7 +239,7 @@ export function highlightConnectedStructures(self: JBrowsePluginMsaViewModel) {
260
239
  export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
261
240
  const { connectedViewId, uniprotId, rows, connectedStructures } = self
262
241
 
263
- if (!uniprotId || rows.length === 0) {
242
+ if (rows.length === 0) {
264
243
  return
265
244
  }
266
245
 
@@ -275,11 +254,7 @@ export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
275
254
  continue
276
255
  }
277
256
 
278
- if (structure.connectedViewId !== connectedViewId) {
279
- continue
280
- }
281
-
282
- if (structure.uniprotId !== uniprotId) {
257
+ if (!structureMatchesMsa({ structure, connectedViewId, uniprotId })) {
283
258
  continue
284
259
  }
285
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,16 +1,24 @@
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.
1
8
  export interface MsaViewInitState {
2
- 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.
3
13
  msaUrl?: string
14
+ querySeqName?: string
4
15
  // a single bgzip `.fa.gz` of per-transcript FASTA blocks; its `.gzi` and name
5
16
  // index `.idx` (name<TAB>offset<TAB>length) are found by suffix. `msaName`
6
17
  // selects one transcript's block by name (a random read), so one genome-scale
7
- // alignment serves any gene without per-gene files or coordinates. See
8
- // 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.
9
20
  msaIndexedLocation?: { uri: string }
10
21
  msaName?: string
11
- treeData?: string
12
- treeUrl?: string
13
- querySeqName?: string
14
22
  }
15
23
 
16
24
  export interface MafRegion {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '2.6.1'
1
+ export const version = '2.6.3'