jbrowse-plugin-msaview 3.8.0 → 3.9.0

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 (129) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -22
  2. package/dist/AddHighlightModel/connectedHighlights.d.ts +9 -0
  3. package/dist/AddHighlightModel/connectedHighlights.js +15 -0
  4. package/dist/AddHighlightModel/connectedHighlights.test.d.ts +1 -0
  5. package/dist/AddHighlightModel/connectedHighlights.test.js +36 -0
  6. package/dist/AddHighlightModel/index.js +0 -1
  7. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +4 -4
  8. package/dist/LaunchMsaView/components/BlastQuery/consts.js +6 -4
  9. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +0 -2
  10. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +2 -2
  11. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +0 -1
  12. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +1 -1
  13. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +5 -5
  14. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +0 -6
  15. package/dist/LaunchMsaView/components/calculateProteinSequence.js +1 -1
  16. package/dist/LaunchMsaView/components/types.d.ts +0 -3
  17. package/dist/LaunchMsaView/util.d.ts +0 -1
  18. package/dist/LaunchMsaView/util.js +1 -1
  19. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +5 -14
  20. package/dist/MsaViewPanel/afterCreateAutoruns.js +77 -86
  21. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  22. package/dist/MsaViewPanel/doLaunchBlast.js +42 -16
  23. package/dist/MsaViewPanel/doLaunchBlast.test.js +54 -0
  24. package/dist/MsaViewPanel/doLaunchOrthologs.js +18 -11
  25. package/dist/MsaViewPanel/launchAutoruns.test.js +0 -1
  26. package/dist/MsaViewPanel/model.d.ts +13 -7
  27. package/dist/MsaViewPanel/model.js +39 -10
  28. package/dist/MsaViewPanel/model.test.js +3 -4
  29. package/dist/MsaViewPanel/msaDataStore.d.ts +1 -2
  30. package/dist/MsaViewPanel/msaDataStore.js +11 -26
  31. package/dist/MsaViewPanel/msaDataStore.test.js +10 -2
  32. package/dist/MsaViewPanel/observeProteinHighlights.test.js +1 -0
  33. package/dist/MsaViewPanel/processInit.test.js +1 -7
  34. package/dist/MsaViewPanel/regionSnapshot.test.js +0 -1
  35. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +0 -1
  36. package/dist/MsaViewPanel/storedData.test.js +238 -145
  37. package/dist/MsaViewPanel/transcriptMap.d.ts +7 -0
  38. package/dist/MsaViewPanel/transcriptMap.js +89 -0
  39. package/dist/MsaViewPanel/transcriptMap.test.d.ts +1 -0
  40. package/dist/MsaViewPanel/transcriptMap.test.js +108 -0
  41. package/dist/jbrowse-plugin-msaview.umd.production.min.js +38 -38
  42. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  43. package/dist/utils/browserAlign.d.ts +5 -3
  44. package/dist/utils/browserAlign.js +29 -23
  45. package/dist/utils/browserAlign.test.js +14 -1
  46. package/dist/utils/ebiBlast.d.ts +0 -8
  47. package/dist/utils/ebiBlast.js +10 -21
  48. package/dist/utils/ebiJobDispatcher.d.ts +9 -9
  49. package/dist/utils/ebiJobDispatcher.js +17 -3
  50. package/dist/utils/eutils.d.ts +0 -2
  51. package/dist/utils/eutils.js +11 -3
  52. package/dist/utils/eutils.test.js +22 -0
  53. package/dist/utils/msa.js +6 -14
  54. package/dist/utils/msaRows.d.ts +0 -8
  55. package/dist/utils/msaRows.js +1 -1
  56. package/dist/utils/ncbiOrthologs.d.ts +7 -5
  57. package/dist/utils/ncbiOrthologs.js +20 -14
  58. package/dist/utils/ncbiOrthologs.test.js +13 -0
  59. package/dist/utils/ncbiTaxonomy.d.ts +2 -2
  60. package/dist/utils/ncbiTaxonomy.js +5 -5
  61. package/dist/utils/pantherOrthologs.d.ts +2 -3
  62. package/dist/utils/pantherOrthologs.js +18 -12
  63. package/dist/utils/phmmer.js +7 -14
  64. package/dist/utils/searchCache.d.ts +18 -0
  65. package/dist/utils/searchCache.js +33 -0
  66. package/dist/utils/searchCache.test.d.ts +1 -0
  67. package/dist/utils/searchCache.test.js +80 -0
  68. package/dist/utils/taxonomyNames.d.ts +1 -1
  69. package/dist/utils/taxonomyNames.js +7 -2
  70. package/dist/utils/taxonomyNamesAbort.test.d.ts +1 -0
  71. package/dist/utils/taxonomyNamesAbort.test.js +36 -0
  72. package/dist/utils/unirefHomologs.d.ts +1 -6
  73. package/dist/utils/unirefHomologs.js +2 -2
  74. package/dist/utils/useFetch.d.ts +1 -1
  75. package/dist/utils/useFetch.js +5 -5
  76. package/dist/utils/useFetchAbort.test.d.ts +1 -0
  77. package/dist/utils/useFetchAbort.test.js +29 -0
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/package.json +1 -1
  81. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +10 -23
  82. package/src/AddHighlightModel/connectedHighlights.test.ts +46 -0
  83. package/src/AddHighlightModel/connectedHighlights.ts +35 -0
  84. package/src/AddHighlightModel/index.tsx +0 -1
  85. package/src/LaunchMsaView/components/BlastQuery/consts.ts +6 -4
  86. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +2 -2
  87. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +1 -1
  88. package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +5 -5
  89. package/src/LaunchMsaView/components/calculateProteinSequence.ts +1 -1
  90. package/src/LaunchMsaView/components/types.ts +0 -4
  91. package/src/LaunchMsaView/util.ts +1 -1
  92. package/src/MsaViewPanel/afterCreateAutoruns.ts +90 -87
  93. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  94. package/src/MsaViewPanel/doLaunchBlast.test.ts +71 -0
  95. package/src/MsaViewPanel/doLaunchBlast.ts +71 -20
  96. package/src/MsaViewPanel/doLaunchOrthologs.ts +28 -11
  97. package/src/MsaViewPanel/launchAutoruns.test.ts +0 -1
  98. package/src/MsaViewPanel/model.test.ts +3 -4
  99. package/src/MsaViewPanel/model.ts +40 -10
  100. package/src/MsaViewPanel/msaDataStore.test.ts +13 -2
  101. package/src/MsaViewPanel/msaDataStore.ts +11 -25
  102. package/src/MsaViewPanel/observeProteinHighlights.test.ts +1 -0
  103. package/src/MsaViewPanel/processInit.test.ts +1 -8
  104. package/src/MsaViewPanel/regionSnapshot.test.ts +0 -1
  105. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +0 -1
  106. package/src/MsaViewPanel/storedData.test.ts +273 -158
  107. package/src/MsaViewPanel/transcriptMap.test.ts +137 -0
  108. package/src/MsaViewPanel/transcriptMap.ts +117 -0
  109. package/src/utils/browserAlign.test.ts +17 -0
  110. package/src/utils/browserAlign.ts +39 -25
  111. package/src/utils/ebiBlast.ts +12 -35
  112. package/src/utils/ebiJobDispatcher.ts +26 -7
  113. package/src/utils/eutils.test.ts +25 -0
  114. package/src/utils/eutils.ts +10 -3
  115. package/src/utils/msa.ts +6 -16
  116. package/src/utils/msaRows.ts +1 -1
  117. package/src/utils/ncbiOrthologs.test.ts +16 -0
  118. package/src/utils/ncbiOrthologs.ts +37 -15
  119. package/src/utils/ncbiTaxonomy.ts +8 -2
  120. package/src/utils/pantherOrthologs.ts +28 -12
  121. package/src/utils/phmmer.ts +7 -16
  122. package/src/utils/searchCache.test.ts +98 -0
  123. package/src/utils/searchCache.ts +75 -0
  124. package/src/utils/taxonomyNames.ts +7 -0
  125. package/src/utils/taxonomyNamesAbort.test.ts +46 -0
  126. package/src/utils/unirefHomologs.ts +2 -2
  127. package/src/utils/useFetch.ts +6 -6
  128. package/src/utils/useFetchAbort.test.tsx +33 -0
  129. package/src/version.ts +1 -1
