@jbrowse/plugin-blat 4.3.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.
- package/LICENSE +201 -0
- package/esm/BlatDialog.d.ts +6 -0
- package/esm/BlatDialog.js +93 -0
- package/esm/IsPcrDialog.d.ts +6 -0
- package/esm/IsPcrDialog.js +101 -0
- package/esm/MonospaceTextField.d.ts +2 -0
- package/esm/MonospaceTextField.js +8 -0
- package/esm/UcscQueryActions.d.ts +8 -0
- package/esm/UcscQueryActions.js +19 -0
- package/esm/UcscQueryFields.d.ts +8 -0
- package/esm/UcscQueryFields.js +31 -0
- package/esm/UcscQueryStatus.d.ts +5 -0
- package/esm/UcscQueryStatus.js +15 -0
- package/esm/UcscResultsWidget/UcscResultsWidget.d.ts +5 -0
- package/esm/UcscResultsWidget/UcscResultsWidget.js +23 -0
- package/esm/UcscResultsWidget/configSchema.d.ts +1 -0
- package/esm/UcscResultsWidget/configSchema.js +2 -0
- package/esm/UcscResultsWidget/hitSummary.d.ts +9 -0
- package/esm/UcscResultsWidget/hitSummary.js +31 -0
- package/esm/UcscResultsWidget/index.d.ts +2 -0
- package/esm/UcscResultsWidget/index.js +13 -0
- package/esm/UcscResultsWidget/stateModel.d.ts +42 -0
- package/esm/UcscResultsWidget/stateModel.js +41 -0
- package/esm/blatQuery.d.ts +75 -0
- package/esm/blatQuery.js +245 -0
- package/esm/desktopBlat.d.ts +9 -0
- package/esm/desktopBlat.js +22 -0
- package/esm/index.d.ts +7 -0
- package/esm/index.js +37 -0
- package/esm/ispcrQuery.d.ts +59 -0
- package/esm/ispcrQuery.js +160 -0
- package/esm/ispcrToSam.d.ts +9 -0
- package/esm/ispcrToSam.js +86 -0
- package/esm/pslToSam.d.ts +54 -0
- package/esm/pslToSam.js +153 -0
- package/esm/ucscDbMap.d.ts +2 -0
- package/esm/ucscDbMap.js +25 -0
- package/esm/ucscShared.d.ts +40 -0
- package/esm/ucscShared.js +116 -0
- package/esm/useUcscQuery.d.ts +40 -0
- package/esm/useUcscQuery.js +199 -0
- package/package.json +54 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ResultTrackConf, UcscHost } from './ucscShared.ts';
|
|
2
|
+
import type { SimpleFeatureSerialized } from '@jbrowse/core/util';
|
|
3
|
+
export declare function runUcscFetch<T>({ urlBase, fallbackUrl, body, parse, }: {
|
|
4
|
+
urlBase: string;
|
|
5
|
+
fallbackUrl?: string;
|
|
6
|
+
body: string;
|
|
7
|
+
parse: (text: string) => T;
|
|
8
|
+
}): Promise<T>;
|
|
9
|
+
export declare function useUcscQuery({ session, handleClose, defaultUrl, directUrl, }: {
|
|
10
|
+
session: UcscHost;
|
|
11
|
+
handleClose: () => void;
|
|
12
|
+
defaultUrl: string;
|
|
13
|
+
directUrl: string;
|
|
14
|
+
}): {
|
|
15
|
+
assembly: string;
|
|
16
|
+
db: string;
|
|
17
|
+
urlBase: string;
|
|
18
|
+
fallbackUrl: string | undefined;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
loading: boolean;
|
|
21
|
+
challenged: boolean;
|
|
22
|
+
error: unknown;
|
|
23
|
+
notFound: string;
|
|
24
|
+
setDb: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
25
|
+
setUrlBase: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
26
|
+
changeApiKey: (key: string) => void;
|
|
27
|
+
changeAssembly: (name: string) => void;
|
|
28
|
+
runQuery: ({ fetchResult, trackIdPrefix, trackName, emptyMessage, resultNoun, }: {
|
|
29
|
+
fetchResult: () => Promise<{
|
|
30
|
+
features: SimpleFeatureSerialized[];
|
|
31
|
+
trackConf?: ResultTrackConf;
|
|
32
|
+
}>;
|
|
33
|
+
trackIdPrefix: string;
|
|
34
|
+
trackName: string;
|
|
35
|
+
emptyMessage: string;
|
|
36
|
+
resultNoun?: 'hit' | 'product';
|
|
37
|
+
}) => Promise<void>;
|
|
38
|
+
solveChallenge: (retry: () => void) => Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
export type UcscQuery = ReturnType<typeof useUcscQuery>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { isElectron, localStorageGetItem, localStorageSetItem, } from '@jbrowse/core/util';
|
|
3
|
+
import { BlatChallengeError, challengeError, isChallengePage, } from './blatQuery.js';
|
|
4
|
+
import { desktopBlatFetch, openBlatChallenge } from './desktopBlat.js';
|
|
5
|
+
import { addResultTrack, resolveUcscDb } from './ucscShared.js';
|
|
6
|
+
// the apiKey is a per-user UCSC account credential, not session state, so it's
|
|
7
|
+
// persisted across dialog opens
|
|
8
|
+
const API_KEY_STORAGE = 'ucsc-blat-apiKey';
|
|
9
|
+
// a browser fetch straight to genome.ucsc.edu is CORS-blocked and surfaces as
|
|
10
|
+
// an opaque TypeError; rethrow with the proxy requirement spelled out
|
|
11
|
+
async function browserUcscFetch(urlBase, body) {
|
|
12
|
+
const response = await fetch(urlBase, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
15
|
+
body,
|
|
16
|
+
}).catch((e) => {
|
|
17
|
+
throw new Error(`Could not reach the UCSC server at ${urlBase}. In the browser this must ` +
|
|
18
|
+
`be a CORS-enabled proxy, not genome.ucsc.edu directly (${e}).`);
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
ok: response.ok,
|
|
22
|
+
status: response.status,
|
|
23
|
+
text: await response.text(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// A CORS proxy in front of hgBlat answers a refusal with `{error}` JSON — a 429
|
|
27
|
+
// when the shared UCSC key's rate budget is spent, which is a sentence worth
|
|
28
|
+
// showing the user rather than the envelope it arrived in.
|
|
29
|
+
function serverMessage(text) {
|
|
30
|
+
let message = text;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(text);
|
|
33
|
+
if (parsed !== null &&
|
|
34
|
+
typeof parsed === 'object' &&
|
|
35
|
+
'error' in parsed &&
|
|
36
|
+
typeof parsed.error === 'string') {
|
|
37
|
+
message = parsed.error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// not JSON, so the body itself is the best message available
|
|
42
|
+
}
|
|
43
|
+
return message;
|
|
44
|
+
}
|
|
45
|
+
// A shared-proxy answer the direct UCSC server could still satisfy: its budget
|
|
46
|
+
// is spent (429), it is broken (5xx), or it never answered at all (0, the
|
|
47
|
+
// synthesized status for a throw). A 4xx other than 429 is the query's own
|
|
48
|
+
// fault and would fail identically upstream, so it is not retried.
|
|
49
|
+
function worthRetryingDirect(status) {
|
|
50
|
+
return status === 0 || status === 429 || status >= 500;
|
|
51
|
+
}
|
|
52
|
+
function ucscFetch(url, body) {
|
|
53
|
+
return isElectron
|
|
54
|
+
? desktopBlatFetch({ url, body })
|
|
55
|
+
: browserUcscFetch(url, body);
|
|
56
|
+
}
|
|
57
|
+
// Runs one UCSC query, routing through the desktop main process (to bypass the
|
|
58
|
+
// renderer's CORS restriction and reuse a solved-challenge cookie) or a direct
|
|
59
|
+
// browser fetch that expects a CORS-enabled proxy. The mode-specific pieces are
|
|
60
|
+
// just the POST body and the response parser.
|
|
61
|
+
//
|
|
62
|
+
// `fallbackUrl` is the desktop escape hatch: the dialog defaults to the shared
|
|
63
|
+
// jbrowse.org proxy so BLAT works with no apiKey and no CAPTCHA, but a desktop
|
|
64
|
+
// client can reach UCSC itself, so a proxy that is out of budget or down
|
|
65
|
+
// degrades to the direct path (and its CAPTCHA) instead of failing. A browser
|
|
66
|
+
// has nowhere to fall back to, and an explicitly chosen server is not
|
|
67
|
+
// second-guessed.
|
|
68
|
+
export async function runUcscFetch({ urlBase, fallbackUrl, body, parse, }) {
|
|
69
|
+
// an unreachable proxy throws rather than answering, and that is exactly the
|
|
70
|
+
// case the fallback exists for — but with no fallback the throw carries the
|
|
71
|
+
// CORS explanation, so it is only swallowed when there is somewhere to go
|
|
72
|
+
const first = fallbackUrl
|
|
73
|
+
? await ucscFetch(urlBase, body).catch(() => ({
|
|
74
|
+
ok: false,
|
|
75
|
+
status: 0,
|
|
76
|
+
text: '',
|
|
77
|
+
}))
|
|
78
|
+
: await ucscFetch(urlBase, body);
|
|
79
|
+
const { ok, status, text } = fallbackUrl && !first.ok && worthRetryingDirect(first.status)
|
|
80
|
+
? await ucscFetch(fallbackUrl, body)
|
|
81
|
+
: first;
|
|
82
|
+
// the Cloudflare Turnstile challenge can arrive with a non-2xx status, so
|
|
83
|
+
// probe the body for a challenge before failing generically — otherwise the
|
|
84
|
+
// solve-CAPTCHA affordance never appears. A 2xx challenge is caught by parse().
|
|
85
|
+
if (!ok) {
|
|
86
|
+
if (isChallengePage(text)) {
|
|
87
|
+
throw challengeError();
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`UCSC request failed (${status}): ${serverMessage(text)}`);
|
|
90
|
+
}
|
|
91
|
+
return parse(text);
|
|
92
|
+
}
|
|
93
|
+
// Shared state and lifecycle for the BLAT and in-silico PCR dialogs: the UCSC
|
|
94
|
+
// connection fields (assembly/db/url/apiKey), and the submit → CAPTCHA-challenge
|
|
95
|
+
// → retry flow. Each dialog supplies its own inputs, validation, and query.
|
|
96
|
+
export function useUcscQuery({ session, handleClose, defaultUrl, directUrl, }) {
|
|
97
|
+
const { assemblyNames } = session;
|
|
98
|
+
const [assembly, setAssembly] = useState(assemblyNames[0] ?? '');
|
|
99
|
+
const [db, setDb] = useState(() => resolveUcscDb(session, assemblyNames[0] ?? ''));
|
|
100
|
+
// mirrors changeApiKey for a key saved by an earlier run: the shared proxy
|
|
101
|
+
// overwrites a client key with its own, so a stored key is only worth
|
|
102
|
+
// anything against UCSC, and starting there is what spends the user's own key
|
|
103
|
+
// rather than the shared budget
|
|
104
|
+
const [urlBase, setUrlBase] = useState(() => isElectron && localStorageGetItem(API_KEY_STORAGE) ? directUrl : defaultUrl);
|
|
105
|
+
const [apiKey, setApiKey] = useState(() => localStorageGetItem(API_KEY_STORAGE) ?? '');
|
|
106
|
+
const [loading, setLoading] = useState(false);
|
|
107
|
+
const [challenged, setChallenged] = useState(false);
|
|
108
|
+
const [error, setError] = useState();
|
|
109
|
+
// a query that ran fine and matched nothing is an answer, not a failure, so
|
|
110
|
+
// it stays out of `error` (which renders as a red ErrorMessage)
|
|
111
|
+
const [notFound, setNotFound] = useState('');
|
|
112
|
+
// The shared proxy overwrites whatever apiKey a client sends with its own, so
|
|
113
|
+
// a key typed here only means anything against UCSC directly. On desktop we
|
|
114
|
+
// can go there, so entering a key moves the server field with it — visibly,
|
|
115
|
+
// rather than quietly sending the query somewhere other than the URL shown.
|
|
116
|
+
// A server the user typed themselves is left alone.
|
|
117
|
+
function changeApiKey(key) {
|
|
118
|
+
setApiKey(key);
|
|
119
|
+
localStorageSetItem(API_KEY_STORAGE, key);
|
|
120
|
+
if (isElectron && (urlBase === defaultUrl || urlBase === directUrl)) {
|
|
121
|
+
setUrlBase(key ? directUrl : defaultUrl);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function changeAssembly(name) {
|
|
125
|
+
setAssembly(name);
|
|
126
|
+
setDb(resolveUcscDb(session, name));
|
|
127
|
+
}
|
|
128
|
+
// only the untouched shared-proxy default falls back: an explicitly chosen
|
|
129
|
+
// server, and the browser (which cannot reach UCSC), get one attempt
|
|
130
|
+
const fallbackUrl = isElectron && urlBase === defaultUrl ? directUrl : undefined;
|
|
131
|
+
// `fetchResult` returns the hit list (which the results widget lists) and,
|
|
132
|
+
// optionally, how to display it — a BLAT query returns a SAM alignments track
|
|
133
|
+
// rather than the default feature track.
|
|
134
|
+
async function runQuery({ fetchResult, trackIdPrefix, trackName, emptyMessage, resultNoun, }) {
|
|
135
|
+
setLoading(true);
|
|
136
|
+
setError(undefined);
|
|
137
|
+
setChallenged(false);
|
|
138
|
+
setNotFound('');
|
|
139
|
+
try {
|
|
140
|
+
const { features, trackConf } = await fetchResult();
|
|
141
|
+
if (features.length) {
|
|
142
|
+
await addResultTrack({
|
|
143
|
+
session,
|
|
144
|
+
assembly,
|
|
145
|
+
features,
|
|
146
|
+
trackIdPrefix,
|
|
147
|
+
trackName,
|
|
148
|
+
trackConf,
|
|
149
|
+
resultNoun,
|
|
150
|
+
});
|
|
151
|
+
handleClose();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
setNotFound(emptyMessage);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
console.error(e);
|
|
159
|
+
if (e instanceof BlatChallengeError) {
|
|
160
|
+
setChallenged(true);
|
|
161
|
+
}
|
|
162
|
+
setError(e);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
setLoading(false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// The window has to open on whichever host issued the challenge, because the
|
|
169
|
+
// cf_clearance cookie a solve leaves behind is that host's. When the fallback
|
|
170
|
+
// is armed the proxy is in front, and it answers a challenge of its own with a
|
|
171
|
+
// 502 the fetch retries past — so a challenge that reaches the user at all was
|
|
172
|
+
// served by the fallback, not by `urlBase`.
|
|
173
|
+
async function solveChallenge(retry) {
|
|
174
|
+
const solved = await openBlatChallenge(fallbackUrl ?? urlBase);
|
|
175
|
+
if (solved) {
|
|
176
|
+
retry();
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
session.notify('CAPTCHA window closed before it was solved', 'warning');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
assembly,
|
|
184
|
+
db,
|
|
185
|
+
urlBase,
|
|
186
|
+
fallbackUrl,
|
|
187
|
+
apiKey,
|
|
188
|
+
loading,
|
|
189
|
+
challenged,
|
|
190
|
+
error,
|
|
191
|
+
notFound,
|
|
192
|
+
setDb,
|
|
193
|
+
setUrlBase,
|
|
194
|
+
changeApiKey,
|
|
195
|
+
changeAssembly,
|
|
196
|
+
runQuery,
|
|
197
|
+
solveChallenge,
|
|
198
|
+
};
|
|
199
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jbrowse/plugin-blat",
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "JBrowse 2 UCSC BLAT client",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"jbrowse",
|
|
8
|
+
"jbrowse2"
|
|
9
|
+
],
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"homepage": "https://jbrowse.org",
|
|
12
|
+
"bugs": "https://github.com/GMOD/jbrowse-components/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/GMOD/jbrowse-components.git",
|
|
16
|
+
"directory": "plugins/blat"
|
|
17
|
+
},
|
|
18
|
+
"author": "JBrowse Team",
|
|
19
|
+
"main": "esm/index.js",
|
|
20
|
+
"files": [
|
|
21
|
+
"esm"
|
|
22
|
+
],
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"esbuild": "^0.28.2"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@jbrowse/mobx-state-tree": "^6.5.1",
|
|
28
|
+
"@mui/icons-material": "^9.3.1",
|
|
29
|
+
"@mui/material": "^9.3.1",
|
|
30
|
+
"mobx": "^7.0.3",
|
|
31
|
+
"mobx-react": "^10.0.2",
|
|
32
|
+
"@jbrowse/core": "^4.3.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "pnpm run /^build:/",
|
|
43
|
+
"test": "echo hi",
|
|
44
|
+
"prebuild": "pnpm clean",
|
|
45
|
+
"build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
|
|
46
|
+
"build:umd": "node scripts/build-umd.ts",
|
|
47
|
+
"publish:umd": "scripts/publish-umd.sh",
|
|
48
|
+
"clean": "rimraf esm dist *.tsbuildinfo"
|
|
49
|
+
},
|
|
50
|
+
"types": "esm/index.d.ts",
|
|
51
|
+
"exports": {
|
|
52
|
+
".": "./esm/index.js"
|
|
53
|
+
}
|
|
54
|
+
}
|