jbrowse-plugin-msaview 3.3.0 → 3.4.1

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 (149) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  15. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  16. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
  17. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  18. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  19. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
  20. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  21. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  22. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  23. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  24. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  25. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  26. package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
  27. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  28. package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
  29. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
  30. package/dist/MsaViewPanel/afterCreateAutoruns.js +95 -72
  31. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  32. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  33. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  34. package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
  35. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  36. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  37. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
  38. package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
  39. package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
  40. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  41. package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
  42. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
  43. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  44. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  45. package/dist/MsaViewPanel/model.d.ts +190 -19
  46. package/dist/MsaViewPanel/model.js +51 -1
  47. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  48. package/dist/MsaViewPanel/msaDataStore.js +16 -7
  49. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  50. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  51. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  52. package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
  53. package/dist/MsaViewPanel/runLaunch.js +65 -0
  54. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  55. package/dist/MsaViewPanel/runLaunch.test.js +129 -0
  56. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/storedData.test.js +160 -0
  58. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  59. package/dist/MsaViewPanel/util.d.ts +18 -0
  60. package/dist/MsaViewPanel/util.js +17 -0
  61. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  62. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  63. package/dist/utils/blastCache.d.ts +10 -6
  64. package/dist/utils/blastCache.js +47 -3
  65. package/dist/utils/blastCache.test.d.ts +1 -0
  66. package/dist/utils/blastCache.test.js +72 -0
  67. package/dist/utils/ebiBlast.d.ts +5 -3
  68. package/dist/utils/ebiBlast.js +6 -4
  69. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  70. package/dist/utils/ebiJobDispatcher.js +13 -6
  71. package/dist/utils/fetch.d.ts +8 -1
  72. package/dist/utils/fetch.js +28 -2
  73. package/dist/utils/msa.d.ts +15 -1
  74. package/dist/utils/msa.js +40 -13
  75. package/dist/utils/msaRows.d.ts +31 -0
  76. package/dist/utils/msaRows.js +67 -0
  77. package/dist/utils/ncbiDomains.d.ts +2 -2
  78. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  79. package/dist/utils/ncbiOrthologs.js +26 -7
  80. package/dist/utils/ncbiOrthologs.test.js +23 -1
  81. package/dist/utils/pantherOrthologs.js +2 -10
  82. package/dist/utils/phmmer.d.ts +54 -0
  83. package/dist/utils/phmmer.js +120 -0
  84. package/dist/utils/poll.d.ts +6 -1
  85. package/dist/utils/poll.js +7 -2
  86. package/dist/utils/taxonomyNames.d.ts +1 -1
  87. package/dist/utils/taxonomyNames.js +6 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +30 -24
  91. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  92. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  93. package/src/AddHighlightModel/index.tsx +1 -1
  94. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
  95. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  96. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  97. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  98. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  99. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  100. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  101. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  102. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  103. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  104. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
  105. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  106. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  107. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  108. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  109. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  110. package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
  111. package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
  112. package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
  113. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  114. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  115. package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
  116. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
  117. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  118. package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
  119. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
  120. package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
  121. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  122. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  123. package/src/MsaViewPanel/model.ts +82 -6
  124. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  125. package/src/MsaViewPanel/msaDataStore.ts +22 -13
  126. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  127. package/src/MsaViewPanel/runLaunch.test.ts +154 -0
  128. package/src/MsaViewPanel/runLaunch.ts +102 -0
  129. package/src/MsaViewPanel/storedData.test.ts +196 -0
  130. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  131. package/src/MsaViewPanel/util.ts +18 -0
  132. package/src/utils/blastCache.test.ts +86 -0
  133. package/src/utils/blastCache.ts +67 -13
  134. package/src/utils/ebiBlast.ts +9 -3
  135. package/src/utils/ebiJobDispatcher.ts +18 -3
  136. package/src/utils/fetch.ts +29 -2
  137. package/src/utils/msa.ts +51 -12
  138. package/src/utils/msaRows.ts +95 -0
  139. package/src/utils/ncbiDomains.ts +4 -2
  140. package/src/utils/ncbiOrthologs.test.ts +25 -0
  141. package/src/utils/ncbiOrthologs.ts +27 -7
  142. package/src/utils/pantherOrthologs.ts +6 -11
  143. package/src/utils/phmmer.ts +178 -0
  144. package/src/utils/poll.ts +8 -1
  145. package/src/utils/taxonomyNames.ts +6 -1
  146. package/src/version.ts +1 -1
  147. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  148. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  149. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -53,6 +53,18 @@ function ncbiUrl(url) {
53
53
  const sep = url.includes('?') ? '&' : '?';
54
54
  return `${url}${sep}tool=${NCBI_TOOL}&email=${encodeURIComponent(NCBI_EMAIL)}`;
55
55
  }
