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,5 @@
1
+ import React from 'react';
2
+ export default function LaunchSettingsDialog({ open, onClose, }: {
3
+ open: boolean;
4
+ onClose: () => void;
5
+ }): React.JSX.Element;
@@ -0,0 +1,23 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, FormGroup, Typography, } from '@mui/material';
3
+ import { getLaunchSideBySide, setLaunchSideBySide } from '../utils/sideBySide';
4
+ // Small, self-contained launch settings (NOT the global preferences dialog):
5
+ // just the options that affect how this protein view opens.
6
+ export default function LaunchSettingsDialog({ open, onClose, }) {
7
+ const [sideBySide, setSideBySide] = useState(() => getLaunchSideBySide());
8
+ return (React.createElement(Dialog, { open: open, onClose: () => {
9
+ onClose();
10
+ } },
11
+ React.createElement(DialogTitle, null, "Launch settings"),
12
+ React.createElement(DialogContent, null,
13
+ React.createElement(FormGroup, null,
14
+ React.createElement(FormControlLabel, { control: React.createElement(Checkbox, { checked: sideBySide }), label: "Open protein view side-by-side with the genome view", onChange: (_, checked) => {
15
+ setSideBySide(checked);
16
+ setLaunchSideBySide(checked);
17
+ } })),
18
+ React.createElement(Typography, { variant: "body2", color: "text.secondary" }, "When enabled, launching a protein view places it to the right of the connected genome view in a split layout instead of stacking it below.")),
19
+ React.createElement(DialogActions, null,
20
+ React.createElement(Button, { variant: "contained", onClick: () => {
21
+ onClose();
22
+ } }, "Close"))));
23
+ }
@@ -1,13 +1,16 @@
1
1
  import React, { useState } from 'react';
2
2
  import { ErrorMessage } from '@jbrowse/core/ui';
3
3
  import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
4
- import { Button, ButtonGroup, Typography } from '@mui/material';
4
+ import SettingsIcon from '@mui/icons-material/Settings';
5
+ import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/material';
5
6
  import LaunchOptionsDialog from './LaunchOptionsDialog';
7
+ import LaunchSettingsDialog from './LaunchSettingsDialog';
6
8
  import SequenceMismatchNotice from './SequenceMismatchNotice';
7
9
  import { getLaunchMissingReasons, safeLaunch } from '../utils/launchHelpers';
8
10
  import { hasMsaViewPlugin, launch1DProteinView, launch3DProteinView, launch3DProteinViewWithMsa, launchMsaView, } from '../utils/launchViewUtils';
9
11
  export default function ProteinViewActions({ handleClose, uniprotId, userSelectedProteinSequence, selectedTranscript, url, confidenceUrl, feature, view, session, alignmentAlgorithm, onAlignmentAlgorithmChange, sequencesMatch, isLoading, error, }) {
10
12
  const [dialogOpen, setDialogOpen] = useState(false);
13
+ const [settingsOpen, setSettingsOpen] = useState(false);
11
14
  const [launchError, setLaunchError] = useState();
12
15
  // Disable launch while loading — SWR's keepPreviousData would otherwise let
13
16
  // a user click Launch on stale results (wrong UniProt ID) during a refetch.
@@ -87,6 +90,11 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
87
90
  return (React.createElement(React.Fragment, null,
88
91
  launchError ? React.createElement(ErrorMessage, { error: launchError }) : null,
89
92
  sequencesMatch === false ? (React.createElement(SequenceMismatchNotice, { alignmentAlgorithm: alignmentAlgorithm, onAlignmentAlgorithmChange: onAlignmentAlgorithmChange })) : null,
93
+ React.createElement(Tooltip, { title: "Launch settings" },
94
+ React.createElement(IconButton, { size: "small", "aria-label": "Launch settings", onClick: () => {
95
+ setSettingsOpen(true);
96
+ } },
97
+ React.createElement(SettingsIcon, { fontSize: "small" }))),
90
98
  React.createElement(Button, { variant: "contained", color: "secondary", size: "small", onClick: () => {
91
99
  handleClose();
92
100
  } }, "Cancel"),
@@ -97,5 +105,8 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
97
105
  setDialogOpen(true);
98
106
  }, "aria-label": "More launch options" },
99
107
  React.createElement(ArrowDropDownIcon, null))),
