jbrowse-plugin-protein3d 0.4.11 → 0.4.13
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/LaunchProteinView/components/LaunchSettingsDialog.d.ts +5 -0
- package/dist/LaunchProteinView/components/LaunchSettingsDialog.js +23 -0
- package/dist/LaunchProteinView/components/ProteinViewActions.js +13 -2
- package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +12 -3
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +2 -1
- package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.js +2 -1
- package/dist/LaunchProteinView/hooks/useFoldseekSearch.js +47 -12
- package/dist/LaunchProteinView/hooks/useStructureFileSequence.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useStructureFileSequence.js +5 -2
- package/dist/LaunchProteinView/services/foldseekApi.d.ts +23 -5
- package/dist/LaunchProteinView/services/foldseekApi.js +21 -13
- package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +2 -1
- package/dist/LaunchProteinView/utils/launchViewUtils.js +7 -2
- package/dist/LaunchProteinView/utils/sideBySide.d.ts +11 -0
- package/dist/LaunchProteinView/utils/sideBySide.js +33 -0
- package/dist/LaunchProteinViewExtensionPoint/index.js +9 -2
- package/dist/ProteinView/applyColorTheme.d.ts +1 -1
- package/dist/ProteinView/loadStructureData.d.ts +18 -0
- package/dist/ProteinView/loadStructureData.js +22 -0
- package/dist/ProteinView/model.d.ts +2 -2
- package/dist/ProteinView/model.js +6 -36
- package/dist/ProteinView/structureLoader.d.ts +30 -0
- package/dist/ProteinView/structureLoader.js +58 -0
- package/dist/config.json +1 -1
- package/dist/fetchUtils.d.ts +1 -1
- package/dist/fetchUtils.js +18 -2
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/molstar-chunk.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
- package/src/LaunchProteinView/components/LaunchSettingsDialog.tsx +63 -0
- package/src/LaunchProteinView/components/ProteinViewActions.tsx +21 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +13 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +3 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.ts +12 -10
- package/src/LaunchProteinView/hooks/useFoldseekSearch.ts +49 -12
- package/src/LaunchProteinView/hooks/useStructureFileSequence.ts +5 -2
- package/src/LaunchProteinView/services/foldseekApi.ts +57 -23
- package/src/LaunchProteinView/utils/launchViewUtils.ts +10 -1
- package/src/LaunchProteinView/utils/sideBySide.ts +55 -0
- package/src/LaunchProteinViewExtensionPoint/index.ts +17 -1
- package/src/ProteinView/loadStructureData.ts +36 -0
- package/src/ProteinView/model.ts +6 -47
- package/src/ProteinView/structureLoader.test.ts +102 -0
- package/src/ProteinView/structureLoader.ts +74 -0
- package/src/fetchUtils.test.ts +27 -0
- package/src/fetchUtils.ts +22 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { addStructureFromData } from './addStructureFromData'
|
|
2
|
+
import { addStructureFromURL } from './addStructureFromURL'
|
|
3
|
+
import { extractPerResidueConfidence } from './extractPerResidueConfidence'
|
|
4
|
+
import { extractStructureSequences } from './extractStructureSequences'
|
|
5
|
+
|
|
6
|
+
import type { PluginContext } from 'molstar/lib/mol-plugin/context'
|
|
7
|
+
|
|
8
|
+
export interface StructureData {
|
|
9
|
+
sequences?: string[]
|
|
10
|
+
confidence?: number[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loads a structure (from inline data or a URL) into the given Molstar plugin
|
|
15
|
+
* and pulls out its per-chain sequences and per-residue confidence. Pure with
|
|
16
|
+
* respect to the model — it only touches the plugin and returns plain data, so
|
|
17
|
+
* callers own the decision of whether/where to store the result.
|
|
18
|
+
*/
|
|
19
|
+
export async function loadStructureData({
|
|
20
|
+
structure,
|
|
21
|
+
plugin,
|
|
22
|
+
}: {
|
|
23
|
+
structure: { data?: string; url?: string }
|
|
24
|
+
plugin: PluginContext
|
|
25
|
+
}): Promise<StructureData> {
|
|
26
|
+
const { model } = structure.data
|
|
27
|
+
? await addStructureFromData({ data: structure.data, plugin })
|
|
28
|
+
: structure.url
|
|
29
|
+
? await addStructureFromURL({ url: structure.url, plugin })
|
|
30
|
+
: { model: undefined }
|
|
31
|
+
const sequences = model ? extractStructureSequences(model) : undefined
|
|
32
|
+
const confidence = model
|
|
33
|
+
? extractPerResidueConfidence(model, sequences?.[0]?.length)
|
|
34
|
+
: undefined
|
|
35
|
+
return { sequences, confidence }
|
|
36
|
+
}
|
package/src/ProteinView/model.ts
CHANGED
|
@@ -5,16 +5,14 @@ import SettingsIcon from '@mui/icons-material/Settings'
|
|
|
5
5
|
import Visibility from '@mui/icons-material/Visibility'
|
|
6
6
|
import { autorun } from 'mobx'
|
|
7
7
|
|
|
8
|
-
import { addStructureFromData } from './addStructureFromData'
|
|
9
|
-
import { addStructureFromURL } from './addStructureFromURL'
|
|
10
8
|
import {
|
|
11
9
|
COLOR_SCHEMES,
|
|
12
10
|
COLOR_SCHEME_VALUES,
|
|
13
11
|
type ProteinColorScheme,
|
|
14
12
|
applyColorTheme,
|
|
15
13
|
} from './applyColorTheme'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
14
|
+
import { loadStructureData } from './loadStructureData'
|
|
15
|
+
import { makeStructureLoader } from './structureLoader'
|
|
18
16
|
import Structure from './structureModel'
|
|
19
17
|
import { superposeStructures } from './superposeStructures'
|
|
20
18
|
import { type AlignmentAlgorithm, DEFAULT_ALIGNMENT_ALGORITHM } from './types'
|
|
@@ -32,25 +30,6 @@ const PERSISTED_SETTINGS = [
|
|
|
32
30
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
33
31
|
import type { PluginContext } from 'molstar/lib/mol-plugin/context'
|
|
34
32
|
|
|
35
|
-
async function loadStructureData({
|
|
36
|
-
structure,
|
|
37
|
-
plugin,
|
|
38
|
-
}: {
|
|
39
|
-
structure: { data?: string; url?: string }
|
|
40
|
-
plugin: PluginContext
|
|
41
|
-
}) {
|
|
42
|
-
const { model } = structure.data
|
|
43
|
-
? await addStructureFromData({ data: structure.data, plugin })
|
|
44
|
-
: structure.url
|
|
45
|
-
? await addStructureFromURL({ url: structure.url, plugin })
|
|
46
|
-
: { model: undefined }
|
|
47
|
-
const sequences = model ? extractStructureSequences(model) : undefined
|
|
48
|
-
const confidence = model
|
|
49
|
-
? extractPerResidueConfidence(model, sequences?.[0]?.length)
|
|
50
|
-
: undefined
|
|
51
|
-
return { sequences, confidence }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
33
|
export interface ProteinViewInitState {
|
|
55
34
|
structures?: {
|
|
56
35
|
url?: string
|
|
@@ -420,30 +399,10 @@ function stateModelFactory() {
|
|
|
420
399
|
}),
|
|
421
400
|
)
|
|
422
401
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (molstarPluginContext) {
|
|
428
|
-
for (const structure of structures) {
|
|
429
|
-
if (!structure.loadedToMolstar) {
|
|
430
|
-
try {
|
|
431
|
-
structure.setStructureData(
|
|
432
|
-
await loadStructureData({
|
|
433
|
-
structure,
|
|
434
|
-
plugin: molstarPluginContext,
|
|
435
|
-
}),
|
|
436
|
-
)
|
|
437
|
-
structure.setLoadedToMolstar(true)
|
|
438
|
-
} catch (e) {
|
|
439
|
-
self.setError(e)
|
|
440
|
-
console.error(e)
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}),
|
|
446
|
-
)
|
|
402
|
+
// Load structures into Molstar as they appear or whenever the plugin
|
|
403
|
+
// context changes. See makeStructureLoader for why the autorun body is
|
|
404
|
+
// synchronous and how it guards against duplicate/stale loads.
|
|
405
|
+
addDisposer(self, autorun(makeStructureLoader(self)))
|
|
447
406
|
},
|
|
448
407
|
}))
|
|
449
408
|
.views(self => ({
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { types } from '@jbrowse/mobx-state-tree'
|
|
2
|
+
import { beforeEach, expect, test, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { loadStructureData } from './loadStructureData'
|
|
5
|
+
import { makeStructureLoader } from './structureLoader'
|
|
6
|
+
|
|
7
|
+
import type { StructureLoaderHost } from './structureLoader'
|
|
8
|
+
|
|
9
|
+
vi.mock('./loadStructureData', () => ({ loadStructureData: vi.fn() }))
|
|
10
|
+
const mockLoad = vi.mocked(loadStructureData)
|
|
11
|
+
|
|
12
|
+
// Minimal stand-ins matching only the surface makeStructureLoader touches, so
|
|
13
|
+
// the test exercises the loader's guard logic without molstar/structureModel.
|
|
14
|
+
const TestStructure = types
|
|
15
|
+
.model('TestStructure', {})
|
|
16
|
+
.volatile(() => ({
|
|
17
|
+
loadedToMolstar: false,
|
|
18
|
+
sequences: undefined as string[] | undefined,
|
|
19
|
+
}))
|
|
20
|
+
.actions(self => ({
|
|
21
|
+
setStructureData(d: { sequences?: string[] }) {
|
|
22
|
+
self.sequences = d.sequences
|
|
23
|
+
},
|
|
24
|
+
setLoadedToMolstar(v: boolean) {
|
|
25
|
+
self.loadedToMolstar = v
|
|
26
|
+
},
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
const TestHost = types
|
|
30
|
+
.model('TestHost', { structures: types.array(TestStructure) })
|
|
31
|
+
.volatile(() => ({
|
|
32
|
+
molstarPluginContext: undefined as object | undefined,
|
|
33
|
+
errors: [] as unknown[],
|
|
34
|
+
}))
|
|
35
|
+
.actions(self => ({
|
|
36
|
+
setPlugin(p: object) {
|
|
37
|
+
self.molstarPluginContext = p
|
|
38
|
+
},
|
|
39
|
+
setError(e: unknown) {
|
|
40
|
+
self.errors.push(e)
|
|
41
|
+
},
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
function setup(plugin: object) {
|
|
45
|
+
const host = TestHost.create({ structures: [{}] })
|
|
46
|
+
host.setPlugin(plugin)
|
|
47
|
+
const load = makeStructureLoader(host as unknown as StructureLoaderHost)
|
|
48
|
+
return { host, load, structure: host.structures[0]! }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tick = () => new Promise<void>(resolve => setTimeout(resolve, 0))
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockLoad.mockReset()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('loads a pending structure and marks it loaded', async () => {
|
|
58
|
+
mockLoad.mockResolvedValue({ sequences: ['ABC'] })
|
|
59
|
+
const { load, structure } = setup({})
|
|
60
|
+
load()
|
|
61
|
+
expect(mockLoad).toHaveBeenCalledTimes(1)
|
|
62
|
+
await tick()
|
|
63
|
+
expect(structure.loadedToMolstar).toBe(true)
|
|
64
|
+
expect(structure.sequences).toEqual(['ABC'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('does not start a second load while one is in flight', () => {
|
|
68
|
+
mockLoad.mockReturnValue(new Promise(() => {}))
|
|
69
|
+
const { load } = setup({})
|
|
70
|
+
load()
|
|
71
|
+
load()
|
|
72
|
+
expect(mockLoad).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('discards a stale-plugin result and reloads into the current plugin', async () => {
|
|
76
|
+
const pluginA = { id: 'A' }
|
|
77
|
+
const pluginB = { id: 'B' }
|
|
78
|
+
let resolveFirst: (v: { sequences?: string[] }) => void = () => {}
|
|
79
|
+
mockLoad
|
|
80
|
+
.mockImplementationOnce(() => new Promise(res => (resolveFirst = res)))
|
|
81
|
+
.mockResolvedValueOnce({ sequences: ['B'] })
|
|
82
|
+
|
|
83
|
+
const { host, load, structure } = setup(pluginA)
|
|
84
|
+
load() // starts loading into pluginA
|
|
85
|
+
host.setPlugin(pluginB) // plugin swapped while loading
|
|
86
|
+
resolveFirst({ sequences: ['A'] }) // pluginA result arrives, now stale
|
|
87
|
+
await tick()
|
|
88
|
+
|
|
89
|
+
expect(structure.sequences).toEqual(['B'])
|
|
90
|
+
expect(structure.loadedToMolstar).toBe(true)
|
|
91
|
+
expect(mockLoad).toHaveBeenCalledTimes(2)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('reports load errors and leaves the structure unloaded', async () => {
|
|
95
|
+
const err = new Error('boom')
|
|
96
|
+
mockLoad.mockRejectedValue(err)
|
|
97
|
+
const { host, load, structure } = setup({})
|
|
98
|
+
load()
|
|
99
|
+
await tick()
|
|
100
|
+
expect(host.errors).toContain(err)
|
|
101
|
+
expect(structure.loadedToMolstar).toBe(false)
|
|
102
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
2
|
+
|
|
3
|
+
import { loadStructureData } from './loadStructureData'
|
|
4
|
+
|
|
5
|
+
import type StructureModel from './structureModel'
|
|
6
|
+
import type { IAnyStateTreeNode, Instance } from '@jbrowse/mobx-state-tree'
|
|
7
|
+
import type { PluginContext } from 'molstar/lib/mol-plugin/context'
|
|
8
|
+
|
|
9
|
+
type StructureInstance = Instance<typeof StructureModel>
|
|
10
|
+
|
|
11
|
+
export type StructureLoaderHost = IAnyStateTreeNode & {
|
|
12
|
+
readonly molstarPluginContext: PluginContext | undefined
|
|
13
|
+
readonly structures: StructureInstance[]
|
|
14
|
+
setError: (error: unknown) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds the body of the autorun that loads structures into Molstar.
|
|
19
|
+
*
|
|
20
|
+
* The returned callback is synchronous on purpose: MobX only tracks
|
|
21
|
+
* observables read before the first `await`, so an async autorun body would
|
|
22
|
+
* stop reacting to later structures/plugin changes. Instead it reads its
|
|
23
|
+
* dependencies synchronously and dispatches a guarded fire-and-forget load for
|
|
24
|
+
* each structure that is neither loaded nor already loading. The guards handle
|
|
25
|
+
* the lifecycle hazards of an external GPU resource:
|
|
26
|
+
*
|
|
27
|
+
* - a non-observable in-flight Set stops a re-entrant run (a new structure
|
|
28
|
+
* pushed, or the plugin swapped mid-load) from starting a duplicate load of
|
|
29
|
+
* the same structure;
|
|
30
|
+
* - a load whose plugin was replaced or whose model was destroyed while
|
|
31
|
+
* awaiting has its result discarded rather than written into a torn-down
|
|
32
|
+
* plugin;
|
|
33
|
+
* - if the plugin was merely swapped (e.g. a view remount), the structure is
|
|
34
|
+
* reloaded into the current plugin so it isn't left stranded unloaded.
|
|
35
|
+
*/
|
|
36
|
+
export function makeStructureLoader(host: StructureLoaderHost) {
|
|
37
|
+
const loadingStructures = new Set<StructureInstance>()
|
|
38
|
+
|
|
39
|
+
function loadInto(structure: StructureInstance, plugin: PluginContext) {
|
|
40
|
+
loadingStructures.add(structure)
|
|
41
|
+
loadStructureData({ structure, plugin })
|
|
42
|
+
.then(data => {
|
|
43
|
+
const current = isAlive(structure)
|
|
44
|
+
? host.molstarPluginContext
|
|
45
|
+
: undefined
|
|
46
|
+
if (current === plugin) {
|
|
47
|
+
structure.setStructureData(data)
|
|
48
|
+
structure.setLoadedToMolstar(true)
|
|
49
|
+
}
|
|
50
|
+
loadingStructures.delete(structure)
|
|
51
|
+
if (current && current !== plugin && !structure.loadedToMolstar) {
|
|
52
|
+
loadInto(structure, current)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.catch((e: unknown) => {
|
|
56
|
+
loadingStructures.delete(structure)
|
|
57
|
+
if (isAlive(host)) {
|
|
58
|
+
host.setError(e)
|
|
59
|
+
console.error(e)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return function loadPendingStructures() {
|
|
65
|
+
const { structures, molstarPluginContext } = host
|
|
66
|
+
if (molstarPluginContext) {
|
|
67
|
+
for (const structure of structures) {
|
|
68
|
+
if (!structure.loadedToMolstar && !loadingStructures.has(structure)) {
|
|
69
|
+
loadInto(structure, molstarPluginContext)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { timeout } from './fetchUtils'
|
|
4
|
+
|
|
5
|
+
test('timeout resolves after the delay', async () => {
|
|
6
|
+
await expect(timeout(1)).resolves.toBeUndefined()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('timeout rejects immediately if the signal is already aborted', async () => {
|
|
10
|
+
const controller = new AbortController()
|
|
11
|
+
controller.abort()
|
|
12
|
+
await expect(timeout(10_000, controller.signal)).rejects.toThrow()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('timeout rejects when the signal aborts mid-wait', async () => {
|
|
16
|
+
const controller = new AbortController()
|
|
17
|
+
const p = timeout(10_000, controller.signal)
|
|
18
|
+
controller.abort()
|
|
19
|
+
await expect(p).rejects.toThrow()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('timeout preserves a custom Error abort reason', async () => {
|
|
23
|
+
const controller = new AbortController()
|
|
24
|
+
const reason = new Error('dialog closed')
|
|
25
|
+
controller.abort(reason)
|
|
26
|
+
await expect(timeout(10_000, controller.signal)).rejects.toBe(reason)
|
|
27
|
+
})
|
package/src/fetchUtils.ts
CHANGED
|
@@ -18,6 +18,26 @@ export async function jsonfetch<T = unknown>(
|
|
|
18
18
|
return response.json()
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
return
|
|
21
|
+
function abortError(signal: AbortSignal) {
|
|
22
|
+
return signal.reason instanceof Error
|
|
23
|
+
? signal.reason
|
|
24
|
+
: new Error('Aborted', { cause: signal.reason })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function timeout(time: number, signal?: AbortSignal) {
|
|
28
|
+
return new Promise<void>((resolve, reject) => {
|
|
29
|
+
if (signal?.aborted) {
|
|
30
|
+
reject(abortError(signal))
|
|
31
|
+
} else {
|
|
32
|
+
const id = setTimeout(resolve, time)
|
|
33
|
+
signal?.addEventListener(
|
|
34
|
+
'abort',
|
|
35
|
+
() => {
|
|
36
|
+
clearTimeout(id)
|
|
37
|
+
reject(abortError(signal))
|
|
38
|
+
},
|
|
39
|
+
{ once: true },
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
23
43
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.4.
|
|
1
|
+
export const version = '0.4.13'
|