56
+ /**
57
+ * A candidate gene reference as the ortholog services know it: a GFF ID prefix
58
+ * (gene:TP53) and a version suffix (NM_000546.6) stripped off. Shared with
59
+ * pantherOrthologs, which asks PANTHER the same question with the same
60
+ * candidates.
61
+ */
62
+ export function cleanGeneCandidate(raw) {
63
+ return raw
64
+ .trim()
65
+ .replace(/^\w+:/, '')
66
+ .replace(/\.\d+$/, '');
67
+ }
56
68
  /**
57
69
  * A free-text gene reference -> NCBI gene id. A bare number is taken as the id
58
70
  * itself; anything else is searched as a gene name within the query taxon.
@@ -69,8 +81,7 @@ export async function resolveGeneId(candidates, taxId) {
69
81
  if (/^\d+$/.test(query)) {
70
82
  return { geneId: query, matched: query };
71
83
  }
72
- // strip a version suffix (NM_000546.6) and any GFF ID prefix (gene:TP53)
73
- const cleaned = query.replace(/^\w+:/, '').replace(/\.\d+$/, '');
84
+ const cleaned = cleanGeneCandidate(query);
74
85
  const term = `${cleaned}[Gene Name] AND ${taxId}[taxid]`;
75
86
  const json = await jsonfetch(ncbiUrl(`${EUTILS}/esearch.fcgi?db=gene&term=${encodeURIComponent(term)}&retmode=json&retmax=1`));
76
87
  const geneId = json.esearchresult?.idlist?.[0];
@@ -179,15 +190,23 @@ function sanitize(name) {
179
190
  * Sanitized, unique single-token labels used identically in the FASTA headers,
180
191
  * the tree leaf names and the domain GFF seq_ids — that identity is how the
181
192
  * viewer pairs a tree leaf to its alignment row to its domain track. Collisions
182
- * get a numeric suffix rather than silently overwriting a row.
193
+ * get a numeric suffix rather than silently overwriting a row, and the suffix
194
+ * climbs past labels already emitted: a species genuinely named `human_2` would
195
+ * otherwise take the label the second `human` is about to get, merging two rows
196
+ * into one.
183
197
  */
184
198
  export function dedupeLabels(names) {
185
- const seen = new Map();
199
+ const used = new Set();
186
200
  return names.map(name => {
187
201
  const base = sanitize(name) || 'row';
188
- const n = seen.get(base) ?? 0;
189
- seen.set(base, n + 1);
190
- return n === 0 ? base : `${base}_${n + 1}`;
202
+ let label = base;
203
+ let n = 1;
204
+ while (used.has(label)) {
205
+ n += 1;
206
+ label = `${base}_${n}`;
207
+ }
208
+ used.add(label);
209
+ return label;
191
210
  });
192
211
  }
