summaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/configs.ts4
-rw-r--r--src/api/connections.ts2
-rw-r--r--src/api/logs.ts123
-rw-r--r--src/api/memory.ts4
-rw-r--r--src/api/proxies.ts34
-rw-r--r--src/api/rule-provider.ts4
-rw-r--r--src/api/rules.ts43
-rw-r--r--src/api/traffic.ts4
-rw-r--r--src/api/version.ts2
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;