jbrowse-plugin-protein3d 0.4.11 → 0.4.12

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 (39) hide show
  1. package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +12 -3
  2. package/dist/LaunchProteinView/hooks/useAlphaFoldData.d.ts +1 -0
  3. package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +2 -1
  4. package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.d.ts +1 -0
  5. package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.js +2 -1
  6. package/dist/LaunchProteinView/hooks/useFoldseekSearch.js +47 -12
  7. package/dist/LaunchProteinView/hooks/useStructureFileSequence.d.ts +1 -0
  8. package/dist/LaunchProteinView/hooks/useStructureFileSequence.js +5 -2
  9. package/dist/LaunchProteinView/services/foldseekApi.d.ts +23 -5
  10. package/dist/LaunchProteinView/services/foldseekApi.js +21 -13
  11. package/dist/ProteinView/applyColorTheme.d.ts +1 -1
  12. package/dist/ProteinView/loadStructureData.d.ts +18 -0
  13. package/dist/ProteinView/loadStructureData.js +22 -0
  14. package/dist/ProteinView/model.d.ts +2 -2
  15. package/dist/ProteinView/model.js +6 -36
  16. package/dist/ProteinView/structureLoader.d.ts +30 -0
  17. package/dist/ProteinView/structureLoader.js +58 -0
  18. package/dist/config.json +1 -1
  19. package/dist/fetchUtils.d.ts +1 -1
  20. package/dist/fetchUtils.js +18 -2
  21. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +13 -13
  22. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  23. package/dist/molstar-chunk.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +4 -2
  27. package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +13 -1
  28. package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +3 -1
  29. package/src/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.ts +12 -10
  30. package/src/LaunchProteinView/hooks/useFoldseekSearch.ts +49 -12
  31. package/src/LaunchProteinView/hooks/useStructureFileSequence.ts +5 -2
  32. package/src/LaunchProteinView/services/foldseekApi.ts +57 -23
  33. package/src/ProteinView/loadStructureData.ts +36 -0
  34. package/src/ProteinView/model.ts +6 -47
  35. package/src/ProteinView/structureLoader.test.ts +102 -0
  36. package/src/ProteinView/structureLoader.ts +74 -0
  37. package/src/fetchUtils.test.ts +27 -0
  38. package/src/fetchUtils.ts +22 -2
  39. package/src/version.ts +1 -1
@@ -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');
@@ -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>;
@@ -4,11 +4,9 @@ import { addDisposer, types } from '@jbrowse/mobx-state-tree';
4
4
  import SettingsIcon from '@mui/icons-material/Settings';
5
5
  import Visibility from '@mui/icons-material/Visibility';
6
6
  import { autorun } from 'mobx';
7
- import { addStructureFromData } from './addStructureFromData';
8
- import { addStructureFromURL } from './addStructureFromURL';
9
7
  import { COLOR_SCHEMES, COLOR_SCHEME_VALUES, applyColorTheme, } from './applyColorTheme';
10
- import { extractPerResidueConfidence } from './extractPerResidueConfidence';
11
- import { extractStructureSequences } from './extractStructureSequences';
8
+ import { loadStructureData } from './loadStructureData';
9
+ import { makeStructureLoader } from './structureLoader';
12
10
  import Structure from './structureModel';
13
11
  import { superposeStructures } from './superposeStructures';
14
12
  import { DEFAULT_ALIGNMENT_ALGORITHM } from './types';
@@ -21,18 +19,6 @@ const PERSISTED_SETTINGS = [
21
19
  'autoScrollAlignment',
22
20
  'compactTracks',
23
21
  ];
