jbrowse-plugin-msaview 3.4.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.
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +4 -6
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +2 -1
- package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +93 -70
- package/dist/MsaViewPanel/components/LaunchProgress.js +11 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +19 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +10 -12
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +38 -24
- package/dist/MsaViewPanel/model.d.ts +160 -10
- package/dist/MsaViewPanel/model.js +51 -1
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +16 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
- package/dist/MsaViewPanel/runLaunch.js +65 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +129 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +35 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.js +32 -0
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/ebiBlast.d.ts +4 -2
- package/dist/utils/ebiBlast.js +6 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/msa.d.ts +4 -2
- package/dist/utils/msa.js +9 -5
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +2 -1
- package/dist/utils/phmmer.js +4 -2
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -14
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +7 -11
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +2 -2
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +100 -66
- package/src/MsaViewPanel/components/LaunchProgress.tsx +20 -2
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +21 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +14 -10
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +41 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +8 -4
- package/src/MsaViewPanel/model.ts +54 -1
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +22 -13
- package/src/MsaViewPanel/runLaunch.test.ts +154 -0
- package/src/MsaViewPanel/runLaunch.ts +102 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/ebiBlast.ts +8 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/msa.ts +11 -3
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +5 -1
- package/src/utils/poll.ts +8 -1
- package/src/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { textfetch } from './fetch'
|
|
1
|
+
import { isAbortError, textfetch } from './fetch'
|
|
2
2
|
import { pollLoop } from './poll'
|
|
3
3
|
import { readLocalStorage } from './useLocalStorage'
|
|
4
4
|
|
|
@@ -35,13 +35,16 @@ const FAILED_STATUSES = new Set(['ERROR', 'FAILURE', 'NOT_FOUND'])
|
|
|
35
35
|
export async function submitEbiJob({
|
|
36
36
|
tool,
|
|
37
37
|
params,
|
|
38
|
+
signal,
|
|
38
39
|
}: {
|
|
39
40
|
tool: string
|
|
40
41
|
params: Record<string, string>
|
|
42
|
+
signal?: AbortSignal
|
|
41
43
|
}) {
|
|
42
44
|
const jobId = await textfetch(`${EBI_BASE}/${tool}/run`, {
|
|
43
45
|
method: 'POST',
|
|
44
46
|
body: new URLSearchParams({ email: getEbiEmail(), ...params }),
|
|
47
|
+
signal,
|
|
45
48
|
})
|
|
46
49
|
return jobId.trim()
|
|
47
50
|
}
|
|
@@ -64,21 +67,31 @@ export async function waitForEbiJob({
|
|
|
64
67
|
jobId,
|
|
65
68
|
intervalSeconds = 10,
|
|
66
69
|
onCountdown,
|
|
70
|
+
signal,
|
|
67
71
|
}: {
|
|
68
72
|
tool: string
|
|
69
73
|
jobId: string
|
|
70
74
|
intervalSeconds?: number
|
|
71
75
|
onCountdown: (secondsRemaining: number) => void
|
|
76
|
+
signal?: AbortSignal
|
|
72
77
|
}) {
|
|
73
78
|
let consecutiveFailures = 0
|
|
74
79
|
await pollLoop({
|
|
75
80
|
intervalSeconds,
|
|
76
81
|
onCountdown,
|
|
82
|
+
signal,
|
|
77
83
|
check: async () => {
|
|
78
84
|
let status: string
|
|
79
85
|
try {
|
|
80
|
-
status = (
|
|
86
|
+
status = (
|
|
87
|
+
await textfetch(`${EBI_BASE}/${tool}/status/${jobId}`, { signal })
|
|
88
|
+
).trim()
|
|
81
89
|
} catch (e) {
|
|
90
|
+
// a cancelled request is the caller giving up, not EBI being
|
|
91
|
+
// unreachable, so it must end the poll rather than be retried
|
|
92
|
+
if (isAbortError(e)) {
|
|
93
|
+
throw e
|
|
94
|
+
}
|
|
82
95
|
consecutiveFailures += 1
|
|
83
96
|
if (consecutiveFailures >= MAX_CONSECUTIVE_STATUS_FAILURES) {
|
|
84
97
|
throw new Error(
|
|
@@ -111,10 +124,12 @@ export async function fetchEbiResult({
|
|
|
111
124
|
tool,
|
|
112
125
|
jobId,
|
|
113
126
|
type,
|
|
127
|
+
signal,
|
|
114
128
|
}: {
|
|
115
129
|
tool: string
|
|
116
130
|
jobId: string
|
|
117
131
|
type: string
|
|
132
|
+
signal?: AbortSignal
|
|
118
133
|
}) {
|
|
119
|
-
return textfetch(`${EBI_BASE}/${tool}/result/${jobId}/${type}
|
|
134
|
+
return textfetch(`${EBI_BASE}/${tool}/result/${jobId}/${type}`, { signal })
|
|
120
135
|
}
|
package/src/utils/fetch.ts
CHANGED
|
@@ -45,6 +45,33 @@ export async function jsonfetch<T>(url: string, args?: RequestInit) {
|
|
|
45
45
|
return response.json() as Promise<T>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function timeout(time: number) {
|
|
49
|
-
return new Promise(
|
|
48
|
+
export function timeout(time: number, signal?: AbortSignal) {
|
|
49
|
+
return new Promise<void>((resolve, reject) => {
|
|
50
|
+
if (signal?.aborted) {
|
|
51
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
// the listener is removed on the natural path too: one signal serves every
|
|
55
|
+
// tick of a poll, and a 10-minute job is ~600 sleeps -- leaving each
|
|
56
|
+
// listener behind accumulates them all on that one signal
|
|
57
|
+
const onAbort = () => {
|
|
58
|
+
clearTimeout(id)
|
|
59
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
60
|
+
}
|
|
61
|
+
const id = setTimeout(() => {
|
|
62
|
+
signal?.removeEventListener('abort', onAbort)
|
|
63
|
+
resolve()
|
|
64
|
+
}, time)
|
|
65
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A cancelled launch has to be told apart from a failed one: it must not render
|
|
71
|
+
* the "failed" panel, and it must not touch a model that may already be gone.
|
|
72
|
+
* `fetch` and the sleep above both reject with a DOMException named AbortError,
|
|
73
|
+
* which subclasses Error.
|
|
74
|
+
*/
|
|
75
|
+
export function isAbortError(e: unknown) {
|
|
76
|
+
return e instanceof Error && e.name === 'AbortError'
|
|
50
77
|
}
|
package/src/utils/msa.ts
CHANGED
|
@@ -43,9 +43,11 @@ const algorithms: Record<
|
|
|
43
43
|
export async function launchTree({
|
|
44
44
|
alignment,
|
|
45
45
|
onProgress,
|
|
46
|
+
signal,
|
|
46
47
|
}: {
|
|
47
48
|
alignment: string
|
|
48
49
|
onProgress: (arg: string) => void
|
|
50
|
+
signal?: AbortSignal
|
|
49
51
|
}) {
|
|
50
52
|
const tool = 'simple_phylogeny'
|
|
51
53
|
onProgress('Building tree...')
|
|
@@ -58,25 +60,29 @@ export async function launchTree({
|
|
|
58
60
|
clustering: 'Neighbour-joining',
|
|
59
61
|
kimura: 'true',
|
|
60
62
|
},
|
|
63
|
+
signal,
|
|
61
64
|
})
|
|
62
65
|
await waitForEbiJob({
|
|
63
66
|
tool,
|
|
64
67
|
jobId,
|
|
68
|
+
signal,
|
|
65
69
|
onCountdown: s => {
|
|
66
70
|
onProgress(`Re-checking tree status in... ${s}`)
|
|
67
71
|
},
|
|
68
72
|
})
|
|
69
|
-
return fetchEbiResult({ tool, jobId, type: 'tree' })
|
|
73
|
+
return fetchEbiResult({ tool, jobId, type: 'tree', signal })
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
export async function launchMSA({
|
|
73
77
|
algorithm,
|
|
74
78
|
sequence,
|
|
75
79
|
onProgress,
|
|
80
|
+
signal,
|
|
76
81
|
}: {
|
|
77
82
|
algorithm: MsaAlgorithm
|
|
78
83
|
sequence: string
|
|
79
84
|
onProgress: (arg: string) => void
|
|
85
|
+
signal?: AbortSignal
|
|
80
86
|
}) {
|
|
81
87
|
const config = algorithms[algorithm]
|
|
82
88
|
|
|
@@ -85,18 +91,20 @@ export async function launchMSA({
|
|
|
85
91
|
const jobId = await submitEbiJob({
|
|
86
92
|
tool: algorithm,
|
|
87
93
|
params: { ...config.params, sequence },
|
|
94
|
+
signal,
|
|
88
95
|
})
|
|
89
96
|
await waitForEbiJob({
|
|
90
97
|
tool: algorithm,
|
|
91
98
|
jobId,
|
|
99
|
+
signal,
|
|
92
100
|
onCountdown: s => {
|
|
93
101
|
onProgress(`Re-checking MSA status in... ${s}`)
|
|
94
102
|
},
|
|
95
103
|
})
|
|
96
104
|
// one finished job, two result files, neither derived from the other
|
|
97
105
|
const [msa, tree] = await Promise.all([
|
|
98
|
-
fetchEbiResult({ tool: algorithm, jobId, type: config.msaResult }),
|
|
99
|
-
fetchEbiResult({ tool: algorithm, jobId, type: config.treeResult }),
|
|
106
|
+
fetchEbiResult({ tool: algorithm, jobId, type: config.msaResult, signal }),
|
|
107
|
+
fetchEbiResult({ tool: algorithm, jobId, type: config.treeResult, signal }),
|
|
100
108
|
])
|
|
101
109
|
return { msa, tree }
|
|
102
110
|
}
|
package/src/utils/ncbiDomains.ts
CHANGED
|
@@ -97,7 +97,7 @@ function parseFeature(featureXml: string): DomainMatch | undefined {
|
|
|
97
97
|
* domain and site annotations, keyed by both the versioned and primary
|
|
98
98
|
* accession so callers can look up by whichever NCBI returned.
|
|
99
99
|
*/
|
|
100
|
-
export function parseCddDomains(xml: string) {
|
|
100
|
+
export function parseCddDomains(xml: string): Map<string, DomainMatch[]> {
|
|
101
101
|
const byAccession = new Map<string, DomainMatch[]>()
|
|
102
102
|
const seqRe = /<GBSeq>([\s\S]*?)<\/GBSeq>/g
|
|
103
103
|
let seqMatch
|
|
@@ -132,7 +132,9 @@ export function parseCddDomains(xml: string) {
|
|
|
132
132
|
* efetch returns them with no job submission or polling. Results are cached in
|
|
133
133
|
* IndexedDB so reopening a view doesn't refetch.
|
|
134
134
|
*/
|
|
135
|
-
export async function fetchProteinDomains(
|
|
135
|
+
export async function fetchProteinDomains(
|
|
136
|
+
accessions: string[],
|
|
137
|
+
): Promise<Map<string, DomainMatch[]>> {
|
|
136
138
|
const unique = [...new Set(accessions)].filter(Boolean)
|
|
137
139
|
const byAccession = new Map<string, DomainMatch[]>()
|
|
138
140
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
cleanGeneCandidate,
|
|
4
5
|
dedupeLabels,
|
|
5
6
|
fetchOrthologGenes,
|
|
6
7
|
fetchRepresentativeProteins,
|
|
@@ -31,6 +32,30 @@ describe('dedupeLabels', () => {
|
|
|
31
32
|
test('falls back for a name with no usable characters', () => {
|
|
32
33
|
expect(dedupeLabels(['...', '...'])).toEqual(['row', 'row_2'])
|
|
33
34
|
})
|
|
35
|
+
|
|
36
|
+
test('climbs past a suffix a literal name already took', () => {
|
|
37
|
+
// a label pairs a tree leaf to its alignment row, so handing the same one
|
|
38
|
+
// to two rows merges two species into one silently. The third label is ugly
|
|
39
|
+
// and unique, which is the trade
|
|
40
|
+
expect(dedupeLabels(['human', 'human', 'human_2'])).toEqual([
|
|
41
|
+
'human',
|
|
42
|
+
'human_2',
|
|
43
|
+
'human_2_2',
|
|
44
|
+
])
|
|
45
|
+
expect(dedupeLabels(['human_2', 'human', 'human'])).toEqual([
|
|
46
|
+
'human_2',
|
|
47
|
+
'human',
|
|
48
|
+
'human_3',
|
|
49
|
+
])
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('cleanGeneCandidate', () => {
|
|
54
|
+
test('strips what a GFF feature carries that a gene lookup does not want', () => {
|
|
55
|
+
expect(cleanGeneCandidate(' gene:TP53 ')).toBe('TP53')
|
|
56
|
+
expect(cleanGeneCandidate('NM_000546.6')).toBe('NM_000546')
|
|
57
|
+
expect(cleanGeneCandidate('TP53')).toBe('TP53')
|
|
58
|
+
})
|
|
34
59
|
})
|
|
35
60
|
|
|
36
61
|
describe('parseFasta', () => {
|
|
@@ -70,6 +70,19 @@ function ncbiUrl(url: string) {
|
|
|
70
70
|
return `${url}${sep}tool=${NCBI_TOOL}&email=${encodeURIComponent(NCBI_EMAIL)}`
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A candidate gene reference as the ortholog services know it: a GFF ID prefix
|
|
75
|
+
* (gene:TP53) and a version suffix (NM_000546.6) stripped off. Shared with
|
|
76
|
+
* pantherOrthologs, which asks PANTHER the same question with the same
|
|
77
|
+
* candidates.
|
|
78
|
+
*/
|
|
79
|
+
export function cleanGeneCandidate(raw: string) {
|
|
80
|
+
return raw
|
|
81
|
+
.trim()
|
|
82
|
+
.replace(/^\w+:/, '')
|
|
83
|
+
.replace(/\.\d+$/, '')
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
/**
|
|
74
87
|
* A free-text gene reference -> NCBI gene id. A bare number is taken as the id
|
|
75
88
|
* itself; anything else is searched as a gene name within the query taxon.
|
|
@@ -89,8 +102,7 @@ export async function resolveGeneId(
|
|
|
89
102
|
if (/^\d+$/.test(query)) {
|
|
90
103
|
return { geneId: query, matched: query }
|
|
91
104
|
}
|
|
92
|
-
|
|
93
|
-
const cleaned = query.replace(/^\w+:/, '').replace(/\.\d+$/, '')
|
|
105
|
+
const cleaned = cleanGeneCandidate(query)
|
|
94
106
|
const term = `${cleaned}[Gene Name] AND ${taxId}[taxid]`
|
|
95
107
|
const json = await jsonfetch<{
|
|
96
108
|
esearchresult?: { idlist?: string[] }
|
|
@@ -261,15 +273,23 @@ function sanitize(name: string) {
|
|
|
261
273
|
* Sanitized, unique single-token labels used identically in the FASTA headers,
|
|
262
274
|
* the tree leaf names and the domain GFF seq_ids — that identity is how the
|
|
263
275
|
* viewer pairs a tree leaf to its alignment row to its domain track. Collisions
|
|
264
|
-
* get a numeric suffix rather than silently overwriting a row
|
|
276
|
+
* get a numeric suffix rather than silently overwriting a row, and the suffix
|
|
277
|
+
* climbs past labels already emitted: a species genuinely named `human_2` would
|
|
278
|
+
* otherwise take the label the second `human` is about to get, merging two rows
|
|
279
|
+
* into one.
|
|
265
280
|
*/
|
|
266
281
|
export function dedupeLabels(names: string[]) {
|
|
267
|
-
const
|
|
282
|
+
const used = new Set<string>()
|
|
268
283
|
return names.map(name => {
|
|
269
284
|
const base = sanitize(name) || 'row'
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
let label = base
|
|
286
|
+
let n = 1
|
|
287
|
+
while (used.has(label)) {
|
|
288
|
+
n += 1
|
|
289
|
+
label = `${base}_${n}`
|
|
290
|
+
}
|
|
291
|
+
used.add(label)
|
|
292
|
+
return label
|
|
273
293
|
})
|
|
274
294
|
}
|
|
275
295
|
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
// to a RefSeq accession.
|
|
18
18
|
|
|
19
19
|
import { jsonfetch } from './fetch'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
cleanGeneCandidate,
|
|
22
|
+
dedupeLabels,
|
|
23
|
+
defaultMaxSpecies,
|
|
24
|
+
} from './ncbiOrthologs'
|
|
21
25
|
import { fetchTaxonomyInfo } from './taxonomyNames'
|
|
22
26
|
|
|
23
27
|
import type { OrthologRow } from './ncbiOrthologs'
|
|
@@ -212,15 +216,6 @@ async function fetchSequences(accessions: string[]) {
|
|
|
212
216
|
return map
|
|
213
217
|
}
|
|
214
218
|
|
|
215
|
-
// strip a version suffix (NM_000546.6) and any GFF ID prefix (gene:TP53), as
|
|
216
|
-
// resolveGeneId does for NCBI
|
|
217
|
-
function cleanCandidate(raw: string) {
|
|
218
|
-
return raw
|
|
219
|
-
.trim()
|
|
220
|
-
.replace(/^\w+:/, '')
|
|
221
|
-
.replace(/\.\d+$/, '')
|
|
222
|
-
}
|
|
223
|
-
|
|
224
219
|
/**
|
|
225
220
|
* One `matchortho` per candidate until PANTHER maps one. A JBrowse feature
|
|
226
221
|
* carries whatever its GFF/BigBed had — `id()`, `name`, `gene_name` — and only
|
|
@@ -234,7 +229,7 @@ async function matchOrthologs(
|
|
|
234
229
|
) {
|
|
235
230
|
let matched: string | undefined
|
|
236
231
|
for (const raw of candidates) {
|
|
237
|
-
const query =
|
|
232
|
+
const query = cleanGeneCandidate(raw)
|
|
238
233
|
if (!query) {
|
|
239
234
|
continue
|
|
240
235
|
}
|
package/src/utils/phmmer.ts
CHANGED
|
@@ -137,11 +137,13 @@ export async function queryPhmmer({
|
|
|
137
137
|
database,
|
|
138
138
|
onProgress,
|
|
139
139
|
onRid,
|
|
140
|
+
signal,
|
|
140
141
|
}: {
|
|
141
142
|
query: string
|
|
142
143
|
database: PhmmerDatabase
|
|
143
144
|
onProgress: (arg: string) => void
|
|
144
145
|
onRid: (arg: string) => void
|
|
146
|
+
signal?: AbortSignal
|
|
145
147
|
}) {
|
|
146
148
|
onProgress('Submitting to EBI phmmer...')
|
|
147
149
|
const jobId = await submitEbiJob({
|
|
@@ -152,19 +154,21 @@ export async function queryPhmmer({
|
|
|
152
154
|
// the alignment is the whole point of using phmmer here
|
|
153
155
|
alignView: 'true',
|
|
154
156
|
},
|
|
157
|
+
signal,
|
|
155
158
|
})
|
|
156
159
|
onRid(jobId)
|
|
157
160
|
|
|
158
161
|
await waitForEbiJob({
|
|
159
162
|
tool: TOOL,
|
|
160
163
|
jobId,
|
|
164
|
+
signal,
|
|
161
165
|
onCountdown: s => {
|
|
162
166
|
onProgress(`Re-checking phmmer status in... ${s}`)
|
|
163
167
|
},
|
|
164
168
|
})
|
|
165
169
|
|
|
166
170
|
const alignment = parsePhmmerAlignment({
|
|
167
|
-
stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto' }),
|
|
171
|
+
stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto', signal }),
|
|
168
172
|
query,
|
|
169
173
|
})
|
|
170
174
|
if (alignment.rows.length === 0) {
|
package/src/utils/poll.ts
CHANGED
|
@@ -5,24 +5,31 @@ import { timeout } from './fetch'
|
|
|
5
5
|
* false when still pending, and throws on failure. Between checks it counts down
|
|
6
6
|
* `intervalSeconds`, calling `onCountdown` each second so the UI can show
|
|
7
7
|
* progress.
|
|
8
|
+
*
|
|
9
|
+
* `signal` ends the loop with an AbortError: the countdown rejects on the tick
|
|
10
|
+
* it is aborted rather than at the end of the interval, so a cancelled launch
|
|
11
|
+
* stops within a second instead of running for the job's lifetime.
|
|
8
12
|
*/
|
|
9
13
|
export async function pollLoop({
|
|
10
14
|
check,
|
|
11
15
|
intervalSeconds,
|
|
12
16
|
onCountdown,
|
|
17
|
+
signal,
|
|
13
18
|
}: {
|
|
14
19
|
check: () => Promise<boolean>
|
|
15
20
|
intervalSeconds: number
|
|
16
21
|
onCountdown: (secondsRemaining: number) => void
|
|
22
|
+
signal?: AbortSignal
|
|
17
23
|
}) {
|
|
18
24
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
19
25
|
while (true) {
|
|
26
|
+
signal?.throwIfAborted()
|
|
20
27
|
if (await check()) {
|
|
21
28
|
return
|
|
22
29
|
}
|
|
23
30
|
for (let i = intervalSeconds; i > 0; i--) {
|
|
24
31
|
onCountdown(i)
|
|
25
|
-
await timeout(1000)
|
|
32
|
+
await timeout(1000, signal)
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.4.
|
|
1
|
+
export const version = '3.4.1'
|