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.
Files changed (52) hide show
  1. package/dist/LaunchProteinView/components/LaunchSettingsDialog.d.ts +5 -0
  2. package/dist/LaunchProteinView/components/LaunchSettingsDialog.js +23 -0
  3. package/dist/LaunchProteinView/components/ProteinViewActions.js +13 -2
  4. package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +12 -3
  5. package/dist/LaunchProteinView/hooks/useAlphaFoldData.d.ts +1 -0
  6. package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +2 -1
  7. package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.d.ts +1 -0
  8. package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.js +2 -1
  9. package/dist/LaunchProteinView/hooks/useFoldseekSearch.js +47 -12
  10. package/dist/LaunchProteinView/hooks/useStructureFileSequence.d.ts +1 -0
  11. package/dist/LaunchProteinView/hooks/useStructureFileSequence.js +5 -2
  12. package/dist/LaunchProteinView/services/foldseekApi.d.ts +23 -5
  13. package/dist/LaunchProteinView/services/foldseekApi.js +21 -13
  14. package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +2 -1
  15. package/dist/LaunchProteinView/utils/launchViewUtils.js +7 -2
  16. package/dist/LaunchProteinView/utils/sideBySide.d.ts +11 -0
  17. package/dist/LaunchProteinView/utils/sideBySide.js +33 -0
  18. package/dist/LaunchProteinViewExtensionPoint/index.js +9 -2
  19. package/dist/ProteinView/applyColorTheme.d.ts +1 -1
  20. package/dist/ProteinView/loadStructureData.d.ts +18 -0
  21. package/dist/ProteinView/loadStructureData.js +22 -0
  22. package/dist/ProteinView/model.d.ts +2 -2
  23. package/dist/ProteinView/model.js +6 -36
  24. package/dist/ProteinView/structureLoader.d.ts +30 -0
  25. package/dist/ProteinView/structureLoader.js +58 -0
  26. package/dist/config.json +1 -1
  27. package/dist/fetchUtils.d.ts +1 -1
  28. package/dist/fetchUtils.js +18 -2
  29. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  30. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  31. package/dist/molstar-chunk.js.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +4 -2
  35. package/src/LaunchProteinView/components/LaunchSettingsDialog.tsx +63 -0
  36. package/src/LaunchProteinView/components/ProteinViewActions.tsx +21 -1
  37. package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +13 -1
  38. package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +3 -1
  39. package/src/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.ts +12 -10
  40. package/src/LaunchProteinView/hooks/useFoldseekSearch.ts +49 -12
  41. package/src/LaunchProteinView/hooks/useStructureFileSequence.ts +5 -2
  42. package/src/LaunchProteinView/services/foldseekApi.ts +57 -23
  43. package/src/LaunchProteinView/utils/launchViewUtils.ts +10 -1
  44. package/src/LaunchProteinView/utils/sideBySide.ts +55 -0
  45. package/src/LaunchProteinViewExtensionPoint/index.ts +17 -1
  46. package/src/ProteinView/loadStructureData.ts +36 -0
  47. package/src/ProteinView/model.ts +6 -47
  48. package/src/ProteinView/structureLoader.test.ts +102 -0
  49. package/src/ProteinView/structureLoader.ts +74 -0
  50. package/src/fetchUtils.test.ts +27 -0
  51. package/src/fetchUtils.ts +22 -2
  52. 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
+ }
@@ -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 { extractPerResidueConfidence } from './extractPerResidueConfidence'
17
- import { extractStructureSequences } from './extractStructureSequences'
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
- addDisposer(
424
- self,
425
- autorun(async () => {
426
- const { structures, molstarPluginContext } = self
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
- export function timeout(time: number) {
22
- return new Promise(res => setTimeout(res, time))
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.11'
1
+ export const version = '0.4.13'