193
212
  /**
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, test, vi } from 'vitest';
2
- import { dedupeLabels, fetchOrthologGenes, fetchRepresentativeProteins, parseFasta, } from './ncbiOrthologs';
2
+ import { cleanGeneCandidate, dedupeLabels, fetchOrthologGenes, fetchRepresentativeProteins, parseFasta, } from './ncbiOrthologs';
3
3
  describe('dedupeLabels', () => {
4
4
  test('sanitizes to single tokens', () => {
5
5
  // labels are used identically as FASTA headers, Newick leaf names and GFF
@@ -22,6 +22,28 @@ describe('dedupeLabels', () => {
22
22
  test('falls back for a name with no usable characters', () => {
23
23
  expect(dedupeLabels(['...', '...'])).toEqual(['row', 'row_2']);
24
24
  });
25
+ test('climbs past a suffix a literal name already took', () => {
26
+ // a label pairs a tree leaf to its alignment row, so handing the same one
27
+ // to two rows merges two species into one silently. The third label is ugly
28
+ // and unique, which is the trade
29
+ expect(dedupeLabels(['human', 'human', 'human_2'])).toEqual([
30
+ 'human',
31
+ 'human_2',
32
+ 'human_2_2',
33
+ ]);
34
+ expect(dedupeLabels(['human_2', 'human', 'human'])).toEqual([
35
+ 'human_2',
36
+ 'human',
37
+ 'human_3',
38
+ ]);
39
+ });
40
+ });
41
+ describe('cleanGeneCandidate', () => {
42
+ test('strips what a GFF feature carries that a gene lookup does not want', () => {
43
+ expect(cleanGeneCandidate(' gene:TP53 ')).toBe('TP53');
44
+ expect(cleanGeneCandidate('NM_000546.6')).toBe('NM_000546');
45
+ expect(cleanGeneCandidate('TP53')).toBe('TP53');
46
+ });
25
47
  });
26
48
  describe('parseFasta', () => {
27
49
  test('keys by the first header token and joins wrapped lines', () => {
@@ -16,7 +16,7 @@
16
16
  // GenPept records with CDD Region features, so the overlay attaches as it does
17
17
  // to a RefSeq accession.
18
18
  import { jsonfetch } from './fetch';
19
- import { dedupeLabels, defaultMaxSpecies } from './ncbiOrthologs';
19
+ import { cleanGeneCandidate, dedupeLabels, defaultMaxSpecies, } from './ncbiOrthologs';
20
20
  import { fetchTaxonomyInfo } from './taxonomyNames';
21
21
  const PANTHER = 'https://pantherdb.org/services/oai/pantherdb';
22
22
  const UNIPROT = 'https://rest.uniprot.org/uniprotkb';
@@ -122,14 +122,6 @@ async function fetchSequences(accessions) {
122
122
  }
123
123
  return map;
124
124
  }
125
- // strip a version suffix (NM_000546.6) and any GFF ID prefix (gene:TP53), as
126
- // resolveGeneId does for NCBI
127
- function cleanCandidate(raw) {
128
- return raw
129
- .trim()
130
- .replace(/^\w+:/, '')
131
- .replace(/\.\d+$/, '');
132
- }
133
125
  /**
134
126
  * One `matchortho` per candidate until PANTHER maps one. A JBrowse feature
135
127
  * carries whatever its GFF/BigBed had — `id()`, `name`, `gene_name` — and only
@@ -139,7 +131,7 @@ function cleanCandidate(raw) {
139
131
  async function matchOrthologs(candidates, taxId, targets) {
140
132
  let matched;
141
133
  for (const raw of candidates) {
142
- const query = cleanCandidate(raw);
134
+ const query = cleanGeneCandidate(raw);
143
135
  if (!query) {
144
136
  continue;
145
137
  }
@@ -0,0 +1,54 @@
1
+ import type { PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts';
2
+ export interface PhmmerRow {
3
+ accession: string;
4
+ id: string;
5
+ sciname: string;
6
+ taxid?: number;
7
+ title?: string;
8
+ /** the matched envelope on the target, e.g. '503-912', absent if unparseable */
9
+ range?: string;
10
+ /** the row as phmmer aligned it, uppercased with '.' inserts turned into '-' */
11
+ aligned: string;
12
+ }
13
+ export interface PhmmerAlignment {
14
+ rows: PhmmerRow[];
15
+ /**
16
+ * the query, placed into the same columns. phmmer does not put the query in
17
+ * its own output, so this is derived — see buildQueryRow.
18
+ */
19
+ queryRow: string;
20
+ }
21
+ /**
22
+ * Human-facing link to a job, shown while it runs and on error.
23
+ *
24
+ * The category has to be sss: jdispatcher serves its shell with a 200 for any
25
+ * category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
26
+ * in a browser.
27
+ */
28
+ export declare function phmmerResultUrl(jobId: string): string;
29
+ /** EBI job ids are prefixed with the tool that made them */
30
+ export declare function isPhmmerJobId(jobId: string): boolean;
31
+ /**
32
+ * Exported for testing against a captured .sto — the annotation names (RF, the
33
+ * DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
34
+ * would notice if HMMER or EBI changed one.
35
+ */
36
+ export declare function parsePhmmerAlignment({ stockholm, query, }: {
37
+ stockholm: string;
38
+ query: string;
39
+ }): PhmmerAlignment;
40
+ export declare function queryPhmmer({ query, database, onProgress, onRid, signal, }: {
41
+ query: string;
42
+ database: PhmmerDatabase;
43
+ onProgress: (arg: string) => void;
44
+ onRid: (arg: string) => void;
45
+ signal?: AbortSignal;
46
+ }): Promise<{
47
+ rows: PhmmerRow[];
48
+ /**
49
+ * the query, placed into the same columns. phmmer does not put the query in
50
+ * its own output, so this is derived — see buildQueryRow.
51
+ */
52
+ queryRow: string;
53
+ rid: string;
54
+ }>;
@@ -0,0 +1,120 @@
1
+ import { StockholmMSA } from 'msa-parsers';
2
+ import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher';
3
+ const TOOL = 'hmmer3_phmmer';
4
+ /**
5
+ * Human-facing link to a job, shown while it runs and on error.
6
+ *
7
+ * The category has to be sss: jdispatcher serves its shell with a 200 for any
8
+ * category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
9
+ * in a browser.
10
+ */
11
+ export function phmmerResultUrl(jobId) {
12
+ return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`;
13
+ }
14
+ /** EBI job ids are prefixed with the tool that made them */
15
+ export function isPhmmerJobId(jobId) {
16
+ return jobId.startsWith(`${TOOL}-`);
17
+ }
18
+ /**
19
+ * phmmer aligns every hit to a profile built from the query, one match state
20
+ * per query residue, and marks those columns 'x' in #=GC RF. So the query's own
21
+ * row is exactly recoverable: walk RF, consume a query residue at each match
22
+ * column, gap everywhere else.
23
+ *
24
+ * This is the one piece of real logic here rather than a library call, and it
25
+ * is checked hard: if the match columns do not account for the query exactly,
26
+ * the columns and the query have drifted apart, and a query row that is off by
27
+ * even one residue would silently mis-map every column to the genome. Throwing
28
+ * is much better than drawing that.
29
+ */
30
+ function buildQueryRow({ rf, query }) {
31
+ let consumed = 0;
32
+ const row = Array.from(rf, c => c === 'x' ? (query[consumed++] ?? '-') : '-').join('');
33
+ if (consumed !== query.length) {
34
+ throw new Error(`phmmer alignment has ${consumed} match columns for a query of ${query.length} residues, so the query row cannot be placed`);
35
+ }
36
+ return row;
37
+ }
38
+ /**
39
+ * '[subseq from] Albumin OS=Homo sapiens OX=9606 GN=ALB PE=1 SV=2' is what a
40
+ * UniProt target's #=GS DE looks like. Hits from the non-UniProt databases
41
+ * phmmer also offers (PDB, AlphaFold, MEROPS...) carry no OS=/OX= at all, so
42
+ * every field here is optional.
43
+ */
44
+ function parseDescription(de) {
45
+ const text = (de ?? '').replace('[subseq from] ', '');
46
+ const sciname = /OS=(.*?)\s+(?:OX|GN|PE|SV)=/.exec(text)?.[1];
47
+ const ox = /OX=(\d+)/.exec(text)?.[1];
48
+ return {
49
+ sciname: sciname ?? 'unknown',
50
+ taxid: ox ? Number.parseInt(ox, 10) : undefined,
51
+ title: text.split(' OS=')[0] || undefined,
52
+ };
53
+ }
54
+ /**
55
+ * Target names look like 'sp|P02768|ALBU_HUMAN/1-609' for UniProt databases and
56
+ * like anything at all for the others, so an unrecognized name becomes its own
57
+ * accession rather than being dropped.
58
+ */
59
+ function parseName(name) {
60
+ const slash = name.lastIndexOf('/');
61
+ const range = slash === -1 ? undefined : /^\d+-\d+$/.exec(name.slice(slash + 1))?.[0];
62
+ const bare = range === undefined ? name : name.slice(0, slash);
63
+ const parts = bare.split('|');
64
+ return parts.length === 3
65
+ ? { accession: parts[1], id: parts[2], range }
66
+ : { accession: bare, id: bare, range };
67
+ }
68
+ /**
69
+ * Exported for testing against a captured .sto — the annotation names (RF, the
70
+ * DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
71
+ * would notice if HMMER or EBI changed one.
72
+ */
73
+ export function parsePhmmerAlignment({ stockholm, query, }) {
74
+ const { gc, gs, seqdata, seqname } = new StockholmMSA(stockholm, 0).getMSA();
75
+ const rf = gc.RF;
76
+ if (!rf) {
77
+ throw new Error('phmmer alignment has no #=GC RF line');
78
+ }
79
+ return {
80
+ queryRow: buildQueryRow({ rf, query }),
81
+ rows: seqname.map(name => ({
82
+ ...parseName(name),
83
+ ...parseDescription(gs.DE?.[name]?.[0]),
84
+ // insert columns come back lowercase with '.' for gaps; the MSA renderer
85
+ // looks colors up by the literal letter, so lowercase would draw
86
+ // uncolored. The insert columns stay visible as gaps in the query row.
87
+ aligned: (seqdata[name] ?? '').replaceAll('.', '-').toUpperCase(),
88
+ })),
89
+ };
90
+ }
91
+ export async function queryPhmmer({ query, database, onProgress, onRid, signal, }) {
92
+ onProgress('Submitting to EBI phmmer...');
93
+ const jobId = await submitEbiJob({
94
+ tool: TOOL,
95
+ params: {
96
+ database,
97
+ sequence: query,
98
+ // the alignment is the whole point of using phmmer here
99
+ alignView: 'true',
100
+ },
101
+ signal,
102
+ });
103
+ onRid(jobId);
104
+ await waitForEbiJob({
105
+ tool: TOOL,
106
+ jobId,
107
+ signal,
108
+ onCountdown: s => {
109
+ onProgress(`Re-checking phmmer status in... ${s}`);
110
+ },
111
+ });
112
+ const alignment = parsePhmmerAlignment({
113
+ stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto', signal }),
114
+ query,
115
+ });
116
+ if (alignment.rows.length === 0) {
117
+ throw new Error('No hits found');
118
+ }
119
+ return { rid: jobId, ...alignment };
120
+ }
@@ -3,9 +3,14 @@
3
3
  * false when still pending, and throws on failure. Between checks it counts down
4
4
  * `intervalSeconds`, calling `onCountdown` each second so the UI can show
5
5
  * progress.
6
+ *
7
+ * `signal` ends the loop with an AbortError: the countdown rejects on the tick
8
+ * it is aborted rather than at the end of the interval, so a cancelled launch
9
+ * stops within a second instead of running for the job's lifetime.
6
10
  */
7
- export declare function pollLoop({ check, intervalSeconds, onCountdown, }: {
11
+ export declare function pollLoop({ check, intervalSeconds, onCountdown, signal, }: {
8
12
  check: () => Promise<boolean>;
9
13
  intervalSeconds: number;
10
14
  onCountdown: (secondsRemaining: number) => void;
15
+ signal?: AbortSignal;
11
16
  }): Promise<void>;
@@ -4,16 +4,21 @@ import { timeout } from './fetch';
4
4
  * false when still pending, and throws on failure. Between checks it counts down
5
5
  * `intervalSeconds`, calling `onCountdown` each second so the UI can show
6
6
  * progress.
7
+ *
8
+ * `signal` ends the loop with an AbortError: the countdown rejects on the tick
9
+ * it is aborted rather than at the end of the interval, so a cancelled launch
10
+ * stops within a second instead of running for the job's lifetime.
7
11
  */
8
- export async function pollLoop({ check, intervalSeconds, onCountdown, }) {
12
+ export async function pollLoop({ check, intervalSeconds, onCountdown, signal, }) {
9
13
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
10
14
  while (true) {
15
+ signal?.throwIfAborted();
11
16
  if (await check()) {
12
17
  return;
13
18
  }
14
19
  for (let i = intervalSeconds; i > 0; i--) {
15
20
  onCountdown(i);
16
- await timeout(1000);
21
+ await timeout(1000, signal);
17
22
  }
18
23
  }
19
24
  }
@@ -2,4 +2,4 @@ export interface TaxonomyInfo {
2
2
  sciname: string;
3
3
  commonName?: string;
4
4
  }
5
- export declare function fetchTaxonomyInfo(taxids: number[]): Promise<Map<number, TaxonomyInfo>>;
5
+ export declare function fetchTaxonomyInfo(taxidsWithRepeats: number[]): Promise<Map<number, TaxonomyInfo>>;
@@ -25,7 +25,12 @@ async function saveTaxonomyCache(entries) {
25
25
  }
26
26
  await tx.done;
27
27
  }
28
- export async function fetchTaxonomyInfo(taxids) {
28
+ export async function fetchTaxonomyInfo(taxidsWithRepeats) {
29
+ // callers pass one taxid per alignment row, and a BLAST hit list is several
30
+ // rows per species: 100 albumin hits are maybe 50 taxa, and asking as they
31
+ // came did 100 IndexedDB reads and sent eutils 100 ids for 50 answers. The
32
+ // result is keyed by taxid, so no caller can tell the difference
33
+ const taxids = [...new Set(taxidsWithRepeats)];
29
34
  const result = new Map();
30
35
  const uncachedTaxids = [];
31
36
  const cachedResults = await getCachedTaxonomies(taxids);
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.3.0";
1
+ export declare const version = "3.4.1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '3.3.0';
1
+ export const version = '3.4.1';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.3.0",
2
+ "version": "3.4.1",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -17,8 +17,8 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@emotion/styled": "^11.14.1",
20
- "@gmod/bgzf-filehandle": "^6.2.0",
21
- "g2p_mapper": "^2.1.5",
20
+ "@gmod/bgzf-filehandle": "^6.6.0",
21
+ "g2p_mapper": "^2.1.7",
22
22
  "idb": "^8.0.3"
23
23
  },
24
24
  "devDependencies": {
@@ -28,51 +28,57 @@
28
28
  "@jbrowse/core": "^4.3.0",
29
29
  "@jbrowse/mobx-state-tree": "^5.13.0",
30
30
  "@jbrowse/plugin-linear-genome-view": "^4.3.0",
31
- "@mui/icons-material": "^9.2.0",
32
- "@mui/material": "^9.2.0",
33
- "@mui/system": "^9.2.0",
34
- "@mui/x-data-grid": "^9.10.0",
31
+ "@mui/icons-material": "^9.4.0",
32
+ "@mui/material": "^9.4.0",
33
+ "@mui/system": "^9.4.0",
34
+ "@mui/x-data-grid": "^9.12.0",
35
35
  "@testing-library/dom": "^10.4.1",
36
- "@testing-library/react": "^16.3.2",
37
- "@types/node": "^26.1.1",
38
- "@types/react": "^19.2.17",
39
- "esbuild": "^0.28.1",
40
- "eslint": "^10.7.0",
36
+ "@testing-library/react": "^16.3.3",
37
+ "@types/node": "^26.4.1",
38
+ "@types/react": "^19.2.18",
39
+ "esbuild": "^0.28.2",
40
+ "eslint": "^10.9.1",
41
41
  "eslint-plugin-import-x": "^4.17.1",
42
42
  "eslint-plugin-react": "^7.37.5",
43
43
  "eslint-plugin-react-hooks": "^7.1.1",
44
- "eslint-plugin-unicorn": "^72.0.0",
44
+ "eslint-plugin-unicorn": "^74.0.0",
45
45
  "git-cliff": "^2.13.1",
46
46
  "jsdom": "^30.0.1",
47
47
  "mobx": "^6.16.1",
48
48
  "mobx-react": "^9.2.2",
49
- "msa-parsers": "^6.0.0",
49
+ "msa-parsers": "^6.4.2",
50
+ "oxfmt": "^0.65.0",
51
+ "oxlint": "^1.81.0",
52
+ "oxlint-tsgolint": "^7.0.2001",
50
53
  "pixelmatch": "^7.2.0",
51
54
  "pngjs": "^7.0.0",
52
- "prettier": "^3.9.6",
53
- "pretty-bytes": "^7.1.1",
54
- "puppeteer": "^25.3.0",
55
+ "pretty-bytes": "^7.1.2",
56
+ "puppeteer": "^25.9.0",
55
57
  "react": "^19.2.8",
56
58
  "react-dom": "^19.2.8",
57
- "react-msaview": "^6.0.0",
59
+ "react-msaview": "^6.4.2",
58
60
  "rimraf": "^6.1.3",
59
61
  "rxjs": "^7.8.2",
60
62
  "serve": "^14.2.6",
61
63
  "tss-react": "^4.9.21",
62
- "typescript": "^6.0.2",
63
- "typescript-eslint": "^8.65.0",
64
- "vitest": "^4.1.10"
64
+ "typescript": "^6.0.3",
65
+ "typescript-eslint": "^8.69.0",
66
+ "vitest": "^4.1.11"
65
67
  },
66
68
  "scripts": {
67
69
  "clean": "rimraf dist",
68
70
  "start": "node esbuild.mjs --watch",
69
- "format": "pnpm prettier --write .",
71
+ "format": "oxfmt",
72
+ "check-format": "oxfmt --check",
70
73
  "build": "tsc && NODE_ENV=production node esbuild.mjs && cp distconfig.json dist/config.json",
71
74
  "prebuild": "pnpm clean",
72
- "lint": "eslint src --report-unused-disable-directives --max-warnings 0",
75
+ "lint": "oxlint --type-aware --deny-warnings",
76
+ "lint:fast": "oxlint --deny-warnings",
77
+ "lint:eslint": "eslint src --report-unused-disable-directives --max-warnings 0",
73
78
  "check-text-source": "node scripts/check-text-source.mjs",
74
79
  "check-ebi-params": "node scripts/check-ebi-params.mjs",
75
80
  "check-mui-imports": "node scripts/check-mui-imports.mjs",
81
+ "check-host-dep-pins": "node scripts/check-host-dep-pins.mjs",
76
82
  "pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly",
77
83
  "test": "vitest run",
78
84
  "test:watch": "vitest",
@@ -82,7 +88,7 @@
82
88
  "test:version": "node scripts/test-versions.mjs run",
83
89
  "host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-msaview.umd.production.min.js",
84
90
  "check-ci": "node scripts/require-green-ci.mjs",
85
- "preversion": "pnpm check-ci && pnpm lint && pnpm build && pnpm host-compat",
91
+ "preversion": "pnpm check-ci && pnpm check-host-dep-pins && pnpm lint && pnpm build && pnpm host-compat",
86
92
  "version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add src/version.ts CHANGELOG.md",
87
93
  "postversion": "git push --follow-tags"
88
94
  }
@@ -3,8 +3,8 @@ import React from 'react'
3
3
  import { getSession } from '@jbrowse/core/util'
4
4
  import { observer } from 'mobx-react'
5
5
 
6
- import { hasHoverPosition, useStyles } from './util'
7
6
  import { isMsaView } from '../MsaViewPanel/model'
7
+ import { hasHoverPosition, useStyles } from './util'
8
8
 
9
9
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
10
10
 
@@ -3,8 +3,8 @@ import React from 'react'
3
3
  import { getSession } from '@jbrowse/core/util'
4
4
  import { observer } from 'mobx-react'
5
5
 
6
- import { hasHoverPosition, useStyles } from './util'
7
6
  import { isMsaView } from '../MsaViewPanel/model'
7
+ import { hasHoverPosition, useStyles } from './util'
8
8
 
9
9
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
10
10
 
@@ -2,8 +2,8 @@ import React from 'react'
2
2
 
3
3
  import { getSession } from '@jbrowse/core/util'
4
4
 
5
- import HighlightComponents from './HighlightComponents'
6
5
  import { isMsaView } from '../MsaViewPanel/model'
6
+ import HighlightComponents from './HighlightComponents'
7
7
 
8
8
  import type PluginManager from '@jbrowse/core/PluginManager'
9
9
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'