100
- React.createElement(LaunchOptionsDialog, { open: dialogOpen, onClose: closeMenu, options: launchOptions })));
108
+ React.createElement(LaunchOptionsDialog, { open: dialogOpen, onClose: closeMenu, options: launchOptions }),
109
+ React.createElement(LaunchSettingsDialog, { open: settingsOpen, onClose: () => {
110
+ setSettingsOpen(false);
111
+ } })));
101
112
  }
@@ -35,7 +35,7 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
35
35
  : effectiveLookupMode === 'manual'
36
36
  ? debouncedManualUniprotId
37
37
  : undefined;
38
- const { isLoading: isAlphaFoldLoading, error: alphaFoldError, url: alphaFoldUrl, confidenceUrl: alphaFoldConfidenceUrl, structureSequence: alphaFoldStructureSequence, } = useAlphaFoldData({
38
+ const { isLoading: isAlphaFoldLoading, isValidating: isAlphaFoldValidating, error: alphaFoldError, url: alphaFoldUrl, confidenceUrl: alphaFoldConfidenceUrl, structureSequence: alphaFoldStructureSequence, } = useAlphaFoldData({
39
39
  uniprotId: isSequenceMode ? undefined : uniprotId,
40
40
  });
41
41
  const { transcripts: transcriptOptions, isoformSequences, isLoading: isIsoformLoading, error: isoformError, selectedTranscriptId: effectiveTranscriptId, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: userSelectedProteinSequence, } = useTranscriptIsoformSelection({
@@ -44,7 +44,7 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
44
44
  structureSequence: alphaFoldStructureSequence,
45
45
  resetKey: uniprotId,
46
46
  });
47
- const { uniprotId: seqSearchUniprotId, cifUrl: seqSearchUrl, plddtDocUrl: seqSearchConfidenceUrl, structureSequence: seqSearchStructureSequence, isLoading: isSequenceSearchLoading, error: sequenceSearchError, } = useAlphaFoldSequenceSearch({
47
+ const { uniprotId: seqSearchUniprotId, cifUrl: seqSearchUrl, plddtDocUrl: seqSearchConfidenceUrl, structureSequence: seqSearchStructureSequence, isLoading: isSequenceSearchLoading, isValidating: isSequenceSearchValidating, error: sequenceSearchError, } = useAlphaFoldSequenceSearch({
48
48
  sequence: userSelectedProteinSequence?.seq,
49
49
  searchType: sequenceSearchType,
50
50
  enabled: isSequenceMode,
@@ -58,6 +58,13 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
58
58
  ? seqSearchStructureSequence
59
59
  : alphaFoldStructureSequence;
60
60
  const finalUniprotId = isSequenceMode ? seqSearchUniprotId : uniprotId;
61
+ // While a structure fetch is in flight, finalStructureSequence may still be
62
+ // the previous selection's sequence (keepPreviousData). Comparing that stale
63
+ // sequence to the freshly-selected transcript would give a wrong match, so
64
+ // the match is treated as unknown until the fetch settles.
65
+ const isStructureValidating = isSequenceMode
66
+ ? isSequenceSearchValidating
67
+ : isAlphaFoldValidating;
61
68
  const loadingStatuses = [
62
69
  isLookupLoading && 'Looking up UniProt ID',
63
70
  isIsoformLoading && 'Loading protein sequences from transcript isoforms',
@@ -104,7 +111,9 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
104
111
  showStructureSelectors: !!isoformSequences &&
105
112
  !!selectedTranscript &&
106
113
  (isSequenceMode || !!(finalStructureSequence && finalUniprotId)),
107
- sequencesMatch: userSelectedProteinSequence?.seq && finalStructureSequence
114
+ sequencesMatch: !isStructureValidating &&
115
+ userSelectedProteinSequence?.seq &&
116
+ finalStructureSequence
108
117
  ? stripStopCodon(userSelectedProteinSequence.seq) ===
109
118
  finalStructureSequence
110
119
  : undefined,
@@ -2,6 +2,7 @@ export default function useAlphaFoldData({ uniprotId, }: {
2
2
  uniprotId?: string;
3
3
  }): {
4
4
  isLoading: boolean;
5
+ isValidating: boolean;
5
6
  error: any;
6
7
  url: string | undefined;
7
8
  confidenceUrl: string | undefined;
@@ -5,9 +5,10 @@ export default function useAlphaFoldData({ uniprotId, }) {
5
5
  const confidenceUrl = uniprotId
6
6
  ? getAlphaFoldConfidenceUrl(uniprotId)
7
7
  : undefined;
8
- const { sequences, isLoading, error } = useStructureFileSequence({ url });
8
+ const { sequences, isLoading, isValidating, error } = useStructureFileSequence({ url });
9
9
  return {
10
10
  isLoading,
11
+ isValidating,
11
12
  error,
12
13
  url,
13
14
  confidenceUrl,
@@ -19,6 +19,7 @@ export default function useAlphaFoldSequenceSearch({ sequence, searchType, enabl
19
19
  enabled?: boolean;
20
20
  }): {
21
21
  isLoading: boolean;
22
+ isValidating: boolean;
22
23
  result: SequenceSummaryResponse | undefined;
23
24
  uniprotId: string | undefined;
24
25
  cifUrl: string | undefined;
@@ -12,7 +12,7 @@ export default function useAlphaFoldSequenceSearch({ sequence, searchType, enabl
12
12
  const cleanSeq = stripStopCodon(sequence.toUpperCase());
13
13
  return searchType === 'md5' ? md5(cleanSeq) : cleanSeq;
14
14
  }, [sequence, searchType]);
15
- const { data, error, isLoading } = useSWR(enabled && searchValue
15
+ const { data, error, isLoading, isValidating } = useSWR(enabled && searchValue
16
16
  ? `https://alphafold.ebi.ac.uk/api/sequence/summary?id=${encodeURIComponent(searchValue)}&type=${searchType}`
17
17
  : null, jsonfetch, {
18
18
  ...STATIC_SWR_OPTIONS,
@@ -20,6 +20,7 @@ export default function useAlphaFoldSequenceSearch({ sequence, searchType, enabl
20
20
  });
21
21
  return {
22
22
  isLoading,
23
+ isValidating,
23
24
  result: data,
24
25
  uniprotId: data?.uniprotAccession,
25
26
  cifUrl: data?.cifUrl,
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import { DEFAULT_DATABASES, predict3Di, submitFoldseekSearch, waitForFoldseekResults, } from '../services/foldseekApi';
3
3
  export default function useFoldseekSearch() {
4
4
  const [results, setResults] = useState();
@@ -7,46 +7,81 @@ export default function useFoldseekSearch() {
7
7
  const [isPredicting, setIsPredicting] = useState(false);
8
8
  const [error, setError] = useState();
9
9
  const [statusMessage, setStatusMessage] = useState('');
10
+ // Aborts the in-flight request (3Di prediction or the up-to-3-minute Foldseek
11
+ // poll) when the dialog closes/unmounts, so it stops hitting the external API
12
+ // and stops updating dead state.
13
+ const abortRef = useRef(null);
14
+ useEffect(() => {
15
+ return () => {
16
+ abortRef.current?.abort();
17
+ };
18
+ }, []);
19
+ const startOperation = () => {
20
+ abortRef.current?.abort();
21
+ const controller = new AbortController();
22
+ abortRef.current = controller;
23
+ return controller.signal;
24
+ };
10
25
  const predictStructure = async (aaSequence) => {
26
+ const signal = startOperation();
11
27
  setIsPredicting(true);
12
28
  setError(undefined);
13
29
  setStatusMessage('Predicting 3Di structure...');
14
30
  try {
15
- const result = await predict3Di(aaSequence);
31
+ const result = await predict3Di({ aaSequence, signal });
16
32
  setPredictData(result);
17
33
  return result;
18
34
  }
19
35
  catch (e) {
20
- console.error(e);
21
- setError(e);
36
+ if (!signal.aborted) {
37
+ console.error(e);
38
+ setError(e);
39
+ }
22
40
  return undefined;
23
41
  }
24
42
  finally {
25
- setIsPredicting(false);
26
- setStatusMessage('');
43
+ if (!signal.aborted) {
44
+ setIsPredicting(false);
45
+ setStatusMessage('');
46
+ }
27
47
  }
28
48
  };
29
49
  const search = async (aaSeq, di3Seq, databases = DEFAULT_DATABASES) => {
50
+ const signal = startOperation();
30
51
  setIsLoading(true);
31
52
  setError(undefined);
32
53
  setStatusMessage('Submitting search...');
33
54
  try {
34
- const ticket = await submitFoldseekSearch(aaSeq, di3Seq, databases);
35
- const result = await waitForFoldseekResults(ticket.id, setStatusMessage);
55
+ const ticket = await submitFoldseekSearch({
56
+ aaSequence: aaSeq,
57
+ di3Sequence: di3Seq,
58
+ databases,
59
+ signal,
60
+ });
61
+ const result = await waitForFoldseekResults({
62
+ ticketId: ticket.id,
63
+ onStatusChange: setStatusMessage,
64
+ signal,
65
+ });
36
66
  setResults(result);
37
67
  return result;
38
68
  }
39
69
  catch (e) {
40
- console.error(e);
41
- setError(e);
70
+ if (!signal.aborted) {
71
+ console.error(e);
72
+ setError(e);
73
+ }
42
74
  return undefined;
43
75
  }
44
76
  finally {
45
- setIsLoading(false);
46
- setStatusMessage('');
77
+ if (!signal.aborted) {
78
+ setIsLoading(false);
79
+ setStatusMessage('');
80
+ }
47
81
  }
48
82
  };
49
83
  const reset = () => {
84
+ abortRef.current?.abort();
50
85
  setResults(undefined);
51
86
  setPredictData(undefined);
52
87
  setError(undefined);
@@ -4,5 +4,6 @@ export default function useStructureFileSequence({ file, url, }: {
4
4
  }): {
5
5
  error: any;
6
6
  isLoading: boolean;
7
+ isValidating: boolean;
7
8
  sequences: string[] | undefined;
8
9
  };
@@ -33,7 +33,7 @@ export default function useStructureFileSequence({ file, url, }) {
33
33
  : url
34
34
  ? ['structure-url', url]
35
35
  : null;
36
- const { data, error, isLoading } = useSWR(key, async () => {
36
+ const { data, error, isLoading, isValidating } = useSWR(key, async () => {
37
37
  const seq = await fetchSequences({ file, url });
38
38
  if (!seq) {
39
39
  throw new Error('no sequences detected in file');
@@ -43,5 +43,8 @@ export default function useStructureFileSequence({ file, url, }) {
43
43
  ...STATIC_SWR_OPTIONS,
44
44
  keepPreviousData: true,
45
45
  });
46
- return { error, isLoading, sequences: data };
46
+ // isValidating distinguishes "fetching for the current key" from the stale
47
+ // data keepPreviousData keeps around during a key change. Consumers comparing
48
+ // this sequence against another need it to avoid matching against stale data.
49
+ return { error, isLoading, isValidating, sequences: data };
47
50
  }
@@ -64,12 +64,23 @@ export interface FoldseekResult {
64
64
  };
65
65
  results: FoldseekDatabaseResult[];
66
66
  }
67
- export declare function predict3Di(aaSequence: string): Promise<{
67
+ export declare function predict3Di({ aaSequence, signal, }: {
68
+ aaSequence: string;
69
+ signal?: AbortSignal;
70
+ }): Promise<{
68
71
  aaSequence: string;
69
72
  di3Sequence: string;
70
73
  }>;
71
- export declare function submitFoldseekSearch(aaSequence: string, di3Sequence: string, databases: FoldseekDatabaseId[]): Promise<FoldseekTicketResponse>;
72
- export declare function pollFoldseekStatus(ticketId: string): Promise<FoldseekTicketResponse>;
74
+ export declare function submitFoldseekSearch({ aaSequence, di3Sequence, databases, signal, }: {
75
+ aaSequence: string;
76
+ di3Sequence: string;
77
+ databases: FoldseekDatabaseId[];
78
+ signal?: AbortSignal;
79
+ }): Promise<FoldseekTicketResponse>;
80
+ export declare function pollFoldseekStatus({ ticketId, signal, }: {
81
+ ticketId: string;
82
+ signal?: AbortSignal;
83
+ }): Promise<FoldseekTicketResponse>;
73
84
  interface FoldseekApiResponse {
74
85
  mode: string;
75
86
  queries: {
@@ -82,6 +93,13 @@ interface FoldseekApiResponse {
82
93
  taxonomyreports: unknown[];
83
94
  }[];
84
95
  }
85
- export declare function getFoldseekResults(ticketId: string): Promise<FoldseekApiResponse>;
86
- export declare function waitForFoldseekResults(ticketId: string, onStatusChange?: (status: string) => void): Promise<FoldseekResult>;
96
+ export declare function getFoldseekResults({ ticketId, signal, }: {
97
+ ticketId: string;
98
+ signal?: AbortSignal;
99
+ }): Promise<FoldseekApiResponse>;
100
+ export declare function waitForFoldseekResults({ ticketId, onStatusChange, signal, }: {
101
+ ticketId: string;
102
+ onStatusChange?: (status: string) => void;
103
+ signal?: AbortSignal;
104
+ }): Promise<FoldseekResult>;
87
105
  export {};
@@ -13,7 +13,7 @@ export const DEFAULT_DATABASES = [
13
13
  'pdb100',
14
14
  'afdb-swissprot',
15
15
  ];
16
- export async function predict3Di(aaSequence) {
16
+ export async function predict3Di({ aaSequence, signal, }) {
17
17
  // Clean the sequence - remove FASTA header, whitespace, stop codons, and non-AA chars
18
18
  const cleanSequence = aaSequence
19
19
  .split('\n')
@@ -23,7 +23,7 @@ export async function predict3Di(aaSequence) {
23
23
  .replace(/\*/g, '') // Remove stop codons before querying 3Di
24
24
  .toUpperCase()
25
25
  .replace(/[^ACDEFGHIKLMNPQRSTVWY]/g, ''); // Keep only valid amino acids
26
- const response = await fetch(`https://3di.foldseek.com/predict/${encodeURIComponent(cleanSequence)}`);
26
+ const response = await fetch(`https://3di.foldseek.com/predict/${encodeURIComponent(cleanSequence)}`, { signal });
27
27
  if (!response.ok) {
28
28
  throw new Error(`3Di prediction failed: ${response.status} ${await response.text()}`);
29
29
  }
@@ -35,7 +35,7 @@ export async function predict3Di(aaSequence) {
35
35
  .trim();
36
36
  return { aaSequence: cleanSequence, di3Sequence: cleanDi3 };
37
37
  }
38
- export async function submitFoldseekSearch(aaSequence, di3Sequence, databases) {
38
+ export async function submitFoldseekSearch({ aaSequence, di3Sequence, databases, signal, }) {
39
39
  // Submit both AA and 3Di sequences (with trailing newline like working example)
40
40
  const fastaContent = `>query\n${aaSequence}\n>3DI\n${di3Sequence}\n`;
41
41
  const params = new URLSearchParams();
@@ -51,14 +51,18 @@ export async function submitFoldseekSearch(aaSequence, di3Sequence, databases) {
51
51
  'Content-Type': 'application/x-www-form-urlencoded',
52
52
  },
53
53
  body: params,
54
+ signal,
54
55
  });
55
- const responseData = await response.json();
56
+ // Read the body as text first so a non-JSON error page (e.g. a gateway/500
57
+ // HTML response) surfaces the real status instead of an opaque JSON
58
+ // SyntaxError that hides it.
59
+ const text = await response.text();
56
60
  if (!response.ok) {
57
- throw new Error(`Foldseek submission failed: ${response.status} ${JSON.stringify(responseData)}`);
61
+ throw new Error(`Foldseek submission failed: ${response.status} ${text}`);
58
62
  }
59
- return responseData;
63
+ return JSON.parse(text);
60
64
  }
61
- export async function pollFoldseekStatus(ticketId) {
65
+ export async function pollFoldseekStatus({ ticketId, signal, }) {
62
66
  // Use the /tickets endpoint (plural) with POST
63
67
  const params = new URLSearchParams();
64
68
  params.append('tickets[]', ticketId);
@@ -68,6 +72,7 @@ export async function pollFoldseekStatus(ticketId) {
68
72
  'Content-Type': 'application/x-www-form-urlencoded',
69
73
  },
70
74
  body: params,
75
+ signal,
71
76
  });
72
77
  if (!response.ok) {
73
78
  throw new Error(`Failed to poll ticket status: ${response.status}`);
@@ -80,21 +85,24 @@ export async function pollFoldseekStatus(ticketId) {
80
85
  }
81
86
  return result;
82
87
  }
83
- export async function getFoldseekResults(ticketId) {
84
- return jsonfetch(`https://search.foldseek.com/api/result/${ticketId}/0`);
88
+ export async function getFoldseekResults({ ticketId, signal, }) {
89
+ return jsonfetch(`https://search.foldseek.com/api/result/${ticketId}/0`, { signal });
85
90
  }
86
- export async function waitForFoldseekResults(ticketId, onStatusChange) {
91
+ export async function waitForFoldseekResults({ ticketId, onStatusChange, signal, }) {
87
92
  const maxAttempts = 180;
88
93
  let attempts = 0;
89
94
  while (attempts < maxAttempts) {
90
- const status = await pollFoldseekStatus(ticketId);
95
+ if (signal?.aborted) {
96
+ throw signal.reason;
97
+ }
98
+ const status = await pollFoldseekStatus({ ticketId, signal });
91
99
  if (status.status === 'ERROR') {
92
100
  console.error('[Foldseek] Search error:', status);
93
101
  throw new Error(`Foldseek search failed: ${status.error ?? 'Unknown error'}`);
94
102
  }
95
103
  if (status.status === 'COMPLETE') {
96
104
  onStatusChange?.('Fetching results...');
97
- const apiResponse = await getFoldseekResults(ticketId);
105
+ const apiResponse = await getFoldseekResults({ ticketId, signal });
98
106
  // Transform API response to our format
99
107
  const results = {
100
108
  query: apiResponse.queries[0] ?? { header: '', sequence: '' },
@@ -106,7 +114,7 @@ export async function waitForFoldseekResults(ticketId, onStatusChange) {
106
114
  return results;
107
115
  }
108
116
  onStatusChange?.(`Search ${status.status.toLowerCase()}... (${attempts + 1}s)`);
109
- await timeout(1000);
117
+ await timeout(1000, signal);
110
118
  attempts++;
111
119
  }
112
120
  throw new Error('Foldseek search timed out');
@@ -21,13 +21,14 @@ interface LaunchViewParams {
21
21
  uniprotId?: string;
22
22
  }
23
23
  export declare function formatViewName(prefix: string, feature: Feature, selectedTranscript?: Feature, uniprotId?: string): string;
24
- export declare function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, }: LaunchViewParams & {
24
+ export declare function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }: LaunchViewParams & {
25
25
  url?: string;
26
26
  data?: string;
27
27
  userProvidedTranscriptSequence?: string;
28
28
  alignmentAlgorithm?: string;
29
29
  displayName?: string;
30
30
  connectedMsaViewId?: string;
31
+ sideBySide?: boolean;
31
32
  }): import("@jbrowse/core/util").AbstractViewModel;
32
33
  export declare function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }: LaunchViewParams & {
33
34
  confidenceUrl?: string;
@@ -1,4 +1,5 @@
1
1
  import { isSessionWithAddTracks } from '@jbrowse/core/util';
2
+ import { getLaunchSideBySide, launchViewSideBySide } from './sideBySide';
2
3
  import { getGeneDisplayName, getTranscriptDisplayName } from './util';
3
4
  import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView';
4
5
  export const ALPHAFOLD_VERSION = 'v6';
@@ -58,7 +59,7 @@ export function formatViewName(prefix, feature, selectedTranscript, uniprotId) {
58
59
  .filter(s => !!s)
59
60
  .join(' - ');
60
61
  }
61
- export function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, }) {
62
+ export function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }) {
62
63
  const snap = {
63
64
  type: 'ProteinView',
64
65
  alignmentAlgorithm,
@@ -75,7 +76,11 @@ export function launch3DProteinView({ session, view, feature, selectedTranscript
75
76
  displayName: displayName ??
76
77
  formatViewName('Protein view', feature, selectedTranscript, uniprotId),
77
78
  };
78
- return session.addView('ProteinView', snap);
79
+ const proteinView = session.addView('ProteinView', snap);
80
+ if (sideBySide ?? getLaunchSideBySide()) {
81
+ launchViewSideBySide(session, proteinView.id);
82
+ }
83
+ return proteinView;
79
84
  }
80
85
  export async function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }) {
81
86
  if (!uniprotId || !isSessionWithAddTracks(session)) {
@@ -0,0 +1,11 @@
1
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
2
+ export declare function getLaunchSideBySide(): boolean;
3
+ export declare function setLaunchSideBySide(value: boolean): void;
4
+ /**
5
+ * Place a freshly-added view to the right of the others in a workspaces (tiled)
6
+ * layout. Mirrors the "Move to split view" view-menu action: queue a splitRight
7
+ * pending move for this view, then enable workspaces so TiledViewsContainer
8
+ * consumes the move on mount (other views land in the left panel, this one in a
9
+ * new right panel). No-op on sessions without workspaces support.
10
+ */
11
+ export declare function launchViewSideBySide(session: AbstractSessionModel, viewId: string): void;
@@ -0,0 +1,33 @@
1
+ // Self-contained launch preference (NOT the global/core preferences system):
2
+ // whether a protein view launched from a genome feature opens side-by-side with
3
+ // its connected genome view (left genome | right protein) instead of stacked.
4
+ const SIDE_BY_SIDE_KEY = 'proteinView-launchSideBySide';
5
+ // Default to side-by-side: a connected genome+protein pair reads best as a
6
+ // left/right split. Users can turn it off in the launch dialog's settings.
7
+ const DEFAULT_SIDE_BY_SIDE = true;
8
+ export function getLaunchSideBySide() {
9
+ const stored = localStorage.getItem(SIDE_BY_SIDE_KEY);
10
+ return stored === null ? DEFAULT_SIDE_BY_SIDE : stored === 'true';
11
+ }
12
+ export function setLaunchSideBySide(value) {
13
+ localStorage.setItem(SIDE_BY_SIDE_KEY, value ? 'true' : 'false');
14
+ }
15
+ function isSessionWithWorkspaces(session) {
16
+ return ('setUseWorkspaces' in session &&
17
+ typeof session.setUseWorkspaces === 'function' &&
18
+ 'setPendingMove' in session &&
19
+ typeof session.setPendingMove === 'function');
20
+ }
21
+ /**
22
+ * Place a freshly-added view to the right of the others in a workspaces (tiled)
23
+ * layout. Mirrors the "Move to split view" view-menu action: queue a splitRight
24
+ * pending move for this view, then enable workspaces so TiledViewsContainer
25
+ * consumes the move on mount (other views land in the left panel, this one in a
26
+ * new right panel). No-op on sessions without workspaces support.
27
+ */
28
+ export function launchViewSideBySide(session, viewId) {
29
+ if (isSessionWithWorkspaces(session)) {
30
+ session.setPendingMove({ type: 'splitRight', viewId });
31
+ session.setUseWorkspaces(true);
32
+ }
33
+ }
@@ -1,4 +1,5 @@
1
1
  import { resolveShortLaunch } from './resolveShortLaunch';
2
+ import { getLaunchSideBySide, launchViewSideBySide, } from '../LaunchProteinView/utils/sideBySide';
2
3
  export default function LaunchProteinViewExtensionPointF(pluginManager) {
3
4
  pluginManager.addToExtensionPoint('LaunchView-ProteinView',
4
5
  // LaunchView extension points are typed as transformers `(extendee, props)
@@ -6,7 +7,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
6
7
  // handlers and ignores the return value. Casting away the signature
7
8
  // mismatch rather than fabricating a fake return.
8
9
  // @ts-expect-error
9
- async ({ session, url, uniprotId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, displayName, height, showControls, showHighlight, zoomToBaseLevel, }) => {
10
+ async ({ session, url, uniprotId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, displayName, height, showControls, showHighlight, zoomToBaseLevel, sideBySide, }) => {
10
11
  // Short-URL form: `uniprotId` + `transcriptId` + `connectedView` (no
11
12
  // explicit `url`/`feature`/sequence). Derive the structure URL, the
12
13
  // transcript feature, and the translated sequence from the connected
@@ -37,6 +38,9 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
37
38
  // `connectedView` is supplied we create the LinearGenomeView here and wire
38
39
  // its id, letting a single spec entry produce a connected genome+protein
39
40
  // pair (e.g. hover a variant to highlight the residue).
41
+ // a connected view this launch created itself can be split beside the
42
+ // protein view; a pre-existing connectedViewId is left in place
43
+ const ownsConnectedView = !connectedViewId && !!connectedView;
40
44
  const resolvedConnectedViewId = connectedViewId ??
41
45
  (connectedView
42
46
  ? session.addView('LinearGenomeView', {
@@ -44,7 +48,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
44
48
  init: connectedView,
45
49
  }).id
46
50
  : undefined);
47
- session.addView('ProteinView', {
51
+ const proteinView = session.addView('ProteinView', {
48
52
  type: 'ProteinView',
49
53
  alignmentAlgorithm,
50
54
  displayName,
@@ -63,5 +67,8 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
63
67
  },
64
68
  ],
65
69
  });
70
+ if (ownsConnectedView && (sideBySide ?? getLaunchSideBySide())) {
71
+ launchViewSideBySide(session, proteinView.id);
72
+ }
66
73
  });
67
74
  }
@@ -31,7 +31,7 @@ export declare const COLOR_SCHEMES: readonly [{
31
31
  readonly label: "Molecule type";
32
32
  }];
33
33
  export type ProteinColorScheme = (typeof COLOR_SCHEMES)[number]['value'];
34
- export declare const COLOR_SCHEME_VALUES: ("uncertainty" | "hydrophobicity" | "chain-id" | "molecule-type" | "residue-name" | "secondary-structure" | "default" | "plddt-confidence")[];
34
+ export declare const COLOR_SCHEME_VALUES: ("default" | "plddt-confidence" | "chain-id" | "secondary-structure" | "hydrophobicity" | "residue-name" | "uncertainty" | "molecule-type")[];
35
35
  export declare function applyColorTheme({ plugin, colorScheme, }: {
36
36
  plugin: PluginContext;
37
37
  colorScheme: ProteinColorScheme;
@@ -0,0 +1,18 @@
1
+ import type { PluginContext } from 'molstar/lib/mol-plugin/context';
2
+ export interface StructureData {
3
+ sequences?: string[];
4
+ confidence?: number[];
5
+ }
6
+ /**
7
+ * Loads a structure (from inline data or a URL) into the given Molstar plugin
8
+ * and pulls out its per-chain sequences and per-residue confidence. Pure with
9
+ * respect to the model — it only touches the plugin and returns plain data, so
10
+ * callers own the decision of whether/where to store the result.
11
+ */
12
+ export declare function loadStructureData({ structure, plugin, }: {
13
+ structure: {
14
+ data?: string;
15
+ url?: string;
16
+ };
17
+ plugin: PluginContext;
18
+ }): Promise<StructureData>;
@@ -0,0 +1,22 @@
1
+ import { addStructureFromData } from './addStructureFromData';
2
+ import { addStructureFromURL } from './addStructureFromURL';
3
+ import { extractPerResidueConfidence } from './extractPerResidueConfidence';
4
+ import { extractStructureSequences } from './extractStructureSequences';
5
+ /**
6
+ * Loads a structure (from inline data or a URL) into the given Molstar plugin
7
+ * and pulls out its per-chain sequences and per-residue confidence. Pure with
8
+ * respect to the model — it only touches the plugin and returns plain data, so
9
+ * callers own the decision of whether/where to store the result.
10
+ */
11
+ export async function loadStructureData({ structure, plugin, }) {
12
+ const { model } = structure.data
13
+ ? await addStructureFromData({ data: structure.data, plugin })
14
+ : structure.url
15
+ ? await addStructureFromURL({ url: structure.url, plugin })
16
+ : { model: undefined };
17
+ const sequences = model ? extractStructureSequences(model) : undefined;
18
+ const confidence = model
19
+ ? extractPerResidueConfidence(model, sequences?.[0]?.length)
20
+ : undefined;
21
+ return { sequences, confidence };
22
+ }
@@ -19,7 +19,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
19
19
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
20
20
  displayName: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
21
21
  minimized: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
22
- }, "colorScheme" | "structures" | "type" | "id" | "init" | "zoomToBaseLevel" | "alignmentAlgorithm" | "connectedMsaViewId" | "showHighlight" | "showAlignment" | "showProteinTracks" | "autoScrollAlignment" | "compactTracks" | "showControls" | "height"> & {
22
+ }, "colorScheme" | "structures" | "id" | "type" | "init" | "zoomToBaseLevel" | "alignmentAlgorithm" | "connectedMsaViewId" | "showHighlight" | "showAlignment" | "showProteinTracks" | "autoScrollAlignment" | "compactTracks" | "showControls" | "height"> & {
23
23
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
24
24
  type: import("@jbrowse/mobx-state-tree").ISimpleType<"ProteinView">;
25
25
  structures: import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IModelType<{
@@ -641,7 +641,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
641
641
  showHighlight: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
642
642
  zoomToBaseLevel: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
643
643
  autoScrollAlignment: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
644
- colorScheme: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<"uncertainty" | "hydrophobicity" | "chain-id" | "molecule-type" | "residue-name" | "secondary-structure" | "default" | "plddt-confidence">, [undefined]>;
644
+ colorScheme: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<"default" | "plddt-confidence" | "chain-id" | "secondary-structure" | "hydrophobicity" | "residue-name" | "uncertainty" | "molecule-type">, [undefined]>;
645
645
  showAlignment: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
646
646
  showProteinTracks: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
647
647
  compactTracks: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;