diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/configs.ts | 4 | ||||
| -rw-r--r-- | src/api/connections.ts | 2 | ||||
| -rw-r--r-- | src/api/logs.ts | 123 | ||||
| -rw-r--r-- | src/api/memory.ts | 4 | ||||
| -rw-r--r-- | src/api/proxies.ts | 34 | ||||
| -rw-r--r-- | src/api/rule-provider.ts | 4 | ||||
| -rw-r--r-- | src/api/rules.ts | 43 | ||||
| -rw-r--r-- | src/api/traffic.ts | 4 | ||||
| -rw-r--r-- | src/api/version.ts | 2 |
9 files changed, 152 insertions, 68 deletions
diff --git a/src/api/configs.ts b/src/api/configs.ts index 3995aa7..63a88c7 100644 --- a/src/api/configs.ts +++ b/src/api/configs.ts @@ -9,9 +9,9 @@ const upgradeCoreEndpoint = '/upgrade'; const upgradeGeoEndpoint = '/upgrade/geo'; const upgradeUIEndpoint = '/upgrade/ui'; -export async function fetchConfigs(apiConfig: ClashAPIConfig) { +export async function fetchConfigs(apiConfig: ClashAPIConfig, signal?: AbortSignal) { const { url, init } = getURLAndInit(apiConfig); - return await fetch(url + endpoint, init); + return await fetch(url + endpoint, { ...init, signal }); } // TODO support PUT /configs diff --git a/src/api/connections.ts b/src/api/connections.ts index 772ec4a..8e5c1fd 100644 --- a/src/api/connections.ts +++ b/src/api/connections.ts @@ -60,7 +60,7 @@ function appendData(s: string) { } }); } catch (err) { - // eslint-disable-next-line no-console + console.log('JSON.parse error', JSON.parse(s)); } subscribers.forEach((s) => s.listner(o)); diff --git a/src/api/logs.ts b/src/api/logs.ts index fd08d56..53bb8ad 100644 --- a/src/api/logs.ts +++ b/src/api/logs.ts @@ -20,27 +20,38 @@ const getRandomStr = () => { }; let even = false; -let fetched = false; let decoded = ''; let ws: WebSocket; -let prevAppendLogFn: AppendLogFn; +let controller: AbortController; +let usingFetchFallback = false; +let currentConnStr: string; + +type UnsubscribeFn = () => void; +// the WS/fetch stream is a module-level singleton so switching away from and +// back to the Logs page doesn't tear down and re-handshake the connection — +// only unsubscribe here, the connection itself outlives any single mount +const subscribers: AppendLogFn[] = []; + +function broadcast(log: Log) { + subscribers.forEach((listener) => listener(log)); +} -function appendData(s: string, callback: AppendLogFn) { +function appendData(s: string) { let o: Partial<Log>; try { o = JSON.parse(s); } catch (err) { - // eslint-disable-next-line no-console - console.log('JSON.parse error', JSON.parse(s)); + + console.log('JSON.parse error', s); + return; } const now = new Date(); const time = formatDate(now); - // mutate input param in place intentionally o.time = time; o.id = +now - 0 + getRandomStr(); o.even = even = !even; - callback(o as Log); + broadcast(o as Log); } function formatDate(d: Date) { @@ -54,7 +65,7 @@ function formatDate(d: Date) { return `${YY}-${MM}-${dd} ${HH}:${mm}:${ss}`; } -function pump(reader: ReadableStreamDefaultReader, appendLog: AppendLogFn) { +function pump(reader: ReadableStreamDefaultReader) { return reader.read().then(({ done, value }) => { const str = textDecoder.decode(value, { stream: !done }); decoded += str; @@ -64,21 +75,21 @@ function pump(reader: ReadableStreamDefaultReader, appendLog: AppendLogFn) { const lastSplit = splits[splits.length - 1]; for (let i = 0; i < splits.length - 1; i++) { - appendData(splits[i], appendLog); + appendData(splits[i]); } if (done) { - appendData(lastSplit, appendLog); + appendData(lastSplit); decoded = ''; - // eslint-disable-next-line no-console + console.log('GET /logs streaming done'); - fetched = false; + usingFetchFallback = false; return; } else { decoded = lastSplit; } - return pump(reader, appendLog); + return pump(reader); }); } @@ -89,47 +100,75 @@ function makeConnStr(c: LogsAPIConfig) { return keys.map((k) => c[k]).join('|'); } -let prevConnStr: string; -let controller: AbortController; +function isConnectionLive() { + return (ws && ws.readyState === WebSocketReadyState.Open) || usingFetchFallback; +} + +function teardown() { + if (ws) { + ws.close(); + ws = undefined; + } + if (controller) { + controller.abort(); + controller = undefined; + } + usingFetchFallback = false; + decoded = ''; +} -export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) { - if (apiConfig.logLevel === 'uninit') return; - if (fetched || (ws && ws.readyState === WebSocketReadyState.Open)) return; - prevAppendLogFn = appendLog; +function subscribe(listener: AppendLogFn): UnsubscribeFn { + subscribers.push(listener); + return function unsubscribe() { + const idx = subscribers.indexOf(listener); + if (idx !== -1) subscribers.splice(idx, 1); + }; +} + +function openConnection(apiConfig: LogsAPIConfig) { const url = buildLogsWebSocketURL(apiConfig, endpoint); ws = new WebSocket(url); ws.addEventListener('error', () => { - fetchLogsWithFetch(apiConfig, appendLog); + ws = undefined; + fetchLogsWithFetch(apiConfig); }); ws.addEventListener('message', function (event) { - appendData(event.data, appendLog); + appendData(event.data); }); } -export function stop() { - if (ws) { - ws.close(); - fetched = false; +export function fetchLogs( + apiConfig: LogsAPIConfig, + appendLog: AppendLogFn +): UnsubscribeFn | undefined { + if (apiConfig.logLevel === 'uninit') return undefined; + + const connStr = makeConnStr(apiConfig); + if (isConnectionLive() && connStr === currentConnStr) { + return subscribe(appendLog); } - if (controller) controller.abort(); + + teardown(); + currentConnStr = connStr; + openConnection(apiConfig); + return subscribe(appendLog); } -export function reconnect(apiConfig: LogsAPIConfig) { - if (!prevAppendLogFn || !ws) return; - ws.close(); - fetched = false; - fetchLogs(apiConfig, prevAppendLogFn); +/** explicitly stop streaming, e.g. when the user hits "pause" */ +export function stop() { + teardown(); } -function fetchLogsWithFetch(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) { - if (controller && makeConnStr(apiConfig) !== prevConnStr) { - controller.abort(); - } else if (fetched) { - return; - } +/** explicitly force a fresh connection, e.g. after changing log level or hitting "resume" */ +export function reconnect(apiConfig: LogsAPIConfig) { + teardown(); + currentConnStr = makeConnStr(apiConfig); + openConnection(apiConfig); +} - fetched = true; - prevConnStr = makeConnStr(apiConfig); +function fetchLogsWithFetch(apiConfig: LogsAPIConfig) { + if (usingFetchFallback) return; + usingFetchFallback = true; controller = new AbortController(); const signal = controller.signal; @@ -141,13 +180,13 @@ function fetchLogsWithFetch(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) { }).then( (response) => { const reader = response.body.getReader(); - pump(reader, appendLog); + pump(reader); }, (err) => { - fetched = false; + usingFetchFallback = false; if (signal.aborted) return; - // eslint-disable-next-line no-console + console.log('GET /logs error:', err.message); }, ); diff --git a/src/api/memory.ts b/src/api/memory.ts index 3e62dcc..8b0c738 100644 --- a/src/api/memory.ts +++ b/src/api/memory.ts @@ -62,7 +62,7 @@ function pump(reader: ReadableStreamDefaultReader) { parseAndAppend(lastSplit); decoded = ''; - // eslint-disable-next-line no-console + console.log('GET /memory streaming done'); fetched = false; return; @@ -110,7 +110,7 @@ function fetchDataWithFetch(apiConfig: ClashAPIConfig) { } }, (err) => { - // eslint-disable-next-line no-console + console.log('fetch /memory error', err); fetched = false; } diff --git a/src/api/proxies.ts b/src/api/proxies.ts index 9def763..ca4bfd4 100644 --- a/src/api/proxies.ts +++ b/src/api/proxies.ts @@ -1,8 +1,17 @@ +import { DEFAULT_LATENCY_TEST_URL } from '../misc/constants'; import { getURLAndInit } from '../misc/request-helper'; import { ClashAPIConfig } from '../types'; const endpoint = '/proxies'; +// Build the query string for a delay/healthcheck request. `expected` (HTTP status +// like '200/204' or '200-299') is optional and only sent when non-empty. +function buildDelayQuery(latencyTestUrl: string, timeout: number, expected?: string) { + const params = new URLSearchParams({ timeout: String(timeout), url: latencyTestUrl }); + if (expected) params.set('expected', expected); + return params.toString(); +} + /* $ curl "http://127.0.0.1:8080/proxies/Proxy" -XPUT -d '{ "name": "ss3" }' -i HTTP/1.1 400 Bad Request @@ -35,11 +44,12 @@ export async function requestToSwitchProxy(apiConfig, name1, name2) { export async function requestDelayForProxy( apiConfig, name, - latencyTestUrl = 'https://www.gstatic.com/generate_204', - timeout = 5000 + latencyTestUrl = DEFAULT_LATENCY_TEST_URL, + timeout = 5000, + expected?: string ) { const { url, init } = getURLAndInit(apiConfig); - const qs = `timeout=${timeout}&url=${encodeURIComponent(latencyTestUrl)}`; + const qs = buildDelayQuery(latencyTestUrl, timeout, expected); const fullURL = `${url}${endpoint}/${encodeURIComponent(name)}/delay?${qs}`; return await fetch(fullURL, init); } @@ -47,11 +57,12 @@ export async function requestDelayForProxy( export async function requestDelayForProxyGroup( apiConfig, name, - latencyTestUrl = 'https://www.gstatic.com/generate_204', - timeout = 5000 + latencyTestUrl = DEFAULT_LATENCY_TEST_URL, + timeout = 5000, + expected?: string ) { const { url, init } = getURLAndInit(apiConfig); - const qs = `url=${encodeURIComponent(latencyTestUrl)}&timeout=${timeout}`; + const qs = buildDelayQuery(latencyTestUrl, timeout, expected); const fullUrl = `${url}/group/${encodeURIComponent(name)}/delay?${qs}`; return await fetch(fullUrl, init); } @@ -71,9 +82,9 @@ export async function updateProviderByName(config, name) { return await fetch(url + '/providers/proxies/' + encodeURIComponent(name), options); } -export async function healthcheckProviderByName(config, name) { +export async function healthcheckProviderByName(config, name, signal?: AbortSignal) { const { url, init } = getURLAndInit(config); - const options = { ...init, method: 'GET' }; + const options = { ...init, method: 'GET', signal }; return await fetch( url + '/providers/proxies/' + encodeURIComponent(name) + '/healthcheck', options @@ -84,11 +95,12 @@ export async function healthcheckProviderProxy( config: ClashAPIConfig, providerName: string, proxyName: string, - latencyTestUrl = 'https://www.gstatic.com/generate_204', - timeout = 5000 + latencyTestUrl = DEFAULT_LATENCY_TEST_URL, + timeout = 5000, + expected?: string ) { const { url, init } = getURLAndInit(config); - const qs = `timeout=${timeout}&url=${encodeURIComponent(latencyTestUrl)}`; + const qs = buildDelayQuery(latencyTestUrl, timeout, expected); const options = { ...init, method: 'GET' }; return await fetch( `${url}/providers/proxies/${encodeURIComponent(providerName)}/${encodeURIComponent( diff --git a/src/api/rule-provider.ts b/src/api/rule-provider.ts index 14d9917..43497c8 100644 --- a/src/api/rule-provider.ts +++ b/src/api/rule-provider.ts @@ -42,7 +42,7 @@ export async function fetchRuleProviders(endpoint: string, apiConfig: ClashAPICo } } catch (err) { // log and ignore - // eslint-disable-next-line no-console + console.log('failed to GET /providers/rules', err); } return normalizeAPIResponse(data); @@ -64,7 +64,7 @@ export async function refreshRuleProviderByName({ return res.ok; } catch (err) { // log and ignore - // eslint-disable-next-line no-console + console.log('failed to PUT /providers/rules/:name', err); return false; } diff --git a/src/api/rules.ts b/src/api/rules.ts index 97f2eee..4e65e9d 100644 --- a/src/api/rules.ts +++ b/src/api/rules.ts @@ -5,13 +5,23 @@ import { ClashAPIConfig } from '~/types'; // const endpoint = '/rules'; -type RuleItem = RuleAPIItem & { id: number }; +export type RuleExtra = { + disabled: boolean; + hitCount: number; + hitAt: string; + missCount: number; + missAt: string; +}; + +export type RuleItem = RuleAPIItem & { id: number }; -type RuleAPIItem = { +export type RuleAPIItem = { + index?: number; type: string; payload: string; proxy: string; size: number; + extra?: RuleExtra; }; function normalizeAPIResponse(json: { rules: Array<RuleAPIItem> }): Array<RuleItem> { @@ -20,8 +30,11 @@ function normalizeAPIResponse(json: { rules: Array<RuleAPIItem> }): Array<RuleIt 'there is no valid rules list in the rules API response' ); - // attach an id - return json.rules.map((r: RuleAPIItem, i: number) => ({ ...r, id: i })); + // attach an id, preferring the backend-provided index over array position + return json.rules.map((r: RuleAPIItem, i: number) => ({ + ...r, + id: typeof r.index === 'number' ? r.index : i, + })); } export async function fetchRules(endpoint: string, apiConfig: ClashAPIConfig) { @@ -34,8 +47,28 @@ export async function fetchRules(endpoint: string, apiConfig: ClashAPIConfig) { } } catch (err) { // log and ignore - // eslint-disable-next-line no-console + console.log('failed to fetch rules', err); } return normalizeAPIResponse(json); } + +export async function updateRuleDisabledStatus( + apiConfig: ClashAPIConfig, + updates: Record<number, boolean> +) { + const { url, init } = getURLAndInit(apiConfig); + try { + const res = await fetch(url + '/rules/disable', { + method: 'PATCH', + ...init, + body: JSON.stringify(updates), + }); + return res.ok; + } catch (err) { + // log and ignore + + console.log('failed to PATCH /rules/disable', err); + return false; + } +} diff --git a/src/api/traffic.ts b/src/api/traffic.ts index b352248..a01940d 100644 --- a/src/api/traffic.ts +++ b/src/api/traffic.ts @@ -62,7 +62,7 @@ function pump(reader: ReadableStreamDefaultReader) { parseAndAppend(lastSplit); decoded = ''; - // eslint-disable-next-line no-console + console.log('GET /traffic streaming done'); fetched = false; return; @@ -110,7 +110,7 @@ function fetchDataWithFetch(apiConfig: ClashAPIConfig) { } }, (err) => { - // eslint-disable-next-line no-console + console.log('fetch /traffic error', err); fetched = false; } diff --git a/src/api/version.ts b/src/api/version.ts index 6c29125..aab2d22 100644 --- a/src/api/version.ts +++ b/src/api/version.ts @@ -20,7 +20,7 @@ export async function fetchVersion( } } catch (err) { // log and ignore - // eslint-disable-next-line no-console + console.log(`failed to fetch ${endpoint}`, err); } return json; |
