@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.
Files changed (42) hide show
  1. package/LICENSE +201 -0
  2. package/esm/BlatDialog.d.ts +6 -0
  3. package/esm/BlatDialog.js +93 -0
  4. package/esm/IsPcrDialog.d.ts +6 -0
  5. package/esm/IsPcrDialog.js +101 -0
  6. package/esm/MonospaceTextField.d.ts +2 -0
  7. package/esm/MonospaceTextField.js +8 -0
  8. package/esm/UcscQueryActions.d.ts +8 -0
  9. package/esm/UcscQueryActions.js +19 -0
  10. package/esm/UcscQueryFields.d.ts +8 -0
  11. package/esm/UcscQueryFields.js +31 -0
  12. package/esm/UcscQueryStatus.d.ts +5 -0
  13. package/esm/UcscQueryStatus.js +15 -0
  14. package/esm/UcscResultsWidget/UcscResultsWidget.d.ts +5 -0
  15. package/esm/UcscResultsWidget/UcscResultsWidget.js +23 -0
  16. package/esm/UcscResultsWidget/configSchema.d.ts +1 -0
  17. package/esm/UcscResultsWidget/configSchema.js +2 -0
  18. package/esm/UcscResultsWidget/hitSummary.d.ts +9 -0
  19. package/esm/UcscResultsWidget/hitSummary.js +31 -0
  20. package/esm/UcscResultsWidget/index.d.ts +2 -0
  21. package/esm/UcscResultsWidget/index.js +13 -0
  22. package/esm/UcscResultsWidget/stateModel.d.ts +42 -0
  23. package/esm/UcscResultsWidget/stateModel.js +41 -0
  24. package/esm/blatQuery.d.ts +75 -0
  25. package/esm/blatQuery.js +245 -0
  26. package/esm/desktopBlat.d.ts +9 -0
  27. package/esm/desktopBlat.js +22 -0
  28. package/esm/index.d.ts +7 -0
  29. package/esm/index.js +37 -0
  30. package/esm/ispcrQuery.d.ts +59 -0
  31. package/esm/ispcrQuery.js +160 -0
  32. package/esm/ispcrToSam.d.ts +9 -0
  33. package/esm/ispcrToSam.js +86 -0
  34. package/esm/pslToSam.d.ts +54 -0
  35. package/esm/pslToSam.js +153 -0
  36. package/esm/ucscDbMap.d.ts +2 -0
  37. package/esm/ucscDbMap.js +25 -0
  38. package/esm/ucscShared.d.ts +40 -0
  39. package/esm/ucscShared.js +116 -0
  40. package/esm/useUcscQuery.d.ts +40 -0
  41. package/esm/useUcscQuery.js +199 -0
  42. 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
+ }