@@ -1,41 +1,25 @@
1
1
  import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
- import { isMsaView } from '../MsaViewPanel/model';
4
+ import { connectedHighlights } from './connectedHighlights';
5
5
  import { hasHoverPosition, useStyles } from './util';
6
6
  const MsaToGenomeHighlight = observer(function MsaToGenomeHighlight2({ model, }) {
7
7
  const { views, hovered } = getSession(model);
8
- const msaView = views
9
- .filter(isMsaView)
10
- .find(v => v.connectedViewId === model.id);
11
- // The persistent click selection always shows. The hover codon is suppressed
12
- // while hovering the LGV — GenomeMouseoverHighlight handles the single-bp
13
- // display in that case, so we don't stack a wider codon band on top of it.
14
- const highlights = [
15
- ...(msaView?.connectedClickHighlights ?? []),
16
- ...(hasHoverPosition(hovered)
17
- ? []
18
- : (msaView?.connectedHoverHighlights ?? [])),
19
- ];
8
+ const highlights = connectedHighlights(views, model.id, hasHoverPosition(hovered));
20
9
  return highlights.length ? (React.createElement(MsaToGenomeHighlightRenderer, { model: model, highlights: highlights })) : null;
21
10
  });
22
- // Inner component: handles the scroll-dependent rendering
23
11
  const MsaToGenomeHighlightRenderer = observer(function ({ model, highlights, }) {
24
12
  const { classes } = useStyles();
25
13
  const { offsetPx } = model;
26
- return (React.createElement(React.Fragment, null, highlights.map((r, idx) => {
27
- // Use the highlight's own refName, which is already in the connected
28
- // view's coordinate space (it comes from the connectedFeature the
29
- // launcher set on this LGV). Do NOT canonicalize: bpToPx matches
30
- // displayed regions by exact refName with no alias resolution, so
31
- // rewriting e.g. "chr17" to the assembly-canonical "17" misses a view
32
- // whose regions are "chr17". (GenomeMouseoverHighlight does the same.)
14
+ return (React.createElement(React.Fragment, null, highlights.map(r => {
15
+ // bpToPx matches refNames exactly, so canonicalizing "chr17" to "17"
16
+ // would miss a view whose regions say "chr17"
33
17
  const s = model.bpToPx({ refName: r.refName, coord: r.start });
34
18
  const e = model.bpToPx({ refName: r.refName, coord: r.end });
35
19
  if (s && e) {
36
20
  const width = Math.max(Math.abs(e.offsetPx - s.offsetPx), 4);
37
21
  const left = Math.min(s.offsetPx, e.offsetPx) - offsetPx;
38
- return (React.createElement("div", { key: `${r.refName}-${r.start}-${r.end}-${idx}`, className: classes.highlight, style: { left, width } }));
22
+ return (React.createElement("div", { key: `${r.refName}-${r.start}-${r.end}`, className: classes.highlight, style: { left, width } }));
39
23
  }
40
24
  return null;
41
25
  })));
@@ -0,0 +1,9 @@
1
+ interface Region {
2
+ refName: string;
3
+ start: number;
4
+ end: number;
5
+ }
6
+ export declare function connectedHighlights(views: readonly {
7
+ type: string;
8
+ }[], genomeViewId: string, genomeHovered: boolean): Region[];
9
+ export {};
@@ -0,0 +1,15 @@
1
+ export function connectedHighlights(views, genomeViewId, genomeHovered) {
2
+ const regions = views
3
+ .filter(isLinkedMsaView)
4
+ .filter(v => v.connectedViewId === genomeViewId)
5
+ .flatMap(v => [
6
+ ...v.connectedClickHighlights,
7
+ ...(genomeHovered ? [] : v.connectedHoverHighlights),
8
+ ]);
9
+ return [
10
+ ...new Map(regions.map(r => [`${r.refName}:${r.start}-${r.end}`, r])).values(),
11
+ ];
12
+ }
13
+ function isLinkedMsaView(view) {
14
+ return view.type === 'MsaView';
15
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { connectedHighlights } from './connectedHighlights';
3
+ const region = (start) => ({ refName: 'chr17', start, end: start + 3 });
4
+ function msaView(id, click, hover) {
5
+ return {
6
+ type: 'MsaView',
7
+ connectedViewId: id,
8
+ connectedClickHighlights: [region(click)],
9
+ connectedHoverHighlights: [region(hover)],
10
+ };
11
+ }
12
+ describe('connectedHighlights', () => {
13
+ const views = [
14
+ { type: 'LinearGenomeView' },
15
+ msaView('lgv1', 100, 200),
16
+ msaView('lgv2', 300, 400),
17
+ msaView('lgv1', 500, 600),
18
+ ];
19
+ it('collects every MSA view linked to the genome view', () => {
20
+ expect(connectedHighlights(views, 'lgv1', false).map(r => r.start)).toEqual([100, 200, 500, 600]);
21
+ });
22
+ it('drops hover codons while the genome view is hovered', () => {
23
+ expect(connectedHighlights(views, 'lgv1', true).map(r => r.start)).toEqual([
24
+ 100, 500,
25
+ ]);
26
+ });
27
+ it('draws a codon two views both highlight once', () => {
28
+ const both = [msaView('lgv1', 100, 200), msaView('lgv1', 100, 200)];
29
+ expect(connectedHighlights(both, 'lgv1', false).map(r => r.start)).toEqual([
30
+ 100, 200,
31
+ ]);
32
+ });
33
+ it('returns nothing for an unlinked genome view', () => {
34
+ expect(connectedHighlights(views, 'lgv3', false)).toEqual([]);
35
+ });
36
+ });
@@ -6,7 +6,6 @@ export default function AddHighlightComponentsModelF(pluginManager) {
6
6
  pluginManager.addToExtensionPoint(
7
7
  // @ts-expect-error v4 hosts have no contributeToExtensionPoint
8
8
  'LinearGenomeView-TracksContainerComponent', (rest, { model }) => {
9
- // Quick check: don't add any components if no MSA view exists
10
9
  const { views } = getSession(model);
11
10
  const hasMsaView = views.some(v => isMsaView(v) && v.connectedViewId === model.id);
12
11
  if (!hasMsaView) {
@@ -13,8 +13,10 @@ export type EbiMsaAlgorithm = (typeof ebiMsaAlgorithms)[number];
13
13
  * page and the rows merged on the query (utils/browserAlign.ts), with the tree
14
14
  * built by react-msaview's neighbour joining. It is the aligner for a launch
15
15
  * that must not depend on EBI, and for one that wants to be quick -- a
16
- * hundred rows take a second or two against a Job Dispatcher queue that has
17
- * been measured at anything from ten seconds to fifteen minutes.
16
+ * hundred 400-residue rows take under a second, and a hundred 1400-residue
17
+ * rows eight, against a Job Dispatcher queue that has been measured at
18
+ * anything from ten seconds to fifteen minutes. Past the per-sequence limit in
19
+ * browserAlign.ts it refuses and names an EBI aligner instead.
18
20
  */
19
21
  export declare const msaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft", "browser"];
20
22
  export type MsaAlgorithm = (typeof msaAlgorithms)[number];
@@ -28,7 +30,6 @@ export declare const msaAlgorithmLabels: Record<MsaAlgorithm, string>;
28
30
  */
29
31
  export declare const blastDatabaseOptions: readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"];
30
32
  export type BlastDatabase = (typeof blastDatabaseOptions)[number];
31
- export declare const defaultBlastDatabase: BlastDatabase;
32
33
  export declare const searchPrograms: readonly ["blastp", "phmmer"];
33
34
  export type SearchProgram = (typeof searchPrograms)[number];
34
35
  export declare const searchProgramLabels: Record<SearchProgram, string>;
@@ -46,7 +47,6 @@ export declare const searchProgramLabels: Record<SearchProgram, string>;
46
47
  */
47
48
  export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot", "rp75", "rp55", "rp35", "rp15"];
48
49
  export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number];
49
- export declare const defaultPhmmerDatabase: PhmmerDatabase;
50
50
  export declare const defaultMaxHits = 100;
51
51
  export declare function snapBlastHitCount(maxHits: number): number;
52
52
  /**
@@ -17,8 +17,10 @@ export const ebiMsaAlgorithms = [
17
17
  * page and the rows merged on the query (utils/browserAlign.ts), with the tree
18
18
  * built by react-msaview's neighbour joining. It is the aligner for a launch
19
19
  * that must not depend on EBI, and for one that wants to be quick -- a
20
- * hundred rows take a second or two against a Job Dispatcher queue that has
21
- * been measured at anything from ten seconds to fifteen minutes.
20
+ * hundred 400-residue rows take under a second, and a hundred 1400-residue
21
+ * rows eight, against a Job Dispatcher queue that has been measured at
22
+ * anything from ten seconds to fifteen minutes. Past the per-sequence limit in
23
+ * browserAlign.ts it refuses and names an EBI aligner instead.
22
24
  */
23
25
  export const msaAlgorithms = [...ebiMsaAlgorithms, 'browser'];
24
26
  export const msaAlgorithmLabels = {
@@ -43,7 +45,7 @@ export const blastDatabaseOptions = [
43
45
  ];
44
46
  // curated, so it returns roughly one good sequence per species rather than the
45
47
  // many near-identical TrEMBL entries an alignment reads poorly
46
- export const defaultBlastDatabase = 'uniprotkb_swissprot';
48
+ const defaultBlastDatabase = 'uniprotkb_swissprot';
47
49
  export const searchPrograms = ['blastp', 'phmmer'];
48
50
  export const searchProgramLabels = {
49
51
  blastp: 'blastp (sequence search)',
@@ -70,7 +72,7 @@ export const phmmerDatabaseOptions = [
70
72
  'rp35',
71
73
  'rp15',
72
74
  ];
73
- export const defaultPhmmerDatabase = 'swissprot';
75
+ const defaultPhmmerDatabase = 'swissprot';
74
76
  /**
75
77
  * The hit counts EBI's ncbiblast accepts for `alignments` and `scores`. A value
76
78
  * off this list is a 400 at submit time, so a request is rounded up to the next
@@ -1,6 +1,4 @@
1
1
  import type { MsaAlgorithm, SearchChoice } from './consts';
2
- export declare const SEARCH_CHOICE_STORAGE_KEY = "msaView-blastSearch";
3
- export declare const MSA_ALGORITHM_STORAGE_KEY = "msaView-msaAlgorithm";
4
2
  export declare function validSearchChoice(stored: unknown): SearchChoice;
5
3
  export declare function validMsaAlgorithm(stored: unknown): MsaAlgorithm;
6
4
  export declare function useStoredSearchChoice(): readonly [SearchChoice, (choice: SearchChoice) => void];
@@ -8,8 +8,8 @@ import { databaseOptionsFor, defaultSearchFor, msaAlgorithms, searchPrograms, }
8
8
  // comes back is checked against the current options before it is used — an
9
9
  // option this plugin has since dropped would otherwise reach EBI and come back
10
10
  // a 400, minutes after the user pressed Submit.
11
- export const SEARCH_CHOICE_STORAGE_KEY = 'msaView-blastSearch';
12
- export const MSA_ALGORITHM_STORAGE_KEY = 'msaView-msaAlgorithm';
11
+ const SEARCH_CHOICE_STORAGE_KEY = 'msaView-blastSearch';
12
+ const MSA_ALGORITHM_STORAGE_KEY = 'msaView-msaAlgorithm';
13
13
  const defaultSearch = defaultSearchFor('blastp');
14
14
  const defaultMsaAlgorithm = 'clustalo';
15
15
  export function validSearchChoice(stored) {
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import type { OrthologSource } from '../../../MsaViewPanel/model';
3
- export declare const orthologSourceLabels: Record<OrthologSource, string>;
4
3
  export declare function validOrthologSource(stored: unknown): OrthologSource;
5
4
  export declare function useStoredOrthologSource(): readonly ["ncbi" | "panther" | "uniref", (source: OrthologSource) => void];
6
5
  export default function OrthologSourceSelect({ value, onChange, className, }: {
@@ -3,7 +3,7 @@ import { MenuItem } from '@mui/material';
3
3
  import TextField2 from '../../../components/TextField2';
4
4
  import { useLocalStorage } from '../../../utils/useLocalStorage';
5
5
  const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
6
- export const orthologSourceLabels = {
6
+ const orthologSourceLabels = {
7
7
  ncbi: 'NCBI orthologs',
8
8
  panther: 'PANTHER',
9
9
  uniref: 'UniRef cluster',
@@ -3,12 +3,12 @@ import TextField2 from '../../../components/TextField2';
3
3
  import { resolveAssemblySpecies, resolveTaxId, } from '../../../utils/ncbiTaxonomy';
4
4
  import { fetchTaxonomyInfo } from '../../../utils/taxonomyNames';
5
5
  import { useDebounced, useFetch } from '../../../utils/useFetch';
6
- async function describeTaxon(query) {
7
- const taxId = await resolveTaxId(query);
6
+ async function describeTaxon(query, signal) {
7
+ const taxId = await resolveTaxId(query, signal);
8
8
  if (!taxId) {
9
9
  throw new Error(`No NCBI taxon matches "${query}"`);
10
10
  }
11
- const info = (await fetchTaxonomyInfo([taxId])).get(taxId);
11
+ const info = (await fetchTaxonomyInfo([taxId], signal)).get(taxId);
12
12
  const label = [info?.sciname, info?.commonName && `(${info.commonName})`]
13
13
  .filter(Boolean)
14
14
  .join(' ');
@@ -40,14 +40,14 @@ export default function QuerySpeciesSelect({ value, assemblyName, onChange, clas
40
40
  // reported the throttle as "blocked by CORS policy" in the helper text.
41
41
  const { data: fromAssembly } = useFetch(assemblyName && typed === undefined
42
42
  ? [assemblyName, 'assembly-species']
43
- : null, () => resolveAssemblySpecies(assemblyName), {
43
+ : null, signal => resolveAssemblySpecies(assemblyName, signal), {
44
44
  onSuccess: found => {
45
45
  if (found) {
46
46
  onChange(found.taxId);
47
47
  }
48
48
  },
49
49
  });
50
- const { data: fromText, error } = useFetch(debounced?.trim() ? [debounced.trim(), 'taxon'] : null, () => describeTaxon(debounced), {
50
+ const { data: fromText, error } = useFetch(debounced?.trim() ? [debounced.trim(), 'taxon'] : null, signal => describeTaxon(debounced, signal), {
51
51
  onSuccess: ({ taxId }) => {
52
52
  onChange(taxId);
53
53
  },
@@ -7,12 +7,6 @@ export declare function calculateProteinSequence({ cds, sequence, geneticCodeId,
7
7
  geneticCodeId?: number;
8
8
  translExcept?: TranslExcept[];
9
9
  }): string;
10
- export declare function revlist(list: Feat[], seqlen: number): {
11
- start: number;
12
- end: number;
13
- type?: string;
14
- phase?: number;
15
- }[];
16
10
  /**
17
11
  * The translation core's own feature panel shows: the contig's or the
18
12
  * feature's genetic code, its alternative initiators, and any `transl_except`
@@ -24,7 +24,7 @@ function cdsId(feat) {
24
24
  function dedupe(list) {
25
25
  return list.filter((item, pos, ary) => !pos || cdsId(item) !== cdsId(ary[pos - 1]));
26
26
  }
27
- export function revlist(list, seqlen) {
27
+ function revlist(list, seqlen) {
28
28
  return list
29
29
  .map(sub => ({
30
30
  ...sub,
@@ -4,6 +4,3 @@ export interface Feat {
4
4
  type?: string;
5
5
  phase?: number;
6
6
  }
7
- export interface SeqState {
8
- seq: string;
9
- }
@@ -7,7 +7,6 @@ export declare function getTranscriptLength(feature: Feature): {
7
7
  mod: number;
8
8
  };
9
9
  export declare function getId(val?: Feature): string;
10
- export declare function getMatchableIds(val?: Feature): string[];
11
10
  export declare function featureMatchesId(feature: Feature, id: string): boolean;
12
11
  export declare function getTranscriptDisplayName(val?: Feature): string;
13
12
  export declare function getGeneDisplayName(val?: Feature): string;
@@ -39,7 +39,7 @@ export function getTranscriptLength(feature) {
39
39
  export function getId(val) {
40
40
  return val?.id() ?? '';
41
41
  }
42
- export function getMatchableIds(val) {
42
+ function getMatchableIds(val) {
43
43
  return val
44
44
  ? uniqueDefined([
45
45
  val.id(),
@@ -1,20 +1,11 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
2
  export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
3
3
  /**
4
- * Keep IndexedDB holding what the view holds. The first run writes a new row and
5
- * records its id in the session snapshot; later runs update that row, because
6
- * the alignment keeps changing after it arrives — react-msaview's
7
- * "calculate neighbor-joining tree" replaces `data.tree` — and a session
8
- * reopened against a stale row restores the wrong picture.
9
- *
10
- * `lastStoredData` is what makes that safe to run on every data change: it is
11
- * the only thing separating "this is new" from "this is what we just wrote", and
12
- * it is recorded whether or not the write succeeded, so a browser refusing
13
- * IndexedDB (private mode) fails once rather than in a loop.
14
- *
15
- * A view whose data comes from a filehandle -- or from the indexed block its
16
- * kept `init` names -- stores nothing at all: the file is the source of truth
17
- * and it is refetched at startup.
4
+ * Keep IndexedDB holding `unsavedDocuments`. A view writes in place only to a
5
+ * row it created (`ownsDataStoreRow`), and never deletes one, since other
6
+ * views may name it: an emptied set drops the id and cleanupOldData ages the
7
+ * row out. `lastStoredData` is recorded even when a write fails, so a browser
8
+ * refusing IndexedDB fails once rather than in a loop.
18
9
  */
19
10
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
20
11
  /**
@@ -8,68 +8,70 @@ import { loadProteinDomains } from './loadProteinDomains';
8
8
  import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
9
9
  import { runLaunch } from './runLaunch';
10
10
  import { getProteinViews } from './structureConnection';
11
+ import { proteinPositionsInRange } from './transcriptMap';
11
12
  import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
12
13
  const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
13
14
  const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
14
15
  const START_OVER = ' Relaunch it from the gene to rebuild it.';
16
+ const EXPIRED_EXTRAS_WARNING = 'Part of this view is no longer in browser storage: annotations, a tree or row metadata too large for the session. Stored documents are kept for 7 days after they were last used, and are lost when site data is cleared.';
15
17
  export function loadStoredData(self) {
16
- const { dataStoreId, rows } = self;
17
- if (dataStoreId && rows.length === 0) {
18
- void (async () => {
19
- try {
20
- self.setLoadingStoredData(true);
21
- const storedData = await retrieveMsaData(dataStoreId);
22
- if (storedData) {
23
- // one transaction, so storeDataToIndexedDB sees the restored data and
24
- // the record of it together and does not write it straight back
25
- transaction(() => {
26
- if (storedData.msa) {
27
- self.setMSA(storedData.msa);
28
- }
29
- if (storedData.tree) {
30
- self.setTree(storedData.tree);
31
- }
32
- if (storedData.treeMetadata) {
33
- self.setTreeMetadata(storedData.treeMetadata);
34
- }
35
- if (storedData.gff) {
36
- self.setGFF(storedData.gff);
37
- }
38
- self.setLastStoredData(currentData(self));
39
- });
18
+ const { dataStoreId } = self;
19
+ if (!dataStoreId) {
20
+ return;
21
+ }
22
+ void (async () => {
23
+ try {
24
+ self.setLoadingStoredData(true);
25
+ const stored = await retrieveMsaData(dataStoreId);
26
+ if (self.dataStoreId !== dataStoreId) {
27
+ return;
28
+ }
29
+ transaction(() => {
30
+ if (stored) {
31
+ const { data } = self;
32
+ if (stored.msa && !data.msa) {
33
+ self.setMSA(stored.msa);
34
+ }
35
+ if (stored.tree && !data.tree) {
36
+ self.setTree(stored.tree);
37
+ }
38
+ if (stored.treeMetadata && !data.treeMetadata) {
39
+ self.setTreeMetadata(stored.treeMetadata);
40
+ }
41
+ if (stored.gff && !data.gff) {
42
+ self.setGFF(stored.gff);
43
+ }
44
+ self.setLastStoredData(stored);
40
45
  }
41
46
  else {
42
- // the id names nothing, so clearing it is what lets react-msaview's
43
- // "Return to import form" actually return instead of landing back
44
- // here. The request that built the alignment is kept, though: it is
45
- // the whole of a retry, and dropping it left the user reading an
46
- // apology with nothing to press.
47
- transaction(() => {
48
- self.setDataStoreId(undefined);
47
+ self.setDataStoreId(undefined);
48
+ if (hasAlignmentSource(self)) {
49
+ self.addWarning(EXPIRED_EXTRAS_WARNING);
50
+ }
51
+ else {
49
52
  self.setError(new Error(EXPIRED_MESSAGE +
50
53
  ((self.blastParams ?? self.orthologParams)
51
54
  ? RELAUNCHABLE
52
55
  : START_OVER)));
53
- });
56
+ }
54
57
  }
55
- }
56
- catch (e) {
57
- console.error('Failed to load MSA data from IndexedDB:', e);
58
- }
59
- finally {
60
- self.setLoadingStoredData(false);
61
- }
62
- })();
63
- }
58
+ });
59
+ }
60
+ catch (e) {
61
+ console.error('Failed to load MSA data from IndexedDB:', e);
62
+ }
63
+ finally {
64
+ self.setLoadingStoredData(false);
65
+ }
66
+ })();
64
67
  }
65
- /**
66
- * Everything react-msaview drops from the snapshot once it passes 50kB. The
67
- * GFF is among them: a local one's filehandle is cleared once it loads, so
68
- * this row is the only copy.
69
- */
70
- function currentData(self) {
71
- const { msa, tree, treeMetadata, gff } = self.data;
72
- return { msa, tree, treeMetadata, gff };
68
+ function hasAlignmentSource(self) {
69
+ return !!(self.data.msa ||
70
+ self.msaFilehandle ||
71
+ self.init?.msaIndexedLocation);
72
+ }
73
+ function isEmpty(data) {
74
+ return !(data.msa || data.tree || data.treeMetadata || data.gff);
73
75
  }
74
76
  function sameData(a, b) {
75
77
  return (!!a &&
@@ -79,44 +81,37 @@ function sameData(a, b) {
79
81
  a.gff === b.gff);
80
82
  }
81
83
  /**
82
- * Keep IndexedDB holding what the view holds. The first run writes a new row and
83
- * records its id in the session snapshot; later runs update that row, because
84
- * the alignment keeps changing after it arrives — react-msaview's
85
- * "calculate neighbor-joining tree" replaces `data.tree` — and a session
86
- * reopened against a stale row restores the wrong picture.
87
- *
88
- * `lastStoredData` is what makes that safe to run on every data change: it is
89
- * the only thing separating "this is new" from "this is what we just wrote", and
90
- * it is recorded whether or not the write succeeded, so a browser refusing
91
- * IndexedDB (private mode) fails once rather than in a loop.
92
- *
93
- * A view whose data comes from a filehandle -- or from the indexed block its
94
- * kept `init` names -- stores nothing at all: the file is the source of truth
95
- * and it is refetched at startup.
84
+ * Keep IndexedDB holding `unsavedDocuments`. A view writes in place only to a
85
+ * row it created (`ownsDataStoreRow`), and never deletes one, since other
86
+ * views may name it: an emptied set drops the id and cleanupOldData ages the
87
+ * row out. `lastStoredData` is recorded even when a write fails, so a browser
88
+ * refusing IndexedDB fails once rather than in a loop.
96
89
  */
97
90
  export function storeDataToIndexedDB(self) {
98
- const { rows, dataStoreId, isStoringData, lastStoredData, init } = self;
99
- const data = currentData(self);
100
- if (rows.length === 0 ||
101
- isStoringData ||
102
- self.msaFilehandle ||
103
- self.treeFilehandle ||
104
- // an indexed view keeps its init and refetches the block, so a row here
105
- // would be one nothing ever reads
106
- !!init?.msaIndexedLocation ||
107
- !(data.msa || data.tree) ||
108
- sameData(lastStoredData, data)) {
91
+ const { dataStoreId, ownsDataStoreRow, isStoringData, loadingStoredData, lastStoredData, } = self;
92
+ const data = self.unsavedDocuments;
93
+ if (isStoringData || loadingStoredData || sameData(lastStoredData, data)) {
94
+ return;
95
+ }
96
+ if (isEmpty(data)) {
97
+ if (lastStoredData) {
98
+ transaction(() => {
99
+ self.setDataStoreId(undefined);
100
+ self.setOwnsDataStoreRow(false);
101
+ self.setLastStoredData(data);
102
+ });
103
+ }
109
104
  return;
110
105
  }
111
- // mark as storing synchronously so re-runs of this autorun (e.g. when data
112
- // observables change while the write is pending) don't kick off a duplicate
113
- // write and leave an orphan IndexedDB entry
114
106
  self.setIsStoringData(true);
115
107
  void (async () => {
116
108
  try {
117
- const id = dataStoreId ?? generateDataStoreId();
109
+ const id = dataStoreId && ownsDataStoreRow ? dataStoreId : generateDataStoreId();
118
110
  if (await storeMsaData(id, data)) {
119
- self.setDataStoreId(id);
111
+ transaction(() => {
112
+ self.setDataStoreId(id);
113
+ self.setOwnsDataStoreRow(true);
114
+ });
120
115
  }
121
116
  }
122
117
  catch (e) {
@@ -297,19 +292,15 @@ function genomeHighlightsToVisibleColumns(self, field) {
297
292
  if (!transcriptToMsaMap || !hasQueryRow(self)) {
298
293
  return [];
299
294
  }
300
- const { g2p } = transcriptToMsaMap;
301
295
  const columns = new Set();
302
296
  for (const view of getProteinViews(getSession(self).views)) {
303
297
  for (const structure of view.structures) {
304
298
  if (structure.connectedViewId !== connectedViewId) {
305
299
  continue;
306
300
  }
307
- for (const highlight of structure[field] ?? []) {
308
- for (let coord = highlight.start; coord < highlight.end; coord++) {
309
- const proteinPos = g2p[coord];
310
- const col = proteinPos === undefined
311
- ? undefined
312
- : transcriptPosToVisibleCol(self, proteinPos);
301
+ for (const { start, end } of structure[field] ?? []) {
302
+ for (const proteinPos of proteinPositionsInRange(transcriptToMsaMap, start, end)) {
303
+ const col = transcriptPosToVisibleCol(self, proteinPos);
313
304
  if (col !== undefined) {
314
305
  columns.add(col);
315
306
  }
@@ -25,7 +25,7 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
25
25
  const expired = !!request && !!model.error && !model.dataInitialized;
26
26
  const launching = pending || expired || (!!init && !model.dataInitialized);
27
27
  return (React.createElement(ErrorBoundary, null,
28
- React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
28
+ React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData && !model.dataInitialized ? (React.createElement("div", { className: classes.loadingContainer },
29
29
  React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
30
30
  });
31
31
  export default MsaViewPanel;