24
- async function loadStructureData({ structure, plugin, }) {
25
- const { model } = structure.data
26
- ? await addStructureFromData({ data: structure.data, plugin })
27
- : structure.url
28
- ? await addStructureFromURL({ url: structure.url, plugin })
29
- : { model: undefined };
30
- const sequences = model ? extractStructureSequences(model) : undefined;
31
- const confidence = model
32
- ? extractPerResidueConfidence(model, sequences?.[0]?.length)
33
- : undefined;
34
- return { sequences, confidence };
35
- }
36
22
  /**
37
23
  * #stateModel Protein3dViewPlugin
38
24
  * extends
@@ -347,26 +333,10 @@ function stateModelFactory() {
347
333
  });
348
334
  }
349
335
  }));
350
- addDisposer(self, autorun(async () => {
351
- const { structures, molstarPluginContext } = self;
352
- if (molstarPluginContext) {
353
- for (const structure of structures) {
354
- if (!structure.loadedToMolstar) {
355
- try {
356
- structure.setStructureData(await loadStructureData({
357
- structure,
358
- plugin: molstarPluginContext,
359
- }));
360
- structure.setLoadedToMolstar(true);
361
- }
362
- catch (e) {
363
- self.setError(e);
364
- console.error(e);
365
- }
366
- }
367
- }
368
- }
369
- }));
336
+ // Load structures into Molstar as they appear or whenever the plugin
337
+ // context changes. See makeStructureLoader for why the autorun body is
338
+ // synchronous and how it guards against duplicate/stale loads.
339
+ addDisposer(self, autorun(makeStructureLoader(self)));
370
340
  },
371
341
  }))
372
342
  .views(self => ({
@@ -0,0 +1,30 @@
1
+ import type StructureModel from './structureModel';
2
+ import type { IAnyStateTreeNode, Instance } from '@jbrowse/mobx-state-tree';
3
+ import type { PluginContext } from 'molstar/lib/mol-plugin/context';
4
+ type StructureInstance = Instance<typeof StructureModel>;
5
+ export type StructureLoaderHost = IAnyStateTreeNode & {
6
+ readonly molstarPluginContext: PluginContext | undefined;
7
+ readonly structures: StructureInstance[];
8
+ setError: (error: unknown) => void;
9
+ };
10
+ /**
11
+ * Builds the body of the autorun that loads structures into Molstar.
12
+ *
13
+ * The returned callback is synchronous on purpose: MobX only tracks
14
+ * observables read before the first `await`, so an async autorun body would
15
+ * stop reacting to later structures/plugin changes. Instead it reads its
16
+ * dependencies synchronously and dispatches a guarded fire-and-forget load for
17
+ * each structure that is neither loaded nor already loading. The guards handle
18
+ * the lifecycle hazards of an external GPU resource:
19
+ *
20
+ * - a non-observable in-flight Set stops a re-entrant run (a new structure
21
+ * pushed, or the plugin swapped mid-load) from starting a duplicate load of
22
+ * the same structure;
23
+ * - a load whose plugin was replaced or whose model was destroyed while
24
+ * awaiting has its result discarded rather than written into a torn-down
25
+ * plugin;
26
+ * - if the plugin was merely swapped (e.g. a view remount), the structure is
27
+ * reloaded into the current plugin so it isn't left stranded unloaded.
28
+ */
29
+ export declare function makeStructureLoader(host: StructureLoaderHost): () => void;
30
+ export {};
@@ -0,0 +1,58 @@
1
+ import { isAlive } from '@jbrowse/mobx-state-tree';
2
+ import { loadStructureData } from './loadStructureData';
3
+ /**
4
+ * Builds the body of the autorun that loads structures into Molstar.
5
+ *
6
+ * The returned callback is synchronous on purpose: MobX only tracks
7
+ * observables read before the first `await`, so an async autorun body would
8
+ * stop reacting to later structures/plugin changes. Instead it reads its
9
+ * dependencies synchronously and dispatches a guarded fire-and-forget load for
10
+ * each structure that is neither loaded nor already loading. The guards handle
11
+ * the lifecycle hazards of an external GPU resource:
12
+ *
13
+ * - a non-observable in-flight Set stops a re-entrant run (a new structure
14
+ * pushed, or the plugin swapped mid-load) from starting a duplicate load of
15
+ * the same structure;
16
+ * - a load whose plugin was replaced or whose model was destroyed while
17
+ * awaiting has its result discarded rather than written into a torn-down
18
+ * plugin;
19
+ * - if the plugin was merely swapped (e.g. a view remount), the structure is
20
+ * reloaded into the current plugin so it isn't left stranded unloaded.
21
+ */
22
+ export function makeStructureLoader(host) {
23
+ const loadingStructures = new Set();
24
+ function loadInto(structure, plugin) {
25
+ loadingStructures.add(structure);
26
+ loadStructureData({ structure, plugin })
27
+ .then(data => {
28
+ const current = isAlive(structure)
29
+ ? host.molstarPluginContext
30
+ : undefined;
31
+ if (current === plugin) {
32
+ structure.setStructureData(data);
33
+ structure.setLoadedToMolstar(true);
34
+ }
35
+ loadingStructures.delete(structure);
36
+ if (current && current !== plugin && !structure.loadedToMolstar) {
37
+ loadInto(structure, current);
38
+ }
39
+ })
40
+ .catch((e) => {
41
+ loadingStructures.delete(structure);
42
+ if (isAlive(host)) {
43
+ host.setError(e);
44
+ console.error(e);
45
+ }
46
+ });
47
+ }
48
+ return function loadPendingStructures() {
49
+ const { structures, molstarPluginContext } = host;
50
+ if (molstarPluginContext) {
51
+ for (const structure of structures) {
52
+ if (!structure.loadedToMolstar && !loadingStructures.has(structure)) {
53
+ loadInto(structure, molstarPluginContext);
54
+ }
55
+ }
56
+ }
57
+ };
58
+ }
package/dist/config.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "plugins": [
3
3
  {
4
4
  "name": "Protein3d",
5
- "url": "https://unpkg.com/jbrowse-plugin-protein3d/dist/jbrowse-plugin-protein3d.umd.production.min.js"
5
+ "url": "https://jbrowse.org/plugins/jbrowse-plugin-protein3d/0.4.12/dist/jbrowse-plugin-protein3d.umd.production.min.js"
6
6
  }
7
7
  ],
8
8
  "assemblies": [
@@ -1,3 +1,3 @@
1
1
  export declare function myfetch(url: string, args?: RequestInit): Promise<Response>;
2
2
  export declare function jsonfetch<T = unknown>(url: string, args?: RequestInit): Promise<T>;
3
- export declare function timeout(time: number): Promise<unknown>;
3
+ export declare function timeout(time: number, signal?: AbortSignal): Promise<void>;
@@ -9,6 +9,22 @@ export async function jsonfetch(url, args) {
9
9
  const response = await myfetch(url, args);
10
10
  return response.json();
11
11
  }
12
- export function timeout(time) {
13
- return new Promise(res => setTimeout(res, time));
12
+ function abortError(signal) {
13
+ return signal.reason instanceof Error
14
+ ? signal.reason
15
+ : new Error('Aborted', { cause: signal.reason });
16
+ }
17
+ export function timeout(time, signal) {
18
+ return new Promise((resolve, reject) => {
19
+ if (signal?.aborted) {
20
+ reject(abortError(signal));
21
+ }
22
+ else {
23
+ const id = setTimeout(resolve, time);
24
+ signal?.addEventListener('abort', () => {
25
+ clearTimeout(id);
26
+ reject(abortError(signal));
27
+ }, { once: true });
28
+ }
29
+ });
14
30
  }